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