4.6.0: video muting and soundcloud client_id

This commit is contained in:
wukko 2022-12-17 17:09:49 +06:00
parent f13a82e152
commit 7f1ba6b36b
26 changed files with 179 additions and 74 deletions

View file

@ -21,6 +21,7 @@ Response Body Type: ``application/json``
| isAudioOnly | boolean | ``true / false`` | ``false`` | | | isAudioOnly | boolean | ``true / false`` | ``false`` | |
| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. | | 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. | | 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. |
### Response Body Variables ### Response Body Variables
| key | type | variables | | key | type | variables |

View file

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "4.5", "version": "4.6.0",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View file

@ -10,7 +10,7 @@ import { appName, genericUserAgent, version, internetExplorerRedirect } from "./
import { getJSON } from "./modules/api.js"; import { getJSON } from "./modules/api.js";
import renderPage from "./modules/pageRender/page.js"; import renderPage from "./modules/pageRender/page.js";
import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js"; import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js";
import { Bright, Cyan } from "./modules/sub/consoleText.js"; import { Bright, Cyan, Green, Red } from "./modules/sub/consoleText.js";
import stream from "./modules/stream/stream.js"; import stream from "./modules/stream/stream.js";
import loc from "./localization/manager.js"; import loc from "./localization/manager.js";
import { buildFront } from "./modules/build.js"; import { buildFront } from "./modules/build.js";
@ -190,8 +190,8 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
res.redirect('/') res.redirect('/')
}); });
app.listen(process.env.port, () => { app.listen(process.env.port, () => {
console.log(`\n${Bright(`${appName} (${version})`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\nCurrent commit: ${Bright(`${commitHash}`)}\nStart time: ${Bright(Math.floor(new Date().getTime()))}\n`) console.log(`\n${Cyan(appName)} ${Bright(`v.${version}-${commitHash}`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\nStart time: ${Bright(Math.floor(new Date().getTime()))}\n`)
}); });
} else { } else {
console.log(`you can't run the server without generating a .env file. please run the setup script first: npm run setup`) console.log(Red(`cobalt hasn't been configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`))
} }

View file

@ -41,6 +41,9 @@
"10-31": "🎃", "10-31": "🎃",
"11-01": "🕯️", "11-01": "🕯️",
"11-02": "🕯️", "11-02": "🕯️",
"12-20": "🎄",
"12-21": "🎄",
"12-22": "🎄",
"12-23": "🎄", "12-23": "🎄",
"12-24": "🎄", "12-24": "🎄",
"12-25": "🎄", "12-25": "🎄",

View file

@ -1,7 +1,7 @@
let ua = navigator.userAgent.toLowerCase(); let ua = navigator.userAgent.toLowerCase();
let isIOS = ua.match("iphone os"); let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || ua.match("iphone os"); let isMobile = ua.match("android") || ua.match("iphone os");
let version = 19; let version = 20;
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); 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>` let notification = `<div class="notification-dot"></div>`
@ -13,7 +13,7 @@ let switchers = {
"vQuality": ["hig", "max", "mid", "low"], "vQuality": ["hig", "max", "mid", "low"],
"aFormat": ["mp3", "best", "ogg", "wav", "opus"] "aFormat": ["mp3", "best", "ogg", "wav", "opus"]
} }
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio"]; let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"];
let exceptions = { // used for mobile devices let exceptions = { // used for mobile devices
"vQuality": "mid" "vQuality": "mid"
} }
@ -339,6 +339,7 @@ async function download(url) {
if (sGet("fullTikTokAudio") === "true") req["isTTFullAudio"] = true; // audio tiktok full if (sGet("fullTikTokAudio") === "true") req["isTTFullAudio"] = true; // audio tiktok full
} else { } else {
req["vQuality"] = sGet("vQuality").slice(0, 4); 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("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; if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req["isNoTTWatermark"] = true;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

View file

@ -1,7 +1,7 @@
{ {
"name": "english", "name": "english",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">contact the maintainer</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">file an issue on github</a>"
}, },
"strings": { "strings": {
"LinkInput": "paste the link here", "LinkInput": "paste the link here",
@ -38,7 +38,6 @@
"SettingsAppearanceSubtitle": "appearance", "SettingsAppearanceSubtitle": "appearance",
"SettingsThemeSubtitle": "theme", "SettingsThemeSubtitle": "theme",
"SettingsFormatSubtitle": "download format", "SettingsFormatSubtitle": "download format",
"SettingsDownloadsSubtitle": "downloads",
"SettingsQualitySubtitle": "quality", "SettingsQualitySubtitle": "quality",
"SettingsThemeAuto": "auto", "SettingsThemeAuto": "auto",
"SettingsThemeLight": "light", "SettingsThemeLight": "light",
@ -108,6 +107,9 @@
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's <span class=\"text-backdrop\">completely free to use</span>. but hey! turns out keeping up a web service used by hundreds of thousands of people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated.", "DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's <span class=\"text-backdrop\">completely free to use</span>. but hey! turns out keeping up a web service used by hundreds of thousands of people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated.",
"DonateVia": "donate via", "DonateVia": "donate via",
"DonateHireMe": "or, as an alternative, you can <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">hire me</a>.", "DonateHireMe": "or, as an alternative, you can <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">hire me</a>.",
"DiscordServer": "join the live conversation about {appName} on the <a class=\"text-backdrop\" href=\"{discord}\" target=\"_blank\">official discord server</a>" "SettingsVideoMute": "mute audio",
"SettingsVideoMuteExplanation": "disables audio in downloaded video when possible. you'll get the source video file if video and audio channels are served in two files by the origin service. ignored when audio mode is on or service only supports audio.",
"SettingsVideoGeneral": "general",
"ErrorSoundCloudNoClientId": "couldn't find client_id that is required to fetch audio data from soundcloud. try again, and if issue persists, {ContactLink}."
} }
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "русский", "name": "русский",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">напиши об этом мейнтейнеру</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">напиши об этом на github</a>"
}, },
"strings": { "strings": {
"LinkInput": "вставь ссылку сюда", "LinkInput": "вставь ссылку сюда",
@ -25,7 +25,7 @@
"ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?", "ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?",
"ErrorBrokenLink": "{s} поддерживается, но с твоей ссылкой что-то не так. может быть, ты её не полностью скопировал?", "ErrorBrokenLink": "{s} поддерживается, но с твоей ссылкой что-то не так. может быть, ты её не полностью скопировал?",
"ErrorNoLink": "я не гадалка и не могу угадывать, что ты хочешь скачать. попробуй в следующий раз вставить ссылку.", "ErrorNoLink": "я не гадалка и не могу угадывать, что ты хочешь скачать. попробуй в следующий раз вставить ссылку.",
"ErrorPageRenderFail": "что-то пошло не так, поэтому у меня не получилось срендерить страницу. если это повторится ещё раз, пожалуйста, {ContactLink}. также приложи хэш текущего коммита ({s}) с действиями для повторения этой ошибки. можно на русском языке. спасибо :)", "ErrorPageRenderFail": "что-то пошло не так и у меня не получилось срендерить страницу. если это повторится ещё раз, пожалуйста, {ContactLink}. также приложи хэш текущего коммита ({s}) с действиями для повторения этой ошибки. можно на русском языке. спасибо :)",
"ErrorRateLimit": "ты делаешь слишком много запросов. успокойся и попробуй ещё раз через несколько минут.", "ErrorRateLimit": "ты делаешь слишком много запросов. успокойся и попробуй ещё раз через несколько минут.",
"ErrorCouldntFetch": "мне не удалось получить инфу о твоей ссылке. проверь её и попробуй ещё раз.", "ErrorCouldntFetch": "мне не удалось получить инфу о твоей ссылке. проверь её и попробуй ещё раз.",
"ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".", "ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".",
@ -38,7 +38,6 @@
"SettingsAppearanceSubtitle": "внешний вид", "SettingsAppearanceSubtitle": "внешний вид",
"SettingsThemeSubtitle": "тема", "SettingsThemeSubtitle": "тема",
"SettingsFormatSubtitle": "формат загрузок", "SettingsFormatSubtitle": "формат загрузок",
"SettingsDownloadsSubtitle": "загрузки",
"SettingsQualitySubtitle": "качество", "SettingsQualitySubtitle": "качество",
"SettingsThemeAuto": "авто", "SettingsThemeAuto": "авто",
"SettingsThemeLight": "светлая", "SettingsThemeLight": "светлая",
@ -54,8 +53,8 @@
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. у webm видео битрейт обычно выше, но устройства на ios не могут проигрывать их без сторонних приложений.", "SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. у webm видео битрейт обычно выше, но устройства на ios не могут проигрывать их без сторонних приложений.",
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.\nесли ты хочешь опубликовать видео с youtube где-то в соц. сетях, то выбирай комбинацию из mp4 и 720p. у таких видео кодек обычно не av1, поэтому они должны работать практически везде.", "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.\nесли ты хочешь опубликовать видео с youtube где-то в соц. сетях, то выбирай комбинацию из mp4 и 720p. у таких видео кодек обычно не av1, поэтому они должны работать практически везде.",
"LinkGitHubIssues": "&gt;&gt; сообщай о проблемах и смотри исходный код на гитхабе", "LinkGitHubIssues": "&gt;&gt; сообщай о проблемах и смотри исходный код на github",
"LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на гитхабе", "LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на github",
"NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.", "NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.",
"DownloadPopupDescriptionIOS": "так как у тебя устройство на ios, тебе нужно зажать кнопку \"скачать\", затем скрыть превью видео и выбрать \"загрузить файл по ссылке\" в появившемся окне.", "DownloadPopupDescriptionIOS": "так как у тебя устройство на ios, тебе нужно зажать кнопку \"скачать\", затем скрыть превью видео и выбрать \"загрузить файл по ссылке\" в появившемся окне.",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода сохранения файла в настройках.", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода сохранения файла в настройках.",
@ -108,6 +107,9 @@
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что хостинг сервиса, которым пользуются сотни тысяч людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!", "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что хостинг сервиса, которым пользуются сотни тысяч людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!",
"DonateVia": "открыть", "DonateVia": "открыть",
"DonateHireMe": "или же ты можешь <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a>.", "DonateHireMe": "или же ты можешь <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a>.",
"DiscordServer": "присоединяйся к живой беседе о {appName} прямо на его <a class=\"text-backdrop\" href=\"{discord}\" target=\"_blank\">официальном discord сервере</a>" "SettingsVideoMute": "отключить аудио",
"SettingsVideoMuteExplanation": "убирает аудио при загрузке видео, когда это возможно. ты получишь исходное видео напрямую от сервиса, если видео и аудио каналы разбиты по файлам. игнорируется если включен режим аудио или сервис поддерживает только аудио загрузки.",
"SettingsVideoGeneral": "основные",
"ErrorSoundCloudNoClientId": "мне не удалось достать client_id, который необходим для получения аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}."
} }
} }

View file

@ -1,11 +1,16 @@
{ {
"current": { "current": {
"version": "4.6",
"title": "mute videos and proper soundcloud support",
"banner": "shutup.png",
"content": "i've been longing to implement both of these things, and here they finally are.\n\nservice-related improvements:\n<div class=\"bullpadding\">&bull; you now can download videos with no audio! simply enable the \"mute audio\" option in settings &gt; audio.\n&bull; soundcloud module has been updated, and downloads should no longer break after some time.</div>\nvisual improvements:\n<div class=\"bullpadding\">&bull; moved some things around in settings popup, and added separators where separation is needed.\n&bull; updated some texts in english and russian.\n&bull; version and commit hash have been joined together, now they're a single unit.</div>\ninternal improvements:\n<div class=\"bullpadding\">&bull; updated api documentation to include isAudioMuted.\n&bull; created render elements for separator and explanation due to high duplication of them in the page.\n&bull; fixed some code quirks.</div>\nhere's how soundcloud downloads got fixed:\n\npreviously, client_id was (stupidly) hardcoded. that means cobalt wasn't able to fetch song data if soundcloud web app got updated.\nnow, cobalt tries to find the up-to-date client_id, caches it in memory, and checks if web app version has changed to update the id accordingly. you can see this change for yourself on github."
},
"history": [{
"version": "4.5", "version": "4.5",
"title": "better, faster, stronger, stable", "title": "better, faster, stronger, stable",
"banner": "meowthstrong.webp", "banner": "meowthstrong.webp",
"content": "your favorite social media downloader just got even better! this update includes a ton of imporvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n<div class=\"bullpadding\">&bull; vimeo module has been revamped, all sorts of videos should now be supported.\n&bull; vimeo audio downloads! you now can download audios from more recent videos.\n&bull; {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n&bull; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n&bull; youtube videos with community warnings should now be possible to download.</div>\nuser interface improvements:\n<div class=\"bullpadding\">&bull; list of supported services is now MUCH easier to read.\n&bull; banners in changelog history should no longer overlap each other.\n&bull; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.</div>\ninternal improvements:\n<div class=\"bullpadding\">&bull; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n&bull; 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&bull; 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\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n&bull; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n&bull; \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n&bull; 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&bull; other code optimizations. there's less clutter overall.</div>\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\" 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 imporvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n<div class=\"bullpadding\">&bull; vimeo module has been revamped, all sorts of videos should now be supported.\n&bull; vimeo audio downloads! you now can download audios from more recent videos.\n&bull; {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n&bull; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n&bull; youtube videos with community warnings should now be possible to download.</div>\nuser interface improvements:\n<div class=\"bullpadding\">&bull; list of supported services is now MUCH easier to read.\n&bull; banners in changelog history should no longer overlap each other.\n&bull; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.</div>\ninternal improvements:\n<div class=\"bullpadding\">&bull; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n&bull; 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&bull; 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\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n&bull; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n&bull; \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n&bull; 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&bull; other code optimizations. there's less clutter overall.</div>\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\" 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."
}, }, {
"history": [{
"version": "4.4", "version": "4.4",
"title": "over 1 million monthly requests. thank you.", "title": "over 1 million monthly requests. thank you.",
"banner": "onemillionr.webp", "banner": "onemillionr.webp",

View file

@ -39,7 +39,15 @@ export function checkbox(action, text, aria, paddingType) {
<span>${text}</span> <span>${text}</span>
</label>` </label>`
} }
export function sep(paddingType) {
let paddingClass = ``
switch(paddingType) {
case 0:
paddingClass += ` top-margin`;
break;
}
return `<div class="separator${paddingClass}"></div>`
}
export function popup(obj) { export function popup(obj) {
let classes = obj.classes ? obj.classes : [] let classes = obj.classes ? obj.classes : []
let body = obj.body; let body = obj.body;
@ -143,7 +151,9 @@ export function footerButtons(obj) {
return ` return `
<div id="footer-buttons">${items}</div>` <div id="footer-buttons">${items}</div>`
} }
export function explanation(text) {
return `<div class="explanation">${text}</div>`
}
export function celebrationsEmoji() { export function celebrationsEmoji() {
let n = new Date().toISOString().split('T')[0].split('-'); let n = new Date().toISOString().split('T')[0].split('-');
let dm = `${n[1]}-${n[2]}`; let dm = `${n[1]}-${n[2]}`;

View file

@ -1,4 +1,4 @@
import { backdropLink, celebrationsEmoji, checkbox, footerButtons, multiPagePopup, popup, popupWithBottomButtons, settingsCategory, switcher } from "./elements.js"; import { backdropLink, celebrationsEmoji, checkbox, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher } from "./elements.js";
import { services as s, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js"; import { services as s, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js"; import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
@ -115,7 +115,7 @@ export default function(obj) {
}, { }, {
text: changelogManager("content") text: changelogManager("content")
}, { }, {
text: `<div class="separator"></div><span class="text-backdrop">${obj.hash}:</span> ${com[0]}`, text: `${sep()}<span class="text-backdrop">${obj.hash}:</span> ${com[0]}`,
classes: ["changelog-subtitle"], classes: ["changelog-subtitle"],
nopadding: true nopadding: true
}, { }, {
@ -153,13 +153,13 @@ export default function(obj) {
text: loc(obj.lang, 'DonateLinksDescription'), text: loc(obj.lang, 'DonateLinksDescription'),
classes: ["explanation"] classes: ["explanation"]
}, { }, {
text: `<div class="separator"></div>`, text: sep(),
raw: true raw: true
}, { }, {
text: donate.replace(/REPLACEME/g, loc(obj.lang, 'ClickToCopy')), text: donate.replace(/REPLACEME/g, loc(obj.lang, 'ClickToCopy')),
classes: ["desc-padding"] classes: ["desc-padding"]
}, { }, {
text: `<div class="separator"></div>`, text: sep(),
raw: true raw: true
}, { }, {
text: loc(obj.lang, 'DonateHireMe', authorInfo.link), text: loc(obj.lang, 'DonateHireMe', authorInfo.link),
@ -173,7 +173,7 @@ export default function(obj) {
closeAria: loc(obj.lang, 'AccessibilityClosePopup'), closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
header: { header: {
aboveTitle: { aboveTitle: {
text: `v.${version} ~ ${obj.hash}`, text: `v.${version}-${obj.hash}`,
url: `${repo}/commit/${obj.hash}` url: `${repo}/commit/${obj.hash}`
}, },
title: `${emoji("⚙️", 30)} ${loc(obj.lang, 'TitlePopupSettings')}` title: `${emoji("⚙️", 30)} ${loc(obj.lang, 'TitlePopupSettings')}`
@ -183,7 +183,7 @@ export default function(obj) {
title: `${emoji("🎬")} ${loc(obj.lang, 'SettingsVideoTab')}`, title: `${emoji("🎬")} ${loc(obj.lang, 'SettingsVideoTab')}`,
content: settingsCategory({ content: settingsCategory({
name: "downloads", name: "downloads",
title: loc(obj.lang, 'SettingsDownloadsSubtitle'), title: loc(obj.lang, 'SettingsVideoGeneral'),
body: switcher({ body: switcher({
name: "vQuality", name: "vQuality",
subtitle: loc(obj.lang, 'SettingsQualitySubtitle'), subtitle: loc(obj.lang, 'SettingsQualitySubtitle'),
@ -202,8 +202,7 @@ export default function(obj) {
"text": `${loc(obj.lang, 'SettingsQualitySwitchLow')}<br/>(${quality.low}p)` "text": `${loc(obj.lang, 'SettingsQualitySwitchLow')}<br/>(${quality.low}p)`
}] }]
}) })
}) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup'), 1) : ''}` }) + settingsCategory({
+ settingsCategory({
name: "youtube", name: "youtube",
body: switcher({ body: switcher({
name: "vFormat", name: "vFormat",
@ -234,7 +233,7 @@ export default function(obj) {
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'), subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
explanation: loc(obj.lang, 'SettingsAudioFormatDescription'), explanation: loc(obj.lang, 'SettingsAudioFormatDescription'),
items: audioFormats items: audioFormats
}) }) + sep(0) + checkbox("muteAudio", loc(obj.lang, 'SettingsVideoMute'), loc(obj.lang, 'SettingsVideoMute'), 3) + explanation(loc(obj.lang, 'SettingsVideoMuteExplanation'))
}) + settingsCategory({ }) + settingsCategory({
name: "tiktok", name: "tiktok",
title: "tiktok & douyin", title: "tiktok & douyin",
@ -263,7 +262,7 @@ export default function(obj) {
}) + settingsCategory({ }) + settingsCategory({
name: "miscellaneous", name: "miscellaneous",
title: loc(obj.lang, 'Miscellaneous'), title: loc(obj.lang, 'Miscellaneous'),
body: checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableNotifications')) body: checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup'), 1) : ''}`
}) })
}], }],
})} })}

View file

@ -107,7 +107,7 @@ export default async function (host, patternMatch, url, lang, obj) {
default: default:
return apiJSON(0, { t: errorUnsupported(lang) }); return apiJSON(0, { t: errorUnsupported(lang) });
} }
return !r.error ? matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly, lang) : apiJSON(0, { return !r.error ? matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly, lang, obj.isAudioMuted) : apiJSON(0, {
t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error)
}); });
} catch (e) { } catch (e) {

View file

@ -2,8 +2,8 @@ import { audioIgnore, services, supportedAudio } from "../config.js"
import { apiJSON } from "../sub/utils.js" import { apiJSON } from "../sub/utils.js"
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
export default function(r, host, ip, audioFormat, isAudioOnly, lang) { export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMuted) {
if (!isAudioOnly && !r.picker) { if (!isAudioOnly && !r.picker && !isAudioMuted) {
switch (host) { switch (host) {
case "twitter": case "twitter":
return apiJSON(1, { u: r.urls }); return apiJSON(1, { u: r.urls });
@ -42,7 +42,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang) {
case "tumblr": case "tumblr":
return apiJSON(1, { u: r.urls }); return apiJSON(1, { u: r.urls });
case "vimeo": case "vimeo":
if (r.filename) { if (Array.isArray(r.urls)) {
return apiJSON(2, { return apiJSON(2, {
type: "render", u: r.urls, service: host, ip: ip, type: "render", u: r.urls, service: host, ip: ip,
filename: r.filename filename: r.filename
@ -51,6 +51,16 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang) {
return apiJSON(1, { u: r.urls }); return apiJSON(1, { u: r.urls });
} }
} }
} else if (isAudioMuted) {
let isSplit = Array.isArray(r.urls);
return apiJSON(2, {
type: isSplit ? "bridge" : "mute",
u: isSplit ? r.urls[0] : r.urls,
service: host,
ip: ip,
filename: r.filename,
mute: true,
});
} else if (r.picker) { } else if (r.picker) {
switch (host) { switch (host) {
case "douyin": case "douyin":

View file

@ -86,7 +86,6 @@
"soundcloud": { "soundcloud": {
"patterns": [":author/:song", ":shortLink"], "patterns": [":author/:song", ":shortLink"],
"bestAudio": "none", "bestAudio": "none",
"clientid": "YeTcsotswIIc4sse5WZsXszVxMtP6eLc",
"enabled": true "enabled": true
} }
} }

View file

@ -4,7 +4,7 @@ export default async function(obj) {
try { try {
let html = await fetch(`https://bilibili.com/video/${obj.id}`, { let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false}); }).then(async (r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) { if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) {

View file

@ -2,7 +2,7 @@ import { maxVideoDuration } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then(async (r) => {return await r.json()}).catch(() => {return false}); let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then(async (r) => {return r.json()}).catch(() => {return false});
if (!data) return { error: 'ErrorCouldntFetch' }; if (!data) return { error: 'ErrorCouldntFetch' };
data = data[0]["data"]["children"][0]["data"]; data = data[0]["data"]["children"][0]["data"];

View file

@ -1,4 +1,39 @@
import { genericUserAgent, maxAudioDuration, services } from "../config.js"; import { genericUserAgent, maxAudioDuration } from "../config.js";
let cachedID = {}
async function findClientID() {
try {
let sc = await fetch('https://soundcloud.com/').then(async (r) => {return r.text()}).catch(() => {return false});
let sc_version = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/));
if (cachedID.version == sc_version) {
return cachedID.id
} else {
let scripts = sc.matchAll(/<script.+src="(.+)">/g);
let clientid;
for (let script of scripts) {
let url = script[1];
if (url && !url.startsWith('https://a-v2.sndcdn.com')) return;
let scrf = await fetch(url).then(async (r) => {return r.text()}).catch(() => {return false});
let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/);
if (id && typeof id[0] === 'string') {
clientid = id[0].match(/[A-Za-z0-9]{32}/)[0];
break;
}
}
cachedID.version = sc_version;
cachedID.id = clientid;
return clientid;
}
} catch (e) {
return false;
}
}
export default async function(obj) { export default async function(obj) {
try { try {
@ -6,18 +41,20 @@ export default async function(obj) {
if (!obj.author && !obj.song && obj.shortLink) { if (!obj.author && !obj.song && obj.shortLink) {
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false}); }).then(async (r) => {return r.text()}).catch(() => {return false});
} }
if (obj.author && obj.song) { if (obj.author && obj.song) {
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, { html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false}); }).then(async (r) => {return r.text()}).catch(() => {return false});
} }
if (!html) return { error: 'ErrorCouldntFetch'}; if (!html) return { error: 'ErrorCouldntFetch'};
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) { if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) {
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0]) let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (json["media"]["transcodings"]) { if (json["media"]["transcodings"]) {
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${services["soundcloud"]["clientid"]}&track_authorization=${json.track_authorization}`; let clientId = await findClientID();
if (clientId) {
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${clientId}&track_authorization=${json.track_authorization}`;
if (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") { if (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
if (json.duration < maxAudioDuration) { if (json.duration < maxAudioDuration) {
let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false}); let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false});
@ -32,6 +69,7 @@ export default async function(obj) {
} }
} else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] } } else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }
} }
} else return { error: 'ErrorSoundCloudNoClientId' }
} else return { error: 'ErrorEmptyDownload' } } else return { error: 'ErrorEmptyDownload' }
} else return { error: ['ErrorBrokenLink', 'soundcloud'] } } else return { error: ['ErrorBrokenLink', 'soundcloud'] }
} catch (e) { } catch (e) {

View file

@ -32,7 +32,7 @@ export default async function(obj) {
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, { let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
redirect: "manual", redirect: "manual",
headers: { "user-agent": userAgent } headers: { "user-agent": userAgent }
}).then(async (r) => {return await r.text()}).catch(() => {return false}); }).then(async (r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) { if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) {
@ -46,7 +46,7 @@ export default async function(obj) {
let detail; let detail;
detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), { detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"} headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
}).then(async (r) => {return await r.json()}).catch(() => {return false}); }).then(async (r) => {return r.json()}).catch(() => {return false});
detail = selector(detail, obj.host, obj.postId); detail = selector(detail, obj.host, obj.postId);

View file

@ -5,7 +5,7 @@ export default async function(obj) {
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', ''); let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, { let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false}); }).then(async (r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) { if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"/>')[0]}`, audioFilename: `tumblr_${obj.id}_audio` } return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"/>')[0]}`, audioFilename: `tumblr_${obj.id}_audio` }

View file

@ -15,13 +15,13 @@ export default async function(obj) {
let req_act = await fetch(`${apiURL}/guest/activate.json`, { let req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST", method: "POST",
headers: _headers headers: _headers
}).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false}); }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' }; if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"]; _headers["x-guest-token"] = req_act["guest_token"];
let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1` let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`
if (!obj.spaceId) { if (!obj.spaceId) {
let req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch((e) => { return false}); let req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch((e) => { return false});
if (!req_status) { if (!req_status) {
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"; _headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
delete _headers["x-guest-token"] delete _headers["x-guest-token"]
@ -29,11 +29,11 @@ export default async function(obj) {
req_act = await fetch(`${apiURL}/guest/activate.json`, { req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST", method: "POST",
headers: _headers headers: _headers
}).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false}); }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' }; if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"]; _headers["x-guest-token"] = req_act["guest_token"];
req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false}); req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
} }
if (!req_status) return { error: 'ErrorCouldntFetch' } if (!req_status) return { error: 'ErrorCouldntFetch' }
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) { if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
@ -47,7 +47,7 @@ export default async function(obj) {
return { error: 'ErrorNoVideosInTweet' } return { error: 'ErrorNoVideosInTweet' }
} }
if (single) { if (single) {
return { urls: single, audioFilename: `twitter_${obj.id}_audio` } return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
} else if (multiple) { } else if (multiple) {
return { picker: multiple } return { picker: multiple }
} else { } else {
@ -64,12 +64,12 @@ export default async function(obj) {
} }
let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then(async (r) => { let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then(async (r) => {
return r.status == 200 ? await r.json() : false; return r.status == 200 ? r.json() : false;
}).catch((e) => {return false}); }).catch((e) => {return false});
if (AudioSpaceById) { if (AudioSpaceById) {
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) { if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then(async (r) => {return r.status == 200 ? await r.json() : false;}).catch(() => {return false;}); let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then(async (r) => {return r.status == 200 ? r.json() : false;}).catch(() => {return false;});
if (!streamStatus) return { error: 'ErrorCouldntFetch' }; if (!streamStatus) return { error: 'ErrorCouldntFetch' };
let participants = AudioSpaceById.data.audioSpace.participants.speakers let participants = AudioSpaceById.data.audioSpace.participants.speakers

View file

@ -2,7 +2,7 @@ import { quality, services } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then(async (r) => {return await r.json()}).catch(() => {return false}); let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then(async (r) => {return r.json()}).catch(() => {return false});
if (!api) return { error: 'ErrorCouldntFetch' }; if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = ""; let downloadType = "";
@ -29,10 +29,10 @@ export default async function(obj) {
} catch (e) { } catch (e) {
best = all[0] best = all[0]
} }
return { urls: best["url"] }; return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` };
case "dash": case "dash":
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"]; let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then(async (r) => {return await r.json()}).catch(() => {return false}); let masterJSON = await fetch(masterJSONURL).then(async (r) => {return r.json()}).catch(() => {return false});
if (!masterJSON) return { error: 'ErrorCouldntFetch' }; if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (masterJSON.video) { if (masterJSON.video) {
let type = ""; let type = "";

View file

@ -7,7 +7,7 @@ export default async function(obj) {
let html; let html;
html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, { html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false}); }).then(async (r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes(`{"lang":`)) { if (html.includes(`{"lang":`)) {
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);

View file

@ -22,8 +22,9 @@ export function createStream(obj) {
exp: exp, exp: exp,
isAudioOnly: !!obj.isAudioOnly, isAudioOnly: !!obj.isAudioOnly,
audioFormat: obj.audioFormat, audioFormat: obj.audioFormat,
time: obj.time, time: obj.time ? obj.time : false,
copy: obj.copy, copy: obj.copy ? true : false,
mute: obj.mute ? true : false,
metadata: obj.fileMetadata ? obj.fileMetadata : false metadata: obj.fileMetadata ? obj.fileMetadata : false
}); });
return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`; return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`;

View file

@ -1,6 +1,6 @@
import { apiJSON } from "../sub/utils.js"; import { apiJSON } from "../sub/utils.js";
import { verifyStream } from "./manage.js"; import { verifyStream } from "./manage.js";
import { streamAudioOnly, streamDefault, streamLiveRender } from "./types.js"; import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js";
export default function(res, ip, id, hmac, exp) { export default function(res, ip, id, hmac, exp) {
try { try {
@ -13,6 +13,9 @@ export default function(res, ip, id, hmac, exp) {
case "render": case "render":
streamLiveRender(streamInfo, res); streamLiveRender(streamInfo, res);
break; break;
case "mute":
streamVideoOnly(streamInfo, res);
break;
default: default:
streamDefault(streamInfo, res); streamDefault(streamInfo, res);
break; break;

View file

@ -6,7 +6,9 @@ import { metadataManager, msToTime } from "../sub/utils.js";
export function streamDefault(streamInfo, res) { export function streamDefault(streamInfo, res) {
try { try {
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename}"`); 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, { const stream = got.get(streamInfo.urls, {
headers: { headers: {
"user-agent": genericUserAgent "user-agent": genericUserAgent
@ -96,3 +98,31 @@ export function streamAudioOnly(streamInfo, res) {
res.end(); res.end();
} }
} }
export function streamVideoOnly(streamInfo, res) {
try {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8',
'-i', streamInfo.urls,
'-c', 'copy', '-an'
]
if (format == "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov')
args.push('-f', format, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe'
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}_mute.${format}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
} catch (e) {
res.end();
}
}

View file

@ -6,7 +6,7 @@ let apiVar = {
vQuality: ["max", "hig", "mid", "low", "los"], vQuality: ["max", "hig", "mid", "low", "los"],
aFormat: ["best", "mp3", "ogg", "wav", "opus"] aFormat: ["best", "mp3", "ogg", "wav", "opus"]
}, },
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio"] booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted"]
} }
export function apiJSON(type, obj) { export function apiJSON(type, obj) {
@ -95,7 +95,8 @@ export function checkJSONPost(obj) {
aFormat: "mp3", aFormat: "mp3",
isAudioOnly: false, isAudioOnly: false,
isNoTTWatermark: false, isNoTTWatermark: false,
isTTFullAudio: false isTTFullAudio: false,
isAudioMuted: false,
} }
try { try {
let objKeys = Object.keys(obj); let objKeys = Object.keys(obj);
@ -106,7 +107,7 @@ export function checkJSONPost(obj) {
if (apiVar.booleanOnly.includes(objKeys[i])) { if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false; def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else { } else {
if (apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]]) if (apiVar.allowed[objKeys[i]] && apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]])
} }
} }
} }