Merge branch 'twitter-gif' into html-cleanup

This commit is contained in:
dumbmoron 2024-01-17 12:08:01 +01:00 committed by GitHub
commit 2acbbadbcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 230 additions and 72 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"
} }
} }

View file

@ -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": "гифки с твиттера и одноклассники"
} }
} }

View file

@ -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>

View file

@ -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) })
} }

View file

@ -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;

View 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("&quot;", '"');
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' }
}

View file

@ -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`
} }
} }

View file

@ -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 {

View file

@ -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)

View file

@ -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"

View file

@ -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": {

View file

@ -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,

View file

@ -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":

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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"
}
}] }]
} }