@@ -1,15 +1,20 @@
|
||||
{
|
||||
"current": {
|
||||
"version": "4.7",
|
||||
"title": "we're better together! thank you for bug reports.",
|
||||
"banner": "bettertogether.webp",
|
||||
"content": "this update includes a bunch of improvements, many of which were made thanks to the community :D\n\nservice-related improvements:\n*; private soundcloud links are now supported (#68);\n*; tiktok usernames with dots in them no longer confuse cobalt (#71);\n*; .ogg files no longer wrongfully include a video channel (#67);\n*; fixed an issue that caused cobalt to freak out when user attempted to download an audio from audio-only service with \"mute video\" option enabled.\n\nui improvements:\n*; all buttons are now of even size and are displayed without any padding issues across all modern browsers and devices;\n*; checkbox is no longer crippled on ios;\n*; many explanation texts have been simplified to get rid of unnecessary bloat (no bullshit, remember?);\n*; moved tiktok section in video settings higher due to higher priority.\n\nstability improvements:\n*; fixed a memory leak that was caused by misconfigured stream information caching (#63).\n\ninternal improvements:\n*; requested streams are now stored in cache for 2 minutes instead of 1000 hours (yes, 1000 hours, i fucked up);\n*; cached data is now reused if user requests same content within 2 minutes;\n*; page render module is now even cleaner than before;\n*; proper support for bullet-points in loc strings.\n\nyou can suggest features or report bugs either on <a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">twitter</a>.\nboth work just fine, use whichever you're more comfortable with.\n\nthank you for using cobalt, and thank you for reading this changelog.\n\nyou're amazing, keep it up :)"
|
||||
},
|
||||
"history": [{
|
||||
"version": "4.6",
|
||||
"title": "mute videos and proper soundcloud support",
|
||||
"banner": "shutup.png",
|
||||
"content": "i've been longing to implement both of these things, and here they finally are.\n\nservice-related improvements:\n<div class=\"bullpadding\">• you now can download videos with no audio! simply enable the \"mute audio\" option in settings > audio.\n• soundcloud module has been updated, and downloads should no longer break after some time.</div>\nvisual improvements:\n<div class=\"bullpadding\">• moved some things around in settings popup, and added separators where separation is needed.\n• updated some texts in english and russian.\n• version and commit hash have been joined together, now they're a single unit.</div>\ninternal improvements:\n<div class=\"bullpadding\">• updated api documentation to include isAudioMuted.\n• simplified the startup message.\n• created render elements for separator and explanation due to high duplication of them in the page.\n• fully deprecated GET method for API requests.\n• fixed some code quirks.</div>\nhere's how soundcloud downloads got fixed:\n\npreviously, client_id was (stupidly) hardcoded. that means cobalt wasn't able to fetch song data if soundcloud web app got updated.\nnow, cobalt tries to find the up-to-date client_id, caches it in memory, and checks if web app version has changed to update the id accordingly. you can see this change for yourself on github."
|
||||
},
|
||||
"history": [{
|
||||
"content": "i've been longing to implement both of these things, and here they finally are.\n\nservice-related improvements:\n{bS}*; you now can download videos with no audio! simply enable the \"mute audio\" option in settings > audio.\n*; soundcloud module has been updated, and downloads should no longer break after some time.{bE}\nvisual improvements:\n{bS}*; moved some things around in settings popup, and added separators where separation is needed.\n*; updated some texts in english and russian.\n*; version and commit hash have been joined together, now they're a single unit.{bE}\ninternal improvements:\n{bS}*; updated api documentation to include isAudioMuted.\n*; simplified the startup message.\n*; created render elements for separator and explanation due to high duplication of them in the page.\n*; fully deprecated GET method for API requests.\n*; fixed some code quirks.{bE}\nhere's how soundcloud downloads got fixed:\n\npreviously, client_id was (stupidly) hardcoded. that means cobalt wasn't able to fetch song data if soundcloud web app got updated.\nnow, cobalt tries to find the up-to-date client_id, caches it in memory, and checks if web app version has changed to update the id accordingly. you can see this change for yourself on github."
|
||||
}, {
|
||||
"version": "4.5",
|
||||
"title": "better, faster, stronger, stable",
|
||||
"banner": "meowthstrong.webp",
|
||||
"content": "your favorite social media downloader just got even better! this update includes a ton of imporvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n<div class=\"bullpadding\">• vimeo module has been revamped, all sorts of videos should now be supported.\n• vimeo audio downloads! you now can download audios from more recent videos.\n• {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n• vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n• youtube videos with community warnings should now be possible to download.</div>\nuser interface improvements:\n<div class=\"bullpadding\">• list of supported services is now MUCH easier to read.\n• banners in changelog history should no longer overlap each other.\n• bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.</div>\ninternal improvements:\n<div class=\"bullpadding\">• cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n• better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n• moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n• ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n• \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n• all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n• other code optimizations. there's less clutter overall.</div>\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
|
||||
"content": "your favorite social media downloader just got even better! this update includes a ton of imporvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n{bS}*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.{bE}\nuser interface improvements:\n{bS}*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.{bE}\ninternal improvements:\n{bS}*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.{bE}\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
|
||||
}, {
|
||||
"version": "4.4",
|
||||
"title": "over 1 million monthly requests. thank you.",
|
||||
|
||||
@@ -27,11 +27,14 @@ for (let i in donations["crypto"]) {
|
||||
donate += `<div class="subtitle${extr}">${i} (REPLACEME)</div><div id="don-${i}" class="text-to-copy" onClick="copy('don-${i}')">${donations["crypto"][i]}</div>`
|
||||
extr = ' top-margin'
|
||||
}
|
||||
|
||||
export default function(obj) {
|
||||
audioFormats[0]["text"] = loc(obj.lang, 'SettingsAudioFormatBest');
|
||||
const t = (str, replace) => { return loc(obj.lang, str, replace) };
|
||||
let ua = obj.useragent.toLowerCase();
|
||||
let isIOS = ua.match("iphone os");
|
||||
let isMobile = ua.match("android") || ua.match("iphone os");
|
||||
audioFormats[0]["text"] = t('SettingsAudioFormatBest');
|
||||
|
||||
try {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -43,10 +46,10 @@ export default function(obj) {
|
||||
|
||||
<meta property="og:url" content="${process.env.selfURL}" />
|
||||
<meta property="og:title" content="${appName}" />
|
||||
<meta property="og:description" content="${loc(obj.lang, 'EmbedBriefDescription')}" />
|
||||
<meta property="og:description" content="${t('EmbedBriefDescription')}" />
|
||||
<meta property="og:image" content="${process.env.selfURL}icons/generic.png" />
|
||||
<meta name="title" content="${appName}" />
|
||||
<meta name="description" content="${loc(obj.lang, 'AboutSummary')}" />
|
||||
<meta name="description" content="${t('AboutSummary')}" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
|
||||
@@ -59,51 +62,51 @@ export default function(obj) {
|
||||
<link rel="stylesheet" href="cobalt.css" />
|
||||
<link rel="stylesheet" href="fonts/notosansmono.css" />
|
||||
|
||||
<noscript><div style="margin: 2rem;">${loc(obj.lang, 'NoScriptMessage')}</div></noscript>
|
||||
<noscript><div style="margin: 2rem;">${t('NoScriptMessage')}</div></noscript>
|
||||
</head>
|
||||
<body id="cobalt-body" data-nosnippet>
|
||||
${multiPagePopup({
|
||||
name: "about",
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
tabs: [{
|
||||
name: "about",
|
||||
title: `${emoji("🐲")} ${loc(obj.lang, 'AboutTab')}`,
|
||||
title: `${emoji("🐲")} ${t('AboutTab')}`,
|
||||
content: popup({
|
||||
name: "about",
|
||||
header: {
|
||||
aboveTitle: {
|
||||
text: loc(obj.lang, 'MadeWithLove'),
|
||||
text: t('MadeWithLove'),
|
||||
url: authorInfo.link
|
||||
},
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
title: loc(obj.lang, 'TitlePopupAbout')
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
title: t('TitlePopupAbout')
|
||||
},
|
||||
body: [{
|
||||
text: loc(obj.lang, 'AboutSummary')
|
||||
text: t('AboutSummary')
|
||||
}, {
|
||||
text: `${loc(obj.lang, 'AboutSupportedServices')}`,
|
||||
text: `${t('AboutSupportedServices')}`,
|
||||
nopadding: true
|
||||
}, {
|
||||
text: `<div class="bullpadding">${enabledServices}.</div>`
|
||||
}, {
|
||||
text: obj.lang !== "ru" ? loc(obj.lang, 'FollowTwitter') : "",
|
||||
text: obj.lang !== "ru" ? t('FollowTwitter') : "",
|
||||
classes: ["desc-padding"]
|
||||
}, {
|
||||
text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')),
|
||||
text: backdropLink(repo, t('LinkGitHubIssues')),
|
||||
classes: ["bottom-link"]
|
||||
}]
|
||||
})
|
||||
}, {
|
||||
name: "changelog",
|
||||
title: `${emoji("🎉")} ${loc(obj.lang, 'ChangelogTab')}`,
|
||||
title: `${emoji("🎉")} ${t('ChangelogTab')}`,
|
||||
content: popup({
|
||||
name: "changelog",
|
||||
header: {
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
title: `${emoji("🪄", 30)} ${loc(obj.lang, 'TitlePopupChangelog')}`
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
title: `${emoji("🪄", 30)} ${t('TitlePopupChangelog')}`
|
||||
},
|
||||
body: [{
|
||||
text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastMajor')}</div>`,
|
||||
text: `<div class="category-title">${t('ChangelogLastMajor')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: changelogManager("banner") ? `<div class="changelog-banner"><img class="changelog-img" src="${changelogManager("banner")}" onerror="this.style.display='none'"></img></div>`: '',
|
||||
@@ -121,48 +124,50 @@ export default function(obj) {
|
||||
}, {
|
||||
text: com[1]
|
||||
}, {
|
||||
text: backdropLink(`${repo}/commits`, loc(obj.lang, 'LinkGitHubChanges')),
|
||||
text: backdropLink(`${repo}/commits`, t('LinkGitHubChanges')),
|
||||
classes: ["bottom-link"]
|
||||
}, {
|
||||
text: `<div class="category-title">${loc(obj.lang, 'ChangelogOlder')}</div>`,
|
||||
text: `<div class="category-title">${t('ChangelogOlder')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: `<div id="changelog-history"><button class="switch bottom-margin" onclick="loadOnDemand('changelog-history', '0')">${loc(obj.lang, "ChangelogPressToExpand")}</button></div>`,
|
||||
text: `<div id="changelog-history"><button class="switch bottom-margin" onclick="loadOnDemand('changelog-history', '0')">${t("ChangelogPressToExpand")}</button></div>`,
|
||||
raw: true
|
||||
}]
|
||||
})
|
||||
}, {
|
||||
name: "donate",
|
||||
title: `${emoji("💰")} ${loc(obj.lang, 'DonationsTab')}`,
|
||||
title: `${emoji("💰")} ${t('DonationsTab')}`,
|
||||
content: popup({
|
||||
name: "donate",
|
||||
header: {
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
title: emoji("💸", 30) + loc(obj.lang, 'TitlePopupDonate'),
|
||||
subtitle: loc(obj.lang, 'DonateSub')
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
title: emoji("💸", 30) + t('TitlePopupDonate')
|
||||
},
|
||||
body: [{
|
||||
text: `<div class="category-title">${t('DonateSub')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: `<div class="changelog-banner"><img class="changelog-img" src="updateBanners/catsleep.webp" onerror="this.style.display='none'"></img></div>`,
|
||||
raw: true
|
||||
},{
|
||||
text: loc(obj.lang, 'DonateExplanation')
|
||||
}, {
|
||||
text: donateLinks.replace(/REPLACEME/g, loc(obj.lang, 'DonateVia')),
|
||||
text: t('DonateExplanation')
|
||||
}, {
|
||||
text: donateLinks.replace(/REPLACEME/g, t('DonateVia')),
|
||||
raw: true
|
||||
}, {
|
||||
text: loc(obj.lang, 'DonateLinksDescription'),
|
||||
text: t('DonateLinksDescription'),
|
||||
classes: ["explanation"]
|
||||
}, {
|
||||
text: sep(),
|
||||
raw: true
|
||||
}, {
|
||||
text: donate.replace(/REPLACEME/g, loc(obj.lang, 'ClickToCopy')),
|
||||
text: donate.replace(/REPLACEME/g, t('ClickToCopy')),
|
||||
classes: ["desc-padding"]
|
||||
}, {
|
||||
text: sep(),
|
||||
raw: true
|
||||
}, {
|
||||
text: loc(obj.lang, 'DonateHireMe', authorInfo.link),
|
||||
text: t('DonateHireMe', authorInfo.link),
|
||||
classes: ["desc-padding"]
|
||||
}]
|
||||
})
|
||||
@@ -170,99 +175,100 @@ export default function(obj) {
|
||||
})}
|
||||
${multiPagePopup({
|
||||
name: "settings",
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
header: {
|
||||
aboveTitle: {
|
||||
text: `v.${version}-${obj.hash}`,
|
||||
url: `${repo}/commit/${obj.hash}`
|
||||
},
|
||||
title: `${emoji("⚙️", 30)} ${loc(obj.lang, 'TitlePopupSettings')}`
|
||||
title: `${emoji("⚙️", 30)} ${t('TitlePopupSettings')}`
|
||||
},
|
||||
tabs: [{
|
||||
name: "video",
|
||||
title: `${emoji("🎬")} ${loc(obj.lang, 'SettingsVideoTab')}`,
|
||||
title: `${emoji("🎬")} ${t('SettingsVideoTab')}`,
|
||||
content: settingsCategory({
|
||||
name: "downloads",
|
||||
title: loc(obj.lang, 'SettingsVideoGeneral'),
|
||||
title: t('SettingsVideoGeneral'),
|
||||
body: switcher({
|
||||
name: "vQuality",
|
||||
subtitle: loc(obj.lang, 'SettingsQualitySubtitle'),
|
||||
explanation: loc(obj.lang, 'SettingsQualityDescription'),
|
||||
subtitle: t('SettingsQualitySubtitle'),
|
||||
explanation: t('SettingsQualityDescription'),
|
||||
items: [{
|
||||
"action": "max",
|
||||
"text": `${loc(obj.lang, 'SettingsQualitySwitchMax')}<br/>(2160p+)`
|
||||
"text": `${t('SettingsQualitySwitchMax')}<br/>(2160p+)`
|
||||
}, {
|
||||
"action": "hig",
|
||||
"text": `${loc(obj.lang, 'SettingsQualitySwitchHigh')}<br/>(${quality.hig}p)`
|
||||
"text": `${t('SettingsQualitySwitchHigh')}<br/>(${quality.hig}p)`
|
||||
}, {
|
||||
"action": "mid",
|
||||
"text": `${loc(obj.lang, 'SettingsQualitySwitchMedium')}<br/>(${quality.mid}p)`
|
||||
"text": `${t('SettingsQualitySwitchMedium')}<br/>(${quality.mid}p)`
|
||||
}, {
|
||||
"action": "low",
|
||||
"text": `${loc(obj.lang, 'SettingsQualitySwitchLow')}<br/>(${quality.low}p)`
|
||||
"text": `${t('SettingsQualitySwitchLow')}<br/>(${quality.low}p)`
|
||||
}]
|
||||
})
|
||||
}) + settingsCategory({
|
||||
name: "youtube",
|
||||
body: switcher({
|
||||
name: "vFormat",
|
||||
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
|
||||
explanation: loc(obj.lang, 'SettingsFormatDescription'),
|
||||
items: [{
|
||||
"action": "mp4",
|
||||
"text": "mp4 (av1)"
|
||||
}, {
|
||||
"action": "webm",
|
||||
"text": "webm (vp9)"
|
||||
}]
|
||||
})
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
body: checkbox("disableTikTokWatermark", loc(obj.lang, 'SettingsRemoveWatermark'))
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'))
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "youtube",
|
||||
body: switcher({
|
||||
name: "vFormat",
|
||||
subtitle: t('SettingsFormatSubtitle'),
|
||||
explanation: t('SettingsFormatDescription'),
|
||||
items: [{
|
||||
"action": "mp4",
|
||||
"text": "mp4 (av1)"
|
||||
}, {
|
||||
"action": "webm",
|
||||
"text": "webm (vp9)"
|
||||
}]
|
||||
})
|
||||
})
|
||||
}, {
|
||||
name: "audio",
|
||||
title: `${emoji("🎶")} ${loc(obj.lang, 'SettingsAudioTab')}`,
|
||||
title: `${emoji("🎶")} ${t('SettingsAudioTab')}`,
|
||||
content: settingsCategory({
|
||||
name: "general",
|
||||
title: loc(obj.lang, 'SettingsAudioTab'),
|
||||
title: t('SettingsAudioTab'),
|
||||
body: switcher({
|
||||
name: "aFormat",
|
||||
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
|
||||
explanation: loc(obj.lang, 'SettingsAudioFormatDescription'),
|
||||
subtitle: t('SettingsFormatSubtitle'),
|
||||
explanation: t('SettingsAudioFormatDescription'),
|
||||
items: audioFormats
|
||||
}) + sep(0) + checkbox("muteAudio", loc(obj.lang, 'SettingsVideoMute'), loc(obj.lang, 'SettingsVideoMute'), 3) + explanation(loc(obj.lang, 'SettingsVideoMuteExplanation'))
|
||||
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation'))
|
||||
}) + settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
body: checkbox("fullTikTokAudio", loc(obj.lang, 'SettingsAudioFullTikTok'), loc(obj.lang, 'SettingsAudioFullTikTok'), 3) + `<div class="explanation">${loc(obj.lang, 'SettingsAudioFullTikTokDescription')}</div>`
|
||||
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), t('SettingsAudioFullTikTok'), 3) + `<div class="explanation">${t('SettingsAudioFullTikTokDescription')}</div>`
|
||||
})
|
||||
}, {
|
||||
name: "other",
|
||||
title: `${emoji("🪅")} ${loc(obj.lang, 'SettingsOtherTab')}`,
|
||||
title: `${emoji("🪅")} ${t('SettingsOtherTab')}`,
|
||||
content: settingsCategory({
|
||||
name: "appearance",
|
||||
title: loc(obj.lang, 'SettingsAppearanceSubtitle'),
|
||||
title: t('SettingsAppearanceSubtitle'),
|
||||
body: switcher({
|
||||
name: "theme",
|
||||
subtitle: loc(obj.lang, 'SettingsThemeSubtitle'),
|
||||
subtitle: t('SettingsThemeSubtitle'),
|
||||
items: [{
|
||||
"action": "auto",
|
||||
"text": loc(obj.lang, 'SettingsThemeAuto')
|
||||
"text": t('SettingsThemeAuto')
|
||||
}, {
|
||||
"action": "dark",
|
||||
"text": loc(obj.lang, 'SettingsThemeDark')
|
||||
"text": t('SettingsThemeDark')
|
||||
}, {
|
||||
"action": "light",
|
||||
"text": loc(obj.lang, 'SettingsThemeLight')
|
||||
"text": t('SettingsThemeLight')
|
||||
}]
|
||||
}) + checkbox("alwaysVisibleButton", loc(obj.lang, 'SettingsKeepDownloadButton'), loc(obj.lang, 'AccessibilityKeepDownloadButton'), 2)
|
||||
}) + checkbox("alwaysVisibleButton", t('SettingsKeepDownloadButton'), t('AccessibilityKeepDownloadButton'), 2)
|
||||
}) + settingsCategory({
|
||||
name: "miscellaneous",
|
||||
title: loc(obj.lang, 'Miscellaneous'),
|
||||
body: checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup'), 1) : ''}`
|
||||
title: t('Miscellaneous'),
|
||||
body: checkbox("disableChangelog", t('SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", t('SettingsEnableDownloadPopup'), t('AccessibilityEnableDownloadPopup'), 1) : ''}`
|
||||
})
|
||||
}],
|
||||
})}
|
||||
@@ -270,25 +276,25 @@ export default function(obj) {
|
||||
name: "download",
|
||||
standalone: true,
|
||||
header: {
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
subtitle: loc(obj.lang, 'TitlePopupDownload')
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
subtitle: t('TitlePopupDownload')
|
||||
},
|
||||
body: switcher({
|
||||
name: "download",
|
||||
subtitle: loc(obj.lang, 'DownloadPopupWayToSave'),
|
||||
explanation: `${!isIOS ? loc(obj.lang, 'DownloadPopupDescription') : loc(obj.lang, 'DownloadPopupDescriptionIOS')}`,
|
||||
items: `<a id="pd-download" class="switch full space-right" target="_blank" href="/">${loc(obj.lang, 'Download')}</a>
|
||||
<div id="pd-copy" class="switch full">${loc(obj.lang, 'CopyURL')}</div>`
|
||||
subtitle: t('DownloadPopupWayToSave'),
|
||||
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
|
||||
items: `<a id="pd-download" class="switch full space-right" target="_blank" href="/">${t('Download')}</a>
|
||||
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
|
||||
})
|
||||
})}
|
||||
${popupWithBottomButtons({
|
||||
name: "picker",
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
header: {
|
||||
title: `<div id="picker-title"></div>`,
|
||||
explanation: `<div id="picker-subtitle"></div>`,
|
||||
},
|
||||
buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${loc(obj.lang, 'ImagePickerDownloadAudio')}</a>`],
|
||||
buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${t('ImagePickerDownloadAudio')}</a>`],
|
||||
content: '<div id="picker-holder"></div>'
|
||||
})}
|
||||
${popup({
|
||||
@@ -297,10 +303,10 @@ export default function(obj) {
|
||||
buttonOnly: true,
|
||||
emoji: emoji("☹️", 48, 1),
|
||||
classes: ["small"],
|
||||
buttonText: loc(obj.lang, 'ErrorPopupCloseButton'),
|
||||
buttonText: t('ErrorPopupCloseButton'),
|
||||
header: {
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
title: loc(obj.lang, 'TitlePopupError')
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
title: t('TitlePopupError')
|
||||
},
|
||||
body: `<div id="desc-error" class="desc-padding subtext"></div>`
|
||||
})}
|
||||
@@ -309,13 +315,13 @@ export default function(obj) {
|
||||
<div id="logo-area">${appName}</div>
|
||||
<div id="download-area" class="mobile-center">
|
||||
<div id="top">
|
||||
<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>
|
||||
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input>
|
||||
<button id="url-clear" onclick="clearInput()" style="display:none;">x</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')}">
|
||||
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}">
|
||||
</div>
|
||||
<div id="bottom">
|
||||
<button id="pasteFromClipboard" class="switch" onclick="pasteClipboard()" aria-label="${loc(obj.lang, 'PasteFromClipboard')}">${emoji("📋", 22)} ${loc(obj.lang, 'PasteFromClipboard')}</button>
|
||||
<button id="audioMode" class="switch" onclick="toggle('audioMode')" aria-label="${loc(obj.lang, 'AccessibilityModeToggle')}">${emoji("✨", 22, 1)}</button>
|
||||
<button id="pasteFromClipboard" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
|
||||
<button id="audioMode" class="switch" onclick="toggle('audioMode')" aria-label="${t('AccessibilityModeToggle')}">${emoji("✨", 22, 1)}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,38 +330,37 @@ export default function(obj) {
|
||||
footerButtons([{
|
||||
name: "about",
|
||||
type: "popup",
|
||||
text: `${emoji(celebrationsEmoji() , 22)} ${loc(obj.lang, 'AboutTab')}`,
|
||||
aria: loc(obj.lang, 'AccessibilityOpenAbout')
|
||||
text: `${emoji(celebrationsEmoji() , 22)} ${t('AboutTab')}`,
|
||||
aria: t('AccessibilityOpenAbout')
|
||||
}, {
|
||||
name: "about",
|
||||
type: "popup",
|
||||
context: "donate",
|
||||
text: `${emoji("💰", 22)} ${loc(obj.lang, 'Donate')}`,
|
||||
aria: loc(obj.lang, 'AccessibilityOpenDonate')
|
||||
text: `${emoji("💰", 22)} ${t('Donate')}`,
|
||||
aria: t('AccessibilityOpenDonate')
|
||||
}, {
|
||||
name: "settings",
|
||||
type: "popup",
|
||||
text: `${emoji("⚙️", 22)} ${loc(obj.lang, 'TitlePopupSettings')}`,
|
||||
aria: loc(obj.lang, 'AccessibilityOpenSettings')
|
||||
}]
|
||||
)}
|
||||
text: `${emoji("⚙️", 22)} ${t('TitlePopupSettings')}`,
|
||||
aria: t('AccessibilityOpenSettings')
|
||||
}])}
|
||||
</footer>
|
||||
</body>
|
||||
<script type="text/javascript">const loc = {
|
||||
noInternet: ` + "`" + loc(obj.lang, 'ErrorNoInternet') + "`" + `,
|
||||
noURLReturned: ` + "`" + loc(obj.lang, 'ErrorNoUrlReturned') + "`" + `,
|
||||
unknownStatus: ` + "`" + loc(obj.lang, 'ErrorUnknownStatus') + "`" + `,
|
||||
collapseHistory: ` + "`" + loc(obj.lang, 'ChangelogPressToHide') + "`" + `,
|
||||
toggleDefault: '${emoji("✨")} ${loc(obj.lang, "ModeToggleAuto")}',
|
||||
toggleAudio: '${emoji("🎶")} ${loc(obj.lang, "ModeToggleAudio")}',
|
||||
pickerDefault: ` + "`" + loc(obj.lang, 'MediaPickerTitle') + "`" + `,
|
||||
pickerImages: ` + "`" + loc(obj.lang, 'ImagePickerTitle') + "`" + `,
|
||||
pickerImagesExpl: ` + "`" + loc(obj.lang, `ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
|
||||
pickerDefaultExpl: ` + "`" + loc(obj.lang, `MediaPickerExplanation${isMobile ? `Phone${isIOS ? "IOS" : ""}` : "PC"}`) + "`" + `,
|
||||
noInternet: ` + "`" + t('ErrorNoInternet') + "`" + `,
|
||||
noURLReturned: ` + "`" + t('ErrorNoUrlReturned') + "`" + `,
|
||||
unknownStatus: ` + "`" + t('ErrorUnknownStatus') + "`" + `,
|
||||
collapseHistory: ` + "`" + t('ChangelogPressToHide') + "`" + `,
|
||||
toggleDefault: '${emoji("✨")} ${t("ModeToggleAuto")}',
|
||||
toggleAudio: '${emoji("🎶")} ${t("ModeToggleAudio")}',
|
||||
pickerDefault: ` + "`" + t('MediaPickerTitle') + "`" + `,
|
||||
pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `,
|
||||
pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
|
||||
pickerDefaultExpl: ` + "`" + t(`MediaPickerExplanation${isMobile ? `Phone${isIOS ? "IOS" : ""}` : "PC"}`) + "`" + `,
|
||||
};</script>
|
||||
<script type="text/javascript" src="cobalt.js"></script>
|
||||
</html>`;
|
||||
} catch (err) {
|
||||
return `${loc(obj.lang, 'ErrorPageRenderFail', obj.hash)}`;
|
||||
return `${t('ErrorPageRenderFail', obj.hash)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
noWatermark: obj.isNoTTWatermark, fullAudio: obj.isTTFullAudio,
|
||||
isAudioOnly: obj.isAudioOnly
|
||||
});
|
||||
if (r.isAudioOnly) obj.isAudioOnly = true
|
||||
if (r.isAudioOnly) obj.isAudioOnly = true;
|
||||
break;
|
||||
case "tumblr":
|
||||
r = await tumblr({
|
||||
@@ -100,6 +100,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
r = await soundcloud({
|
||||
author: patternMatch["author"], song: patternMatch["song"], url: url,
|
||||
shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false,
|
||||
accessKey: patternMatch["accessKey"] ? patternMatch["accessKey"] : false,
|
||||
format: obj.aFormat,
|
||||
lang: lang
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
return apiJSON(1, { u: r.urls });
|
||||
}
|
||||
}
|
||||
} else if (isAudioMuted) {
|
||||
} else if (isAudioMuted && !isAudioOnly) {
|
||||
let isSplit = Array.isArray(r.urls);
|
||||
return apiJSON(2, {
|
||||
type: isSplit ? "bridge" : "mute",
|
||||
@@ -81,13 +81,13 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
picker: r.picker, service: host
|
||||
})
|
||||
}
|
||||
} else {
|
||||
} else if (isAudioOnly) {
|
||||
if ((host === "reddit" && r.typeId === 1) || (host === "vimeo" && !r.filename) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
|
||||
let type = "render";
|
||||
let copy = false;
|
||||
|
||||
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
|
||||
if ((host == "tiktok" || host == "douyin") && isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) {
|
||||
if ((host == "tiktok" || host == "douyin") && services.tiktok.audioFormats.includes(audioFormat)) {
|
||||
if (r.isMp3) {
|
||||
if (audioFormat === "mp3" || audioFormat === "best") {
|
||||
audioFormat = "mp3"
|
||||
@@ -115,5 +115,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
filename: r.audioFilename, isAudioOnly: true,
|
||||
audioFormat: audioFormat, copy: copy, fileMetadata: r.fileMetadata ? r.fileMetadata : false
|
||||
})
|
||||
} else {
|
||||
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"soundcloud": {
|
||||
"patterns": [":author/:song", ":shortLink"],
|
||||
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],
|
||||
"bestAudio": "none",
|
||||
"enabled": true
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function(obj) {
|
||||
}).then((r) => {return r.text()}).catch(() => {return false});
|
||||
}
|
||||
if (obj.author && obj.song) {
|
||||
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, {
|
||||
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`, {
|
||||
headers: {"user-agent": genericUserAgent}
|
||||
}).then((r) => {return r.text()}).catch(() => {return false});
|
||||
}
|
||||
@@ -54,7 +54,8 @@ export default async function(obj) {
|
||||
if (json["media"]["transcodings"]) {
|
||||
let clientId = await findClientID();
|
||||
if (clientId) {
|
||||
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${clientId}&track_authorization=${json.track_authorization}`;
|
||||
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive")
|
||||
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:") {
|
||||
if (json.duration < maxAudioDuration) {
|
||||
let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false});
|
||||
|
||||
@@ -1,40 +1,50 @@
|
||||
import NodeCache from "node-cache";
|
||||
|
||||
import { UUID, encrypt } from "../sub/crypto.js";
|
||||
import { sha256 } from "../sub/crypto.js";
|
||||
import { streamLifespan } from "../config.js";
|
||||
|
||||
const streamCache = new NodeCache({ stdTTL: streamLifespan, checkperiod: 120 });
|
||||
const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, deleteOnExpire: true });
|
||||
const salt = process.env.streamSalt;
|
||||
|
||||
export function createStream(obj) {
|
||||
let streamUUID = UUID(),
|
||||
exp = Math.floor(new Date().getTime()) + streamLifespan,
|
||||
ghmac = encrypt(`${streamUUID},${obj.service},${obj.ip},${exp}`, salt)
|
||||
streamCache.on("expired", (key) => {
|
||||
streamCache.del(key);
|
||||
});
|
||||
|
||||
streamCache.set(streamUUID, {
|
||||
id: streamUUID,
|
||||
service: obj.service,
|
||||
type: obj.type,
|
||||
urls: obj.u,
|
||||
filename: obj.filename,
|
||||
hmac: ghmac,
|
||||
ip: obj.ip,
|
||||
exp: exp,
|
||||
isAudioOnly: !!obj.isAudioOnly,
|
||||
audioFormat: obj.audioFormat,
|
||||
time: obj.time ? obj.time : false,
|
||||
copy: obj.copy ? true : false,
|
||||
mute: obj.mute ? true : false,
|
||||
metadata: obj.fileMetadata ? obj.fileMetadata : false
|
||||
});
|
||||
return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`;
|
||||
export function createStream(obj) {
|
||||
let streamID = sha256(`${obj.ip},${obj.service},${obj.filename},${obj.audioFormat},${obj.mute}`, salt),
|
||||
exp = Math.floor(new Date().getTime()) + streamLifespan,
|
||||
ghmac = sha256(`${streamID},${obj.service},${obj.ip},${exp}`, salt);
|
||||
|
||||
if (!streamCache.has(streamID)) {
|
||||
streamCache.set(streamID, {
|
||||
id: streamID,
|
||||
service: obj.service,
|
||||
type: obj.type,
|
||||
urls: obj.u,
|
||||
filename: obj.filename,
|
||||
hmac: ghmac,
|
||||
ip: obj.ip,
|
||||
exp: exp,
|
||||
isAudioOnly: !!obj.isAudioOnly,
|
||||
audioFormat: obj.audioFormat,
|
||||
time: obj.time ? obj.time : false,
|
||||
copy: !!obj.copy,
|
||||
mute: !!obj.mute,
|
||||
metadata: obj.fileMetadata ? obj.fileMetadata : false
|
||||
});
|
||||
} else {
|
||||
let streamInfo = streamCache.get(streamID);
|
||||
exp = streamInfo.exp;
|
||||
ghmac = streamInfo.hmac;
|
||||
}
|
||||
return `${process.env.selfURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
|
||||
}
|
||||
|
||||
export function verifyStream(ip, id, hmac, exp) {
|
||||
try {
|
||||
let streamInfo = streamCache.get(id);
|
||||
if (streamInfo) {
|
||||
let ghmac = encrypt(`${id},${streamInfo.service},${ip},${exp}`, salt);
|
||||
let ghmac = sha256(`${id},${streamInfo.service},${ip},${exp}`, salt);
|
||||
if (hmac == ghmac && ip == streamInfo.ip && ghmac == streamInfo.hmac && exp > Math.floor(new Date().getTime()) && exp == streamInfo.exp) {
|
||||
return streamInfo;
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createHmac, randomUUID } from "crypto";
|
||||
import { createHmac } from "crypto";
|
||||
|
||||
export function encrypt(str, salt) {
|
||||
export function sha256(str, salt) {
|
||||
return createHmac("sha256", salt).update(str).digest("hex");
|
||||
}
|
||||
export function UUID() {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,18 @@ export function msToTime(d) {
|
||||
return r;
|
||||
}
|
||||
export function cleanURL(url, host) {
|
||||
let forbiddenChars = ['}', '{', '(', ')', '\\', '@', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'"]
|
||||
let forbiddenChars = ['}', '{', '(', ')', '\\', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@"]
|
||||
switch(host) {
|
||||
case "youtube":
|
||||
url = url.split('&')[0];
|
||||
break;
|
||||
case "tiktok":
|
||||
url = url.replace(/@([a-zA-Z]+(\.[a-zA-Z]+)+)/, "@a")
|
||||
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], '')
|
||||
}
|
||||
@@ -70,14 +81,6 @@ export function cleanURL(url, host) {
|
||||
if (url.includes('youtube.com/shorts/')) {
|
||||
url = url.split('?')[0].replace('shorts/', 'watch?v=');
|
||||
}
|
||||
if (host === "youtube") {
|
||||
url = url.split('&')[0];
|
||||
} else {
|
||||
url = url.split('?')[0];
|
||||
if (url.substring(url.length - 1) === "/") {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
}
|
||||
return url.slice(0, 128)
|
||||
}
|
||||
export function languageCode(req) {
|
||||
|
||||
Reference in New Issue
Block a user