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 | ✅ | ✅ | ✅ | |
|
| Streamable | ✅ | ✅ | ✅ | |
|
||||||
| TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. |
|
| TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. |
|
||||||
| Tumblr | ✅ | ✅ | ✅ | Support for audio file downloads. |
|
| Tumblr | ✅ | ✅ | ✅ | Support for audio file downloads. |
|
||||||
| Twitch Clips & Videos | ✅ | ✅ | ✅ | |
|
| Twitch Clips | ✅ | ✅ | ✅ | |
|
||||||
| Twitter/X * | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. |
|
| Twitter/X * | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. |
|
||||||
| Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. |
|
| Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. |
|
||||||
| Vine Archive | ✅ | ✅ | ✅ | |
|
| Vine Archive | ✅ | ✅ | ✅ | |
|
||||||
|
|
|
@ -125,7 +125,6 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||||
break;
|
break;
|
||||||
case "twitch":
|
case "twitch":
|
||||||
r = await twitch({
|
r = await twitch({
|
||||||
vodId: patternMatch["video"] ? patternMatch["video"] : false,
|
|
||||||
clipId: patternMatch["clip"] ? patternMatch["clip"] : false,
|
clipId: patternMatch["clip"] ? patternMatch["clip"] : false,
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
isAudioOnly: obj.isAudioOnly
|
isAudioOnly: obj.isAudioOnly
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { maxVideoDuration } from "../../config.js";
|
import { maxVideoDuration } from "../../config.js";
|
||||||
import { getM3U8Formats } from "../../sub/utils.js";
|
|
||||||
|
|
||||||
const gqlURL = "https://gql.twitch.tv/gql";
|
const gqlURL = "https://gql.twitch.tv/gql";
|
||||||
const m3u8URL = "https://usher.ttvnw.net";
|
|
||||||
const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" };
|
const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" };
|
||||||
|
|
||||||
async function getClip(obj) {
|
export default async function (obj) {
|
||||||
let req_metadata = await fetch(gqlURL, {
|
let req_metadata = await fetch(gqlURL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: clientIdHead,
|
headers: clientIdHead,
|
||||||
|
@ -76,105 +74,3 @@ async function getClip(obj) {
|
||||||
audioFilename: `twitchclip_${clipMetadata.id}_audio`
|
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
|
"enabled": true
|
||||||
},
|
},
|
||||||
"twitch": {
|
"twitch": {
|
||||||
"alias": "twitch clips & videos",
|
"alias": "twitch clips",
|
||||||
"tld": "tv",
|
"tld": "tv",
|
||||||
"patterns": ["videos/:video", ":channel/clip/:clip"],
|
"patterns": [":channel/clip/:clip"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,5 +34,5 @@ export const testers = {
|
||||||
|
|
||||||
"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 || patternMatch["video"] && patternMatch["video"].length <= 10)),
|
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100)),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue