twitter: remux all videos
- increased stream link lifespan to 90 seconds - decreased max video duration back to 3 hours
This commit is contained in:
parent
aef9b390b0
commit
5bd50fd55f
10 changed files with 78 additions and 54 deletions
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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, '');
|
||||
|
|
Loading…
Reference in a new issue