From f500d8b5f95b0d66012095405131069f689394c6 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 17:46:33 +0600 Subject: [PATCH 01/31] better support section in about - added status page to support section - updated order of items - clean up --- src/config.json | 28 +++++++++++++++------------- src/front/cobalt.css | 1 + src/front/emoji/loudspeaker.svg | 6 ++++++ src/front/emoji/wrench.svg | 3 +++ src/localization/languages/en.json | 9 +++++---- src/localization/languages/ru.json | 11 ++++++----- src/localization/manager.js | 7 ++++++- src/modules/emoji.js | 4 +++- src/modules/pageRender/elements.js | 6 +++--- src/modules/pageRender/page.js | 20 ++++++++++---------- 10 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 src/front/emoji/loudspeaker.svg create mode 100644 src/front/emoji/wrench.svg diff --git a/src/config.json b/src/config.json index cd170594..da1ec4c6 100644 --- a/src/config.json +++ b/src/config.json @@ -8,37 +8,37 @@ "contact": "https://wukko.me/contacts", "support": { "default": { + "email": { + "emoji": "📧", + "url": "mailto:support@cobalt.tools", + "name": "support@cobalt.tools" + }, "twitter": { "emoji": "🐦", "url": "https://twitter.com/justusecobalt", - "handle": "@justusecobalt" + "name": "@justusecobalt" }, "discord": { "emoji": "👾", "url": "https://discord.gg/pQPt8HBUPu", - "handle": "cobalt community server" + "name": "cobalt discord server" }, "mastodon": { "emoji": "🐘", "url": "https://wetdry.world/@cobalt", - "handle": "@cobalt@wetdry.world" - }, - "support email": { - "emoji": "📧", - "url": "mailto:support@cobalt.tools", - "handle": "support@cobalt.tools" + "name": "@cobalt@wetdry.world" } }, "ru": { - "канал в telegram": { + "telegram": { "emoji": "📬", "url": "https://t.me/justusecobalt_ru", - "handle": "@justusecobalt_ru" + "name": "канал в telegram" }, - "поддержка по почте": { + "email": { "emoji": "📧", "url": "mailto:support@cobalt.tools", - "handle": "support@cobalt.tools" + "name": "support@cobalt.tools" } } } @@ -58,7 +58,9 @@ } }, "links": { - "saveToGalleryShortcut": "https://www.icloud.com/shortcuts/b401917928fd407daf1db0fd07eb7e78" + "saveToGalleryShortcut": "https://www.icloud.com/shortcuts/b401917928fd407daf1db0fd07eb7e78", + "statusPage": "https://status.cobalt.tools/", + "troubleshootingGuide": "https://github.com/wukko/cobalt/blob/current/docs/troubleshooting.md" }, "celebrations": { "01-01": "🎄", diff --git a/src/front/cobalt.css b/src/front/cobalt.css index ce364dc3..be41f997 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -797,6 +797,7 @@ button:active, .collapse-body { display: none; padding: var(--padding-1); + padding-bottom: 1rem; user-select: text; -webkit-user-select: text; } diff --git a/src/front/emoji/loudspeaker.svg b/src/front/emoji/loudspeaker.svg new file mode 100644 index 00000000..6acd5873 --- /dev/null +++ b/src/front/emoji/loudspeaker.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/front/emoji/wrench.svg b/src/front/emoji/wrench.svg new file mode 100644 index 00000000..b186d3b3 --- /dev/null +++ b/src/front/emoji/wrench.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index ddb5cb76..159a2e78 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -1,7 +1,7 @@ { "name": "english", "substrings": { - "ContactLink": "create an issue on github" + "ContactLink": "check the status page or create an issue on github." }, "strings": { "AppTitleCobalt": "cobalt", @@ -45,7 +45,6 @@ "SettingsEnableDownloadPopup": "ask how to save", "AccessibilityEnableDownloadPopup": "ask what to do with downloads", "SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.", - "LinkGitHubChanges": ">> see previous commits and contribute on github", "NoScriptMessage": "cobalt uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.", "DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add this siri shortcut.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request, and press \"always allow\".\n\nalternative method:\npress and hold the download button, hide the video preview, and select \"download linked file\" to download.\nthen, open safari downloads, select the file you downloaded, open share menu, and finally press \"save video\".", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.", @@ -127,7 +126,7 @@ "FeatureErrorGeneric": "your browser doesn't allow or support this feature. check if there are any updates available and try again!", "ClipboardErrorFirefox": "you're using firefox where all clipboard reading functionality is disabled.\n\nyou can fix this by following steps listed here!\n\n...or you can paste the link manually instead.", "ClipboardErrorNoPermission": "cobalt can't access the most recent item in your clipboard without your permission.\n\nif you don't want to give access, just paste the link manually instead.\n\nif you do, go to site settings and enable the clipboard permission.", - "SupportSelfTroubleshooting": "experiencing issues? try self-troubleshooting guide first!", + "SupportSelfTroubleshooting": "experiencing issues? try one of these first:", "AccessibilityGoBack": "go back and close the popup", "CollapseKeyboard": "keyboard shortcuts", "KeyboardShortcutsIntro": "use cobalt even faster with keyboard shortcuts:", @@ -157,6 +156,8 @@ "FilenamePreviewAudioTitle": "Audio Title", "FilenamePreviewAudioAuthor": "Audio Author", "UrgentFilenameUpdate": "customizable file names!", - "UrgentTwitterPatch": "fixes and easier downloads" + "UrgentTwitterPatch": "fixes and easier downloads", + "StatusPage": "service status page", + "TroubleshootingGuide": "self-troubleshooting guide" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 3b00fa3c..c81eb558 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -1,14 +1,14 @@ { "name": "русский", "substrings": { - "ContactLink": "напиши об этом на github (можно на русском)" + "ContactLink": "глянь статус серверов или напиши о проблеме на github (можно на русском)" }, "strings": { "AppTitleCobalt": "кобальт", "LinkInput": "вставь ссылку сюда", "AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.", "EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.", - "MadeWithLove": "сделано wukko, с <3", + "MadeWithLove": "сделано с любовью <3", "AccessibilityInputArea": "зона вставки ссылки", "AccessibilityOpenAbout": "открыть окно с инфой", "AccessibilityDownloadButton": "кнопка скачивания", @@ -45,7 +45,6 @@ "SettingsEnableDownloadPopup": "выбор метода скачивания", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.", - "LinkGitHubChanges": ">> смотри предыдущие изменения на github", "NoScriptMessage": "кобальт использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.", "DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь этот сценарий siri.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.", @@ -128,7 +127,7 @@ "FeatureErrorGeneric": "твой браузер не разрешает или не поддерживает эту функцию. проверь наличие обновлений и попробуй ещё раз!", "ClipboardErrorFirefox": "ты используешь firefox в котором все функции чтения из буфера обмена отключены по умолчанию.\n\nно это можно исправить следуя шагам, описанным здесь\n\n...или же ты можешь просто вставить ссылку вручную.", "ClipboardErrorNoPermission": "кобальт не может прочитать последний элемент в буфере обмена без твоего разрешения.\n\nесли ты не хочешь давать доступ, просто вставь ссылку вручную.\n\nну а если хочешь, то открой настройки сайта и разреши доступ на чтение буфера обмена.", - "SupportSelfTroubleshooting": "возникли проблемы? попробуй сначала исправить всё сам по этому гиду!", + "SupportSelfTroubleshooting": "возникли проблемы? попробуй сначала что-то из этого:", "AccessibilityGoBack": "вернуться назад и закрыть окно", "CollapseKeyboard": "горячие клавиши", "KeyboardShortcutsIntro": "пользуйся кобальтом ещё быстрее с горячими клавишами:", @@ -159,6 +158,8 @@ "FilenamePreviewAudioTitle": "Название Аудио", "FilenamePreviewAudioAuthor": "Автор Аудио", "UrgentFilenameUpdate": "изменяемые названия файлов!", - "UrgentTwitterPatch": "фиксы и удобное скачивание" + "UrgentTwitterPatch": "фиксы и удобное скачивание", + "StatusPage": "статус серверов", + "TroubleshootingGuide": "гайд по устранению проблем" } } diff --git a/src/localization/manager.js b/src/localization/manager.js index 2a2389b9..ce396891 100644 --- a/src/localization/manager.js +++ b/src/localization/manager.js @@ -16,7 +16,12 @@ export async function loadLoc() { } export function replaceBase(s) { - return s.replace(/\n/g, '
').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{repo}/g, repo).replace(/\*;/g, "•"); + return s + .replace(/\n/g, '
') + .replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut) + .replace(/{repo}/g, repo) + .replace(/{statusPage}/g, links.statusPage) + .replace(/\*;/g, "•"); } export function replaceAll(lang, str, string, replacement) { let s = replaceBase(str[string]) diff --git a/src/modules/emoji.js b/src/modules/emoji.js index b2d759ed..48806b87 100644 --- a/src/modules/emoji.js +++ b/src/modules/emoji.js @@ -39,7 +39,9 @@ const names = { "🎞️": "film_frames", "🎧": "headphone", "📧": "email", - "📬": "mailbox" + "📬": "mailbox", + "📢": "loudspeaker", + "🔧": "wrench" } let sizing = { 18: 0.8, diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index 8415fb25..f74bb097 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -158,15 +158,15 @@ export function popupWithBottomButtons(obj) { ` } -export function socialLink(emji, name, handle, url) { - return `` +export function socialLink(emji, name, url) { + return `` } export function socialLinks(lang) { let links = authorInfo.support[lang] ? authorInfo.support[lang] : authorInfo.support.default; let r = ``; for (let i in links) { r += socialLink( - emoji(links[i].emoji), i, links[i].handle, links[i].url + emoji(links[i].emoji), links[i].name, links[i].url ) } return r diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 9e486169..a741dbc9 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -1,5 +1,5 @@ import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc, sponsoredList, betaTag } from "./elements.js"; -import { services as s, authorInfo, version, repo, donations, supportedAudio } from "../config.js"; +import { services as s, authorInfo, version, repo, donations, supportedAudio, links } from "../config.js"; import { getCommitInfo } from "../sub/currentCommit.js"; import loc from "../../localization/manager.js"; import emoji from "../emoji.js"; @@ -146,15 +146,15 @@ export default function(obj) { }, { name: "support", title: `${emoji("❤️‍🩹")} ${t("CollapseSupport")}`, - body: - `${t("SupportSelfTroubleshooting")}

` - + `${t("FollowSupport")}
` - + `${socialLinks(obj.lang)}
` - + `${t("SourceCode")}
` - + `${socialLink( - emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo - )}
- ${t("SupportNote")}` + body: `${t("SupportSelfTroubleshooting")}` + + `${socialLink(emoji("📢"), t("StatusPage"), links.statusPage)}` + + `${socialLink(emoji("🔧"), t("TroubleshootingGuide"), links.troubleshootingGuide)}` + + `
` + + `${t("FollowSupport")}` + + `${socialLinks(obj.lang)}` + + `
` + + `${t("SourceCode")}` + + `${socialLink(emoji("🐙"), repo.replace("https://github.com/", ''), repo)}` }, { name: "privacy", title: `${emoji("🔒")} ${t("CollapsePrivacy")}`, From dd563eb752f0883e295b6042db3387f802d5871f Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 9 Dec 2023 11:00:54 +0000 Subject: [PATCH 02/31] api: rework url parsing - tlds are now parsed and validated correctly (e.g. ".co.uk" works now) - url patterns are pre-compiled instead of being compiled for every request - aliases are computed in a safe manner using the URL object where possible --- package.json | 1 + src/modules/api.js | 41 ++++----- src/modules/config.js | 9 ++ src/modules/processing/hostOverrides.js | 112 ++++++++++++++++++------ src/modules/sub/utils.js | 34 +------ 5 files changed, 116 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index c4b03eb4..ed639ba2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "hls-parser": "^0.10.7", "nanoid": "^4.0.2", "node-cache": "^5.1.2", + "psl": "^1.9.0", "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", diff --git a/src/modules/api.js b/src/modules/api.js index 62e9a7c6..19f657cc 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -1,35 +1,32 @@ -import UrlPattern from "url-pattern"; +import { services } from "./config.js"; -import { services as patterns } from "./config.js"; - -import { cleanURL, apiJSON } from "./sub/utils.js"; +import { apiJSON } from "./sub/utils.js"; import { errorUnsupported } from "./sub/errors.js"; import loc from "../localization/manager.js"; import match from "./processing/match.js"; -import hostOverrides from "./processing/hostOverrides.js"; +import { hasValidHostname, normalizeURL } from "./processing/url.js"; export async function getJSON(originalURL, lang, obj) { try { - let patternMatch, url = encodeURI(decodeURIComponent(originalURL)), - hostname = new URL(url).hostname.split('.'), - host = hostname[hostname.length - 2]; + const url = normalizeURL(decodeURIComponent(originalURL)); - if (!url.startsWith('https://')) return apiJSON(0, { t: errorUnsupported(lang) }); - - let overrides = hostOverrides(host, url); - host = overrides.host; - url = overrides.url; - - if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) return apiJSON(0, { t: errorUnsupported(lang) }); - - let pathToMatch = cleanURL(url, host).split(`.${patterns[host]['tld'] ? patterns[host]['tld'] : "com"}/`)[1].replace('.', ''); - for (let i in patterns[host]["patterns"]) { - patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(pathToMatch); - if (patternMatch) break + if (!hasValidHostname(url) || !services[host].enabled) { + return apiJSON(0, { t: errorUnsupported(lang) }); } - if (!patternMatch) return apiJSON(0, { t: errorUnsupported(lang) }); - return await match(host, patternMatch, url, lang, obj) + let patternMatch; + for (const pattern of services[host].patterns) { + patternMatch = pattern.match( + url.pathname.substring(1) + url.search + ); + if (patternMatch) break; + } + + if (!patternMatch) { + return apiJSON(0, { t: errorUnsupported(lang) }); + } + + return await match(host, patternMatch, url.toString(), lang, obj) } catch (e) { return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }) } diff --git a/src/modules/config.js b/src/modules/config.js index 6fbe9d43..a0525ae8 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -1,8 +1,17 @@ +import UrlPattern from "url-pattern"; import { loadJSON } from "./sub/loadFromFs.js"; const config = loadJSON("./src/config.json"); const packageJson = loadJSON("./package.json"); const servicesConfigJson = loadJSON("./src/modules/processing/servicesConfig.json"); +Object.values(servicesConfigJson.config).forEach(service => { + service.patterns = service.patterns.map( + pattern => new UrlPattern(pattern, { + segmentValueCharset: UrlPattern.defaultOptions.segmentValueCharset + '\\.' + }) + ) +}) + export const services = servicesConfigJson.config, audioIgnore = servicesConfigJson.audioIgnore, diff --git a/src/modules/processing/hostOverrides.js b/src/modules/processing/hostOverrides.js index 88553e35..86d45add 100644 --- a/src/modules/processing/hostOverrides.js +++ b/src/modules/processing/hostOverrides.js @@ -1,48 +1,102 @@ -export default function (inHost, inURL) { - let host = String(inHost); - let url = String(inURL); +import { services } from "./config.js"; +import { strict as assert } from "node:assert"; +import psl from "psl"; - switch(host) { +export function aliasURL(url) { + assert(url instanceof URL); + + const host = psl.parse(url.hostname); + const parts = url.pathname.split('/'); + + switch (host.sld) { case "youtube": - if (url.startsWith("https://youtube.com/live/") || url.startsWith("https://www.youtube.com/live/")) { - url = url.split("?")[0].replace("www.", ""); - url = `https://youtube.com/watch?v=${url.replace("https://youtube.com/live/", "")}` - } - if (url.includes('youtube.com/shorts/')) { - url = url.split('?')[0].replace('shorts/', 'watch?v='); + if (url.pathname.startsWith('/live/') || url.pathname.startsWith('/shorts/')) { + url.pathname = '/watch'; + // ['', 'live' || 'shorts', id, ...rest] + url.search = `?v=${encodeURIComponent(parts[2])}` } break; case "youtu": - if (url.startsWith("https://youtu.be/")) { - host = "youtube"; - url = `https://youtube.com/watch?v=${url.replace("https://youtu.be/", "")}` + if (url.hostname === 'youtu.be' && parts.length === 2) { + /* youtu.be urls can be weird, e.g. https://youtu.be///asdasd// still works + ** but we only care about the 1st segment of the path */ + url = new URL(`https://youtube.com/watch?v=${ + encodeURIComponent(parts[1]) + }`) } break; + case "vxtwitter": case "x": - if (url.startsWith("https://x.com/")) { - host = "twitter"; - url = url.replace("https://x.com/", "https://twitter.com/") - } - if (url.startsWith("https://vxtwitter.com/")) { - host = "twitter"; - url = url.replace("https://vxtwitter.com/", "https://twitter.com/") + if (['x.com', 'vxtwitter.com'].includes(url.hostname)) { + url.hostname = 'twitter.com' } break; + case "tumblr": - if (!url.includes("blog/view")) { - if (url.slice(-1) === '/') url = url.slice(0, -1); - url = url.replace(url.split('/')[5], '') + if (!url.pathname.includes("/blog/view")) { + if (url.pathname.endsWith('/')) + url.pathname = url.pathname.slice(0, -1); + url.pathname = url.pathname.replace(parts[5], '') } break; + case "twitch": - if (url.includes('clips.twitch.tv')) { - url = url.split('?')[0].replace('clips.twitch.tv/', 'twitch.tv/_/clip/'); + if (url.hostname === 'clips.twitch.tv' && parts.length >= 2) { + url = new URL(`https://twitch.tv/_/clip/${parts[1]}`); } break; } - return { - host: host, - url: url - } + + return { url, host: host.sld } } + +export function cleanURL({ url, host }) { + assert(url instanceof URL); + let stripQuery = true; + + if (host === 'pinterest') { + url.hostname = 'pinterest.com' + } else if (host === 'vk' && url.pathname.includes('/clip')) { + if (url.searchParams.get('z')) + url.search = '?z=' + encodeURIComponent(url.searchParams.get('z')); + stripQuery = false; + } else if (host === 'youtube' && url.searchParams.get('v')) { + url.search = '?v=' + encodeURIComponent(url.searchParams.get('v')); + stripQuery = false; + } + + if (stripQuery) { + url.search = url.hash = '' + } + + if (url.pathname.endsWith('/')) + url.pathname = url.pathname.slice(0, -1); + + return url +} + +export function normalizeURL(url) { + return cleanURL( + aliasURL( + new URL(url.replace(/^https\/\//, 'https://')) + ) + ); +} + +export function hasValidHostname(url) { + const host = psl.parse(url.hostname); + if (host.error) return false; + + const service = services[host.sld]; + if (!service) return false; + + if ((service.tld ?? 'com') !== host.tld) return false; + + const anySubdomainAllowed = service.subdomains === '*'; + const validSubdomain = [null, 'www', ...(service.subdomains ?? [])].includes(host.subdomain); + if (!validSubdomain && !anySubdomainAllowed) + return false; + + return true; +} \ No newline at end of file diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index e165a68a..ef64d07b 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -52,29 +52,7 @@ export function metadataManager(obj) { for (let i in keys) { if (tags.includes(keys[i])) commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) } return commands; } -export function cleanURL(url, host) { - switch (host) { - case "vk": - url = url.includes('clip') ? url.split('&')[0] : url.split('?')[0]; - break; - case "youtube": - url = url.split('&')[0]; - break; - case "tiktok": - url = url.replace(/@([a-zA-Z]+(\.[a-zA-Z]+)+)/, "@a") - case "pinterest": - url = url.replace(/:\/\/(?:www.)pinterest(?:\.[a-z.]+)/, "://pinterest.com") - default: - url = url.split('?')[0]; - if (url.substring(url.length - 1) === "/") url = url.substring(0, url.length - 1); - break; - } - for (let i in forbiddenChars) { - url = url.replaceAll(forbiddenChars[i], '') - } - url = url.replace('https//', 'https://') - return url.slice(0, 128) -} + export function cleanString(string) { for (let i in forbiddenCharsString) { string = string.replaceAll("/", "_").replaceAll(forbiddenCharsString[i], '') @@ -121,13 +99,9 @@ export function checkJSONPost(obj) { } } - if (def.dubLang) def.dubLang = verifyLanguageCode(obj.dubLang); - - obj["url"] = decodeURIComponent(String(obj["url"])); - let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), - host = hostname[hostname.length - 2]; - def["url"] = encodeURIComponent(cleanURL(obj["url"], host)); - + if (def.dubLang) + def.dubLang = verifyLanguageCode(obj.dubLang); + def.url = obj.url; return def } catch (e) { return false From 2e1eb1b864b071f976777105b758729c600f47d1 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 12 Dec 2023 15:47:29 +0000 Subject: [PATCH 03/31] api: rename hostOverrides to 'url' it does a bit more than it did before now --- src/modules/processing/{hostOverrides.js => url.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/modules/processing/{hostOverrides.js => url.js} (100%) diff --git a/src/modules/processing/hostOverrides.js b/src/modules/processing/url.js similarity index 100% rename from src/modules/processing/hostOverrides.js rename to src/modules/processing/url.js From 149c16abbb7dbbdceaf3e1ed138608a5f55c435f Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 12 Dec 2023 23:19:01 +0000 Subject: [PATCH 04/31] url: make youtu.be alias rule more lax --- src/modules/processing/url.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 86d45add..a58254a9 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -17,7 +17,7 @@ export function aliasURL(url) { } break; case "youtu": - if (url.hostname === 'youtu.be' && parts.length === 2) { + if (url.hostname === 'youtu.be' && parts.length >= 2) { /* youtu.be urls can be weird, e.g. https://youtu.be///asdasd// still works ** but we only care about the 1st segment of the path */ url = new URL(`https://youtube.com/watch?v=${ From f9feaa41ce77b14c3782ea64acc74361780f6853 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 22:43:36 +0000 Subject: [PATCH 05/31] tumblr: stricter subdomain parsing --- src/modules/processing/services/tumblr.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index 7ae7336c..f978e5a4 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -1,9 +1,12 @@ +import psl from "psl"; import { genericUserAgent } from "../../config.js"; export default async function(obj) { - let html = await fetch(`https://${ - obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '') - }.tumblr.com/post/${obj.id}`, { + const { subdomain } = psl.parse(obj.url); + if (subdomain?.includes('.')) + return { error: 'ErrorBrokenLink' } + + let html = await fetch(`https://${obj.user ?? subdomain}.tumblr.com/post/${obj.id}`, { headers: { "user-agent": genericUserAgent } }).then((r) => { return r.text() }).catch(() => { return false }); From c458423e03153bbb4d40cbb894128f8324429bd5 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 22:43:57 +0000 Subject: [PATCH 06/31] match: light cleanup --- src/modules/processing/match.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 86b6af82..096cf47f 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -37,7 +37,6 @@ export default async function(host, patternMatch, url, lang, obj) { break; case "vk": r = await vk({ - url: url, userId: patternMatch["userId"], videoId: patternMatch["videoId"], quality: obj.vQuality @@ -57,11 +56,13 @@ export default async function(host, patternMatch, url, lang, obj) { isAudioMuted: obj.isAudioMuted, dubLang: obj.dubLang } - if (url.match('music.youtube.com') || isAudioOnly === true) { + + if (new URL(url).hostname === 'music.youtube.com' || isAudioOnly === true) { fetchInfo.quality = "max"; fetchInfo.format = "vp9"; fetchInfo.isAudioOnly = true } + r = await youtube(fetchInfo); break; case "reddit": @@ -83,9 +84,9 @@ export default async function(host, patternMatch, url, lang, obj) { break; case "tumblr": r = await tumblr({ - id: patternMatch["id"], - url: url, - user: patternMatch["user"] || false + id: patternMatch.id, + user: patternMatch.user, + url }); break; case "vimeo": From 3056624b3d574c2ef8d2bcff7005cba46041c095 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 22:48:38 +0000 Subject: [PATCH 07/31] servicesConfig: set up subdomains --- src/modules/processing/servicesConfig.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 6425f0f7..e33bdab4 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -24,22 +24,26 @@ "youtube": { "alias": "youtube videos, shorts & music", "patterns": ["watch?v=:id", "embed/:id", "watch/:id"], + "subdomains": ["music"], "bestAudio": "opus", "enabled": true }, "tumblr": { "patterns": ["post/:id", "blog/view/:user/:id", ":user/:id", ":user/:id/:trackingId"], + "subdomains": "*", "enabled": true }, "tiktok": { "alias": "tiktok videos, photos & audio", "patterns": [":user/video/:postId", ":id", "t/:id"], + "subdomains": ["vt", "vm"], "audioFormats": ["best", "m4a", "mp3"], "enabled": true }, "douyin": { "alias": "douyin videos & audio", "patterns": ["video/:postId", ":id"], + "subdomains": ["v"], "enabled": false }, "vimeo": { @@ -49,6 +53,7 @@ }, "soundcloud": { "patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"], + "subdomains": ["on"], "bestAudio": "opus", "enabled": true }, From 662360509c4c55b3431db8bbd3c82f2f5a927ba2 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 22:51:24 +0000 Subject: [PATCH 08/31] url: return host instead of bool for success --- src/modules/api.js | 5 +++-- src/modules/processing/url.js | 14 ++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/modules/api.js b/src/modules/api.js index 19f657cc..eebbfe27 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -4,13 +4,14 @@ import { apiJSON } from "./sub/utils.js"; import { errorUnsupported } from "./sub/errors.js"; import loc from "../localization/manager.js"; import match from "./processing/match.js"; -import { hasValidHostname, normalizeURL } from "./processing/url.js"; +import { getHostIfValid, normalizeURL } from "./processing/url.js"; export async function getJSON(originalURL, lang, obj) { try { const url = normalizeURL(decodeURIComponent(originalURL)); + const host = getHostIfValid(url); - if (!hasValidHostname(url) || !services[host].enabled) { + if (!host || !services[host].enabled) { return apiJSON(0, { t: errorUnsupported(lang) }); } diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index a58254a9..75202f5f 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -84,19 +84,17 @@ export function normalizeURL(url) { ); } -export function hasValidHostname(url) { +export function getHostIfValid(url) { const host = psl.parse(url.hostname); - if (host.error) return false; + if (host.error) return; const service = services[host.sld]; - if (!service) return false; - - if ((service.tld ?? 'com') !== host.tld) return false; + if (!service) return; + if ((service.tld ?? 'com') !== host.tld) return; const anySubdomainAllowed = service.subdomains === '*'; const validSubdomain = [null, 'www', ...(service.subdomains ?? [])].includes(host.subdomain); - if (!validSubdomain && !anySubdomainAllowed) - return false; + if (!validSubdomain && !anySubdomainAllowed) return; - return true; + return host.sld; } \ No newline at end of file From 30c9652b6e8803509cc7d563113214bb583a793b Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:03:41 +0000 Subject: [PATCH 09/31] url: typo --- src/modules/processing/url.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 75202f5f..0420a465 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -1,4 +1,4 @@ -import { services } from "./config.js"; +import { services } from "../config.js"; import { strict as assert } from "node:assert"; import psl from "psl"; From 81e68c37f500218ce79793981678941ee97879b6 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:04:05 +0000 Subject: [PATCH 10/31] processing: pass URL object instead of string --- src/modules/api.js | 2 +- src/modules/processing/match.js | 8 ++++++-- src/modules/processing/services/soundcloud.js | 7 ++++--- src/modules/processing/services/tumblr.js | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/modules/api.js b/src/modules/api.js index eebbfe27..21132022 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -27,7 +27,7 @@ export async function getJSON(originalURL, lang, obj) { return apiJSON(0, { t: errorUnsupported(lang) }); } - return await match(host, patternMatch, url.toString(), lang, obj) + return await match(host, patternMatch, url, lang, obj) } catch (e) { return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }) } diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 096cf47f..fd7e6ec9 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -1,3 +1,5 @@ +import { strict as assert } from "node:assert"; + import { apiJSON } from "../sub/utils.js"; import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js"; @@ -23,6 +25,8 @@ import twitch from "./services/twitch.js"; import rutube from "./services/rutube.js"; export default async function(host, patternMatch, url, lang, obj) { + assert(url instanceof URL); + try { let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata; @@ -57,7 +61,7 @@ export default async function(host, patternMatch, url, lang, obj) { dubLang: obj.dubLang } - if (new URL(url).hostname === 'music.youtube.com' || isAudioOnly === true) { + if (url.hostname === 'music.youtube.com' || isAudioOnly === true) { fetchInfo.quality = "max"; fetchInfo.format = "vp9"; fetchInfo.isAudioOnly = true @@ -100,7 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) { case "soundcloud": isAudioOnly = true; r = await soundcloud({ - url: url, + url, author: patternMatch["author"], song: patternMatch["song"], shortLink: patternMatch["shortLink"] || false, diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js index fcc6de02..b13c0440 100644 --- a/src/modules/processing/services/soundcloud.js +++ b/src/modules/processing/services/soundcloud.js @@ -39,17 +39,18 @@ export default async function(obj) { if (!clientId) return { error: 'ErrorSoundCloudNoClientId' }; let link; - if (obj.shortLink && !obj.author && !obj.song) { + if (obj.url.hostname === 'on.soundcloud.com' && obj.shortLink) { link = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`, { redirect: "manual" }).then((r) => { if (r.status === 302 && r.headers.get("location").startsWith("https://soundcloud.com/")) { return r.headers.get("location").split('?', 1)[0] } - return false - }).catch(() => { return false }); + }).catch(() => {}); } + if (!link && obj.author && obj.song) { link = `https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}` } + if (!link) return { error: 'ErrorCouldntFetch' }; let json = await fetch(`https://api-v2.soundcloud.com/resolve?url=${link}&client_id=${clientId}`).then((r) => { diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index f978e5a4..f894e4e4 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -2,7 +2,7 @@ import psl from "psl"; import { genericUserAgent } from "../../config.js"; export default async function(obj) { - const { subdomain } = psl.parse(obj.url); + const { subdomain } = psl.parse(obj.url.hostname); if (subdomain?.includes('.')) return { error: 'ErrorBrokenLink' } From 34d8333d726db624fdb1fad21ab7a00527e7791e Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:05:57 +0000 Subject: [PATCH 11/31] tumblr: render error template for broken links --- src/modules/processing/services/tumblr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index f894e4e4..90eb45c2 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -4,7 +4,7 @@ import { genericUserAgent } from "../../config.js"; export default async function(obj) { const { subdomain } = psl.parse(obj.url.hostname); if (subdomain?.includes('.')) - return { error: 'ErrorBrokenLink' } + return { error: ['ErrorBrokenLink', 'tumblr'] } let html = await fetch(`https://${obj.user ?? subdomain}.tumblr.com/post/${obj.id}`, { headers: { "user-agent": genericUserAgent } From ba35ec923e87382e013f52c74848794d5640f330 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:14:22 +0000 Subject: [PATCH 12/31] url: re-parse hostname after validating --- src/modules/processing/url.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 0420a465..ab995b72 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -48,11 +48,12 @@ export function aliasURL(url) { break; } - return { url, host: host.sld } + return url } -export function cleanURL({ url, host }) { +export function cleanURL(url) { assert(url instanceof URL); + const host = psl.parse(url.hostname).sld; let stripQuery = true; if (host === 'pinterest') { From 0244c40d0b58213e94289d5a8ef7c02c44d2f329 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:23:21 +0000 Subject: [PATCH 13/31] config: add "@" to allowed pattern symbols needed for tiktok urls --- src/modules/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/config.js b/src/modules/config.js index a0525ae8..5e079536 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -7,7 +7,7 @@ const servicesConfigJson = loadJSON("./src/modules/processing/servicesConfig.jso Object.values(servicesConfigJson.config).forEach(service => { service.patterns = service.patterns.map( pattern => new UrlPattern(pattern, { - segmentValueCharset: UrlPattern.defaultOptions.segmentValueCharset + '\\.' + segmentValueCharset: UrlPattern.defaultOptions.segmentValueCharset + '@\\.' }) ) }) From 3a00bc7f8d971370bdaa30812b285185d46dd4df Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:37:10 +0000 Subject: [PATCH 14/31] url: remove tumblr aliasing not quite sure what its purpose is/was anyways (tracking id removal? it's not used anyways) --- src/modules/processing/url.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index ab995b72..a7f35f2d 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -12,7 +12,7 @@ export function aliasURL(url) { case "youtube": if (url.pathname.startsWith('/live/') || url.pathname.startsWith('/shorts/')) { url.pathname = '/watch'; - // ['', 'live' || 'shorts', id, ...rest] + // parts := ['', 'live' || 'shorts', id, ...rest] url.search = `?v=${encodeURIComponent(parts[2])}` } break; @@ -33,14 +33,6 @@ export function aliasURL(url) { } break; - case "tumblr": - if (!url.pathname.includes("/blog/view")) { - if (url.pathname.endsWith('/')) - url.pathname = url.pathname.slice(0, -1); - url.pathname = url.pathname.replace(parts[5], '') - } - break; - case "twitch": if (url.hostname === 'clips.twitch.tv' && parts.length >= 2) { url = new URL(`https://twitch.tv/_/clip/${parts[1]}`); From 818c236782993c89b810d26f502fcd3645318c95 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:42:53 +0000 Subject: [PATCH 15/31] package.json: lock psl version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ed639ba2..e839fe34 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "hls-parser": "^0.10.7", "nanoid": "^4.0.2", "node-cache": "^5.1.2", - "psl": "^1.9.0", + "psl": "1.9.0", "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", From e1fa32beb3eb60696a9e525a298070dde28886db Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:46:49 +0000 Subject: [PATCH 16/31] front: don't mangle and encode urls when sending to api --- src/front/cobalt.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 183716cb..b3d3b7a8 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,4 +1,4 @@ -const version = 39; +const version = 40; const ua = navigator.userAgent.toLowerCase(); const isIOS = ua.match("iphone os"); @@ -358,7 +358,7 @@ async function download(url) { eid("url-clear").style.display = "none"; eid("url-input-area").disabled = true; let req = { - url: encodeURIComponent(url.split("&")[0].split('%')[0]), + url, aFormat: sGet("aFormat").slice(0, 4), filenamePattern: sGet("filenamePattern"), dubLang: false From 18a3c06a9eb3b2e250ab51ca80390f55e90e38d7 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 14 Dec 2023 23:57:00 +0000 Subject: [PATCH 17/31] url: always strip username, password, port, fragment --- src/modules/processing/url.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index a7f35f2d..246d9620 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -60,9 +60,11 @@ export function cleanURL(url) { } if (stripQuery) { - url.search = url.hash = '' + url.search = '' } + url.username = url.password = url.port = url.hash = '' + if (url.pathname.endsWith('/')) url.pathname = url.pathname.slice(0, -1); From 5928b21feee9fa4a51debbdd1bf2760bff660096 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Mon, 18 Dec 2023 12:44:18 +0000 Subject: [PATCH 18/31] tumblr: fix priority of subdomain/segment for username --- src/modules/processing/services/tumblr.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index 90eb45c2..08b3a3e2 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -2,11 +2,13 @@ import psl from "psl"; import { genericUserAgent } from "../../config.js"; export default async function(obj) { - const { subdomain } = psl.parse(obj.url.hostname); + let { subdomain } = psl.parse(obj.url.hostname); if (subdomain?.includes('.')) return { error: ['ErrorBrokenLink', 'tumblr'] } + else if (subdomain === 'www') + subdomain = undefined; - let html = await fetch(`https://${obj.user ?? subdomain}.tumblr.com/post/${obj.id}`, { + let html = await fetch(`https://${subdomain ?? obj.user}.tumblr.com/post/${obj.id}`, { headers: { "user-agent": genericUserAgent } }).then((r) => { return r.text() }).catch(() => { return false }); From aaa61cfee9b6f0c60ef87ebd7d3b5194cb9d054a Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 18:04:52 +0600 Subject: [PATCH 19/31] processing url: alt domains for services and fixvx support --- src/modules/processing/servicesConfig.json | 1 + src/modules/processing/url.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index e33bdab4..b2b260f3 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -13,6 +13,7 @@ }, "twitter": { "alias": "twitter videos & voice", + "altDomains": ["x.com", "vxtwitter.com", "fixvx.com"], "patterns": [":user/status/:id", ":user/status/:id/video/:v"], "enabled": true }, diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 246d9620..bb602109 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -27,8 +27,9 @@ export function aliasURL(url) { break; case "vxtwitter": + case "fixvx": case "x": - if (['x.com', 'vxtwitter.com'].includes(url.hostname)) { + if (services.twitter.altDomains.includes(url.hostname)) { url.hostname = 'twitter.com' } break; From 509d24fd87a24871c4d235a0a5f8adceae019bc2 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 18:07:01 +0600 Subject: [PATCH 20/31] servicesConfig: add subdomains for twitter and youtube --- src/modules/processing/servicesConfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index b2b260f3..9d296114 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -14,6 +14,7 @@ "twitter": { "alias": "twitter videos & voice", "altDomains": ["x.com", "vxtwitter.com", "fixvx.com"], + "subdomains": ["mobile", "www"], "patterns": [":user/status/:id", ":user/status/:id/video/:v"], "enabled": true }, @@ -25,7 +26,7 @@ "youtube": { "alias": "youtube videos, shorts & music", "patterns": ["watch?v=:id", "embed/:id", "watch/:id"], - "subdomains": ["music"], + "subdomains": ["music", "www", "m"], "bestAudio": "opus", "enabled": true }, From 88666eeeaa546c9a85661b61c3443d5b5fc3d2fd Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 18:08:17 +0600 Subject: [PATCH 21/31] servicesConfig: www was unnecessary... --- src/modules/processing/servicesConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 9d296114..1ac431e6 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -26,7 +26,7 @@ "youtube": { "alias": "youtube videos, shorts & music", "patterns": ["watch?v=:id", "embed/:id", "watch/:id"], - "subdomains": ["music", "www", "m"], + "subdomains": ["music", "m"], "bestAudio": "opus", "enabled": true }, From d6e4b5ac20fb51186e819edddef916fcf53c7138 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 18:08:47 +0600 Subject: [PATCH 22/31] servicesConfig: www is unnecessary here too --- src/modules/processing/servicesConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 1ac431e6..46519cf2 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -14,7 +14,7 @@ "twitter": { "alias": "twitter videos & voice", "altDomains": ["x.com", "vxtwitter.com", "fixvx.com"], - "subdomains": ["mobile", "www"], + "subdomains": ["mobile"], "patterns": [":user/status/:id", ":user/status/:id/video/:v"], "enabled": true }, From ddc87ca42bb4cf1f7e07283897a679d0a3227057 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 18:20:51 +0600 Subject: [PATCH 23/31] api: clean url upon entry, not down the road --- src/core/api.js | 2 +- src/modules/api.js | 5 ++--- src/modules/processing/url.js | 2 +- src/modules/sub/utils.js | 4 +++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 4e78fbb5..71ce8d3e 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -97,7 +97,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { let chck = checkJSONPost(request); if (!chck) throw new Error(); - j = await getJSON(chck["url"], lang, chck); + j = await getJSON(chck.url, lang, chck); } else { j = apiJSON(0, { t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink') diff --git a/src/modules/api.js b/src/modules/api.js index 21132022..c3549bb3 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -4,11 +4,10 @@ import { apiJSON } from "./sub/utils.js"; import { errorUnsupported } from "./sub/errors.js"; import loc from "../localization/manager.js"; import match from "./processing/match.js"; -import { getHostIfValid, normalizeURL } from "./processing/url.js"; +import { getHostIfValid } from "./processing/url.js"; -export async function getJSON(originalURL, lang, obj) { +export async function getJSON(url, lang, obj) { try { - const url = normalizeURL(decodeURIComponent(originalURL)); const host = getHostIfValid(url); if (!host || !services[host].enabled) { diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index bb602109..2f1ac87f 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -93,4 +93,4 @@ export function getHostIfValid(url) { if (!validSubdomain && !anySubdomainAllowed) return; return host.sld; -} \ No newline at end of file +} diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index ef64d07b..28d37c6c 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -1,3 +1,4 @@ +import { normalizeURL } from "../processing/url.js"; import { createStream } from "../stream/manage.js"; const apiVar = { @@ -72,6 +73,7 @@ export function unicodeDecode(str) { } export function checkJSONPost(obj) { let def = { + url: normalizeURL(decodeURIComponent(obj.url)), vCodec: "h264", vQuality: "720", aFormat: "mp3", @@ -101,7 +103,7 @@ export function checkJSONPost(obj) { if (def.dubLang) def.dubLang = verifyLanguageCode(obj.dubLang); - def.url = obj.url; + return def } catch (e) { return false From 197198ad79809e48ad0dc8941e40ec3de8b6c09d Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 22:21:06 +0600 Subject: [PATCH 24/31] soundcloud: fall back to mp3 when no opus found also made match action decider readable --- src/modules/processing/match.js | 3 +- src/modules/processing/matchActionDecider.js | 143 ++++++++++-------- src/modules/processing/services/soundcloud.js | 15 +- src/modules/processing/servicesConfig.json | 2 +- src/test/tests.json | 8 + 5 files changed, 104 insertions(+), 67 deletions(-) diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index fd7e6ec9..3317dc6c 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -108,8 +108,7 @@ export default async function(host, patternMatch, url, lang, obj) { author: patternMatch["author"], song: patternMatch["song"], shortLink: patternMatch["shortLink"] || false, - accessKey: patternMatch["accessKey"] || false, - format: obj.aFormat + accessKey: patternMatch["accessKey"] || false }); break; case "instagram": diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index face4433..a7b8740b 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -3,7 +3,7 @@ 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, filenamePattern) { +export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern) { let action, responseType = 2, defaultParams = { @@ -13,7 +13,8 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d createFilename(r.filenameAttributes, filenamePattern, isAudioOnly, isAudioMuted) : r.filename, fileMetadata: !disableMetadata ? r.fileMetadata : false }, - params = {} + params = {}, + audioFormat = String(userFormat) if (r.isPhoto) action = "photo"; else if (r.picker) action = "picker" @@ -32,9 +33,49 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d } switch (action) { + default: + return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); + case "photo": responseType = 1; break; + + case "singleM3U8": + params = { type: "remux" } + break; + + case "muteVideo": + params = { + type: Array.isArray(r.urls) ? "bridge" : "mute", + u: Array.isArray(r.urls) ? r.urls[0] : r.urls, + mute: true + } + if (host === "reddit" && r.typeId === 1) responseType = 1; + break; + + case "picker": + responseType = 5; + switch (host) { + case "instagram": + case "twitter": + params = { picker: r.picker }; + break; + case "douyin": + case "tiktok": + let pickerType = "render"; + if (audioFormat === "mp3" || audioFormat === "best") { + audioFormat = "mp3"; + pickerType = "bridge" + } + params = { + type: pickerType, + picker: r.picker, + u: Array.isArray(r.urls) ? r.urls[1] : r.urls, + copy: audioFormat === "best" ? true : false + } + } + break; + case "video": switch (host) { case "bilibili": @@ -78,81 +119,63 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d break; } break; - case "singleM3U8": - params = { type: "remux" } - break; - case "muteVideo": - params = { - type: Array.isArray(r.urls) ? "bridge" : "mute", - u: Array.isArray(r.urls) ? r.urls[0] : r.urls, - mute: true - } - if (host === "reddit" && r.typeId === 1) responseType = 1; - break; - - case "picker": - responseType = 5; - switch (host) { - case "instagram": - case "twitter": - params = { picker: r.picker }; - break; - case "douyin": - case "tiktok": - let pickerType = "render"; - if (audioFormat === "mp3" || audioFormat === "best") { - audioFormat = "mp3"; - pickerType = "bridge" - } - params = { - type: pickerType, - picker: r.picker, - u: Array.isArray(r.urls) ? r.urls[1] : r.urls, - copy: audioFormat === "best" ? true : false - } - } - break; case "audio": if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) { return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }) } - let processType = "render"; - let copy = false; + let processType = "render", + copy = false; - if (!supportedAudio.includes(audioFormat)) audioFormat = "best"; + if (!supportedAudio.includes(audioFormat)) { + audioFormat = "best" + } - if ((host === "tiktok" || host === "douyin") - && services.tiktok.audioFormats.includes(audioFormat)) { - if (r.isMp3) { - if (audioFormat === "mp3" || audioFormat === "best") { - audioFormat = "mp3"; - processType = "bridge" - } - } else if (audioFormat === "best") { + const isBestAudio = audioFormat === "best"; + const isBestOrMp3 = audioFormat === "mp3" || isBestAudio; + const isBestAudioDefined = isBestAudio && services[host]["bestAudio"]; + const isBestHostAudio = services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]); + + const isTikTok = host === "tiktok" || host === "douyin"; + const isTumblr = host === "tumblr" && !r.filename; + const isSoundCloud = host === "soundcloud"; + + if (isTikTok && services.tiktok.audioFormats.includes(audioFormat)) { + if (r.isMp3 && isBestOrMp3) { + audioFormat = "mp3"; + processType = "bridge" + } else if (isBestAudio) { audioFormat = "m4a"; processType = "bridge" } } - if (host === "tumblr" && !r.filename - && (audioFormat === "best" || audioFormat === "mp3")) { + + if (isSoundCloud && services.soundcloud.audioFormats.includes(audioFormat)) { + if (r.isMp3 && isBestOrMp3) { + audioFormat = "mp3"; + processType = "render" + copy = true + } else if (isBestAudio || audioFormat === "opus") { + audioFormat = "opus"; + processType = "render" + copy = true + } + } + + if (isTumblr && isBestOrMp3) { audioFormat = "mp3"; processType = "bridge" } - if ((audioFormat === "best" && services[host]["bestAudio"]) - || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) { + + if (isBestAudioDefined || isBestHostAudio) { audioFormat = services[host]["bestAudio"]; - if (host === "soundcloud") { - processType = "render" - copy = true - } else { - processType = "bridge" - } - } else if (audioFormat === "best") { + processType = "bridge"; + } else if (isBestAudio && !isSoundCloud) { audioFormat = "m4a"; - copy = true; + copy = true } + if (r.isM3U8 || host === "vimeo") { copy = false; processType = "render" @@ -165,8 +188,6 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d copy: copy } break; - default: - return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); } return apiJSON(responseType, {...defaultParams, ...params}) diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js index b13c0440..46aae5df 100644 --- a/src/modules/processing/services/soundcloud.js +++ b/src/modules/processing/services/soundcloud.js @@ -60,8 +60,16 @@ export default async function(obj) { if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' }; - let fileUrlBase = json.media.transcodings.filter(v => v.preset === "opus_0_0")[0]["url"], - fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; + let isMp3, + selectedStream = json.media.transcodings.filter(v => v.preset === "opus_0_0") + + // fall back to mp3 if no opus is available + if (selectedStream.length === 0) { + selectedStream = json.media.transcodings.filter(v => v.preset === "mp3_0_0") + isMp3 = true + } + let fileUrlBase = selectedStream[0]["url"]; + let 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' }; @@ -83,6 +91,7 @@ export default async function(obj) { title: fileMetadata.title, author: fileMetadata.artist }, - fileMetadata: fileMetadata + isMp3, + fileMetadata } } diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 46519cf2..e85b9070 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -56,7 +56,7 @@ "soundcloud": { "patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"], "subdomains": ["on"], - "bestAudio": "opus", + "audioFormats": ["best", "opus", "mp3"], "enabled": true }, "instagram": { diff --git a/src/test/tests.json b/src/test/tests.json index 369852ba..c224f5e4 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -304,6 +304,14 @@ "code": 200, "status": "stream" } + }, { + "name": "no opus audio, fallback to mp3", + "url": "https://soundcloud.com/frums/credits", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } }], "youtube": [{ "name": "4k video (h264, 1440)", From 354fbdfa55e4a9207049ea7a65426b43eb322c11 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 22:38:10 +0600 Subject: [PATCH 25/31] package: bump version up to 7.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e839fe34..499dc0ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.7.5", + "version": "7.8", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From 0dcd36c16f0b248c9a803aa6c6aadc9dd3b0fd2c Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 22:40:02 +0600 Subject: [PATCH 26/31] tumblr: formatting --- src/modules/processing/services/tumblr.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index 08b3a3e2..6b56ada0 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -3,10 +3,12 @@ import { genericUserAgent } from "../../config.js"; export default async function(obj) { let { subdomain } = psl.parse(obj.url.hostname); - if (subdomain?.includes('.')) + + if (subdomain?.includes('.')) { return { error: ['ErrorBrokenLink', 'tumblr'] } - else if (subdomain === 'www') - subdomain = undefined; + } else if (subdomain === 'www') { + subdomain = undefined + } let html = await fetch(`https://${subdomain ?? obj.user}.tumblr.com/post/${obj.id}`, { headers: { "user-agent": genericUserAgent } @@ -29,5 +31,5 @@ export default async function(obj) { } } else r = { error: 'ErrorEmptyDownload' }; - return r; + return r } From ca04acc468f992edf5b627c6e1c135a0f8fca20c Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 22:45:04 +0600 Subject: [PATCH 27/31] tumblr: fix at.tumblr link handling --- src/modules/processing/services/tumblr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index 6b56ada0..75d354e7 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -6,7 +6,7 @@ export default async function(obj) { if (subdomain?.includes('.')) { return { error: ['ErrorBrokenLink', 'tumblr'] } - } else if (subdomain === 'www') { + } else if (subdomain === 'www' || subdomain === 'at') { subdomain = undefined } From 1fbd0a2c05f7f56d11daa1014cf7e90768cc1660 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Dec 2023 23:57:33 +0600 Subject: [PATCH 28/31] front: optimise ui (mostly address area) - using :first-child and :last-child instead of classes for switchers - improved scaling - less mess in css --- src/front/cobalt.css | 75 +++++++++++++++--------------- src/localization/languages/en.json | 6 +-- src/localization/languages/ru.json | 6 +-- src/modules/pageRender/elements.js | 6 +-- src/modules/pageRender/page.js | 5 +- 5 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/front/cobalt.css b/src/front/cobalt.css index be41f997..200f288f 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -254,16 +254,17 @@ button:active, } #cobalt-main-box { position: fixed; - width: 60%; + width: 40rem; height: auto; display: flex; - flex-direction: row; + flex-direction: column; + align-content: center; + align-items: center; } #logo { text-align: left; font-size: 1rem; white-space: nowrap; - width: 7rem; height: 2.5rem; align-items: center; display: flex; @@ -295,7 +296,7 @@ button:active, } #url-input-area { background: none; - padding: 0 1rem; + padding-left: calc(20px + 1.4rem); width: 100%; color: var(--accent); border: 0; @@ -316,6 +317,15 @@ button:active, outline: none; border-bottom: var(--border-10); } +#link-icon { + display: flex; + position: absolute; + width: 20px; + padding-top: 0.2rem; + left: 0.7rem; + flex-wrap: nowrap; + color: var(--accent-subtext); +} #download-button { height: 2.5rem; color: var(--accent); @@ -331,6 +341,10 @@ button:active, color: var(--accent-subtext); cursor: not-allowed; } +#cobalt-main-box .switch, +#footer .switch { + box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset; +} #footer { bottom: 0; width: 100%; @@ -458,9 +472,6 @@ button:active, .popup.scrollable { height: 95%; } -.scrollable .bottom-link { - padding-bottom: 2rem; -} .changelog-subtitle { font-size: 1.3rem; padding-bottom: var(--gap-no-icon); @@ -948,7 +959,7 @@ button:active, .text-to-copy, .text-to-copy.text-backdrop, #filename-preview { - border-radius: 5px / 6px; + border-radius: 6px / 7px; } [type=checkbox] { border-radius: 3px / 4px; @@ -969,28 +980,28 @@ button:active, border-top: var(--accent-highlight) solid 0.1rem; bottom: -1px; } -.switches .first { - border-top-left-radius: 5px 6px; - border-bottom-left-radius: 5px 6px; +.switches :first-child { + border-top-left-radius: 6px 7px; + border-bottom-left-radius: 6px 7px; } -.switches .last { - border-top-right-radius: 5px 6px; - border-bottom-right-radius: 5px 6px; +.switches :last-child { + border-top-right-radius: 6px 7px; + border-bottom-right-radius: 6px 7px; } .text-backdrop { border-radius: 3px / 4px; } -.collapse-list.first, -.collapse-list.first .collapse-header { - border-top-left-radius: 6px 7px; - border-top-right-radius: 6px 7px; +.collapse-list:first-child, +.collapse-list:first-child .collapse-header { + border-top-left-radius: 7px 8px; + border-top-right-radius: 7px 8px; } -.collapse-list.last, -.collapse-list.last .collapse-header { - border-bottom-left-radius: 6px 7px; - border-bottom-right-radius: 6px 7px; +.collapse-list:last-child, +.collapse-list:last-child .collapse-header { + border-bottom-left-radius: 7px 8px; + border-bottom-right-radius: 7px 8px; } -.collapse-list.last.expanded .collapse-header { +.collapse-list:last-child.expanded .collapse-header { border-radius: 0; } /* prevent resizing fliecker on ios if web app is installed as standalone */ @@ -1009,9 +1020,6 @@ button:active, } } @media screen and (max-width: 1440px) { - #cobalt-main-box { - width: 65%; - } .popup.small { width: 30% } @@ -1025,9 +1033,6 @@ button:active, } } @media screen and (max-width: 1200px) { - #cobalt-main-box { - width: 70%; - } .popup.small { width: 35% } @@ -1036,9 +1041,6 @@ button:active, } } @media screen and (max-width: 1025px) { - #cobalt-main-box { - width: 75%; - } .popup.small { width: 40% } @@ -1063,14 +1065,14 @@ button:active, width: calc(100% - 1.3rem); } } -@media screen and (max-width: 720px) { +@media screen and (max-width: 660px) { #cobalt-main-box { width: calc(100% - (0.7rem * 2)); } #cobalt-main-box #bottom { - flex-direction: column-reverse; + flex-direction: row-reverse; } - #cobalt-main-box #bottom button { + #cobalt-main-box #bottom #audioMode button, #audioMode { width: 100%; } #footer { @@ -1167,9 +1169,6 @@ button:active, #popup-tabs { padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem); } - .bottom-link { - padding-bottom: 2rem; - } .popup-content-inner, .tab-content-settings, .popup-tabs-child, diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 159a2e78..6ca13c36 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -76,12 +76,12 @@ "ImagePickerExplanationPhone": "press and hold an image to save it.", "ErrorNoUrlReturned": "i didn't get a download link from the server. this should never happen. try again, but if it still doesn't work, {ContactLink}.", "ErrorUnknownStatus": "i received a response i can't process. this should never happen. try again, but if it still doesn't work, {ContactLink}.", - "PasteFromClipboard": "paste and download", + "PasteFromClipboard": "paste", "ChangelogOlder": "previous versions", "ChangelogPressToExpand": "expand", "Miscellaneous": "miscellaneous", - "ModeToggleAuto": "auto mode", - "ModeToggleAudio": "audio mode", + "ModeToggleAuto": "auto", + "ModeToggleAudio": "audio", "SettingsDisableNotifications": "hide notifications", "MediaPickerTitle": "pick what to save", "MediaPickerExplanationPC": "click or right click to download what you want.", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index c81eb558..47a57814 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -76,12 +76,12 @@ "ImagePickerExplanationPhone": "зажми и удерживай картинку, чтобы её сохранить.", "ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.", "ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.", - "PasteFromClipboard": "вставить и скачать", + "PasteFromClipboard": "вставить", "ChangelogOlder": "предыдущие версии (тоже на английском)", "ChangelogPressToExpand": "раскрыть", "Miscellaneous": "разное", - "ModeToggleAuto": "авто режим", - "ModeToggleAudio": "аудио режим", + "ModeToggleAuto": "авто", + "ModeToggleAudio": "аудио", "SettingsDisableNotifications": "cкрыть уведомления", "MediaPickerTitle": "выбери, что сохранить", "MediaPickerExplanationPC": "кликни то, что хочешь скачать. также можно скачать правой кнопки мыши.", diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index f74bb097..96468816 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -10,6 +10,8 @@ export const dropdownSVG = ` ` +export const linkSVG = '' + export function switcher(obj) { let items = ``; if (obj.name === "download") { @@ -17,8 +19,6 @@ export function switcher(obj) { } else { for (let i = 0; i < obj.items.length; i++) { let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : []; - if (i === 0) classes.push("first"); - if (i === (obj.items.length - 1)) classes.push("last"); items += `` } } @@ -119,8 +119,6 @@ export function collapsibleList(arr) { for (let i = 0; i < arr.length; i++) { let classes = arr[i]["classes"] ? arr[i]["classes"] : []; - if (i === 0) classes.push("first"); - if (i === (arr.length - 1)) classes.push("last"); items += `
${arr[i]["title"]}
diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index a741dbc9..b5edcb64 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -1,4 +1,4 @@ -import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc, sponsoredList, betaTag } from "./elements.js"; +import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc, sponsoredList, betaTag, linkSVG } from "./elements.js"; import { services as s, authorInfo, version, repo, donations, supportedAudio, links } from "../config.js"; import { getCommitInfo } from "../sub/currentCommit.js"; import loc from "../../localization/manager.js"; @@ -571,7 +571,8 @@ export default function(obj) {
- + +
From cc47f9fd8af0731d12d883dc39a2d711b3eb88b0 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 26 Dec 2023 00:08:58 +0600 Subject: [PATCH 29/31] update: add 7.8 update message and emoji --- src/front/emoji/bubbles.svg | 30 ++++++++++++++++++++++++++++++ src/localization/languages/en.json | 3 ++- src/localization/languages/ru.json | 3 ++- src/modules/emoji.js | 3 ++- src/modules/pageRender/page.js | 4 ++-- 5 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 src/front/emoji/bubbles.svg diff --git a/src/front/emoji/bubbles.svg b/src/front/emoji/bubbles.svg new file mode 100644 index 00000000..e5bccc36 --- /dev/null +++ b/src/front/emoji/bubbles.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 6ca13c36..0435d218 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -158,6 +158,7 @@ "UrgentFilenameUpdate": "customizable file names!", "UrgentTwitterPatch": "fixes and easier downloads", "StatusPage": "service status page", - "TroubleshootingGuide": "self-troubleshooting guide" + "TroubleshootingGuide": "self-troubleshooting guide", + "UpdateNewYears": "new years clean up" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 47a57814..cdd5fbb7 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -160,6 +160,7 @@ "UrgentFilenameUpdate": "изменяемые названия файлов!", "UrgentTwitterPatch": "фиксы и удобное скачивание", "StatusPage": "статус серверов", - "TroubleshootingGuide": "гайд по устранению проблем" + "TroubleshootingGuide": "гайд по устранению проблем", + "UpdateNewYears": "новогодняя уборка" } } diff --git a/src/modules/emoji.js b/src/modules/emoji.js index 48806b87..f2bab1b9 100644 --- a/src/modules/emoji.js +++ b/src/modules/emoji.js @@ -41,7 +41,8 @@ const names = { "📧": "email", "📬": "mailbox", "📢": "loudspeaker", - "🔧": "wrench" + "🔧": "wrench", + "🫧": "bubbles" } let sizing = { 18: 0.8, diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index b5edcb64..f54e0738 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -562,8 +562,8 @@ export default function(obj) {