From 617e4270883c3da4e680af385548ebcf46567cfe Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 24 Apr 2024 16:44:29 +0600 Subject: [PATCH 01/20] web: add maskable icons back to manifest --- src/front/icons/maskable/128.png | Bin 0 -> 815 bytes src/front/icons/maskable/192.png | Bin 0 -> 1014 bytes src/front/icons/maskable/384.png | Bin 0 -> 1856 bytes src/front/icons/maskable/48.png | Bin 0 -> 390 bytes src/front/icons/maskable/512.png | Bin 0 -> 2828 bytes src/front/icons/maskable/72.png | Bin 0 -> 569 bytes src/front/icons/maskable/96.png | Bin 0 -> 617 bytes src/front/manifest.webmanifest | 42 +++++++++++++++++++++++++++++++ 8 files changed, 42 insertions(+) create mode 100644 src/front/icons/maskable/128.png create mode 100644 src/front/icons/maskable/192.png create mode 100644 src/front/icons/maskable/384.png create mode 100644 src/front/icons/maskable/48.png create mode 100644 src/front/icons/maskable/512.png create mode 100644 src/front/icons/maskable/72.png create mode 100644 src/front/icons/maskable/96.png diff --git a/src/front/icons/maskable/128.png b/src/front/icons/maskable/128.png new file mode 100644 index 0000000000000000000000000000000000000000..e8213cfe5828cc4435d15e4da25d5e57b3f2c472 GIT binary patch literal 815 zcmV+~1JL}5P)C0002YP)t-s00030 z|Ns5{{T3D$`T6gwv<-QBdbw3CyQgoK2BeSJ7MIN;#m$jHdIx3{aStB{b8 ziHV7HbaZ89Wll~`OG`^dMMVh-3GD3b=H}+)SUnwoKOaadSbR8&+y zKR-J=J2EmdBqSvB^YiWP?Y+IdsHmuwm6d^kfm&KxK|w*<+S%6WNt=~O%#0006W zNkl~PJx+&|Ey*O!vMtM z0l3frw1Tc_06Oak7&-Xg;QW2S#V`+SP;tw7(*UgV-4J)bEL6tXHm_6jcfe)WL#2`C z^pvCxYQZOPnYnmjEe_e1R;e}afJxF`qSEYw&U<>KR%V+}`GZQW7=tcuQS(b4F!4$* zDxC`GqGF9MM9MbdR7Pdl&bDKpT4M}c!j*R4%@WWZmegL32{75OQCUy3Z4yxH&w)uY z_33g45$FmNq*kl|mydf?-m|GNtW)~{E|o1R*Dc>xsS14TfCgL(FF%k2Y<5N8IAGFq zmQjR!I-$qjFb%Erf4HCZ}!tI^jGABIgZQgf~3>jWYa&{Am z)r<5N26}69JOE|`)Ren=`+P;>w>VzNx{WH5lAQyxuj}pI0}e=@2iX(aNa~vycsO{` ziUCXmI(Z;01Kd&!U>NZ7^Zo literal 0 HcmV?d00001 diff --git a/src/front/icons/maskable/192.png b/src/front/icons/maskable/192.png new file mode 100644 index 0000000000000000000000000000000000000000..8268d89a58c3c6334d887d403c4a9a615343c5f3 GIT binary patch literal 1014 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 nv_N+M?+@W;^HqDlV&cz;Xd literal 0 HcmV?d00001 diff --git a/src/front/icons/maskable/48.png b/src/front/icons/maskable/48.png new file mode 100644 index 0000000000000000000000000000000000000000..02a5bca0fb8b6cf17cd1327fab8f337e61c79489 GIT binary patch literal 390 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 literal 0 HcmV?d00001 diff --git a/src/front/icons/maskable/72.png b/src/front/icons/maskable/72.png new file mode 100644 index 0000000000000000000000000000000000000000..903f6bd500691990bc32f79d2192de30a594dbac GIT binary patch literal 569 zcmV-90>=G`P)U1D4i4kv<4{mg+}zyK($d1h!nwJ*b8~YFMdNuC9rRiG+lN zczAdage<5vZq|SJIvoDZf)%Lvw2I#ZK=M#Qy;1d0m_2sQ#amIqLvuF=xisi6$UIuz; zb6ur&hh5M0l9dq)!pXaUZ^FLHW>XW;P5BP`aN`?s&5l?Pn9ZZe+0xgV$Q#-C<2SNj z3;m;=H+pQcu@HK-A3tRDYEO~S*LK?Rog}FBlPt2w6wIbv9YG6rk|gmb8~ZEUS3jCQtRvM z=jZ3-z?PPllarH=kB?<#Wkp3rKtMne5)uy&58B$=&CSij!^6VD!n?b>y1Kff zqoag`giA|HIyyQyI5;36AhcV!LI3~(b4f%&RA}Dq)<>IyFc5&@45AbfM5WogYu#S| z{}0 Date: Fri, 26 Apr 2024 09:27:36 +0600 Subject: [PATCH 02/20] youtube: replace innertube client --- src/modules/processing/services/youtube.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 10e813af..8a773375 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -33,7 +33,7 @@ export default async function(o) { } try { - info = await yt.getBasicInfo(o.id, 'ANDROID'); + info = await yt.getBasicInfo(o.id, 'YTMUSIC_ANDROID'); } catch (e) { return { error: 'ErrorCantConnectToServiceAPI' }; } From 0feacf0ae5b3d816a1a9b88128d1d2e7f3ab4b8a Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 12:25:22 +0600 Subject: [PATCH 03/20] youtube: use web client and decipher urls --- src/modules/processing/services/youtube.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 8a773375..63e02eb3 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -33,7 +33,7 @@ export default async function(o) { } try { - info = await yt.getBasicInfo(o.id, 'YTMUSIC_ANDROID'); + info = await yt.getBasicInfo(o.id, 'WEB'); } catch (e) { return { error: 'ErrorCantConnectToServiceAPI' }; } @@ -43,7 +43,9 @@ export default async function(o) { if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' }; if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' }; - let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter(e => + let bestQuality, hasAudio; + + let adaptive_formats = info.streaming_data.adaptive_formats.filter(e => e.mime_type.includes(c[o.format].codec) || e.mime_type.includes(c[o.format].aCodec) ).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); @@ -96,7 +98,7 @@ export default async function(o) { if (hasAudio && o.isAudioOnly) return { type: "render", isAudioOnly: true, - urls: audio.url, + urls: audio.decipher(yt.session.player), filenameAttributes: filenameAttributes, fileMetadata: fileMetadata } @@ -108,14 +110,14 @@ export default async function(o) { if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') { match = info.streaming_data.formats.find(checkSingle); type = "bridge"; - urls = match?.url; + urls = match?.decipher(yt.session.player); } const video = adaptive_formats.find(checkRender); if (!match && video) { match = video; type = "render"; - urls = [video.url, audio.url]; + urls = [video.decipher(yt.session.player), audio.decipher(yt.session.player)]; } if (match) { From 8771b7d7d4fc854c2e0d12eb10a7bc523730fb80 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 12:25:46 +0600 Subject: [PATCH 04/20] package: bump youtubei.js version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b0b3443..dddc5a03 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ "set-cookie-parser": "2.6.0", "undici": "^6.7.0", "url-pattern": "1.0.3", - "youtubei.js": "^9.2.0" + "youtubei.js": "^9.3.0" } } From 43101b604c734827e5937695ba11584b791a78a0 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 15:07:32 +0600 Subject: [PATCH 05/20] stream/types: proper headers for all http requests & refactor --- src/modules/stream/types.js | 79 +++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 2b7d7482..d3e33438 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -5,6 +5,30 @@ import { metadataManager } from "../sub/utils.js"; import { request } from "undici"; import { create as contentDisposition } from "content-disposition-header"; +const defaultHeaders = { + 'user-agent': genericUserAgent +} +const serviceHeaders = { + bilibili: { + referer: 'https://www.bilibili.com/' + }, + youtube: { + accept: '*/*', + origin: 'https://www.youtube.com', + referer: 'https://www.youtube.com', + DNT: '?1' + } +} + +function getHeaders(service) { + return { ...defaultHeaders, ...serviceHeaders[service] } +} +function toRawHeaders(headers) { + return Object.entries(headers) + .map(([key, value]) => `${key}: ${value}\r\n`) + .join(''); +} + function closeRequest(controller) { try { controller.abort() } catch {} } @@ -53,7 +77,7 @@ export async function streamDefault(streamInfo, res) { res.setHeader('Content-disposition', contentDisposition(filename)); const { body: stream, headers } = await request(streamInfo.urls, { - headers: { 'user-agent': genericUserAgent }, + headers: getHeaders(streamInfo.service), signal: abortController.signal, maxRedirections: 16 }); @@ -68,49 +92,43 @@ export async function streamDefault(streamInfo, res) { } export async function streamLiveRender(streamInfo, res) { - let abortController = new AbortController(), process; + let process, abortController = new AbortController(); + const shutdown = () => ( closeRequest(abortController), killProcess(process), closeResponse(res) ); + const headers = getHeaders(streamInfo.service); + const rawHeaders = toRawHeaders(headers); + try { if (streamInfo.urls.length !== 2) return shutdown(); const { body: audio } = await request(streamInfo.urls[1], { - maxRedirections: 16, signal: abortController.signal, - headers: { - 'user-agent': genericUserAgent, - referer: streamInfo.service === 'bilibili' - ? 'https://www.bilibili.com/' - : undefined, - } + headers, + signal: abortController.signal, + maxRedirections: 16 }); const format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; + let args = [ '-loglevel', '-8', - '-user_agent', genericUserAgent - ]; - - if (streamInfo.service === 'bilibili') { - args.push( - '-headers', 'Referer: https://www.bilibili.com/\r\n', - ) - } - - args.push( + '-headers', rawHeaders, '-i', streamInfo.urls[0], '-i', 'pipe:3', '-map', '0:v', '-map', '1:a', - ); + ] args = args.concat(ffmpegArgs[format]); + if (streamInfo.metadata) { args = args.concat(metadataManager(streamInfo.metadata)) } + args.push('-f', format, 'pipe:4'); process = spawn(...getCommand(args), { @@ -128,6 +146,7 @@ export async function streamLiveRender(streamInfo, res) { audio.on('error', shutdown); audioInput.on('error', shutdown); + audio.pipe(audioInput); pipe(muxOutput, res, shutdown); @@ -145,13 +164,11 @@ export function streamAudioOnly(streamInfo, res) { try { let args = [ '-loglevel', '-8', - '-user_agent', genericUserAgent - ]; + '-headers', toRawHeaders(getHeaders(streamInfo.service)), + ] if (streamInfo.service === "twitter") { args.push('-seekable', '0'); - } else if (streamInfo.service === 'bilibili') { - args.push('-headers', 'Referer: https://www.bilibili.com/\r\n'); } args.push( @@ -162,12 +179,12 @@ export function streamAudioOnly(streamInfo, res) { if (streamInfo.metadata) { args = args.concat(metadataManager(streamInfo.metadata)) } - let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]; - args = args.concat(arg); + args = args.concat(ffmpegArgs[streamInfo.copy ? 'copy' : 'audio']); if (ffmpegArgs[streamInfo.audioFormat]) { args = args.concat(ffmpegArgs[streamInfo.audioFormat]) } + args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); process = spawn(...getCommand(args), { @@ -196,13 +213,12 @@ export function streamVideoOnly(streamInfo, res) { try { let args = [ - '-loglevel', '-8' + '-loglevel', '-8', + '-headers', toRawHeaders(getHeaders(streamInfo.service)), ] if (streamInfo.service === "twitter") { args.push('-seekable', '0') - } else if (streamInfo.service === 'bilibili') { - args.push('-headers', 'Referer: https://www.bilibili.com/\r\n') } args.push( @@ -222,6 +238,7 @@ export function streamVideoOnly(streamInfo, res) { if (format === "mp4") { args.push('-movflags', 'faststart+frag_keyframe+empty_moov') } + args.push('-f', format, 'pipe:3'); process = spawn(...getCommand(args), { @@ -254,10 +271,12 @@ export function convertToGif(streamInfo, res) { let args = [ '-loglevel', '-8' ] + if (streamInfo.service === "twitter") { args.push('-seekable', '0') } - args.push('-i', streamInfo.urls) + + args.push('-i', streamInfo.urls); args = args.concat(ffmpegArgs["gif"]); args.push('-f', "gif", 'pipe:3'); From 13d7ca3af441248bc866ed3adf92b29e4c6e33eb Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 06:03:05 +0600 Subject: [PATCH 06/20] servicesConfig: add support for m.bilibili.com subdomain --- src/modules/processing/servicesConfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 633fa2a6..95880129 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -7,6 +7,7 @@ "video/:comId", "_shortLink/:comShortLink", "_tv/:lang/video/:tvId", "_tv/video/:tvId" ], + "subdomains": ["m"], "enabled": true }, "reddit": { From 66e58d21ec3e7bdd4f2b38e1e36cfb8d14d6ca6b Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Fri, 26 Apr 2024 11:53:50 +0000 Subject: [PATCH 07/20] feat: internal streams --- src/core/api.js | 29 ++++++++++++------ src/modules/stream/internal.js | 24 +++++++++++++++ src/modules/stream/manage.js | 42 ++++++++++++++++++++++++++ src/modules/stream/stream.js | 3 ++ src/modules/stream/types.js | 55 ++++++++++++++++++---------------- 5 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 src/modules/stream/internal.js diff --git a/src/core/api.js b/src/core/api.js index eda3c014..9dd4b1cc 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -11,7 +11,7 @@ import { Bright, Cyan } from "../modules/sub/consoleText.js"; import stream from "../modules/stream/stream.js"; import loc from "../localization/manager.js"; import { generateHmac } from "../modules/sub/crypto.js"; -import { verifyStream } from "../modules/stream/manage.js"; +import { verifyStream, getInternalStream } from "../modules/stream/manage.js"; export function runAPI(express, app, gitCommit, gitBranch, __dirname) { const corsConfig = process.env.CORS_WILDCARD === '0' ? { @@ -123,13 +123,13 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.get('/api/:type', (req, res) => { try { + let j; switch (req.params.type) { case 'stream': const q = req.query; const checkQueries = q.t && q.e && q.h && q.s && q.i; const checkBaseLength = q.t.length === 21 && q.e.length === 13; const checkSafeLength = q.h.length === 43 && q.s.length === 43 && q.i.length === 22; - if (checkQueries && checkBaseLength && checkSafeLength) { let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i); if (streamInfo.error) { @@ -141,12 +141,23 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { }); } return stream(res, streamInfo); - } else { - let j = apiJSON(0, { - t: "bad request. stream link may be incomplete or corrupted." - }) - return res.status(j.status).json(j.body); - } + } + + j = apiJSON(0, { + t: "bad request. stream link may be incomplete or corrupted." + }) + return res.status(j.status).json(j.body); + case 'istream': + if (!req.ip.endsWith('127.0.0.1')) + return res.sendStatus(403); + if (('' + req.query.t).length !== 21) + return res.sendStatus(400); + + let streamInfo = getInternalStream(req.query.t); + if (!streamInfo) return res.sendStatus(404); + streamInfo.headers = req.headers; + + return stream(res, { type: 'internal', ...streamInfo }); case 'serverInfo': return res.status(200).json({ version: version, @@ -158,7 +169,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { startTime: `${startTimestamp}` }); default: - let j = apiJSON(0, { + j = apiJSON(0, { t: "unknown response type" }) return res.status(j.status).json(j.body); diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js new file mode 100644 index 00000000..a1c99ff5 --- /dev/null +++ b/src/modules/stream/internal.js @@ -0,0 +1,24 @@ +import { request } from 'undici' + +export async function internalStream(streamInfo, res) { + try { + const req = await request(streamInfo.url, { + headers: streamInfo.headers, + signal: streamInfo.controller.signal, + maxRedirections: 16 + }); + + res.status(req.statusCode); + + for (const [ name, value ] of Object.entries(req.headers)) + res.setHeader(name, value) + + if (req.statusCode < 200 || req.statusCode > 299) + return res.destroy(); + + req.body.pipe(res); + req.body.on('error', () => res.destroy()); + } catch { + streamInfo.controller.abort(); + } +} \ No newline at end of file diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index d4cb1e68..680fd8f9 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -4,6 +4,7 @@ import { nanoid } from 'nanoid'; import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; import { streamLifespan } from "../config.js"; +import { strict as assert } from "assert"; const streamNoAccess = { error: "i couldn't verify if you have access to this stream. go back and try again!", @@ -24,6 +25,7 @@ streamCache.on("expired", (key) => { streamCache.del(key); }) +const internalStreamCache = {}; const hmacSalt = randomBytes(64).toString('hex'); export function createStream(obj) { @@ -67,6 +69,34 @@ export function createStream(obj) { return streamLink.toString(); } +export function getInternalStream(id) { + return internalStreamCache[id]; +} + +export function createInternalStream(obj = {}) { + assert(typeof obj.url === 'string'); + + const streamID = nanoid(); + internalStreamCache[streamID] = { + url: obj.url, + controller: new AbortController() + }; + + let streamLink = new URL('/api/istream', `http://127.0.0.1:${process.env.API_PORT}`); + streamLink.searchParams.set('t', streamID); + return streamLink.toString(); +} + +export function destroyInternalStream(url) { + const id = new URL(url).searchParams.get('t'); + assert(id); + + if (internalStreamCache[id]) { + internalStreamCache[id].controller.abort(); + delete internalStreamCache[id]; + } +} + export function verifyStream(id, hmac, exp, secret, iv) { try { const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt); @@ -82,6 +112,18 @@ export function verifyStream(id, hmac, exp, secret, iv) { if (Number(exp) <= new Date().getTime()) return streamNoExist; + if (!streamInfo.originalUrls) { + streamInfo.originalUrls = streamInfo.urls; + } + + if (typeof streamInfo.originalUrls === 'string') { + streamInfo.urls = createInternalStream({ url: streamInfo.originalUrls }); + } else if (Array.isArray(streamInfo.originalUrls)) { + for (const idx in streamInfo.originalUrls) { + streamInfo.originalUrls[idx] = createInternalStream({ url: streamInfo.originalUrls[idx] }); + } + } else throw 'invalid urls'; + return streamInfo; } catch (e) { diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index f254dacc..0b9ba42c 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -1,4 +1,5 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js"; +import { internalStream } from './internal.js' export default async function(res, streamInfo) { try { @@ -7,6 +8,8 @@ export default async function(res, streamInfo) { return; } switch (streamInfo.type) { + case "internal": + return await internalStream(streamInfo, res); case "render": await streamLiveRender(streamInfo, res); break; diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index d3e33438..10bd3a66 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -1,10 +1,12 @@ -import { spawn } from "child_process"; -import ffmpeg from "ffmpeg-static"; -import { ffmpegArgs, genericUserAgent } from "../config.js"; -import { metadataManager } from "../sub/utils.js"; import { request } from "undici"; +import ffmpeg from "ffmpeg-static"; +import { spawn } from "child_process"; import { create as contentDisposition } from "content-disposition-header"; +import { metadataManager } from "../sub/utils.js"; +import { destroyInternalStream } from "./manage.js"; +import { ffmpegArgs, genericUserAgent } from "../config.js"; + const defaultHeaders = { 'user-agent': genericUserAgent } @@ -67,7 +69,11 @@ function getCommand(args) { export async function streamDefault(streamInfo, res) { const abortController = new AbortController(); - const shutdown = () => (closeRequest(abortController), closeResponse(res)); + const shutdown = () => ( + closeRequest(abortController), + closeResponse(res), + destroyInternalStream(streamInfo.urls) + ); try { let filename = streamInfo.filename; @@ -91,13 +97,12 @@ export async function streamDefault(streamInfo, res) { } } -export async function streamLiveRender(streamInfo, res) { - let process, abortController = new AbortController(); - +export function streamLiveRender(streamInfo, res) { + let process; const shutdown = () => ( - closeRequest(abortController), killProcess(process), - closeResponse(res) + closeResponse(res), + streamInfo.urls.map(destroyInternalStream) ); const headers = getHeaders(streamInfo.service); @@ -106,19 +111,13 @@ export async function streamLiveRender(streamInfo, res) { try { if (streamInfo.urls.length !== 2) return shutdown(); - const { body: audio } = await request(streamInfo.urls[1], { - headers, - signal: abortController.signal, - maxRedirections: 16 - }); - const format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; let args = [ '-loglevel', '-8', '-headers', rawHeaders, '-i', streamInfo.urls[0], - '-i', 'pipe:3', + '-i', streamInfo.urls[1], '-map', '0:v', '-map', '1:a', ] @@ -129,25 +128,21 @@ export async function streamLiveRender(streamInfo, res) { args = args.concat(metadataManager(streamInfo.metadata)) } - args.push('-f', format, 'pipe:4'); + args.push('-f', format, 'pipe:3'); process = spawn(...getCommand(args), { windowsHide: true, stdio: [ 'inherit', 'inherit', 'inherit', - 'pipe', 'pipe' + 'pipe' ], }); - const [,,, audioInput, muxOutput] = process.stdio; + const [,,, muxOutput] = process.stdio; res.setHeader('Connection', 'keep-alive'); res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename)); - audio.on('error', shutdown); - audioInput.on('error', shutdown); - - audio.pipe(audioInput); pipe(muxOutput, res, shutdown); process.on('close', shutdown); @@ -159,7 +154,11 @@ export async function streamLiveRender(streamInfo, res) { export function streamAudioOnly(streamInfo, res) { let process; - const shutdown = () => (killProcess(process), closeResponse(res)); + const shutdown = () => ( + killProcess(process), + closeResponse(res), + destroyInternalStream(streamInfo.urls) + ); try { let args = [ @@ -209,7 +208,11 @@ export function streamAudioOnly(streamInfo, res) { export function streamVideoOnly(streamInfo, res) { let process; - const shutdown = () => (killProcess(process), closeResponse(res)); + const shutdown = () => ( + killProcess(process), + closeResponse(res), + destroyInternalStream(streamInfo.urls) + ); try { let args = [ From 5f1dc89c42e98be19a9472ebcbb0f6b9bbca1e70 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 10:47:09 +0000 Subject: [PATCH 08/20] stream/types: attempt to pass through headers only if they exist --- src/modules/stream/types.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 10bd3a66..b5320003 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -88,8 +88,11 @@ export async function streamDefault(streamInfo, res) { maxRedirections: 16 }); - res.setHeader('content-type', headers['content-type']); - res.setHeader('content-length', headers['content-length']); + for (const headerName of ['content-type', 'content-length']) { + if (headers[headerName]) { + res.setHeader(headerName, headers[headerName]); + } + } pipe(stream, res, shutdown); } catch { From ec746f57a738cd85d2f43d8d543cb6329e4dfbb0 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 10:48:22 +0000 Subject: [PATCH 09/20] stream/manage: pass service name to internal stream --- src/modules/stream/manage.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 680fd8f9..8b6b8c62 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -79,6 +79,7 @@ export function createInternalStream(obj = {}) { const streamID = nanoid(); internalStreamCache[streamID] = { url: obj.url, + service: obj.service, controller: new AbortController() }; @@ -117,10 +118,16 @@ export function verifyStream(id, hmac, exp, secret, iv) { } if (typeof streamInfo.originalUrls === 'string') { - streamInfo.urls = createInternalStream({ url: streamInfo.originalUrls }); + streamInfo.urls = createInternalStream({ + url: streamInfo.originalUrls, + ...streamInfo + }); } else if (Array.isArray(streamInfo.originalUrls)) { for (const idx in streamInfo.originalUrls) { - streamInfo.originalUrls[idx] = createInternalStream({ url: streamInfo.originalUrls[idx] }); + streamInfo.originalUrls[idx] = createInternalStream({ + url: streamInfo.originalUrls[idx], + ...streamInfo + }); } } else throw 'invalid urls'; From 49eaa7d4ed2df843750ba429e695072df62f4e9d Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 10:59:27 +0000 Subject: [PATCH 10/20] stream: extract headers to shared file --- src/modules/stream/shared.js | 21 +++++++++++++++++++++ src/modules/stream/types.js | 21 ++------------------- 2 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 src/modules/stream/shared.js diff --git a/src/modules/stream/shared.js b/src/modules/stream/shared.js new file mode 100644 index 00000000..2f898c52 --- /dev/null +++ b/src/modules/stream/shared.js @@ -0,0 +1,21 @@ +import { genericUserAgent } from "../config.js"; + +const defaultHeaders = { + 'user-agent': genericUserAgent +} + +const serviceHeaders = { + bilibili: { + referer: 'https://www.bilibili.com/' + }, + youtube: { + accept: '*/*', + origin: 'https://www.youtube.com', + referer: 'https://www.youtube.com', + DNT: '?1' + } +} + +export function getHeaders(service) { + return { ...defaultHeaders, ...serviceHeaders[service] } +} \ No newline at end of file diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index b5320003..c8873381 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -5,26 +5,9 @@ import { create as contentDisposition } from "content-disposition-header"; import { metadataManager } from "../sub/utils.js"; import { destroyInternalStream } from "./manage.js"; -import { ffmpegArgs, genericUserAgent } from "../config.js"; +import { ffmpegArgs } from "../config.js"; +import { getHeaders } from "./shared.js"; -const defaultHeaders = { - 'user-agent': genericUserAgent -} -const serviceHeaders = { - bilibili: { - referer: 'https://www.bilibili.com/' - }, - youtube: { - accept: '*/*', - origin: 'https://www.youtube.com', - referer: 'https://www.youtube.com', - DNT: '?1' - } -} - -function getHeaders(service) { - return { ...defaultHeaders, ...serviceHeaders[service] } -} function toRawHeaders(headers) { return Object.entries(headers) .map(([key, value]) => `${key}: ${value}\r\n`) From 6eb4af125bf0eb32506ff561f56a9016135e5cc3 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 11:00:45 +0000 Subject: [PATCH 11/20] stream/internal: special youtube stream handling --- src/modules/stream/internal.js | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index a1c99ff5..449be22e 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -1,6 +1,76 @@ import { request } from 'undici' +import { Readable } from 'node:stream' +import { assert } from 'console' +import { getHeaders } from './shared.js' + +const CHUNK_SIZE = BigInt(8e6); // 8 MB +const min = (a, b) => a < b ? a : b; + +async function* readChunks(streamInfo, size) { + let read = 0n; + while (read < size) { + if (streamInfo.controller.signal.aborted) { + throw new Error("controller aborted"); + } + + const chunk = await request(streamInfo.url, { + headers: { + ...getHeaders('youtube'), + Range: `bytes=${read}-${read + CHUNK_SIZE}` + }, + signal: streamInfo.controller.signal + }); + + const expected = min(CHUNK_SIZE, size - read); + const received = BigInt(chunk.headers['content-length']); + + if (received < expected / 2n) { + streamInfo.controller.abort(); + } + + for await (const data of chunk.body) { + yield data; + } + + read += received; + } +} + +function chunkedStream(streamInfo, size) { + assert(streamInfo.controller instanceof AbortController); + const stream = Readable.from(readChunks(streamInfo, size)); + return stream; +} + +async function handleYoutubeStream(streamInfo, res) { + try { + const req = await fetch(streamInfo.url, { + headers: getHeaders('youtube'), + method: 'HEAD', + signal: streamInfo.controller.signal + }); + + streamInfo.url = req.url; + const size = BigInt(req.headers.get('content-length')); + + if (req.status !== 200 || !size) + return res.destroy(); + + const stream = chunkedStream(streamInfo, size); + + res.setHeader('content-type', req.headers.get('content-type')); + stream.pipe(res); + stream.on('error', () => res.destroy()); + } catch { + res.destroy(); + } +} export async function internalStream(streamInfo, res) { + if (streamInfo.service === 'youtube') { + return handleYoutubeStream(streamInfo, res); + } + try { const req = await request(streamInfo.url, { headers: streamInfo.headers, From 3d3a717f3ef519223460cb86639583e06bc4330e Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 11:10:26 +0000 Subject: [PATCH 12/20] stream/internal: also copy content-length where applicable --- src/modules/stream/internal.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index 449be22e..db39fb05 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -58,7 +58,11 @@ async function handleYoutubeStream(streamInfo, res) { const stream = chunkedStream(streamInfo, size); - res.setHeader('content-type', req.headers.get('content-type')); + for (const headerName of ['content-type', 'content-length']) { + const headerValue = req.headers.get(headerName); + if (headerValue) res.setHeader(headerName, headerValue); + } + stream.pipe(res); stream.on('error', () => res.destroy()); } catch { From dd56ae60e76b37801746bad74a6a479f393dc084 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 11:30:16 +0000 Subject: [PATCH 13/20] stream/internal: don't copy Host header from request its basically always gonna be localhost:9k --- src/modules/stream/internal.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index db39fb05..75e18ece 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -77,7 +77,10 @@ export async function internalStream(streamInfo, res) { try { const req = await request(streamInfo.url, { - headers: streamInfo.headers, + headers: { + ...streamInfo.headers, + host: undefined + }, signal: streamInfo.controller.signal, maxRedirections: 16 }); From 66b3697b24a241752c8380a6be625ae87255abcd Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:05:43 +0600 Subject: [PATCH 14/20] youtube: update stub handling --- src/modules/processing/services/youtube.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 63e02eb3..a844f976 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -23,7 +23,9 @@ const c = { } export default async function(o) { - let info, isDubbed, quality = o.quality === "max" ? "9000" : o.quality; //set quality 9000(p) to be interpreted as max + let info, isDubbed, + quality = o.quality === "max" ? "9000" : o.quality; // 9000(p) - max quality + function qual(i) { if (!i.quality_label) { return; @@ -43,6 +45,15 @@ export default async function(o) { if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' }; if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' }; + // return a critical error if returned video is "Video Not Available" + // or a similar stub by youtube + if (info.basic_info.id !== o.id) { + return { + error: 'ErrorCantConnectToServiceAPI', + critical: true + } + } + let bestQuality, hasAudio; let adaptive_formats = info.streaming_data.adaptive_formats.filter(e => @@ -89,12 +100,6 @@ export default async function(o) { youtubeDubName: isDubbed ? o.dubLang : false } - if (filenameAttributes.title === "Video Not Available" && filenameAttributes.author === "YouTube Viewers") - return { - error: 'ErrorCantConnectToServiceAPI', - critical: true - } - if (hasAudio && o.isAudioOnly) return { type: "render", isAudioOnly: true, From d09e6a311059d8a699e4273a0bd802bf775e92d6 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:42:45 +0600 Subject: [PATCH 15/20] localization: update strings related to youtube --- src/localization/languages/en.json | 7 +++---- src/localization/languages/ru.json | 11 +++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 546c2841..7d468cc8 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -101,10 +101,10 @@ "FollowSupport": "keep in touch with cobalt for news, support, and more:", "SourceCode": "explore source code, report issues, 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 rendering, then data about requested content is encrypted and temporarily stored in server's RAM. it's necessary for this feature to function.\n\nencrypted data is stored for 90 seconds and then permanently removed.\n\nstored data is only possible to decrypt with unique encryption keys from your download link. furthermore, the official cobalt codebase doesn't provide a way to read temporarily stored data 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 acts unexpectedly. try again or try another settings.", + "ErrorYTUnavailable": "this youtube video is unavailable. it could be age or region restricted. try another one!", + "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality in settings!", "SettingsCodecSubtitle": "youtube codec", - "SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: poor player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.", + "SettingsCodecDescription": "h264: best support across apps/platforms, average detail level. max quality is 1080p.\nav1: best quality, small file size, most detail. supports 8k & HDR.\nvp9: same quality as av1, but file is x2 bigger. supports 4k & HDR.\n\npick h264 if you want best compatibility.\n\npick av1 if you want best quality and efficiency.", "SettingsAudioDub": "youtube audio track", "SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and cobalt) language is used.", "SettingsDubDefault": "original", @@ -113,7 +113,6 @@ "SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by cobalt into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.", "ShareURL": "share", "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.", "PopupCloseDone": "done", "Accessibility": "accessibility", "SettingsReduceTransparency": "reduce transparency", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 8f66b5b0..a1695553 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -102,11 +102,11 @@ "FollowSupport": "подписывайся на соц.сети кобальта для новостей и поддержки:", "SourceCode": "шарься в исходнике, пиши о проблемах, или же форкай репозиторий:", "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется рендер, то зашифрованные данные о ней временно хранятся в ОЗУ сервера. это необходимо для работы данной функции.\n\nзашифрованные данные хранятся в течение 90 секунд и затем безвозвратно удаляются.\n\ncохранённые данные можно расшифровать только с помощью уникальных ключей шифрования из твоей ссылки на скачивание. кроме того, официальная кодовая база кобальта не предусматривает возможности чтения эти данные вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", - "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", - "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", - "SettingsCodecSubtitle": "кодек для видео с youtube", - "SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями.", - "SettingsAudioDub": "звуковая дорожка для видео с youtube", + "ErrorYTUnavailable": "это видео недоступно. возможно оно ограничено по доступу или региону. попробуй другое!", + "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество в настройках!", + "SettingsCodecSubtitle": "кодек для youtube видео", + "SettingsCodecDescription": "h264: лучшая совместимость, средний уровень детализированности. максимальное качество - 1080p.\nav1: лучшее качество, маленький размер файла, наибольшее количество деталей. поддерживает 8k и HDR.\nvp9: такая же детализированность, как и у av1, но файл в 2 раза больше. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость.\nвыбирай av1, если ты хочешь лучшее качество и эффективность.", + "SettingsAudioDub": "звуковая дорожка для youtube видео", "SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.", "SettingsDubDefault": "оригинал", "SettingsDubAuto": "авто", @@ -114,7 +114,6 @@ "SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: кобальт совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, кобальт скачает \"dash\".", "ShareURL": "поделиться", "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость ограничена. попробуй другой!", - "ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.", "PopupCloseDone": "готово", "Accessibility": "общедоступность", "SettingsReduceTransparency": "уменьшить прозрачность", From 656c0a34955aba6510db8706145ef1749944f6e8 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:51:12 +0600 Subject: [PATCH 16/20] stream: add semicolons to imports --- src/modules/stream/internal.js | 8 ++++---- src/modules/stream/stream.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index 75e18ece..412ba546 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -1,7 +1,7 @@ -import { request } from 'undici' -import { Readable } from 'node:stream' -import { assert } from 'console' -import { getHeaders } from './shared.js' +import { request } from 'undici'; +import { Readable } from 'node:stream'; +import { assert } from 'console'; +import { getHeaders } from './shared.js'; const CHUNK_SIZE = BigInt(8e6); // 8 MB const min = (a, b) => a < b ? a : b; diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index 0b9ba42c..3de1cb3e 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -1,5 +1,5 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js"; -import { internalStream } from './internal.js' +import { internalStream } from './internal.js'; export default async function(res, streamInfo) { try { @@ -24,7 +24,7 @@ export default async function(res, streamInfo) { await streamDefault(streamInfo, res); break; } - } catch (e) { + } catch { res.status(500).json({ status: "error", text: "Internal Server Error" }); } } From d27366dc8aca236985c782d422caf75a6e6a4e40 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:58:03 +0600 Subject: [PATCH 17/20] stream/manage: remove unnecessary variable from catch --- src/modules/stream/manage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 8b6b8c62..03821a8b 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -133,7 +133,7 @@ export function verifyStream(id, hmac, exp, secret, iv) { return streamInfo; } - catch (e) { + catch { return { error: "something went wrong and i couldn't verify this stream. go back and try again!", status: 500 From d4d2f0a6f1d31b422500242a93a84f68f29e8b5e Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 19:02:05 +0600 Subject: [PATCH 18/20] package: bump version to 7.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dddc5a03..a2c270fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.12.6", + "version": "7.13", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From c86e209e55fe21443fd83c6c27c5b7ff073e2033 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 20:29:30 +0600 Subject: [PATCH 19/20] pinterest: fix video link parsing --- src/modules/processing/services/pinterest.js | 21 +++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/modules/processing/services/pinterest.js b/src/modules/processing/services/pinterest.js index 0f14eebf..2364b729 100644 --- a/src/modules/processing/services/pinterest.js +++ b/src/modules/processing/services/pinterest.js @@ -1,22 +1,16 @@ import { genericUserAgent } from "../../config.js"; -const videoLinkBase = { - "regular": "https://v1.pinimg.com/videos/mc/720p/", - "story": "https://v1.pinimg.com/videos/mc/720p/" -} +const linkRegex = /"url":"(https:\/\/v1.pinimg.com\/videos\/.*?)"/g; export default async function(o) { - let id = o.id, type = "regular"; + let id = o.id; 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.includes("--")) { - id = id.split("--")[1]; - type = "story"; - } + if (id.includes("--")) id = id.split("--")[1]; if (!id) return { error: 'ErrorCouldntFetch' }; let html = await fetch(`https://www.pinterest.com/pin/${id}/`, { @@ -25,11 +19,14 @@ export default async function(o) { if (!html) return { error: 'ErrorCouldntFetch' }; - let videoLink = html.split(`"url":"${videoLinkBase[type]}`)[1]?.split('"')[0]; - if (!html.includes(videoLink)) return { error: 'ErrorEmptyDownload' }; + let videoLink = [...html.matchAll(linkRegex)] + .map(([, link]) => link) + .filter(a => a.endsWith('.mp4') && a.includes('720p'))[0]; + + if (!videoLink) return { error: 'ErrorEmptyDownload' }; return { - urls: `${videoLinkBase[type]}${videoLink}`, + urls: videoLink, filename: `pinterest_${o.id}.mp4`, audioFilename: `pinterest_${o.id}_audio` } From 291a3c2e53c476fab1da303681bf2527b14acb1d Mon Sep 17 00:00:00 2001 From: KwiatekMiki <79092746+KwiatekMiki@users.noreply.github.com> Date: Sat, 27 Apr 2024 16:37:24 +0200 Subject: [PATCH 20/20] servicesConfig: add support for /channels/uploader/id vimeo links (#459) added support for /channels/uploader/id vimeo links closes https://github.com/wukko/cobalt/issues/458 --- src/modules/processing/servicesConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 95880129..1a51d17a 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -67,7 +67,7 @@ "enabled": false }, "vimeo": { - "patterns": [":id", "video/:id", ":id/:password"], + "patterns": [":id", "video/:id", ":id/:password", "/channels/:user/:id"], "enabled": true, "bestAudio": "mp3" },