From 3432c914826f577132c3f0c03f8829e98a8e7d3c Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 9 Feb 2023 20:45:17 +0600 Subject: [PATCH 01/11] refactoring & fixes - added duration check to vimeo module - fixed quality picking in vimeo module for progressive video type - dropping requests from ie users instead of redirecting - probably something else but i forgot to be honest --- package.json | 2 +- src/cobalt.js | 29 +++-- src/config.json | 7 +- src/modules/api.js | 57 ++++++---- src/modules/build.js | 4 +- src/modules/config.js | 1 - src/modules/services/bilibili.js | 41 ++++--- src/modules/services/reddit.js | 43 +++++--- src/modules/services/soundcloud.js | 109 +++++++++--------- src/modules/services/tiktok.js | 47 ++++---- src/modules/services/tumblr.js | 9 +- src/modules/services/twitter.js | 84 +++++++------- src/modules/services/vimeo.js | 82 +++++++------- src/modules/services/vk.js | 78 +++++++------ src/modules/services/youtube.js | 165 ++++++++++++++-------------- src/modules/setup.js | 21 ++-- src/modules/stream/manage.js | 14 +-- src/modules/stream/selectQuality.js | 6 +- src/modules/stream/stream.js | 34 +++--- src/modules/stream/types.js | 66 +++++------ src/modules/sub/utils.js | 33 +++--- 21 files changed, 479 insertions(+), 453 deletions(-) diff --git a/package.json b/package.json index f0772552..f3387dcf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "4.8", + "version": "4.9-dev", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/cobalt.js b/src/cobalt.js index 74004f91..7512db78 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -6,7 +6,7 @@ import * as fs from "fs"; import rateLimit from "express-rate-limit"; import { shortCommit } from "./modules/sub/currentCommit.js"; -import { appName, genericUserAgent, version, internetExplorerRedirect } from "./modules/config.js"; +import { appName, genericUserAgent, version } from "./modules/config.js"; import { getJSON } from "./modules/api.js"; import renderPage from "./modules/pageRender/page.js"; import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js"; @@ -57,6 +57,13 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && } next(); }); + app.use((req, res, next) => { + if (req.header("user-agent") && req.header("user-agent").includes("Trident")) { + res.destroy() + } + next(); + }); + app.use('/api/json', express.json({ verify: (req, res, buf) => { try { @@ -150,20 +157,12 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && res.redirect('/api/json') }); app.get("/", (req, res) => { - if (req.header("user-agent") && req.header("user-agent").includes("Trident")) { - if (internetExplorerRedirect.newNT.includes(req.header("user-agent").split('NT ')[1].split(';')[0])) { - res.redirect(internetExplorerRedirect.new) - } else { - res.redirect(internetExplorerRedirect.old) - } - } else { - res.send(renderPage({ - "hash": commitHash, - "type": "default", - "lang": languageCode(req), - "useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent - })) - } + res.send(renderPage({ + "hash": commitHash, + "type": "default", + "lang": languageCode(req), + "useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent + })) }); app.get("/favicon.ico", (req, res) => { res.redirect('/icons/favicon.ico'); diff --git a/src/config.json b/src/config.json index 4ce8c81f..fe59d601 100644 --- a/src/config.json +++ b/src/config.json @@ -2,7 +2,7 @@ "streamLifespan": 120000, "maxVideoDuration": 7500000, "maxAudioDuration": 7500000, - "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", + "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "authorInfo": { "name": "wukko", "link": "https://wukko.me/", @@ -18,11 +18,6 @@ } } }, - "internetExplorerRedirect": { - "newNT": ["6.1", "6.2", "6.3", "10.0"], - "old": "https://mypal-browser.org/", - "new": "https://www.mozilla.org/firefox/new/" - }, "donations": { "crypto": { "bitcoin": "bc1q59jyyjvrzj4c22rkk3ljeecq6jmpyscgz9spnd", diff --git a/src/modules/api.js b/src/modules/api.js index 70f84016..9a6db6c8 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -10,32 +10,43 @@ import match from "./processing/match.js"; export async function getJSON(originalURL, lang, obj) { try { let url = decodeURIComponent(originalURL); - if (!url.includes('http://')) { - let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), - host = hostname[hostname.length - 2], - patternMatch; - if (host === "youtu") { + if (url.startsWith('http://')) { + return apiJSON(0, { t: errorUnsupported(lang) }); + } + let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), + host = hostname[hostname.length - 2], + patternMatch; + + // TO-DO: bring all tests into one unified module instead of placing them in several places + switch(host) { + case "youtu": host = "youtube"; url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`; - } - if (host === "goo" && url.substring(0, 30) === "https://soundcloud.app.goo.gl/") { - host = "soundcloud" - url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}` - } - if (host === "tumblr" && !url.includes("blog/view")) { - if (url.slice(-1) == '/') url = url.slice(0, -1); - url = url.replace(url.split('/')[5], ''); - } - if (host && host.length < 20 && host in patterns && patterns[host]["enabled"]) { - for (let i in patterns[host]["patterns"]) { - patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]); - if (patternMatch) break; + break; + case "goo": + if (url.substring(0, 30) === "https://soundcloud.app.goo.gl/"){ + host = "soundcloud" + url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}` } - if (patternMatch) { - return await match(host, patternMatch, url, lang, obj); - } else return apiJSON(0, { t: errorUnsupported(lang) }); - } else return apiJSON(0, { t: errorUnsupported(lang) }); - } else return apiJSON(0, { t: errorUnsupported(lang) }); + break; + case "tumblr": + if (!url.includes("blog/view")) { + if (url.slice(-1) == '/') url = url.slice(0, -1); + url = url.replace(url.split('/')[5], ''); + } + break; + } + if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) { + return apiJSON(0, { t: errorUnsupported(lang) }); + } + for (let i in patterns[host]["patterns"]) { + patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]); + if (patternMatch) break; + } + if (!patternMatch) { + return apiJSON(0, { t: errorUnsupported(lang) }); + } + return await match(host, patternMatch, url, lang, obj); } catch (e) { return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }); } diff --git a/src/modules/build.js b/src/modules/build.js index fdeebdab..0dd7279e 100644 --- a/src/modules/build.js +++ b/src/modules/build.js @@ -4,9 +4,9 @@ export async function buildFront() { try { await esbuild.build({ entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'], - outdir: `min/`, + outdir: 'min/', minify: true, - loader: { ".js": "js", ".css": "css" } + loader: { '.js': 'js', '.css': 'css' } }) } catch (e) { return; diff --git a/src/modules/config.js b/src/modules/config.js index 8f27e950..82a109ee 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -15,7 +15,6 @@ export const repo = packageJson["bugs"]["url"].replace('/issues', ''), authorInfo = config.authorInfo, quality = config.quality, - internetExplorerRedirect = config.internetExplorerRedirect, donations = config.donations, ffmpegArgs = config.ffmpegArgs, supportedAudio = config.supportedAudio, diff --git a/src/modules/services/bilibili.js b/src/modules/services/bilibili.js index 5d1902f7..17d12f15 100644 --- a/src/modules/services/bilibili.js +++ b/src/modules/services/bilibili.js @@ -1,28 +1,37 @@ import { genericUserAgent, maxVideoDuration } from "../config.js"; +// TO-DO: quality picking export default async function(obj) { try { let html = await fetch(`https://bilibili.com/video/${obj.id}`, { headers: {"user-agent": genericUserAgent} - }).then((r) => {return r.text()}).catch(() => {return false}); - if (!html) return { error: 'ErrorCouldntFetch' }; + }).then((r) => { return r.text() }).catch(() => { return false }); + if (!html) { + return { error: 'ErrorCouldntFetch' }; + } - if (html.includes('')[0]); - if (streamData.data.timelength <= maxVideoDuration) { - let video = streamData["data"]["dash"]["video"].filter((v) => { - if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; - }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); - let audio = streamData["data"]["dash"]["audio"].filter((a) => { - if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; - }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); - return { urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], time: streamData.data.timelength, audioFilename: `bilibili_${obj.id}_audio`, filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` }; - } else { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - } else { + if (!(html.includes('')[0]); + if (streamData.data.timelength > maxVideoDuration) { + return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + } + + let video = streamData["data"]["dash"]["video"].filter((v) => { + if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; + }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); + + let audio = streamData["data"]["dash"]["audio"].filter((a) => { + if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; + }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); + + return { + urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], + time: streamData.data.timelength, + audioFilename: `bilibili_${obj.id}_audio`, + filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` + }; } catch (e) { return { error: 'ErrorBadFetch' }; } diff --git a/src/modules/services/reddit.js b/src/modules/services/reddit.js index 2323a028..3a8fe25f 100644 --- a/src/modules/services/reddit.js +++ b/src/modules/services/reddit.js @@ -1,26 +1,39 @@ import { maxVideoDuration } from "../config.js"; +// TO-DO: add support for gifs (#80) export default async function(obj) { try { - let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => {return r.json()}).catch(() => {return false}); - if (!data) return { error: 'ErrorCouldntFetch' }; + let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => { return r.json() }).catch(() => { return false }); + if (!data) { + return { error: 'ErrorCouldntFetch' }; + } data = data[0]["data"]["children"][0]["data"]; - if ("reddit_video" in data["secure_media"] && data["secure_media"]["reddit_video"]["duration"] * 1000 < maxVideoDuration) { - let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0], - audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`; - - await fetch(audio, {method: "HEAD"}).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''}); - - let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3] - if (audio.length > 0) { - return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` }; - } else { - return { typeId: 1, urls: video }; - } - } else { + if (!"reddit_video" in data["secure_media"]) { return { error: 'ErrorEmptyDownload' }; } + if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) { + return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + } + let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0], + audio = video.match('.mp4') + ? `${video.split('_')[0]}_audio.mp4` + : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`; + + await fetch(audio, {method: "HEAD"}).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''}); + + let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]; + + if (!audio.length > 0) { + return { typeId: 1, urls: video }; + } + return { + typeId: 2, + type: "render", + urls: [video, audio], + audioFilename: `reddit_${id}_audio`, + filename: `reddit_${id}.mp4` + }; } catch (err) { return { error: 'ErrorBadFetch' }; } diff --git a/src/modules/services/soundcloud.js b/src/modules/services/soundcloud.js index af26a92c..e6026cb5 100644 --- a/src/modules/services/soundcloud.js +++ b/src/modules/services/soundcloud.js @@ -4,32 +4,31 @@ let cachedID = {} async function findClientID() { try { - let sc = await fetch('https://soundcloud.com/').then((r) => {return r.text()}).catch(() => {return false}); - let sc_version = String(sc.match(/')[0]) + if (!json["media"]["transcodings"]) { + return { error: 'ErrorEmptyDownload' } + } + let clientId = await findClientID(); + if (!clientId) { + return { error: 'ErrorSoundCloudNoClientId' } + } + let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive") + let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; + if (!fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") { + return { error: 'ErrorEmptyDownload' } + } + if (json.duration > maxAudioDuration) { + return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] } + } + let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false }); + if (!file) { + return { error: 'ErrorCouldntFetch' }; + } + return { + urls: file, + audioFilename: `soundcloud_${json.id}`, + fileMetadata: { + title: json.title, + artist: json.user.username, + } } - if (!html) return { error: 'ErrorCouldntFetch'}; - if (html.includes('')[0]) - if (json["media"]["transcodings"]) { - let clientId = await findClientID(); - if (clientId) { - let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive") - let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; - if (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") { - if (json.duration < maxAudioDuration) { - let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false}); - if (!file) return { error: 'ErrorCouldntFetch' }; - return { - urls: file, - audioFilename: `soundcloud_${json.id}`, - fileMetadata: { - title: json.title, - artist: json.user.username, - } - } - } else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] } - } - } else return { error: 'ErrorSoundCloudNoClientId' } - } else return { error: 'ErrorEmptyDownload' } - } else return { error: ['ErrorBrokenLink', 'soundcloud'] } } catch (e) { return { error: 'ErrorBadFetch' }; } diff --git a/src/modules/services/tiktok.js b/src/modules/services/tiktok.js index ec1b1ce9..ab30311f 100644 --- a/src/modules/services/tiktok.js +++ b/src/modules/services/tiktok.js @@ -12,18 +12,18 @@ let config = { } } function selector(j, h, id) { - if (j) { - let t; - switch (h) { - case "tiktok": - t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true }) - break; - case "douyin": - t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true }) - break; - } - if (t.length > 0) { return t[0] } else return false - } else return false + if (!j) return false + let t; + switch (h) { + case "tiktok": + t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true }) + break; + case "douyin": + t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true }) + break; + } + if (!t.length > 0) return false + return t[0] } export default async function(obj) { @@ -32,7 +32,7 @@ export default async function(obj) { let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, { redirect: "manual", headers: { "user-agent": userAgent } - }).then((r) => {return r.text()}).catch(() => {return false}); + }).then((r) => { return r.text() }).catch(() => { return false }); if (!html) return { error: 'ErrorCouldntFetch' }; if (html.slice(0, 17) === ' {return r.json()}).catch(() => {return false}); + }).then((r) => { return r.json() }).catch(() => { return false }); detail = selector(detail, obj.host, obj.postId); @@ -60,20 +62,19 @@ export default async function(obj) { images = detail["images"] ? detail["images"] : false } if (!obj.isAudioOnly && !images) { - video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play"); - videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark - if (!obj.noWatermark) { - video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0] - videoFilename = `${filenameBase}_video.mp4` + video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0] + videoFilename = `${filenameBase}_video.mp4` + if (obj.noWatermark) { + video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play"); + videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark } } else { let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0]; + audio = fallback; + audioFilename = `${filenameBase}_audio_fv`; // fv - from video if (obj.fullAudio || fallback.includes("music")) { audio = detail["music"]["play_url"]["url_list"][0] audioFilename = `${filenameBase}_audio` - } else { - audio = fallback - audioFilename = `${filenameBase}_audio_fv` // fv - from video } if (audio.slice(-4) === ".mp3") isMp3 = true; } diff --git a/src/modules/services/tumblr.js b/src/modules/services/tumblr.js index e0bde0a1..372388ac 100644 --- a/src/modules/services/tumblr.js +++ b/src/modules/services/tumblr.js @@ -5,11 +5,12 @@ export default async function(obj) { let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', ''); let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, { headers: {"user-agent": genericUserAgent} - }).then((r) => {return r.text()}).catch(() => {return false}); + }).then((r) => { return r.text() }).catch(() => { return false }); if (!html) return { error: 'ErrorCouldntFetch' }; - if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) { - return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, audioFilename: `tumblr_${obj.id}_audio` } - } else return { error: 'ErrorEmptyDownload' } + if (!html.includes('property="og:video" content="https://va.media.tumblr.com/')) { + return { error: 'ErrorEmptyDownload' } + } + return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, audioFilename: `tumblr_${obj.id}_audio` } } catch (e) { return { error: 'ErrorBadFetch' }; } diff --git a/src/modules/services/twitter.js b/src/modules/services/twitter.js index c9766c2e..32e7918a 100644 --- a/src/modules/services/twitter.js +++ b/src/modules/services/twitter.js @@ -36,23 +36,22 @@ export default async function(obj) { req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); } if (!req_status) return { error: 'ErrorCouldntFetch' } - if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) { - let single, multiple = [], media = req_status["extended_entities"]["media"]; - media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true }) - if (media.length > 1) { - for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } - } else if (media.length > 0) { - single = bestQuality(media[0]["video_info"]["variants"]) - } else { - return { error: 'ErrorNoVideosInTweet' } - } - if (single) { - return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` } - } else if (multiple) { - return { picker: multiple } - } else { - return { error: 'ErrorNoVideosInTweet' } - } + if (!req_status["extended_entities"] && req_status["extended_entities"]["media"]) { + return { error: 'ErrorNoVideosInTweet' } + } + let single, multiple = [], media = req_status["extended_entities"]["media"]; + media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true }) + if (media.length > 1) { + for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } + } else if (media.length === 1) { + single = bestQuality(media[0]["video_info"]["variants"]) + } else { + return { error: 'ErrorNoVideosInTweet' } + } + if (single) { + return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` } + } else if (multiple) { + return { picker: multiple } } else { return { error: 'ErrorNoVideosInTweet' } } @@ -67,34 +66,33 @@ export default async function(obj) { return r.status == 200 ? r.json() : false; }).catch((e) => {return false}); - if (AudioSpaceById) { - if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) { - let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then((r) => {return r.status == 200 ? r.json() : false;}).catch(() => {return false;}); - if (!streamStatus) return { error: 'ErrorCouldntFetch' }; - - let participants = AudioSpaceById.data.audioSpace.participants.speakers - let listOfParticipants = `Twitter Space speakers: ` - for (let i in participants) { - listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` - } - listOfParticipants = listOfParticipants.slice(0, -2); - return { - urls: streamStatus.source.noRedirectPlaybackUrl, - audioFilename: `twitterspaces_${obj.spaceId}`, - isAudioOnly: true, - fileMetadata: { - title: AudioSpaceById.data.audioSpace.metadata.title, - artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`, - comment: listOfParticipants, - // cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "") - } - } - } else { - return { error: 'TwitterSpaceWasntRecorded' }; - } - } else { + if (!AudioSpaceById) { return { error: 'ErrorEmptyDownload' } } + if (!AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) { + return { error: 'TwitterSpaceWasntRecorded' }; + } + let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, + { headers: _headers }).then((r) =>{return r.status == 200 ? r.json() : false;}).catch(() => {return false;}); + if (!streamStatus) return { error: 'ErrorCouldntFetch' }; + + let participants = AudioSpaceById.data.audioSpace.participants.speakers + let listOfParticipants = `Twitter Space speakers: ` + for (let i in participants) { + listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` + } + listOfParticipants = listOfParticipants.slice(0, -2); + return { + urls: streamStatus.source.noRedirectPlaybackUrl, + audioFilename: `twitterspaces_${obj.spaceId}`, + isAudioOnly: true, + fileMetadata: { + title: AudioSpaceById.data.audioSpace.metadata.title, + artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`, + comment: listOfParticipants, + // cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "") + } + } } } catch (err) { return { error: 'ErrorBadFetch' }; diff --git a/src/modules/services/vimeo.js b/src/modules/services/vimeo.js index a5e88f3c..fe1af72f 100644 --- a/src/modules/services/vimeo.js +++ b/src/modules/services/vimeo.js @@ -1,14 +1,14 @@ -import { quality, services } from "../config.js"; +import { maxVideoDuration, quality, services } from "../config.js"; export default async function(obj) { try { let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => {return r.json()}).catch(() => {return false}); if (!api) return { error: 'ErrorCouldntFetch' }; - let downloadType = ""; + let downloadType = "dash"; if (JSON.stringify(api).includes('"progressive":[{')) { downloadType = "progressive"; - } else if (JSON.stringify(api).includes('"files":{"dash":{"')) downloadType = "dash"; + } switch(downloadType) { case "progressive": @@ -19,10 +19,13 @@ export default async function(obj) { let pref = parseInt(quality[obj.quality], 10) for (let i in all) { let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10) + if (currQuality === pref) { + best = all[i]; + break + } if (currQuality < pref) { - break; - } else if (currQuality == pref) { - best = all[i] + best = all[i-1]; + break } } } @@ -31,45 +34,46 @@ export default async function(obj) { } return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` }; case "dash": + if (api.video.duration > maxVideoDuration / 1000) { + return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + } let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"]; let masterJSON = await fetch(masterJSONURL).then((r) => {return r.json()}).catch(() => {return false}); + if (!masterJSON) return { error: 'ErrorCouldntFetch' }; - if (masterJSON.video) { - let type = ""; - if (masterJSON.base_url.includes("parcel")) { - type = "parcel" - } else if (masterJSON.base_url == "../") { - type = "chop" - } - let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)); - let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;}); - - let bestVideo = masterJSON_Video[0] - let bestAudio = masterJSON_Audio[0] - switch (type) { - case "parcel": - if (obj.quality != "max") { - let pref = parseInt(quality[obj.quality], 10) - for (let i in masterJSON_Video) { - let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10) - if (currQuality < pref) { - break; - } else if (currQuality == pref) { - bestVideo = masterJSON_Video[i] - } + if (!masterJSON.video) { + return { error: 'ErrorEmptyDownload' } + } + let type = "parcel"; + if (masterJSON.base_url == "../") { + type = "chop" + } + let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)); + let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;}); + + let bestVideo = masterJSON_Video[0] + let bestAudio = masterJSON_Audio[0] + switch (type) { + case "parcel": + if (obj.quality != "max") { + let pref = parseInt(quality[obj.quality], 10) + for (let i in masterJSON_Video) { + let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10) + if (currQuality < pref) { + break; + } else if (currQuality == pref) { + bestVideo = masterJSON_Video[i] } } - let baseUrl = masterJSONURL.split("/sep/")[0] - let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`; - let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`; + } + let baseUrl = masterJSONURL.split("/sep/")[0] + let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`; + let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`; - return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` } - case "chop": // TO-DO: support chop type of streams - default: - return { error: 'ErrorEmptyDownload' } - } - } else { - return { error: 'ErrorEmptyDownload' } + return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` } + case "chop": // TO-DO: support chop type of streams + default: + return { error: 'ErrorEmptyDownload' } } default: return { error: 'ErrorEmptyDownload' } diff --git a/src/modules/services/vk.js b/src/modules/services/vk.js index ca2d826e..0c4872be 100644 --- a/src/modules/services/vk.js +++ b/src/modules/services/vk.js @@ -9,49 +9,45 @@ export default async function(obj) { headers: {"user-agent": genericUserAgent} }).then((r) => {return r.text()}).catch(() => {return false}); if (!html) return { error: 'ErrorCouldntFetch' }; - if (html.includes(`{"lang":`)) { - let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); - if (js["mvData"]["is_active_live"] == '0') { - if (js["mvData"]["duration"] <= maxVideoDuration / 1000) { - let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 })); - - let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]; - if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) { - repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"]; - } - let attr = repr[repr.length - 1]["_attributes"]; - let selectedQuality; - let qualities = Object.keys(services.vk.quality_match); - for (let i in qualities) { - if (qualities[i] == attr["height"]) { - selectedQuality = `url${attr["height"]}`; - break; - } - if (qualities[i] == attr["width"]) { - selectedQuality = `url${attr["width"]}`; - break; - } - } - let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1) - let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]); - let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"]; - if (selectedQuality in js["player"]["params"][0]) { - return { - urls: js["player"]["params"][0][`url${userQuality}`], - filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4` - }; - } else { - return { error: 'ErrorEmptyDownload' }; - } - } else { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - } else { - return { error: 'ErrorLiveVideo' }; - } - } else { + if (!html.includes(`{"lang":`)) { return { error: 'ErrorEmptyDownload' }; } + let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); + if (!js["mvData"]["is_active_live"] == '0') { + return { error: 'ErrorLiveVideo' }; + } + if (js["mvData"]["duration"] > maxVideoDuration / 1000) { + return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + } + let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 })); + + let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]; + if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) { + repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"]; + } + let attr = repr[repr.length - 1]["_attributes"]; + let selectedQuality; + let qualities = Object.keys(services.vk.quality_match); + for (let i in qualities) { + if (qualities[i] == attr["height"]) { + selectedQuality = `url${attr["height"]}`; + break; + } + if (qualities[i] == attr["width"]) { + selectedQuality = `url${attr["width"]}`; + break; + } + } + let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1) + let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]); + let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"]; + if (!selectedQuality in js["player"]["params"][0]) { + return { error: 'ErrorEmptyDownload' }; + } + return { + urls: js["player"]["params"][0][`url${userQuality}`], + filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4` + }; } catch (err) { return { error: 'ErrorBadFetch' }; } diff --git a/src/modules/services/youtube.js b/src/modules/services/youtube.js index 6d247e4d..dfba7583 100644 --- a/src/modules/services/youtube.js +++ b/src/modules/services/youtube.js @@ -5,93 +5,88 @@ import selectQuality from "../stream/selectQuality.js"; export default async function(obj) { try { let infoInitial = await ytdl.getInfo(obj.id); - if (infoInitial) { - let info = infoInitial.formats; - if (!info[0]["isLive"]) { - let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true; - }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - if (!obj.isAudioOnly) { - video = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) { - if (obj.quality != "max") { - if (a["hasAudio"] && mq[obj.quality] == a["height"]) { - fullVideoMatch.push(a) - } else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) { - videoMatch.push(a); - } - } - return true - } - }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - if (obj.quality != "max") { - if (videoMatch.length == 0) { - let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim()) - videoMatch = video.filter((a) => { - if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true; - }) - } else if (fullVideoMatch.length > 0) { - videoMatch = [fullVideoMatch[0]] - } - } else videoMatch = [video[0]]; - if (obj.quality == "los") videoMatch = [video[video.length - 1]]; - } - let generalMeta = { - title: infoInitial.videoDetails.title, - artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(), - } - if (audio[0]["approxDurationMs"] <= maxVideoDuration) { - if (!obj.isAudioOnly && videoMatch.length > 0) { - if (video.length > 0 && audio.length > 0) { - if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) { - return { - type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` - }; - } else { - return { - type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` - }; - } - } else { - return { error: 'ErrorBadFetch' }; - } - } else if (!obj.isAudioOnly) { - return { - type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}` - }; - } else if (audio.length > 0) { - let r = { - type: "render", - isAudioOnly: true, - urls: audio[0]["url"], - audioFilename: `youtube_${obj.id}_audio`, - fileMetadata: generalMeta - }; - if (infoInitial.videoDetails.description) { - let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by"); - if (isAutoGenAudio) { - let descItems = infoInitial.videoDetails.description.split("\n\n") - r.fileMetadata.album = descItems[2] - r.fileMetadata.copyright = descItems[3] - if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim(); - } - } - return r - } else { - return { error: 'ErrorBadFetch' }; - } - } else { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - } else { - return { error: 'ErrorLiveVideo' }; - } - } else { + if (!infoInitial) { return { error: 'ErrorCantConnectToServiceAPI' }; } + let info = infoInitial.formats; + if (info[0]["isLive"]) { + return { error: 'ErrorLiveVideo' }; + } + let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => { + if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true; + }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); + if (!obj.isAudioOnly) { + video = info.filter((a) => { + if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) { + if (obj.quality != "max") { + if (a["hasAudio"] && mq[obj.quality] == a["height"]) { + fullVideoMatch.push(a) + } else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) { + videoMatch.push(a); + } + } + return true + } + }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); + if (obj.quality != "max") { + if (videoMatch.length == 0) { + let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim()) + videoMatch = video.filter((a) => { + if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true; + }) + } else if (fullVideoMatch.length > 0) { + videoMatch = [fullVideoMatch[0]] + } + } else videoMatch = [video[0]]; + if (obj.quality == "los") videoMatch = [video[video.length - 1]]; + } + let generalMeta = { + title: infoInitial.videoDetails.title, + artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(), + } + if (audio[0]["approxDurationMs"] > maxVideoDuration) { + return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + } + if (!obj.isAudioOnly && videoMatch.length > 0) { + if (video.length === 0 && audio.length === 0) { + return { error: 'ErrorBadFetch' }; + } + if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) { + return { + type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"], + filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` + }; + } + return { + type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"], + filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` + }; + } else if (!obj.isAudioOnly) { + return { + type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"], + filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}` + }; + } else if (audio.length > 0) { + let r = { + type: "render", + isAudioOnly: true, + urls: audio[0]["url"], + audioFilename: `youtube_${obj.id}_audio`, + fileMetadata: generalMeta + }; + if (infoInitial.videoDetails.description) { + let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by"); + if (isAutoGenAudio) { + let descItems = infoInitial.videoDetails.description.split("\n\n") + r.fileMetadata.album = descItems[2] + r.fileMetadata.copyright = descItems[3] + if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim(); + } + } + return r + } else { + return { error: 'ErrorBadFetch' }; + } } catch (e) { return { error: 'ErrorBadFetch' }; } diff --git a/src/modules/setup.js b/src/modules/setup.js index 625d4beb..aa64370e 100644 --- a/src/modules/setup.js +++ b/src/modules/setup.js @@ -33,22 +33,15 @@ console.log( ) rl.question(q, r1 => { - if (r1) { - ob['selfURL'] = `https://${r1}/` - } else { - ob['selfURL'] = `http://localhost` - } + ob['selfURL'] = `http://localhost:9000/` + ob['port'] = 9000 + if (r1) ob['selfURL'] = `https://${r1}/` + console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)")) + rl.question(q, r2 => { - if (!r1 && !r2) { - ob['selfURL'] = `http://localhost:9000/` - ob['port'] = 9000 - } else if (!r1 && r2) { - ob['selfURL'] = `http://localhost:${r2}/` - ob['port'] = r2 - } else { - ob['port'] = r2 - } + if (r2) ob['port'] = r2 + if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/` final() }); }) diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 7da39d9c..065c8c2d 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -43,16 +43,14 @@ export function createStream(obj) { export function verifyStream(ip, id, hmac, exp) { try { let streamInfo = streamCache.get(id); - if (streamInfo) { - let ghmac = sha256(`${id},${streamInfo.service},${ip},${exp}`, salt); - if (hmac == ghmac && exp.toString() == streamInfo.exp && ghmac == streamInfo.hmac && ip == streamInfo.ip && exp > Math.floor(new Date().getTime())) { - return streamInfo; - } else { - return { error: 'Unauthorized', status: 401 }; - } - } else { + if (!streamInfo) { return { error: 'this stream token does not exist', status: 400 }; } + let ghmac = sha256(`${id},${streamInfo.service},${ip},${exp}`, salt); + if (hmac == ghmac && exp.toString() == streamInfo.exp && ghmac == streamInfo.hmac && ip == streamInfo.ip && exp > Math.floor(new Date().getTime())) { + return streamInfo; + } + return { error: 'Unauthorized', status: 401 }; } catch (e) { return { status: 500, body: { status: "error", text: "Internal Server Error" } }; } diff --git a/src/modules/stream/selectQuality.js b/src/modules/stream/selectQuality.js index d21a5f23..9ddf1fdc 100644 --- a/src/modules/stream/selectQuality.js +++ b/src/modules/stream/selectQuality.js @@ -1,5 +1,6 @@ import { services, quality as mq } from "../config.js"; +// TO-DO: remake entirety of this module to be more of how quality picking is done in vimeo module function closest(goal, array) { return array.sort().reduce(function (prev, curr) { return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); @@ -15,9 +16,7 @@ export default function(service, quality, maxQuality) { if (quality >= maxQuality || quality == maxQuality) return maxQuality; if (quality < maxQuality) { - if (services[service]["quality"][quality]) { - return quality - } else { + if (!services[service]["quality"][quality]) { let s = Object.keys(services[service]["quality_match"]).filter((q) => { if (q <= quality) { return true @@ -25,5 +24,6 @@ export default function(service, quality, maxQuality) { }) return closest(quality, s) } + return quality } } diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index eb843086..7f9b42e6 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -5,24 +5,24 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } fro export default function(res, ip, id, hmac, exp) { try { let streamInfo = verifyStream(ip, id, hmac, exp); - if (!streamInfo.error) { - if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") { - streamAudioOnly(streamInfo, res); - } else { - switch (streamInfo.type) { - case "render": - streamLiveRender(streamInfo, res); - break; - case "mute": - streamVideoOnly(streamInfo, res); - break; - default: - streamDefault(streamInfo, res); - break; - } - } - } else { + if (streamInfo.error) { res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); + return; + } + if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") { + streamAudioOnly(streamInfo, res); + return; + } + switch (streamInfo.type) { + case "render": + streamLiveRender(streamInfo, res); + break; + case "mute": + streamVideoOnly(streamInfo, res); + break; + default: + streamDefault(streamInfo, res); + break; } } catch (e) { res.status(500).json({ status: "error", text: "Internal Server Error" }); diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index ef6ac22c..50ce1389 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -27,39 +27,41 @@ export function streamDefault(streamInfo, res) { } export function streamLiveRender(streamInfo, res) { try { - if (streamInfo.urls.length === 2) { - let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ - '-loglevel', '-8', - '-i', streamInfo.urls[0], - '-i', streamInfo.urls[1], - '-map', '0:v', - '-map', '1:a', - ]; - args = args.concat(ffmpegArgs[format]) - if (streamInfo.time) args.push('-t', msToTime(streamInfo.time)); - args.push('-f', format, 'pipe:3'); - const ffmpegProcess = spawn(ffmpeg, args, { - windowsHide: true, - stdio: [ - 'inherit', 'inherit', 'inherit', - 'pipe' - ], - }); - res.setHeader('Connection', 'keep-alive'); - res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`); - ffmpegProcess.stdio[3].pipe(res); - ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); - ffmpegProcess.on('close', () => ffmpegProcess.kill()); - ffmpegProcess.on('exit', () => ffmpegProcess.kill()); - res.on('finish', () => ffmpegProcess.kill()); - res.on('close', () => ffmpegProcess.kill()); - ffmpegProcess.on('error', (err) => { - ffmpegProcess.kill(); - res.end(); - }); - } else { + if (!streamInfo.urls.length === 2) { res.end(); + return; } + let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ + '-loglevel', '-8', + '-i', streamInfo.urls[0], + '-i', streamInfo.urls[1], + '-map', '0:v', + '-map', '1:a', + ]; + args = args.concat(ffmpegArgs[format]) + if (streamInfo.time) args.push('-t', msToTime(streamInfo.time)); + args.push('-f', format, 'pipe:3'); + const ffmpegProcess = spawn(ffmpeg, args, { + windowsHide: true, + stdio: [ + 'inherit', 'inherit', 'inherit', + 'pipe' + ], + }); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`); + ffmpegProcess.stdio[3].pipe(res); + + ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); + ffmpegProcess.on('close', () => ffmpegProcess.kill()); + ffmpegProcess.on('exit', () => ffmpegProcess.kill()); + res.on('finish', () => ffmpegProcess.kill()); + res.on('close', () => ffmpegProcess.kill()); + ffmpegProcess.on('error', (err) => { + ffmpegProcess.kill(); + res.end(); + }); + } catch (e) { res.end(); } @@ -93,6 +95,7 @@ export function streamAudioOnly(streamInfo, res) { res.setHeader('Connection', 'keep-alive'); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`); ffmpegProcess.stdio[3].pipe(res); + ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); ffmpegProcess.on('close', () => ffmpegProcess.kill()); ffmpegProcess.on('exit', () => ffmpegProcess.kill()); @@ -125,6 +128,7 @@ export function streamVideoOnly(streamInfo, res) { res.setHeader('Connection', 'keep-alive'); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}_mute.${format}"`); ffmpegProcess.stdio[3].pipe(res); + ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); ffmpegProcess.on('close', () => ffmpegProcess.kill()); ffmpegProcess.on('exit', () => ffmpegProcess.kill()); diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 6c65f5fb..78efaf14 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -103,25 +103,24 @@ export function checkJSONPost(obj) { } try { let objKeys = Object.keys(obj); - if (objKeys.length < 8 && obj.url) { - let defKeys = Object.keys(def); - for (let i in objKeys) { - if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) { - if (apiVar.booleanOnly.includes(objKeys[i])) { - def[objKeys[i]] = obj[objKeys[i]] ? true : false; - } else { - if (apiVar.allowed[objKeys[i]] && apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]]) - } - } - } - obj["url"] = decodeURIComponent(String(obj["url"])) - let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), - host = hostname[hostname.length - 2] - def["url"] = encodeURIComponent(cleanURL(obj["url"], host)) - return def - } else { + if (!(objKeys.length < 8 && obj.url)) { return false } + let defKeys = Object.keys(def); + for (let i in objKeys) { + if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) { + if (apiVar.booleanOnly.includes(objKeys[i])) { + def[objKeys[i]] = obj[objKeys[i]] ? true : false; + } else { + if (apiVar.allowed[objKeys[i]] && apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]]) + } + } + } + obj["url"] = decodeURIComponent(String(obj["url"])) + let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), + host = hostname[hostname.length - 2] + def["url"] = encodeURIComponent(cleanURL(obj["url"], host)) + return def } catch (e) { return false; } From dacaaf5b27c4d80cd54b42891b2431036c95b0f3 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 Feb 2023 13:40:49 +0600 Subject: [PATCH 02/11] 5.0-dev1 - rewrote and/or optimized all service modules - rewrote matching and processing modules to optimize readability and performance - added support for reddit gifs - fixed various issues with twitter error explanations - code optimizations and enhancements (such as finally getting rid of ==, prettier and more readable formatting, etc) - added branch information - all functions in currentCommit submodule run only once and cache received data - added a test script. only twitter and soundcloud are 100% covered and tested atm, will add tests (and probably fixes) for the rest of services in next commits - changed some localization strings for russian - added more clarity to rate limit message - moved services folder into processing folder --- .gitignore | 2 +- package.json | 5 +- src/cobalt.js | 28 +- src/front/cobalt.js | 2 +- src/localization/languages/en.json | 2 +- src/localization/languages/ru.json | 6 +- src/modules/api.js | 2 +- src/modules/pageRender/elements.js | 2 +- src/modules/pageRender/page.js | 2 +- src/modules/processing/match.js | 71 +++-- src/modules/processing/matchActionDecider.js | 242 +++++++++-------- src/modules/processing/services/bilibili.js | 28 ++ src/modules/processing/services/reddit.js | 28 ++ src/modules/processing/services/soundcloud.js | 74 +++++ src/modules/processing/services/tiktok.js | 112 ++++++++ src/modules/processing/services/tumblr.js | 14 + src/modules/processing/services/twitter.js | 101 +++++++ src/modules/processing/services/vimeo.js | 77 ++++++ src/modules/processing/services/vk.js | 45 +++ src/modules/processing/services/youtube.js | 88 ++++++ src/modules/processing/servicesConfig.json | 1 + .../processing/servicesPatternTesters.js | 29 +- src/modules/services/bilibili.js | 38 --- src/modules/services/reddit.js | 40 --- src/modules/services/soundcloud.js | 90 ------ src/modules/services/tiktok.js | 117 -------- src/modules/services/tumblr.js | 17 -- src/modules/services/twitter.js | 100 ------- src/modules/services/vimeo.js | 84 ------ src/modules/services/vk.js | 54 ---- src/modules/services/youtube.js | 93 ------- src/modules/stream/manage.js | 3 +- src/modules/stream/selectQuality.js | 4 +- src/modules/stream/types.js | 2 +- src/modules/sub/currentCommit.js | 19 +- src/modules/sub/errors.js | 5 +- src/modules/sub/utils.js | 15 +- src/test/services.json | 256 ++++++++++++++++++ src/test/test.js | 66 +++++ 39 files changed, 1139 insertions(+), 825 deletions(-) create mode 100644 src/modules/processing/services/bilibili.js create mode 100644 src/modules/processing/services/reddit.js create mode 100644 src/modules/processing/services/soundcloud.js create mode 100644 src/modules/processing/services/tiktok.js create mode 100644 src/modules/processing/services/tumblr.js create mode 100644 src/modules/processing/services/twitter.js create mode 100644 src/modules/processing/services/vimeo.js create mode 100644 src/modules/processing/services/vk.js create mode 100644 src/modules/processing/services/youtube.js delete mode 100644 src/modules/services/bilibili.js delete mode 100644 src/modules/services/reddit.js delete mode 100644 src/modules/services/soundcloud.js delete mode 100644 src/modules/services/tiktok.js delete mode 100644 src/modules/services/tumblr.js delete mode 100644 src/modules/services/twitter.js delete mode 100644 src/modules/services/vimeo.js delete mode 100644 src/modules/services/vk.js delete mode 100644 src/modules/services/youtube.js create mode 100644 src/test/services.json create mode 100644 src/test/test.js diff --git a/.gitignore b/.gitignore index 01d4a695..97df23a8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ package-lock.json .env # esbuild -min \ No newline at end of file +min diff --git a/package.json b/package.json index f3387dcf..581536df 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "4.9-dev", + "version": "5.0-dev1", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", @@ -10,7 +10,8 @@ }, "scripts": { "start": "node src/cobalt", - "setup": "node src/modules/setup" + "setup": "node src/modules/setup", + "test": "node src/test/test" }, "repository": { "type": "git", diff --git a/src/cobalt.js b/src/cobalt.js index 7512db78..e8173cca 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -1,11 +1,11 @@ -import "dotenv/config" +import "dotenv/config"; import express from "express"; import cors from "cors"; import * as fs from "fs"; import rateLimit from "express-rate-limit"; -import { shortCommit } from "./modules/sub/currentCommit.js"; +import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js"; import { appName, genericUserAgent, version } from "./modules/config.js"; import { getJSON } from "./modules/api.js"; import renderPage from "./modules/pageRender/page.js"; @@ -18,13 +18,14 @@ import { changelogHistory } from "./modules/pageRender/onDemand.js"; import { sha256 } from "./modules/sub/crypto.js"; const commitHash = shortCommit(); +const branch = getCurrentBranch(); const app = express(); app.disable('x-powered-by'); if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && process.env.port) { const apiLimiter = rateLimit({ - windowMs: 1 * 60 * 1000, + windowMs: 60000, max: 12, standardHeaders: true, legacyHeaders: false, @@ -33,7 +34,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && } }); const apiLimiterStream = rateLimit({ - windowMs: 1 * 60 * 1000, + windowMs: 60000, max: 12, standardHeaders: true, legacyHeaders: false, @@ -63,7 +64,6 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && } next(); }); - app.use('/api/json', express.json({ verify: (req, res, buf) => { try { @@ -76,6 +76,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && } } })); + app.post('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), async (req, res) => { try { let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt); @@ -89,10 +90,10 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && let j = await getJSON(chck["url"], languageCode(req), chck) res.status(j.status).json(j.body); } else if (request.url && !chck) { - let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorCouldntFetch') }); + let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorCouldntFetch') }); res.status(j.status).json(j.body); } else { - let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorNoLink') }) + let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorNoLink') }) res.status(j.status).json(j.body); } } catch (e) { @@ -108,12 +109,16 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') }) } }); + app.get('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), (req, res) => { try { let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt); switch (req.params.type) { case 'json': - res.status(405).json({ 'status': 'error', 'text': 'GET method for this request has been deprecated. see https://github.com/wukko/cobalt/blob/current/docs/API.md for up-to-date API documentation.' }); + res.status(405).json({ + 'status': 'error', + 'text': 'GET method for this endpoint has been deprecated. see https://github.com/wukko/cobalt/blob/current/docs/API.md for up-to-date API documentation.' + }); break; case 'stream': if (req.query.p) { @@ -153,6 +158,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') }) } }); + app.get("/api", (req, res) => { res.redirect('/api/json') }); @@ -161,7 +167,8 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && "hash": commitHash, "type": "default", "lang": languageCode(req), - "useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent + "useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent, + "branch": branch })) }); app.get("/favicon.ico", (req, res) => { @@ -170,9 +177,10 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && app.get("/*", (req, res) => { res.redirect('/') }); + app.listen(process.env.port, () => { let startTime = new Date(); - console.log(`\n${Cyan(appName)} ${Bright(`v.${version}-${commitHash}`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\n`) + console.log(`\n${Cyan(appName)} ${Bright(`v.${version}-${commitHash} (${branch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\n`) }); } else { console.log(Red(`cobalt hasn't been configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`)) diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 0628d355..13333ae9 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -273,7 +273,7 @@ function toggle(toggl) { } function loadSettings() { try { - if (typeof(navigator.clipboard.readText) == "undefined") throw new Error(); + if (typeof(navigator.clipboard.readText) === undefined) throw new Error(); } catch (err) { eid("pasteFromClipboard").style.display = "none" } diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 3e70c30b..384c1695 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -25,7 +25,7 @@ "ErrorBrokenLink": "{s} is supported, but something is wrong with your link. maybe you didn't copy it fully?", "ErrorNoLink": "i can't guess what you want to download! please give me a link.", "ErrorPageRenderFail": "something went wrong and page couldn't render. if it's a recurring or critical issue, please {ContactLink}. it'd be useful if you provided current commit hash ({s}) and error recreation steps. thank you in advance :D", - "ErrorRateLimit": "you're making too many requests. calm down and try again in a bit.", + "ErrorRateLimit": "you're making too many requests. try again in a minute!", "ErrorCouldntFetch": "couldn't get any info about your link. check if it's correct and try again.", "ErrorLengthLimit": "current length limit is {s} minutes. video that you tried to download is longer than {s} minutes. pick something else!", "ErrorBadFetch": "an error occurred when i tried to get info about your link. are you sure it works? check if it does, and try again.", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index e09b016e..2a312065 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -7,7 +7,7 @@ "LinkInput": "вставь ссылку сюда", "AboutSummary": "{appName} — твой друг при скачивании контента из соц. сетей. никакой рекламы или трекеров. вставляешь ссылку и получаешь файл. ничего лишнего.", "EmbedBriefDescription": "сохраняй что хочешь, без мороки и вторжения в личное пространство", - "MadeWithLove": "сделано с <3 wukko", + "MadeWithLove": "сделано wukko, с <3", "AccessibilityInputArea": "зона вставки ссылки", "AccessibilityOpenAbout": "открыть окно с инфой", "AccessibilityDownloadButton": "кнопка скачивания", @@ -23,9 +23,9 @@ "ErrorSomethingWentWrong": "что-то пошло совсем не так, и у меня не получилось ничего для тебя достать. ты можешь попробовать ещё раз, но если так и не получится, {ContactLink}.", "ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?", "ErrorBrokenLink": "{s} поддерживается, но с твоей ссылкой что-то не так. может быть, ты её не полностью скопировал?", - "ErrorNoLink": "я не гадалка и не могу угадывать, что ты хочешь скачать. попробуй в следующий раз вставить ссылку.", + "ErrorNoLink": "пока что я не умею угадывать, что ты хочешь скачать. дай мне, пожалуйста, ссылку :(", "ErrorPageRenderFail": "что-то пошло не так и у меня не получилось срендерить страницу. если это повторится ещё раз, пожалуйста, {ContactLink}. также приложи хэш текущего коммита ({s}) с действиями для повторения этой ошибки. можно на русском языке. спасибо :)", - "ErrorRateLimit": "ты делаешь слишком много запросов. успокойся и попробуй ещё раз через несколько минут.", + "ErrorRateLimit": "ты делаешь слишком много запросов. попробуй ещё раз через минуту!", "ErrorCouldntFetch": "мне не удалось получить инфу о твоей ссылке. проверь её и попробуй ещё раз.", "ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".", "ErrorBadFetch": "произошла ошибка при получении инфы о твоей ссылке. ты уверен, что она работает? проверь её, и попробуй ещё раз.", diff --git a/src/modules/api.js b/src/modules/api.js index 9a6db6c8..1e0d9a92 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -31,7 +31,7 @@ export async function getJSON(originalURL, lang, obj) { break; case "tumblr": if (!url.includes("blog/view")) { - if (url.slice(-1) == '/') url = url.slice(0, -1); + if (url.slice(-1) === '/') url = url.slice(0, -1); url = url.replace(url.split('/')[5], ''); } break; diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index 909927ac..f6300f23 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -2,7 +2,7 @@ import { celebrations } from "../config.js"; export function switcher(obj) { let items = ``; - if (obj.name == "download") { + if (obj.name === "download") { items = obj.items; } else { for (let i = 0; i < obj.items.length; i++) { diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 87d4a0ad..0c90a756 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -186,7 +186,7 @@ export default function(obj) { closeAria: t('AccessibilityClosePopup'), header: { aboveTitle: { - text: `v.${version}-${obj.hash}`, + text: `v.${version}-${obj.hash} (${obj.branch})`, url: `${repo}/commit/${obj.hash}` }, title: `${emoji("⚙️", 30)} ${t('TitlePopupSettings')}` diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index ff150b83..654a9f5a 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -5,23 +5,24 @@ import loc from "../../localization/manager.js"; import { testers } from "./servicesPatternTesters.js"; -import bilibili from "../services/bilibili.js"; -import reddit from "../services/reddit.js"; -import twitter from "../services/twitter.js"; -import youtube from "../services/youtube.js"; -import vk from "../services/vk.js"; -import tiktok from "../services/tiktok.js"; -import tumblr from "../services/tumblr.js"; +import bilibili from "./services/bilibili.js"; +import reddit from "./services/reddit.js"; +import twitter from "./services/twitter.js"; +import youtube from "./services/youtube.js"; +import vk from "./services/vk.js"; +import tiktok from "./services/tiktok.js"; +import tumblr from "./services/tumblr.js"; import matchActionDecider from "./matchActionDecider.js"; -import vimeo from "../services/vimeo.js"; -import soundcloud from "../services/soundcloud.js"; +import vimeo from "./services/vimeo.js"; +import soundcloud from "./services/soundcloud.js"; export default async function (host, patternMatch, url, lang, obj) { try { - if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) }); - if (!(testers[host](patternMatch))) throw Error(); + let r, isAudioOnly = !!obj.isAudioOnly; + + if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) }); + if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang) }); - let r; switch (host) { case "twitter": r = await twitter({ @@ -29,14 +30,14 @@ export default async function (host, patternMatch, url, lang, obj) { spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false, lang: lang }); - if (r.isAudioOnly) obj.isAudioOnly = true break; case "vk": r = await vk({ url: url, userId: patternMatch["userId"], videoId: patternMatch["videoId"], - lang: lang, quality: obj.vQuality + lang: lang, + quality: obj.vQuality }); break; case "bilibili": @@ -48,10 +49,11 @@ export default async function (host, patternMatch, url, lang, obj) { case "youtube": let fetchInfo = { id: patternMatch["id"].slice(0, 11), - lang: lang, quality: obj.vQuality, + lang: lang, + quality: obj.vQuality, format: "webm" }; - if (url.match('music.youtube.com') || obj.isAudioOnly == true) obj.vFormat = "audio"; + if (url.match('music.youtube.com') || isAudioOnly === true) obj.vFormat = "audio"; switch (obj.vFormat) { case "mp4": fetchInfo["format"] = "mp4"; @@ -60,7 +62,7 @@ export default async function (host, patternMatch, url, lang, obj) { fetchInfo["format"] = "webm"; fetchInfo["isAudioOnly"] = true; fetchInfo["quality"] = "max"; - obj.isAudioOnly = true; + isAudioOnly = true; break; } r = await youtube(fetchInfo); @@ -69,7 +71,8 @@ export default async function (host, patternMatch, url, lang, obj) { r = await reddit({ sub: patternMatch["sub"], id: patternMatch["id"], - title: patternMatch["title"], lang: lang, + title: patternMatch["title"], + lang: lang, }); break; case "douyin": @@ -77,28 +80,33 @@ export default async function (host, patternMatch, url, lang, obj) { r = await tiktok({ host: host, postId: patternMatch["postId"], - id: patternMatch["id"], lang: lang, - noWatermark: obj.isNoTTWatermark, fullAudio: obj.isTTFullAudio, - isAudioOnly: obj.isAudioOnly + id: patternMatch["id"], + lang: lang, + noWatermark: obj.isNoTTWatermark, + fullAudio: obj.isTTFullAudio, + isAudioOnly: isAudioOnly }); - if (r.isAudioOnly) obj.isAudioOnly = true; break; case "tumblr": r = await tumblr({ - id: patternMatch["id"], url: url, user: patternMatch["user"] ? patternMatch["user"] : false, + id: patternMatch["id"], + url: url, + user: patternMatch["user"] ? patternMatch["user"] : false, lang: lang }); break; case "vimeo": r = await vimeo({ - id: patternMatch["id"].slice(0, 11), quality: obj.vQuality, + id: patternMatch["id"].slice(0, 11), + quality: obj.vQuality, lang: lang }); break; case "soundcloud": - obj.isAudioOnly = true; + isAudioOnly = true; r = await soundcloud({ - author: patternMatch["author"], song: patternMatch["song"], url: url, + author: patternMatch["author"], + song: patternMatch["song"], url: url, shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false, accessKey: patternMatch["accessKey"] ? patternMatch["accessKey"] : false, format: obj.aFormat, @@ -108,10 +116,15 @@ export default async function (host, patternMatch, url, lang, obj) { default: return apiJSON(0, { t: errorUnsupported(lang) }); } - return !r.error ? matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly, lang, obj.isAudioMuted) : apiJSON(0, { - t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) - }); + + if (r.isAudioOnly) isAudioOnly = true; + let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted; + + if (r.error) return apiJSON(0, { t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) }); + + return matchActionDecider(r, host, obj.ip, obj.aFormat, isAudioOnly, lang, isAudioMuted); } catch (e) { + console.log(e) return apiJSON(0, { t: genericError(lang, host) }) } } diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index a8070cde..7e76d602 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -1,121 +1,137 @@ -import { audioIgnore, services, supportedAudio } from "../config.js" -import { apiJSON } from "../sub/utils.js" +import { audioIgnore, services, supportedAudio } from "../config.js"; +import { apiJSON } from "../sub/utils.js"; import loc from "../../localization/manager.js"; export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMuted) { - if (!isAudioOnly && !r.picker && !isAudioMuted) { - switch (host) { - case "twitter": - return apiJSON(1, { u: r.urls }); - case "vk": - return apiJSON(2, { - type: "bridge", u: r.urls, service: host, ip: ip, - filename: r.filename, - }); - case "bilibili": - return apiJSON(2, { - type: "render", u: r.urls, service: host, ip: ip, - filename: r.filename, - time: r.time - }); - case "youtube": - return apiJSON(2, { - type: r.type, u: r.urls, service: host, ip: ip, - filename: r.filename, - time: r.time, - }); - case "reddit": - return apiJSON(r.typeId, { - type: r.type, u: r.urls, service: host, ip: ip, - filename: r.filename, - }); - case "tiktok": - return apiJSON(2, { - type: "bridge", u: r.urls, service: host, ip: ip, - filename: r.filename, - }); - case "douyin": - return apiJSON(2, { - type: "bridge", u: r.urls, service: host, ip: ip, - filename: r.filename, - }); - case "tumblr": - return apiJSON(1, { u: r.urls }); - case "vimeo": - if (Array.isArray(r.urls)) { - return apiJSON(2, { - type: "render", u: r.urls, service: host, ip: ip, - filename: r.filename - }); - } else { - return apiJSON(1, { u: r.urls }); - } - } - } else if (isAudioMuted && !isAudioOnly) { - let isSplit = Array.isArray(r.urls); - return apiJSON(2, { - type: isSplit ? "bridge" : "mute", - u: isSplit ? r.urls[0] : r.urls, + let action, + responseType = 2, + defaultParams = { + u: r.urls, service: host, ip: ip, filename: r.filename, - mute: true, - }); - } else if (r.picker) { - switch (host) { - case "douyin": - case "tiktok": - let type = "render"; - if (audioFormat === "mp3" || audioFormat === "best") { - audioFormat = "mp3" - type = "bridge" - } - return apiJSON(5, { - type: type, - picker: r.picker, - u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip, - filename: r.audioFilename, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false, - }) - case "twitter": - return apiJSON(5, { - picker: r.picker, service: host - }) - } - } else if (isAudioOnly) { - if ((host === "reddit" && r.typeId === 1) || (host === "vimeo" && !r.filename) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); - let type = "render"; - let copy = false; - - if (!supportedAudio.includes(audioFormat)) audioFormat = "best"; - if ((host == "tiktok" || host == "douyin") && services.tiktok.audioFormats.includes(audioFormat)) { - if (r.isMp3) { - if (audioFormat === "mp3" || audioFormat === "best") { - audioFormat = "mp3" - type = "bridge" - } - } else if (audioFormat === "best") { - audioFormat = "m4a" - type = "bridge" - } - } - if ((audioFormat === "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) { - audioFormat = services[host]["bestAudio"] - type = "bridge" - } else if (audioFormat === "best") { - audioFormat = "m4a" - copy = true - if (r.audioFilename.includes("twitterspaces")) { - audioFormat = "mp3" - copy = false - } - } - return apiJSON(2, { - type: type, - u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip, - filename: r.audioFilename, isAudioOnly: true, - audioFormat: audioFormat, copy: copy, fileMetadata: r.fileMetadata ? r.fileMetadata : false - }) - } else { - return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }); + }, + params = {} + + if (isAudioMuted) action = "muteVideo"; + if (!isAudioOnly && !r.picker && !isAudioMuted) action = "video"; + if (r.picker) action = "picker"; + if (isAudioOnly) action = "audio"; + + if (action === "picker" || action === "audio") { + defaultParams.filename = r.audioFilename; + defaultParams.isAudioOnly = true; + defaultParams.audioFormat = audioFormat; } + + switch (action) { + case "video": + switch (host) { + case "bilibili": + params = { type: "render", time: r.time }; + break; + case "youtube": + params = { type: r.type, time: r.time }; + break; + case "reddit": + responseType = r.typeId; + params = { type: r.type }; + break; + case "vimeo": + if (Array.isArray(r.urls)) { + params = { type: "render" } + } else { + responseType = 1; + } + break; + + case "vk": + case "douyin": + case "tiktok": + params = { type: "bridge" }; + break; + + case "tumblr": + case "twitter": + responseType = 1; + break; + } + break; + + case "muteVideo": + params = { + type: Array.isArray(r.urls) ? "bridge" : "mute", + u: Array.isArray(r.urls) ? r.urls[0] : r.urls, + mute: true + } + break; + + case "picker": + responseType = 5; + switch (host) { + case "twitter": + params = { picker: r.picker }; + break; + case "douyin": + case "tiktok": + let pickerType = "render"; + if (audioFormat === "mp3" || audioFormat === "best") { + audioFormat = "mp3"; + pickerType = "bridge" + } + params = { + type: pickerType, + picker: r.picker, + u: Array.isArray(r.urls) ? r.urls[1] : r.urls, + copy: audioFormat === "best" ? true : false + } + } + break; + + case "audio": + if ((host === "reddit" && r.typeId === 1) || (host === "vimeo" && !r.filename) || 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 (r.isMp3) { + if (audioFormat === "mp3" || audioFormat === "best") { + audioFormat = "mp3"; + processType = "bridge" + } + } else if (audioFormat === "best") { + audioFormat = "m4a"; + processType = "bridge" + } + } + + if ((audioFormat === "best" && services[host]["bestAudio"]) + || services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) { + audioFormat = services[host]["bestAudio"]; + processType = "bridge" + } else if (audioFormat === "best") { + audioFormat = "m4a"; + copy = true; + if (r.audioFilename.includes("twitterspaces")) { + audioFormat = "mp3" + copy = false + } + } + + params = { + type: processType, + u: Array.isArray(r.urls) ? r.urls[1] : r.urls, + audioFormat: audioFormat, + copy: copy, + fileMetadata: r.fileMetadata ? r.fileMetadata : false + } + break; + default: + return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); + } + + return apiJSON(responseType, {...defaultParams, ...params}) } diff --git a/src/modules/processing/services/bilibili.js b/src/modules/processing/services/bilibili.js new file mode 100644 index 00000000..8f9ab286 --- /dev/null +++ b/src/modules/processing/services/bilibili.js @@ -0,0 +1,28 @@ +import { genericUserAgent, maxVideoDuration } from "../../config.js"; + +// TO-DO: quality picking & bilibili.tv support +export default async function(obj) { + let html = await fetch(`https://bilibili.com/video/${obj.id}`, { + headers: { "user-agent": genericUserAgent } + }).then((r) => { return r.text() }).catch(() => { return false }); + if (!html) return { error: 'ErrorCouldntFetch' }; + if (!(html.includes('')[0]); + if (streamData.data.timelength > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + + let video = streamData["data"]["dash"]["video"].filter((v) => { + if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; + }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); + + let audio = streamData["data"]["dash"]["audio"].filter((a) => { + if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; + }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); + + return { + urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], + time: streamData.data.timelength, + audioFilename: `bilibili_${obj.id}_audio`, + filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` + }; +} diff --git a/src/modules/processing/services/reddit.js b/src/modules/processing/services/reddit.js new file mode 100644 index 00000000..4465ec04 --- /dev/null +++ b/src/modules/processing/services/reddit.js @@ -0,0 +1,28 @@ +import { maxVideoDuration } from "../../config.js"; + +export default async function(obj) { + let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => { return r.json() }).catch(() => { return false }); + if (!data) return { error: 'ErrorCouldntFetch' }; + + data = data[0]["data"]["children"][0]["data"]; + + if (data.url.endsWith('.gif')) return { typeId: 1, urls: data.url }; + + if (!"reddit_video" in data["secure_media"]) return { error: 'ErrorEmptyDownload' }; + if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + + let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0], + audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`; + await fetch(audio, { method: "HEAD" }).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''}); + + let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]; + if (!audio.length > 0) return { typeId: 1, urls: video }; + + return { + typeId: 2, + type: "render", + urls: [video, audio], + audioFilename: `reddit_${id}_audio`, + filename: `reddit_${id}.mp4` + }; +} diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js new file mode 100644 index 00000000..312f60ef --- /dev/null +++ b/src/modules/processing/services/soundcloud.js @@ -0,0 +1,74 @@ +import { maxAudioDuration } from "../../config.js"; + +let cachedID = {}; + +async function findClientID() { + try { + let sc = await fetch('https://soundcloud.com/').then((r) => { return r.text() }).catch(() => { return false }); + let scVersion = String(sc.match(/')[0]) + if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' }; + + let clientId = await findClientID(); + if (!clientId) return { error: 'ErrorSoundCloudNoClientId' }; + + let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive") + let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; + if (!fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' }; + + if (json.duration > maxAudioDuration) return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }; + + let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false }); + if (!file) return { error: 'ErrorCouldntFetch' }; + + return { + urls: file, + audioFilename: `soundcloud_${json.id}`, + fileMetadata: { + title: json.title, + artist: json.user.username, + } + } +} diff --git a/src/modules/processing/services/tiktok.js b/src/modules/processing/services/tiktok.js new file mode 100644 index 00000000..4ac0d19c --- /dev/null +++ b/src/modules/processing/services/tiktok.js @@ -0,0 +1,112 @@ +import { genericUserAgent } from "../../config.js"; + +let userAgent = genericUserAgent.split(' Chrome/1')[0], + config = { + tiktok: { + short: "https://vt.tiktok.com/", + api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9®ion=US&carrier_region=US", + }, + douyin: { + short: "https://v.douyin.com/", + api: "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={postId}", + } +} + +function selector(j, h, id) { + if (!j) return false; + let t; + switch (h) { + case "tiktok": + t = j["aweme_list"].filter((v) => { if (v["aweme_id"] === id) return true }); + break; + case "douyin": + t = j['item_list'].filter((v) => { if (v["aweme_id"] === id) return true }); + break; + } + if (!t.length > 0) return false; + return t[0]; +} + +export default async function(obj) { + if (!obj.postId) { + let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, { + redirect: "manual", + headers: { "user-agent": userAgent } + }).then((r) => { return r.text() }).catch(() => { return false }); + if (!html) return { error: 'ErrorCouldntFetch' }; + + if (html.slice(0, 17) === ' { return r.json() }).catch(() => { return false }); + + detail = selector(detail, obj.host, obj.postId); + if (!detail) return { error: 'ErrorCouldntFetch' }; + + let video, videoFilename, audioFilename, isMp3, audio, images, filenameBase = `${obj.host}_${obj.postId}`; + if (obj.host === "tiktok") { + images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false + } else { + images = detail["images"] ? detail["images"] : false + } + + if (!obj.isAudioOnly && !images) { + video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0]; + videoFilename = `${filenameBase}_video.mp4`; + if (obj.noWatermark) { + video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play"); + videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark + } + } else { + let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0]; + audio = fallback; + audioFilename = `${filenameBase}_audio_fv`; // fv - from video + if (obj.fullAudio || fallback.includes("music")) { + audio = detail["music"]["play_url"]["url_list"][0] + audioFilename = `${filenameBase}_audio` + } + if (audio.slice(-4) === ".mp3") isMp3 = true; + } + + if (video) return { + urls: video, + filename: videoFilename + } + if (images && obj.isAudioOnly) { + return { + urls: audio, + audioFilename: audioFilename, + isAudioOnly: true, + isMp3: isMp3, + } + } + if (images) { + let imageLinks = []; + for (let i in images) { + let sel = obj.host === "tiktok" ? images[i]["display_image"]["url_list"] : images[i]["url_list"]; + sel = sel.filter((p) => { if (p.includes(".jpeg?")) return true; }) + imageLinks.push({url: sel[0]}) + } + return { + picker: imageLinks, + urls: audio, + audioFilename: audioFilename, + isAudioOnly: true, + isMp3: isMp3, + } + } + if (audio) return { + urls: audio, + audioFilename: audioFilename, + isAudioOnly: true, + isMp3: isMp3, + } +} diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js new file mode 100644 index 00000000..45291f07 --- /dev/null +++ b/src/modules/processing/services/tumblr.js @@ -0,0 +1,14 @@ +import { genericUserAgent } from "../../config.js"; + +export default async function(obj) { + let html = await fetch(`https://${ + obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '') + }.tumblr.com/post/${obj.id}`, { + headers: { "user-agent": genericUserAgent } + }).then((r) => { return r.text() }).catch(() => { return false }); + + if (!html) return { error: 'ErrorCouldntFetch' }; + if (!html.includes('property="og:video" content="https://va.media.tumblr.com/')) return { error: 'ErrorEmptyDownload' }; + + return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, audioFilename: `tumblr_${obj.id}_audio` } +} diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js new file mode 100644 index 00000000..f46c63a4 --- /dev/null +++ b/src/modules/processing/services/twitter.js @@ -0,0 +1,101 @@ +import { genericUserAgent } from "../../config.js"; + +function bestQuality(arr) { + return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0] +} +const apiURL = "https://api.twitter.com/1.1" + +// TO-DO: move from 1.1 api to graphql +export default async function(obj) { + let _headers = { + "user-agent": genericUserAgent, + "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", + // ^ no explicit content, but with multi media support + "host": "api.twitter.com" + }; + let req_act = await fetch(`${apiURL}/guest/activate.json`, { + method: "POST", + headers: _headers + }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false }); + if (!req_act) return { error: 'ErrorCouldntFetch' }; + + _headers["x-guest-token"] = req_act["guest_token"]; + let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`; + + if (!obj.spaceId) { + let req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false }); + if (!req_status) { + _headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"; + // ^ explicit content, but no multi media support + delete _headers["x-guest-token"] + + req_act = await fetch(`${apiURL}/guest/activate.json`, { + method: "POST", + headers: _headers + }).then((r) => { return r.status === 200 ? r.json() : false}).catch(() => { return false }); + if (!req_act) return { error: 'ErrorCouldntFetch' }; + + _headers["x-guest-token"] = req_act["guest_token"]; + req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false }); + } + if (!req_status) return { error: 'ErrorCouldntFetch' }; + if (!req_status["extended_entities"] || !req_status["extended_entities"]["media"]) return { error: 'ErrorNoVideosInTweet' }; + + let single, multiple = [], media = req_status["extended_entities"]["media"]; + media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true }) + if (media.length > 1) { + for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } + } else if (media.length === 1) { + single = bestQuality(media[0]["video_info"]["variants"]) + } else { + return { error: 'ErrorNoVideosInTweet' } + } + + if (single) { + return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` } + } else if (multiple) { + return { picker: multiple } + } else { + return { error: 'ErrorNoVideosInTweet' } + } + } else { + _headers["host"] = "twitter.com" + _headers["content-type"] = "application/json" + + let query = { + variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withSuperFollowsUserFields":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":true,"withReplays":true}, + features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"verified_phone_label_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_uc_gql_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":false,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"responsive_web_enhance_cards_enabled":true} + } + query.variables = new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1); + query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1); + query = `https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${query.variables}&features=${query.features}` + + let AudioSpaceById = await fetch(query, { headers: _headers }).then((r) => {return r.status === 200 ? r.json() : false}).catch((e) => { return false }); + if (!AudioSpaceById) return { error: 'ErrorEmptyDownload' }; + + if (!AudioSpaceById.data.audioSpace.metadata) return { error: 'ErrorEmptyDownload' }; + if (!AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) return { error: 'TwitterSpaceWasntRecorded' }; + + let streamStatus = await fetch( + `https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers } + ).then((r) =>{ return r.status === 200 ? r.json() : false }).catch(() => { return false }); + if (!streamStatus) return { error: 'ErrorCouldntFetch' }; + + let participants = AudioSpaceById.data.audioSpace.participants.speakers; + let listOfParticipants = `Twitter Space speakers: `; + for (let i in participants) { listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` } + listOfParticipants = listOfParticipants.slice(0, -2); + + return { + urls: streamStatus.source.noRedirectPlaybackUrl, + audioFilename: `twitterspaces_${obj.spaceId}`, + isAudioOnly: true, + fileMetadata: { + title: AudioSpaceById.data.audioSpace.metadata.title, + artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`, + comment: listOfParticipants, + // cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "") + } + } + } +} diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js new file mode 100644 index 00000000..534ce05d --- /dev/null +++ b/src/modules/processing/services/vimeo.js @@ -0,0 +1,77 @@ +import { maxVideoDuration, quality, services } from "../../config.js"; + +export default async function(obj) { + let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => { return r.json() }).catch(() => { return false }); + if (!api) return { error: 'ErrorCouldntFetch' }; + + let downloadType = "dash"; + if (JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive"; + + switch(downloadType) { + case "progressive": + let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width)); + let best = all[0]; + + try { + if (obj.quality != "max") { + let pref = parseInt(quality[obj.quality], 10) + for (let i in all) { + let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10) + if (currQuality === pref) { + best = all[i]; + break + } + if (currQuality < pref) { + best = all[i-1]; + break + } + } + } + } catch (e) { + best = all[0] + } + + return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` }; + case "dash": + if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + + let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"]; + let masterJSON = await fetch(masterJSONURL).then((r) => { return r.json() }).catch(() => { return false }); + + if (!masterJSON) return { error: 'ErrorCouldntFetch' }; + if (!masterJSON.video) return { error: 'ErrorEmptyDownload' }; + + let type = "parcel"; + if (masterJSON.base_url === "../") type = "chop"; + + let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)); + let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;}); + let bestVideo = masterJSON_Video[0], bestAudio = masterJSON_Audio[0]; + + switch (type) { + case "parcel": + if (obj.quality != "max") { + let pref = parseInt(quality[obj.quality], 10) + for (let i in masterJSON_Video) { + let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10) + if (currQuality < pref) { + break; + } else if (String(currQuality) === String(pref)) { + bestVideo = masterJSON_Video[i] + } + } + } + + let baseUrl = masterJSONURL.split("/sep/")[0]; + let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`, + audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`; + + return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` } + case "chop": // TO-DO: support for chop stream type + default: + return { error: 'ErrorEmptyDownload' } + } + default: + return { error: 'ErrorEmptyDownload' } + } +} diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js new file mode 100644 index 00000000..2ab7feb7 --- /dev/null +++ b/src/modules/processing/services/vk.js @@ -0,0 +1,45 @@ +import { xml2json } from "xml-js"; +import { genericUserAgent, maxVideoDuration, services } from "../../config.js"; +import selectQuality from "../../stream/selectQuality.js"; + +export default async function(obj) { + let html; + html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, { + headers: { "user-agent": genericUserAgent } + }).then((r) => { return r.text() }).catch(() => { return false }); + if (!html) return { error: 'ErrorCouldntFetch' }; + if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' }; + + let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); + + if (!Number(js["mvData"]["is_active_live"]) === 0) return { error: 'ErrorLiveVideo' }; + if (js["mvData"]["duration"] > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + + let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 })); + let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]; + if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"]; + + let selectedQuality, + attr = repr[repr.length - 1]["_attributes"], + qualities = Object.keys(services.vk.quality_match); + for (let i in qualities) { + if (qualities[i] === attr["height"]) { + selectedQuality = `url${attr["height"]}`; + break + } + if (qualities[i] === attr["width"]) { + selectedQuality = `url${attr["width"]}`; + break + } + } + + let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1); + let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]); + let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"]; + if (!selectedQuality in js["player"]["params"][0]) return { error: 'ErrorEmptyDownload' }; + + return { + urls: js["player"]["params"][0][`url${userQuality}`], + filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4` + } +} diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js new file mode 100644 index 00000000..8ec87453 --- /dev/null +++ b/src/modules/processing/services/youtube.js @@ -0,0 +1,88 @@ +import ytdl from "better-ytdl-core"; +import { maxVideoDuration, quality as mq } from "../../config.js"; +import selectQuality from "../../stream/selectQuality.js"; + +export default async function(obj) { + let isAudioOnly = !!obj.isAudioOnly, + infoInitial = await ytdl.getInfo(obj.id); + if (!infoInitial) return { error: 'ErrorCantConnectToServiceAPI' }; + + let info = infoInitial.formats; + if (info[0]["isLive"]) return { error: 'ErrorLiveVideo' }; + + let videoMatch = [], fullVideoMatch = [], video = [], + audio = info.filter((a) => { + if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true + }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); + + if (audio.length === 0) return { error: 'ErrorBadFetch' }; + if (audio[0]["approxDurationMs"] > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + + if (!isAudioOnly) { + video = info.filter((a) => { + if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) { + if (obj.quality != "max") { + if (a["hasAudio"] && mq[obj.quality] == a["height"]) { + fullVideoMatch.push(a) + } else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) { + videoMatch.push(a) + } + } + return true + } + }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); + + if (obj.quality != "max") { + if (videoMatch.length === 0) { + let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim()); + videoMatch = video.filter((a) => { + if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true + }) + } else if (fullVideoMatch.length > 0) { + videoMatch = [fullVideoMatch[0]] + } + } else videoMatch = [video[0]]; + if (obj.quality === "los") videoMatch = [video[video.length - 1]]; + } + if (video.length === 0) isAudioOnly = true; + + if (isAudioOnly) { + let r = { + type: "render", + isAudioOnly: true, + urls: audio[0]["url"], + audioFilename: `youtube_${obj.id}_audio`, + fileMetadata: { + title: infoInitial.videoDetails.title, + artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(), + } + } + if (infoInitial.videoDetails.description) { + let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by"); + if (isAutoGenAudio) { + let descItems = infoInitial.videoDetails.description.split("\n\n") + r.fileMetadata.album = descItems[2] + r.fileMetadata.copyright = descItems[3] + if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim(); + } + } + return r + } + let singleTest; + if (videoMatch.length > 0) { + singleTest = videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]; + return { + type: singleTest ? "bridge" : "render", + urls: singleTest ? videoMatch[0]["url"] : [videoMatch[0]["url"], audio[0]["url"]], + time: videoMatch[0]["approxDurationMs"], + filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` + } + } + singleTest = video[0]["hasVideo"] && video[0]["hasAudio"]; + return { + type: singleTest ? "bridge" : "render", + urls: singleTest ? video[0]["url"] : [video[0]["url"], audio[0]["url"]], + time: video[0]["approxDurationMs"], + filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${obj.format}` + } +} diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index fa1fd968..25c6f3f5 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -8,6 +8,7 @@ "enabled": true }, "reddit": { + "alias": "reddit videos & gifs", "patterns": ["r/:sub/comments/:id/:title"], "enabled": true }, diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index 56ca2b28..13695714 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -1,27 +1,28 @@ export const testers = { - "twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20) || (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13), + "twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20) + || (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13), - "vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] && - patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9), + "vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] + && patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9), "bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12), "youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 11), - "reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] && - patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96), + "reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] + && patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96), - "tiktok": (patternMatch) => ((patternMatch["user"] && patternMatch["postId"] && patternMatch["postId"].length <= 21) || - (patternMatch["id"] && patternMatch["id"].length <= 13)), + "tiktok": (patternMatch) => ((patternMatch["user"] && patternMatch["postId"] && patternMatch["postId"].length <= 21) + || (patternMatch["id"] && patternMatch["id"].length <= 13)), - "douyin": (patternMatch) => ((patternMatch["postId"] && patternMatch["postId"].length <= 21) || - (patternMatch["id"] && patternMatch["id"].length <= 13)), + "douyin": (patternMatch) => ((patternMatch["postId"] && patternMatch["postId"].length <= 21) + || (patternMatch["id"] && patternMatch["id"].length <= 13)), - "tumblr": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length < 21) || - (patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)), + "tumblr": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length < 21) + || (patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)), "vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)), - "soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"] && (patternMatch["author"].length + patternMatch["song"].length) <= 96) || - (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32)) -}; \ No newline at end of file + "soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"] + && (patternMatch["author"].length + patternMatch["song"].length) <= 96) || (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32)) +} diff --git a/src/modules/services/bilibili.js b/src/modules/services/bilibili.js deleted file mode 100644 index 17d12f15..00000000 --- a/src/modules/services/bilibili.js +++ /dev/null @@ -1,38 +0,0 @@ -import { genericUserAgent, maxVideoDuration } from "../config.js"; - -// TO-DO: quality picking -export default async function(obj) { - try { - let html = await fetch(`https://bilibili.com/video/${obj.id}`, { - headers: {"user-agent": genericUserAgent} - }).then((r) => { return r.text() }).catch(() => { return false }); - if (!html) { - return { error: 'ErrorCouldntFetch' }; - } - - if (!(html.includes('')[0]); - if (streamData.data.timelength > maxVideoDuration) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - - let video = streamData["data"]["dash"]["video"].filter((v) => { - if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; - }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); - - let audio = streamData["data"]["dash"]["audio"].filter((a) => { - if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; - }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); - - return { - urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], - time: streamData.data.timelength, - audioFilename: `bilibili_${obj.id}_audio`, - filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` - }; - } catch (e) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/reddit.js b/src/modules/services/reddit.js deleted file mode 100644 index 3a8fe25f..00000000 --- a/src/modules/services/reddit.js +++ /dev/null @@ -1,40 +0,0 @@ -import { maxVideoDuration } from "../config.js"; - -// TO-DO: add support for gifs (#80) -export default async function(obj) { - try { - let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => { return r.json() }).catch(() => { return false }); - if (!data) { - return { error: 'ErrorCouldntFetch' }; - } - data = data[0]["data"]["children"][0]["data"]; - - if (!"reddit_video" in data["secure_media"]) { - return { error: 'ErrorEmptyDownload' }; - } - if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0], - audio = video.match('.mp4') - ? `${video.split('_')[0]}_audio.mp4` - : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`; - - await fetch(audio, {method: "HEAD"}).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''}); - - let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]; - - if (!audio.length > 0) { - return { typeId: 1, urls: video }; - } - return { - typeId: 2, - type: "render", - urls: [video, audio], - audioFilename: `reddit_${id}_audio`, - filename: `reddit_${id}.mp4` - }; - } catch (err) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/soundcloud.js b/src/modules/services/soundcloud.js deleted file mode 100644 index e6026cb5..00000000 --- a/src/modules/services/soundcloud.js +++ /dev/null @@ -1,90 +0,0 @@ -import { genericUserAgent, maxAudioDuration } from "../config.js"; - -let cachedID = {} - -async function findClientID() { - try { - let sc = await fetch('https://soundcloud.com/').then((r) => { return r.text() }).catch(() => { return false }); - let scVersion = String(sc.match(/')[0]) - if (!json["media"]["transcodings"]) { - return { error: 'ErrorEmptyDownload' } - } - let clientId = await findClientID(); - if (!clientId) { - return { error: 'ErrorSoundCloudNoClientId' } - } - let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive") - let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; - if (!fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") { - return { error: 'ErrorEmptyDownload' } - } - if (json.duration > maxAudioDuration) { - return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] } - } - let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false }); - if (!file) { - return { error: 'ErrorCouldntFetch' }; - } - return { - urls: file, - audioFilename: `soundcloud_${json.id}`, - fileMetadata: { - title: json.title, - artist: json.user.username, - } - } - } catch (e) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/tiktok.js b/src/modules/services/tiktok.js deleted file mode 100644 index ab30311f..00000000 --- a/src/modules/services/tiktok.js +++ /dev/null @@ -1,117 +0,0 @@ -import { genericUserAgent } from "../config.js"; - -let userAgent = genericUserAgent.split(' Chrome/1')[0] -let config = { - tiktok: { - short: "https://vt.tiktok.com/", - api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9®ion=US&carrier_region=US", - }, - douyin: { - short: "https://v.douyin.com/", - api: "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={postId}", - } -} -function selector(j, h, id) { - if (!j) return false - let t; - switch (h) { - case "tiktok": - t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true }) - break; - case "douyin": - t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true }) - break; - } - if (!t.length > 0) return false - return t[0] -} - -export default async function(obj) { - try { - if (!obj.postId) { - let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, { - redirect: "manual", - headers: { "user-agent": userAgent } - }).then((r) => { return r.text() }).catch(() => { return false }); - if (!html) return { error: 'ErrorCouldntFetch' }; - - if (html.slice(0, 17) === ' { return r.json() }).catch(() => { return false }); - - detail = selector(detail, obj.host, obj.postId); - - if (!detail) return { error: 'ErrorCouldntFetch' } - - let video, videoFilename, audioFilename, isMp3, audio, images, - filenameBase = `${obj.host}_${obj.postId}`; - if (obj.host == "tiktok") { - images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false - } else { - images = detail["images"] ? detail["images"] : false - } - if (!obj.isAudioOnly && !images) { - video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0] - videoFilename = `${filenameBase}_video.mp4` - if (obj.noWatermark) { - video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play"); - videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark - } - } else { - let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0]; - audio = fallback; - audioFilename = `${filenameBase}_audio_fv`; // fv - from video - if (obj.fullAudio || fallback.includes("music")) { - audio = detail["music"]["play_url"]["url_list"][0] - audioFilename = `${filenameBase}_audio` - } - if (audio.slice(-4) === ".mp3") isMp3 = true; - } - if (video) return { - urls: video, - filename: videoFilename - } - if (images && obj.isAudioOnly) { - return { - urls: audio, - audioFilename: audioFilename, - isAudioOnly: true, - isMp3: isMp3, - } - } - if (images) { - let imageLinks = []; - for (let i in images) { - let sel = obj.host == "tiktok" ? images[i]["display_image"]["url_list"] : images[i]["url_list"]; - sel = sel.filter((p) => { if (p.includes(".jpeg?")) return true; }) - imageLinks.push({url: sel[0]}) - } - return { - picker: imageLinks, - urls: audio, - audioFilename: audioFilename, - isAudioOnly: true, - isMp3: isMp3, - } - } - if (audio) return { - urls: audio, - audioFilename: audioFilename, - isAudioOnly: true, - isMp3: isMp3, - } - } catch (e) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/tumblr.js b/src/modules/services/tumblr.js deleted file mode 100644 index 372388ac..00000000 --- a/src/modules/services/tumblr.js +++ /dev/null @@ -1,17 +0,0 @@ -import { genericUserAgent } from "../config.js"; - -export default async function(obj) { - try { - let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', ''); - let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, { - headers: {"user-agent": genericUserAgent} - }).then((r) => { return r.text() }).catch(() => { return false }); - if (!html) return { error: 'ErrorCouldntFetch' }; - if (!html.includes('property="og:video" content="https://va.media.tumblr.com/')) { - return { error: 'ErrorEmptyDownload' } - } - return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, audioFilename: `tumblr_${obj.id}_audio` } - } catch (e) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/twitter.js b/src/modules/services/twitter.js deleted file mode 100644 index 32e7918a..00000000 --- a/src/modules/services/twitter.js +++ /dev/null @@ -1,100 +0,0 @@ -import { genericUserAgent } from "../config.js"; - -function bestQuality(arr) { - return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0] -} -const apiURL = "https://api.twitter.com/1.1" - -export default async function(obj) { - try { - let _headers = { - "user-agent": genericUserAgent, - "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", - "host": "api.twitter.com" - }; - let req_act = await fetch(`${apiURL}/guest/activate.json`, { - method: "POST", - headers: _headers - }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); - - if (!req_act) return { error: 'ErrorCouldntFetch' }; - _headers["x-guest-token"] = req_act["guest_token"]; - let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1` - if (!obj.spaceId) { - let req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch((e) => { return false}); - if (!req_status) { - _headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"; - delete _headers["x-guest-token"] - - req_act = await fetch(`${apiURL}/guest/activate.json`, { - method: "POST", - headers: _headers - }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); - if (!req_act) return { error: 'ErrorCouldntFetch' }; - - _headers["x-guest-token"] = req_act["guest_token"]; - req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); - } - if (!req_status) return { error: 'ErrorCouldntFetch' } - if (!req_status["extended_entities"] && req_status["extended_entities"]["media"]) { - return { error: 'ErrorNoVideosInTweet' } - } - let single, multiple = [], media = req_status["extended_entities"]["media"]; - media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true }) - if (media.length > 1) { - for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } - } else if (media.length === 1) { - single = bestQuality(media[0]["video_info"]["variants"]) - } else { - return { error: 'ErrorNoVideosInTweet' } - } - if (single) { - return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` } - } else if (multiple) { - return { picker: multiple } - } else { - return { error: 'ErrorNoVideosInTweet' } - } - } else { - _headers["host"] = "twitter.com" - _headers["content-type"] = "application/json" - let query = { - variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withSuperFollowsUserFields":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":true,"withReplays":true}, features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"verified_phone_label_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_uc_gql_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":false,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"responsive_web_enhance_cards_enabled":true} - } - - let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then((r) => { - return r.status == 200 ? r.json() : false; - }).catch((e) => {return false}); - - if (!AudioSpaceById) { - return { error: 'ErrorEmptyDownload' } - } - if (!AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) { - return { error: 'TwitterSpaceWasntRecorded' }; - } - let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, - { headers: _headers }).then((r) =>{return r.status == 200 ? r.json() : false;}).catch(() => {return false;}); - if (!streamStatus) return { error: 'ErrorCouldntFetch' }; - - let participants = AudioSpaceById.data.audioSpace.participants.speakers - let listOfParticipants = `Twitter Space speakers: ` - for (let i in participants) { - listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` - } - listOfParticipants = listOfParticipants.slice(0, -2); - return { - urls: streamStatus.source.noRedirectPlaybackUrl, - audioFilename: `twitterspaces_${obj.spaceId}`, - isAudioOnly: true, - fileMetadata: { - title: AudioSpaceById.data.audioSpace.metadata.title, - artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`, - comment: listOfParticipants, - // cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "") - } - } - } - } catch (err) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/vimeo.js b/src/modules/services/vimeo.js deleted file mode 100644 index fe1af72f..00000000 --- a/src/modules/services/vimeo.js +++ /dev/null @@ -1,84 +0,0 @@ -import { maxVideoDuration, quality, services } from "../config.js"; - -export default async function(obj) { - try { - let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => {return r.json()}).catch(() => {return false}); - if (!api) return { error: 'ErrorCouldntFetch' }; - - let downloadType = "dash"; - if (JSON.stringify(api).includes('"progressive":[{')) { - downloadType = "progressive"; - } - - switch(downloadType) { - case "progressive": - let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width)); - let best = all[0] - try { - if (obj.quality != "max") { - let pref = parseInt(quality[obj.quality], 10) - for (let i in all) { - let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10) - if (currQuality === pref) { - best = all[i]; - break - } - if (currQuality < pref) { - best = all[i-1]; - break - } - } - } - } catch (e) { - best = all[0] - } - return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` }; - case "dash": - if (api.video.duration > maxVideoDuration / 1000) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"]; - let masterJSON = await fetch(masterJSONURL).then((r) => {return r.json()}).catch(() => {return false}); - - if (!masterJSON) return { error: 'ErrorCouldntFetch' }; - if (!masterJSON.video) { - return { error: 'ErrorEmptyDownload' } - } - let type = "parcel"; - if (masterJSON.base_url == "../") { - type = "chop" - } - let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)); - let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;}); - - let bestVideo = masterJSON_Video[0] - let bestAudio = masterJSON_Audio[0] - switch (type) { - case "parcel": - if (obj.quality != "max") { - let pref = parseInt(quality[obj.quality], 10) - for (let i in masterJSON_Video) { - let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10) - if (currQuality < pref) { - break; - } else if (currQuality == pref) { - bestVideo = masterJSON_Video[i] - } - } - } - let baseUrl = masterJSONURL.split("/sep/")[0] - let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`; - let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`; - - return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` } - case "chop": // TO-DO: support chop type of streams - default: - return { error: 'ErrorEmptyDownload' } - } - default: - return { error: 'ErrorEmptyDownload' } - } - } catch (e) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/vk.js b/src/modules/services/vk.js deleted file mode 100644 index 0c4872be..00000000 --- a/src/modules/services/vk.js +++ /dev/null @@ -1,54 +0,0 @@ -import { xml2json } from "xml-js"; -import { genericUserAgent, maxVideoDuration, services } from "../config.js"; -import selectQuality from "../stream/selectQuality.js"; - -export default async function(obj) { - try { - let html; - html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, { - headers: {"user-agent": genericUserAgent} - }).then((r) => {return r.text()}).catch(() => {return false}); - if (!html) return { error: 'ErrorCouldntFetch' }; - if (!html.includes(`{"lang":`)) { - return { error: 'ErrorEmptyDownload' }; - } - let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); - if (!js["mvData"]["is_active_live"] == '0') { - return { error: 'ErrorLiveVideo' }; - } - if (js["mvData"]["duration"] > maxVideoDuration / 1000) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 })); - - let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]; - if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) { - repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"]; - } - let attr = repr[repr.length - 1]["_attributes"]; - let selectedQuality; - let qualities = Object.keys(services.vk.quality_match); - for (let i in qualities) { - if (qualities[i] == attr["height"]) { - selectedQuality = `url${attr["height"]}`; - break; - } - if (qualities[i] == attr["width"]) { - selectedQuality = `url${attr["width"]}`; - break; - } - } - let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1) - let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]); - let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"]; - if (!selectedQuality in js["player"]["params"][0]) { - return { error: 'ErrorEmptyDownload' }; - } - return { - urls: js["player"]["params"][0][`url${userQuality}`], - filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4` - }; - } catch (err) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/services/youtube.js b/src/modules/services/youtube.js deleted file mode 100644 index dfba7583..00000000 --- a/src/modules/services/youtube.js +++ /dev/null @@ -1,93 +0,0 @@ -import ytdl from "better-ytdl-core"; -import { maxVideoDuration, quality as mq } from "../config.js"; -import selectQuality from "../stream/selectQuality.js"; - -export default async function(obj) { - try { - let infoInitial = await ytdl.getInfo(obj.id); - if (!infoInitial) { - return { error: 'ErrorCantConnectToServiceAPI' }; - } - let info = infoInitial.formats; - if (info[0]["isLive"]) { - return { error: 'ErrorLiveVideo' }; - } - let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true; - }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - if (!obj.isAudioOnly) { - video = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) { - if (obj.quality != "max") { - if (a["hasAudio"] && mq[obj.quality] == a["height"]) { - fullVideoMatch.push(a) - } else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) { - videoMatch.push(a); - } - } - return true - } - }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - if (obj.quality != "max") { - if (videoMatch.length == 0) { - let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim()) - videoMatch = video.filter((a) => { - if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true; - }) - } else if (fullVideoMatch.length > 0) { - videoMatch = [fullVideoMatch[0]] - } - } else videoMatch = [video[0]]; - if (obj.quality == "los") videoMatch = [video[video.length - 1]]; - } - let generalMeta = { - title: infoInitial.videoDetails.title, - artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(), - } - if (audio[0]["approxDurationMs"] > maxVideoDuration) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - } - if (!obj.isAudioOnly && videoMatch.length > 0) { - if (video.length === 0 && audio.length === 0) { - return { error: 'ErrorBadFetch' }; - } - if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) { - return { - type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` - }; - } - return { - type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` - }; - } else if (!obj.isAudioOnly) { - return { - type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}` - }; - } else if (audio.length > 0) { - let r = { - type: "render", - isAudioOnly: true, - urls: audio[0]["url"], - audioFilename: `youtube_${obj.id}_audio`, - fileMetadata: generalMeta - }; - if (infoInitial.videoDetails.description) { - let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by"); - if (isAutoGenAudio) { - let descItems = infoInitial.videoDetails.description.split("\n\n") - r.fileMetadata.album = descItems[2] - r.fileMetadata.copyright = descItems[3] - if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim(); - } - } - return r - } else { - return { error: 'ErrorBadFetch' }; - } - } catch (e) { - return { error: 'ErrorBadFetch' }; - } -} diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 065c8c2d..5da23c3c 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -47,7 +47,8 @@ export function verifyStream(ip, id, hmac, exp) { return { error: 'this stream token does not exist', status: 400 }; } let ghmac = sha256(`${id},${streamInfo.service},${ip},${exp}`, salt); - if (hmac == ghmac && exp.toString() == streamInfo.exp && ghmac == streamInfo.hmac && ip == streamInfo.ip && exp > Math.floor(new Date().getTime())) { + if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac) + && String(ip) === streamInfo.ip && Number(exp) > Math.floor(new Date().getTime())) { return streamInfo; } return { error: 'Unauthorized', status: 401 }; diff --git a/src/modules/stream/selectQuality.js b/src/modules/stream/selectQuality.js index 9ddf1fdc..56244482 100644 --- a/src/modules/stream/selectQuality.js +++ b/src/modules/stream/selectQuality.js @@ -8,12 +8,12 @@ function closest(goal, array) { } export default function(service, quality, maxQuality) { - if (quality == "max") return maxQuality; + if (quality === "max") return maxQuality; quality = parseInt(mq[quality], 10) maxQuality = parseInt(maxQuality, 10) - if (quality >= maxQuality || quality == maxQuality) return maxQuality; + if (quality >= maxQuality || quality === maxQuality) return maxQuality; if (quality < maxQuality) { if (!services[service]["quality"][quality]) { diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 50ce1389..5160d83f 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -116,7 +116,7 @@ export function streamVideoOnly(streamInfo, res) { '-i', streamInfo.urls, '-c', 'copy', '-an' ] - 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'); const ffmpegProcess = spawn(ffmpeg, args, { windowsHide: true, diff --git a/src/modules/sub/currentCommit.js b/src/modules/sub/currentCommit.js index 7dfb1f9b..f3c145f5 100644 --- a/src/modules/sub/currentCommit.js +++ b/src/modules/sub/currentCommit.js @@ -1,10 +1,23 @@ import { execSync } from "child_process"; +let commit, commitInfo, branch; + export function shortCommit() { - return execSync('git rev-parse --short HEAD').toString().trim() + if (commit) return commit; + let c = execSync('git rev-parse --short HEAD').toString().trim(); + commit = c; + return c } export function getCommitInfo() { - let d = execSync(`git show -s --format='%s;;;%B'`).toString().trim().replace(/[\r\n]/gm, '\n').split(';;;') - d[1] = d[1].replace(d[0], '').trim().toString().replace(/[\r\n]/gm, '
') + if (commitInfo) return commitInfo; + let d = execSync(`git show -s --format='%s;;;%B'`).toString().trim().replace(/[\r\n]/gm, '\n').split(';;;'); + d[1] = d[1].replace(d[0], '').trim().toString().replace(/[\r\n]/gm, '
'); + commitInfo = d; return d } +export function getCurrentBranch() { + if (branch) return branch; + let b = execSync('git branch --show-current').toString().trim(); + branch = b; + return b +} diff --git a/src/modules/sub/errors.js b/src/modules/sub/errors.js index d4570ae3..c73ffe85 100644 --- a/src/modules/sub/errors.js +++ b/src/modules/sub/errors.js @@ -3,6 +3,9 @@ import loc from "../../localization/manager.js"; export function errorUnsupported(lang) { return loc(lang, 'ErrorUnsupported'); } -export function genericError(lang, host) { +export function brokenLink(lang, host) { return loc(lang, 'ErrorBrokenLink', host); } +export function genericError(lang, host) { + return loc(lang, 'ErrorBadFetch', host); +} diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 78efaf14..e8bcd9d2 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -103,10 +103,9 @@ export function checkJSONPost(obj) { } try { let objKeys = Object.keys(obj); - if (!(objKeys.length < 8 && obj.url)) { - return false - } + if (!(objKeys.length <= 8 && obj.url)) return false; let defKeys = Object.keys(def); + for (let i in objKeys) { if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) { if (apiVar.booleanOnly.includes(objKeys[i])) { @@ -116,12 +115,14 @@ export function checkJSONPost(obj) { } } } - obj["url"] = decodeURIComponent(String(obj["url"])) + + obj["url"] = decodeURIComponent(String(obj["url"])); let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), - host = hostname[hostname.length - 2] - def["url"] = encodeURIComponent(cleanURL(obj["url"], host)) + host = hostname[hostname.length - 2]; + def["url"] = encodeURIComponent(cleanURL(obj["url"], host)); + return def } catch (e) { - return false; + return false } } diff --git a/src/test/services.json b/src/test/services.json new file mode 100644 index 00000000..fcf38416 --- /dev/null +++ b/src/test/services.json @@ -0,0 +1,256 @@ +{ + "twitter": [{ + "name": "regular video", + "url": "https://twitter.com/TwitterSpaces/status/1526955853743546372?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "embedded twitter video", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "mixed media (image + gif)", + "url": "https://twitter.com/Twitter/status/1580661436132757506?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "picker: mixed media (3 gifs + image)", + "url": "https://twitter.com/emerald_pedrod/status/1582418163521581063?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "picker" + } + }, { + "name": "audio from embedded twitter video (mp3, isAudioOnly)", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": true, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "audio from embedded twitter video (best, isAudioOnly)", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "best", + "isAudioOnly": true, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "audio from embedded twitter video (ogg, isAudioOnly, isAudioMuted)", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "best", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "muted embedded twitter video", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "inexistent post", + "url": "https://twitter.com/test/status/9487653", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "post with no media content", + "url": "https://twitter.com/elonmusk/status/1604617643973124097?s=20", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "recorded space by nyc (best)", + "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "recorded space by nyc (mp3)", + "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "recorded space by nyc (wav, isAudioMuted)", + "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", + "params": { + "aFormat": "wav", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "recorded space by service95 & dualipa (mp3, isAudioMuted, isAudioOnly)", + "url": "https://twitter.com/i/spaces/1nAJErvvVXgxL", + "params": { + "aFormat": "mp3", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "unavailable space", + "url": "https://twitter.com/i/spaces/1OwGWwjRjVVGQ?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "inexistent space", + "url": "https://twitter.com/i/spaces/10Wkie2j29iiI", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 400, + "status": "error" + } + }], + "soundcloud": [{ + "name": "public song (best)", + "url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "public song (mp3, isAudioMuted)", + "url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "private song", + "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "private song (wav, isAudioMuted)", + "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "params": { + "aFormat": "wav", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "private song (ogg, isAudioMuted, isAudioOnly)", + "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "params": { + "aFormat": "ogg", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }] +} \ No newline at end of file diff --git a/src/test/test.js b/src/test/test.js new file mode 100644 index 00000000..ff4dc95e --- /dev/null +++ b/src/test/test.js @@ -0,0 +1,66 @@ +import "dotenv/config"; + +import { getJSON } from "../modules/api.js"; +import { services } from "../modules/config.js"; +import loadJSON from "../modules/sub/loadJSON.js"; +import { checkJSONPost } from "../modules/sub/utils.js"; + +let tests = loadJSON('./src/test/services.json'); + +let noTest = []; +let failed = []; +let success = 0; + +function addToFail(service, testName, url, response) { + failed.push({ + service: service, + name: testName, + url: url, + response: response + }) +} +for (let i in services) { + if (tests[i]) { + console.log(`\nRunning tests for ${i}...\n`) + for (let k = 0; k < tests[i].length; k++) { + let test = tests[i][k]; + + console.log(`Running test ${k+1}: ${test.name}`); + console.log('params:'); + let params = {...{url: test.url}, ...test.params}; + console.log(params); + + let chck = checkJSONPost(params); + if (chck) { + chck["ip"] = "d21ec524bc2ade41bef569c0361ac57728c69e2764b5cb3cb310fe36568ca53f"; // random sha256 + let j = await getJSON(chck["url"], "en", chck); + console.log('\nReceived:'); + console.log(j) + if (j.status === test.expected.code && j.body.status === test.expected.status) { + console.log("\n✅ Success.\n"); + success++ + } else { + console.log(`\n❌ Fail. Expected: ${test.expected.code} & ${test.expected.status}, received: ${j.status} & ${j.body.status}\n`); + addToFail(i, test.name, test.url, j) + } + } else { + console.log("\n❌ couldn't validate the request JSON.\n"); + addToFail(i, test.name, test.url, {}) + } + } + console.log("\n\n") + } else { + console.warn(`No tests found for ${i}.`); + noTest.push(i) + } +} + +console.log(`\n✅ ${success} tests succeeded.`); +console.log(`❌ ${failed.length} tests failed.`); +console.log(`❔ ${noTest.length} services weren't tested.`); + +console.log(`\nFailed tests:`); +console.log(failed) + +console.log(`\nMissing tests:`); +console.log(noTest) From 0e7a281366d4a71605698f1cf9011b1d14bf0d8e Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 Feb 2023 13:41:28 +0600 Subject: [PATCH 03/11] accidentally left error logging --- src/modules/processing/match.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 654a9f5a..c851697f 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -124,7 +124,6 @@ export default async function (host, patternMatch, url, lang, obj) { return matchActionDecider(r, host, obj.ip, obj.aFormat, isAudioOnly, lang, isAudioMuted); } catch (e) { - console.log(e) return apiJSON(0, { t: genericError(lang, host) }) } } From 75a85972aa281903f14bc5f44dacc7531dd8cdde Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 19:44:58 +0600 Subject: [PATCH 04/11] 5.0 - finished writing tests for all services - fixed douyin support - fixed tiktok picker that was broken by previous commit - temporarily removed douyin photos from list of supported services - fixed support for "user view" vk clip links - slightly improved the testing script --- package.json | 2 +- src/modules/api.js | 10 +- src/modules/processing/matchActionDecider.js | 6 +- src/modules/processing/services/bilibili.js | 2 +- src/modules/processing/services/tiktok.js | 32 +- src/modules/processing/servicesConfig.json | 6 +- src/modules/sub/utils.js | 1 + src/test/services.json | 256 ------- src/test/test.js | 23 +- src/test/tests.json | 742 +++++++++++++++++++ 10 files changed, 784 insertions(+), 296 deletions(-) delete mode 100644 src/test/services.json create mode 100644 src/test/tests.json diff --git a/package.json b/package.json index 581536df..a271a701 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "5.0-dev1", + "version": "5.0-dev2", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/modules/api.js b/src/modules/api.js index 1e0d9a92..9dc8a236 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -36,16 +36,14 @@ export async function getJSON(originalURL, lang, obj) { } break; } - if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) { - return apiJSON(0, { t: errorUnsupported(lang) }); - } + if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) return apiJSON(0, { t: errorUnsupported(lang) }); + for (let i in patterns[host]["patterns"]) { patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]); if (patternMatch) break; } - if (!patternMatch) { - return apiJSON(0, { t: errorUnsupported(lang) }); - } + if (!patternMatch) return apiJSON(0, { t: errorUnsupported(lang) }); + return await match(host, patternMatch, url, lang, obj); } catch (e) { return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }); diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 7e76d602..fb0892a4 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -12,11 +12,11 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute filename: r.filename, }, params = {} - - if (isAudioMuted) action = "muteVideo"; + if (!isAudioOnly && !r.picker && !isAudioMuted) action = "video"; + if (isAudioOnly && !r.picker) action = "audio"; if (r.picker) action = "picker"; - if (isAudioOnly) action = "audio"; + if (isAudioMuted) action = "muteVideo"; if (action === "picker" || action === "audio") { defaultParams.filename = r.audioFilename; diff --git a/src/modules/processing/services/bilibili.js b/src/modules/processing/services/bilibili.js index 8f9ab286..82964a3a 100644 --- a/src/modules/processing/services/bilibili.js +++ b/src/modules/processing/services/bilibili.js @@ -1,6 +1,6 @@ import { genericUserAgent, maxVideoDuration } from "../../config.js"; -// TO-DO: quality picking & bilibili.tv support +// TO-DO: quality picking, bilibili.tv support, and higher quality downloads (currently requires an account) export default async function(obj) { let html = await fetch(`https://bilibili.com/video/${obj.id}`, { headers: { "user-agent": genericUserAgent } diff --git a/src/modules/processing/services/tiktok.js b/src/modules/processing/services/tiktok.js index 4ac0d19c..ccd10f2c 100644 --- a/src/modules/processing/services/tiktok.js +++ b/src/modules/processing/services/tiktok.js @@ -8,7 +8,7 @@ let userAgent = genericUserAgent.split(' Chrome/1')[0], }, douyin: { short: "https://v.douyin.com/", - api: "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={postId}", + api: "https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id={postId}", } } @@ -17,14 +17,14 @@ function selector(j, h, id) { let t; switch (h) { case "tiktok": - t = j["aweme_list"].filter((v) => { if (v["aweme_id"] === id) return true }); + t = j["aweme_list"].filter((v) => { if (v["aweme_id"] === id) return true })[0]; break; case "douyin": - t = j['item_list'].filter((v) => { if (v["aweme_id"] === id) return true }); + t = j['aweme_detail']; break; } - if (!t.length > 0) return false; - return t[0]; + if (t.length < 3) return false; + return t; } export default async function(obj) { @@ -34,7 +34,7 @@ export default async function(obj) { headers: { "user-agent": userAgent } }).then((r) => { return r.text() }).catch(() => { return false }); if (!html) return { error: 'ErrorCouldntFetch' }; - + if (html.slice(0, 17) === '
', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@"] switch(host) { + case "vk": case "youtube": url = url.split('&')[0]; break; diff --git a/src/test/services.json b/src/test/services.json deleted file mode 100644 index fcf38416..00000000 --- a/src/test/services.json +++ /dev/null @@ -1,256 +0,0 @@ -{ - "twitter": [{ - "name": "regular video", - "url": "https://twitter.com/TwitterSpaces/status/1526955853743546372?s=20", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "redirect" - } - }, { - "name": "embedded twitter video", - "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "redirect" - } - }, { - "name": "mixed media (image + gif)", - "url": "https://twitter.com/Twitter/status/1580661436132757506?s=20", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "redirect" - } - }, { - "name": "picker: mixed media (3 gifs + image)", - "url": "https://twitter.com/emerald_pedrod/status/1582418163521581063?s=20", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "picker" - } - }, { - "name": "audio from embedded twitter video (mp3, isAudioOnly)", - "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", - "params": { - "aFormat": "mp3", - "isAudioOnly": true, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "audio from embedded twitter video (best, isAudioOnly)", - "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", - "params": { - "aFormat": "best", - "isAudioOnly": true, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "audio from embedded twitter video (ogg, isAudioOnly, isAudioMuted)", - "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", - "params": { - "aFormat": "best", - "isAudioOnly": true, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "muted embedded twitter video", - "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "inexistent post", - "url": "https://twitter.com/test/status/9487653", - "params": { - "aFormat": "best", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 400, - "status": "error" - } - }, { - "name": "post with no media content", - "url": "https://twitter.com/elonmusk/status/1604617643973124097?s=20", - "params": { - "aFormat": "best", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 400, - "status": "error" - } - }, { - "name": "recorded space by nyc (best)", - "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", - "params": { - "aFormat": "best", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "recorded space by nyc (mp3)", - "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "recorded space by nyc (wav, isAudioMuted)", - "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", - "params": { - "aFormat": "wav", - "isAudioOnly": false, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "recorded space by service95 & dualipa (mp3, isAudioMuted, isAudioOnly)", - "url": "https://twitter.com/i/spaces/1nAJErvvVXgxL", - "params": { - "aFormat": "mp3", - "isAudioOnly": true, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "unavailable space", - "url": "https://twitter.com/i/spaces/1OwGWwjRjVVGQ?s=20", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 400, - "status": "error" - } - }, { - "name": "inexistent space", - "url": "https://twitter.com/i/spaces/10Wkie2j29iiI", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 400, - "status": "error" - } - }], - "soundcloud": [{ - "name": "public song (best)", - "url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing", - "params": { - "aFormat": "best", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "public song (mp3, isAudioMuted)", - "url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "private song", - "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "private song (wav, isAudioMuted)", - "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", - "params": { - "aFormat": "wav", - "isAudioOnly": false, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "private song (ogg, isAudioMuted, isAudioOnly)", - "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", - "params": { - "aFormat": "ogg", - "isAudioOnly": true, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }] -} \ No newline at end of file diff --git a/src/test/test.js b/src/test/test.js index ff4dc95e..f03c8dbb 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -5,17 +5,18 @@ import { services } from "../modules/config.js"; import loadJSON from "../modules/sub/loadJSON.js"; import { checkJSONPost } from "../modules/sub/utils.js"; -let tests = loadJSON('./src/test/services.json'); +let tests = loadJSON('./src/test/tests.json'); let noTest = []; let failed = []; let success = 0; -function addToFail(service, testName, url, response) { +function addToFail(service, testName, url, status, response) { failed.push({ service: service, name: testName, url: url, + status: status, response: response }) } @@ -41,11 +42,11 @@ for (let i in services) { success++ } else { console.log(`\n❌ Fail. Expected: ${test.expected.code} & ${test.expected.status}, received: ${j.status} & ${j.body.status}\n`); - addToFail(i, test.name, test.url, j) + addToFail(i, test.name, test.url, j.body.status, j) } } else { console.log("\n❌ couldn't validate the request JSON.\n"); - addToFail(i, test.name, test.url, {}) + addToFail(i, test.name, test.url, "unknown", {}) } } console.log("\n\n") @@ -55,12 +56,16 @@ for (let i in services) { } } -console.log(`\n✅ ${success} tests succeeded.`); +console.log(`✅ ${success} tests succeeded.`); console.log(`❌ ${failed.length} tests failed.`); console.log(`❔ ${noTest.length} services weren't tested.`); -console.log(`\nFailed tests:`); -console.log(failed) +if (failed.length > 0) { + console.log(`\nFailed tests:`); + console.log(failed) +} -console.log(`\nMissing tests:`); -console.log(noTest) +if (noTest.length > 0) { + console.log(`\nMissing tests:`); + console.log(noTest) +} diff --git a/src/test/tests.json b/src/test/tests.json new file mode 100644 index 00000000..e4935177 --- /dev/null +++ b/src/test/tests.json @@ -0,0 +1,742 @@ +{ + "twitter": [{ + "name": "regular video", + "url": "https://twitter.com/TwitterSpaces/status/1526955853743546372?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "embedded twitter video", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "mixed media (image + gif)", + "url": "https://twitter.com/Twitter/status/1580661436132757506?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "picker: mixed media (3 gifs + image)", + "url": "https://twitter.com/emerald_pedrod/status/1582418163521581063?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "picker" + } + }, { + "name": "audio from embedded twitter video (mp3, isAudioOnly)", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": true, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "audio from embedded twitter video (best, isAudioOnly)", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "best", + "isAudioOnly": true, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "audio from embedded twitter video (ogg, isAudioOnly, isAudioMuted)", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "best", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "muted embedded twitter video", + "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "inexistent post", + "url": "https://twitter.com/test/status/9487653", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "post with no media content", + "url": "https://twitter.com/elonmusk/status/1604617643973124097?s=20", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "recorded space by nyc (best)", + "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "recorded space by nyc (mp3)", + "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "recorded space by nyc (wav, isAudioMuted)", + "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", + "params": { + "aFormat": "wav", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "recorded space by service95 & dualipa (mp3, isAudioMuted, isAudioOnly)", + "url": "https://twitter.com/i/spaces/1nAJErvvVXgxL", + "params": { + "aFormat": "mp3", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "unavailable space", + "url": "https://twitter.com/i/spaces/1OwGWwjRjVVGQ?s=20", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "inexistent space", + "url": "https://twitter.com/i/spaces/10Wkie2j29iiI", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }], + "soundcloud": [{ + "name": "public song (best)", + "url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing", + "params": { + "aFormat": "best", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "public song (mp3, isAudioMuted)", + "url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "private song", + "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "private song (wav, isAudioMuted)", + "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "params": { + "aFormat": "wav", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "private song (ogg, isAudioMuted, isAudioOnly)", + "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "params": { + "aFormat": "ogg", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }], + "youtube": [{ + "name": "4k video (mp4, hig)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "mp4", + "vQuality": "hig", + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (webm, mid)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "webm", + "vQuality": "mid", + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (mp4, max)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "mp4", + "vQuality": "max", + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (webm, max)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "webm", + "vQuality": "max", + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (webm, max, isAudioMuted)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "webm", + "vQuality": "max", + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (mp4, max, isAudioMuted)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "webm", + "vQuality": "max", + "aFormat": "mp3", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (mp4, max, isAudioMuted, isAudioOnly, mp3)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "webm", + "vQuality": "max", + "aFormat": "mp3", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (mp4, max, isAudioMuted, isAudioOnly, best)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vFormat": "webm", + "vQuality": "max", + "aFormat": "best", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "music (mp3, isAudioOnly, isAudioMuted)", + "url": "https://music.youtube.com/watch?v=5rGTsvZCEdk&feature=share", + "params": { + "aFormat": "mp3", + "isAudioOnly": true, + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "music (mp3)", + "url": "https://music.youtube.com/watch?v=5rGTsvZCEdk&feature=share", + "params": { + "aFormat": "mp3", + "isAudioOnly": false, + "isAudioMuted": false + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "short, defaults", + "url": "https://www.youtube.com/shorts/r5FpeOJItbw", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "inexistent video", + "url": "https://youtube.com/watch?v=gnjuHYWGEW", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }], + "vk": [{ + "name": "clip, defaults", + "url": "https://vk.com/clip-57274055_456239788", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "clip, low", + "url": "https://vk.com/clip-57274055_456239788", + "params": { + "vQuality": "low" + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "clip different link, max", + "url": "https://vk.com/clips-57274055?z=clip-57274055_456239788", + "params": { + "vQuality": "max" + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "video, defaults", + "url": "https://vk.com/video-57274055_456239399", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "inexistent video", + "url": "https://vk.com/video-53333333_456233333", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }], + "douyin": [{ + "name": "short link video, with watermark", + "url": "https://v.douyin.com/2p4Aya7/", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "short link video (isNoTTWatermark)", + "url": "https://v.douyin.com/2p4Aya7/", + "params": { + "isNoTTWatermark": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "short link video (isAudioOnly)", + "url": "https://v.douyin.com/2p4Aya7/", + "params": { + "isAudioOnly": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "short link video (isAudioOnly, isTTFullAudio)", + "url": "https://v.douyin.com/2p4Aya7/", + "params": { + "isAudioOnly": true, + "isTTFullAudio": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "long link video (isNoTTWatermark)", + "url": "https://www.douyin.com/video/7120601033314716968", + "params": { + "isNoTTWatermark": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "images", + "url": "https://v.douyin.com/MdVwo31/", + "params": {}, + "expected": { + "code": 200, + "status": "picker" + } + }, { + "name": "long link inexistent", + "url": "https://www.douyin.com/video/7120851458451417478", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "short link inexistent", + "url": "https://v.douyin.com/2p4ewa7/", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }], + "tiktok": [{ + "name": "short link (vt) video, with watermark", + "url": "https://vt.tiktok.com/ZS85U86aa/", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "short link (vt) video (isNoTTWatermark)", + "url": "https://vt.tiktok.com/ZS85U86aa/", + "params": { + "isNoTTWatermark": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "short link (vm) video (isAudioOnly)", + "url": "https://vm.tiktok.com/ZMYrYAf34/", + "params": { + "isAudioOnly": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "short link (vm) video (isAudioOnly, isTTFullAudio)", + "url": "https://vm.tiktok.com/ZMYrYAf34/", + "params": { + "isAudioOnly": true, + "isTTFullAudio": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "long link video (isNoTTWatermark)", + "url": "https://www.tiktok.com/@fatfatmillycat/video/7195741644585454894", + "params": { + "isNoTTWatermark": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "images", + "url": "https://vt.tiktok.com/ZS8JP89eB/", + "params": {}, + "expected": { + "code": 200, + "status": "picker" + } + }, { + "name": "long link inexistent", + "url": "https://www.tiktok.com/@blablabla/video/7120851458451417478", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }, { + "name": "short link inexistent", + "url": "https://vt.tiktok.com/2p4ewa7/", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }], + "bilibili": [{ + "name": "1080p video", + "url": "https://www.bilibili.com/video/BV18i4y1m7xV/", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "1080p video muted", + "url": "https://www.bilibili.com/video/BV18i4y1m7xV/", + "params": { + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "1080p vertical video", + "url": "https://www.bilibili.com/video/BV1uu411z7VV/", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "1080p vertical video muted", + "url": "https://www.bilibili.com/video/BV1uu411z7VV/", + "params": { + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }], + "tumblr": [{ + "name": "at.tumblr link", + "url": "https://at.tumblr.com/music/704177038274281472/n7x7pr7x4w2b", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "user subdomain link", + "url": "https://garfield-69.tumblr.com/post/696499862852780032", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "web app link", + "url": "https://www.tumblr.com/rongzhi/707729381162958848/english-added-by-me?source=share", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }], + "vimeo": [{ + "name": "4k progressive", + "url": "https://vimeo.com/288386543", + "params": { + "vQuality": "max" + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "720p progressive", + "url": "https://vimeo.com/288386543", + "params": { + "vQuality": "mid" + }, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "1080p dash parcel", + "url": "https://vimeo.com/774694040", + "params": { + "vQuality": "hig" + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "720p dash parcel", + "url": "https://vimeo.com/774694040", + "params": { + "vQuality": "mid" + }, + "expected": { + "code": 200, + "status": "stream" + } + }], + "reddit": [{ + "name": "video with audio", + "url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "video with audio (isAudioOnly)", + "url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3", + "params": { + "isAudioOnly": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "video with audio (isAudioMuted)", + "url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3", + "params": { + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "video without audio", + "url": "https://www.reddit.com/r/catvideos/comments/ftoeo7/luna_doesnt_want_to_be_bothered_while_shes_napping/?utm_source=share&utm_medium=web2x&context=3", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "actual gif, not looping video", + "url": "https://www.reddit.com/r/whenthe/comments/109wqy1/god_really_did_some_trolling/?utm_source=share&utm_medium=web2x&context=3", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }] +} \ No newline at end of file From 18199c534fda4633df079270dd8b6b317f7b8c12 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 19:49:18 +0600 Subject: [PATCH 05/11] changed the socialLink element class some adblocking filters block .social-link class, and this is not an ad, at all :/ --- src/front/cobalt.css | 2 +- src/modules/pageRender/elements.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/front/cobalt.css b/src/front/cobalt.css index eb10b61c..88c5b133 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -278,7 +278,7 @@ input[type="checkbox"] { .italic { font-style: italic; } -.social-link { +.cobalt-support-link { display: flex; flex-direction: row; justify-content: flex-start; diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index f6300f23..43a40363 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -129,7 +129,7 @@ export function backdropLink(link, text) { return `${text}` } export function socialLink(emoji, name, handle, url) { - return `` + return `` } export function settingsCategory(obj) { return `
From 3c578d6d49e63caf21e1749de38ed032ca088902 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 20:02:52 +0600 Subject: [PATCH 06/11] cleaning up what i missed --- src/cobalt.js | 4 ++-- src/modules/pageRender/onDemand.js | 2 +- src/modules/processing/services/reddit.js | 2 +- src/modules/processing/services/soundcloud.js | 2 +- src/modules/processing/services/twitter.js | 2 +- src/modules/processing/services/vimeo.js | 4 ++-- src/modules/processing/services/vk.js | 2 +- src/modules/processing/services/youtube.js | 14 +++++++------- src/modules/stream/types.js | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/cobalt.js b/src/cobalt.js index e8173cca..f21835a4 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -69,8 +69,8 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && try { JSON.parse(buf); if (buf.length > 720) throw new Error(); - if (req.header('Content-Type') != "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid content type header' }) - if (req.header('Accept') != "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid accept header' }) + if (String(req.header('Content-Type')) !== "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid content type header' }) + if (String(req.header('Accept')) !== "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid accept header' }) } catch(e) { res.status(500).json({ 'status': 'error', 'text': 'invalid json body.' }) } diff --git a/src/modules/pageRender/onDemand.js b/src/modules/pageRender/onDemand.js index 7ab1373a..6fed7c0b 100644 --- a/src/modules/pageRender/onDemand.js +++ b/src/modules/pageRender/onDemand.js @@ -6,7 +6,7 @@ export function changelogHistory() { // blockId 0 let historyLen = history.length for (let i in history) { - let separator = (i != 0 && i != historyLen) ? '
' : '' + let separator = (i !== 0 && i !== historyLen) ? '
' : '' render += `${separator}${history[i]["banner"] ? `
` : ''}` } return render; diff --git a/src/modules/processing/services/reddit.js b/src/modules/processing/services/reddit.js index 4465ec04..30d51b71 100644 --- a/src/modules/processing/services/reddit.js +++ b/src/modules/processing/services/reddit.js @@ -13,7 +13,7 @@ export default async function(obj) { let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0], audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`; - await fetch(audio, { method: "HEAD" }).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''}); + await fetch(audio, { method: "HEAD" }).then((r) => {if (Number(r.status) !== 200) audio = ''}).catch(() => {audio = ''}); let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]; if (!audio.length > 0) return { typeId: 1, urls: video }; diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js index 312f60ef..aa3a695a 100644 --- a/src/modules/processing/services/soundcloud.js +++ b/src/modules/processing/services/soundcloud.js @@ -56,7 +56,7 @@ export default async function(obj) { let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive") let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; - if (!fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' }; + if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' }; if (json.duration > maxAudioDuration) return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }; diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js index f46c63a4..77eb375e 100644 --- a/src/modules/processing/services/twitter.js +++ b/src/modules/processing/services/twitter.js @@ -74,7 +74,7 @@ export default async function(obj) { if (!AudioSpaceById) return { error: 'ErrorEmptyDownload' }; if (!AudioSpaceById.data.audioSpace.metadata) return { error: 'ErrorEmptyDownload' }; - if (!AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) return { error: 'TwitterSpaceWasntRecorded' }; + if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay !== true) return { error: 'TwitterSpaceWasntRecorded' }; let streamStatus = await fetch( `https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers } diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js index 534ce05d..7f402597 100644 --- a/src/modules/processing/services/vimeo.js +++ b/src/modules/processing/services/vimeo.js @@ -13,7 +13,7 @@ export default async function(obj) { let best = all[0]; try { - if (obj.quality != "max") { + if (obj.quality !== "max") { let pref = parseInt(quality[obj.quality], 10) for (let i in all) { let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10) @@ -50,7 +50,7 @@ export default async function(obj) { switch (type) { case "parcel": - if (obj.quality != "max") { + if (obj.quality !== "max") { let pref = parseInt(quality[obj.quality], 10) for (let i in masterJSON_Video) { let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10) diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js index 2ab7feb7..c30d1e7f 100644 --- a/src/modules/processing/services/vk.js +++ b/src/modules/processing/services/vk.js @@ -12,7 +12,7 @@ export default async function(obj) { let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); - if (!Number(js["mvData"]["is_active_live"]) === 0) return { error: 'ErrorLiveVideo' }; + if (Number(js["mvData"]["is_active_live"]) !== 0) return { error: 'ErrorLiveVideo' }; if (js["mvData"]["duration"] > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 })); diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 8ec87453..d96af51f 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -12,7 +12,7 @@ export default async function(obj) { let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true + if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] === obj.format) return true }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); if (audio.length === 0) return { error: 'ErrorBadFetch' }; @@ -20,11 +20,11 @@ export default async function(obj) { if (!isAudioOnly) { video = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) { - if (obj.quality != "max") { - if (a["hasAudio"] && mq[obj.quality] == a["height"]) { + if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] === obj.format) { + if (obj.quality !== "max") { + if (a["hasAudio"] && String(mq[obj.quality]) === String(a["height"])) { fullVideoMatch.push(a) - } else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) { + } else if (!a["hasAudio"] && String(mq[obj.quality]) === String(a["height"])) { videoMatch.push(a) } } @@ -32,11 +32,11 @@ export default async function(obj) { } }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - if (obj.quality != "max") { + if (obj.quality !== "max") { if (videoMatch.length === 0) { let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim()); videoMatch = video.filter((a) => { - if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true + if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() === String(ss)) return true }) } else if (fullVideoMatch.length > 0) { videoMatch = [fullVideoMatch[0]] diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 5160d83f..ef77d238 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -27,7 +27,7 @@ export function streamDefault(streamInfo, res) { } export function streamLiveRender(streamInfo, res) { try { - if (!streamInfo.urls.length === 2) { + if (streamInfo.urls.length !== 2) { res.end(); return; } From 20ae9acfe870937e04f6e5a97a20d439b022a690 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 20:23:48 +0600 Subject: [PATCH 07/11] 5.0 --- package.json | 2 +- src/front/updateBanners/valentines.webp | Bin 0 -> 128850 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/front/updateBanners/valentines.webp diff --git a/package.json b/package.json index a271a701..791b8a41 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "5.0-dev2", + "version": "5.0", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/front/updateBanners/valentines.webp b/src/front/updateBanners/valentines.webp new file mode 100644 index 0000000000000000000000000000000000000000..8e18cd1ba2af3633185c81ea1c0ec50bbe8eb6c4 GIT binary patch literal 128850 zcmV(;K-<4kNk&FW_W=M`MM6+kP&il$0000I0002-0RVLY06|VkO$Gn}0RR90{{R3% zPEAIHK>z>%000000Pz6;bpZgT00073P&goLK>z@7Q30I+D)Iq#0X~sFnMb9gqM@#H z++eU02~Ec!{LhJ8_ZV^m7>;@0$~1(CUT!|JGTrw4^lyF-#KRHASudCWJO5wC{Z|;9 zwcgAf$^7@!ry@JYz-Q`T;=LcI_w}xVxbzQG|CgRm+_&h0|8$O`Ndzhcei?u5-V|%s zSG*WH80ArBKbQTeXgBY-NS`sa!QQ{7rK6xDQwAOu`X_Y^+JX@Gc~n^<7+_}7J^j6< zZoUwBmv1J9+If_Yh(8PYl^ZR5e5uqOeb;&Cpii9@C=qrSDLgx%l?0FQ?L$Y_dl6&? zp!IrG>vz&wJPZH{HdL7E)y)oD}4X+rSj|D+6eEK1dl0{vjw+K6P;Ke4bxAnRu z^G=>$uN%7#Su4Qci6KOqw)F0#f$Gf%I{Mysf0dx;-oYu+ULZF5PojVhSa(q6biVht zeL!}B**9zJ5g?dJn6c14{#xXJ{$MV`Zyiq=Q^IkC-cfH~gbj1do^GJ=wr!jHE>)q%RV+>Y!A(khu zh<~%(DFniXm=>MoE>Fy-%<+R=jBj(IpLD*Ob4{&$K_xTP?2P&M3bd4#hy~y^nw9(vq;xtggTMqMp1mSTvbaOgupug z%$a?vx*2zWb#;jErpB-+VqNqXZ6|$!GK6B3ymHR(IksAiWh%A8y~Ew**^x9Pp~+-j zHwa1CfQdpP*~dz8MS{AxD00D6oS#s3YQEUynA%>Kj5zsL^m{v@VXaS#Gnt~xdftZE zHsyeudib4LKmkSl(}4RgD~CZ*5_m1I0tul!_5EdgEWMzru+(w)Lze2gZqC9v zxJ!6y))xIJ4tmH6)0gpRb78l9$tT;_O?KrLx{9C61BLYk?{OKmP zwgZh+{4>BF7T;RT& z^G8;KJ5hLgr}(8X|CIR_XIw-8Hot^(H>Zfq3`NoIK)do1u zWy`8Y49br24$4bMRT|oYxLo!0>Mh@V_TIAkn)x5?!7uo>+X^MrQ;fk^b-(�>9bm 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�aMUOIJrpK^fS0J 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 Ck*V+i literal 0 HcmV?d00001 From a1fea0a5f10907c4d405674a6211276b9aaa756a Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 20:30:57 +0600 Subject: [PATCH 08/11] fixes --- src/front/cobalt.js | 2 +- src/modules/processing/match.js | 2 +- src/modules/processing/services/reddit.js | 2 +- src/modules/processing/services/tiktok.js | 16 +++++++++------- src/modules/processing/services/vk.js | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 13333ae9..0628d355 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -273,7 +273,7 @@ function toggle(toggl) { } function loadSettings() { try { - if (typeof(navigator.clipboard.readText) === undefined) throw new Error(); + if (typeof(navigator.clipboard.readText) == "undefined") throw new Error(); } catch (err) { eid("pasteFromClipboard").style.display = "none" } diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index c851697f..3336bb4a 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -1,5 +1,5 @@ import { apiJSON } from "../sub/utils.js"; -import { errorUnsupported, genericError } from "../sub/errors.js"; +import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js"; import loc from "../../localization/manager.js"; diff --git a/src/modules/processing/services/reddit.js b/src/modules/processing/services/reddit.js index 30d51b71..816a8da6 100644 --- a/src/modules/processing/services/reddit.js +++ b/src/modules/processing/services/reddit.js @@ -8,7 +8,7 @@ export default async function(obj) { if (data.url.endsWith('.gif')) return { typeId: 1, urls: data.url }; - if (!"reddit_video" in data["secure_media"]) return { error: 'ErrorEmptyDownload' }; + if (!("reddit_video" in data["secure_media"])) return { error: 'ErrorEmptyDownload' }; if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0], diff --git a/src/modules/processing/services/tiktok.js b/src/modules/processing/services/tiktok.js index ccd10f2c..dac6fe43 100644 --- a/src/modules/processing/services/tiktok.js +++ b/src/modules/processing/services/tiktok.js @@ -28,7 +28,9 @@ function selector(j, h, id) { } export default async function(obj) { - if (!obj.postId) { + let postId = obj.postId ? obj.postId : false; + + if (!postId) { let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, { redirect: "manual", headers: { "user-agent": userAgent } @@ -36,22 +38,22 @@ export default async function(obj) { if (!html) return { error: 'ErrorCouldntFetch' }; if (html.slice(0, 17) === ' { return r.json() }).catch(() => { return false }); - detail = selector(detail, obj.host, obj.postId); + detail = selector(detail, obj.host, postId); if (!detail) return { error: 'ErrorCouldntFetch' }; - let video, videoFilename, audioFilename, isMp3, audio, images, filenameBase = `${obj.host}_${obj.postId}`; + let video, videoFilename, audioFilename, isMp3, audio, images, filenameBase = `${obj.host}_${postId}`; if (obj.host === "tiktok") { images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false } else { diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js index c30d1e7f..b7370af2 100644 --- a/src/modules/processing/services/vk.js +++ b/src/modules/processing/services/vk.js @@ -36,7 +36,7 @@ export default async function(obj) { let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1); let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]); let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"]; - if (!selectedQuality in js["player"]["params"][0]) return { error: 'ErrorEmptyDownload' }; + if (!(selectedQuality in js["player"]["params"][0])) return { error: 'ErrorEmptyDownload' }; return { urls: js["player"]["params"][0][`url${userQuality}`], From 73b5da8df09491d0a1ace99e9b665b863a0853b4 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 20:34:12 +0600 Subject: [PATCH 09/11] Update .deepsource.toml --- .deepsource.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index e8c615cb..4e066861 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -2,4 +2,7 @@ version = 1 [[analyzers]] name = "javascript" -enabled = true \ No newline at end of file +enabled = true +test_patterns = [ + "src/test/test.js" +] From ff9f2c5cce4835d24d4cddb770501e7e6fa6cd77 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 20:39:09 +0600 Subject: [PATCH 10/11] deepsource config update --- .deepsource.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index 4e066861..d6a19e95 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,8 +1,11 @@ version = 1 +test_patterns = [ + "src/test/test.js" +] + [[analyzers]] name = "javascript" enabled = true -test_patterns = [ - "src/test/test.js" -] + [analyzers.meta] + environment = ["nodejs"] From 3617382bb0cd5097c34a12f2fa1e8bed69912d40 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 13 Feb 2023 20:42:16 +0600 Subject: [PATCH 11/11] more clean up --- src/modules/stream/types.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index ef77d238..6c7fd691 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -15,10 +15,10 @@ export function streamDefault(streamInfo, res) { }, isStream: true }); - stream.pipe(res).on('error', (err) => { + stream.pipe(res).on('error', () => { res.end(); }); - stream.on('error', (err) => { + stream.on('error', () => { res.end(); }); } catch (e) { @@ -57,7 +57,7 @@ export function streamLiveRender(streamInfo, res) { ffmpegProcess.on('exit', () => ffmpegProcess.kill()); res.on('finish', () => ffmpegProcess.kill()); res.on('close', () => ffmpegProcess.kill()); - ffmpegProcess.on('error', (err) => { + ffmpegProcess.on('error', () => { ffmpegProcess.kill(); res.end(); }); @@ -101,7 +101,7 @@ export function streamAudioOnly(streamInfo, res) { ffmpegProcess.on('exit', () => ffmpegProcess.kill()); res.on('finish', () => ffmpegProcess.kill()); res.on('close', () => ffmpegProcess.kill()); - ffmpegProcess.on('error', (err) => { + ffmpegProcess.on('error', () => { ffmpegProcess.kill(); res.end(); }); @@ -134,7 +134,7 @@ export function streamVideoOnly(streamInfo, res) { ffmpegProcess.on('exit', () => ffmpegProcess.kill()); res.on('finish', () => ffmpegProcess.kill()); res.on('close', () => ffmpegProcess.kill()); - ffmpegProcess.on('error', (err) => { + ffmpegProcess.on('error', () => { ffmpegProcess.kill(); res.end(); });