soundcloud and tooltip and some other stuff (3.3)

This commit is contained in:
wukko 2022-08-22 20:10:54 +06:00
parent 08cbc05018
commit 189ecf8fe7
17 changed files with 135 additions and 37 deletions

View file

@ -29,6 +29,13 @@ It preserves original media quality so you get best downloads possible (unless y
- Polish: [@hexandcube](https://github.com/hexandcube)
- Ukrainian: Löffel
## Languages that are always up to date
- English
- Russian
- Ukrainian
Other languages may be missing some strings or changes, you can help with updating those!
## How you can help cobalt speak your language
Take English or Russian localization from [this directory](https://github.com/wukko/cobalt/tree/current/src/localization/languages) and use it as a base for your translation. Then simply make a pull request and it'll be out for everyone upon review!

View file

@ -1,7 +1,7 @@
{
"name": "cobalt",
"description": "save what you love",
"version": "3.2",
"version": "3.3",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",

View file

@ -1,6 +1,7 @@
{
"streamLifespan": 3600000,
"maxVideoDuration": 1920000,
"maxAudioDuration": 4200000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"authorInfo": {
"name": "wukko",

View file

@ -453,6 +453,18 @@ input[type="checkbox"] {
.emoji {
margin-right: 0.4rem;
}
.tooltip {
position: absolute;
margin-top: -6rem;
margin-left: -0.5rem;
line-height: 1.2;
text-align: left;
pointer-events: none;
color: var(--accent-unhover-2)!important;
}
.button:active .tooltip {
display: none;
}
/* adapt the page according to screen size */
@media screen and (min-width: 2300px) {
html {

View file

@ -1,5 +1,5 @@
let isIOS = navigator.userAgent.toLowerCase().match("iphone os");
let version = 4
let version = 5;
let switchers = {
"theme": ["auto", "light", "dark"],
@ -7,7 +7,7 @@ let switchers = {
"quality": ["max", "hig", "mid", "low"],
"audioFormat": ["best", "mp3", "ogg", "wav", "opus"]
}
let exceptions = { // fuck you apple
let exceptions = { // used solely for ios devices, because they're less capable than everything else.
"ytFormat": "mp4",
"audioFormat": "mp3"
}
@ -197,6 +197,7 @@ function toggle(toggle) {
let state = sGet(toggle);
if (state) {
sSet(toggle, opposite(state))
if (opposite(state) == "true") sSet(`${toggle}ToggledOnce`, "true");
} else {
sSet(toggle, "false")
}
@ -208,7 +209,7 @@ function updateToggle(toggle, state) {
eid(toggle).innerHTML = loc.toggleAudio;
break;
case "false":
eid(toggle).innerHTML = loc.toggleDefault;
eid(toggle).innerHTML = sGet(`${toggle}ToggledOnce`) == "true" ? loc.toggleDefault : loc.pressToChange + loc.toggleDefault;
break;
}
}

View file

@ -5,10 +5,10 @@
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">let me know</a>"
},
"strings": {
"ChangelogContentTitle": "ukrainian localization and new error popup (3.2)",
"FollowTwitter": "follow cobalt's twitter account for polls, updates, and more: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"ChangelogContent": "- added ukrainian localization (thanks to löffel).\n- new error popup! it's now prettier, more compact, and has an easily accessible close button.\n- russian localization has been patched up a bit\n- cleaned up css a bit\n- added github contributors to made with love message.\n- emojis have been tuned to have the same shade of yellow.\n- updated translation guidelines in readme a bit.",
"ChangelogContentTitle": "soundcloud and better usability (3.3)",
"ChangelogContent": "- full support for soundcloud is here. you now can save your favorite songs from there, if you want to.\n- did you know that there's an audio download mode in cobalt? if you didn't, there's now a tooltip that shows you how to switch between modes.\n- added length limit to conversion of audios, because converting a 3 hour audio to wav will give you a 4gb file, and that's just unreasonable. you can still download audio in original (best) format without any limits.\n- if best and preferred audio format match, cobalt won't needlessly convert the audio anymore.\n- fixed format override for ios, you still might have to toggle between them once.\n- increased input area length limit on frontend because some reddit and soundcloud links wouldn't fit.\n- version in settings now opens current commit page on github, instead of general commits page. it also opens in a new tab instead of replacing the current one.\n- fixed some localization stuff in english, russian, and ukrainian. it's now easier to understand what mode is on, and general cobalt description in russian doesn't sound awkward anymore.",
"FollowTwitter": "follow cobalt's twitter account for polls, updates, and more: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"LinkInput": "paste the link here",
"AboutSummary": "{appName} is your go-to place for social media downloads. zero ads, trackers, or any other creepy bullshit attached. simply paste a share link and you're ready to rock!",
"AboutSupportedServices": "currently supported services:",
@ -57,7 +57,6 @@
"SettingsQualitySwitchMedium": "medium\n",
"SettingsQualitySwitchLow": "low\n",
"SettingsQualitySwitchLowest": "lowest",
"SettingsFormatSwitchAudio": "only audio",
"SettingsKeepDownloadButton": "keep &gt;&gt; visible",
"AccessibilityKeepDownloadButton": "keep the download button always visible",
"SettingsEnableDownloadPopup": "ask for a way to save",
@ -73,7 +72,7 @@
"DownloadPopupDescriptionIOS": "since you have an ios device, you have to press and hold the download button and then select \"download video\" in appeared popup to save the video. this will be required for as long as apple forces safari webview upon all browser developers on ios.",
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"DownloadPopupWayToSave": "pick a way to save",
"ClickToCopy": "click to copy",
"ClickToCopy": "press to copy",
"Download": "download",
"CopyURL": "copy url",
"AboutTab": "about",
@ -84,7 +83,6 @@
"SettingsOtherTab": "other",
"ChangelogLastCommit": "last commit",
"ChangelogLastMajor": "last major update",
"ModeToggleDefault": "smart mode",
"AccessibilityModeToggle": "toggle download mode",
"DonateLinksDescription": "donation links open in a new tab. this is the best way to donate money, if you want me to receive it directly.",
"SettingsAudioFormatBest": "best",
@ -92,6 +90,10 @@
"Keyphrase": "save what you love",
"SettingsDisableChangelogOnUpdate": "don't show changelog after major updates",
"SettingsRemoveWatermark": "disable watermark",
"ErrorPopupCloseButton": "got it"
"ErrorPopupCloseButton": "got it",
"ModeToggle": "mode",
"ModeToggleSmart": "smart",
"PressToChange": "press to change",
"ErrorLengthAudioConvert": "current length limit for audio conversion is {s} minutes. pick \"best\" format instead!"
}
}

View file

@ -6,10 +6,10 @@
},
"strings": {
"LinkInput": "вставь ссылку сюда",
"AboutSummary": "{appName} — твой друг при скачивании контента из соц. сетей. никакой рекламы или трекеров. вставляешь ссылку, получаешь файл, наслаждаешься жизнью.",
"AboutSummary": "{appName} — твой друг при скачивании контента из соц. сетей. никакой рекламы или трекеров. вставляешь ссылку и получаешь файл. ничего лишнего.",
"AboutSupportedServices": "что поддерживается:",
"EmbedBriefDescription": "сохраняй что хочешь, без мороки и вторжения в личное пространство",
"MadeWithLove": "сделано с <3 всеми контрибьюторами на гитхабе и wukko",
"MadeWithLove": "сделано с <3 контрибьюторами на гитхабе и wukko",
"AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с информацией",
"AccessibilityDownloadButton": "кнопка скачивания",
@ -53,7 +53,6 @@
"SettingsQualitySwitchMedium": "среднее\n",
"SettingsQualitySwitchLow": "низкое\n",
"SettingsQualitySwitchLowest": "худшее",
"SettingsFormatSwitchAudio": "только аудио",
"SettingsKeepDownloadButton": "оставлять &gt;&gt; на экране",
"AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране",
"SettingsEnableDownloadPopup": "спрашивать, как сохранять",
@ -80,7 +79,6 @@
"SettingsOtherTab": "другое",
"ChangelogLastCommit": "последний коммит (на английском)",
"ChangelogLastMajor": "последнее обновление (на английском)",
"ModeToggleDefault": "умный режим",
"AccessibilityModeToggle": "переключить режим скачивания",
"DonateLinksDescription": "ссылки на донаты открываются в новой вкладке. это лучший метод пожертвовать деньги, если ты хочешь, чтобы я получил их лично, а не в виде крипто.",
"SettingsAudioFormatBest": "лучший",
@ -88,6 +86,10 @@
"Keyphrase": "сохраняй то, что любишь",
"SettingsDisableChangelogOnUpdate": "не показывать изменения после обновлений",
"SettingsRemoveWatermark": "убрать ватермарку",
"ErrorPopupCloseButton": "ясно"
"ErrorPopupCloseButton": "ясно",
"ModeToggle": "режим",
"ModeToggleSmart": "умный",
"PressToChange": "нажми, чтобы изменить",
"ErrorLengthAudioConvert": "я не могу конвертировать аудио дольше чем {s} минут(ы). выбери \"лучший\" формат аудио, чтобы скачать аудио такой продолжительности."
}
}

View file

@ -9,7 +9,7 @@
"AboutSummary": "{appName} твій помічник із завантаження контенту з соцмереж. ніякої реклами, ніяких трекерів. вставляєш лінк, отримаєш файл, і допиваєш у спокої свій смузі.",
"AboutSupportedServices": "ось що підтримується:",
"EmbedBriefDescription": "зберігай контент із соцмереж без реклами і трекерів",
"MadeWithLove": "зроблено з <3 всіма контриб'юторами на github и wukko <3",
"MadeWithLove": "зроблено з <3 контриб'юторами на github и wukko <3",
"AccessibilityInputArea": "строка, щоб вставити в неї лінк",
"AccessibilityOpenAbout": "відкрити інфу про {appName}",
"AccessibilityDownloadButton": "кнопка завантаження",
@ -53,7 +53,6 @@
"SettingsQualitySwitchMedium": "середня\n",
"SettingsQualitySwitchLow": "низька\n",
"SettingsQualitySwitchLowest": "найнижча",
"SettingsFormatSwitchAudio": "тільки аудіо",
"SettingsKeepDownloadButton": "зробити &gt;&gt; видимим",
"AccessibilityKeepDownloadButton": "завжди показувати кнопку завантаження",
"SettingsEnableDownloadPopup": "питати щодо засобу зберігання",
@ -80,7 +79,6 @@
"SettingsOtherTab": "інше",
"ChangelogLastCommit": "останній коммит (англійською мовою)",
"ChangelogLastMajor": "останнє велике оновлення (англійською мовою)",
"ModeToggleDefault": "розумний режим",
"AccessibilityModeToggle": "змінити режим завантаження",
"DonateLinksDescription": "лінки на донати відкриваються у нових вкладках. це найкращій спосіб донатити, якщо хочеш, щоб ми безпосередньо отримували гроші.",
"SettingsAudioFormatBest": "найкращий",
@ -88,6 +86,10 @@
"Keyphrase": "збережи, що любиш",
"SettingsDisableChangelogOnUpdate": "не показувати \"що нового\" після великих оновлень",
"SettingsRemoveWatermark": "прибрати вотермарку",
"ErrorPopupCloseButton": "ясно"
"ErrorPopupCloseButton": "ясно",
"ModeToggle": "режим",
"ModeToggleSmart": "розумний",
"PressToChange": "натисни, щоб змінити",
"ErrorLengthAudioConvert": "я не можу конвертувати аудіо довше ніж {s} хвилин (и). вибери \"найкращий\" формат аудіо, щоб завантажити аудіо такої тривалості."
}
}

View file

@ -18,6 +18,10 @@ export async function getJSON(originalURL, ip, lang, format, quality, audioForma
host = "youtube";
url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`;
}
if (host == "goo" && url.substring(0, 30) == "https://soundcloud.app.goo.gl/") {
host = "soundcloud"
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
}
if (host == "tumblr" && !url.includes("blog/view")) {
if (url.slice(-1) == '/') url = url.slice(0, -1);
url = url.replace(url.split('/')[5], '');

View file

@ -8,6 +8,7 @@ export const
version = packageJson.version,
streamLifespan = config.streamLifespan,
maxVideoDuration = config.maxVideoDuration,
maxAudioDuration = config.maxAudioDuration,
genericUserAgent = config.genericUserAgent,
repo = packageJson["bugs"]["url"].replace('/issues', ''),
authorInfo = config.authorInfo,

View file

@ -13,6 +13,7 @@ import douyin from "./services/douyin.js";
import tumblr from "./services/tumblr.js";
import matchActionDecider from "./sub/matchActionDecider.js";
import vimeo from "./services/vimeo.js";
import soundcloud from "./services/soundcloud.js";
export default async function (host, patternMatch, url, ip, lang, format, quality, audioFormat, isAudioOnly, noWatermark) {
try {
@ -91,6 +92,15 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit
lang: lang
});
break;
case "soundcloud":
isAudioOnly = true;
r = await soundcloud({
author: patternMatch["author"], song: patternMatch["song"], url: url,
shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false,
format: audioFormat,
lang: lang
});
break;
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}

View file

@ -44,7 +44,7 @@ export function popup(obj) {
${obj.buttonOnly ? obj.emoji : ``}
<div id="popup-header" class="popup-header">
${obj.standalone && !obj.buttonOnly ? `<button id="popup-close" class="button mono" onclick="popup('${obj.name}', 0)" ${obj.header.closeAria ? `aria-label="${obj.header.closeAria}"` : ''}>x</button>` : ''}
${obj.header.aboveTitle ? `<a id="popup-above-title" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div>
@ -52,7 +52,7 @@ export function popup(obj) {
${body}${obj.buttonOnly ? `<button id="close-error" class="switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
</div>
${obj.footer ? `<div id="popup-footer" class="popup-footer">
<a id="popup-bottom" class="popup-footer-content" href="${obj.footer.url}">${obj.footer.text}</a>
<a id="popup-bottom" class="popup-footer-content" target="_blank" href="${obj.footer.url}">${obj.footer.text}</a>
</div>` : ''}
${obj.standalone ? `</div>` : ''}`
}
@ -69,7 +69,7 @@ export function multiPagePopup(obj) {
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
${obj.header.aboveTitle ? `<a id="popup-above-title" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}</div>` : ''}${tabContent}</div>
<div id="popup-tabs" class="switches popup-tabs">${tabs}</div>
@ -90,8 +90,8 @@ export function settingsCategory(obj) {
export function footerButtons(obj) {
let items = ``
for (let i = 0; i < obj.length; i++) {
let func = `${obj[i]["type"] == "toggle" ? `toggle('${obj[i]["name"]}')` : `popup('${obj[i]["name"]}', 1)`}`
items += `<button id="${obj[i]["name"]}" class="button footer-button" onclick="${func}" aria-label="${obj[i]["aria"]}">${obj[i]["icon"]}</button> `
let func = `${obj[i]["type"] == "toggle" ? `toggle('${obj[i]["name"]}')` : `popup('${obj[i]["name"]}', 1)`}`;
items += `<button id="${obj[i]["name"]}" class="button footer-button" onclick="${func}" aria-label="${obj[i]["aria"]}">${obj[i]["icon"]}</button>`;
}
return `
<div id="footer-buttons">${items}</div>`

View file

@ -153,7 +153,7 @@ export default function(obj) {
header: {
aboveTitle: {
text: `v.${version} ~ ${obj.hash}`,
url: repo
url: `${repo}/commit/${obj.hash}`
},
title: `${emoji("⚙️", 30)} ${loc(obj.lang, 'TitlePopupSettings')}`
},
@ -268,7 +268,7 @@ export default function(obj) {
<div id="cobalt-main-box" class="center box" style="visibility: hidden;">
<div id="logo-area">${appName}</div>
<div id="download-area" class="mobile-center">
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="110" autocapitalize="off" placeholder="${loc(obj.lang, 'LinkInput')}" aria-label="${loc(obj.lang, 'AccessibilityInputArea')}" oninput="button()">
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${loc(obj.lang, 'LinkInput')}" aria-label="${loc(obj.lang, 'AccessibilityInputArea')}" oninput="button()">
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${loc(obj.lang, 'AccessibilityDownloadButton')}">
</div>
</div>
@ -292,7 +292,13 @@ export default function(obj) {
)}
</footer>
</body>
<script type="text/javascript">const loc = {noInternet:"${loc(obj.lang, 'ErrorNoInternet')}", noURLReturned: "${loc(obj.lang, 'ErrorBadFetch')}", toggleDefault:'${emoji("✨")} ${loc(obj.lang, "ModeToggleDefault")}', toggleAudio:'${emoji("🎶")} ${loc(obj.lang, "SettingsFormatSwitchAudio")}'};</script>
<script type="text/javascript">const loc = {
noInternet: "${loc(obj.lang, 'ErrorNoInternet')}",
noURLReturned: "${loc(obj.lang, 'ErrorBadFetch')}",
toggleDefault: '${emoji("✨")} ${loc(obj.lang, "ModeToggleSmart")} ${loc(obj.lang, "ModeToggle")}',
toggleAudio: '${emoji("🎶")} ${loc(obj.lang, "SettingsAudioTab")} ${loc(obj.lang, "ModeToggle")}',
pressToChange: '<div class="tooltip">▼ ${loc(obj.lang, 'PressToChange')}</div>'
};</script>
<script type="text/javascript" src="cobalt.js"></script>
</html>`;
} catch (err) {

View file

@ -0,0 +1,42 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent, maxAudioDuration, services } from "../config.js";
export default async function(obj) {
try {
let html;
if (!obj.author && !obj.song && obj.shortLink) {
html = await got.get(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'soundcloud') };
});
html = html.body
}
if (obj.author && obj.song) {
html = await got.get(`https://soundcloud.com/${obj.author}/${obj.song}`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'soundcloud') };
});
html = html.body
}
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) {
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (json["media"]["transcodings"]) {
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${services["soundcloud"]["clientid"]}&track_authorization=${json.track_authorization}`;
if (fileUrl.substring(0, 54) == "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
if ((json.duration < maxAudioDuration) || obj.format == "best" || obj.format == "mp3") {
let file = await got.get(fileUrl, { headers: { "user-agent": genericUserAgent } });
file.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'soundcloud') };
});
file = JSON.parse(file.body).url
return { urls: file, audioFilename: `soundcloud_${json.id}` }
} else return { error: loc(obj.lang, 'ErrorLengthAudioConvert', maxAudioDuration / 60000) }
}
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'soundcloud') }
} catch (e) {
console.log(e)
return { error: loc(obj.lang, 'ErrorBadFetch') };
}
}

View file

@ -43,6 +43,7 @@
"alias": "youtube, youtube music",
"patterns": ["watch?v=:id"],
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
"bestAudio": "opus",
"quality": {
"1080": "hig",
"720": "mid",
@ -65,5 +66,11 @@
"vimeo": {
"patterns": [":id"],
"enabled": true
},
"soundcloud": {
"patterns": [":author/:song", ":shortLink"],
"bestAudio": "mp3",
"clientid": "lnFbWHXluNwOkW7TxTYUXrrse0qj1C72",
"enabled": true
}
}

View file

@ -21,4 +21,7 @@ export let testers = {
(patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)),
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
"soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"] && (patternMatch["author"].length + patternMatch["song"].length) <= 96) ||
(patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32))
};

View file

@ -1,4 +1,4 @@
import { supportedAudio } from "../config.js"
import { services, supportedAudio } from "../config.js"
import { apiJSON } from "./utils.js"
export default function(r, host, ip, audioFormat, isAudioOnly) {
@ -48,14 +48,12 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
let type = "render"
let copy = false
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if (audioFormat == "best") {
if (host != "youtube") {
audioFormat = "m4a"
copy = true
} else {
audioFormat = "opus"
type = "bridge"
}
if ((audioFormat == "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat == services[host]["bestAudio"])) {
audioFormat = services[host]["bestAudio"]
type = "bridge"
} else if (audioFormat == "best") {
audioFormat = "m4a"
copy = true
}
if (host == "reddit" && r.typeId == 1 || host == "vk" || host == "vimeo") return apiJSON(0, { t: r.audioFilename });
return apiJSON(2, {