From 5bd50fd55fe8041f2742ac8778c020203aa6dd0f Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 2 Dec 2023 20:44:19 +0600 Subject: [PATCH 1/7] twitter: remux all videos - increased stream link lifespan to 90 seconds - decreased max video duration back to 3 hours --- package.json | 2 +- src/config.json | 4 +- src/localization/languages/en.json | 2 +- src/localization/languages/ru.json | 2 +- src/modules/processing/matchActionDecider.js | 17 +++--- src/modules/processing/services/twitter.js | 11 +++- src/modules/stream/manage.js | 24 ++++++--- src/modules/stream/stream.js | 2 +- src/modules/stream/types.js | 56 ++++++++++++-------- src/modules/sub/utils.js | 12 ----- 10 files changed, 78 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 6b1ca082..e1db6461 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.6.8", + "version": "7.7", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/config.json b/src/config.json index b1d17674..303af1ed 100644 --- a/src/config.json +++ b/src/config.json @@ -1,6 +1,6 @@ { - "streamLifespan": 20000, - "maxVideoDuration": 18000000, + "streamLifespan": 90000, + "maxVideoDuration": 10800000, "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "authorInfo": { "name": "wukko", diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 62ef6a90..7612ff47 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -105,7 +105,7 @@ "FollowSupport": "keep in touch with cobalt for support, polls, news, and more:", "SupportNote": "please note that response may take a while, there's only one person managing everything.", "SourceCode": "report issues, explore source code, star or fork the repo:", - "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for 20 seconds and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's source code yourself and see that everything is as stated.", + "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for 90 seconds and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's source code yourself and see that everything is as stated.", "ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api sometimes acts unexpectedly. try again or try another settings.", "SettingsCodecSubtitle": "youtube codec", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index ec2e60d7..81ccb8cd 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -106,7 +106,7 @@ "FollowSupport": "подписывайся на соц.сети кобальта для новостей, поддержки, участия в опросах, и многого другого:", "SupportNote": "так как я занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.", "SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", - "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение 20 секунд. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", + "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение 90 секунд. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", "SettingsCodecSubtitle": "кодек для видео с youtube", diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index eedf8ec1..422d9a62 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -40,6 +40,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d case "bilibili": params = { type: "render" }; break; + case "twitter": case "youtube": params = { type: r.type }; break; @@ -64,7 +65,6 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d case "vine": case "instagram": case "tumblr": - case "twitter": case "pinterest": case "streamable": responseType = 1; @@ -72,7 +72,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d } break; case "singleM3U8": - params = { type: "videoM3U8" } + params = { type: "remux" } break; case "muteVideo": params = { @@ -107,14 +107,17 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d break; case "audio": - if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); + if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) { + return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }) + } let processType = "render"; let copy = false; if (!supportedAudio.includes(audioFormat)) audioFormat = "best"; - if ((host === "tiktok" || host === "douyin") && 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"; @@ -125,11 +128,13 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d processType = "bridge" } } - if (host === "tumblr" && !r.filename && (audioFormat === "best" || audioFormat === "mp3")) { + if (host === "tumblr" && !r.filename + && (audioFormat === "best" || audioFormat === "mp3")) { audioFormat = "mp3"; processType = "bridge" } - if ((audioFormat === "best" && services[host]["bestAudio"]) || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) { + if ((audioFormat === "best" && services[host]["bestAudio"]) + || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) { audioFormat = services[host]["bestAudio"]; if (host === "soundcloud") { processType = "render" diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js index da3b2b44..110a43e1 100644 --- a/src/modules/processing/services/twitter.js +++ b/src/modules/processing/services/twitter.js @@ -1,4 +1,5 @@ import { genericUserAgent } from "../../config.js"; +import { createStream } from "../../stream/manage.js"; function bestQuality(arr) { return arr.filter(v => v["content_type"] === "video/mp4").sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"] @@ -39,7 +40,7 @@ export default async function(obj) { let tweet = await fetch(query, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false - }).catch((e) => { return false }); + }).catch(() => { return false }); // {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}} if (tweet?.data?.tweetResult?.result?.__typename !== "Tweet") { @@ -64,7 +65,12 @@ export default async function(obj) { multiple.push({ type: "video", thumb: media[i]["media_url_https"], - url: bestQuality(media[i]["video_info"]["variants"]) + url: createStream({ + service: "twitter", + type: "remux", + u: bestQuality(media[i]["video_info"]["variants"]), + filename: `twitter_${obj.id}_${Number(i) + 1}.mp4` + }) }) } } else if (media.length === 1) { @@ -75,6 +81,7 @@ export default async function(obj) { if (single) { return { + type: "remux", urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index b3954d1a..3ec43b68 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -5,16 +5,22 @@ import { nanoid } from 'nanoid'; import { sha256 } from "../sub/crypto.js"; import { streamLifespan } from "../config.js"; -const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, deleteOnExpire: true }); -const streamSalt = randomBytes(64).toString('hex'); +const streamCache = new NodeCache({ + stdTTL: streamLifespan/1000, + checkperiod: 10, + deleteOnExpire: true +}) streamCache.on("expired", (key) => { streamCache.del(key); -}); +}) + +const streamSalt = randomBytes(64).toString('hex'); export function createStream(obj) { + let lifespan = streamLifespan let streamID = nanoid(), - exp = Math.floor(new Date().getTime()) + streamLifespan, + exp = Math.floor(new Date().getTime()) + lifespan, ghmac = sha256(`${streamID},${obj.service},${exp}`, streamSalt); if (!streamCache.has(streamID)) { @@ -44,14 +50,20 @@ export function createStream(obj) { export function verifyStream(id, hmac, exp) { try { let streamInfo = streamCache.get(id.toString()); - if (!streamInfo) return { error: "this download link has expired or doesn't exist. go back and try again!", status: 400 }; + if (!streamInfo) return { + error: "this download link has expired or doesn't exist. go back and try again!", + status: 400 + } let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt); if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac) && Number(exp) > Math.floor(new Date().getTime())) { return streamInfo; } - return { error: "i couldn't verify if you have access to this download. go back and try again!", status: 401 }; + return { + error: "i couldn't verify if you have access to this stream. go back and try again!", + status: 401 + } } catch (e) { return { status: 500, body: { status: "error", text: "Internal Server Error" } }; } diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index 4b03196b..fb4f7ed6 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -10,7 +10,7 @@ export default async function(res, streamInfo) { case "render": await streamLiveRender(streamInfo, res); break; - case "videoM3U8": + case "remux": case "mute": streamVideoOnly(streamInfo, res); break; diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index c584789f..03a78205 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -1,7 +1,7 @@ import { spawn } from "child_process"; import ffmpeg from "ffmpeg-static"; import { ffmpegArgs, genericUserAgent } from "../config.js"; -import { getThreads, metadataManager } from "../sub/utils.js"; +import { metadataManager } from "../sub/utils.js"; import { request } from "undici"; import { create as contentDisposition } from "content-disposition-header"; import { AbortController } from "abort-controller" @@ -40,7 +40,10 @@ export async function streamDefault(streamInfo, res) { const shutdown = () => (closeRequest(abortController), closeResponse(res)); try { - const filename = streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename; + let filename = streamInfo.filename; + if (streamInfo.isAudioOnly) { + filename = `${streamInfo.filename}.${streamInfo.audioFormat}` + } res.setHeader('Content-disposition', contentDisposition(filename)); const { body: stream, headers } = await request(streamInfo.urls, { @@ -60,7 +63,11 @@ export async function streamDefault(streamInfo, res) { export async function streamLiveRender(streamInfo, res) { let abortController = new AbortController(), process; - const shutdown = () => (closeRequest(abortController), killProcess(process), closeResponse(res)); + const shutdown = () => ( + closeRequest(abortController), + killProcess(process), + closeResponse(res) + ); try { if (streamInfo.urls.length !== 2) return shutdown(); @@ -72,7 +79,6 @@ export async function streamLiveRender(streamInfo, res) { let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ '-loglevel', '-8', - '-threads', `${getThreads()}`, '-i', streamInfo.urls[0], '-i', 'pipe:3', '-map', '0:v', @@ -80,7 +86,9 @@ export async function streamLiveRender(streamInfo, res) { ]; args = args.concat(ffmpegArgs[format]); - if (streamInfo.metadata) args = args.concat(metadataManager(streamInfo.metadata)); + if (streamInfo.metadata) { + args = args.concat(metadataManager(streamInfo.metadata)) + } args.push('-f', format, 'pipe:4'); process = spawn(ffmpeg, args, { @@ -115,25 +123,19 @@ export function streamAudioOnly(streamInfo, res) { try { let args = [ '-loglevel', '-8', - '-threads', `${getThreads()}`, - '-i', streamInfo.urls + '-i', streamInfo.urls, + '-vn' ] if (streamInfo.metadata) { - if (streamInfo.metadata.cover) { // currently corrupts the audio - args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0') - } else { - args.push('-vn') - } args = args.concat(metadataManager(streamInfo.metadata)) - } else { - args.push('-vn') } - let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]; args = args.concat(arg); - if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]); + if (ffmpegArgs[streamInfo.audioFormat]) { + args = args.concat(ffmpegArgs[streamInfo.audioFormat]) + } args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); process = spawn(ffmpeg, args, { @@ -162,16 +164,26 @@ export function streamVideoOnly(streamInfo, res) { try { let args = [ - '-loglevel', '-8', - '-threads', `${getThreads()}`, + '-loglevel', '-8' + ] + if (streamInfo.service === "twitter") { + args.push('-seekable', '0') + } + args.push( '-i', streamInfo.urls, '-c', 'copy' - ] - if (streamInfo.mute) args.push('-an'); - if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") args.push('-bsf:a', 'aac_adtstoasc'); + ) + if (streamInfo.mute) { + args.push('-an') + } + if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") { + args.push('-bsf:a', 'aac_adtstoasc') + } let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; - if (format === "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov'); + if (format === "mp4") { + args.push('-movflags', 'faststart+frag_keyframe+empty_moov') + } args.push('-f', format, 'pipe:3'); process = spawn(ffmpeg, args, { diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index cfb56aa8..111cb6c8 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -134,18 +134,6 @@ export function checkJSONPost(obj) { export function getIP(req) { return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip; } -export function getThreads() { - try { - if (process.env.ffmpegThreads && process.env.ffmpegThreads.length <= 3 - && (Number(process.env.ffmpegThreads) >= 0 && Number(process.env.ffmpegThreads) <= 256)) { - return process.env.ffmpegThreads - } else { - return '0' - } - } catch (e) { - return '0' - } -} export function cleanHTML(html) { let clean = html.replace(/ {4}/g, ''); clean = clean.replace(/\n/g, ''); From 3e8c059a3ab722834fbad53aba18acbb6744c6a8 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 2 Dec 2023 21:52:38 +0600 Subject: [PATCH 2/7] vimeo: fix parsing and resolution in filename - all videos/audios should now be downloadable - proper resolution is now displayed in basic and pretty filename styles --- src/modules/processing/matchActionDecider.js | 4 -- src/modules/processing/services/vimeo.js | 50 +++++++------------- src/modules/stream/types.js | 9 +++- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 422d9a62..4efd0f12 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -145,10 +145,6 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d } else if (audioFormat === "best") { audioFormat = "m4a"; copy = true; - if (!r.filenameAttributes && r.audioFilename.includes("twitterspaces")) { - audioFormat = "mp3" - copy = false - } } if (r.isM3U8 || host === "vimeo") { copy = false; diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js index b60a1c39..90dc9b3d 100644 --- a/src/modules/processing/services/vimeo.js +++ b/src/modules/processing/services/vimeo.js @@ -63,40 +63,26 @@ export default async function(obj) { if (!masterJSON) return { error: 'ErrorCouldntFetch' }; if (!masterJSON.video) return { error: 'ErrorEmptyDownload' }; - let type = "parcel"; - if (masterJSON.base_url === "../") type = "chop"; - - let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)), + let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)).filter(a => a['format'] === "mp42"), bestVideo = masterJSON_Video[0]; - if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality); - - let videoUrl, audioUrl, baseUrl = masterJSONURL.split("/sep/")[0]; - switch (type) { - case "parcel": - let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter(a => a['mime_type'] === "audio/mp4"), - bestAudio = masterJSON_Audio[0]; - videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`, - audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`; - break; - case "chop": - videoUrl = `${baseUrl}/sep/video/${bestVideo.id}/master.m3u8`; - break; + if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) { + bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality) } - if (videoUrl) { - return { - urls: audioUrl ? [videoUrl, audioUrl] : videoUrl, - isM3U8: audioUrl ? false : true, - fileMetadata: fileMetadata, - filenameAttributes: { - service: "vimeo", - id: obj.id, - title: fileMetadata.title, - author: fileMetadata.artist, - resolution: `${bestVideo["width"]}x${bestVideo["height"]}`, - qualityLabel: `${bestVideo["height"]}p`, - extension: "mp4" - } + + let masterM3U8 = `${masterJSONURL.split("/sep/")[0]}/sep/video/${bestVideo.id}/master.m3u8`; + + return { + urls: masterM3U8, + isM3U8: true, + fileMetadata: fileMetadata, + filenameAttributes: { + service: "vimeo", + id: obj.id, + title: fileMetadata.title, + author: fileMetadata.artist, + resolution: `${bestVideo["width"]}x${bestVideo["height"]}`, + qualityLabel: `${resolutionMatch[bestVideo["width"]]}p`, + extension: "mp4" } } - return { error: 'ErrorEmptyDownload' } } diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 03a78205..c2bd2910 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -122,10 +122,15 @@ export function streamAudioOnly(streamInfo, res) { try { let args = [ - '-loglevel', '-8', + '-loglevel', '-8' + ] + if (streamInfo.service === "twitter") { + args.push('-seekable', '0') + } + args.push( '-i', streamInfo.urls, '-vn' - ] + ) if (streamInfo.metadata) { args = args.concat(metadataManager(streamInfo.metadata)) From afab7f94a726056bee2c3739d6bc38a4f41eb99d Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 2 Dec 2023 22:01:58 +0600 Subject: [PATCH 3/7] api & web: ports in env are no longer strictly required --- docs/examples/docker-compose.example.yml | 2 -- src/cobalt.js | 10 +++++++--- src/core/api.js | 8 ++++---- src/core/web.js | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index 8a5f9d67..2262933f 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -17,7 +17,6 @@ services: #- 127.0.0.1:9000:9000 environment: - - apiPort=9000 # replace apiURL with your instance's target url in same format - apiURL=https://co.wuk.sh/ # replace apiName with your instance's distinctive name @@ -48,7 +47,6 @@ services: #- 127.0.0.1:9001:9001 environment: - - webPort=9001 # replace webURL with your instance's target url in same format - webURL=https://cobalt.tools/ # replace apiURL with preferred api instance url diff --git a/src/cobalt.js b/src/cobalt.js index 949cccba..6a148860 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -21,8 +21,8 @@ app.disable('x-powered-by'); await loadLoc(); -const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port)); -const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port)); +const apiMode = process.env.apiURL && !process.env.webURL; +const webMode = process.env.webURL && !process.env.apiURL; if (apiMode) { const { runAPI } = await import('./core/api.js'); @@ -31,5 +31,9 @@ if (apiMode) { const { runWeb } = await import('./core/web.js'); await runWeb(express, app, gitCommit, gitBranch, __dirname) } else { - console.log(Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`)) + console.log( + Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + + Bright(`please run the setup script to fix this: `) + + Green(`npm run setup`) + ) } diff --git a/src/core/api.js b/src/core/api.js index 84464b56..4e78fbb5 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -139,9 +139,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { version: version, commit: gitCommit, branch: gitBranch, - name: process.env.apiName ? process.env.apiName : "unknown", + name: process.env.apiName || "unknown", url: process.env.apiURL, - cors: process.env.cors && process.env.cors === "0" ? 0 : 1, + cors: process.env?.cors === "0" ? 0 : 1, startTime: `${startTimestamp}` }); default: @@ -167,12 +167,12 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { res.redirect('/api/json') }); - app.listen(process.env.apiPort, () => { + app.listen(process.env.apiPort || 9000, () => { console.log(`\n` + `${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + `URL: ${Cyan(`${process.env.apiURL}`)}\n` + - `Port: ${process.env.apiPort}\n` + `Port: ${process.env.apiPort || 9000}\n` ) }); } diff --git a/src/core/web.js b/src/core/web.js index c2512c1f..08a6ffed 100644 --- a/src/core/web.js +++ b/src/core/web.js @@ -76,12 +76,12 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) { return res.redirect('/') }); - app.listen(process.env.webPort, () => { + app.listen(process.env.webPort || 9001, () => { console.log(`\n` + `${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + `URL: ${Cyan(`${process.env.webURL}`)}\n` + - `Port: ${process.env.webPort}\n` + `Port: ${process.env.webPort || 9001}\n` ) }) } From 83d82f5da9f05f57c9cddf92e4d8de408a85931e Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 2 Dec 2023 22:51:08 +0600 Subject: [PATCH 4/7] web: saving cobalt streams via action chooser --- src/cobalt.js | 2 +- src/front/cobalt.js | 13 ++++++++++--- src/modules/setup.js | 3 --- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cobalt.js b/src/cobalt.js index 6a148860..2d90e07e 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -22,7 +22,7 @@ app.disable('x-powered-by'); await loadLoc(); const apiMode = process.env.apiURL && !process.env.webURL; -const webMode = process.env.webURL && !process.env.apiURL; +const webMode = process.env.webURL && process.env.apiURL; if (apiMode) { const { runAPI } = await import('./core/api.js'); diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 4a3ad9e6..15ebaa8e 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -423,9 +423,16 @@ async function download(url) { let jp = await res.json(); if (jp.status === "continue") { changeDownloadButton(2, '>>>'); - if (isMobile || isSafari) { - window.location.href = j.url; - } else window.open(j.url, '_blank'); + if (sGet("downloadPopup") === "true") { + popup('download', 1, j.url) + setTimeout(() => { + popup('download', 0); + }, 90000) + } else { + if (isMobile || isSafari) { + window.location.href = j.url; + } else window.open(j.url, '_blank'); + } setTimeout(() => { changeButton(1) }, 2500); } else { changeButton(0, jp.text); diff --git a/src/modules/setup.js b/src/modules/setup.js index cb5aa184..8895d88c 100644 --- a/src/modules/setup.js +++ b/src/modules/setup.js @@ -29,9 +29,6 @@ console.log( `${Cyan(`Hey, this is cobalt v.${version}!`)}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}` ) -console.log( - `\n${Bright("⚠️ Please notice that since v.6.0 cobalt is hosted in two parts. API and web app are now separate.\nMerged hosting is no longer available.")}` -) function setup() { console.log(Bright("\nWhat kind of server will this instance be?\nOptions: api, web.")); From 08edf28ccf8c14b131c75f3f0def5da7494cde1d Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 2 Dec 2023 16:59:37 +0000 Subject: [PATCH 5/7] build: add major version tag for docker images --- .github/workflows/docker.yml | 18 ++++++++++-------- docs/examples/docker-compose.example.yml | 6 +++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d70b1821..8b7042d9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -29,20 +29,22 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Get version from package.json - id: package-version - uses: martinbeentjes/npm-get-version-action@v1.3.1 - - name: Get short commit hash - id: commit-hash - run: echo "commit_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - name: Get release metadata + id: release-meta + run: | + version=$(cat package.json | jq -r .version) + echo "commit_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "version=$version" >> $GITHUB_OUTPUT + echo "major_version=$(echo "$version" | cut -d. -f1)" >> $GITHUB_OUTPUT - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v4 with: tags: | type=raw,value=latest - type=raw,value=${{ steps.package-version.outputs.current-version }} - type=raw,value=${{ steps.package-version.outputs.current-version }}-${{ steps.commit-hash.outputs.commit_short }} + type=raw,value=${{ steps.release-meta.outputs.version }} + type=raw,value=${{ steps.release-meta.outputs.major_version }} + type=raw,value=${{ steps.release-meta.outputs.version }}-${{ steps.release-meta.outputs.commit_short }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index 2262933f..b5ce8a30 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -2,7 +2,7 @@ version: '3.5' services: cobalt-api: - image: ghcr.io/wukko/cobalt:latest + image: ghcr.io/wukko/cobalt:7 restart: unless-stopped container_name: cobalt-api @@ -32,7 +32,7 @@ services: #- ./cookies.json:/cookies.json cobalt-web: - image: ghcr.io/wukko/cobalt:latest + image: ghcr.io/wukko/cobalt:7 restart: unless-stopped container_name: cobalt-web @@ -61,4 +61,4 @@ services: restart: unless-stopped command: --cleanup --scope cobalt --interval 900 volumes: - - /var/run/docker.sock:/var/run/docker.sock \ No newline at end of file + - /var/run/docker.sock:/var/run/docker.sock From 89c50676859bed903730ccb8ab898ad82aa69c2c Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 2 Dec 2023 23:10:19 +0600 Subject: [PATCH 6/7] web: fix auto hiding of download popup --- 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 15ebaa8e..0ff6ab3f 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -426,8 +426,8 @@ async function download(url) { if (sGet("downloadPopup") === "true") { popup('download', 1, j.url) setTimeout(() => { - popup('download', 0); - }, 90000) + hideAllPopups() + }, 85000) } else { if (isMobile || isSafari) { window.location.href = j.url; From 760f55bdb4749929794cd4e2db3e74d6c85e32ff Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 2 Dec 2023 23:47:34 +0600 Subject: [PATCH 7/7] 7.7 changelog and banner --- src/front/cobalt.js | 2 +- src/front/updateBanners/meowthpolishegg.webp | Bin 0 -> 15688 bytes src/localization/languages/en.json | 3 ++- src/localization/languages/ru.json | 3 ++- src/modules/changelog/changelog.json | 14 ++++++++++++-- src/modules/pageRender/page.js | 4 ++-- 6 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 src/front/updateBanners/meowthpolishegg.webp diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 0ff6ab3f..47a0d368 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,4 +1,4 @@ -const version = 38; +const version = 39; const ua = navigator.userAgent.toLowerCase(); const isIOS = ua.match("iphone os"); diff --git a/src/front/updateBanners/meowthpolishegg.webp b/src/front/updateBanners/meowthpolishegg.webp new file mode 100644 index 0000000000000000000000000000000000000000..d0891b41751086873d886b2d44ce0ffee86ec03c GIT binary patch literal 15688 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⪙IX9Dxd&jyd|Fwkwt;jC@doHdKs42h`wC$Lb?P*FKNu zKt7-+-H&v5#=c@RI>~FnjL5nGeC#{Hp1|?UU7Dx|95se(#(hg>Hf5Nnfk#oy1x2(DuN zL^#P@<9_i=`(h^f8X5I;x)_FKOh~f3sohQEKyvRS^%GT}y$m)F8o<$Bdmdw&5kE4A z=#vkAFCS(P0dJc8bn zW?Hq>ggSCg`zc4oZzgB|AwyaH#U7_7HvJr5$=0qAm~RJgi2AVLuHwotiNrrl&$hMP zD0lkeRE2JT&}#07jx^c#OF4J?4BUnZhS)PGV&zV(F1}S?By4(UhK@#>=PRyo0@=s0vpfyJkWiF zbLCe1QEPN@xgC%If*IN4C5CG44>DLc#$WM_uNn0l=D2^ZU9ytt>}&K>(6~-cp9Stc zct_P4+W8p`%trej7z8EG)wYjh8H_u!2F@OR*X8^pw{6&bas<}w2GMlj@_P{8$9Q5ph!yL>?5l2oN6Sqk?$j&XeC+XJ-o~t@o?`b86YJ>rxBzUaGp6GDC|HU z!8+M}7>x^08z8Q$A_;);&o6;Qnd6@^xO>f6GO@^HAdH4Jdp{$Uqz7~=Rnesy;O#~X zgx}P4VC_b=Ll8s;fDFn+mfDPz;E{ogy-d)>w$ilp>?ZYapg$S3-LcA`HsVr-N7kzW z;iy!IZP_nOYpV64;*U^FJ~+9Kf0995yw34ow`l0JY z_2KT52Ynnsj>U|)Oec^vH$4mQ1dI!!FvsS|aefG!Weh+0ef2enG0J9If2<#%-p_E0 z*0Sw6uu{43$V1Rdw5Jei(<}Kow22o<|ze3yaWlM zz>`R{G?(X$K?@wxHU1=d5sr1sd8ua)EMSgziw^WW+53PZR^n(Ee84f`iM>R0jjB-h zW=yROSoKOrXS{xyj{vkOoTqZD7^C;%$r*ia^xw8nQ755Q~-i?I~1&ILjd~#}}}nofrEf_7gKw?KEQX$+<2 zlKjkuvtN^+njU##&UeeVPJpD((wB1AV=uQl$#J{`B5&(3BkiB;QPQJ^F=fcIvQzj^ z_-rw5T%RKPQR(rD${#=nP;asJ_FjVl*@4U0P&12$1X&6;m@rsU=TBCO}>q>mm{mS`7QC+Q4J76j|uVHGyc`Lt_%fnjbm zjUN8It~#4kO|)yUKx^*H#poJb>C%+j;69^|>`fN>?UW}Q{)*~a3^j;AnJ6O$RJ5c2 zSRPwKreTD-qwe{;P7CDLLVk7SXMopI4ec1#!4u%gJcn;;#O3<<4#v4cS3>&=A9Z!m z@Bb83!Z0As((S1h)`I7;RY!Iq4jf)|>0&O$Sb~&N>E~5VG>tVEhVoeBC-Ozu!$0n~ zcflMssnzJ}Mjg8O8>k-#4B4|YvCBciyH3X!zAoD4u9S*R{iu}doC5{*cLJAI%i z0_{)ioB!afJ1p&jg>?Q;-57CmwvQ|9>t<{0A9M!gAW%O)lgX8f#AR@`(G96E-ho(U%Q-DHpI;q+>oCibAHw z(6<_lczYwF;FiMvZa%*mNq^iFasrx{#f}A#Ql)@fGi5a z^qXk1rC=V9=1if<^;i0B!3~kY2V=`Aqu2e_nfb1_dKZeinef?I>eyD7xEYa@598Mv z5A(;e^L@3~ws_6>b4;O{-8HT_nd#u$BAxw%AA3FVgy2FWzqlOD{7u zvufH0Q-eOA_~B9Wr2RaBmQ7ndxT zbw^zSokr6pKB`T4Dn0=jG8e}?Q-VE5&Gbr_DjePenQADwOK;)hjhY@s&ZxF5W6=f0 z2#2>^15wcnMVEx|&k^f*lsCqZqmRPB1!!)j&L@$p=%KLaNZJnwEqXPNeo%tZ2qn~R zD>8}F>Pu@B>LjdRzbqX<&tj25@C|xuBC~NpY`T;ijM7=hSy3B(z`D=+dC?E;XHk1| zGO2uEL9KCMVx8J4T}d4)$U|w!8jc3sI<-ZL3ir_q8ta4TevxnP*&0QocL;pQ5Mv4= zr1?yQx2uJ6%`i+f8)ljt?!1^-Iwza8yUY)gY0KXwJxvO5=qTz5dk6N-pFn-zG#UKt zm^ry#@ngF@6i*d~{h(F%O^EoQ>XxM~0Hu83@Uj%Kpg&=}&!UQ)U>^$?(cF{1-srOX zyS~geK>q@UE`zldBp2W1*Rd*bQl!yIw_2S1ai&mS|4G1?wOiP{0Igb505z7a`-Je8 z`!r_gq#<**QEr@a$ONdk5Uz61mC+Z86;s%xyNn1Ar>}_km0yeOKCZ=&FH_+^c?-jo zzb1ms74_FFB2`d~S`vjWAQ=brFf(`?DTOg_tI5oYhnCAF<+$!lyNIoyhq#^;G!<$ybbK(ItFRa6}lsf~jm%v|)0cjeWz zzXWSGRgu+-w0V$Gp8jO+M`IXWaHU#>d!(R8%+J!+SUH7K6LMMp*}AW9v}^d}ahq2! zJ2hpyUfV>OQKpHe(8ITrqWrvuL9kSsnoIfq2#n@PL}yKB)NSNfFFIYGkKwkBnRsYB z)oMd_mtT9H9VIldYcW_m^{8ESyv4LP>rkT#u5(Vm3qIglG~iqw>*~O5qh#kc z;_FP7Zth&Pl5M{KioZ4^4SE&dy#n__okEPgWhasJcJH z>zcN)D00NBx1wL72p)*RF8{(V8*D=$^;l3 zh}2&$kI;MbRn1W6FTi~A=z2j4-lA*(yRR%Ai9eXkkt23 zq$_jq{_d>%c28kU{&5e%d9zm`vcZ(Mwv(btq6+@#^J&m9v`4$H_-Um=_NU{TqEpC` zq6Er7u)b_wN&YNDb0g<(mFLO5jC}}Wxc5ozi`2)%g-bTa?>@L*3|*t0ZibYFrE@Hj zL_Fj`E=ocVt~2!dwQ-lM5jT>yguJ9#PM+>bd93*oLrMR_B_936nX>I}Ge)4X8QwmH zGmg7OE+xwz@u;lLSVfDip#%XfE z12onHWDH;OL+1~*T^uH5s$%GJDGZ_sOBzGUy@J&96EK8lTmpv3G+aD_Zdl492hqW2 zP;z-umd_w)W)5QpL2JdH?WPA=w&X(@DWx@(Ups{Kym>-{?wI`8A2)YSK2k66)EC)C zunsx`J&bL?HWoLkEAX_*+MLjq72cUq4$ynBOV&S9J1sud1f8#D%y~N7M8jm0iNGhe zgL9*!(_slO=Ff(>G>z`+^b@*9OoJOhYtI%PO=>|t(AVW zk6UES+1t4a+terF*BaK~BgjEfZ$-;h_R5-gSx0-wV=5xNe_kywwTQElXF4Lzo{!t(O~rQfo0=FHIF)u#;Yd{qPzT_B(2gP% zRLEL}51qKu)>ypo#6XmB4yU!gk5HAK=tt49i!GX4k1skbzLPS;=MWTg_!OmhNm}`{ z9bvd+G*83#Gi=`5qtpFJ?w)|hH!ysDMbng!(4bx`{=y`AdadIN?*IAL0X>t1OCL-j zrT=%Hn@KhlF2e_JI{Gwk4Z5;jTf zx?e*YFFZ@28^;%Pg8SW#KWgjoj?5D0s&$e>GAH|2yepI4l(g7KjW_w==}KUD&_oVM zAG@CG2`%(26RjGqORE%Ic8-(^_+>7|8w~hFTt6&}M-2BTRF;cSA3=Tm2u6giX8yJ* z*b$f$7HXchB}Jk7`}}>HGO}#yQLK%9@>|J{?m1o5EzR%ZPnSgIohXlyX3Wn&Jy8DB zwx2*lJQ|2~y#2076uc>HreFj83Zg)(BZfl^r0gC5fTFj`?vv)Ra(i70v-Za;xC!Ab zP44nQ%)&uElXW4|GU)*!yM><`@b$31C z6li24XY}&&n(xMj49(cE%{^nMpgSotv_owz4-IE1d=68p#Rm!ItaouT&I%byMZ)}w zj>Qi=B#2?%`c{>AyyK&nMb-U0I#;I_PQoCt-!S{#dilrYMpc*e5o@Usx@l1K2O|)z+Ylz_WV+CF zWB|gZTQ>{B`-5t}hGn4VpLyWDAggCe9RboA_9Ga|av@AbBJLaXP?c1uK%-1eX)sxG(&{VS`~ro=v?(15l`9Wv`6|zy z^0fk7U?v_v>>>QqRVF+fI=830aeXbNopj!k0PM-28K(KG9ycziXpSmxMSW9Y!%MCo zeho}7t)Y3_Xy**O7$IqK-S^eNfiURHf_x(3zo(P-z0H_}?8(>SGUb5!qjh3Z&-v*1 zCj{Gna#2nO;Hjj#(hYU~?Q@Q?E6*p+jVv~%SFcTi5j|;%ygqen*29TEyryq86=~5sY)NQE>-QL zHzf^^Y-7L_Pp~Po{e;$OKO8jtN;oHs7)qL$5L&|9n1ntlhzs5y-+a%2^d>hoA_$KG zF(nmH6x?lFS`fbUQ98XHHi<{^h2G!3%SH?kA2PTQ0h=wbZ zF&qn$W%}CU?rv3AXb?mE>-ISXZqA90e2fz9e0Pi6p@H5#T0=#g7}|Jn?` zm?k1gyjA!;;uOFJXWJjCd@d1My6z5Uev)l3i05HknN(i6TkZF{&ylK$m`h4ktoY0X zXTVfB*|_Cb8_gBT6D-y#5;GKsbpfrM-{PA;qbS1(G_Df=CpX~K>8w&SMA|Ji6HC_q zF{O?%Hew})cxxf|104j^#WEvmD?=ikm#2~>?87A^lqd^h^n?LBA@*cEC815bMm$b= zxn5IxgS|doV^u2R4?z2iGxXhsCkaGY1;b{`f|a(SwuN^5=o)aW%_&z@#d0>#t&F77 z?<@~{x`)M}&!o^U$Jtyt7wPBQ;*XS6iX(x8pw&O|s!~B+6)M%<-=)B5uXbA@GoIEfwZWI~p%H3zre0Q0|qgsc$8ZQ)_kw)QRQCr$KpGpeM~&w~xhJUNH^Cn%+gP zJi;wN*hgnW-NR-ra*v&o@)Dt=7biy$+W;@=7p#Cd8~$i9Tt}zs;Yg*0M%A7l57@5l zR{Tk`Iu&M!LlB?A&n8G9U)tx_zv}naVX}lPQ<}1YNP$z&A#C!RvtDdz8h24cw~r)Y zY_csh=hFme7o5RtGC*(G{knlvXPwz zz_sD?e#U^@x3Saq1|KFdYxSi@1bJ(`T4`JB5SW2KRR3YcTq1rwv-gYi=-lU{4-5y? zV7OO(z#Sg>qhBIOVY~drnyb0S9fJU7244qMxW!~FC!_5phZ0R5@p;tV? zHe5jpxBKQO`mbgV?PjBv5IS;}9e}V$hA2LN2J501Dq5W5SlRmtHY3JQE~3V=A*Xj7 zf;y05PWM&c<7>`dgSqscV>-8xiK4o>&UM2%Z}1Zk*&Pkuv5$&W16^rO_KK`q6F} z0+YEnv?Qv-&Z$C0tbEyyhiZp-i`fp{Viz9&>n=b<= zSf8W10y)W&1U2#Gi#!%L>pYz#Hx4ZAmoRUOPqazbTuaztzY-TLz!j;>&;0_H$PcfJ zlG>ULkcaNR(CDAVMcTa5Cf94NTU$MN!Iz>OJ{I_BH60IsC*S0bmN7G;;)bFc5Ae-U zaUYz2T(y~x13!q-$MyNjQC6hN+lr9m@dS2)69zZC;Zy=KW5DlqF&bcXQYA55E zC@S`6Eb9sNT`>w+wgmA^Acde>MbS!7lx<5MaWuT-Uw_6e9N|`Cg&UWy7TPA#o@Ycc z@Kw=q&T-D;RI7|MD$KwRUcvm#(Eqgs-1nZqfJ@H#lW20Pa&L-_c60wu>%gs7He~wx zAeD#NK2SC!_J#)ZSz~`tESw*mzH^56ek-bymc;D7Tqw+l3Jsk8{g+^Bpj*N`p`|Ac z9rup;WHk*GH4|^Ys%IYR*&xs%3ex6E+TMQhC0Z zDEts!7t=pq>qcjk+(>!AI{l^r9g8_tR)eWxjMMDnh~q}(eWBtQwQ?N3vkTLw7;ET; zP~itXWCt`Ye|n}Jo?%+sX%CGNto1L=oy6yNv7swP4WxIYizOvx2R#@r?O*d zAk7JG7g^Qd!334g4A!A8RlyFIihV`7vXNw?7*BwZjh*}mD1Mj!fX^x)%WYTugJSUT zNNWn_wko#)4n{s@7vDWJHoBYvFWR`hOYg}dBHzRsda#9G4o^Xilf!F{+YN!M2!7W)IMlOD z{|%iXr^$xpGvR*rXWL>w-i<+)E)~TRB-8w_4;DmL%dyc$G+si8{&;Q+ZQqJ1x3GO| z4BW#+b@l|$7R&lIaVKN|G|JiLA|!!)BeIUOcTLg!O;tY39=$8Z8U%HH!mF~ox6|2W z29QZjtta37l0?S|!&twBkL!aN*0q0!YK>l@0%=>fz$GSk3J6?@6A^SEM}(I@28(Y; zV`^nhwQJ)vYtv_MG6tz0ur%1k%IEbhf~BNM7j9%-pNV=&z`7)25Btz>8-Xf4QNRs# zX8nisyRsa#CF7- zSA_ruqThWWY+M`C<-N9mERU2ebZ0Lr3iwwV;d4(`3r};7Ly-s!p&_;yCV$c|C8l zpov%soeI5*XsGAGQMux33a+*aNe(MvIB2Crkx)h2zZzH!1;eJ=l!#=@9IA>maK*e_fP&PFH>|@?AFb{n92=!E_#agr)nt-Cmk@&^~nw7+$Dvq zxy=_5b80%TFv-n3ieA8+bhw%arezSSPs4+hWl1Udj5+(USV_(RyT*F0@($vrorSVn z!mpvbNDO^3OXuiT;>PR|fpZW=rpsb?F_a1fhlse>*#=6Qa0`AHA!3$2iBv-!-h~pJ z0iE4uMquj5p2HQa=3C-qQ|Oy9*3^B_LYCETI#D&lj*fVxreU)rrRRK3@sYt1ssZyb zKNXoEBBPWK%dv=U*hZ8A&p`7ee|TE4D#xwuEliK0ry54MNEv~pQXtm*c@PctD4K%P zVV8D*0U$aqqcTmA2t+thf1*m&WI4EW8cI0xvh#VI3kSlT$DL;A$){AJd2mbSDwYI5 zgNf(A^V`G?b>=!pd~z{Gr>eG)keE4_t!bgiV5A4`=0Q$?x8g1Ge2p|E@O)~_$p}>3 zd7cByMzccDn{}d<#ecN|c>%W@B zcmL1$-yZzCFc|!M^n-zki7_7N-wXr{0?7MMoRNVc59q%c^ML=8@yBW&$bT5by$I}o KG~dbo-TohK=4(0t literal 0 HcmV?d00001 diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 7612ff47..463d8cd9 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -156,6 +156,7 @@ "FilenamePreviewVideoTitle": "Video Title", "FilenamePreviewAudioTitle": "Audio Title", "FilenamePreviewAudioAuthor": "Audio Author", - "UrgentFilenameUpdate": "customizable file names!" + "UrgentFilenameUpdate": "customizable file names!", + "UrgentTwitterPatch": "fixes and easier downloads" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 81ccb8cd..9e9bbe0c 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -158,6 +158,7 @@ "FilenamePreviewVideoTitle": "Название Видео", "FilenamePreviewAudioTitle": "Название Аудио", "FilenamePreviewAudioAuthor": "Автор Аудио", - "UrgentFilenameUpdate": "изменяемые названия файлов!" + "UrgentFilenameUpdate": "изменяемые названия файлов!", + "UrgentTwitterPatch": "фиксы и удобное скачивание" } } diff --git a/src/modules/changelog/changelog.json b/src/modules/changelog/changelog.json index 3455405b..b9b1fbdc 100644 --- a/src/modules/changelog/changelog.json +++ b/src/modules/changelog/changelog.json @@ -1,5 +1,16 @@ { "current": { + "version": "7.7", + "date": "December 2, 2023", + "title": "bugfixes and better downloads!", + "banner": { + "file": "meowthpolishegg.webp", + "width": 851, + "height": 640 + }, + "content": "this update fixes various issues with supported services. no new features yet, but twitter fix is surely something good to have in the meantime!\n\nservice improvements:\n*; broken twitter videos are now automatically fixed by cobalt.\n*; all vimeo videos and audios should now possible to download.\n*; vimeo: fixed short resolution displayed in \"basic\" and \"pretty\" filename styles.\n\ninterface improvements:\n*; streamables are now easier to save on ios.\n\ninternal improvements:\n*; port env variable is now not strictly necessary for cobalt to run.\n*; minor clean up.\n\nchanges since 7.6:\n*; fix for an issue related to youtube dubs.\n*; fixed a memory leak related to live renders.\n*; handling all errors related to twitter downloads.\n*; fixed support for reddit links in various languages.\n*; added rich filenames support for twitch clips.\n*; updated support and donation lists.\n\nstay tuned for future updates and have a great day :D" + }, + "history": [{ "version": "7.6", "date": "October 15, 2023", "title": "customizable file names, instagram stories, and first cobalt sponsor!", @@ -9,8 +20,7 @@ "height": 640 }, "content": "as many have (very) often requested, cobalt now lets you pick between several file name format styles!\ngo to settings > other and change it to whichever you like! there's a preview of each style, so you know how exactly files are gonna look like.\n\nif you liked file names the way they were before, don't worry: classic style is still the default :)\n\non a different but not any less important note: cobalt is now sponsored by royalehosting.net!\noverall service performance and stability is gonna be better, but also more content will be possible to download thanks to geniuine server locations. and yes, still no ads or trackers.\n\nthis update also includes a bunch of other changes, check them out:\n\nservice improvements:\n*; added support for instagram stories thanks to #194.\n*; fixed reddit support thanks to #221.\n*; added support for rich file names for youtube, vimeo, soundcloud, rutube, and vk.\n*; numbers and emoji no longer disappear from file name and metadata.\n*; mute and audio dub file name tags don't appear together anymore.\n*; youtube: dub file name tag doesn't appear anymore if audio track is default.\n\ninterface improvements:\n*; added a list of sponsors to about tab. if you host an instance, it's disabled by default, but can be enabled with showSponsors env variable.\n*; about button now opens about tab when no new changelog is available.\n*; fixed download button thickness on ios.\n\nyou now can reach out to cobalt via email for support! it's located in the about tab along with other socials, such as discord.\n\ni hope you enjoy this long-awaited update and have a blissful day :D" - }, - "history": [{ + }, { "version": "7.5", "date": "September 16, 2023", "title": "support for twitch clips and rutube!", diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index b572a8b7..e782e25b 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -562,8 +562,8 @@ export default function(obj) {