parent
9b17300492
commit
6465ac8d6f
21 changed files with 388 additions and 425 deletions
30
README.md
30
README.md
|
@ -10,22 +10,22 @@ Live: [co.wukko.me](https://co.wukko.me/)
|
|||
## What's cobalt?
|
||||
cobalt is a social media downloader with zero bullshit. It's friendly, efficient, and doesn't bother you with shock ads or privacy invasion "consent" popups.
|
||||
|
||||
It tries to preserve original media quality, and in most instances you get best downloads possible (you can set your preferences in settings).
|
||||
It tries to preserve original media quality, and in most cases you get best quality possible (you can set your preferences in settings).
|
||||
|
||||
## Supported services
|
||||
| Service | Video + Audio | Only audio | Additional features |
|
||||
| -------- | :---: | :---: | :----- |
|
||||
| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. |
|
||||
| Twitter Spaces | ❌️ | ✅ | Audio metadata. |
|
||||
| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. |
|
||||
| YouTube Music | ❌ | ✅ | Audio metadata. |
|
||||
| Reddit | ✅ | ✅ | |
|
||||
| TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermark. |
|
||||
| SoundCloud | ❌ | ✅ | Audio metadata, downloads from private links |
|
||||
| bilibili.com | ✅ | ✅ | |
|
||||
| Tumblr | ✅ | ✅ | |
|
||||
| Vimeo | ✅ | ❌️ | |
|
||||
| VK Videos & Clips | ✅ | ❌️ | |
|
||||
| Service | Video + Audio | Only audio | Additional features |
|
||||
| -------- | :---: | :---: | :----- |
|
||||
| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. |
|
||||
| Twitter Spaces | ❌️ | ✅ | Audio metadata. |
|
||||
| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. |
|
||||
| YouTube Music | ❌ | ✅ | Audio metadata. |
|
||||
| Reddit | ✅ | ✅ | GIFs and videos. |
|
||||
| TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermark. |
|
||||
| SoundCloud | ❌ | ✅ | Audio metadata, downloads from private links. |
|
||||
| bilibili.com | ✅ | ✅ | |
|
||||
| Tumblr | ✅ | ✅ | |
|
||||
| Vimeo | ✅ | ❌️ | |
|
||||
| VK Videos & Clips | ✅ | ❌️ | |
|
||||
|
||||
## cobalt API
|
||||
cobalt has an open API that you can use for free. It's pretty straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself.
|
||||
|
@ -64,7 +64,7 @@ You might find cobalt's source code a bit messy, but I do my best to improve it
|
|||
- node-cache
|
||||
- url-pattern
|
||||
- xml-js
|
||||
- better-ytdl-core
|
||||
- youtubei.js
|
||||
|
||||
Setup script installs all needed `npm` dependencies, but you have to install `Node.js` and `git` yourself.
|
||||
|
||||
|
|
21
docs/API.md
21
docs/API.md
|
@ -7,16 +7,17 @@ Request Body Type: ``application/json``<br>
|
|||
Response Body Type: ``application/json``
|
||||
|
||||
### Request Body Variables
|
||||
| key | type | variables | default | description |
|
||||
|:----------------|:--------|:----------------------------------|:-----------|:----------------------------------------------------------------------|
|
||||
| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. |
|
||||
| vFormat | string | ``mp4 / webm`` | ``mp4`` | Applies only to YouTube downloads. ``mp4`` is recommended for phones. |
|
||||
| vQuality | string | ``los / low / mid / hig / max`` | ``hig`` | ``mid`` quality is recommended for phones. |
|
||||
| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | |
|
||||
| isAudioOnly | boolean | ``true / false`` | ``false`` | |
|
||||
| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. |
|
||||
| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. |
|
||||
| isAudioMuted | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. |
|
||||
| key | type | variables | default | description |
|
||||
|:----------------|:--------|:----------------------------------|:----------|:-------------------------------------------------------------------------------|
|
||||
| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. |
|
||||
| vCodec | string | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. |
|
||||
| vQuality | string | ``los / low / mid / hig / max`` | ``720`` | ``720`` quality is recommended for phones. |
|
||||
| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | |
|
||||
| isAudioOnly | boolean | ``true / false`` | ``false`` | |
|
||||
| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. |
|
||||
| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. |
|
||||
| isAudioMuted | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. |
|
||||
| dubLang | boolean | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. |
|
||||
|
||||
### Response Body Variables
|
||||
| key | type | variables |
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cobalt",
|
||||
"description": "save what you love",
|
||||
"version": "5.0",
|
||||
"version": "5.1",
|
||||
"author": "wukko",
|
||||
"exports": "./src/cobalt.js",
|
||||
"type": "module",
|
||||
|
@ -23,7 +23,6 @@
|
|||
},
|
||||
"homepage": "https://github.com/wukko/cobalt#readme",
|
||||
"dependencies": {
|
||||
"better-ytdl-core": "^1.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"esbuild": "^0.14.51",
|
||||
|
@ -33,6 +32,7 @@
|
|||
"got": "^12.1.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"url-pattern": "1.0.3",
|
||||
"xml-js": "^1.6.11"
|
||||
"xml-js": "^1.6.11",
|
||||
"youtubei.js": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,24 +80,26 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
|||
app.post('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), async (req, res) => {
|
||||
try {
|
||||
let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
|
||||
let lang = languageCode(req);
|
||||
switch (req.params.type) {
|
||||
case 'json':
|
||||
try {
|
||||
let request = req.body;
|
||||
request.dubLang = request.dubLang ? lang : false;
|
||||
let chck = checkJSONPost(request);
|
||||
if (request.url && chck) {
|
||||
chck["ip"] = ip;
|
||||
let j = await getJSON(chck["url"], languageCode(req), chck)
|
||||
let j = await getJSON(chck["url"], lang, chck)
|
||||
res.status(j.status).json(j.body);
|
||||
} else if (request.url && !chck) {
|
||||
let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorCouldntFetch') });
|
||||
let j = apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') });
|
||||
res.status(j.status).json(j.body);
|
||||
} else {
|
||||
let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorNoLink') })
|
||||
let j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') })
|
||||
res.status(j.status).json(j.body);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(lang, 'ErrorCantProcess') })
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -114,12 +116,6 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
|||
try {
|
||||
let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
|
||||
switch (req.params.type) {
|
||||
case 'json':
|
||||
res.status(405).json({
|
||||
'status': 'error',
|
||||
'text': 'GET method for this endpoint has been deprecated. see https://github.com/wukko/cobalt/blob/current/docs/API.md for up-to-date API documentation.'
|
||||
});
|
||||
break;
|
||||
case 'stream':
|
||||
if (req.query.p) {
|
||||
res.status(200).json({ "status": "continue" });
|
||||
|
|
|
@ -28,11 +28,6 @@
|
|||
"boosty": "https://boosty.to/wukko"
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"hig": "1440",
|
||||
"mid": "720",
|
||||
"low": "480"
|
||||
},
|
||||
"celebrations": {
|
||||
"01-01": "🎄",
|
||||
"02-17": "😺",
|
||||
|
@ -64,7 +59,7 @@
|
|||
"webm": ["-c:v", "copy", "-c:a", "copy"],
|
||||
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
|
||||
"copy": ["-c:a", "copy"],
|
||||
"audio": ["-vn", "-ar", "48000", "-ac", "2", "-b:a", "320k"],
|
||||
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
|
||||
"m4a": ["-movflags", "frag_keyframe+empty_moov"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
let ua = navigator.userAgent.toLowerCase();
|
||||
let isIOS = ua.match("iphone os");
|
||||
let isMobile = ua.match("android") || ua.match("iphone os");
|
||||
let version = 23;
|
||||
let version = 24;
|
||||
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
|
||||
let notification = `<div class="notification-dot"></div>`
|
||||
|
||||
|
@ -9,13 +9,14 @@ let store = {}
|
|||
|
||||
let switchers = {
|
||||
"theme": ["auto", "light", "dark"],
|
||||
"vFormat": ["mp4", "webm"],
|
||||
"vQuality": ["hig", "max", "mid", "low"],
|
||||
"aFormat": ["mp3", "best", "ogg", "wav", "opus"]
|
||||
"vCodec": ["h264", "av1", "vp9"],
|
||||
"vQuality": ["1080", "max", "2160", "1440", "720", "480", "360"],
|
||||
"aFormat": ["mp3", "best", "ogg", "wav", "opus"],
|
||||
"dubLang": ["original", "auto"]
|
||||
}
|
||||
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"];
|
||||
let exceptions = { // used for mobile devices
|
||||
"vQuality": "mid"
|
||||
"vQuality": "720"
|
||||
}
|
||||
|
||||
function eid(id) {
|
||||
|
@ -216,17 +217,14 @@ function popup(type, action, text) {
|
|||
eid("popup-backdrop").style.visibility = vis(action);
|
||||
eid(`popup-${type}`).style.visibility = vis(action);
|
||||
}
|
||||
function updateMP4Text() {
|
||||
eid("vFormat-mp4").innerHTML = sGet("vQuality") === "mid" ? "mp4 (h264/av1)" : "mp4 (av1)";
|
||||
}
|
||||
function changeSwitcher(li, b) {
|
||||
if (b) {
|
||||
if (!switchers[li].includes(b)) b = switchers[li][0];
|
||||
sSet(li, b);
|
||||
for (let i in switchers[li]) {
|
||||
(switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`)
|
||||
}
|
||||
if (li === "theme") detectColorScheme();
|
||||
if (li === "vQuality") updateMP4Text();
|
||||
} else {
|
||||
let pref = switchers[li][0];
|
||||
if (isMobile && exceptions[li]) pref = exceptions[li];
|
||||
|
@ -295,7 +293,6 @@ function loadSettings() {
|
|||
for (let i in switchers) {
|
||||
changeSwitcher(i, sGet(i))
|
||||
}
|
||||
updateMP4Text();
|
||||
}
|
||||
function changeButton(type, text) {
|
||||
switch (type) {
|
||||
|
@ -336,16 +333,22 @@ async function download(url) {
|
|||
let req = {
|
||||
url: encodeURIComponent(url.split("&")[0].split('%')[0]),
|
||||
aFormat: sGet("aFormat").slice(0, 4),
|
||||
dubLang: false
|
||||
}
|
||||
if (sGet("dubLang") === "auto") {
|
||||
req.dubLang = true
|
||||
} else if (sGet("dubLang") === "custom") {
|
||||
req.dubLang = true
|
||||
}
|
||||
if (sGet("audioMode") === "true") {
|
||||
req["isAudioOnly"] = true;
|
||||
req["isNoTTWatermark"] = true; // video tiktok no watermark
|
||||
if (sGet("fullTikTokAudio") === "true") req["isTTFullAudio"] = true; // audio tiktok full
|
||||
req.isAudioOnly = true;
|
||||
req.isNoTTWatermark = true; // video tiktok no watermark
|
||||
if (sGet("fullTikTokAudio") === "true") req.isTTFullAudio = true; // audio tiktok full
|
||||
} else {
|
||||
req["vQuality"] = sGet("vQuality").slice(0, 4);
|
||||
if (sGet("muteAudio") === "true") req["isAudioMuted"] = true;
|
||||
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req["vFormat"] = sGet("vFormat").slice(0, 4);
|
||||
if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req["isNoTTWatermark"] = true;
|
||||
req.vQuality = sGet("vQuality").slice(0, 4);
|
||||
if (sGet("muteAudio") === "true") req.isAudioMuted = true;
|
||||
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req.vCodec = sGet("vCodec").slice(0, 4);
|
||||
if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req.isNoTTWatermark = true;
|
||||
}
|
||||
await fetch('/api/json', { method: "POST", body: JSON.stringify(req), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }).then(async (r) => {
|
||||
let j = await r.json();
|
||||
|
|
BIN
src/front/updateBanners/happymeowth.webp
Normal file
BIN
src/front/updateBanners/happymeowth.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
|
@ -29,29 +29,23 @@
|
|||
"ErrorCouldntFetch": "couldn't get any info about your link. check if it's correct and try again.",
|
||||
"ErrorLengthLimit": "current length limit is {s} minutes. video that you tried to download is longer than {s} minutes. pick something else!",
|
||||
"ErrorBadFetch": "an error occurred when i tried to get info about your link. are you sure it works? check if it does, and try again.",
|
||||
"ErrorCorruptedStream": "this download is unfortunately corrupted. try again or try a different format and resolution.",
|
||||
"ErrorCorruptedStream": "this download is unfortunately corrupted. try again!",
|
||||
"ErrorNoInternet": "there's no internet or {appName} api is down. check your connection and try again.",
|
||||
"ErrorCantConnectToServiceAPI": "i couldn't connect to {s} api. seems like either {s} is down or {appName} server ip got blocked. try again later.",
|
||||
"ErrorCantConnectToServiceAPI": "i couldn't connect to the service api. it could be down, or {appName} could've gotten blocked. try again a bit later!",
|
||||
"ErrorEmptyDownload": "i don't see anything i could download from here. try a different link.",
|
||||
"ErrorLiveVideo": "i can't look into future and download a video live of which is ongoing. wait for the stream to finish and try again!",
|
||||
"ErrorLiveVideo": "this is a live video, i am yet to learn how to look into future. wait for the stream to finish and try again!",
|
||||
"SettingsAppearanceSubtitle": "appearance",
|
||||
"SettingsThemeSubtitle": "theme",
|
||||
"SettingsFormatSubtitle": "download format",
|
||||
"SettingsFormatSubtitle": "format",
|
||||
"SettingsQualitySubtitle": "quality",
|
||||
"SettingsThemeAuto": "auto",
|
||||
"SettingsThemeLight": "light",
|
||||
"SettingsThemeDark": "dark",
|
||||
"SettingsQualitySwitchMax": "max",
|
||||
"SettingsQualitySwitchHigh": "high",
|
||||
"SettingsQualitySwitchMedium": "medium",
|
||||
"SettingsQualitySwitchLow": "low",
|
||||
"SettingsQualitySwitchLowest": "lowest",
|
||||
"SettingsKeepDownloadButton": "keep >> visible",
|
||||
"AccessibilityKeepDownloadButton": "keep the download button always visible",
|
||||
"SettingsEnableDownloadPopup": "ask for a way to save",
|
||||
"AccessibilityEnableDownloadPopup": "ask what to do with downloads",
|
||||
"SettingsFormatDescription": "select webm if you want max quality available. webm videos are usually higher bitrate, but ios devices can't play them natively.",
|
||||
"SettingsQualityDescription": "if selected quality isn't available, closest one gets picked instead.\nif you want to post a youtube video on social media, select a combination of mp4 and 720p.",
|
||||
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
|
||||
"LinkGitHubChanges": ">> see previous commits and contribute on github",
|
||||
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. i don't have any ads or trackers, pinky promise.",
|
||||
"DownloadPopupDescriptionIOS": "press and hold the download button, hide the video preview, and then select \"download linked file\" to save.",
|
||||
|
@ -70,7 +64,7 @@
|
|||
"AccessibilityModeToggle": "toggle download mode",
|
||||
"DonateLinksDescription": "donation links open in a new tab. this is the best way to donate if you want me to receive your donation directly.",
|
||||
"SettingsAudioFormatBest": "best",
|
||||
"SettingsAudioFormatDescription": "when best format is selected, you get audio in best quality available, because it's not re-encoded. everything else will be re-encoded.",
|
||||
"SettingsAudioFormatDescription": "when best format is selected, you get audio in best quality available, it's not re-encoded. everything else will be re-encoded.",
|
||||
"Keyphrase": "save what you love",
|
||||
"SettingsRemoveWatermark": "disable watermark",
|
||||
"ErrorPopupCloseButton": "got it",
|
||||
|
@ -105,8 +99,7 @@
|
|||
"DonateVia": "donate via",
|
||||
"DonateHireMe": "or, as an alternative, you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a>.",
|
||||
"SettingsVideoMute": "mute audio",
|
||||
"SettingsVideoMuteExplanation": "disables audio in downloaded video when possible. ignored when audio mode is on or service only supports audio.",
|
||||
"SettingsVideoGeneral": "general",
|
||||
"SettingsVideoMuteExplanation": "disables audio in downloaded video when possible.",
|
||||
"ErrorSoundCloudNoClientId": "couldn't find client_id that is required to fetch audio data from soundcloud. try again, and if issue persists, {ContactLink}.",
|
||||
"CollapseServices": "supported services",
|
||||
"CollapseSupport": "support & source code",
|
||||
|
@ -115,6 +108,14 @@
|
|||
"FollowSupport": "follow {appName} on mastodon or twitter for support, polls, news, and more:",
|
||||
"SupportNote": "please note that questions and issues may take a while to respond to, there's only one person managing everything.",
|
||||
"SourceCode": "report issues, explore source code, star or fork the repo:",
|
||||
"PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, <span class=\"text-backdrop\">salted sha256 hash of your ip address</span> and information about requested stream are temporarily stored in server's RAM for <span class=\"text-backdrop\">2 minutes</span>. after 2 minutes all previously stored information is permanently removed. hash of your ip address is <span class=\"text-backdrop\">used for limiting stream access only to you</span>.\nno one (even me) has access to this data, because official cobalt codebase doesn't provide a way to read it outside of processing functions in the first place.\n\nyou can check {appName}'s <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github repo</a> for yourself and see that indeed nothing is stored permanently."
|
||||
"PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, <span class=\"text-backdrop\">salted sha256 hash of your ip address</span> and information about requested stream are temporarily stored in server's RAM for <span class=\"text-backdrop\">2 minutes</span>. after 2 minutes all previously stored information is permanently removed. hash of your ip address is <span class=\"text-backdrop\">used for limiting stream access only to you</span>.\nno one (even me) has access to this data, because official {appName} codebase doesn't provide a way to read it outside of processing functions in the first place.\n\nyou can check {appName}'s <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github repo</a> for yourself and see that indeed nothing is stored permanently.",
|
||||
"ErrorYTUnavailable": "this youtube video is unavailable or age restricted. i am currently unable to download videos with sensitive content. try another one!",
|
||||
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nnote: youtube api sometimes acts unexpectedly. blame google for this, not me.",
|
||||
"SettingsCodecSubtitle": "youtube codec",
|
||||
"SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: low player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\nif you want best editor/player/social media compatibility, pick h264.",
|
||||
"SettingsAudioDub": "youtube audio track",
|
||||
"SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and {appName}) language is used.",
|
||||
"SettingsDubDefault": "original",
|
||||
"SettingsDubAuto": "auto"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,29 +29,23 @@
|
|||
"ErrorCouldntFetch": "мне не удалось получить инфу о твоей ссылке. проверь её и попробуй ещё раз.",
|
||||
"ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".",
|
||||
"ErrorBadFetch": "произошла ошибка при получении инфы о твоей ссылке. ты уверен, что она работает? проверь её, и попробуй ещё раз.",
|
||||
"ErrorCorruptedStream": "этот файл сломан на стороне {s}. ты можешь попробовать ещё раз, но если не получится, то попробуй другой формат и разрешение.",
|
||||
"ErrorCorruptedStream": "этот файл сломан на стороне сервера. попробуй ещё раз чуть позже!.",
|
||||
"ErrorNoInternet": "кажется, нет подключения к интернету. возможно лежит сервер {appName}. в любом случае, проверь подключение к интернету и попробуй ещё раз.",
|
||||
"ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу {s}. скорее всего {s} лежит, или же ip адрес {appName} добавили в чёрный список. попробуй ещё раз чуть позже.",
|
||||
"ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. скорее всего он лежит, или же ip адрес {appName} добавили в чёрный список. попробуй ещё раз чуть позже!",
|
||||
"ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!",
|
||||
"ErrorLiveVideo": "я не гадалка, и не умею заглядывать в будущее. дождись окончания прямого эфира и попробуй ещё раз чуть позже.",
|
||||
"ErrorLiveVideo": "я не гадалка, и пока что не умею заглядывать в будущее. дождись окончания прямого эфира и попробуй ещё раз!",
|
||||
"SettingsAppearanceSubtitle": "внешний вид",
|
||||
"SettingsThemeSubtitle": "тема",
|
||||
"SettingsFormatSubtitle": "формат загрузок",
|
||||
"SettingsFormatSubtitle": "формат",
|
||||
"SettingsQualitySubtitle": "качество",
|
||||
"SettingsThemeAuto": "авто",
|
||||
"SettingsThemeLight": "светлая",
|
||||
"SettingsThemeDark": "тёмная",
|
||||
"SettingsQualitySwitchMax": "макс",
|
||||
"SettingsQualitySwitchHigh": "высокое",
|
||||
"SettingsQualitySwitchMedium": "среднее",
|
||||
"SettingsQualitySwitchLow": "низкое",
|
||||
"SettingsQualitySwitchLowest": "худшее",
|
||||
"SettingsKeepDownloadButton": "оставлять >> на экране",
|
||||
"AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране",
|
||||
"SettingsEnableDownloadPopup": "спрашивать, что делать при скачивании",
|
||||
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
|
||||
"SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. у webm видео битрейт обычно выше, но устройства на ios не могут проигрывать их без сторонних приложений.",
|
||||
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.\nесли ты хочешь опубликовать видео с youtube где-то в соц. сетях, то выбирай комбинацию из mp4 и 720p.",
|
||||
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
|
||||
"LinkGitHubChanges": ">> смотри предыдущие изменения на github",
|
||||
"NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.",
|
||||
"DownloadPopupDescriptionIOS": "зажми кнопку \"скачать\", затем скрой превью видео и выбери \"загрузить файл по ссылке\" в появившемся окне.",
|
||||
|
@ -70,7 +64,7 @@
|
|||
"AccessibilityModeToggle": "переключить режим скачивания",
|
||||
"DonateLinksDescription": "ссылки на донаты открываются в новой вкладке. это наилучший способ отправить донат, если ты хочешь, чтобы я получил его напрямую.",
|
||||
"SettingsAudioFormatBest": "лучший",
|
||||
"SettingsAudioFormatDescription": "когда выбран \"лучший\" формат, ты получишь аудио лучшего качества, так как оно не будет сконвертировано. если же выбрано что-то другое, то аудио будет немного сжато.",
|
||||
"SettingsAudioFormatDescription": "когда выбран \"лучший\" формат, ты получишь аудио наилучшего качества, так как оно не будет сконвертировано. если же выбрано что-то другое, то аудио будет немного сжато.",
|
||||
"Keyphrase": "сохраняй то, что любишь",
|
||||
"SettingsRemoveWatermark": "убрать ватермарку",
|
||||
"ErrorPopupCloseButton": "ясно",
|
||||
|
@ -105,8 +99,7 @@
|
|||
"DonateVia": "открыть",
|
||||
"DonateHireMe": "или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a>.",
|
||||
"SettingsVideoMute": "отключить аудио",
|
||||
"SettingsVideoMuteExplanation": "убирает аудио при загрузке видео, когда это возможно. игнорируется если включен режим аудио или сервис поддерживает только аудио загрузки.",
|
||||
"SettingsVideoGeneral": "основные",
|
||||
"SettingsVideoMuteExplanation": "убирает аудио при загрузке видео когда это возможно.",
|
||||
"ErrorSoundCloudNoClientId": "мне не удалось достать client_id, который необходим для получения аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.",
|
||||
"CollapseServices": "что поддерживается?",
|
||||
"CollapseSupport": "поддержка и исходный код",
|
||||
|
@ -115,6 +108,14 @@
|
|||
"FollowSupport": "подписывайся на аккаунты {appName} на mastodon или twitter для новостей, поддержки, участия в опросах, и многого другого:",
|
||||
"SupportNote": "помни, что ответ на твой вопрос может занять время, так как только один человек занимается и разработкой и поддержкой.",
|
||||
"SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:",
|
||||
"PrivacyPolicy": "политика конфиденциальности {appName} довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - не моё дело, а только твоё.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы данной функции.\n\nв этом случае, <span class=\"text-backdrop\">sha256 хэш (с солью) твоего ip адреса</span> и данные о запрошенном стриме хранятся в оперативной памяти сервера в течение <span class=\"text-backdrop\">2-х минут</span>. по истечении этого времени всё стирается. хэш твоего ip адреса используется для предоставления доступа к стриму только тебе. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как код {appName} специально не позволяет читать такие данные снаружи.\n\nты можешь посмотреть <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">исходный код {appName}</a> и убедиться, что всё так, как описано."
|
||||
"PrivacyPolicy": "политика конфиденциальности {appName} довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - не моё дело, а только твоё.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы данной функции.\n\nв этом случае, <span class=\"text-backdrop\">sha256 хэш (с солью) твоего ip адреса</span> и данные о запрошенном стриме хранятся в оперативной памяти сервера в течение <span class=\"text-backdrop\">2-х минут</span>. по истечении этого времени всё стирается. хэш твоего ip адреса используется для предоставления доступа к стриму только тебе. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как код {appName} специально не позволяет читать такие данные снаружи.\n\nты можешь посмотреть <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">исходный код {appName}</a> и убедиться, что всё так, как описано.",
|
||||
"ErrorYTUnavailable": "это видео недоступно или же ограничено по возрасту на youtube. пока что я не умею скачивать подобные видео. попробуй другое!",
|
||||
"ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!",
|
||||
"SettingsCodecSubtitle": "кодек для видео с youtube",
|
||||
"SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nесли тебе нужна максимальная совместимость с плеерами/редакторами/соц.сетями, то выбирай h264.",
|
||||
"SettingsAudioDub": "звуковая дорожка для видео с youtube",
|
||||
"SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера (и {appName}).",
|
||||
"SettingsDubDefault": "оригинал",
|
||||
"SettingsDubAuto": "авто"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
{
|
||||
"current": {
|
||||
"version": "5.1",
|
||||
"title": "the evil has been defeated",
|
||||
"banner": "happymeowth.webp",
|
||||
"content": "hey, ever wanted to download a youtube video without a hassle? cobalt is here to help. this update fixes all issues related to youtube downloads.\nnot only that, but it also introduces features never before seen in a downloader, such as youtube dub downloads! read below to see what's up :)\n\n<span class=\"text-backdrop\">tl;dr:</span>\n*; audio in youtube videos FINALLY no longer gets cut off.\n*; you now can pick any video resolution you want (from 360p to 8k) and any possible youtube video codec (h264/av1/vp9).\n*; you now can download youtube videos with dubs in your native language. just check settings > audio.\n*; youtube processing has been vastly sped up.\n\nok, now onto the nerdy part of changelog. this update is pretty huge and includes improvements across the board.\n\nservice improvements:\n*; all youtube functionality has been reworked. cobalt now relies on innertube apis, not web scraping.\n*; random audio cut off issue has been fixed, let me know if it ever occurs again. (closes #62, #66, #75).\n*; added support for youtube dubs. currently it's using your browser's default language when enabled, but i have plans on making a picker. i'll ask people on twitter and mastodon if this feature is needed, and add a picker in next updates.\n*; instead of adding more quality presets, i added granular quality options. pick whatever you like, from 360p up to 4320p (for all services, not just youtube).\n*; replaced a format picker with codec picker for youtube. you can pick h264, av1, or vp9. all of them should work as expected.\n*; youtube audio files are now properly matched to corresponding video files.\n*; it's now always possible to download pristine h264 720p/360p videos from youtube. these videos will work ANYWHERE, so they're default for mobile.\n*; youtube requests are no longer permanently cached, ram usage should drop even further.\n*; youtube video and audio file names now include codec and dub language when applicable.\n*; general performance of entire youtube download process has been greatly improved.\n*; vk module has been reworked to be more compact and not make use of outdated technique of quality picking. should also be way more reliable.\n\ninternal improvements:\n*; cleaned up services config, all constants have been moved directly to modules for quicker access.\n*; matching module has been slightly cleaned up.\n\ninterface improvements:\n*; many descriptions and error messages have been slightly tuned to be less wordy.\n*; unnecessary title duplications in settings have been merged into one.\n*; added more clarity to quality and codec descriptions.\n\nif you use cobalt api, please note that you have to update your creation to support new features.\n\nthis is the second batch of 5.x improvements, there's way more to come. thank you for being here, i really appreciate your support.\n\nif you want to thank me (the developer), there's a nice tab under this changelog that has \"donations\" text on it. anything helps me continue developing and hosting the friendliest media downloader :D"
|
||||
},
|
||||
"history": [{
|
||||
"version": "5.0",
|
||||
"title": "it's all about attention to detail!",
|
||||
"banner": "valentines.webp",
|
||||
"content": "happy valentine's day! i have an update for you, as a gift :D\n\ntl;dr: added support for <span class=\"text-backdrop\">reddit gifs</span>, fixed douyin downloads, fixed vimeo quality picking, revamped entirety of codebase, and many other fixes.\n\nhere's more info:\n\nthis update is mostly about cleaning up and polishing the codebase, but it also has some new features. here's what's up:\n\nservice-related improvements:\n*; you now can download gifs from reddit!\n*; attempting to download a video from douyin no longer throws an error (bytedance changed the api endpoint, yet again).\n*; fixed quality picking for vimeo downloads.\n*; fixed length limit check in vimeo module.\n*; fixed support for \"user view\" vk clips links.\n*; various twitter errors are now displayed correctly instead of falling back to the default error.\n*; state of all services is now tested on each commit.\n\nui improvements:\n*; cobalt social links no longer disappear if you have an aggressive ad blocking extension installed.\n*; various localization improvements for both english and russian.\n*; changed some service aliases to display full list of supported downloads.\n*; added current branch information to version text (in settings).\n*; fixed typos in older changelogs.\n\ninternal improvements:\n*; <span class=\"text-backdrop\">everything</span> has been sanitized, improved, and refactored. code is now much easier to read and maintain.\n*; rewrote and/or optimized all modules that were messy or inefficient.\n*; all git interaction functions now store info in cache instead of fetching it every time the function is called.\n*; added a test script that checks functionality of all supported services.\n*; updated deepsource config. checks are more accurate now.\n*; requests from internet explorer are now dropped entirely instead of redirecting people stuck in 90s to a proper browser download page. this was done to avoid (my) personal bias towards browsers.\n\ni put a ton of effort into this version, and i hope you like it as much as i do.\n\nthank you for using cobalt. there's so much more to come :)"
|
||||
},
|
||||
"history": [{
|
||||
}, {
|
||||
"version": "4.8",
|
||||
"title": "prettier than ever",
|
||||
"banner": "catmakeup.webp",
|
||||
|
@ -24,7 +29,7 @@
|
|||
"version": "4.5",
|
||||
"title": "better, faster, stronger, stable",
|
||||
"banner": "meowthstrong.webp",
|
||||
"content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop italic\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
|
||||
"content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; cobalt now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop italic\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
|
||||
}, {
|
||||
"version": "4.4",
|
||||
"title": "over 1 million monthly requests. thank you.",
|
||||
|
@ -33,12 +38,12 @@
|
|||
}, {
|
||||
"version": "4.3.2",
|
||||
"title": "twitter improvements & changelog overhaul",
|
||||
"content": "- you can download explicit content from twitter.\n- direct video links from twitter are properly supported (video/1, video/2, etc.).\n- changelog history got support for banners.\n- changelog categories are not messy anymore.\n- {appName} version in changelogs is now highlighted.\n- changelog history got separators to make text easier to read.\n- changelog history can be collapsed after loading.\n- download button takes less time to change back to pressable state.\n\nif you're a developer and would like to play around with cobalt's api, then read more about it in older changelogs below!"
|
||||
"content": "- you can download explicit content from twitter.\n- direct video links from twitter are properly supported (video/1, video/2, etc.).\n- changelog history got support for banners.\n- changelog categories are not messy anymore.\n- cobalt version in changelogs is now highlighted.\n- changelog history got separators to make text easier to read.\n- changelog history can be collapsed after loading.\n- download button takes less time to change back to pressable state.\n\nif you're a developer and would like to play around with cobalt's api, then read more about it in older changelogs below!"
|
||||
}, {
|
||||
"version": "4.3",
|
||||
"title": "developers, developers, developers, developers",
|
||||
"banner": "developersdevelopersdevelopers.webp",
|
||||
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop italic\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on {appName} for getting content from social media. the api has been revamped and <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- {appName} web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using {appName}'s api, make sure to mention <a class=\"text-backdrop italic\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
|
||||
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop italic\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on cobalt for getting content from social media. the api has been revamped and <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- cobalt web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using cobalt's api, make sure to mention <a class=\"text-backdrop italic\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
|
||||
}, {
|
||||
"version": "4.2",
|
||||
"title": "optimized quality picking and 8k video support",
|
||||
|
@ -46,15 +51,15 @@
|
|||
}, {
|
||||
"version": "4.1",
|
||||
"title": "better tiktok image downloads",
|
||||
"content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, {appName} will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in {appName} 4.x yet, check out the previous changelog down below. there's a ton of stuff to like."
|
||||
"content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, cobalt will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in cobalt 4.x yet, check out the previous changelog down below. there's a ton of stuff to like."
|
||||
}, {
|
||||
"version": "4.0",
|
||||
"title": "better and faster than ever",
|
||||
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- {appName} now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when {appName} is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, {appName} will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>."
|
||||
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>."
|
||||
}, {
|
||||
"version": "3.7",
|
||||
"title": "support for multi media tweets is here!",
|
||||
"content": "{appName} now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and {appName} will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for {appName}, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- {appName} is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through {appName}'s code."
|
||||
"content": "cobalt now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and cobalt will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for cobalt, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- cobalt is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through cobalt's code."
|
||||
}, {
|
||||
"version": "3.6.2 + 3.6.3",
|
||||
"title": "less disturbance",
|
||||
|
@ -62,7 +67,7 @@
|
|||
}, {
|
||||
"version": "3.6",
|
||||
"title": "improvements all around!",
|
||||
"content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- {appName} is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up."
|
||||
"content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- cobalt is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up."
|
||||
}, {
|
||||
"version": "3.5.4",
|
||||
"title": "tiktok support is back :D",
|
||||
|
@ -70,10 +75,10 @@
|
|||
}, {
|
||||
"version": "3.5.2",
|
||||
"title": "vk clips support, improved changelog system, and less bugs",
|
||||
"content": "new features: \n- added support for vk clips. {appName} now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, {appName} now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- {appName} should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side."
|
||||
"content": "new features: \n- added support for vk clips. cobalt now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, cobalt now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- cobalt should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side."
|
||||
}, {
|
||||
"version": "3.5",
|
||||
"title": "ui revamp and usability improvements",
|
||||
"content": "new features:\n- {appName} now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, {appName} won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in {appName}. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved."
|
||||
"content": "new features:\n- cobalt now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, cobalt won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in cobalt. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved."
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export const
|
|||
genericUserAgent = config.genericUserAgent,
|
||||
repo = packageJson["bugs"]["url"].replace('/issues', ''),
|
||||
authorInfo = config.authorInfo,
|
||||
quality = config.quality,
|
||||
donations = config.donations,
|
||||
ffmpegArgs = config.ffmpegArgs,
|
||||
supportedAudio = config.supportedAudio,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { backdropLink, celebrationsEmoji, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js";
|
||||
import { services as s, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js";
|
||||
import { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
|
||||
import { getCommitInfo } from "../sub/currentCommit.js";
|
||||
import loc from "../../localization/manager.js";
|
||||
import emoji from "../emoji.js";
|
||||
|
@ -196,23 +196,31 @@ export default function(obj) {
|
|||
title: `${emoji("🎬")} ${t('SettingsVideoTab')}`,
|
||||
content: settingsCategory({
|
||||
name: "downloads",
|
||||
title: t('SettingsVideoGeneral'),
|
||||
title: t('SettingsQualitySubtitle'),
|
||||
body: switcher({
|
||||
name: "vQuality",
|
||||
subtitle: t('SettingsQualitySubtitle'),
|
||||
explanation: t('SettingsQualityDescription'),
|
||||
items: [{
|
||||
"action": "max",
|
||||
"text": `${t('SettingsQualitySwitchMax')}<br/>(2160p+)`
|
||||
"text": "4320p+"
|
||||
}, {
|
||||
"action": "hig",
|
||||
"text": `${t('SettingsQualitySwitchHigh')}<br/>(${quality.hig}p)`
|
||||
"action": "2160",
|
||||
"text": "2160p"
|
||||
}, {
|
||||
"action": "mid",
|
||||
"text": `${t('SettingsQualitySwitchMedium')}<br/>(${quality.mid}p)`
|
||||
"action": "1440",
|
||||
"text": "1440p"
|
||||
}, {
|
||||
"action": "low",
|
||||
"text": `${t('SettingsQualitySwitchLow')}<br/>(${quality.low}p)`
|
||||
"action": "1080",
|
||||
"text": "1080p"
|
||||
}, {
|
||||
"action": "720",
|
||||
"text": "720p"
|
||||
}, {
|
||||
"action": "480",
|
||||
"text": "480p"
|
||||
}, {
|
||||
"action": "360",
|
||||
"text": "360p"
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
@ -222,17 +230,19 @@ export default function(obj) {
|
|||
body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'), 3)
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "youtube",
|
||||
name: t('SettingsCodecSubtitle'),
|
||||
body: switcher({
|
||||
name: "vFormat",
|
||||
subtitle: t('SettingsFormatSubtitle'),
|
||||
explanation: t('SettingsFormatDescription'),
|
||||
name: "vCodec",
|
||||
explanation: t('SettingsCodecDescription'),
|
||||
items: [{
|
||||
"action": "mp4",
|
||||
"text": "mp4 (av1)"
|
||||
"action": "h264",
|
||||
"text": "h264 (mp4)"
|
||||
}, {
|
||||
"action": "webm",
|
||||
"text": "webm (vp9)"
|
||||
"action": "av1",
|
||||
"text": "av1 (mp4)"
|
||||
}, {
|
||||
"action": "vp9",
|
||||
"text": "vp9 (webm)"
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
@ -241,18 +251,32 @@ export default function(obj) {
|
|||
title: `${emoji("🎶")} ${t('SettingsAudioTab')}`,
|
||||
content: settingsCategory({
|
||||
name: "general",
|
||||
title: t('SettingsAudioTab'),
|
||||
body: switcher({
|
||||
name: "aFormat",
|
||||
subtitle: t('SettingsFormatSubtitle'),
|
||||
explanation: t('SettingsAudioFormatDescription'),
|
||||
items: audioFormats
|
||||
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation'))
|
||||
}) + settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + `<div class="explanation">${t('SettingsAudioFullTikTokDescription')}</div>`
|
||||
})
|
||||
title: t('SettingsFormatSubtitle'),
|
||||
body:
|
||||
switcher({
|
||||
name: "aFormat",
|
||||
explanation: t('SettingsAudioFormatDescription'),
|
||||
items: audioFormats
|
||||
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation'))
|
||||
}) + settingsCategory({
|
||||
name: "dub",
|
||||
title: t("SettingsAudioDub"),
|
||||
body: switcher({
|
||||
name: "dubLang",
|
||||
explanation: t('SettingsAudioDubDescription'),
|
||||
items: [{
|
||||
"action": "original",
|
||||
"text": t('SettingsDubDefault')
|
||||
}, {
|
||||
"action": "auto",
|
||||
"text": t('SettingsDubAuto')
|
||||
}]
|
||||
})
|
||||
}) + settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + explanation(t('SettingsAudioFullTikTokDescription'))
|
||||
})
|
||||
}, {
|
||||
name: "other",
|
||||
title: `${emoji("🪅")} ${t('SettingsOtherTab')}`,
|
||||
|
|
|
@ -27,8 +27,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
case "twitter":
|
||||
r = await twitter({
|
||||
id: patternMatch["id"] ? patternMatch["id"] : false,
|
||||
spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false,
|
||||
lang: lang
|
||||
spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false
|
||||
});
|
||||
break;
|
||||
case "vk":
|
||||
|
@ -36,34 +35,27 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
url: url,
|
||||
userId: patternMatch["userId"],
|
||||
videoId: patternMatch["videoId"],
|
||||
lang: lang,
|
||||
quality: obj.vQuality
|
||||
});
|
||||
break;
|
||||
case "bilibili":
|
||||
r = await bilibili({
|
||||
id: patternMatch["id"].slice(0, 12),
|
||||
lang: lang
|
||||
id: patternMatch["id"].slice(0, 12)
|
||||
});
|
||||
break;
|
||||
case "youtube":
|
||||
let fetchInfo = {
|
||||
id: patternMatch["id"].slice(0, 11),
|
||||
lang: lang,
|
||||
quality: obj.vQuality,
|
||||
format: "webm"
|
||||
};
|
||||
if (url.match('music.youtube.com') || isAudioOnly === true) obj.vFormat = "audio";
|
||||
switch (obj.vFormat) {
|
||||
case "mp4":
|
||||
fetchInfo["format"] = "mp4";
|
||||
break;
|
||||
case "audio":
|
||||
fetchInfo["format"] = "webm";
|
||||
fetchInfo["isAudioOnly"] = true;
|
||||
fetchInfo["quality"] = "max";
|
||||
isAudioOnly = true;
|
||||
break;
|
||||
format: obj.vCodec,
|
||||
isAudioOnly: isAudioOnly,
|
||||
isAudioMuted: obj.isAudioMuted,
|
||||
dubLang: obj.dubLang
|
||||
}
|
||||
if (url.match('music.youtube.com') || isAudioOnly === true) {
|
||||
fetchInfo.quality = "max";
|
||||
fetchInfo.format = "vp9";
|
||||
fetchInfo.isAudioOnly = true
|
||||
}
|
||||
r = await youtube(fetchInfo);
|
||||
break;
|
||||
|
@ -71,8 +63,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
r = await reddit({
|
||||
sub: patternMatch["sub"],
|
||||
id: patternMatch["id"],
|
||||
title: patternMatch["title"],
|
||||
lang: lang,
|
||||
title: patternMatch["title"]
|
||||
});
|
||||
break;
|
||||
case "douyin":
|
||||
|
@ -81,7 +72,6 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
host: host,
|
||||
postId: patternMatch["postId"],
|
||||
id: patternMatch["id"],
|
||||
lang: lang,
|
||||
noWatermark: obj.isNoTTWatermark,
|
||||
fullAudio: obj.isTTFullAudio,
|
||||
isAudioOnly: isAudioOnly
|
||||
|
@ -91,15 +81,13 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
r = await tumblr({
|
||||
id: patternMatch["id"],
|
||||
url: url,
|
||||
user: patternMatch["user"] ? patternMatch["user"] : false,
|
||||
lang: lang
|
||||
user: patternMatch["user"] ? patternMatch["user"] : false
|
||||
});
|
||||
break;
|
||||
case "vimeo":
|
||||
r = await vimeo({
|
||||
id: patternMatch["id"].slice(0, 11),
|
||||
quality: obj.vQuality,
|
||||
lang: lang
|
||||
quality: obj.vQuality
|
||||
});
|
||||
break;
|
||||
case "soundcloud":
|
||||
|
@ -109,8 +97,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
song: patternMatch["song"], url: url,
|
||||
shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false,
|
||||
accessKey: patternMatch["accessKey"] ? patternMatch["accessKey"] : false,
|
||||
format: obj.aFormat,
|
||||
lang: lang
|
||||
format: obj.aFormat
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { maxVideoDuration, quality, services } from "../../config.js";
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
|
||||
const resolutionMatch = {
|
||||
"3840": "2160",
|
||||
"1920": "1080",
|
||||
"1280": "720",
|
||||
"960": "480"
|
||||
}
|
||||
|
||||
export default async function(obj) {
|
||||
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => { return r.json() }).catch(() => { return false });
|
||||
|
@ -14,7 +21,7 @@ export default async function(obj) {
|
|||
|
||||
try {
|
||||
if (obj.quality !== "max") {
|
||||
let pref = parseInt(quality[obj.quality], 10)
|
||||
let pref = parseInt(obj.quality, 10)
|
||||
for (let i in all) {
|
||||
let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10)
|
||||
if (currQuality === pref) {
|
||||
|
@ -51,9 +58,9 @@ export default async function(obj) {
|
|||
switch (type) {
|
||||
case "parcel":
|
||||
if (obj.quality !== "max") {
|
||||
let pref = parseInt(quality[obj.quality], 10)
|
||||
let pref = parseInt(obj.quality, 10)
|
||||
for (let i in masterJSON_Video) {
|
||||
let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10)
|
||||
let currQuality = parseInt(resolutionMatch[masterJSON_Video[i]["width"]], 10)
|
||||
if (currQuality < pref) {
|
||||
break;
|
||||
} else if (String(currQuality) === String(pref)) {
|
||||
|
|
|
@ -1,45 +1,50 @@
|
|||
import { xml2json } from "xml-js";
|
||||
import { genericUserAgent, maxVideoDuration, services } from "../../config.js";
|
||||
import selectQuality from "../../stream/selectQuality.js";
|
||||
import { genericUserAgent, maxVideoDuration } from "../../config.js";
|
||||
|
||||
export default async function(obj) {
|
||||
const representationMatch = {
|
||||
"2160": 7,
|
||||
"1440": 6,
|
||||
"1080": 5,
|
||||
"720": 4,
|
||||
"480": 3,
|
||||
"360": 2,
|
||||
"240": 1,
|
||||
"144": 0
|
||||
}
|
||||
const resolutionMatch = {
|
||||
"3840": "2160",
|
||||
"2560": "1440",
|
||||
"1920": "1080",
|
||||
"1280": "720",
|
||||
"852": "480",
|
||||
"640": "360",
|
||||
"426": "240",
|
||||
// "256": "144"
|
||||
}
|
||||
|
||||
export default async function(o) {
|
||||
let html;
|
||||
html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, {
|
||||
html = await fetch(`https://vk.com/video-${o.userId}_${o.videoId}`, {
|
||||
headers: { "user-agent": genericUserAgent }
|
||||
}).then((r) => { return r.text() }).catch(() => { return false });
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let quality = o.quality === "max" ? 7 : representationMatch[o.quality];
|
||||
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
|
||||
|
||||
if (Number(js["mvData"]["is_active_live"]) !== 0) return { error: 'ErrorLiveVideo' };
|
||||
if (js["mvData"]["duration"] > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
if (Number(js.mvData.is_active_live) !== 0) return { error: 'ErrorLiveVideo' };
|
||||
if (js.mvData.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 }));
|
||||
let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"];
|
||||
if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"];
|
||||
let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 }));
|
||||
let repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"];
|
||||
let bestQuality = repr[repr.length - 1];
|
||||
let resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height'
|
||||
if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality];
|
||||
|
||||
let selectedQuality,
|
||||
attr = repr[repr.length - 1]["_attributes"],
|
||||
qualities = Object.keys(services.vk.quality_match);
|
||||
for (let i in qualities) {
|
||||
if (qualities[i] === attr["height"]) {
|
||||
selectedQuality = `url${attr["height"]}`;
|
||||
break
|
||||
}
|
||||
if (qualities[i] === attr["width"]) {
|
||||
selectedQuality = `url${attr["width"]}`;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1);
|
||||
let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]);
|
||||
let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"];
|
||||
if (!(selectedQuality in js["player"]["params"][0])) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
return {
|
||||
urls: js["player"]["params"][0][`url${userQuality}`],
|
||||
filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4`
|
||||
}
|
||||
if (bestQuality) return {
|
||||
urls: js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`],
|
||||
filename: `vk_${o.userId}_${o.videoId}_${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4`
|
||||
};
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
|
|
|
@ -1,88 +1,92 @@
|
|||
import ytdl from "better-ytdl-core";
|
||||
import { maxVideoDuration, quality as mq } from "../../config.js";
|
||||
import selectQuality from "../../stream/selectQuality.js";
|
||||
import { Innertube } from 'youtubei.js';
|
||||
import { maxVideoDuration } from '../../config.js';
|
||||
|
||||
export default async function(obj) {
|
||||
let isAudioOnly = !!obj.isAudioOnly,
|
||||
infoInitial = await ytdl.getInfo(obj.id);
|
||||
if (!infoInitial) return { error: 'ErrorCantConnectToServiceAPI' };
|
||||
const yt = await Innertube.create();
|
||||
|
||||
let info = infoInitial.formats;
|
||||
if (info[0]["isLive"]) return { error: 'ErrorLiveVideo' };
|
||||
|
||||
let videoMatch = [], fullVideoMatch = [], video = [],
|
||||
audio = info.filter((a) => {
|
||||
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] === obj.format) return true
|
||||
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
|
||||
if (audio.length === 0) return { error: 'ErrorBadFetch' };
|
||||
if (audio[0]["approxDurationMs"] > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
if (!isAudioOnly) {
|
||||
video = info.filter((a) => {
|
||||
if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] === obj.format) {
|
||||
if (obj.quality !== "max") {
|
||||
if (a["hasAudio"] && String(mq[obj.quality]) === String(a["height"])) {
|
||||
fullVideoMatch.push(a)
|
||||
} else if (!a["hasAudio"] && String(mq[obj.quality]) === String(a["height"])) {
|
||||
videoMatch.push(a)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
|
||||
if (obj.quality !== "max") {
|
||||
if (videoMatch.length === 0) {
|
||||
let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim());
|
||||
videoMatch = video.filter((a) => {
|
||||
if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() === String(ss)) return true
|
||||
})
|
||||
} else if (fullVideoMatch.length > 0) {
|
||||
videoMatch = [fullVideoMatch[0]]
|
||||
}
|
||||
} else videoMatch = [video[0]];
|
||||
if (obj.quality === "los") videoMatch = [video[video.length - 1]];
|
||||
const c = {
|
||||
h264: {
|
||||
codec: "avc1",
|
||||
aCodec: "mp4a",
|
||||
container: "mp4"
|
||||
},
|
||||
av1: {
|
||||
codec: "av01",
|
||||
aCodec: "mp4a",
|
||||
container: "mp4"
|
||||
},
|
||||
vp9: {
|
||||
codec: "vp9",
|
||||
aCodec: "opus",
|
||||
container: "webm"
|
||||
}
|
||||
if (video.length === 0) isAudioOnly = true;
|
||||
}
|
||||
|
||||
if (isAudioOnly) {
|
||||
export default async function(o) {
|
||||
let info, isDubbed, quality = o.quality === "max" ? "9000" : o.quality; //set quality 9000(p) to be interpreted as max
|
||||
try {
|
||||
info = await yt.getBasicInfo(o.id, 'ANDROID');
|
||||
} catch (e) {
|
||||
return { error: 'ErrorCantConnectToServiceAPI' };
|
||||
}
|
||||
|
||||
if (!info) return { error: 'ErrorCantConnectToServiceAPI' };
|
||||
if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' };
|
||||
if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' };
|
||||
|
||||
let adaptive_formats = info.streaming_data.adaptive_formats.filter((e) => {
|
||||
if (e["mime_type"].includes(c[o.format].codec) || e["mime_type"].includes(c[o.format].aCodec)) return true
|
||||
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
let bestQuality = adaptive_formats[0]['quality_label'].split('p')[0];
|
||||
|
||||
let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec));
|
||||
let checkBestAudio = (i) => (i["has_audio"] && !i["has_video"]);
|
||||
let checkBestVideo = (i) => (i['quality_label'].split('p')[0] === bestQuality && !i["has_audio"] && i["has_video"]);
|
||||
let checkRightVideo = (i) => (i['quality_label'].split('p')[0] === quality && !i["has_audio"] && i["has_video"]);
|
||||
|
||||
if (!o.isAudioOnly && !o.isAudioMuted) {
|
||||
let single = info.streaming_data.formats.find(i => checkSingle(i));
|
||||
if (single) return {
|
||||
type: "bridge",
|
||||
urls: single.url,
|
||||
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`
|
||||
}
|
||||
};
|
||||
|
||||
if (info.basic_info.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
let audio = adaptive_formats.find(i => checkBestAudio(i) && i["is_original"]);
|
||||
if (o.dubLang) {
|
||||
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang);
|
||||
if (dubbedAudio) {
|
||||
audio = dubbedAudio;
|
||||
isDubbed = true
|
||||
}
|
||||
}
|
||||
if (o.isAudioOnly) {
|
||||
let r = {
|
||||
type: "render",
|
||||
isAudioOnly: true,
|
||||
urls: audio[0]["url"],
|
||||
audioFilename: `youtube_${obj.id}_audio`,
|
||||
urls: audio.url,
|
||||
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
|
||||
fileMetadata: {
|
||||
title: infoInitial.videoDetails.title,
|
||||
artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(),
|
||||
title: info.basic_info.title,
|
||||
artist: info.basic_info.author.replace("- Topic", "").trim(),
|
||||
}
|
||||
}
|
||||
if (infoInitial.videoDetails.description) {
|
||||
let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by");
|
||||
if (isAutoGenAudio) {
|
||||
let descItems = infoInitial.videoDetails.description.split("\n\n")
|
||||
r.fileMetadata.album = descItems[2]
|
||||
r.fileMetadata.copyright = descItems[3]
|
||||
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) {
|
||||
let descItems = info.basic_info.short_description.split("\n\n")
|
||||
r.fileMetadata.album = descItems[2]
|
||||
r.fileMetadata.copyright = descItems[3]
|
||||
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
|
||||
};
|
||||
return r
|
||||
}
|
||||
let singleTest;
|
||||
if (videoMatch.length > 0) {
|
||||
singleTest = videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"];
|
||||
return {
|
||||
type: singleTest ? "bridge" : "render",
|
||||
urls: singleTest ? videoMatch[0]["url"] : [videoMatch[0]["url"], audio[0]["url"]],
|
||||
time: videoMatch[0]["approxDurationMs"],
|
||||
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
|
||||
}
|
||||
}
|
||||
singleTest = video[0]["hasVideo"] && video[0]["hasAudio"];
|
||||
return {
|
||||
type: singleTest ? "bridge" : "render",
|
||||
urls: singleTest ? video[0]["url"] : [video[0]["url"], audio[0]["url"]],
|
||||
time: video[0]["approxDurationMs"],
|
||||
filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${obj.format}`
|
||||
}
|
||||
};
|
||||
|
||||
let video = adaptive_formats.find(i => ((Number(quality) > Number(bestQuality)) ? checkBestVideo(i) : checkRightVideo(i)));
|
||||
if (video && audio) return {
|
||||
type: "render",
|
||||
urls: [video.url, audio.url],
|
||||
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`
|
||||
};
|
||||
|
||||
return { error: 'ErrorYTTryOtherCodec' }
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"bilibili": {
|
||||
"alias": "bilibili (.com only)",
|
||||
"patterns": ["video/:id"],
|
||||
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
|
||||
"enabled": true
|
||||
},
|
||||
"reddit": {
|
||||
|
@ -20,43 +19,12 @@
|
|||
"vk": {
|
||||
"alias": "vk video & clips",
|
||||
"patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:duplicate?z=clip-:userId_:videoId"],
|
||||
"quality_match": {
|
||||
"2160": 7,
|
||||
"1440": 6,
|
||||
"1080": 5,
|
||||
"720": 3,
|
||||
"480": 2,
|
||||
"360": 1,
|
||||
"240": 0,
|
||||
"144": 4
|
||||
},
|
||||
"representation_match": {
|
||||
"2160": 7,
|
||||
"1440": 6,
|
||||
"1080": 5,
|
||||
"720": 4,
|
||||
"480": 3,
|
||||
"360": 2,
|
||||
"240": 1,
|
||||
"144": 0
|
||||
},
|
||||
"quality": {
|
||||
"1080": "hig",
|
||||
"720": "mid",
|
||||
"480": "low"
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"youtube": {
|
||||
"alias": "youtube videos & shorts & music",
|
||||
"patterns": ["watch?v=:id"],
|
||||
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
|
||||
"bestAudio": "opus",
|
||||
"quality": {
|
||||
"1080": "hig",
|
||||
"720": "mid",
|
||||
"480": "low"
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"tumblr": {
|
||||
|
@ -76,12 +44,6 @@
|
|||
},
|
||||
"vimeo": {
|
||||
"patterns": [":id"],
|
||||
"resolutionMatch": {
|
||||
"3840": "2160",
|
||||
"1920": "1080",
|
||||
"1280": "720",
|
||||
"960": "480"
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"soundcloud": {
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import { services, quality as mq } from "../config.js";
|
||||
|
||||
// TO-DO: remake entirety of this module to be more of how quality picking is done in vimeo module
|
||||
function closest(goal, array) {
|
||||
return array.sort().reduce(function (prev, curr) {
|
||||
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
|
||||
});
|
||||
}
|
||||
|
||||
export default function(service, quality, maxQuality) {
|
||||
if (quality === "max") return maxQuality;
|
||||
|
||||
quality = parseInt(mq[quality], 10)
|
||||
maxQuality = parseInt(maxQuality, 10)
|
||||
|
||||
if (quality >= maxQuality || quality === maxQuality) return maxQuality;
|
||||
|
||||
if (quality < maxQuality) {
|
||||
if (!services[service]["quality"][quality]) {
|
||||
let s = Object.keys(services[service]["quality_match"]).filter((q) => {
|
||||
if (q <= quality) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
return closest(quality, s)
|
||||
}
|
||||
return quality
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ import { metadataManager, msToTime } from "../sub/utils.js";
|
|||
|
||||
export function streamDefault(streamInfo, res) {
|
||||
try {
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]
|
||||
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
|
||||
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`;
|
||||
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`);
|
||||
const stream = got.get(streamInfo.urls, {
|
||||
headers: {
|
||||
|
@ -31,26 +31,39 @@ export function streamLiveRender(streamInfo, res) {
|
|||
res.end();
|
||||
return;
|
||||
}
|
||||
let audio = got.get(streamInfo.urls[1], { isStream: true });
|
||||
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
|
||||
'-loglevel', '-8',
|
||||
'-i', streamInfo.urls[0],
|
||||
'-i', streamInfo.urls[1],
|
||||
'-i', 'pipe:3',
|
||||
'-map', '0:v',
|
||||
'-map', '1:a',
|
||||
];
|
||||
args = args.concat(ffmpegArgs[format])
|
||||
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
|
||||
args.push('-f', format, 'pipe:3');
|
||||
const ffmpegProcess = spawn(ffmpeg, args, {
|
||||
args.push('-f', format, 'pipe:4');
|
||||
let ffmpegProcess = spawn(ffmpeg, args, {
|
||||
windowsHide: true,
|
||||
stdio: [
|
||||
'inherit', 'inherit', 'inherit',
|
||||
'pipe'
|
||||
'pipe', 'pipe'
|
||||
],
|
||||
});
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
|
||||
ffmpegProcess.stdio[3].pipe(res);
|
||||
res.on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.end();
|
||||
});
|
||||
ffmpegProcess.stdio[4].pipe(res).on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.end();
|
||||
});;
|
||||
audio.pipe(ffmpegProcess.stdio[3]).on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.end();
|
||||
});
|
||||
|
||||
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('close', () => ffmpegProcess.kill());
|
||||
|
@ -73,8 +86,8 @@ export function streamAudioOnly(streamInfo, res) {
|
|||
'-i', streamInfo.urls
|
||||
]
|
||||
if (streamInfo.metadata) {
|
||||
if (streamInfo.metadata.cover) { // doesn't work on the server but works locally, no idea why
|
||||
args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0', '-filter:v', 'scale=w=400:h=400,format=yuvj420p')
|
||||
if (streamInfo.metadata.cover) { // currently corrupts the audio
|
||||
args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0')
|
||||
} else {
|
||||
args.push('-vn')
|
||||
}
|
||||
|
@ -82,7 +95,6 @@ export function streamAudioOnly(streamInfo, res) {
|
|||
}
|
||||
let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]
|
||||
args = args.concat(arg)
|
||||
if (streamInfo.metadata.cover) args.push("-c:v", "mjpeg")
|
||||
if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]);
|
||||
args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3');
|
||||
const ffmpegProcess = spawn(ffmpeg, args, {
|
||||
|
|
|
@ -2,11 +2,11 @@ import { createStream } from "../stream/manage.js";
|
|||
|
||||
let apiVar = {
|
||||
allowed: {
|
||||
vFormat: ["mp4", "webm"],
|
||||
vQuality: ["max", "hig", "mid", "low", "los"],
|
||||
vCodec: ["h264", "av1", "vp9"],
|
||||
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
|
||||
aFormat: ["best", "mp3", "ogg", "wav", "opus"]
|
||||
},
|
||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted"]
|
||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang"]
|
||||
}
|
||||
|
||||
export function apiJSON(type, obj) {
|
||||
|
@ -84,8 +84,11 @@ export function cleanURL(url, host) {
|
|||
}
|
||||
return url.slice(0, 128)
|
||||
}
|
||||
export function verifyLanguageCode(code) {
|
||||
return RegExp(/[a-z]{2}/).test(String(code.slice(0, 2).toLowerCase())) ? String(code.slice(0, 2).toLowerCase()) : "en"
|
||||
}
|
||||
export function languageCode(req) {
|
||||
return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en"
|
||||
return req.header('Accept-Language') ? verifyLanguageCode(req.header('Accept-Language')) : "en"
|
||||
}
|
||||
export function unicodeDecode(str) {
|
||||
return str.replace(/\\u[\dA-F]{4}/gi, (unicode) => {
|
||||
|
@ -94,13 +97,14 @@ export function unicodeDecode(str) {
|
|||
}
|
||||
export function checkJSONPost(obj) {
|
||||
let def = {
|
||||
vFormat: "mp4",
|
||||
vQuality: "hig",
|
||||
vCodec: "h264",
|
||||
vQuality: "720",
|
||||
aFormat: "mp3",
|
||||
isAudioOnly: false,
|
||||
isNoTTWatermark: false,
|
||||
isTTFullAudio: false,
|
||||
isAudioMuted: false,
|
||||
dubLang: false
|
||||
}
|
||||
try {
|
||||
let objKeys = Object.keys(obj);
|
||||
|
@ -117,6 +121,8 @@ export function checkJSONPost(obj) {
|
|||
}
|
||||
}
|
||||
|
||||
if (def.dubLang) def.dubLang = verifyLanguageCode(obj.dubLang);
|
||||
|
||||
obj["url"] = decodeURIComponent(String(obj["url"]));
|
||||
let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
|
||||
host = hostname[hostname.length - 2];
|
||||
|
|
|
@ -246,69 +246,55 @@
|
|||
}
|
||||
}],
|
||||
"youtube": [{
|
||||
"name": "4k video (mp4, hig)",
|
||||
"name": "4k video (h264, 1440)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "mp4",
|
||||
"vQuality": "hig",
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": false,
|
||||
"isAudioMuted": false
|
||||
"vCodec": "h264",
|
||||
"vQuality": "1440"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (webm, mid)",
|
||||
"name": "4k video (vp9, 720)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "webm",
|
||||
"vQuality": "mid",
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": false,
|
||||
"isAudioMuted": false
|
||||
"vCodec": "vp9",
|
||||
"vQuality": "720"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (mp4, max)",
|
||||
"name": "4k video (av1, max)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "mp4",
|
||||
"vCodec": "av1",
|
||||
"vQuality": "max"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (h264, 720)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vCodec": "h264",
|
||||
"vQuality": "720"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (vp9, max, isAudioMuted)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vCodec": "vp9",
|
||||
"vQuality": "max",
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": false,
|
||||
"isAudioMuted": false
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (webm, max)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "webm",
|
||||
"vQuality": "max",
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": false,
|
||||
"isAudioMuted": false
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (webm, max, isAudioMuted)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "webm",
|
||||
"vQuality": "max",
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": false,
|
||||
"isAudioMuted": true
|
||||
},
|
||||
"expected": {
|
||||
|
@ -316,10 +302,22 @@
|
|||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (mp4, max, isAudioMuted)",
|
||||
"name": "4k video (h264, max, isAudioMuted)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "webm",
|
||||
"vCodec": "h264",
|
||||
"vQuality": "max",
|
||||
"isAudioMuted": true
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (av1, max, isAudioMuted, isAudioOnly, mp3)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vCodec": "av1",
|
||||
"vQuality": "max",
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": true,
|
||||
|
@ -330,24 +328,10 @@
|
|||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (mp4, max, isAudioMuted, isAudioOnly, mp3)",
|
||||
"name": "4k video (av1, max, isAudioMuted, isAudioOnly, best)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "webm",
|
||||
"vQuality": "max",
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": true,
|
||||
"isAudioMuted": true
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "4k video (mp4, max, isAudioMuted, isAudioOnly, best)",
|
||||
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
|
||||
"params": {
|
||||
"vFormat": "webm",
|
||||
"vCodec": "av1",
|
||||
"vQuality": "max",
|
||||
"aFormat": "best",
|
||||
"isAudioOnly": true,
|
||||
|
@ -407,10 +391,10 @@
|
|||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "clip, low",
|
||||
"name": "clip, 360",
|
||||
"url": "https://vk.com/clip-57274055_456239788",
|
||||
"params": {
|
||||
"vQuality": "low"
|
||||
"vQuality": "360"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
|
@ -657,7 +641,7 @@
|
|||
"name": "4k progressive",
|
||||
"url": "https://vimeo.com/288386543",
|
||||
"params": {
|
||||
"vQuality": "max"
|
||||
"vQuality": "2160"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
|
@ -667,7 +651,7 @@
|
|||
"name": "720p progressive",
|
||||
"url": "https://vimeo.com/288386543",
|
||||
"params": {
|
||||
"vQuality": "mid"
|
||||
"vQuality": "720"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
|
@ -677,7 +661,7 @@
|
|||
"name": "1080p dash parcel",
|
||||
"url": "https://vimeo.com/774694040",
|
||||
"params": {
|
||||
"vQuality": "hig"
|
||||
"vQuality": "1440"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
|
@ -687,7 +671,7 @@
|
|||
"name": "720p dash parcel",
|
||||
"url": "https://vimeo.com/774694040",
|
||||
"params": {
|
||||
"vQuality": "mid"
|
||||
"vQuality": "360"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
|
|
Loading…
Reference in a new issue