diff --git a/README.md b/README.md index 6638c72e..ade04cd2 100644 --- a/README.md +++ b/README.md @@ -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 | ✅ | ✅ | ✅ | | diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 5c34b6df..7b769dc0 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -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 diff --git a/src/modules/processing/services/twitch.js b/src/modules/processing/services/twitch.js index 31b239e1..6d7e6d94 100644 --- a/src/modules/processing/services/twitch.js +++ b/src/modules/processing/services/twitch.js @@ -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 -} diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 288c1d17..47f501fe 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -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 } } diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index 19fca2ee..a22b2d58 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -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)), }