8k and quality picker revamp

This commit is contained in:
wukko 2022-11-04 14:49:58 +06:00
parent e24a3d84d6
commit 378fecd849
14 changed files with 64 additions and 49 deletions

View file

@ -17,7 +17,7 @@ It preserves original media quality so you get best downloads possible (unless y
| -------- | :---: | :---: | :----- |
| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. |
| Twitter Spaces | ❌️ | ✅ | Audio metadata. |
| YouTube & Shorts | ✅ | ✅ | Support for 4K, HDR and high FPS videos. |
| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. |
| YouTube Music | ❌ | ✅ | Audio metadata. |
| Reddit | ✅ | ✅ | |
| TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermarks. |

View file

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

View file

@ -64,9 +64,9 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
if (req.query.url && req.query.url.length < 150) {
let j = await getJSON(req.query.url.trim(), languageCode(req), {
ip: req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip,
format: req.query.format ? req.query.format.slice(0, 5) : "webm",
quality: req.query.quality ? req.query.quality.slice(0, 3) : "max",
audioFormat: req.query.audioFormat ? req.query.audioFormat.slice(0, 4) : false,
format: req.query.format ? req.query.format.slice(0, 5) : "mp4",
quality: req.query.quality ? req.query.quality.slice(0, 3) : "mid",
audioFormat: req.query.audioFormat ? req.query.audioFormat.slice(0, 4) : "mp3",
isAudioOnly: !!req.query.audio,
noWatermark: !!req.query.nw,
fullAudio: !!req.query.ttfull,

View file

@ -25,7 +25,7 @@
}
},
"quality": {
"hig": "1080",
"hig": "1440",
"mid": "720",
"low": "480"
},

View file

@ -1,18 +1,19 @@
let isIOS = navigator.userAgent.toLowerCase().match("iphone os");
let ua = navigator.userAgent.toLowerCase();
let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || ua.match("iphone os");
let version = 14;
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
let notification = `<div class="notification-dot"></div>`
let switchers = {
"theme": ["auto", "light", "dark"],
"ytFormat": ["webm", "mp4"],
"quality": ["max", "hig", "mid", "low"],
"defaultAudioFormat": ["mp3", "best", "ogg", "wav", "opus"]
"vFormat": ["mp4", "webm"],
"vQuality": ["hig", "max", "mid", "low"],
"aFormat": ["mp3", "best", "ogg", "wav", "opus"]
}
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio"];
let exceptions = { // used solely for ios devices
"ytFormat": "mp4",
"defaultAudioFormat": "mp3"
let exceptions = { // used for mobile devices
"vQuality": "mid"
}
function eid(id) {
@ -208,6 +209,9 @@ function popup(type, action, text) {
eid("popup-backdrop").style.visibility = vis(action);
eid(`popup-${type}`).style.visibility = vis(action);
}
function updateMP4Text() {
eid("vFormat-mp4").innerHTML = sGet("vQuality") === "mid" ? "mp4 (h264/av1)" : "mp4 (av1)";
}
function changeSwitcher(li, b) {
if (b) {
sSet(li, b);
@ -215,9 +219,10 @@ function changeSwitcher(li, b) {
(switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`)
}
if (li === "theme") detectColorScheme();
if (li === "vQuality") updateMP4Text();
} else {
let pref = switchers[li][0];
if (isIOS && exceptions[li]) pref = exceptions[li];
if (isMobile && exceptions[li]) pref = exceptions[li];
sSet(li, pref);
for (let i in switchers[li]) {
(switchers[li][i] === pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`)
@ -299,6 +304,10 @@ function changeButton(type, text) {
break;
}
}
function resetSettings() {
localStorage.clear();
window.location.reload();
}
async function pasteClipboard() {
let t = await navigator.clipboard.readText();
if (regex.test(t)) {
@ -314,7 +323,7 @@ async function download(url) {
let format = ``;
if (audioMode === "false") {
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) {
format = `&format=${sGet("ytFormat")}`
format = `&format=${sGet("vFormat")}`
} else if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") {
format = `&nw=true`
}
@ -322,8 +331,8 @@ async function download(url) {
format = `&nw=true`
if (sGet("fullTikTokAudio") === "true") format += `&ttfull=true`
}
let mode = (sGet("audioMode") === "true") ? `audio=true` : `quality=${sGet("quality")}`
await fetch(`/api/json?audioFormat=${sGet("defaultAudioFormat")}&${mode}${format}&url=${encodeURIComponent(url)}`).then(async (r) => {
let mode = (sGet("audioMode") === "true") ? `audio=true` : `quality=${sGet("vQuality")}`
await fetch(`/api/json?audioFormat=${sGet("aFormat")}&${mode}${format}&url=${encodeURIComponent(url)}`).then(async (r) => {
let j = await r.json();
if (j.status !== "error" && j.status !== "rate-limit") {
if (j.url) {

View file

@ -53,8 +53,8 @@
"AccessibilityKeepDownloadButton": "keep the download button always visible",
"SettingsEnableDownloadPopup": "ask for a way to save",
"AccessibilityEnableDownloadPopup": "ask what to do with downloads",
"SettingsFormatDescription": "select webm if you want max quality available. webm videos are usually higher quality but ios devices can't play them natively.",
"SettingsQualityDescription": "if selected resolution isn't available, closest one gets picked instead. if you want to post a youtube video on twitter, then select a combination of mp4 and 720p. twitter likes videos like that way more.",
"SettingsFormatDescription": "select webm if you want max quality available. webm videos are usually higher bitrate, but ios devices can't play them natively.",
"SettingsQualityDescription": "if selected quality isn't available, closest one gets picked instead.\nif you want to post a youtube video on social media, then select a combination of mp4 and 720p. those videos are usually not in av1 codec, so they should play just fine basically everywhere.",
"DonateSubtitle": "help me pay for hosting",
"DonateDescription": "i don't really like crypto in its current state, but it's the only reliable way for me to receive money and pay for anything abroad.",
"LinkGitHubIssues": "&gt;&gt; report issues and check out the source code on github",

View file

@ -53,8 +53,8 @@
"AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране",
"SettingsEnableDownloadPopup": "спрашивать, что делать при скачивании",
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений.",
"SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай комбинацию из mp4 и 720p. твиттер такие видео обычно воспринимает намного лучше.",
"SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. у webm видео битрейт обычно выше, но устройства на ios не могут проигрывать их без сторонних приложений.",
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.\nесли ты хочешь опубликовать видео с youtube где-то в соц. сетях, то выбирай комбинацию из mp4 и 720p. у таких видео кодек обычно не av1, поэтому они должны работать практически везде.",
"DonateSubtitle": "помоги мне платить за хостинг",
"DonateDescription": "я не люблю крипто в его текущем состоянии, но у меня нет другого надёжного способа оплаты хостинга.",
"LinkGitHubIssues": "&gt;&gt; сообщай о проблемах и смотри исходный код на гитхабе",

View file

@ -1,10 +1,14 @@
{
"current": {
"version": "4.2",
"title": "optimized quality picking and 8k video support",
"content": "- this update fixes quality picking that was accidentally broken in 4.0 update.\n- you now can download videos in 8k from youtube. why would you that? no idea. but i'm more than happy to give you this option.\n- default video quality for downloads from pc is now 1440p, and 720p for phones.\n- default video format is now mp4 for everyone.\n- default audio format is now mp3 for everyone.\n\nyou can always change new defaults back to whatever you prefer in settings.\n\nother changes:\n- added more clarity to quality picker description.\n- youtube video codecs are now right in the picker.\n- setup script is now easier to understand."
},
"history": [{
"version": "4.1",
"title": "better tiktok image downloads",
"content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, {appName} will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in {appName} 4.x yet, check out the previous changelog down below. there's a ton of stuff to like."
},
"history": [{
}, {
"version": "4.0",
"title": "better and faster than ever",
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- {appName} now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when {appName} is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, {appName} will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>.\n\nfun fact: average {appName} user is 10 times cooler than a regular person."

View file

@ -172,12 +172,12 @@ export default function(obj) {
name: "downloads",
title: loc(obj.lang, 'SettingsDownloadsSubtitle'),
body: switcher({
name: "quality",
name: "vQuality",
subtitle: loc(obj.lang, 'SettingsQualitySubtitle'),
explanation: loc(obj.lang, 'SettingsQualityDescription'),
items: [{
"action": "max",
"text": loc(obj.lang, 'SettingsQualitySwitchMax')
"text": `${loc(obj.lang, 'SettingsQualitySwitchMax')}<br/>(2160p+)`
}, {
"action": "hig",
"text": `${loc(obj.lang, 'SettingsQualitySwitchHigh')}<br/>(${quality.hig}p)`
@ -193,13 +193,15 @@ export default function(obj) {
+ settingsCategory({
name: "youtube",
body: switcher({
name: "ytFormat",
name: "vFormat",
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
explanation: loc(obj.lang, 'SettingsFormatDescription'),
items: [{
"action": "mp4"
"action": "mp4",
"text": "mp4 (av1)"
}, {
"action": "webm"
"action": "webm",
"text": "webm (vp9)"
}]
})
})
@ -215,7 +217,7 @@ export default function(obj) {
name: "general",
title: loc(obj.lang, 'SettingsAudioTab'),
body: switcher({
name: "defaultAudioFormat",
name: "aFormat",
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
explanation: loc(obj.lang, 'SettingsAudioFormatDescription'),
items: audioFormats

View file

@ -15,7 +15,7 @@ export default async function(obj) {
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength <= maxVideoDuration) {
let video = streamData["data"]["dash"]["video"].filter((v) => {
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/") && v["height"] !== 4320) return true;
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;

View file

@ -15,7 +15,7 @@ export default async function(obj) {
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0]
try {
if (obj.quality !== "max") {
if (obj.quality != "max") {
let pref = parseInt(quality[obj.quality], 10)
for (let i in all) {
let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10)

View file

@ -10,32 +10,32 @@ export default async function(obj) {
let info = infoInitial.formats;
if (!info[0]["isLive"]) {
let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] === obj.format) return true;
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true;
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (!obj.isAudioOnly) {
video = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] === obj.format && a["height"] !== 4320) {
if (obj.quality !== "max") {
if (a["hasAudio"] && mq[obj.quality] === a["height"]) {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) {
if (obj.quality != "max") {
if (a["hasAudio"] && mq[obj.quality] == a["height"]) {
fullVideoMatch.push(a)
} else if (!a["hasAudio"] && mq[obj.quality] === a["height"]) {
} else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) {
videoMatch.push(a);
}
}
return true
}
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (obj.quality !== "max") {
if (videoMatch.length === 0) {
if (obj.quality != "max") {
if (videoMatch.length == 0) {
let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim())
videoMatch = video.filter((a) => {
if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() === ss) return true;
if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true;
})
} else if (fullVideoMatch.length > 0) {
videoMatch = [fullVideoMatch[0]]
}
} else videoMatch = [video[0]];
if (obj.quality === "los") videoMatch = [video[video.length - 1]];
if (obj.quality == "los") videoMatch = [video[video.length - 1]];
}
let generalMeta = {
title: infoInitial.videoDetails.title,

View file

@ -16,20 +16,20 @@ let final = () => {
for (let i in ob) {
appendFileSync(envPath, `${i}=${ob[i]}\n`)
}
console.log(Bright("\nI've created a .env file with selfURL, port, and stream salt."))
console.log(Bright("\nAwesome! I've created a fresh .env file for you."))
console.log(`${Bright("Now I'll run")} ${Cyan("npm install")} ${Bright("to install all dependencies. It shouldn't take long.\n\n")}`)
execSync('npm install', { stdio: [0, 1, 2] });
console.log(`\n\n${Green("All done!\n")}`)
console.log("You can re-run this script any time to update the configuration.")
console.log("\nYou're now ready to start the main project.\nHave fun!")
console.log(`\n\n${Cyan("All done!\n")}`)
console.log(Bright("You can re-run this script at any time to update the configuration."))
console.log(Bright("\nYou're now ready to start cobalt. Simply run ") + Cyan("npm start") + Bright('!\nHave fun :)'))
rl.close()
}
console.log(
`${Cyan("Welcome to cobalt!")}\n${Bright("We'll get you up and running in no time.\nLet's start by creating a ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
`${Cyan("Welcome to cobalt!")}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
)
console.log(
Bright("\nWhat's the selfURL we'll be running on? (localhost)")
Bright("\nWhat's the domain this instance will be running on? (localhost)\nExample: co.wukko.me")
)
rl.question(q, r1 => {
@ -38,7 +38,7 @@ rl.question(q, r1 => {
} else {
ob['selfURL'] = `http://localhost`
}
console.log(Bright("\nGreat! Now, what's the port we'll be running on? (9000)"))
console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)"))
rl.question(q, r2 => {
if (!r1 && !r2) {
ob['selfURL'] = `http://localhost:9000/`

View file

@ -7,12 +7,12 @@ function closest(goal, array) {
}
export default function(service, quality, maxQuality) {
if (quality === "max") return maxQuality;
if (quality == "max") return maxQuality;
quality = parseInt(mq[quality], 10)
maxQuality = parseInt(maxQuality, 10)
if (quality >= maxQuality || quality === maxQuality) return maxQuality;
if (quality >= maxQuality || quality == maxQuality) return maxQuality;
if (quality < maxQuality) {
if (services[service]["quality"][quality]) {