twitter: remux all videos

- increased stream link lifespan to 90 seconds
- decreased max video duration back to 3 hours
This commit is contained in:
wukko 2023-12-02 20:44:19 +06:00
parent aef9b390b0
commit 5bd50fd55f
10 changed files with 78 additions and 54 deletions

View file

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

View file

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

View file

@ -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 <span class=\"text-backdrop\">20 seconds</span> 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 <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> 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 <span class=\"text-backdrop\">90 seconds</span> 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 <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> 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",

View file

@ -106,7 +106,7 @@
"FollowSupport": "подписывайся на соц.сети кобальта для новостей, поддержки, участия в опросах, и многого другого:",
"SupportNote": "так как я занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.",
"SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:",
"PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение <span class=\"text-backdrop\">20 секунд</span>. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.",
"PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение <span class=\"text-backdrop\">90 секунд</span>. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.",
"ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!",
"ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!",
"SettingsCodecSubtitle": "кодек для видео с youtube",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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