diff --git a/package.json b/package.json index 6b1ca082..e1db6461 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.6.8", + "version": "7.7", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/config.json b/src/config.json index b1d17674..303af1ed 100644 --- a/src/config.json +++ b/src/config.json @@ -1,6 +1,6 @@ { - "streamLifespan": 20000, - "maxVideoDuration": 18000000, + "streamLifespan": 90000, + "maxVideoDuration": 10800000, "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "authorInfo": { "name": "wukko", diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 62ef6a90..7612ff47 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -105,7 +105,7 @@ "FollowSupport": "keep in touch with cobalt for support, polls, news, and more:", "SupportNote": "please note that response may take a while, there's only one person managing everything.", "SourceCode": "report issues, explore source code, star or fork the repo:", - "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for 20 seconds and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's source code yourself and see that everything is as stated.", + "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for 90 seconds and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's source code yourself and see that everything is as stated.", "ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api sometimes acts unexpectedly. try again or try another settings.", "SettingsCodecSubtitle": "youtube codec", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index ec2e60d7..81ccb8cd 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -106,7 +106,7 @@ "FollowSupport": "подписывайся на соц.сети кобальта для новостей, поддержки, участия в опросах, и многого другого:", "SupportNote": "так как я занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.", "SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", - "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение 20 секунд. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", + "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение 90 секунд. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", "SettingsCodecSubtitle": "кодек для видео с youtube", diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index eedf8ec1..422d9a62 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -40,6 +40,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d case "bilibili": params = { type: "render" }; break; + case "twitter": case "youtube": params = { type: r.type }; break; @@ -64,7 +65,6 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d case "vine": case "instagram": case "tumblr": - case "twitter": case "pinterest": case "streamable": responseType = 1; @@ -72,7 +72,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d } break; case "singleM3U8": - params = { type: "videoM3U8" } + params = { type: "remux" } break; case "muteVideo": params = { @@ -107,14 +107,17 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d break; case "audio": - if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); + if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) { + return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }) + } let processType = "render"; let copy = false; if (!supportedAudio.includes(audioFormat)) audioFormat = "best"; - if ((host === "tiktok" || host === "douyin") && services.tiktok.audioFormats.includes(audioFormat)) { + if ((host === "tiktok" || host === "douyin") + && services.tiktok.audioFormats.includes(audioFormat)) { if (r.isMp3) { if (audioFormat === "mp3" || audioFormat === "best") { audioFormat = "mp3"; @@ -125,11 +128,13 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d processType = "bridge" } } - if (host === "tumblr" && !r.filename && (audioFormat === "best" || audioFormat === "mp3")) { + if (host === "tumblr" && !r.filename + && (audioFormat === "best" || audioFormat === "mp3")) { audioFormat = "mp3"; processType = "bridge" } - if ((audioFormat === "best" && services[host]["bestAudio"]) || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) { + if ((audioFormat === "best" && services[host]["bestAudio"]) + || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) { audioFormat = services[host]["bestAudio"]; if (host === "soundcloud") { processType = "render" diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js index da3b2b44..110a43e1 100644 --- a/src/modules/processing/services/twitter.js +++ b/src/modules/processing/services/twitter.js @@ -1,4 +1,5 @@ import { genericUserAgent } from "../../config.js"; +import { createStream } from "../../stream/manage.js"; function bestQuality(arr) { return arr.filter(v => v["content_type"] === "video/mp4").sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"] @@ -39,7 +40,7 @@ export default async function(obj) { let tweet = await fetch(query, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false - }).catch((e) => { return false }); + }).catch(() => { return false }); // {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}} if (tweet?.data?.tweetResult?.result?.__typename !== "Tweet") { @@ -64,7 +65,12 @@ export default async function(obj) { multiple.push({ type: "video", thumb: media[i]["media_url_https"], - url: bestQuality(media[i]["video_info"]["variants"]) + url: createStream({ + service: "twitter", + type: "remux", + u: bestQuality(media[i]["video_info"]["variants"]), + filename: `twitter_${obj.id}_${Number(i) + 1}.mp4` + }) }) } } else if (media.length === 1) { @@ -75,6 +81,7 @@ export default async function(obj) { if (single) { return { + type: "remux", urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index b3954d1a..3ec43b68 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -5,16 +5,22 @@ import { nanoid } from 'nanoid'; import { sha256 } from "../sub/crypto.js"; import { streamLifespan } from "../config.js"; -const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, deleteOnExpire: true }); -const streamSalt = randomBytes(64).toString('hex'); +const streamCache = new NodeCache({ + stdTTL: streamLifespan/1000, + checkperiod: 10, + deleteOnExpire: true +}) streamCache.on("expired", (key) => { streamCache.del(key); -}); +}) + +const streamSalt = randomBytes(64).toString('hex'); export function createStream(obj) { + let lifespan = streamLifespan let streamID = nanoid(), - exp = Math.floor(new Date().getTime()) + streamLifespan, + exp = Math.floor(new Date().getTime()) + lifespan, ghmac = sha256(`${streamID},${obj.service},${exp}`, streamSalt); if (!streamCache.has(streamID)) { @@ -44,14 +50,20 @@ export function createStream(obj) { export function verifyStream(id, hmac, exp) { try { let streamInfo = streamCache.get(id.toString()); - if (!streamInfo) return { error: "this download link has expired or doesn't exist. go back and try again!", status: 400 }; + if (!streamInfo) return { + error: "this download link has expired or doesn't exist. go back and try again!", + status: 400 + } let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt); if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac) && Number(exp) > Math.floor(new Date().getTime())) { return streamInfo; } - return { error: "i couldn't verify if you have access to this download. go back and try again!", status: 401 }; + return { + error: "i couldn't verify if you have access to this stream. go back and try again!", + status: 401 + } } catch (e) { return { status: 500, body: { status: "error", text: "Internal Server Error" } }; } diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index 4b03196b..fb4f7ed6 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -10,7 +10,7 @@ export default async function(res, streamInfo) { case "render": await streamLiveRender(streamInfo, res); break; - case "videoM3U8": + case "remux": case "mute": streamVideoOnly(streamInfo, res); break; diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index c584789f..03a78205 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -1,7 +1,7 @@ import { spawn } from "child_process"; import ffmpeg from "ffmpeg-static"; import { ffmpegArgs, genericUserAgent } from "../config.js"; -import { getThreads, metadataManager } from "../sub/utils.js"; +import { metadataManager } from "../sub/utils.js"; import { request } from "undici"; import { create as contentDisposition } from "content-disposition-header"; import { AbortController } from "abort-controller" @@ -40,7 +40,10 @@ export async function streamDefault(streamInfo, res) { const shutdown = () => (closeRequest(abortController), closeResponse(res)); try { - const filename = streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename; + let filename = streamInfo.filename; + if (streamInfo.isAudioOnly) { + filename = `${streamInfo.filename}.${streamInfo.audioFormat}` + } res.setHeader('Content-disposition', contentDisposition(filename)); const { body: stream, headers } = await request(streamInfo.urls, { @@ -60,7 +63,11 @@ export async function streamDefault(streamInfo, res) { export async function streamLiveRender(streamInfo, res) { let abortController = new AbortController(), process; - const shutdown = () => (closeRequest(abortController), killProcess(process), closeResponse(res)); + const shutdown = () => ( + closeRequest(abortController), + killProcess(process), + closeResponse(res) + ); try { if (streamInfo.urls.length !== 2) return shutdown(); @@ -72,7 +79,6 @@ export async function streamLiveRender(streamInfo, res) { let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ '-loglevel', '-8', - '-threads', `${getThreads()}`, '-i', streamInfo.urls[0], '-i', 'pipe:3', '-map', '0:v', @@ -80,7 +86,9 @@ export async function streamLiveRender(streamInfo, res) { ]; args = args.concat(ffmpegArgs[format]); - if (streamInfo.metadata) args = args.concat(metadataManager(streamInfo.metadata)); + if (streamInfo.metadata) { + args = args.concat(metadataManager(streamInfo.metadata)) + } args.push('-f', format, 'pipe:4'); process = spawn(ffmpeg, args, { @@ -115,25 +123,19 @@ export function streamAudioOnly(streamInfo, res) { try { let args = [ '-loglevel', '-8', - '-threads', `${getThreads()}`, - '-i', streamInfo.urls + '-i', streamInfo.urls, + '-vn' ] if (streamInfo.metadata) { - if (streamInfo.metadata.cover) { // currently corrupts the audio - args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0') - } else { - args.push('-vn') - } args = args.concat(metadataManager(streamInfo.metadata)) - } else { - args.push('-vn') } - let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]; args = args.concat(arg); - if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]); + if (ffmpegArgs[streamInfo.audioFormat]) { + args = args.concat(ffmpegArgs[streamInfo.audioFormat]) + } args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); process = spawn(ffmpeg, args, { @@ -162,16 +164,26 @@ export function streamVideoOnly(streamInfo, res) { try { let args = [ - '-loglevel', '-8', - '-threads', `${getThreads()}`, + '-loglevel', '-8' + ] + if (streamInfo.service === "twitter") { + args.push('-seekable', '0') + } + args.push( '-i', streamInfo.urls, '-c', 'copy' - ] - if (streamInfo.mute) args.push('-an'); - if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") args.push('-bsf:a', 'aac_adtstoasc'); + ) + if (streamInfo.mute) { + args.push('-an') + } + if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") { + args.push('-bsf:a', 'aac_adtstoasc') + } let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; - if (format === "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov'); + if (format === "mp4") { + args.push('-movflags', 'faststart+frag_keyframe+empty_moov') + } args.push('-f', format, 'pipe:3'); process = spawn(ffmpeg, args, { diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index cfb56aa8..111cb6c8 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -134,18 +134,6 @@ export function checkJSONPost(obj) { export function getIP(req) { return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip; } -export function getThreads() { - try { - if (process.env.ffmpegThreads && process.env.ffmpegThreads.length <= 3 - && (Number(process.env.ffmpegThreads) >= 0 && Number(process.env.ffmpegThreads) <= 256)) { - return process.env.ffmpegThreads - } else { - return '0' - } - } catch (e) { - return '0' - } -} export function cleanHTML(html) { let clean = html.replace(/ {4}/g, ''); clean = clean.replace(/\n/g, '');