diff --git a/docs/API.md b/docs/API.md index 7d5fa7d..6b3c105 100644 --- a/docs/API.md +++ b/docs/API.md @@ -14,18 +14,19 @@ Request Body Type: ``application/json``
Response Body Type: ``application/json`` ### Request Body Variables -| key | type | variables | default | description | -|:--------------------|:------------|:----------------------------------|:----------|:-------------------------------------------------------------------------------| -| ``url`` | ``string`` | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. | -| ``vCodec`` | ``string`` | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. | -| ``vQuality`` | ``string`` | ``144 / ... / 2160 / max`` | ``720`` | ``720`` quality is recommended for phones. | -| ``aFormat`` | ``string`` | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | | -| ``isAudioOnly`` | ``boolean`` | ``true / false`` | ``false`` | | -| ``isNoTTWatermark`` | ``boolean`` | ``true / false`` | ``false`` | Changes whether downloaded TikTok videos have watermarks. | -| ``isTTFullAudio`` | ``boolean`` | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. | -| ``isAudioMuted`` | ``boolean`` | ``true / false`` | ``false`` | Disables audio track in video downloads. | -| ``dubLang`` | ``boolean`` | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. | -| ``disableMetadata`` | ``boolean`` | ``true / false`` | ``false`` | Disables file metadata when set to ``true``. | +| key | type | variables | default | description | +|:--------------------|:------------|:-------------------------------------|:------------|:-------------------------------------------------------------------------------| +| ``url`` | ``string`` | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. | +| ``vCodec`` | ``string`` | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. | +| ``vQuality`` | ``string`` | ``144 / ... / 2160 / max`` | ``720`` | ``720`` quality is recommended for phones. | +| ``aFormat`` | ``string`` | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | | +| ``filenamePattern`` | ``boolean`` | ``classic / pretty / basic / nerdy`` | ``classic`` | Changes the way files are named. Previews can be seen in the web app. | +| ``isAudioOnly`` | ``boolean`` | ``true / false`` | ``false`` | | +| ``isNoTTWatermark`` | ``boolean`` | ``true / false`` | ``false`` | Changes whether downloaded TikTok videos have watermarks. | +| ``isTTFullAudio`` | ``boolean`` | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. | +| ``isAudioMuted`` | ``boolean`` | ``true / false`` | ``false`` | Disables audio track in video downloads. | +| ``dubLang`` | ``boolean`` | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. | +| ``disableMetadata`` | ``boolean`` | ``true / false`` | ``false`` | Disables file metadata when set to ``true``. | ### Response Body Variables | key | type | variables | diff --git a/package.json b/package.json index 2b39799..06fb1b7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "start": "node src/cobalt", "setup": "node src/modules/setup", "test": "node src/test/test", - "build": "node src/modules/buildStatic" + "build": "node src/modules/buildStatic", + "testFilenames": "node src/test/testFilenamePresets" }, "repository": { "type": "git", @@ -24,6 +25,7 @@ }, "homepage": "https://github.com/wukko/cobalt#readme", "dependencies": { + "content-disposition-header": "0.6.0", "cors": "^2.8.5", "dotenv": "^16.0.1", "esbuild": "^0.14.51", @@ -36,6 +38,6 @@ "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", - "youtubei.js": "^5.4.0" + "youtubei.js": "^6.4.1" } } diff --git a/src/config.json b/src/config.json index c14f6ce..56c8ed5 100644 --- a/src/config.json +++ b/src/config.json @@ -13,15 +13,20 @@ "url": "https://twitter.com/justusecobalt", "handle": "@justusecobalt" }, + "discord": { + "emoji": "👾", + "url": "https://discord.gg/pQPt8HBUPu", + "handle": "cobalt community server" + }, "mastodon": { "emoji": "🐘", "url": "https://wetdry.world/@cobalt", "handle": "@cobalt@wetdry.world" }, - "discord": { - "emoji": "👾", - "url": "https://discord.gg/pQPt8HBUPu", - "handle": "cobalt community server" + "support email": { + "emoji": "📧", + "url": "mailto:support@cobalt.tools", + "handle": "support@cobalt.tools" } } } diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 07c1415..e17b552 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -886,6 +886,46 @@ button:active, opacity: 1; transition: opacity 0.2s ease-out; } +.sponsored-by-text { + text-align: center!important; + font-size: .85rem; + color: var(--accent-subtext); + user-select: none; +} +#sponsored-logos { + width: 100%; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 0.2rem 1rem; + margin-bottom: 1rem; +} +.sponsored-logo svg { + height: inherit; + width: inherit; +} +.sponsored-logo svg path { + fill: var(--accent-subtext); +} +#filename-preview { + background: var(--accent-button); + margin-top: 0.8rem; +} +.filename-item { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 1rem; + padding: 0.5rem 0.7rem; +} +.filename-item.line { + border-bottom: 0.1rem solid var(--accent-button-elevated); +} +.filename-label { + color: var(--accent-subtext); + font-size: 0.8rem; +} /* rounded corners */ #bottom #paste, #footer .switch, @@ -900,7 +940,8 @@ button:active, #popup-about .switch, #popup-tabs .switch, .text-to-copy, -.text-to-copy.text-backdrop { +.text-to-copy.text-backdrop, +#filename-preview { border-radius: 5px / 6px; } [type=checkbox] { @@ -946,27 +987,6 @@ button:active, .collapse-list.last.expanded .collapse-header { border-radius: 0; } -.sponsored-by-text { - text-align: center!important; - font-size: .85rem; - color: var(--accent-subtext); - user-select: none; -} -#sponsored-logos { - width: 100%; - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 0.2rem 1rem; - margin-bottom: 1rem; -} -.sponsored-logo svg { - height: inherit; - width: inherit; -} -.sponsored-logo svg path { - fill: var(--accent-subtext); -} /* prevent resizing fliecker on ios if web app is installed as standalone */ @media all and (display-mode: standalone) { #home.visible { diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 5a69bdf..4a3ad9e 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,4 +1,4 @@ -const version = 37; +const version = 38; const ua = navigator.userAgent.toLowerCase(); const isIOS = ua.match("iphone os"); @@ -17,7 +17,8 @@ const switchers = { "aFormat": ["mp3", "best", "ogg", "wav", "opus"], "dubLang": ["original", "auto"], "vimeoDash": ["false", "true"], - "audioMode": ["false", "true"] + "audioMode": ["false", "true"], + "filenamePattern": ["classic", "pretty", "basic", "nerdy"] }; const checkboxes = [ "alwaysVisibleButton", @@ -33,14 +34,20 @@ const checkboxes = [ const exceptions = { // used for mobile devices "vQuality": "720" }; -const bottomPopups = ["error", "download"] +const bottomPopups = ["error", "download"]; const pageQuery = new URLSearchParams(window.location.search); let store = {}; -function changeAPI(url) { - apiURL = url; +function fixApiUrl(url) { + return url.endsWith('/') ? url.slice(0, -1) : url +} + +let apiURL = fixApiUrl(defaultApiUrl); + +function changeApi(url) { + apiURL = fixApiUrl(url); return true } function eid(id) { @@ -127,6 +134,8 @@ function detectColorScheme() { document.documentElement.setAttribute("data-theme", theme); } function changeTab(evnt, tabId, tabClass) { + if (tabId === "tab-settings-other") updateFilenamePreview(); + let tabcontent = document.getElementsByClassName(`tab-content-${tabClass}`); let tablinks = document.getElementsByClassName(`tab-${tabClass}`); @@ -274,6 +283,7 @@ function changeSwitcher(li, b) { (switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`) } if (li === "theme") detectColorScheme(); + if (li === "filenamePattern") updateFilenamePreview(); } else { let pref = switchers[li][0]; if (isMobile && exceptions[li]) pref = exceptions[li]; @@ -292,28 +302,6 @@ function checkbox(action) { } action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck(); } -function loadSettings() { - if (sGet("alwaysVisibleButton") === "true") { - eid("alwaysVisibleButton").checked = true; - eid("download-button").value = '>>' - eid("download-button").style.padding = '0 1rem'; - } - if (sGet("downloadPopup") === "true" && !isIOS) { - eid("downloadPopup").checked = true; - } - if (sGet("reduceTransparency") === "true" || isOldFirefox) { - eid("cobalt-body").classList.add('no-transparency'); - } - if (sGet("disableAnimations") === "true") { - eid("cobalt-body").classList.add('no-animation'); - } - for (let i = 0; i < checkboxes.length; i++) { - if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true; - } - for (let i in switchers) { - changeSwitcher(i, sGet(i)) - } -} function changeButton(type, text) { switch (type) { case 0: //error @@ -372,6 +360,7 @@ async function download(url) { let req = { url: encodeURIComponent(url.split("&")[0].split('%')[0]), aFormat: sGet("aFormat").slice(0, 4), + filenamePattern: sGet("filenamePattern"), dubLang: false } if (sGet("dubLang") === "auto") { @@ -514,6 +503,73 @@ function unpackSettings(b64) { } return changed } +function updateFilenamePreview() { + let videoFilePreview = ``; + let audioFilePreview = ``; + let resMatch = { + "max": "3840x2160", + "2160": "3840x2160", + "1440": "2560x1440", + "1080": "1920x1080", + "720": "1280x720", + "480": "854x480", + "360": "640x360", + } + // "dubLang" + // sGet("muteAudio") === "true" + switch(sGet("filenamePattern")) { + case "classic": + videoFilePreview = `youtube_yPYZpwSpKmA_${resMatch[sGet('vQuality')]}_${sGet('vCodec')}` + + `${sGet("muteAudio") === "true" ? "_mute" : ""}.${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`; + audioFilePreview = `youtube_yPYZpwSpKmA_audio.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`; + break; + case "pretty": + videoFilePreview = + `${loc.FilenamePreviewVideoTitle} ` + + `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, ` + + `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`; + audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} (soundcloud).${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`; + break; + case "basic": + videoFilePreview = + `${loc.FilenamePreviewVideoTitle} ` + + `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}${sGet("muteAudio") === "true" ? " mute" : ""}).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`; + audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor}.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`; + break; + case "nerdy": + videoFilePreview = + `${loc.FilenamePreviewVideoTitle} ` + + `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, ` + + `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube, yPYZpwSpKmA).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`; + audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} (soundcloud, 1242868615).${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`; + break; + } + eid("video-filename-text").innerHTML = videoFilePreview + eid("audio-filename-text").innerHTML = audioFilePreview +} +function loadSettings() { + if (sGet("alwaysVisibleButton") === "true") { + eid("alwaysVisibleButton").checked = true; + eid("download-button").value = '>>' + eid("download-button").style.padding = '0 1rem'; + } + if (sGet("downloadPopup") === "true" && !isIOS) { + eid("downloadPopup").checked = true; + } + if (sGet("reduceTransparency") === "true" || isOldFirefox) { + eid("cobalt-body").classList.add('no-transparency'); + } + if (sGet("disableAnimations") === "true") { + eid("cobalt-body").classList.add('no-animation'); + } + for (let i = 0; i < checkboxes.length; i++) { + if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true; + } + for (let i in switchers) { + changeSwitcher(i, sGet(i)) + } + updateFilenamePreview() +} window.onload = () => { loadCelebrationsEmoji(); diff --git a/src/front/emoji/3d/film_frames.svg b/src/front/emoji/3d/film_frames.svg new file mode 100644 index 0000000..4caf873 --- /dev/null +++ b/src/front/emoji/3d/film_frames.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/front/emoji/3d/headphone.svg b/src/front/emoji/3d/headphone.svg new file mode 100644 index 0000000..90fc6cd --- /dev/null +++ b/src/front/emoji/3d/headphone.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/front/emoji/email.svg b/src/front/emoji/email.svg new file mode 100644 index 0000000..144c953 --- /dev/null +++ b/src/front/emoji/email.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/front/emoji/film_frames.svg b/src/front/emoji/film_frames.svg new file mode 100644 index 0000000..7471d43 --- /dev/null +++ b/src/front/emoji/film_frames.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/front/emoji/headphone.svg b/src/front/emoji/headphone.svg new file mode 100644 index 0000000..1c9b670 --- /dev/null +++ b/src/front/emoji/headphone.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/front/updateBanners/meowthcenter.png b/src/front/updateBanners/meowthcenter.png new file mode 100644 index 0000000..2dc39e2 Binary files /dev/null and b/src/front/updateBanners/meowthcenter.png differ diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 0ce31dc..3264998 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -107,7 +107,7 @@ "SourceCode": "report issues, explore source code, star or fork the repo:", "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for 20 seconds and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's source code yourself and see that everything is as stated.", "ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", - "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nnote: youtube api sometimes acts unexpectedly. blame google for this, not me.", + "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api sometimes acts unexpectedly. try again or try another settings.", "SettingsCodecSubtitle": "youtube codec", "SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: low player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.", "SettingsAudioDub": "youtube audio track", @@ -145,6 +145,17 @@ "DataTransferSuccess": "btw, your settings have been transferred automatically :)", "DataTransferError": "something went wrong when transferring your preferences. you'll have to open settings and configure cobalt by hand.", "SupportNotAffiliated": "cobalt is not affiliated with any services listed above.", - "SponsoredBy": "sponsored by" + "SponsoredBy": "sponsored by", + "FilenameTitle": "file name style", + "FilenamePatternClassic": "classic", + "FilenamePatternPretty": "pretty", + "FilenamePatternBasic": "basic", + "FilenamePatternNerdy": "nerdy", + "FilenameDescription": "classic: default cobalt file name pattern.\npretty: title and info in brackets.\nbasic: title and basic info in brackets.\nnerdy: title and all info in brackets.\n\nsome services don’t support rich file names and always use the classic style.", + "Preview": "preview", + "FilenamePreviewVideoTitle": "Video Title", + "FilenamePreviewAudioTitle": "Audio Title", + "FilenamePreviewAudioAuthor": "Audio Author", + "UrgentFilenameUpdate": "customizable file names!" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index db0fa5b..87bd882 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -147,6 +147,17 @@ "DataTransferError": "при переносе настроек что-то пошло не так. придётся зайти в настройки и настроить кобальт вручную.", "SupportNotAffiliated": "кобальт не аффилирован ни с одним из перечисленных выше сервисов.", "SupportMetaNoticeRU": "деятельность meta platforms inc. (владелец instagram) запрещена на территории россии.", - "SponsoredBy": "спонсируется" + "SponsoredBy": "спонсируется", + "FilenameTitle": "стиль названий файлов", + "FilenamePatternClassic": "классический", + "FilenamePatternPretty": "красивый", + "FilenamePatternBasic": "простой", + "FilenamePatternNerdy": "полный", + "FilenameDescription": "классический: стандартный стиль названия файлов кобальта.\nкрасивый: название и инфа в скобках.\nпростой: название и основная инфа в скобках.\nполный: название и вся инфа в скобках.\n\nнекоторые сервисы не поддерживают красивые имена файлов и всегда используют классический стиль.", + "Preview": "превью", + "FilenamePreviewVideoTitle": "Название Видео", + "FilenamePreviewAudioTitle": "Название Аудио", + "FilenamePreviewAudioAuthor": "Автор Аудио", + "UrgentFilenameUpdate": "изменяемые названия файлов!" } } diff --git a/src/modules/changelog/changelog.json b/src/modules/changelog/changelog.json index db2a0f8..9ea24d6 100644 --- a/src/modules/changelog/changelog.json +++ b/src/modules/changelog/changelog.json @@ -1,5 +1,16 @@ { "current": { + "version": "7.6", + "date": "October 15, 2023", + "title": "customizable file names, instagram stories, and first cobalt sponsor!", + "banner": { + "file": "meowthcenter.png", + "width": 851, + "height": 640 + }, + "content": "as many have (very) often requested, cobalt now lets you pick between several file name format styles!\ngo to settings > other and change it to whichever you like! there's a preview of each style, so you know how exactly files are gonna look like.\n\nif you liked file names the way they were before, don't worry: classic style is still the default :)\n\non a different but not any less important note: cobalt is now sponsored by royalehosting.net!\noverall service performance and stability is gonna be better, but also more content will be possible to download thanks to geniuine server locations. and yes, still no ads or trackers.\n\nthis update also includes a bunch of other changes, check them out:\n\nservice improvements:\n*; added support for instagram stories thanks to #194.\n*; fixed reddit support thanks to #221.\n*; added support for rich file names for youtube, vimeo, soundcloud, rutube, and vk.\n*; mute and audio dub file name tags don't appear together anymore.\n*; youtube: dub file name tag doesn't appear anymore if audio track is default.\n\ninterface improvements:\n*; added a list of sponsors to about tab. if you host an instance, it's disabled by default, but can be enabled with showSponsors env variable.\n*; about button now opens about tab when no new changelog is available.\n*; fixed download button thickness on ios.\n\nyou now can reach out to cobalt via email for support! it's located in the about tab along with other socials, such as discord.\n\ni hope you enjoy this long-awaited update and have a blissful day :D" + }, + "history": [{ "version": "7.5", "date": "September 16, 2023", "title": "support for twitch clips and rutube!", @@ -9,8 +20,7 @@ "height": 640 }, "content": "hey! this update (finally) adds support for twitch clips and rutube, among other smaller changes.\n\nservice improvements:\n*; added support for twitch clips. no vods, they're unnecessary. just clip whatever you want to download!\n*; added support for rutube in case you ever wanted to download something russian.\n\ninterface improvements:\n*; added a note about cobalt not being affiliated with any supported services.\n*; added a note about meta (the company) in russian.\n*; better russian localization. will keep improving it to make it sound not so robotic over time.\n\nother improvements:\n*; all official servers are now using the docker package. and so should you!\n*; moved the load balancer to poland. requests should be slightly faster now.\n*; minor codebase clean up.\n\nif you're confused about the new domain, read the older changelog! just scroll lower and press \"expand\".\n\ni hope you find this update useful and have a wonderful day :)\n\nbtw, cobalt has a pretty active community server on discord. go to about > support & source code to join!" - }, - "history": [{ + }, { "version": "7.4", "date": "September 9, 2023", "title": "new domain, what's coming in future, bug fixes, and more!", diff --git a/src/modules/emoji.js b/src/modules/emoji.js index 04e053f..39cd94b 100644 --- a/src/modules/emoji.js +++ b/src/modules/emoji.js @@ -35,12 +35,16 @@ const names = { "📑": "boring_document", "🧮": "abacus", "😸": "cat_grin", - "📰": "newspaper" + "📰": "newspaper", + "🎞️": "film_frames", + "🎧": "headphone", + "📧": "email" } let sizing = { 18: 0.8, 22: 0.4, 30: 0.7, + 32: 0.8, 48: 0.9, 64: 0.9, 78: 0.9 diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 3de0110..8381d42 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -332,7 +332,8 @@ export default function(obj) { }]) }) + settingsCategory({ - name: t('SettingsCodecSubtitle'), + name: "codec", + title: t('SettingsCodecSubtitle'), body: switcher({ name: "vCodec", explanation: t('SettingsCodecDescription'), @@ -349,7 +350,8 @@ export default function(obj) { }) }) + settingsCategory({ - name: t('SettingsVimeoPrefer'), + name: "vimeo", + title: t('SettingsVimeoPrefer'), body: switcher({ name: "vimeoDash", explanation: t('SettingsVimeoPreferDescription'), @@ -426,6 +428,43 @@ export default function(obj) { }] }) }) + + settingsCategory({ + name: "filename", + title: t('FilenameTitle'), + body: switcher({ + name: "filenamePattern", + items: [{ + action: "classic", + text: t('FilenamePatternClassic') + }, { + action: "basic", + text: t('FilenamePatternBasic') + }, { + action: "pretty", + text: t('FilenamePatternPretty') + }, { + action: "nerdy", + text: t('FilenamePatternNerdy') + }] + }) + + `
+
+ ${emoji('🎞️', 32, 1, 1)} +
+
${t('Preview')}
+
+
+
+
+ ${emoji('🎧', 32, 1, 1)} +
+
${t('Preview')}
+
+
+
+
` + + explanation(t('FilenameDescription')) + }) + settingsCategory({ name: "accessibility", title: t('Accessibility'), @@ -523,8 +562,8 @@ export default function(obj) { diff --git a/src/modules/processing/createFilename.js b/src/modules/processing/createFilename.js new file mode 100644 index 0000000..ca72bf4 --- /dev/null +++ b/src/modules/processing/createFilename.js @@ -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 - Loossemble (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(',)', ')') +} diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 44831c7..c0d7ccf 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -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; @@ -150,7 +150,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) }) } diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 32ce4a5..eedf8ec 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -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 } diff --git a/src/modules/processing/services/pinterest.js b/src/modules/processing/services/pinterest.js index 335d736..086573c 100644 --- a/src/modules/processing/services/pinterest.js +++ b/src/modules/processing/services/pinterest.js @@ -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` + } } diff --git a/src/modules/processing/services/rutube.js b/src/modules/processing/services/rutube.js index c62191d..6b0c607 100644 --- a/src/modules/processing/services/rutube.js +++ b/src/modules/processing/services/rutube.js @@ -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 } } diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js index 99c812e..dc9e2e2 100644 --- a/src/modules/processing/services/soundcloud.js +++ b/src/modules/processing/services/soundcloud.js @@ -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 } } diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js index 487032a..a251041 100644 --- a/src/modules/processing/services/twitter.js +++ b/src/modules/processing/services/twitter.js @@ -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 { diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js index 3765b64..dc300f7 100644 --- a/src/modules/processing/services/vimeo.js +++ b/src/modules/processing/services/vimeo.js @@ -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' } diff --git a/src/modules/processing/services/vine.js b/src/modules/processing/services/vine.js index 21596d4..ea3a519 100644 --- a/src/modules/processing/services/vine.js +++ b/src/modules/processing/services/vine.js @@ -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' } } diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js index 995d013..b10a4c0 100644 --- a/src/modules/processing/services/vk.js +++ b/src/modules/processing/services/vk.js @@ -1,17 +1,21 @@ import { genericUserAgent, maxVideoDuration } from "../../config.js"; +import { cleanString } from "../../sub/utils.js"; const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"]; export default async function(o) { - let html, url, - quality = o.quality === "max" ? 2160 : o.quality, - filename = `vk_${o.userId}_${o.videoId}_`; + let html, url, quality = o.quality === "max" ? 2160 : o.quality; html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, { headers: { "user-agent": genericUserAgent } - }).then((r) => { return r.text() }).catch(() => { return false }); + }).then((r) => { return r.arrayBuffer() }).catch(() => { return false }); if (!html) return { error: 'ErrorCouldntFetch' }; + + // decode cyrillic from windows-1251 because vk still uses apis from prehistoring times + let decoder = new TextDecoder('windows-1251'); + html = decoder.decode(html); + if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' }; let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); @@ -28,11 +32,23 @@ export default async function(o) { if (Number(quality) > Number(o.quality)) quality = o.quality; url = js.player.params[0][`url${quality}`]; - filename += `${quality}p.mp4` - if (url && filename) return { + let fileMetadata = { + title: cleanString(js.player.params[0].md_title.replace(/\p{Emoji}/gu, '').trim()), + artist: cleanString(js.player.params[0].md_author.replace(/\p{Emoji}/gu, '').trim()), + } + + if (url) return { urls: url, - filename: filename + filenameAttributes: { + service: "vk", + id: `${o.userId}_${o.videoId}`, + title: fileMetadata.title, + author: fileMetadata.artist, + resolution: `${quality}p`, + qualityLabel: `${quality}p`, + extension: "mp4" + } } return { error: 'ErrorEmptyDownload' } } diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 38da410..71a617b 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -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 = single.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' } } diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index ffc868b..e979e34 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -3,6 +3,7 @@ import ffmpeg from "ffmpeg-static"; import { ffmpegArgs, genericUserAgent } from "../config.js"; import { getThreads, metadataManager } from "../sub/utils.js"; import { request } from 'undici'; +import { create as contentDisposition } from "content-disposition-header"; function fail(res) { if (!res.headersSent) res.sendStatus(500); @@ -12,8 +13,7 @@ function fail(res) { export async function streamDefault(streamInfo, res) { try { let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; - let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`; - res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`); + res.setHeader('Content-disposition', contentDisposition(streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename)); const { body: stream, headers } = await request(streamInfo.urls, { headers: { 'user-agent': genericUserAgent }, @@ -59,7 +59,7 @@ export async function streamLiveRender(streamInfo, res) { ], }); res.setHeader('Connection', 'keep-alive'); - res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`); + res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename)); res.on('error', () => { ffmpegProcess.kill(); fail(res); @@ -127,7 +127,7 @@ export function streamAudioOnly(streamInfo, res) { ], }); res.setHeader('Connection', 'keep-alive'); - res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`); + res.setHeader('Content-Disposition', contentDisposition(`${streamInfo.filename}.${streamInfo.audioFormat}`)); ffmpegProcess.stdio[3].pipe(res); ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); @@ -163,7 +163,7 @@ export function streamVideoOnly(streamInfo, res) { ], }); res.setHeader('Connection', 'keep-alive'); - res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}${streamInfo.mute ? '_mute' : ''}.${format}"`); + res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename)); ffmpegProcess.stdio[3].pipe(res); ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 720acb8..28b6abb 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -4,7 +4,8 @@ const apiVar = { allowed: { vCodec: ["h264", "av1", "vp9"], vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"], - aFormat: ["best", "mp3", "ogg", "wav", "opus"] + aFormat: ["best", "mp3", "ogg", "wav", "opus"], + filenamePattern: ["classic", "pretty", "basic", "nerdy"] }, booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"] } @@ -94,6 +95,7 @@ export function checkJSONPost(obj) { vCodec: "h264", vQuality: "720", aFormat: "mp3", + filenamePattern: "classic", isAudioOnly: false, isNoTTWatermark: false, isTTFullAudio: false, diff --git a/src/test/testFilenamePresets.js b/src/test/testFilenamePresets.js new file mode 100644 index 0000000..ba088cb --- /dev/null +++ b/src/test/testFilenamePresets.js @@ -0,0 +1,70 @@ +import createFilename from "../modules/processing/createFilename.js"; + +let tests = [ + { + f: { + service: 'youtube', + id: 'MMK3L4W70g4', + title: "Loossemble (루셈블) - 'Sensitive' MV", + author: 'Loossemble', + youtubeDubName: false, + qualityLabel: '2160p', + resolution: '3840x2160', + extension: 'webm', + youtubeFormat: 'vp9' + }, + isAudioOnly: false, + isAudioMuted: false + }, + { + f: { + service: 'youtube', + id: 'MMK3L4W70g4', + title: "Loossemble (루셈블) - 'Sensitive' MV", + author: 'Loossemble', + youtubeDubName: false, + qualityLabel: '2160p', + resolution: '3840x2160', + extension: 'webm', + youtubeFormat: 'vp9' + }, + isAudioOnly: true, + isAudioMuted: false + }, + { + f: { + service: 'youtube', + id: 'MMK3L4W70g4', + title: "Loossemble (루셈블) - 'Sensitive' MV", + author: 'Loossemble', + youtubeDubName: false, + qualityLabel: '2160p', + resolution: '3840x2160', + extension: 'webm', + youtubeFormat: 'vp9' + }, + isAudioOnly: false, + isAudioMuted: true + }, + { + f: { + service: 'vimeo', + id: 'MMK3L4W70g4', + title: "Loossemble (루셈블) - 'Sensitive' MV", + author: 'Loossemble', + qualityLabel: '2160p', + resolution: '3840x2160', + extension: 'mp4' + }, + isAudioOnly: false, + isAudioMuted: true + } +] + +for (let i = 0; i < tests.length; i++) { + console.log(`---${i}---`) + console.log(createFilename(tests[i].f, "classic", tests[i].isAudioOnly, tests[i].isAudioMuted)) + console.log(createFilename(tests[i].f, "basic", tests[i].isAudioOnly, tests[i].isAudioMuted)) + console.log(createFilename(tests[i].f, "pretty", tests[i].isAudioOnly, tests[i].isAudioMuted)) + console.log(createFilename(tests[i].f, "nerdy", tests[i].isAudioOnly, tests[i].isAudioMuted)) +}