vimeo support revamp and bug fixes

- completely reworked vimeo module.
- added support for audio downloads from vimeo.
- added support for chop type of dash for vimeo.
- added ability to choose between progressive and dash vimeo downloads. both to api and settings on frontend.
- added support for single m3u8 playlists. will be useful for future additions and is currently used for vimeo.
- proper error is now shown if there are no matching vimeo videos found
- temporarily disabled douyin support because bytedance killed off old endpoint.
- fixed the issue related to periods in tiktok usernames. (closes #96)
- fixed error text value patching in match module.
- fixed video stream removal for audio only option, wouldn't work in some edge cases.
- minor clean up.
This commit is contained in:
wukko
2023-03-15 22:18:31 +06:00
parent f6ee934949
commit 6e9f9efa28
16 changed files with 149 additions and 117 deletions

View File

@ -21,7 +21,7 @@ export default async function (host, patternMatch, url, lang, obj) {
let r, isAudioOnly = !!obj.isAudioOnly;
if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) });
if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang) });
if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang, host) });
switch (host) {
case "twitter":
@ -87,7 +87,9 @@ export default async function (host, patternMatch, url, lang, obj) {
case "vimeo":
r = await vimeo({
id: patternMatch["id"].slice(0, 11),
quality: obj.vQuality
quality: obj.vQuality,
isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash
});
break;
case "soundcloud":

View File

@ -14,6 +14,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
params = {}
if (!isAudioOnly && !r.picker && !isAudioMuted) action = "video";
if (r.isM3U8) action = "singleM3U8";
if (isAudioOnly && !r.picker) action = "audio";
if (r.picker) action = "picker";
if (isAudioMuted) action = "muteVideo";
@ -57,7 +58,9 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
break;
}
break;
case "singleM3U8":
params = { type: "videoM3U8" }
break;
case "muteVideo":
params = {
type: Array.isArray(r.urls) ? "bridge" : "mute",
@ -89,7 +92,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
break;
case "audio":
if ((host === "reddit" && r.typeId === 1) || (host === "vimeo" && !r.filename) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
let processType = "render";
let copy = false;
@ -120,6 +123,10 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
copy = false
}
}
if (r.isM3U8 || host === "vimeo") {
copy = false;
processType = "render"
}
params = {
type: processType,

View File

@ -54,8 +54,8 @@ export default async function(obj) {
let clientId = await findClientID();
if (!clientId) return { error: 'ErrorSoundCloudNoClientId' };
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive")
let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive"),
fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' };
if (json.duration > maxVideoDuration) return { error: ['ErrorLengthAudioConvert', maxVideoDuration / 60000] };

View File

@ -4,11 +4,11 @@ const userAgent = genericUserAgent.split(' Chrome/1')[0],
config = {
tiktok: {
short: "https://vt.tiktok.com/",
api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9&region=US&carrier_region=US",
api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9&region=US&carrier_region=US"
},
douyin: {
short: "https://v.douyin.com/",
api: "https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id={postId}",
api: "https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id={postId}"
}
}

View File

@ -81,8 +81,8 @@ export default async function(obj) {
).then((r) =>{ return r.status === 200 ? r.json() : false }).catch(() => { return false });
if (!streamStatus) return { error: 'ErrorCouldntFetch' };
let participants = AudioSpaceById.data.audioSpace.participants.speakers;
let listOfParticipants = `Twitter Space speakers: `;
let participants = AudioSpaceById.data.audioSpace.participants.speakers,
listOfParticipants = `Twitter Space speakers: `;
for (let i in participants) { listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` }
listOfParticipants = listOfParticipants.slice(0, -2);

View File

@ -2,83 +2,84 @@ import { maxVideoDuration } from "../../config.js";
const resolutionMatch = {
"3840": "2160",
"2732": "1440",
"2048": "1080",
"1920": "1080",
"1366": "720",
"1280": "720",
"960": "480"
"960": "480",
"640": "360",
"426": "240"
}
// ^ vimeo you're fucked in the head for this ^
const qualityMatch = {
"2160": "4K",
"1440": "2K",
"480": "540",
"4K": "2160",
"2K": "1440",
"540": "480"
}
export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality;
if (!quality || obj.isAudioOnly) quality = "9000";
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => { return r.json() }).catch(() => { return false });
if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = "dash";
if (JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
if (!obj.forceDash && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
switch(downloadType) {
case "progressive":
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0];
if (downloadType !== "dash") {
if (qualityMatch[quality]) quality = qualityMatch[quality];
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0];
try {
if (obj.quality !== "max") {
let pref = parseInt(obj.quality, 10)
for (let i in all) {
let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10)
if (currQuality === pref) {
best = all[i];
break
}
if (currQuality < pref) {
best = all[i-1];
break
}
}
}
} catch (e) {
best = all[0]
}
let bestQuality = all[0]["quality"].split('p')[0];
bestQuality = qualityMatch[bestQuality] ? qualityMatch[bestQuality] : bestQuality;
if (Number(quality) < Number(bestQuality)) best = all.find(i => i["quality"].split('p')[0] === quality);
return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` };
case "dash":
if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then((r) => { return r.json() }).catch(() => { return false });
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
let type = "parcel";
if (masterJSON.base_url === "../") type = "chop";
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width));
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;});
let bestVideo = masterJSON_Video[0], bestAudio = masterJSON_Audio[0];
switch (type) {
case "parcel":
if (obj.quality !== "max") {
let pref = parseInt(obj.quality, 10)
for (let i in masterJSON_Video) {
let currQuality = parseInt(resolutionMatch[masterJSON_Video[i]["width"]], 10)
if (currQuality < pref) {
break;
} else if (String(currQuality) === String(pref)) {
bestVideo = masterJSON_Video[i]
}
}
}
let baseUrl = masterJSONURL.split("/sep/")[0];
let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`,
audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` }
case "chop": // TO-DO: support for chop stream type
default:
return { error: 'ErrorEmptyDownload' }
}
default:
return { error: 'ErrorEmptyDownload' }
if (!best) return { error: 'ErrorEmptyDownload' };
return { urls: best["url"], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${best["width"]}x${best["height"]}.mp4` }
}
if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then((r) => { return r.json() }).catch(() => { return false });
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
let type = "parcel";
if (masterJSON.base_url === "../") type = "chop";
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)),
bestVideo = masterJSON_Video[0];
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality);
let videoUrl, audioUrl, baseUrl = masterJSONURL.split("/sep/")[0];
switch (type) {
case "parcel":
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a) => { if (a['mime_type'] === "audio/mp4") return true }),
bestAudio = masterJSON_Audio[0];
videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`,
audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
break;
case "chop":
videoUrl = `${baseUrl}/sep/video/${bestVideo.id}/master.m3u8`;
break;
}
if (videoUrl) {
return {
urls: audioUrl ? [videoUrl, audioUrl] : videoUrl,
isM3U8: audioUrl ? false : true,
audioFilename: `vimeo_${obj.id}_audio`,
filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4`
}
}
return { error: 'ErrorEmptyDownload' }
}

View File

@ -10,8 +10,7 @@ const representationMatch = {
"360": 2,
"240": 1,
"144": 0
}
const resolutionMatch = {
}, resolutionMatch = {
"3840": "2160",
"2560": "1440",
"1920": "1080",
@ -30,16 +29,16 @@ export default async function(o) {
if (!html) return { error: 'ErrorCouldntFetch' };
if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' };
let quality = o.quality === "max" ? 7 : representationMatch[o.quality];
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
let quality = o.quality === "max" ? 7 : representationMatch[o.quality],
js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
if (Number(js.mvData.is_active_live) !== 0) return { error: 'ErrorLiveVideo' };
if (js.mvData.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 }));
let repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"];
let bestQuality = repr[repr.length - 1];
let resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height'
let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })),
repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"],
bestQuality = repr[repr.length - 1],
resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height';
if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality];
if (bestQuality) return {

View File

@ -44,8 +44,8 @@ export default async function(o) {
if (!bestQuality && !o.isAudioOnly || !hasAudio) return { error: 'ErrorYTTryOtherCodec' };
if (info.basic_info.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let checkBestAudio = (i) => (i["has_audio"] && !i["has_video"]);
let audio = adaptive_formats.find(i => checkBestAudio(i) && i["is_original"]);
let checkBestAudio = (i) => (i["has_audio"] && !i["has_video"]),
audio = adaptive_formats.find(i => checkBestAudio(i) && i["is_original"]);
if (o.dubLang) {
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang);
@ -74,11 +74,11 @@ export default async function(o) {
return r
}
let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec));
let checkBestVideo = (i) => (i['quality_label'].split('p')[0] === bestQuality && !i["has_audio"] && i["has_video"]);
let checkRightVideo = (i) => (i['quality_label'].split('p')[0] === quality && !i["has_audio"] && i["has_video"]);
let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
checkBestVideo = (i) => (i['quality_label'].split('p')[0] === bestQuality && !i["has_audio"] && i["has_video"]),
checkRightVideo = (i) => (i['quality_label'].split('p')[0] === quality && !i["has_audio"] && i["has_video"]);
if (!o.isAudioOnly && !o.isAudioMuted) {
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
let single = info.streaming_data.formats.find(i => checkSingle(i));
if (single) return {
type: "bridge",

View File

@ -40,11 +40,12 @@
"douyin": {
"alias": "douyin videos & audio",
"patterns": ["video/:postId", ":id"],
"enabled": true
"enabled": false
},
"vimeo": {
"patterns": [":id"],
"enabled": true
"enabled": true,
"bestAudio": "mp3"
},
"soundcloud": {
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],