filename pattern customization

- added metadata for rutube and vimeo.
- added a picker for preferred filename pattern.
- fixed content disposition header.
- mute and audio dub tags don't appear together in a file name anymore.
- youtube: dub file name tag doesn't appear anymore if audio track is default.
This commit is contained in:
wukko
2023-10-12 23:14:54 +06:00
parent a57ee53b21
commit 16f74094b9
18 changed files with 249 additions and 59 deletions

View File

@ -0,0 +1,78 @@
export default function(f, template, isAudioOnly, isAudioMuted) {
let filename = '';
switch(template) {
default:
case "classic":
// youtube_MMK3L4W70g4_1920x1080_h264_mute.mp4
// youtube_MMK3L4W70g4_audio.mp3
filename += `${f.service}_${f.id}`;
if (!isAudioOnly) {
if (f.resolution) filename += `_${f.resolution}`;
if (f.youtubeFormat) filename += `_${f.youtubeFormat}`;
if (!isAudioMuted && f.youtubeDubName) filename += `_${f.youtubeDubName}`;
if (isAudioMuted) filename += '_mute';
filename += `.${f.extension}`
} else {
filename += `_audio`;
if (f.youtubeDubName) filename += `_${f.youtubeDubName}`;
}
break;
case "pretty":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, mute, youtube).mp4
// How secure is 256 bit security? - 3Blue1Brown (es, youtube).mp3
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}, `;
if (!isAudioMuted && f.youtubeDubName) filename += `${f.youtubeDubName}, `;
if (isAudioMuted) filename += 'mute, ';
filename += `${f.service}`;
filename += ')';
filename += `.${f.extension}`
} else {
filename += `- ${f.author} (`;
if (f.youtubeDubName) filename += `${f.youtubeDubName}, `;
filename += `${f.service})`
}
break;
case "basic":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, ru).mp4
// How secure is 256 bit security? - 3Blue1Brown (es).mp3
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}`;
if (!isAudioMuted && f.youtubeDubName) filename += `, ${f.youtubeDubName}`;
if (isAudioMuted) filename += ', mute';
filename += ')';
filename += `.${f.extension}`
} else {
filename += `- ${f.author}`;
if (f.youtubeDubName) filename += ` (${f.youtubeDubName})`;
}
break;
case "nerdy":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, ru, youtube, MMK3L4W70g4).mp4
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, ru, youtube, MMK3L4W70g4).mp4
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}, `;
if (!isAudioMuted && f.youtubeDubName) filename += `${f.youtubeDubName}, `;
if (isAudioMuted) filename += 'mute, ';
filename += `${f.service}, ${f.id}`;
filename += ')'
filename += `.${f.extension}`
} else {
filename += `- ${f.author} (`;
if (f.youtubeDubName) filename += `${f.youtubeDubName}, `;
filename += `${f.service}, ${f.id})`
}
break;
}
return filename.replace(' ,', '').replace(', )', ')').replace(',)', ')')
}

View File

@ -22,7 +22,7 @@ import streamable from "./services/streamable.js";
import twitch from "./services/twitch.js";
import rutube from "./services/rutube.js";
export default async function (host, patternMatch, url, lang, obj) {
export default async function(host, patternMatch, url, lang, obj) {
try {
let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata;
@ -147,7 +147,7 @@ export default async function (host, patternMatch, url, lang, obj) {
if (r.error) return apiJSON(0, { t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) });
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata);
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, obj.filenamePattern);
} catch (e) {
return apiJSON(0, { t: genericError(lang, host) })
}

View File

@ -1,14 +1,16 @@
import { audioIgnore, services, supportedAudio } from "../config.js";
import { apiJSON } from "../sub/utils.js";
import loc from "../../localization/manager.js";
import createFilename from "./createFilename.js";
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata) {
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern) {
let action,
responseType = 2,
defaultParams = {
u: r.urls,
service: host,
filename: r.filename,
filename: r.filenameAttributes ?
createFilename(r.filenameAttributes, filenamePattern, isAudioOnly, isAudioMuted) : r.filename,
fileMetadata: !disableMetadata ? r.fileMetadata : false
},
params = {}
@ -21,10 +23,13 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
else action = "video";
if (action === "picker" || action === "audio") {
defaultParams.filename = r.audioFilename;
if (!r.filenameAttributes) defaultParams.filename = r.audioFilename;
defaultParams.isAudioOnly = true;
defaultParams.audioFormat = audioFormat;
}
if (isAudioMuted && !r.filenameAttributes) {
defaultParams.filename = r.filename.replace('.', '_mute.')
}
switch (action) {
case "photo":
@ -135,7 +140,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
} else if (audioFormat === "best") {
audioFormat = "m4a";
copy = true;
if (r.audioFilename.includes("twitterspaces")) {
if (!r.filenameAttributes && r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}

View File

@ -20,5 +20,10 @@ export default async function(obj) {
if (!video) return { error: 'ErrorEmptyDownload' };
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
return { urls: video.url, filename: `pinterest_${pinId}.mp4`, audioFilename: `pinterest_${pinId}_audio` }
return {
urls: video.url,
filename: `pinterest_${pinId}.mp4`,
audioFilename: `pinterest_${pinId}_audio`
}
}

View File

@ -1,5 +1,6 @@
import HLS from 'hls-parser';
import { maxVideoDuration } from "../../config.js";
import { cleanString } from '../../sub/utils.js';
export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality;
@ -20,11 +21,23 @@ export default async function(obj) {
if (Number(quality) < bestQuality.resolution.height) {
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
}
let fileMetadata = {
title: cleanString(play.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(play.author.name.replace(/\p{Emoji}/gu, '').trim()),
}
return {
urls: bestQuality.uri,
isM3U8: true,
audioFilename: `rutube_${play.id}_audio`,
filename: `rutube_${play.id}_${bestQuality.resolution.width}x${bestQuality.resolution.height}.mp4`
filenameAttributes: {
service: "rutube",
id: play.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,
qualityLabel: `${bestQuality.resolution.height}p`,
extension: "mp4"
},
fileMetadata: fileMetadata
}
}

View File

@ -69,12 +69,19 @@ export default async function(obj) {
let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false });
if (!file) return { error: 'ErrorCouldntFetch' };
let fileMetadata = {
title: cleanString(json.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(json.user.username.replace(/\p{Emoji}/gu, '').trim()),
}
return {
urls: file,
audioFilename: `soundcloud_${json.id}`,
fileMetadata: {
title: cleanString(json.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(json.user.username.replace(/\p{Emoji}/gu, '').trim()),
}
filenameAttributes: {
service: "soundcloud",
id: json.id,
title: fileMetadata.title,
author: fileMetadata.artist
},
fileMetadata: fileMetadata
}
}

View File

@ -66,7 +66,11 @@ export default async function(obj) {
}
if (single) {
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
return {
urls: single,
filename: `twitter_${obj.id}.mp4`,
audioFilename: `twitter_${obj.id}_audio`
}
} else if (multiple) {
return { picker: multiple }
} else {

View File

@ -1,6 +1,6 @@
import { maxVideoDuration } from "../../config.js";
import { cleanString } from '../../sub/utils.js';
// vimeo you're fucked in the head for this
const resolutionMatch = {
"3840": "2160",
"2732": "1440",
@ -33,6 +33,11 @@ export default async function(obj) {
let downloadType = "dash";
if (!obj.forceDash && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
let fileMetadata = {
title: cleanString(api.video.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(api.video.owner.name.replace(/\p{Emoji}/gu, '').trim()),
}
if (downloadType !== "dash") {
if (qualityMatch[quality]) quality = qualityMatch[quality];
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
@ -43,7 +48,11 @@ export default async function(obj) {
if (Number(quality) < Number(bestQuality)) best = all.find(i => i["quality"].split('p')[0] === quality);
if (!best) return { error: 'ErrorEmptyDownload' };
return { urls: best["url"], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${best["width"]}x${best["height"]}.mp4` }
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] };
@ -77,8 +86,16 @@ export default async function(obj) {
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`
fileMetadata: fileMetadata,
filenameAttributes: {
service: "vimeo",
id: obj.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestVideo["width"]}x${bestVideo["height"]}`,
qualityLabel: `${bestVideo["height"]}p`,
extension: "mp4"
}
}
}
return { error: 'ErrorEmptyDownload' }

View File

@ -2,7 +2,11 @@ export default async function(obj) {
let post = await fetch(`https://archive.vine.co/posts/${obj.id}.json`).then((r) => { return r.json() }).catch(() => { return false });
if (!post) return { error: 'ErrorEmptyDownload' };
if (post.videoUrl) return { urls: post.videoUrl.replace("http://", "https://"), filename: `vine_${obj.id}.mp4`, audioFilename: `vine_${obj.id}_audio` };
if (post.videoUrl) return {
urls: post.videoUrl.replace("http://", "https://"),
filename: `vine_${obj.id}.mp4`,
audioFilename: `vine_${obj.id}_audio`
}
return { error: 'ErrorEmptyDownload' }
}

View File

@ -54,13 +54,12 @@ export default async function(o) {
audio = adaptive_formats.find(i => checkBestAudio(i) && !i["is_dubbed"]);
if (o.dubLang) {
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang);
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang && !i["audio_track"].audio_is_default);
if (dubbedAudio) {
audio = dubbedAudio;
isDubbed = true
}
}
let fileMetadata = {
title: cleanString(info.basic_info.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(info.basic_info.author.replace("- Topic", "").replace(/\p{Emoji}/gu, '').trim()),
@ -72,13 +71,21 @@ export default async function(o) {
if (descItems[4].startsWith("Released on:")) {
fileMetadata.date = descItems[4].replace("Released on: ", '').trim()
}
};
}
let filenameAttributes = {
service: "youtube",
id: o.id,
title: fileMetadata.title,
author: fileMetadata.artist,
youtubeDubName: isDubbed ? o.dubLang : false
}
if (hasAudio && o.isAudioOnly) return {
type: "render",
isAudioOnly: true,
urls: audio.url,
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
let checkSingle = (i) => ((qual(i) === quality || qual(i) === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
@ -87,21 +94,33 @@ export default async function(o) {
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
let single = info.streaming_data.formats.find(i => checkSingle(i));
if (single) return {
type: "bridge",
urls: single.url,
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`,
fileMetadata: fileMetadata
if (single) {
filenameAttributes.qualityLabel = video.quality_label;
filenameAttributes.resolution = `${single.width}x${single.height}`;
filenameAttributes.extension = c[o.format].container;
filenameAttributes.youtubeFormat = o.format;
return {
type: "bridge",
urls: single.url,
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
}
};
}
let video = adaptive_formats.find(i => ((Number(quality) > Number(bestQuality)) ? checkBestVideo(i) : checkRightVideo(i)));
if (video && audio) return {
type: "render",
urls: [video.url, audio.url],
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`,
fileMetadata: fileMetadata
};
if (video && audio) {
filenameAttributes.qualityLabel = video.quality_label;
filenameAttributes.resolution = `${video.width}x${video.height}`;
filenameAttributes.extension = c[o.format].container;
filenameAttributes.youtubeFormat = o.format;
return {
type: "render",
urls: [video.url, audio.url],
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
}
return { error: 'ErrorYTTryOtherCodec' }
}