mirror of
https://github.com/wukko/cobalt.git
synced 2025-06-12 21:27:39 +02:00
Merge branch 'current' into feat/twitch
This commit is contained in:
37
src/modules/processing/cookie/cookie.js
Normal file
37
src/modules/processing/cookie/cookie.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
||||
export default class Cookie {
|
||||
constructor(input) {
|
||||
assert(typeof input === 'object');
|
||||
this._values = {};
|
||||
this.set(input)
|
||||
}
|
||||
set(values) {
|
||||
Object.entries(values).forEach(
|
||||
([ key, value ]) => this._values[key] = value
|
||||
)
|
||||
}
|
||||
unset(keys) {
|
||||
for (const key of keys) delete this._values[key]
|
||||
}
|
||||
static fromString(str) {
|
||||
const obj = {};
|
||||
|
||||
str.split('; ').forEach(cookie => {
|
||||
const key = cookie.split('=')[0];
|
||||
const value = cookie.split('=').splice(1).join('=');
|
||||
obj[key] = value
|
||||
})
|
||||
|
||||
return new Cookie(obj)
|
||||
}
|
||||
toString() {
|
||||
return Object.entries(this._values).map(([ name, value ]) => `${name}=${value}`).join('; ')
|
||||
}
|
||||
toJSON() {
|
||||
return this.toString()
|
||||
}
|
||||
values() {
|
||||
return Object.freeze({ ...this._values })
|
||||
}
|
||||
}
|
5
src/modules/processing/cookie/cookies_example.json
Normal file
5
src/modules/processing/cookie/cookies_example.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"instagram": [
|
||||
"mid=replace; ig_did=this; csrftoken=cookie"
|
||||
]
|
||||
}
|
62
src/modules/processing/cookie/manager.js
Normal file
62
src/modules/processing/cookie/manager.js
Normal file
@ -0,0 +1,62 @@
|
||||
import Cookie from './cookie.js';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';
|
||||
|
||||
const WRITE_INTERVAL = 60000,
|
||||
cookiePath = process.env.cookiePath,
|
||||
COUNTER = Symbol('counter');
|
||||
|
||||
let cookies = {}, dirty = false, intervalId;
|
||||
|
||||
const setup = async () => {
|
||||
try {
|
||||
if (!cookiePath) return;
|
||||
|
||||
cookies = await readFile(cookiePath, 'utf8');
|
||||
cookies = JSON.parse(cookies);
|
||||
intervalId = setInterval(writeChanges, WRITE_INTERVAL)
|
||||
} catch { /* no cookies for you */ }
|
||||
}
|
||||
|
||||
setup();
|
||||
|
||||
function writeChanges() {
|
||||
if (!dirty) return;
|
||||
dirty = false;
|
||||
|
||||
writeFile(cookiePath, JSON.stringify(cookies, null, 4)).catch(() => {
|
||||
clearInterval(intervalId)
|
||||
})
|
||||
}
|
||||
|
||||
export function getCookie(service) {
|
||||
if (!cookies[service] || !cookies[service].length) return;
|
||||
|
||||
let n;
|
||||
if (cookies[service][COUNTER] === undefined) {
|
||||
n = cookies[service][COUNTER] = 0
|
||||
} else {
|
||||
++cookies[service][COUNTER]
|
||||
n = (cookies[service][COUNTER] %= cookies[service].length)
|
||||
}
|
||||
|
||||
const cookie = cookies[service][n];
|
||||
if (typeof cookie === 'string') cookies[service][n] = Cookie.fromString(cookie);
|
||||
|
||||
return cookies[service][n]
|
||||
}
|
||||
|
||||
export function updateCookie(cookie, headers) {
|
||||
if (!cookie) return;
|
||||
|
||||
const parsed = parseSetCookie(
|
||||
splitCookiesString(headers.get('set-cookie')),
|
||||
{ decodeValues: false }
|
||||
), values = {}
|
||||
|
||||
cookie.unset(parsed.filter(c => c.expires < new Date()).map(c => c.name));
|
||||
parsed.filter(c => !c.expires || c.expires > new Date()).forEach(c => values[c.name] = c.value);
|
||||
|
||||
cookie.set(values);
|
||||
if (Object.keys(values).length) dirty = true
|
||||
}
|
40
src/modules/processing/hostOverrides.js
Normal file
40
src/modules/processing/hostOverrides.js
Normal file
@ -0,0 +1,40 @@
|
||||
export default function (inHost, inURL) {
|
||||
let host = String(inHost);
|
||||
let url = String(inURL);
|
||||
|
||||
switch(host) {
|
||||
case "youtube":
|
||||
if (url.startsWith("https://youtube.com/live/") || url.startsWith("https://www.youtube.com/live/")) {
|
||||
url = url.split("?")[0].replace("www.", "");
|
||||
url = `https://youtube.com/watch?v=${url.replace("https://youtube.com/live/", "")}`
|
||||
}
|
||||
break;
|
||||
case "youtu":
|
||||
if (url.startsWith("https://youtu.be/")) {
|
||||
host = "youtube";
|
||||
url = `https://youtube.com/watch?v=${url.replace("https://youtu.be/", "")}`
|
||||
}
|
||||
break;
|
||||
case "vxtwitter":
|
||||
case "x":
|
||||
if (url.startsWith("https://x.com/")) {
|
||||
host = "twitter";
|
||||
url = url.replace("https://x.com/", "https://twitter.com/")
|
||||
}
|
||||
if (url.startsWith("https://vxtwitter.com/")) {
|
||||
host = "twitter";
|
||||
url = url.replace("https://vxtwitter.com/", "https://twitter.com/")
|
||||
}
|
||||
break;
|
||||
case "tumblr":
|
||||
if (!url.includes("blog/view")) {
|
||||
if (url.slice(-1) === '/') url = url.slice(0, -1);
|
||||
url = url.replace(url.split('/')[5], '')
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {
|
||||
host: host,
|
||||
url: url
|
||||
}
|
||||
}
|
@ -17,11 +17,13 @@ import vimeo from "./services/vimeo.js";
|
||||
import soundcloud from "./services/soundcloud.js";
|
||||
import instagram from "./services/instagram.js";
|
||||
import vine from "./services/vine.js";
|
||||
import pinterest from "./services/pinterest.js";
|
||||
import streamable from "./services/streamable.js";
|
||||
import twitch from "./services/twitch.js";
|
||||
|
||||
export default async function (host, patternMatch, url, lang, obj) {
|
||||
try {
|
||||
let r, isAudioOnly = !!obj.isAudioOnly;
|
||||
let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata;
|
||||
|
||||
if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang, host) });
|
||||
@ -111,6 +113,16 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
case "vine":
|
||||
r = await vine({ id: patternMatch["id"] });
|
||||
break;
|
||||
case "pinterest":
|
||||
r = await pinterest({ id: patternMatch["id"] });
|
||||
break;
|
||||
case "streamable":
|
||||
r = await streamable({
|
||||
id: patternMatch["id"],
|
||||
quality: obj.vQuality,
|
||||
isAudioOnly: isAudioOnly,
|
||||
});
|
||||
break;
|
||||
case "twitch":
|
||||
r = await twitch({
|
||||
vodId: patternMatch["video"] ? patternMatch["video"] : false,
|
||||
@ -119,7 +131,6 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
isAudioOnly: obj.isAudioOnly,
|
||||
format: obj.vFormat
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
@ -129,7 +140,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
|
||||
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);
|
||||
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata);
|
||||
} catch (e) {
|
||||
return apiJSON(0, { t: genericError(lang, host) })
|
||||
}
|
||||
|
@ -2,22 +2,23 @@ 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) {
|
||||
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata) {
|
||||
let action,
|
||||
responseType = 2,
|
||||
defaultParams = {
|
||||
u: r.urls,
|
||||
service: host,
|
||||
ip: ip,
|
||||
filename: r.filename,
|
||||
fileMetadata: !disableMetadata ? r.fileMetadata : false
|
||||
},
|
||||
params = {}
|
||||
|
||||
if (!isAudioOnly && !r.picker && !isAudioMuted) action = "video";
|
||||
if (r.isM3U8) action = "singleM3U8";
|
||||
if (isAudioOnly && !r.picker) action = "audio";
|
||||
if (r.picker) action = "picker";
|
||||
if (isAudioMuted) action = "muteVideo";
|
||||
|
||||
if (r.isPhoto) action = "photo";
|
||||
else if (r.picker) action = "picker"
|
||||
else if (isAudioMuted) action = "muteVideo";
|
||||
else if (isAudioOnly) action = "audio";
|
||||
else if (r.isM3U8) action = "singleM3U8";
|
||||
else action = "video";
|
||||
|
||||
if (action === "picker" || action === "audio") {
|
||||
defaultParams.filename = r.audioFilename;
|
||||
@ -26,13 +27,16 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "photo":
|
||||
responseType = 1;
|
||||
break;
|
||||
case "video":
|
||||
switch (host) {
|
||||
case "bilibili":
|
||||
params = { type: "render", time: r.time };
|
||||
params = { type: "render" };
|
||||
break;
|
||||
case "youtube":
|
||||
params = { type: r.type, time: r.time };
|
||||
params = { type: r.type };
|
||||
break;
|
||||
case "reddit":
|
||||
responseType = r.typeId;
|
||||
@ -56,6 +60,8 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
case "instagram":
|
||||
case "tumblr":
|
||||
case "twitter":
|
||||
case "pinterest":
|
||||
case "streamable":
|
||||
responseType = 1;
|
||||
break;
|
||||
}
|
||||
@ -69,6 +75,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
u: Array.isArray(r.urls) ? r.urls[0] : r.urls,
|
||||
mute: true
|
||||
}
|
||||
if (host === "reddit" && r.typeId === 1) responseType = 1;
|
||||
break;
|
||||
|
||||
case "picker":
|
||||
@ -113,9 +120,11 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
processType = "bridge"
|
||||
}
|
||||
}
|
||||
|
||||
if ((audioFormat === "best" && services[host]["bestAudio"])
|
||||
|| services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) {
|
||||
if (host === "tumblr" && !r.filename && (audioFormat === "best" || audioFormat === "mp3")) {
|
||||
audioFormat = "mp3";
|
||||
processType = "bridge"
|
||||
}
|
||||
if ((audioFormat === "best" && services[host]["bestAudio"]) || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) {
|
||||
audioFormat = services[host]["bestAudio"];
|
||||
processType = "bridge"
|
||||
} else if (audioFormat === "best") {
|
||||
@ -135,8 +144,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
type: processType,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls,
|
||||
audioFormat: audioFormat,
|
||||
copy: copy,
|
||||
fileMetadata: r.fileMetadata ? r.fileMetadata : false
|
||||
copy: copy
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -11,17 +11,16 @@ export default async function(obj) {
|
||||
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[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 video = streamData["data"]["dash"]["video"].filter(v =>
|
||||
!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")
|
||||
).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));
|
||||
let audio = streamData["data"]["dash"]["audio"].filter(a =>
|
||||
!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")
|
||||
).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`
|
||||
};
|
||||
|
@ -1,34 +1,100 @@
|
||||
import got from "got";
|
||||
import { createStream } from "../../stream/manage.js";
|
||||
import { genericUserAgent } from "../../config.js";
|
||||
import { getCookie, updateCookie } from '../cookie/manager.js';
|
||||
|
||||
export default async function(obj) {
|
||||
// i hate this implementation but fetch doesn't work here for some reason (i personally blame facebook)
|
||||
let html;
|
||||
let data;
|
||||
try {
|
||||
html = await got.get(`https://www.instagram.com/p/${obj.id}/`)
|
||||
html.on('error', () => {
|
||||
html = false;
|
||||
});
|
||||
html = html ? html.body : false;
|
||||
const url = new URL('https://www.instagram.com/graphql/query/');
|
||||
url.searchParams.set('query_hash', 'b3055c01b4b222b8a47dc12b090e4e64')
|
||||
url.searchParams.set('variables', JSON.stringify({
|
||||
child_comment_count: 3,
|
||||
fetch_comment_count: 40,
|
||||
has_threaded_comments: true,
|
||||
parent_comment_count: 24,
|
||||
shortcode: obj.id
|
||||
}))
|
||||
|
||||
const cookie = getCookie('instagram');
|
||||
|
||||
data = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
||||
'User-Agent': genericUserAgent,
|
||||
'X-Ig-App-Id': '936619743392459',
|
||||
'X-Asbd-Id': '129477',
|
||||
'x-ig-www-claim': cookie?._wwwClaim || '0',
|
||||
'x-csrftoken': cookie?.values()?.csrftoken,
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'accept-encoding': 'gzip, deflate, br',
|
||||
'accept-language': 'en-US,en;q=0.9,en;q=0.8',
|
||||
cookie
|
||||
}
|
||||
})
|
||||
|
||||
if (data.headers.get('X-Ig-Set-Www-Claim') && cookie) {
|
||||
cookie._wwwClaim = data.headers.get('X-Ig-Set-Www-Claim');
|
||||
}
|
||||
|
||||
updateCookie(cookie, data.headers);
|
||||
data = (await data.json()).data;
|
||||
} catch (e) {
|
||||
html = false;
|
||||
data = false;
|
||||
}
|
||||
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!html.includes('application/ld+json')) return { error: 'ErrorEmptyDownload' };
|
||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let single, multiple = [], postInfo = JSON.parse(html.split('script type="application/ld+json"')[1].split('">')[1].split('</script>')[0]);
|
||||
|
||||
if (postInfo.video.length > 1) {
|
||||
for (let i in postInfo.video) { multiple.push({type: "video", thumb: postInfo.video[i]["thumbnailUrl"], url: postInfo.video[i]["contentUrl"]}) }
|
||||
} else if (postInfo.video.length === 1) {
|
||||
single = postInfo.video[0]["contentUrl"]
|
||||
let single, multiple = [];
|
||||
const sidecar = data?.shortcode_media?.edge_sidecar_to_children;
|
||||
if (sidecar) {
|
||||
sidecar.edges.forEach(e => {
|
||||
if (e.node?.is_video) {
|
||||
multiple.push({
|
||||
type: "video",
|
||||
// thumbnails have `Cross-Origin-Resource-Policy` set to `same-origin`, so we need to proxy them
|
||||
thumb: createStream({
|
||||
service: "instagram",
|
||||
type: "default",
|
||||
u: e.node?.display_url,
|
||||
filename: "image.jpg"
|
||||
}),
|
||||
url: e.node?.video_url
|
||||
})
|
||||
} else {
|
||||
multiple.push({
|
||||
type: "photo",
|
||||
thumb: createStream({
|
||||
service: "instagram",
|
||||
type: "default",
|
||||
u: e.node?.display_url,
|
||||
filename: "image.jpg"
|
||||
}),
|
||||
url: e.node?.display_url
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (data?.shortcode_media?.video_url) {
|
||||
single = data.shortcode_media.video_url
|
||||
} else if (data?.shortcode_media?.display_url) {
|
||||
return {
|
||||
urls: data?.shortcode_media?.display_url,
|
||||
isPhoto: true
|
||||
}
|
||||
} else {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
|
||||
if (single) {
|
||||
return { urls: single, filename: `instagram_${obj.id}.mp4`, audioFilename: `instagram_${obj.id}_audio` }
|
||||
} else if (multiple) {
|
||||
return {
|
||||
urls: single,
|
||||
filename: `instagram_${obj.id}.mp4`,
|
||||
audioFilename: `instagram_${obj.id}_audio`
|
||||
}
|
||||
} else if (multiple.length) {
|
||||
return { picker: multiple }
|
||||
} else {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
|
24
src/modules/processing/services/pinterest.js
Normal file
24
src/modules/processing/services/pinterest.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
|
||||
export default async function(obj) {
|
||||
const pinId = obj.id.split('--').reverse()[0];
|
||||
if (!(/^\d+$/.test(pinId))) return { error: 'ErrorCantGetID' };
|
||||
let data = await fetch(`https://www.pinterest.com/resource/PinResource/get?data=${encodeURIComponent(JSON.stringify({
|
||||
options: {
|
||||
field_set_key: "unauth_react_main_pin",
|
||||
id: pinId
|
||||
}
|
||||
}))}`).then((r) => { return r.json() }).catch(() => { return false });
|
||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
data = data["resource_response"]["data"];
|
||||
|
||||
let video = null;
|
||||
|
||||
if (data.videos !== null) video = data.videos.video_list.V_720P;
|
||||
else if (data.story_pin_data !== null) video = data.story_pin_data.pages[0].blocks[0].video.video_list.V_EXP7;
|
||||
|
||||
if (!video) return { error: 'ErrorEmptyDownload' };
|
||||
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
return { urls: video.url, filename: `pinterest_${pinId}.mp4`, audioFilename: `pinterest_${pinId}_audio` }
|
||||
}
|
@ -11,17 +11,25 @@ export default async function(obj) {
|
||||
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 (Number(r.status) !== 200) audio = ''}).catch(() => {audio = ''});
|
||||
let audio = false,
|
||||
video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
|
||||
audioFileLink = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
|
||||
|
||||
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3];
|
||||
if (!audio.length > 0) return { typeId: 1, urls: video };
|
||||
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
|
||||
|
||||
// fallback for videos with differentiating audio quality
|
||||
if (!audio) {
|
||||
audioFileLink = `${video.split('_')[0]}_AUDIO_128.mp4`
|
||||
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
|
||||
}
|
||||
|
||||
let id = video.split('/')[3];
|
||||
|
||||
if (!audio) return { typeId: 1, urls: video };
|
||||
return {
|
||||
typeId: 2,
|
||||
type: "render",
|
||||
urls: [video, audio],
|
||||
urls: [video, audioFileLink],
|
||||
audioFilename: `reddit_${id}_audio`,
|
||||
filename: `reddit_${id}.mp4`
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
import { cleanString } from "../../sub/utils.js";
|
||||
|
||||
let cachedID = {};
|
||||
|
||||
@ -34,28 +35,33 @@ async function findClientID() {
|
||||
}
|
||||
|
||||
export default async function(obj) {
|
||||
let html;
|
||||
if (!obj.author && !obj.song && obj.shortLink) {
|
||||
html = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false });
|
||||
}
|
||||
if (obj.author && obj.song) {
|
||||
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`).then((r) => { return r.text() }).catch(() => { return false });
|
||||
}
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!(html.includes('<script>window.__sc_hydration = ')
|
||||
&& html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},')
|
||||
&& html.includes('{"hydratable":"sound","data":'))) {
|
||||
return { error: ['ErrorBrokenLink', 'soundcloud'] }
|
||||
}
|
||||
|
||||
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[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 link;
|
||||
if (obj.shortLink && !obj.author && !obj.song) {
|
||||
link = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`, { redirect: "manual" }).then((r) => {
|
||||
if (r.status === 302 && r.headers.get("location").startsWith("https://soundcloud.com/")) {
|
||||
return r.headers.get("location").split('?', 1)[0]
|
||||
}
|
||||
return false
|
||||
}).catch(() => { return false });
|
||||
}
|
||||
if (!link && obj.author && obj.song) {
|
||||
link = `https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`
|
||||
}
|
||||
if (!link) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let json = await fetch(`https://api-v2.soundcloud.com/resolve?url=${link}&client_id=${clientId}`).then((r) => {
|
||||
return r.status === 200 ? r.json() : false
|
||||
}).catch(() => { return false });
|
||||
if (!json) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let fileUrlBase = json.media.transcodings.filter(v => v.preset === "opus_0_0")[0]["url"],
|
||||
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 > maxVideoDuration) return { error: ['ErrorLengthAudioConvert', maxVideoDuration / 60000] };
|
||||
@ -67,8 +73,8 @@ export default async function(obj) {
|
||||
urls: file,
|
||||
audioFilename: `soundcloud_${json.id}`,
|
||||
fileMetadata: {
|
||||
title: json.title,
|
||||
artist: json.user.username,
|
||||
title: cleanString(json.title.replace(/\p{Emoji}/gu, '').trim()),
|
||||
artist: cleanString(json.user.username.replace(/\p{Emoji}/gu, '').trim()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
src/modules/processing/services/streamable.js
Normal file
19
src/modules/processing/services/streamable.js
Normal file
@ -0,0 +1,19 @@
|
||||
export default async function(obj) {
|
||||
let video = await fetch(`https://api.streamable.com/videos/${obj.id}`).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
if (!video) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let best = video.files['mp4-mobile'];
|
||||
if (video.files.mp4 && (obj.isAudioOnly || obj.quality === "max" || obj.quality >= Number("720"))) {
|
||||
best = video.files.mp4;
|
||||
}
|
||||
|
||||
if (best) return {
|
||||
urls: best.url,
|
||||
filename: `streamable_${obj.id}_${best.width}x${best.height}.mp4`,
|
||||
audioFilename: `streamable_${obj.id}_audio`,
|
||||
fileMetadata: {
|
||||
title: video.title
|
||||
}
|
||||
}
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
@ -17,7 +17,7 @@ function selector(j, h, id) {
|
||||
let t;
|
||||
switch (h) {
|
||||
case "tiktok":
|
||||
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] === id) return true })[0];
|
||||
t = j["aweme_list"].filter(v => v["aweme_id"] === id)[0];
|
||||
break;
|
||||
case "douyin":
|
||||
t = j['aweme_detail'];
|
||||
@ -92,7 +92,7 @@ export default async function(obj) {
|
||||
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; })
|
||||
sel = sel.filter(p => p.includes(".jpeg?"))
|
||||
imageLinks.push({url: sel[0]})
|
||||
}
|
||||
return {
|
||||
|
@ -8,7 +8,21 @@ export default async function(obj) {
|
||||
}).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]}`, filename: `tumblr_${obj.id}.mp4`, audioFilename: `tumblr_${obj.id}_audio` }
|
||||
let r;
|
||||
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
|
||||
r = {
|
||||
urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`,
|
||||
filename: `tumblr_${obj.id}.mp4`,
|
||||
audioFilename: `tumblr_${obj.id}_audio`
|
||||
}
|
||||
} else if (html.includes('property="og:audio" content="https://a.tumblr.com/')) {
|
||||
r = {
|
||||
urls: `https://a.tumblr.com/${html.split('property="og:audio" content="https://a.tumblr.com/')[1].split('"')[0]}`,
|
||||
audioFilename: `tumblr_${obj.id}`,
|
||||
isAudioOnly: true
|
||||
}
|
||||
} else r = { error: 'ErrorEmptyDownload' };
|
||||
|
||||
return r;
|
||||
}
|
||||
|
@ -1,22 +1,22 @@
|
||||
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]
|
||||
return arr.filter(v => v["content_type"] === "video/mp4").sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"]
|
||||
}
|
||||
const apiURL = "https://api.twitter.com"
|
||||
|
||||
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",
|
||||
"x-twitter-client-language": "en",
|
||||
"x-twitter-active-user": "yes",
|
||||
"Accept-Language": "en"
|
||||
"accept-language": "en"
|
||||
};
|
||||
let conversationURL = `${apiURL}/2/timeline/conversation/${obj.id}.json?cards_platform=Web-12&tweet_mode=extended&include_cards=1&include_ext_media_availability=true&include_ext_sensitive_media_warning=true&simple_quoted_tweet=true&trim_user=1`;
|
||||
let activateURL = `${apiURL}/1.1/guest/activate.json`;
|
||||
|
||||
let activateURL = `https://api.twitter.com/1.1/guest/activate.json`;
|
||||
let graphqlTweetURL = `https://twitter.com/i/api/graphql/0hWvDhmW8YQ-S_ib3azIrw/TweetResultByRestId`;
|
||||
let graphqlSpaceURL = `https://twitter.com/i/api/graphql/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById`;
|
||||
|
||||
let req_act = await fetch(activateURL, {
|
||||
method: "POST",
|
||||
@ -24,40 +24,39 @@ export default async function(obj) {
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
if (!req_act) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
_headers["host"] = "twitter.com";
|
||||
_headers["content-type"] = "application/json";
|
||||
|
||||
_headers["x-guest-token"] = req_act["guest_token"];
|
||||
_headers["cookie"] = `guest_id=v1%3A${req_act["guest_token"]};`;
|
||||
_headers["cookie"] = `guest_id=v1%3A${req_act["guest_token"]}`;
|
||||
|
||||
if (!obj.spaceId) {
|
||||
let conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
|
||||
if (!conversation || !conversation.globalObjects.tweets[obj.id]) {
|
||||
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
|
||||
// ^ explicit content, but no multi media support
|
||||
delete _headers["x-guest-token"];
|
||||
delete _headers["cookie"];
|
||||
|
||||
req_act = await fetch(activateURL, {
|
||||
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"];
|
||||
_headers['cookie'] = `guest_id=v1%3A${req_act["guest_token"]};`;
|
||||
|
||||
conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
if (obj.id) {
|
||||
let query = {
|
||||
variables: {"tweetId": obj.id, "withCommunity": false, "includePromotedContent": false, "withVoice": false},
|
||||
features: {"creator_subscriptions_tweet_preview_api_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_media_download_video_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false}
|
||||
}
|
||||
if (!conversation || !conversation.globalObjects.tweets[obj.id]) return { error: 'ErrorTweetUnavailable' };
|
||||
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 = `${graphqlTweetURL}?variables=${query.variables}&features=${query.features}`;
|
||||
|
||||
let baseMedia, baseTweet = conversation.globalObjects.tweets[obj.id];
|
||||
if (baseTweet.retweeted_status_id_str && conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities) {
|
||||
baseMedia = conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities
|
||||
let TweetResultByRestId = await fetch(query, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
|
||||
|
||||
// {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}}
|
||||
if (!TweetResultByRestId || TweetResultByRestId.data.tweetResult.result.__typename !== "Tweet") return { error: 'ErrorTweetUnavailable' };
|
||||
|
||||
let baseMedia,
|
||||
baseTweet = TweetResultByRestId.data.tweetResult.result.legacy;
|
||||
|
||||
if (baseTweet.retweeted_status_result && baseTweet.retweeted_status_result.result.legacy.extended_entities.media) {
|
||||
baseMedia = baseTweet.retweeted_status_result.result.legacy.extended_entities
|
||||
} else if (baseTweet.extended_entities && baseTweet.extended_entities.media) {
|
||||
baseMedia = baseTweet.extended_entities
|
||||
}
|
||||
if (!baseMedia) return { error: 'ErrorNoVideosInTweet' };
|
||||
|
||||
let single, multiple = [], media = baseMedia["media"];
|
||||
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
|
||||
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) {
|
||||
@ -73,7 +72,9 @@ export default async function(obj) {
|
||||
} else {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
} else {
|
||||
}
|
||||
// spaces no longer work with guest authorization
|
||||
if (obj.spaceId) {
|
||||
_headers["host"] = "twitter.com";
|
||||
_headers["content-type"] = "application/json";
|
||||
|
||||
@ -83,7 +84,7 @@ export default async function(obj) {
|
||||
}
|
||||
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/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById?variables=${query.variables}&features=${query.features}`;
|
||||
query = `${graphqlSpaceURL}?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' };
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
|
||||
// vimeo you're fucked in the head for this
|
||||
const resolutionMatch = {
|
||||
"3840": "2160",
|
||||
"2732": "1440",
|
||||
@ -11,7 +12,6 @@ const resolutionMatch = {
|
||||
"640": "360",
|
||||
"426": "240"
|
||||
}
|
||||
// ^ vimeo you're fucked in the head for this ^
|
||||
|
||||
const qualityMatch = {
|
||||
"2160": "4K",
|
||||
@ -64,7 +64,7 @@ export default async function(obj) {
|
||||
let videoUrl, audioUrl, baseUrl = masterJSONURL.split("/sep/")[0];
|
||||
switch (type) {
|
||||
case "parcel":
|
||||
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 masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter(a => a['mime_type'] === "audio/mp4"),
|
||||
bestAudio = masterJSON_Audio[0];
|
||||
videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`,
|
||||
audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
|
||||
|
@ -1,59 +1,38 @@
|
||||
import { xml2json } from "xml-js";
|
||||
import { genericUserAgent, maxVideoDuration } from "../../config.js";
|
||||
|
||||
const representationMatch = {
|
||||
"2160": 7,
|
||||
"1440": 6,
|
||||
"1080": 5,
|
||||
"720": 4,
|
||||
"480": 3,
|
||||
"360": 2,
|
||||
"240": 1,
|
||||
"144": 0
|
||||
}, resolutionMatch = {
|
||||
"3840": "2160",
|
||||
"2560": "1440",
|
||||
"1920": "1080",
|
||||
"1280": "720",
|
||||
"852": "480",
|
||||
"640": "360",
|
||||
"426": "240",
|
||||
// "256": "144"
|
||||
}
|
||||
const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"];
|
||||
|
||||
export default async function(o) {
|
||||
let html, url, filename = `vk_${o.userId}_${o.videoId}_`;
|
||||
let html, url,
|
||||
quality = o.quality === "max" ? 2160 : o.quality,
|
||||
filename = `vk_${o.userId}_${o.videoId}_`;
|
||||
|
||||
html = await fetch(`https://vk.com/video${o.userId}_${o.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 quality = o.quality === "max" ? 7 : representationMatch[o.quality],
|
||||
js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
|
||||
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] };
|
||||
|
||||
if (js.player.params[0]["manifest"]) {
|
||||
let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })),
|
||||
repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"],
|
||||
bestQuality = repr[repr.length - 1],
|
||||
resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height';
|
||||
|
||||
if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality];
|
||||
|
||||
url = js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`];
|
||||
filename += `${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4`
|
||||
|
||||
} else if (js.player.params[0]["url240"]) { // fallback for when video is too old
|
||||
url = js.player.params[0]["url240"];
|
||||
filename += `320x240.mp4`
|
||||
for (let i in resolutions) {
|
||||
if (js.player.params[0][`url${resolutions[i]}`]) {
|
||||
quality = resolutions[i];
|
||||
break
|
||||
}
|
||||
}
|
||||
if (Number(quality) > Number(o.quality)) quality = o.quality;
|
||||
|
||||
url = js.player.params[0][`url${quality}`];
|
||||
filename += `${quality}p.mp4`
|
||||
|
||||
if (url && filename) return {
|
||||
urls: url,
|
||||
filename: filename
|
||||
};
|
||||
}
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Innertube } from 'youtubei.js';
|
||||
import { maxVideoDuration } from '../../config.js';
|
||||
import { cleanString } from '../../sub/utils.js';
|
||||
|
||||
const yt = await Innertube.create();
|
||||
|
||||
@ -23,6 +24,10 @@ const c = {
|
||||
|
||||
export default async function(o) {
|
||||
let info, isDubbed, quality = o.quality === "max" ? "9000" : o.quality; //set quality 9000(p) to be interpreted as max
|
||||
function qual(i) {
|
||||
return i['quality_label'].split('p')[0].split('s')[0]
|
||||
}
|
||||
|
||||
try {
|
||||
info = await yt.getBasicInfo(o.id, 'ANDROID');
|
||||
} catch (e) {
|
||||
@ -30,22 +35,23 @@ export default async function(o) {
|
||||
}
|
||||
|
||||
if (!info) return { error: 'ErrorCantConnectToServiceAPI' };
|
||||
|
||||
if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' };
|
||||
if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' };
|
||||
|
||||
let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter((e) => {
|
||||
if (e["mime_type"].includes(c[o.format].codec) || e["mime_type"].includes(c[o.format].aCodec)) return true
|
||||
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter(e =>
|
||||
e["mime_type"].includes(c[o.format].codec) || e["mime_type"].includes(c[o.format].aCodec)
|
||||
).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
|
||||
bestQuality = adaptive_formats.find(i => i["has_video"]);
|
||||
hasAudio = adaptive_formats.find(i => i["has_audio"]);
|
||||
|
||||
if (bestQuality) bestQuality = bestQuality['quality_label'].split('p')[0];
|
||||
if (bestQuality) bestQuality = qual(bestQuality);
|
||||
if (!bestQuality && !o.isAudioOnly || !hasAudio) return { error: 'ErrorYTTryOtherCodec' };
|
||||
if (info.basic_info.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
let checkBestAudio = (i) => (i["has_audio"] && !i["has_video"]),
|
||||
audio = adaptive_formats.find(i => checkBestAudio(i) && i["is_original"]);
|
||||
audio = adaptive_formats.find(i => checkBestAudio(i) && !i["is_dubbed"]);
|
||||
|
||||
if (o.dubLang) {
|
||||
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang);
|
||||
@ -54,35 +60,38 @@ export default async function(o) {
|
||||
isDubbed = true
|
||||
}
|
||||
}
|
||||
if (hasAudio && o.isAudioOnly) {
|
||||
let r = {
|
||||
type: "render",
|
||||
isAudioOnly: true,
|
||||
urls: audio.url,
|
||||
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
|
||||
fileMetadata: {
|
||||
title: info.basic_info.title,
|
||||
artist: info.basic_info.author.replace("- Topic", "").trim(),
|
||||
}
|
||||
};
|
||||
if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) {
|
||||
let descItems = info.basic_info.short_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 fileMetadata = {
|
||||
title: cleanString(info.basic_info.title.replace(/\p{Emoji}/gu, '').trim()),
|
||||
artist: cleanString(info.basic_info.author.replace("- Topic", "").replace(/\p{Emoji}/gu, '').trim()),
|
||||
}
|
||||
let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
|
||||
checkBestVideo = (i) => (i["has_video"] && !i["has_audio"] && i['quality_label'].split('p')[0] === bestQuality),
|
||||
checkRightVideo = (i) => (i["has_video"] && !i["has_audio"] && i['quality_label'].split('p')[0] === quality);
|
||||
if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) {
|
||||
let descItems = info.basic_info.short_description.split("\n\n");
|
||||
fileMetadata.album = descItems[2];
|
||||
fileMetadata.copyright = descItems[3];
|
||||
if (descItems[4].startsWith("Released on:")) {
|
||||
fileMetadata.date = descItems[4].replace("Released on: ", '').trim()
|
||||
}
|
||||
};
|
||||
|
||||
if (hasAudio && o.isAudioOnly) return {
|
||||
type: "render",
|
||||
isAudioOnly: true,
|
||||
urls: audio.url,
|
||||
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
|
||||
fileMetadata: fileMetadata
|
||||
}
|
||||
let checkSingle = (i) => ((qual(i) === quality || qual(i) === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
|
||||
checkBestVideo = (i) => (i["has_video"] && !i["has_audio"] && qual(i) === bestQuality),
|
||||
checkRightVideo = (i) => (i["has_video"] && !i["has_audio"] && qual(i) === quality);
|
||||
|
||||
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
|
||||
let single = info.streaming_data.formats.find(i => checkSingle(i));
|
||||
if (single) return {
|
||||
type: "bridge",
|
||||
urls: single.url,
|
||||
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`
|
||||
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`,
|
||||
fileMetadata: fileMetadata
|
||||
}
|
||||
};
|
||||
|
||||
@ -90,7 +99,8 @@ export default async function(o) {
|
||||
if (video && audio) return {
|
||||
type: "render",
|
||||
urls: [video.url, audio.url],
|
||||
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`
|
||||
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`,
|
||||
fileMetadata: fileMetadata
|
||||
};
|
||||
|
||||
return { error: 'ErrorYTTryOtherCodec' }
|
||||
|
@ -2,7 +2,7 @@
|
||||
"audioIgnore": ["vk"],
|
||||
"config": {
|
||||
"bilibili": {
|
||||
"alias": "bilibili (.com only)",
|
||||
"alias": "bilibili.com videos",
|
||||
"patterns": ["video/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
@ -12,7 +12,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"twitter": {
|
||||
"alias": "twitter posts & spaces & voice",
|
||||
"alias": "twitter videos & voice",
|
||||
"patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"],
|
||||
"enabled": true
|
||||
},
|
||||
@ -22,8 +22,8 @@
|
||||
"enabled": true
|
||||
},
|
||||
"youtube": {
|
||||
"alias": "youtube videos & shorts & music",
|
||||
"patterns": ["watch?v=:id"],
|
||||
"alias": "youtube videos, shorts & music",
|
||||
"patterns": ["watch?v=:id", "embed/:id"],
|
||||
"bestAudio": "opus",
|
||||
"enabled": true
|
||||
},
|
||||
@ -32,7 +32,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"tiktok": {
|
||||
"alias": "tiktok videos & photos & audio",
|
||||
"alias": "tiktok videos, photos & audio",
|
||||
"patterns": [":user/video/:postId", ":id", "t/:id"],
|
||||
"audioFormats": ["best", "m4a", "mp3"],
|
||||
"enabled": true
|
||||
@ -43,7 +43,7 @@
|
||||
"enabled": false
|
||||
},
|
||||
"vimeo": {
|
||||
"patterns": [":id"],
|
||||
"patterns": [":id", "video/:id"],
|
||||
"enabled": true,
|
||||
"bestAudio": "mp3"
|
||||
},
|
||||
@ -53,7 +53,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"instagram": {
|
||||
"alias": "instagram reels & video posts",
|
||||
"alias": "instagram reels & posts",
|
||||
"patterns": ["reels/:id", "reel/:id", "p/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
@ -63,7 +63,17 @@
|
||||
"patterns": ["v/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"twitch": {
|
||||
"pinterest": {
|
||||
"alias": "pinterest videos & stories",
|
||||
"patterns": ["pin/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"streamable": {
|
||||
"alias": "streamable.com",
|
||||
"patterns": [":id", "o/:id", "e/:id", "s/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"twitch": {
|
||||
"alias": "twitch vods & videos & clips",
|
||||
"tld": "tv",
|
||||
"patterns": ["videos/:video", ":channel/clip/:clip"],
|
||||
|
@ -23,12 +23,16 @@ export const testers = {
|
||||
|
||||
"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)),
|
||||
"soundcloud": (patternMatch) => (patternMatch["author"]?.length <= 25 && patternMatch["song"]?.length <= 255)
|
||||
|| (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32),
|
||||
|
||||
"instagram": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
"vine": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
"pinterest": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 128),
|
||||
|
||||
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6),
|
||||
|
||||
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100 || patternMatch["video"] && patternMatch["video"].length <= 10)),
|
||||
}
|
||||
|
Reference in New Issue
Block a user