diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js index d79d2e92..30ee3368 100644 --- a/src/modules/processing/services/vimeo.js +++ b/src/modules/processing/services/vimeo.js @@ -1,5 +1,5 @@ import { env } from "../../config.js"; -import { cleanString } from '../../sub/utils.js'; +import { cleanString, merge } from '../../sub/utils.js'; import HLS from "hls-parser"; @@ -16,32 +16,70 @@ const resolutionMatch = { "426": 240 } -export default async function(obj) { - let quality = obj.quality === "max" ? 9000 : Number(obj.quality); - if (quality < 240) quality = 240; - if (!quality || obj.isAudioOnly) quality = 9000; - - const url = new URL(`https://player.vimeo.com/video/${obj.id}/config`); - if (obj.password) { - url.searchParams.set('h', obj.password); +const requestApiInfo = (videoId, password) => { + if (password) { + videoId += `:${password}` } - const api = await fetch(url) + return fetch( + `https://api.vimeo.com/videos/${videoId}`, + { + headers: { + Accept: 'application/vnd.vimeo.*+json; version=3.4.2', + 'User-Agent': 'Vimeo/10.19.0 (com.vimeo; build:101900.57.0; iOS 17.5.1) Alamofire/5.9.0 VimeoNetworking/5.0.0', + Authorization: 'Basic MTMxNzViY2Y0NDE0YTQ5YzhjZTc0YmU0NjVjNDQxYzNkYWVjOWRlOTpHKzRvMmgzVUh4UkxjdU5FRW80cDNDbDhDWGR5dVJLNUJZZ055dHBHTTB4V1VzaG41bEx1a2hiN0NWYWNUcldSSW53dzRUdFRYZlJEZmFoTTArOTBUZkJHS3R4V2llYU04Qnl1bERSWWxUdXRidjNqR2J4SHFpVmtFSUcyRktuQw==', + 'Accept-Language': 'en' + } + } + ) + .then(a => a.json()) + .catch(() => {}); +} + +const compareQuality = (rendition, requestedQuality) => { + const quality = parseInt(rendition); + return Math.abs(quality - requestedQuality); +} + +const getDirectLink = (data, quality) => { + if (!data.files) return; + + const match = data.files + .filter(f => f.rendition?.endsWith('p')) + .reduce((prev, next) => { + const delta = { + prev: compareQuality(prev.rendition, quality), + next: compareQuality(next.rendition, quality) + }; + + return delta.prev < delta.next ? prev : next; + }); + + if (!match) return; + + return { + urls: match.link, + filenameAttributes: { + resolution: `${match.width}x${match.height}`, + qualityLabel: match.rendition, + extension: "mp4" + } + } +} + +const getHLS = async (configURL, obj) => { + if (!configURL) return; + + const api = await fetch(configURL) .then(r => r.json()) .catch(() => {}); - if (!api) return { error: 'ErrorCouldntFetch' }; - const fileMetadata = { - title: cleanString(api.video.title.trim()), - artist: cleanString(api.video.owner.name.trim()), + if (api.video?.duration > env.durationLimit) { + return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; } - if (api.video?.duration > env.durationLimit) - return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; - const urlMasterHLS = api.request?.files?.hls?.cdns?.akfire_interconnect_quic?.url; - if (!urlMasterHLS) return { error: 'ErrorCouldntFetch' } const masterHLS = await fetch(urlMasterHLS) @@ -57,9 +95,9 @@ export default async function(obj) { let bestQuality; - if (quality < resolutionMatch[variants[0]?.resolution?.width]) { + if (obj.quality < resolutionMatch[variants[0]?.resolution?.width]) { bestQuality = variants.find(v => - (quality === resolutionMatch[v.resolution.width]) + (obj.quality === resolutionMatch[v.resolution.width]) ); } @@ -84,15 +122,48 @@ export default async function(obj) { return { urls, isM3U8: true, - fileMetadata: fileMetadata, filenameAttributes: { - service: "vimeo", - id: obj.id, - title: fileMetadata.title, - author: fileMetadata.artist, resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`, qualityLabel: `${resolutionMatch[bestQuality.resolution.width]}p`, extension: "mp4" } } } + +export default async function(obj) { + let quality = obj.quality === "max" ? 9000 : Number(obj.quality); + if (quality < 240) quality = 240; + if (!quality || obj.isAudioOnly) quality = 9000; + + const info = await requestApiInfo(obj.id, obj.password); + let response; + + if (obj.isAudioOnly) { + response = await getHLS(info.config_url, { ...obj, quality }); + } + + if (!response) response = getDirectLink(info, quality); + if (!response) response = { error: 'ErrorEmptyDownload' }; + + if (response.error) { + return response; + } + + const fileMetadata = { + title: cleanString(info.name), + artist: cleanString(info.user.name), + }; + + return merge( + { + fileMetadata, + filenameAttributes: { + service: "vimeo", + id: obj.id, + title: fileMetadata.title, + author: fileMetadata.artist, + } + }, + response + ); +} diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 678eb70e..4a75bc0f 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -44,3 +44,17 @@ export function cleanHTML(html) { clean = clean.replace(/\n/g, ''); return clean } + +export function merge(a, b) { + for (const k of Object.keys(b)) { + if (Array.isArray(b[k])) { + a[k] = [...(a[k] ?? []), ...b[k]]; + } else if (typeof b[k] === 'object') { + a[k] = merge(a[k], b[k]); + } else { + a[k] = b[k]; + } + } + + return a; +} diff --git a/src/util/tests.json b/src/util/tests.json index 501ac2c0..982d99cb 100644 --- a/src/util/tests.json +++ b/src/util/tests.json @@ -674,7 +674,7 @@ "params": {}, "expected": { "code": 200, - "status": "stream" + "status": "redirect" } }], "reddit": [{