remove vods
there's no point in downloading entire streams. people can clip what they need and download that instead!
This commit is contained in:
parent
f7247b87f0
commit
ad8a9c454d
5 changed files with 5 additions and 110 deletions
|
@ -24,7 +24,7 @@ Paste the link, get the video, move on. It's that simple. Just how it should be.
|
|||
| Streamable | ✅ | ✅ | ✅ | |
|
||||
| TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. |
|
||||
| Tumblr | ✅ | ✅ | ✅ | Support for audio file downloads. |
|
||||
| Twitch Clips & Videos | ✅ | ✅ | ✅ | |
|
||||
| Twitch Clips | ✅ | ✅ | ✅ | |
|
||||
| Twitter/X * | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. |
|
||||
| Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. |
|
||||
| Vine Archive | ✅ | ✅ | ✅ | |
|
||||
|
|
|
@ -125,7 +125,6 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
break;
|
||||
case "twitch":
|
||||
r = await twitch({
|
||||
vodId: patternMatch["video"] ? patternMatch["video"] : false,
|
||||
clipId: patternMatch["clip"] ? patternMatch["clip"] : false,
|
||||
quality: obj.vQuality,
|
||||
isAudioOnly: obj.isAudioOnly
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { maxVideoDuration } from "../../config.js";
|
||||
import { getM3U8Formats } from "../../sub/utils.js";
|
||||
|
||||
const gqlURL = "https://gql.twitch.tv/gql";
|
||||
const m3u8URL = "https://usher.ttvnw.net";
|
||||
const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" };
|
||||
|
||||
async function getClip(obj) {
|
||||
export default async function (obj) {
|
||||
let req_metadata = await fetch(gqlURL, {
|
||||
method: "POST",
|
||||
headers: clientIdHead,
|
||||
|
@ -76,105 +74,3 @@ async function getClip(obj) {
|
|||
audioFilename: `twitchclip_${clipMetadata.id}_audio`
|
||||
}
|
||||
}
|
||||
async function getVideo(obj) {
|
||||
let req_metadata = await fetch(gqlURL, {
|
||||
method: "POST",
|
||||
headers: clientIdHead,
|
||||
body: JSON.stringify([
|
||||
{
|
||||
"operationName": "VideoMetadata",
|
||||
"variables": {
|
||||
"channelLogin": "",
|
||||
"videoID": obj.vodId
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "226edb3e692509f727fd56821f5653c05740242c82b0388883e0c0e75dcbf687"
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false; }).catch(() => { return false });
|
||||
if (!req_metadata) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let vodMetadata = req_metadata[0].data.video;
|
||||
|
||||
if (vodMetadata.previewThumbnailURL.endsWith('/404_processing_{width}x{height}.png')) return { error: 'ErrorLiveVideo' };
|
||||
if (vodMetadata.lengthSeconds > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
if (!vodMetadata.owner) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let req_token = await fetch(gqlURL, {
|
||||
method: "POST",
|
||||
headers: clientIdHead,
|
||||
body: JSON.stringify({
|
||||
query: `{
|
||||
videoPlaybackAccessToken(
|
||||
id: "${obj.vodId}",
|
||||
params: {
|
||||
platform: "web",
|
||||
playerBackend: "mediaplayer",
|
||||
playerType: "site"
|
||||
}
|
||||
)
|
||||
{
|
||||
value
|
||||
signature
|
||||
}
|
||||
}`
|
||||
})
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false; }).catch(() => { return false });
|
||||
if (!req_token) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let req_m3u8 = await fetch(
|
||||
`${m3u8URL}/vod/${obj.vodId}.m3u8?${
|
||||
new URLSearchParams({
|
||||
allow_source: 'true',
|
||||
allow_audio_only: 'true',
|
||||
allow_spectre: 'true',
|
||||
player: 'twitchweb',
|
||||
playlist_include_framerate: 'true',
|
||||
nauth: req_token.data.videoPlaybackAccessToken.value,
|
||||
nauthsig: req_token.data.videoPlaybackAccessToken.signature
|
||||
}
|
||||
)}`, {
|
||||
headers: clientIdHead
|
||||
}
|
||||
).then((r) => { return r.status === 200 ? r.text() : false; }).catch(() => { return false });
|
||||
|
||||
if (!req_m3u8) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let formats = getM3U8Formats(req_m3u8);
|
||||
let generalMeta = {
|
||||
title: vodMetadata.title,
|
||||
artist: `Twitch Broadcast by @${vodMetadata.owner.login}`,
|
||||
}
|
||||
if (obj.isAudioOnly) {
|
||||
return {
|
||||
type: "render",
|
||||
isM3U8: true,
|
||||
time: vodMetadata.lengthSeconds * 1000,
|
||||
urls: formats.find(f => f.id === 'audio_only').url,
|
||||
audioFilename: `twitchvod_${obj.vodId}_audio`,
|
||||
fileMetadata: generalMeta
|
||||
}
|
||||
} else {
|
||||
let format = formats.find(f => f.resolution && f.resolution[1] === obj.quality) || formats[0];
|
||||
return {
|
||||
urls: format.url,
|
||||
isM3U8: true,
|
||||
time: vodMetadata.lengthSeconds * 1000,
|
||||
filename: `twitchvod_${obj.vodId}_${format.resolution[0]}x${format.resolution[1]}.mp4`,
|
||||
fileMetadata: generalMeta
|
||||
}
|
||||
}
|
||||
}
|
||||
export default async function (obj) {
|
||||
let response = { error: 'ErrorEmptyDownload' };
|
||||
if (obj.clipId) {
|
||||
response = await getClip(obj)
|
||||
} else if (obj.vodId) {
|
||||
response = await getVideo(obj)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -74,9 +74,9 @@
|
|||
"enabled": true
|
||||
},
|
||||
"twitch": {
|
||||
"alias": "twitch clips & videos",
|
||||
"alias": "twitch clips",
|
||||
"tld": "tv",
|
||||
"patterns": ["videos/:video", ":channel/clip/:clip"],
|
||||
"patterns": [":channel/clip/:clip"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,5 +34,5 @@ export const testers = {
|
|||
|
||||
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6),
|
||||
|
||||
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100 || patternMatch["video"] && patternMatch["video"].length <= 10)),
|
||||
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100)),
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue