Merge branch 'twitter-gif' into html-cleanup
This commit is contained in:
commit
2acbbadbcb
20 changed files with 230 additions and 72 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "cobalt",
|
"name": "cobalt",
|
||||||
"description": "save what you love",
|
"description": "save what you love",
|
||||||
"version": "7.8.6",
|
"version": "7.9",
|
||||||
"author": "wukko",
|
"author": "wukko",
|
||||||
"exports": "./src/cobalt.js",
|
"exports": "./src/cobalt.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
@ -90,7 +90,8 @@
|
||||||
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
|
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
|
||||||
"copy": ["-c:a", "copy"],
|
"copy": ["-c:a", "copy"],
|
||||||
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
|
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
|
||||||
"m4a": ["-movflags", "frag_keyframe+empty_moov"]
|
"m4a": ["-movflags", "frag_keyframe+empty_moov"],
|
||||||
|
"gif": ["-vf", "scale=-1:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", "-loop", "0"]
|
||||||
},
|
},
|
||||||
"sponsors": [{
|
"sponsors": [{
|
||||||
"name": "royale",
|
"name": "royale",
|
||||||
|
|
|
@ -30,6 +30,7 @@ const checkboxes = [
|
||||||
"reduceTransparency",
|
"reduceTransparency",
|
||||||
"disableAnimations",
|
"disableAnimations",
|
||||||
"disableMetadata",
|
"disableMetadata",
|
||||||
|
"twitterGif",
|
||||||
];
|
];
|
||||||
const exceptions = { // used for mobile devices
|
const exceptions = { // used for mobile devices
|
||||||
"vQuality": "720"
|
"vQuality": "720"
|
||||||
|
@ -381,6 +382,7 @@ async function download(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sGet("disableMetadata") === "true") req.disableMetadata = true;
|
if (sGet("disableMetadata") === "true") req.disableMetadata = true;
|
||||||
|
if (sGet("twitterGif") === "true") req.twitterGif = true;
|
||||||
|
|
||||||
let j = await fetch(`${apiURL}/api/json`, {
|
let j = await fetch(`${apiURL}/api/json`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
@ -117,7 +117,6 @@
|
||||||
"ShareURL": "share",
|
"ShareURL": "share",
|
||||||
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
|
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
|
||||||
"ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.",
|
"ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.",
|
||||||
"UrgentDonate": "cobalt needs your help!",
|
|
||||||
"PopupCloseDone": "done",
|
"PopupCloseDone": "done",
|
||||||
"Accessibility": "accessibility",
|
"Accessibility": "accessibility",
|
||||||
"SettingsReduceTransparency": "reduce transparency",
|
"SettingsReduceTransparency": "reduce transparency",
|
||||||
|
@ -134,10 +133,7 @@
|
||||||
"KeyboardShortcutClosePopup": "close all popups",
|
"KeyboardShortcutClosePopup": "close all popups",
|
||||||
"CollapseLegal": "terms and ethics",
|
"CollapseLegal": "terms and ethics",
|
||||||
"FairUse": "cobalt is a web tool that makes it easier to download content from the internet and takes <span class=\"text-backdrop\">zero liability</span>. processing servers work like <span class=\"text-backdrop\">limited proxies</span>, so no media content is ever cached or stored.\n\nyou (end user) are responsible for what you download, how you use and distribute that content. please be mindful when using content of others and always credit original creators.\n\nwhen used in education purposes (lecture, homework, etc) please attach the source link.\n\nfair use and credits benefit everyone.",
|
"FairUse": "cobalt is a web tool that makes it easier to download content from the internet and takes <span class=\"text-backdrop\">zero liability</span>. processing servers work like <span class=\"text-backdrop\">limited proxies</span>, so no media content is ever cached or stored.\n\nyou (end user) are responsible for what you download, how you use and distribute that content. please be mindful when using content of others and always credit original creators.\n\nwhen used in education purposes (lecture, homework, etc) please attach the source link.\n\nfair use and credits benefit everyone.",
|
||||||
"UrgentFeatureUpdate71": "more supported services!",
|
|
||||||
"UrgentThanks": "thank you for support!",
|
|
||||||
"SettingsDisableMetadata": "don't add metadata",
|
"SettingsDisableMetadata": "don't add metadata",
|
||||||
"UrgentNewDomain": "new domain, same cobalt",
|
|
||||||
"NewDomainWelcomeTitle": "hey there!",
|
"NewDomainWelcomeTitle": "hey there!",
|
||||||
"NewDomainWelcome": "cobalt is moving! same features, same owner, simply a more rememberable domain. and still no ads.\n\n<span class=\"text-backdrop\">cobalt.tools</span> is the new main domain, aka where you are now. make sure to update your bookmarks and reinstall the web app!",
|
"NewDomainWelcome": "cobalt is moving! same features, same owner, simply a more rememberable domain. and still no ads.\n\n<span class=\"text-backdrop\">cobalt.tools</span> is the new main domain, aka where you are now. make sure to update your bookmarks and reinstall the web app!",
|
||||||
"DataTransferSuccess": "btw, your settings have been transferred automatically :)",
|
"DataTransferSuccess": "btw, your settings have been transferred automatically :)",
|
||||||
|
@ -154,11 +150,12 @@
|
||||||
"FilenamePreviewVideoTitle": "Video Title",
|
"FilenamePreviewVideoTitle": "Video Title",
|
||||||
"FilenamePreviewAudioTitle": "Audio Title",
|
"FilenamePreviewAudioTitle": "Audio Title",
|
||||||
"FilenamePreviewAudioAuthor": "Audio Author",
|
"FilenamePreviewAudioAuthor": "Audio Author",
|
||||||
"UrgentFilenameUpdate": "customizable file names!",
|
|
||||||
"UrgentTwitterPatch": "fixes and easier downloads",
|
|
||||||
"StatusPage": "service status page",
|
"StatusPage": "service status page",
|
||||||
"TroubleshootingGuide": "self-troubleshooting guide",
|
"TroubleshootingGuide": "self-troubleshooting guide",
|
||||||
"DonateImageDescription": "cat sleeping on a laptop keyboard and typing letters repeatedly",
|
"DonateImageDescription": "cat sleeping on a laptop keyboard and typing letters repeatedly",
|
||||||
"UpdateNewYears": "new years clean up"
|
"UpdateNewYears": "new years clean up",
|
||||||
|
"SettingsTwitterGif": "convert gifs to .gif",
|
||||||
|
"SettingsTwitterGifDescription": "converting looping videos to .gif reduces quality and majorly increases file size. if you want best efficiency, keep this setting off.",
|
||||||
|
"UpdateTwitterGif": "twitter gifs and pinterest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,6 @@
|
||||||
"ShareURL": "поделиться",
|
"ShareURL": "поделиться",
|
||||||
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!",
|
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!",
|
||||||
"ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.",
|
"ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.",
|
||||||
"UrgentDonate": "нужна твоя помощь!",
|
|
||||||
"PopupCloseDone": "готово",
|
"PopupCloseDone": "готово",
|
||||||
"Accessibility": "общедоступность",
|
"Accessibility": "общедоступность",
|
||||||
"SettingsReduceTransparency": "уменьшить прозрачность",
|
"SettingsReduceTransparency": "уменьшить прозрачность",
|
||||||
|
@ -135,10 +134,7 @@
|
||||||
"KeyboardShortcutClosePopup": "закрыть все окна",
|
"KeyboardShortcutClosePopup": "закрыть все окна",
|
||||||
"CollapseLegal": "принципы и этика",
|
"CollapseLegal": "принципы и этика",
|
||||||
"FairUse": "кобальт - это веб инструмент для облегчения скачивания контента из интернета. сервера обработки работают как <span class=\"text-backdrop\">ограниченные прокси</span>, так что ничего никогда не сохраняется или кэшируется.\n\nкобальт <span class=\"text-backdrop\">не несёт никакой ответственности</span>, только ты (конечный пользователь) несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент. будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nприкладывай ссылку на источник при использовании в образовательных целях (лекции, домашние задания и т.п.)\n\nчестное использование и указание авторства выгодно всем.",
|
"FairUse": "кобальт - это веб инструмент для облегчения скачивания контента из интернета. сервера обработки работают как <span class=\"text-backdrop\">ограниченные прокси</span>, так что ничего никогда не сохраняется или кэшируется.\n\nкобальт <span class=\"text-backdrop\">не несёт никакой ответственности</span>, только ты (конечный пользователь) несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент. будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nприкладывай ссылку на источник при использовании в образовательных целях (лекции, домашние задания и т.п.)\n\nчестное использование и указание авторства выгодно всем.",
|
||||||
"UrgentFeatureUpdate71": "расширение поддержки сервисов!",
|
|
||||||
"UrgentThanks": "спасибо за поддержку!",
|
|
||||||
"SettingsDisableMetadata": "не добавлять метаданные",
|
"SettingsDisableMetadata": "не добавлять метаданные",
|
||||||
"UrgentNewDomain": "новый домен, тот же кобальт",
|
|
||||||
"NewDomainWelcomeTitle": "привет!",
|
"NewDomainWelcomeTitle": "привет!",
|
||||||
"NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\n<span class=\"text-backdrop\">cobalt.tools</span> - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!",
|
"NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\n<span class=\"text-backdrop\">cobalt.tools</span> - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!",
|
||||||
"DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)",
|
"DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)",
|
||||||
|
@ -156,11 +152,12 @@
|
||||||
"FilenamePreviewVideoTitle": "Название Видео",
|
"FilenamePreviewVideoTitle": "Название Видео",
|
||||||
"FilenamePreviewAudioTitle": "Название Аудио",
|
"FilenamePreviewAudioTitle": "Название Аудио",
|
||||||
"FilenamePreviewAudioAuthor": "Автор Аудио",
|
"FilenamePreviewAudioAuthor": "Автор Аудио",
|
||||||
"UrgentFilenameUpdate": "изменяемые названия файлов!",
|
|
||||||
"UrgentTwitterPatch": "фиксы и удобное скачивание",
|
|
||||||
"StatusPage": "статус серверов",
|
"StatusPage": "статус серверов",
|
||||||
"TroubleshootingGuide": "гайд по устранению проблем",
|
"TroubleshootingGuide": "гайд по устранению проблем",
|
||||||
"DonateImageDescription": "кошка спит на клавиатуре ноутбука и многократно печатает буквы",
|
"DonateImageDescription": "кошка спит на клавиатуре ноутбука и многократно печатает буквы",
|
||||||
"UpdateNewYears": "новогодняя уборка"
|
"UpdateNewYears": "новогодняя уборка",
|
||||||
|
"SettingsTwitterGif": "конвертировать гифки в .gif",
|
||||||
|
"SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.",
|
||||||
|
"UpdateTwitterGif": "гифки с твиттера и одноклассники"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -335,6 +335,16 @@ export default function(obj) {
|
||||||
padding: "no-margin"
|
padding: "no-margin"
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
+ settingsCategory({
|
||||||
|
name: "twitter",
|
||||||
|
title: "twitter",
|
||||||
|
body: checkbox([{
|
||||||
|
action: "twitterGif",
|
||||||
|
name: t("SettingsTwitterGif"),
|
||||||
|
padding: "no-margin"
|
||||||
|
}])
|
||||||
|
+ explanation(t('SettingsTwitterGifDescription'))
|
||||||
|
})
|
||||||
+ settingsCategory({
|
+ settingsCategory({
|
||||||
name: "codec",
|
name: "codec",
|
||||||
title: t('SettingsCodecSubtitle'),
|
title: t('SettingsCodecSubtitle'),
|
||||||
|
@ -576,7 +586,7 @@ export default function(obj) {
|
||||||
<div id="download-area">
|
<div id="download-area">
|
||||||
<div id="top">
|
<div id="top">
|
||||||
<div id="link-icon">${linkSVG}</div>
|
<div id="link-icon">${linkSVG}</div>
|
||||||
<input id="url-input-area" class="mono" type="text" autocomplete="off" spellcheck="false" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()">
|
<input id="url-input-area" class="mono" type="text" autocomplete="off" spellcheck="false" maxlength="256" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()">
|
||||||
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
|
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
|
||||||
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled aria-label="${t('AccessibilityDownloadButton')}">
|
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled aria-label="${t('AccessibilityDownloadButton')}">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import reddit from "./services/reddit.js";
|
||||||
import twitter from "./services/twitter.js";
|
import twitter from "./services/twitter.js";
|
||||||
import youtube from "./services/youtube.js";
|
import youtube from "./services/youtube.js";
|
||||||
import vk from "./services/vk.js";
|
import vk from "./services/vk.js";
|
||||||
|
import ok from "./services/ok.js";
|
||||||
import tiktok from "./services/tiktok.js";
|
import tiktok from "./services/tiktok.js";
|
||||||
import tumblr from "./services/tumblr.js";
|
import tumblr from "./services/tumblr.js";
|
||||||
import vimeo from "./services/vimeo.js";
|
import vimeo from "./services/vimeo.js";
|
||||||
|
@ -37,24 +38,31 @@ export default async function(host, patternMatch, url, lang, obj) {
|
||||||
case "twitter":
|
case "twitter":
|
||||||
r = await twitter({
|
r = await twitter({
|
||||||
id: patternMatch.id,
|
id: patternMatch.id,
|
||||||
index: patternMatch.index - 1
|
index: patternMatch.index - 1,
|
||||||
|
toGif: obj.twitterGif
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "vk":
|
case "vk":
|
||||||
r = await vk({
|
r = await vk({
|
||||||
userId: patternMatch["userId"],
|
userId: patternMatch.userId,
|
||||||
videoId: patternMatch["videoId"],
|
videoId: patternMatch.videoId,
|
||||||
|
quality: obj.vQuality
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "ok":
|
||||||
|
r = await ok({
|
||||||
|
id: patternMatch.id,
|
||||||
quality: obj.vQuality
|
quality: obj.vQuality
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "bilibili":
|
case "bilibili":
|
||||||
r = await bilibili({
|
r = await bilibili({
|
||||||
id: patternMatch["id"].slice(0, 12)
|
id: patternMatch.id.slice(0, 12)
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "youtube":
|
case "youtube":
|
||||||
let fetchInfo = {
|
let fetchInfo = {
|
||||||
id: patternMatch["id"].slice(0, 11),
|
id: patternMatch.id.slice(0, 11),
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
format: obj.vCodec,
|
format: obj.vCodec,
|
||||||
isAudioOnly: isAudioOnly,
|
isAudioOnly: isAudioOnly,
|
||||||
|
@ -72,16 +80,16 @@ export default async function(host, patternMatch, url, lang, obj) {
|
||||||
break;
|
break;
|
||||||
case "reddit":
|
case "reddit":
|
||||||
r = await reddit({
|
r = await reddit({
|
||||||
sub: patternMatch["sub"],
|
sub: patternMatch.sub,
|
||||||
id: patternMatch["id"]
|
id: patternMatch.id
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "douyin":
|
case "douyin":
|
||||||
case "tiktok":
|
case "tiktok":
|
||||||
r = await tiktok({
|
r = await tiktok({
|
||||||
host: host,
|
host: host,
|
||||||
postId: patternMatch["postId"],
|
postId: patternMatch.postId,
|
||||||
id: patternMatch["id"],
|
id: patternMatch.id,
|
||||||
noWatermark: obj.isNoTTWatermark,
|
noWatermark: obj.isNoTTWatermark,
|
||||||
fullAudio: obj.isTTFullAudio,
|
fullAudio: obj.isTTFullAudio,
|
||||||
isAudioOnly: isAudioOnly
|
isAudioOnly: isAudioOnly
|
||||||
|
@ -96,7 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) {
|
||||||
break;
|
break;
|
||||||
case "vimeo":
|
case "vimeo":
|
||||||
r = await vimeo({
|
r = await vimeo({
|
||||||
id: patternMatch["id"].slice(0, 11),
|
id: patternMatch.id.slice(0, 11),
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
isAudioOnly: isAudioOnly,
|
isAudioOnly: isAudioOnly,
|
||||||
forceDash: isAudioOnly ? true : obj.vimeoDash
|
forceDash: isAudioOnly ? true : obj.vimeoDash
|
||||||
|
@ -106,10 +114,10 @@ export default async function(host, patternMatch, url, lang, obj) {
|
||||||
isAudioOnly = true;
|
isAudioOnly = true;
|
||||||
r = await soundcloud({
|
r = await soundcloud({
|
||||||
url,
|
url,
|
||||||
author: patternMatch["author"],
|
author: patternMatch.author,
|
||||||
song: patternMatch["song"],
|
song: patternMatch.song,
|
||||||
shortLink: patternMatch["shortLink"] || false,
|
shortLink: patternMatch.shortLink || false,
|
||||||
accessKey: patternMatch["accessKey"] || false
|
accessKey: patternMatch.accessKey || false
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "instagram":
|
case "instagram":
|
||||||
|
@ -120,31 +128,32 @@ export default async function(host, patternMatch, url, lang, obj) {
|
||||||
break;
|
break;
|
||||||
case "vine":
|
case "vine":
|
||||||
r = await vine({
|
r = await vine({
|
||||||
id: patternMatch["id"]
|
id: patternMatch.id
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "pinterest":
|
case "pinterest":
|
||||||
r = await pinterest({
|
r = await pinterest({
|
||||||
id: patternMatch["id"]
|
id: patternMatch.id,
|
||||||
|
shortLink: patternMatch.shortLink || false
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "streamable":
|
case "streamable":
|
||||||
r = await streamable({
|
r = await streamable({
|
||||||
id: patternMatch["id"],
|
id: patternMatch.id,
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
isAudioOnly: isAudioOnly,
|
isAudioOnly: isAudioOnly,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "twitch":
|
case "twitch":
|
||||||
r = await twitch({
|
r = await twitch({
|
||||||
clipId: patternMatch["clip"] || false,
|
clipId: patternMatch.clip || false,
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
isAudioOnly: obj.isAudioOnly
|
isAudioOnly: obj.isAudioOnly
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "rutube":
|
case "rutube":
|
||||||
r = await rutube({
|
r = await rutube({
|
||||||
id: patternMatch["id"],
|
id: patternMatch.id,
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
isAudioOnly: isAudioOnly
|
isAudioOnly: isAudioOnly
|
||||||
});
|
});
|
||||||
|
@ -166,7 +175,11 @@ export default async function(host, patternMatch, url, lang, obj) {
|
||||||
: loc(lang, r.error)
|
: loc(lang, r.error)
|
||||||
})
|
})
|
||||||
|
|
||||||
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, obj.filenamePattern)
|
return matchActionDecider(
|
||||||
|
r, host, obj.aFormat, isAudioOnly,
|
||||||
|
lang, isAudioMuted, disableMetadata,
|
||||||
|
obj.filenamePattern, obj.twitterGif
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return apiJSON(0, { t: genericError(lang, host) })
|
return apiJSON(0, { t: genericError(lang, host) })
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { apiJSON } from "../sub/utils.js";
|
||||||
import loc from "../../localization/manager.js";
|
import loc from "../../localization/manager.js";
|
||||||
import createFilename from "./createFilename.js";
|
import createFilename from "./createFilename.js";
|
||||||
|
|
||||||
export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern) {
|
export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif) {
|
||||||
let action,
|
let action,
|
||||||
responseType = 2,
|
responseType = 2,
|
||||||
defaultParams = {
|
defaultParams = {
|
||||||
|
@ -14,13 +14,14 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
||||||
fileMetadata: !disableMetadata ? r.fileMetadata : false
|
fileMetadata: !disableMetadata ? r.fileMetadata : false
|
||||||
},
|
},
|
||||||
params = {},
|
params = {},
|
||||||
audioFormat = String(userFormat)
|
audioFormat = String(userFormat);
|
||||||
|
|
||||||
if (r.isPhoto) action = "photo";
|
if (r.isPhoto) action = "photo";
|
||||||
else if (r.picker) action = "picker"
|
else if (r.picker) action = "picker"
|
||||||
else if (isAudioMuted) action = "muteVideo";
|
else if (isAudioMuted) action = "muteVideo";
|
||||||
else if (isAudioOnly) action = "audio";
|
else if (isAudioOnly) action = "audio";
|
||||||
else if (r.isM3U8) action = "singleM3U8";
|
else if (r.isM3U8) action = "singleM3U8";
|
||||||
|
else if (r.isGif && toGif) action = "gif";
|
||||||
else action = "video";
|
else action = "video";
|
||||||
|
|
||||||
if (action === "picker" || action === "audio") {
|
if (action === "picker" || action === "audio") {
|
||||||
|
@ -40,6 +41,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
||||||
responseType = 1;
|
responseType = 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "gif":
|
||||||
|
params = { type: "gif" }
|
||||||
|
break;
|
||||||
|
|
||||||
case "singleM3U8":
|
case "singleM3U8":
|
||||||
params = { type: "remux" }
|
params = { type: "remux" }
|
||||||
break;
|
break;
|
||||||
|
|
56
src/modules/processing/services/ok.js
Normal file
56
src/modules/processing/services/ok.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { genericUserAgent, maxVideoDuration } from "../../config.js";
|
||||||
|
import { cleanString } from "../../sub/utils.js";
|
||||||
|
|
||||||
|
const resolutions = {
|
||||||
|
"ultra": "2160",
|
||||||
|
"quad": "1440",
|
||||||
|
"full": "1080",
|
||||||
|
"hd": "720",
|
||||||
|
"sd": "480",
|
||||||
|
"low": "360",
|
||||||
|
"lowest": "240",
|
||||||
|
"mobile": "144"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function(o) {
|
||||||
|
let quality = o.quality === "max" ? "2160" : o.quality;
|
||||||
|
|
||||||
|
let html = await fetch(`https://ok.ru/video/${o.id}`, {
|
||||||
|
headers: { "user-agent": genericUserAgent }
|
||||||
|
}).then((r) => { return r.text() }).catch(() => { return false });
|
||||||
|
|
||||||
|
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||||
|
if (!html.includes(`<div data-module="OKVideo" data-options="{`)) {
|
||||||
|
return { error: 'ErrorEmptyDownload' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoData = html.split(`<div data-module="OKVideo" data-options="`)[1].split('" data-')[0].replaceAll(""", '"');
|
||||||
|
videoData = JSON.parse(JSON.parse(videoData).flashvars.metadata);
|
||||||
|
|
||||||
|
if (videoData.provider !== "UPLOADED_ODKL") return { error: 'ErrorUnsupported' };
|
||||||
|
if (videoData.movie.is_live) return { error: 'ErrorLiveVideo' };
|
||||||
|
if (videoData.movie.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||||
|
|
||||||
|
let videos = videoData.videos.filter(v => !v.disallowed);
|
||||||
|
let bestVideo = videos.find(v => resolutions[v.name] === quality) || videos[videos.length - 1];
|
||||||
|
|
||||||
|
let fileMetadata = {
|
||||||
|
title: cleanString(videoData.movie.title.trim()),
|
||||||
|
author: cleanString(videoData.author.name.trim()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestVideo) return {
|
||||||
|
urls: bestVideo.url,
|
||||||
|
filenameAttributes: {
|
||||||
|
service: "ok",
|
||||||
|
id: o.id,
|
||||||
|
title: fileMetadata.title,
|
||||||
|
author: fileMetadata.author,
|
||||||
|
resolution: `${resolutions[bestVideo.name]}p`,
|
||||||
|
qualityLabel: `${resolutions[bestVideo.name]}p`,
|
||||||
|
extension: "mp4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: 'ErrorEmptyDownload' }
|
||||||
|
}
|
|
@ -1,29 +1,36 @@
|
||||||
import { maxVideoDuration } from "../../config.js";
|
import { genericUserAgent } from "../../config.js";
|
||||||
|
|
||||||
export default async function(obj) {
|
const videoLinkBase = {
|
||||||
const pinId = obj.id.split('--').reverse()[0];
|
"regular": "https://v1.pinimg.com/videos/mc/720p/",
|
||||||
if (!(/^\d+$/.test(pinId))) return { error: 'ErrorCantGetID' };
|
"story": "https://v1.pinimg.com/videos/mc/720p/"
|
||||||
let data = await fetch(`https://www.pinterest.com/resource/PinResource/get?data=${encodeURIComponent(JSON.stringify({
|
|
||||||
options: {
|
|
||||||
field_set_key: "unauth_react_main_pin",
|
|
||||||
id: pinId
|
|
||||||
}
|
}
|
||||||
}))}`).then((r) => { return r.json() }).catch(() => { return false });
|
|
||||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
|
||||||
|
|
||||||
data = data["resource_response"]["data"];
|
export default async function(o) {
|
||||||
|
let id = o.id, type = "regular";
|
||||||
|
|
||||||
let video = null;
|
if (id.includes("--")) {
|
||||||
|
id = id.split("--")[1];
|
||||||
|
type = "story";
|
||||||
|
}
|
||||||
|
if (!o.id && o.shortLink) {
|
||||||
|
id = await fetch(`https://api.pinterest.com/url_shortener/${o.shortLink}/redirect/`, { redirect: "manual" }).then((r) => {
|
||||||
|
return r.headers.get("location").split('pin/')[1].split('/')[0]
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
if (!id) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
if (data.videos !== null) video = data.videos.video_list.V_720P;
|
let html = await fetch(`https://www.pinterest.com/pin/${id}/`, {
|
||||||
else if (data.story_pin_data !== null) video = data.story_pin_data.pages[0].blocks[0].video.video_list.V_EXP7;
|
headers: { "user-agent": genericUserAgent }
|
||||||
|
}).then((r) => { return r.text() }).catch(() => { return false });
|
||||||
|
|
||||||
if (!video) return { error: 'ErrorEmptyDownload' };
|
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||||
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
|
||||||
|
let videoLink = html.split(`"url":"${videoLinkBase[type]}`)[1]?.split('"')[0];
|
||||||
|
if (!html.includes(videoLink)) return { error: 'ErrorEmptyDownload' };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
urls: video.url,
|
urls: `${videoLinkBase[type]}${videoLink}`,
|
||||||
filename: `pinterest_${pinId}.mp4`,
|
filename: `pinterest_${o.id}.mp4`,
|
||||||
audioFilename: `pinterest_${pinId}_audio`
|
audioFilename: `pinterest_${o.id}_audio`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ const requestTweet = (tweetId, token) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function({ id, index }) {
|
export default async function({ id, index, toGif }) {
|
||||||
let guestToken = await getGuestToken();
|
let guestToken = await getGuestToken();
|
||||||
if (!guestToken) return { error: 'ErrorCouldntFetch' };
|
if (!guestToken) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
|
@ -110,7 +110,8 @@ export default async function({ id, index }) {
|
||||||
type: needsFixing(media[0]) ? "remux" : "normal",
|
type: needsFixing(media[0]) ? "remux" : "normal",
|
||||||
urls: bestQuality(media[0].video_info.variants),
|
urls: bestQuality(media[0].video_info.variants),
|
||||||
filename: `twitter_${id}.mp4`,
|
filename: `twitter_${id}.mp4`,
|
||||||
audioFilename: `twitter_${id}_audio`
|
audioFilename: `twitter_${id}_audio`,
|
||||||
|
isGif: media[0].type === "animated_gif"
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
const picker = media.map((video, i) => {
|
const picker = media.map((video, i) => {
|
||||||
|
@ -120,7 +121,9 @@ export default async function({ id, index }) {
|
||||||
service: 'twitter',
|
service: 'twitter',
|
||||||
type: 'remux',
|
type: 'remux',
|
||||||
u: url,
|
u: url,
|
||||||
filename: `twitter_${id}_${i + 1}.mp4`
|
filename: `twitter_${id}_${i + 1}.mp4`,
|
||||||
|
isGif: media[0].type === "animated_gif",
|
||||||
|
toGif: toGif ?? false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { cleanString } from '../../sub/utils.js';
|
||||||
const resolutionMatch = {
|
const resolutionMatch = {
|
||||||
"3840": "2160",
|
"3840": "2160",
|
||||||
"2732": "1440",
|
"2732": "1440",
|
||||||
|
"2560": "1440",
|
||||||
"2048": "1080",
|
"2048": "1080",
|
||||||
"1920": "1080",
|
"1920": "1080",
|
||||||
"1366": "720",
|
"1366": "720",
|
||||||
|
@ -63,7 +64,7 @@ export default async function(obj) {
|
||||||
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
|
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
|
||||||
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
|
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
|
||||||
|
|
||||||
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)).filter(a => a['format'] === "mp42"),
|
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)).filter(a => ["dash", "mp42"].includes(a['format'])),
|
||||||
bestVideo = masterJSON_Video[0];
|
bestVideo = masterJSON_Video[0];
|
||||||
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) {
|
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) {
|
||||||
bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality)
|
bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality)
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default async function(o) {
|
||||||
|
|
||||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
// decode cyrillic from windows-1251 because vk still uses apis from prehistoring times
|
// decode cyrillic from windows-1251 because vk still uses apis from prehistoric times
|
||||||
let decoder = new TextDecoder('windows-1251');
|
let decoder = new TextDecoder('windows-1251');
|
||||||
html = decoder.decode(html);
|
html = decoder.decode(html);
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ export default async function(o) {
|
||||||
|
|
||||||
let fileMetadata = {
|
let fileMetadata = {
|
||||||
title: cleanString(js.player.params[0].md_title.trim()),
|
title: cleanString(js.player.params[0].md_title.trim()),
|
||||||
artist: cleanString(js.player.params[0].md_author.trim()),
|
author: cleanString(js.player.params[0].md_author.trim()),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url) return {
|
if (url) return {
|
||||||
|
@ -44,7 +44,7 @@ export default async function(o) {
|
||||||
service: "vk",
|
service: "vk",
|
||||||
id: `${o.userId}_${o.videoId}`,
|
id: `${o.userId}_${o.videoId}`,
|
||||||
title: fileMetadata.title,
|
title: fileMetadata.title,
|
||||||
author: fileMetadata.artist,
|
author: fileMetadata.author,
|
||||||
resolution: `${quality}p`,
|
resolution: `${quality}p`,
|
||||||
qualityLabel: `${quality}p`,
|
qualityLabel: `${quality}p`,
|
||||||
extension: "mp4"
|
extension: "mp4"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"audioIgnore": ["vk"],
|
"audioIgnore": ["vk", "ok"],
|
||||||
"config": {
|
"config": {
|
||||||
"bilibili": {
|
"bilibili": {
|
||||||
"alias": "bilibili.com videos",
|
"alias": "bilibili.com videos",
|
||||||
|
@ -28,6 +28,12 @@
|
||||||
"patterns": ["video:userId_:videoId", "clip:userId_:videoId", "clips:duplicate?z=clip:userId_:videoId"],
|
"patterns": ["video:userId_:videoId", "clip:userId_:videoId", "clips:duplicate?z=clip:userId_:videoId"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
"ok": {
|
||||||
|
"alias": "ok video",
|
||||||
|
"tld": "ru",
|
||||||
|
"patterns": ["video/:id"],
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"youtube": {
|
"youtube": {
|
||||||
"alias": "youtube videos, shorts & music",
|
"alias": "youtube videos, shorts & music",
|
||||||
"patterns": ["watch?v=:id", "embed/:id", "watch/:id"],
|
"patterns": ["watch?v=:id", "embed/:id", "watch/:id"],
|
||||||
|
@ -80,7 +86,7 @@
|
||||||
},
|
},
|
||||||
"pinterest": {
|
"pinterest": {
|
||||||
"alias": "pinterest videos & stories",
|
"alias": "pinterest videos & stories",
|
||||||
"patterns": ["pin/:id"],
|
"patterns": ["pin/:id", "url_shortener/:shortLink"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"streamable": {
|
"streamable": {
|
||||||
|
|
|
@ -6,8 +6,11 @@ export const testers = {
|
||||||
patternMatch.postId?.length <= 12
|
patternMatch.postId?.length <= 12
|
||||||
|| (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24),
|
|| (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24),
|
||||||
|
|
||||||
|
"ok": (patternMatch) =>
|
||||||
|
patternMatch.id?.length <= 16,
|
||||||
|
|
||||||
"pinterest": (patternMatch) =>
|
"pinterest": (patternMatch) =>
|
||||||
patternMatch.id?.length <= 128,
|
patternMatch.id?.length <= 128 || patternMatch.shortLink?.length <= 32,
|
||||||
|
|
||||||
"reddit": (patternMatch) =>
|
"reddit": (patternMatch) =>
|
||||||
patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10,
|
patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10,
|
||||||
|
|
|
@ -25,6 +25,13 @@ export function aliasURL(url) {
|
||||||
}`)
|
}`)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "pin":
|
||||||
|
if (url.hostname === 'pin.it' && parts.length === 2) {
|
||||||
|
url = new URL(`https://pinterest.com/url_shortener/${
|
||||||
|
encodeURIComponent(parts[1])
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "vxtwitter":
|
case "vxtwitter":
|
||||||
case "fixvx":
|
case "fixvx":
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js";
|
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js";
|
||||||
|
|
||||||
export default async function(res, streamInfo) {
|
export default async function(res, streamInfo) {
|
||||||
try {
|
try {
|
||||||
|
@ -10,6 +10,9 @@ export default async function(res, streamInfo) {
|
||||||
case "render":
|
case "render":
|
||||||
await streamLiveRender(streamInfo, res);
|
await streamLiveRender(streamInfo, res);
|
||||||
break;
|
break;
|
||||||
|
case "gif":
|
||||||
|
convertToGif(streamInfo, res);
|
||||||
|
break;
|
||||||
case "remux":
|
case "remux":
|
||||||
case "mute":
|
case "mute":
|
||||||
streamVideoOnly(streamInfo, res);
|
streamVideoOnly(streamInfo, res);
|
||||||
|
|
|
@ -212,3 +212,40 @@ export function streamVideoOnly(streamInfo, res) {
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertToGif(streamInfo, res) {
|
||||||
|
let process;
|
||||||
|
const shutdown = () => (killProcess(process), closeResponse(res));
|
||||||
|
|
||||||
|
try {
|
||||||
|
let args = [
|
||||||
|
'-loglevel', '-8'
|
||||||
|
]
|
||||||
|
if (streamInfo.service === "twitter") {
|
||||||
|
args.push('-seekable', '0')
|
||||||
|
}
|
||||||
|
args.push('-i', streamInfo.urls)
|
||||||
|
args = args.concat(ffmpegArgs["gif"]);
|
||||||
|
args.push('-f', "gif", 'pipe:3');
|
||||||
|
|
||||||
|
process = spawn(ffmpeg, args, {
|
||||||
|
windowsHide: true,
|
||||||
|
stdio: [
|
||||||
|
'inherit', 'inherit', 'inherit',
|
||||||
|
'pipe'
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [,,, muxOutput] = process.stdio;
|
||||||
|
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename.split('.')[0] + ".gif"));
|
||||||
|
|
||||||
|
pipe(muxOutput, res, shutdown);
|
||||||
|
|
||||||
|
process.on('close', shutdown);
|
||||||
|
res.on('finish', shutdown);
|
||||||
|
} catch {
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ const apiVar = {
|
||||||
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
|
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
|
||||||
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
|
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
|
||||||
},
|
},
|
||||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"]
|
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
|
||||||
}
|
}
|
||||||
const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
|
const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
|
||||||
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
||||||
|
@ -84,7 +84,8 @@ export function checkJSONPost(obj) {
|
||||||
isAudioMuted: false,
|
isAudioMuted: false,
|
||||||
disableMetadata: false,
|
disableMetadata: false,
|
||||||
dubLang: false,
|
dubLang: false,
|
||||||
vimeoDash: false
|
vimeoDash: false,
|
||||||
|
twitterGif: false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let objKeys = Object.keys(obj);
|
let objKeys = Object.keys(obj);
|
||||||
|
|
|
@ -1162,5 +1162,14 @@
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"status": "stream"
|
"status": "stream"
|
||||||
}
|
}
|
||||||
|
}],
|
||||||
|
"ok": [{
|
||||||
|
"name": "regular video",
|
||||||
|
"url": "https://ok.ru/video/7204071410346",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "stream"
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue