Merge branch 'current' into instagram-stories

This commit is contained in:
dumbmoron
2023-09-16 23:34:23 +02:00
committed by GitHub
32 changed files with 1045 additions and 269 deletions

View File

@ -8,6 +8,9 @@ export default function (inHost, inURL) {
url = url.split("?")[0].replace("www.", "");
url = `https://youtube.com/watch?v=${url.replace("https://youtube.com/live/", "")}`
}
if (url.includes('youtube.com/shorts/')) {
url = url.split('?')[0].replace('shorts/', 'watch?v=');
}
break;
case "youtu":
if (url.startsWith("https://youtu.be/")) {
@ -32,6 +35,11 @@ export default function (inHost, inURL) {
url = url.replace(url.split('/')[5], '')
}
break;
case "twitch":
if (url.includes('clips.twitch.tv')) {
url = url.split('?')[0].replace('clips.twitch.tv/', 'twitch.tv/_/clip/');
}
break;
}
return {
host: host,

View File

@ -19,10 +19,12 @@ 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";
import rutube from "./services/rutube.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) });
@ -125,6 +127,20 @@ export default async function (host, patternMatch, url, lang, obj) {
isAudioOnly: isAudioOnly,
});
break;
case "twitch":
r = await twitch({
clipId: patternMatch["clip"] ? patternMatch["clip"] : false,
quality: obj.vQuality,
isAudioOnly: obj.isAudioOnly
});
break;
case "rutube":
r = await rutube({
id: patternMatch["id"],
quality: obj.vQuality,
isAudioOnly: isAudioOnly
});
break;
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}
@ -134,7 +150,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.aFormat, isAudioOnly, lang, isAudioMuted);
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata);
} catch (e) {
return apiJSON(0, { t: genericError(lang, host) })
}

View File

@ -2,17 +2,17 @@ import { audioIgnore, services, supportedAudio } from "../config.js";
import { apiJSON } from "../sub/utils.js";
import loc from "../../localization/manager.js";
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted) {
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata) {
let action,
responseType = 2,
defaultParams = {
u: r.urls,
service: host,
filename: r.filename,
fileMetadata: r.fileMetadata ? r.fileMetadata : false
fileMetadata: !disableMetadata ? r.fileMetadata : false
},
params = {}
if (r.isPhoto) action = "photo";
else if (r.picker) action = "picker"
else if (isAudioMuted) action = "muteVideo";
@ -55,7 +55,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted) {
case "tiktok":
params = { type: "bridge" };
break;
case "vine":
case "instagram":
case "tumblr":

View File

@ -0,0 +1,30 @@
import HLS from 'hls-parser';
import { maxVideoDuration } from "../../config.js";
export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality;
let play = await fetch(`https://rutube.ru/api/play/options/${obj.id}/?no_404=true&referer&pver=v2`).then((r) => { return r.json() }).catch(() => { return false });
if (!play) return { error: 'ErrorCouldntFetch' };
if ("hls" in play.live_streams) return { error: 'ErrorLiveVideo' };
if (!play.video_balancer || play.detail) return { error: 'ErrorEmptyDownload' };
if (play.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let m3u8 = await fetch(play.video_balancer.m3u8).then((r) => { return r.text() }).catch(() => { return false });
if (!m3u8) return { error: 'ErrorCouldntFetch' };
m3u8 = HLS.parse(m3u8).variants.sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let bestQuality = m3u8[0];
if (Number(quality) < bestQuality.resolution.height) {
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
}
return {
urls: bestQuality.uri,
isM3U8: true,
audioFilename: `rutube_${play.id}_audio`,
filename: `rutube_${play.id}_${bestQuality.resolution.width}x${bestQuality.resolution.height}.mp4`
}
}

View File

@ -0,0 +1,76 @@
import { maxVideoDuration } from "../../config.js";
const gqlURL = "https://gql.twitch.tv/gql";
const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" };
export default async function (obj) {
let req_metadata = await fetch(gqlURL, {
method: "POST",
headers: clientIdHead,
body: JSON.stringify({
query: `{
clip(slug: "${obj.clipId}") {
broadcaster {
login
}
createdAt
curator {
login
}
durationSeconds
id
medium: thumbnailURL(width: 480, height: 272)
title
videoQualities {
quality
sourceURL
}
}
}`
})
}).then((r) => { return r.status === 200 ? r.json() : false; }).catch(() => { return false });
if (!req_metadata) return { error: 'ErrorCouldntFetch' };
let clipMetadata = req_metadata.data.clip;
if (clipMetadata.durationSeconds > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
if (!clipMetadata.videoQualities || !clipMetadata.broadcaster) return { error: 'ErrorEmptyDownload' };
let req_token = await fetch(gqlURL, {
method: "POST",
headers: clientIdHead,
body: JSON.stringify([
{
"operationName": "VideoAccessToken_Clip",
"variables": {
"slug": obj.clipId
},
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11"
}
}
}
])
}).then((r) => { return r.status === 200 ? r.json() : false; }).catch(() => { return false });
if (!req_token) return { error: 'ErrorCouldntFetch' };
let formats = clipMetadata.videoQualities;
let format = formats.find(f => f.quality === obj.quality) || formats[0];
return {
type: "bridge",
urls: `${format.sourceURL}?${new URLSearchParams({
sig: req_token[0].data.clip.playbackAccessToken.signature,
token: req_token[0].data.clip.playbackAccessToken.value
})}`,
fileMetadata: {
title: clipMetadata.title,
artist: `Twitch Clip by @${clipMetadata.broadcaster.login}, clipped by @${clipMetadata.curator.login}`,
},
filename: `twitchclip_${clipMetadata.id}_${format.quality}p.mp4`,
audioFilename: `twitchclip_${clipMetadata.id}_audio`
}
}

View File

@ -22,7 +22,7 @@
"enabled": true
},
"youtube": {
"alias": "youtube videos & shorts & music",
"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
@ -75,6 +75,18 @@
"alias": "streamable.com",
"patterns": [":id", "o/:id", "e/:id", "s/:id"],
"enabled": true
},
"twitch": {
"alias": "twitch clips",
"tld": "tv",
"patterns": [":channel/clip/:clip"],
"enabled": true
},
"rutube": {
"alias": "rutube videos",
"tld": "ru",
"patterns": ["video/:id", "play/embed/:id"],
"enabled": true
}
}
}
}

View File

@ -22,16 +22,20 @@ export const testers = {
|| (patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)),
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
"soundcloud": (patternMatch) => (patternMatch["author"]?.length <= 25 && patternMatch["song"]?.length <= 255)
|| (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32),
"instagram": (patternMatch) => (patternMatch.postId?.length <= 12)
|| (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24),
"vine": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
"pinterest": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 128),
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6)
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6),
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100)),
"rutube": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length === 32)),
}