From b925d27d673371d64a9f199ec9dabbf37e580499 Mon Sep 17 00:00:00 2001 From: Himan <43524404+yerrak00@users.noreply.github.com> Date: Mon, 25 Mar 2024 20:51:54 -0700 Subject: [PATCH 01/99] Add share_target support to PWA manifest Signed-off-by: Himan <43524404+yerrak00@users.noreply.github.com> --- src/front/manifest.webmanifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/manifest.webmanifest b/src/front/manifest.webmanifest index 8cd571e5..68269f26 100644 --- a/src/front/manifest.webmanifest +++ b/src/front/manifest.webmanifest @@ -1 +1 @@ -{"name":"cobalt","short_name":"cobalt","start_url":"/","icons":[{"src":"/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"},{"src":"/icons/generic.png","sizes":"512x512","type":"image/png","purpose":"any"},{"src":"/icons/maskable/x48.png","sizes":"48x48","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x72.png","sizes":"72x72","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x96.png","sizes":"96x96","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x128.png","sizes":"128x128","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x192.png","sizes":"192x192","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x384.png","sizes":"384x384","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x1280.png","sizes":"1280x1280","type":"image/png","purpose":"maskable"}],"theme_color":"#000000","background_color":"#000000","display":"standalone"} \ No newline at end of file +{"name":"cobalt","short_name":"cobalt","start_url":"/","icons":[{"src":"/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"},{"src":"/icons/generic.png","sizes":"512x512","type":"image/png","purpose":"any"},{"src":"/icons/maskable/x48.png","sizes":"48x48","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x72.png","sizes":"72x72","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x96.png","sizes":"96x96","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x128.png","sizes":"128x128","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x192.png","sizes":"192x192","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x384.png","sizes":"384x384","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x1280.png","sizes":"1280x1280","type":"image/png","purpose":"maskable"}],"share_target":{"action":"/","params":{"text":"u","url":"u"}},"theme_color":"#000000","background_color":"#000000","display":"standalone"} From 17fdae23c96b1d007394f29721ba8aa4e6969fdd Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 26 Mar 2024 13:15:25 +0600 Subject: [PATCH 02/99] front: make webmanifest readable Signed-off-by: wukko --- src/front/manifest.webmanifest | 82 +++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/front/manifest.webmanifest b/src/front/manifest.webmanifest index 68269f26..68b64c31 100644 --- a/src/front/manifest.webmanifest +++ b/src/front/manifest.webmanifest @@ -1 +1,81 @@ -{"name":"cobalt","short_name":"cobalt","start_url":"/","icons":[{"src":"/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"},{"src":"/icons/generic.png","sizes":"512x512","type":"image/png","purpose":"any"},{"src":"/icons/maskable/x48.png","sizes":"48x48","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x72.png","sizes":"72x72","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x96.png","sizes":"96x96","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x128.png","sizes":"128x128","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x192.png","sizes":"192x192","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x384.png","sizes":"384x384","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"},{"src":"/icons/maskable/x1280.png","sizes":"1280x1280","type":"image/png","purpose":"maskable"}],"share_target":{"action":"/","params":{"text":"u","url":"u"}},"theme_color":"#000000","background_color":"#000000","display":"standalone"} +{ + "name": "cobalt", + "short_name": "cobalt", + "start_url": "/", + "icons": [ + { + "src": "/icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/icons/generic.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/maskable/x48.png", + "sizes": "48x48", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/x1280.png", + "sizes": "1280x1280", + "type": "image/png", + "purpose": "maskable" + } + ], + "share_target": { + "action": "/", + "params": { + "text": "u", + "url": "u" + } + }, + "theme_color": "#000000", + "background_color": "#000000", + "display": "standalone" +} From 7799fe4661c246e273dc3c1a73318e451d340e86 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 16 Apr 2024 21:51:03 +0600 Subject: [PATCH 03/99] instagram: update api endpoint, add dtsg token parsing & caching --- src/modules/processing/services/instagram.js | 96 +++++++++++++------- 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index 8d8a11d7..41775c36 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -3,28 +3,54 @@ import { genericUserAgent } from "../../config.js"; import { getCookie, updateCookie } from "../cookie/manager.js"; const commonInstagramHeaders = { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'User-Agent': genericUserAgent, - 'X-Ig-App-Id': '936619743392459', - 'X-Asbd-Id': '129477', - 'x-requested-with': 'XMLHttpRequest', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', + 'sec-gpc': '1', 'Sec-Fetch-Site': 'same-origin', - 'upgrade-insecure-requests': '1', - 'accept-encoding': 'gzip, deflate, br', - 'accept-language': 'en-US,en;q=0.9,en;q=0.8', } -async function request(url, cookie) { +const cachedDtsg = { + value: '', + expiry: 0 +} + +async function findDtsgId(cookie) { + try { + if (cachedDtsg.expiry > Date.now()) return cachedDtsg.value; + + const data = await fetch('https://www.instagram.com/', { + headers: { + ...commonInstagramHeaders, + cookie + } + }).then(r => r.text()); + + const token = data.match(/"dtsg":{"token":"(.*?)"/)[1]; + + cachedDtsg.value = token; + cachedDtsg.expiry = Date.now() + 86400000; + + if (token) return token; + return false; + } + catch {} +} + +async function request(url, cookie, method = 'GET', requestData) { + let headers = { + ...commonInstagramHeaders, + 'x-ig-www-claim': cookie?._wwwClaim || '0', + 'x-csrftoken': cookie?.values()?.csrftoken, + cookie + } + if (method === 'POST') { + headers['content-type'] = 'application/x-www-form-urlencoded'; + } + const data = await fetch(url, { - headers: { - ...commonInstagramHeaders, - 'x-ig-www-claim': cookie?._wwwClaim || '0', - 'x-csrftoken': cookie?.values()?.csrftoken, - cookie - } - }) + method, + headers, + body: requestData && new URLSearchParams(requestData), + }); if (data.headers.get('X-Ig-Set-Www-Claim') && cookie) cookie._wwwClaim = data.headers.get('X-Ig-Set-Www-Claim'); @@ -37,24 +63,32 @@ async function getPost(id) { let data; try { const cookie = getCookie('instagram'); + let dtsgId; + + if (cookie) { + dtsgId = await findDtsgId(cookie); + } const url = new URL('https://www.instagram.com/graphql/query/'); - url.searchParams.set('query_hash', 'b3055c01b4b222b8a47dc12b090e4e64') - url.searchParams.set('variables', JSON.stringify({ - child_comment_count: 3, - fetch_comment_count: 40, - has_threaded_comments: true, - parent_comment_count: 24, - shortcode: id - })) - data = (await request(url, cookie)).data; + const requestData = { + jazoest: '26297', + variables: JSON.stringify({ + shortcode: id, + __relay_internal__pv__PolarisShareMenurelayprovider: false + }), + doc_id: '24852649951017035' + }; + if (dtsgId) { + requestData.fb_dtsg = dtsgId; + } + data = (await request(url, cookie, 'POST', requestData)).data; } catch {} if (!data) return { error: 'ErrorCouldntFetch' }; - const sidecar = data?.shortcode_media?.edge_sidecar_to_children; + const sidecar = data?.xdt_shortcode_media?.edge_sidecar_to_children; if (sidecar) { const picker = sidecar.edges.filter(e => e.node?.display_url) .map(e => { @@ -75,15 +109,15 @@ async function getPost(id) { }); if (picker.length) return { picker } - } else if (data?.shortcode_media?.video_url) { + } else if (data?.xdt_shortcode_media?.video_url) { return { - urls: data.shortcode_media.video_url, + urls: data.xdt_shortcode_media.video_url, filename: `instagram_${id}.mp4`, audioFilename: `instagram_${id}_audio` } - } else if (data?.shortcode_media?.display_url) { + } else if (data?.xdt_shortcode_media?.display_url) { return { - urls: data.shortcode_media.display_url, + urls: data.xdt_shortcode_media.display_url, isPhoto: true } } From 8f5642763bf552b9ad94ef0567e0f9de7fb24b34 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 16 Apr 2024 21:51:27 +0600 Subject: [PATCH 04/99] soundcloud: let -> const cachedID --- src/modules/processing/services/soundcloud.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js index 46aae5df..37baee54 100644 --- a/src/modules/processing/services/soundcloud.js +++ b/src/modules/processing/services/soundcloud.js @@ -1,7 +1,10 @@ import { maxVideoDuration } from "../../config.js"; import { cleanString } from "../../sub/utils.js"; -let cachedID = {}; +const cachedID = { + version: '', + id: '' +}; async function findClientID() { try { From 9d239525e1efb56c70c3b201e27aeab34241cbb3 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 16 Apr 2024 22:04:38 +0600 Subject: [PATCH 05/99] instagram: lower expiry frame by 10 seconds --- src/modules/processing/services/instagram.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index 41775c36..ddb1c2a2 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -27,7 +27,7 @@ async function findDtsgId(cookie) { const token = data.match(/"dtsg":{"token":"(.*?)"/)[1]; cachedDtsg.value = token; - cachedDtsg.expiry = Date.now() + 86400000; + cachedDtsg.expiry = Date.now() + 86390000; if (token) return token; return false; From 3389c595dfbfea4d0cf2a6d2f040894ef146f2ff Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 16 Apr 2024 23:51:35 +0600 Subject: [PATCH 06/99] instagram: update stories endpoint and parsing --- src/modules/processing/services/instagram.js | 30 +++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index ddb1c2a2..a5b20d72 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -3,9 +3,10 @@ import { genericUserAgent } from "../../config.js"; import { getCookie, updateCookie } from "../cookie/manager.js"; const commonInstagramHeaders = { - 'User-Agent': genericUserAgent, + 'user-agent': genericUserAgent, 'sec-gpc': '1', - 'Sec-Fetch-Site': 'same-origin', + 'sec-fetch-site': 'same-origin', + 'x-ig-app-id': '936619743392459' } const cachedDtsg = { @@ -137,22 +138,31 @@ async function usernameToId(username, cookie) { async function getStory(username, id) { const cookie = getCookie('instagram'); - if (!cookie) return { error: 'ErrorUnsupported' } + if (!cookie) return { error: 'ErrorUnsupported' }; const userId = await usernameToId(username, cookie); - if (!userId) return { error: 'ErrorEmptyDownload' } + if (!userId) return { error: 'ErrorEmptyDownload' }; + + const dtsgId = await findDtsgId(cookie); - const url = new URL('https://www.instagram.com/api/v1/feed/reels_media/'); - url.searchParams.set('reel_ids', userId); - url.searchParams.set('media_id', id); + const url = new URL('https://www.instagram.com/api/graphql/'); + const requestData = { + fb_dtsg: dtsgId, + jazoest: '26438', + variables: JSON.stringify({ + reel_ids_arr : [ userId ], + }), + server_timestamps: true, + doc_id: '25317500907894419' + }; let media; try { - const data = await request(url, cookie); - media = data?.reels_media?.find(m => m.id === userId); + const data = (await request(url, cookie, 'POST', requestData)); + media = data?.data?.xdt_api__v1__feed__reels_media?.reels_media?.find(m => m.id === userId); } catch {} - const item = media.items[media.media_ids.indexOf(id)]; + const item = media.items.filter(m => m.pk === id)[0]; if (!item) return { error: 'ErrorEmptyDownload' }; if (item.video_versions) { From 55a93629a6bba39d16ec7462e273b4016aee550b Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 17 Apr 2024 00:00:29 +0600 Subject: [PATCH 07/99] instagram: replace filter with find for story media filtering --- src/modules/processing/services/instagram.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index a5b20d72..5c696709 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -162,7 +162,7 @@ async function getStory(username, id) { media = data?.data?.xdt_api__v1__feed__reels_media?.reels_media?.find(m => m.id === userId); } catch {} - const item = media.items.filter(m => m.pk === id)[0]; + const item = media.items.find(m => m.pk === id); if (!item) return { error: 'ErrorEmptyDownload' }; if (item.video_versions) { From d4be75d404ba54c75ddc4a16dde5a199b18bdea2 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 17 Apr 2024 00:08:29 +0600 Subject: [PATCH 08/99] front: remove maskable icons --- src/front/icons/maskable/x128.png | Bin 2648 -> 0 bytes src/front/icons/maskable/x1280.png | Bin 39521 -> 0 bytes src/front/icons/maskable/x192.png | Bin 3571 -> 0 bytes src/front/icons/maskable/x384.png | Bin 8751 -> 0 bytes src/front/icons/maskable/x48.png | Bin 854 -> 0 bytes src/front/icons/maskable/x512.png | Bin 14589 -> 0 bytes src/front/icons/maskable/x72.png | Bin 1508 -> 0 bytes src/front/icons/maskable/x96.png | Bin 1752 -> 0 bytes src/front/manifest.webmanifest | 48 ----------------------------- 9 files changed, 48 deletions(-) delete mode 100644 src/front/icons/maskable/x128.png delete mode 100644 src/front/icons/maskable/x1280.png delete mode 100644 src/front/icons/maskable/x192.png delete mode 100644 src/front/icons/maskable/x384.png delete mode 100644 src/front/icons/maskable/x48.png delete mode 100644 src/front/icons/maskable/x512.png delete mode 100644 src/front/icons/maskable/x72.png delete mode 100644 src/front/icons/maskable/x96.png diff --git a/src/front/icons/maskable/x128.png b/src/front/icons/maskable/x128.png deleted file mode 100644 index 49a3370e2776e0c929043184dc32c304dd0462b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2648 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 diff --git a/src/front/icons/maskable/x1280.png b/src/front/icons/maskable/x1280.png deleted file mode 100644 index 7aff5da7ad9b7be1f0a54472344a8363bdcaac81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39521 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 diff --git a/src/front/icons/maskable/x384.png b/src/front/icons/maskable/x384.png deleted file mode 100644 index e63b50d551bb4b0622f9911edce6f8c8a4b1ef92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8751 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 diff --git a/src/front/icons/maskable/x512.png b/src/front/icons/maskable/x512.png deleted file mode 100644 index a58bb230bfe99302e25b7690453bb9e3ada96f34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14589 zcmeHuS6EY9)a@n|QHnvNNLP-43PMzh6bYiDR6VGGlps+MDG}*4AWD%E&p{DEilU+d zLZnMqigW~|Ly*uy51}Qb+!fD%zwh41`*dG?0rpPz-fPdb<``p+xrn`NZX~crVh;pC z0+%kH`x}B_;7=ID%L9I`2lUavFAnd&jm|=!I;BVuBnMqOXK*#ZVQFl4vEuna&508d z2C#$Tzqq)gdByGIQXq-Oi-#&NUgn*Hj1Nh4M9A&C$D#L`KOP3F4?lF`^GRjxeLQe+ z(E~QH#0Rj?t$th=46X|q&^v9Zo+n<{DR1@Heq11V-@kfd>-q*|v%7Zryb9yU$_>)70S9ooC)`|+7>xY^eC{$5nk$?$fARZ!ywIoLXXUzIg+tvp zf25!IeF%r_IN8@BvkW;nvcA36x&8Y#a!@%B`)oKe8U{l~pZ9P5{Us!HPk?^4rwcYu~hhd2n%=npRp^+ zK|a<#Ii|l4xuAjHBmFb*KN0`4RM=_wCprJ1${!s6LqPw(nbS0-ZqKp2C(AWE%e!|N zVdSs7l`E$HrS6gqUH|cDhdb;s6k)*23q>lS(-pm@pIN;#IbQLXv`@Ez>+)!Yuixiy z4j$DT{jSl{eiwt6N~}^(x|m19fG&w(Bit@wg;k;~OUo?v)DgyDXY&C_Ikm7^L1o`+ezYz~oE{btidm*o= zXJY`B={7Bgblir)>Q>!!Kc9T53B~(PzKWJHE^_WLZkEM{Xzwf*>G>vRX;XN@@iRmc zYnT4E0gX0Ln_;d?Zt!WA_K(kUN&O3%^(Gv;{C8zhX&4IPeUOtGrmaU`{YL6BQ8lYv zDz*JWok`P4Iwp&1clB`R&Or-IHTM3;9%)2(%N|j3#t$0jIlmG4)JnkT&T} zQqS~rRK}!01c>NOFr(kzqo27TYB?P9DCBi)qlnd;11Fu|2%Brw=R^u${mH!>LUX7n z>~Zk~gb#<4w-GDlo4nx+-AE}v@C}AbM+q%Aqc`5(Pd!a1I``+jaJ(xA`!Z#h_?3l*%y2j=i}BiM#3b`<0X4rQNrm zbQ2Wjc4MaHVyeL!s=gY;HEW{{wFf^YlR!$#q(t+CZ5Cx#{A!Op`52@?$mllu;zT&7 z;P*Ja^J@3ta%vJB9FC`!?TfWrAvGQ;$szhImg$nAB;GcZs+7{B#93!;zg){=R&5zp z32cvV)jZdfybmII9fV_H?3$Z8S2%=AGl@6Myx)2xuwZ9!;@xa^6r?sIo5YD;3b!`> zJ}jt*de#}bAEcO4pN?x$G6Ww*h{-)bj@~=7@xkIPrAxnd#D=8K9sb@dq=np@$vhQG zZ9|!7?u^c^vHZXg$>7okN&k_vW*m;=hZ>Sg@h#nbA1o>J52_Ok2SCc^oAnXFbw1!QAD+9@MV?hmRMMSFOLN(|r~a@5On|1}|4AN(aroI?c-;_}cLphuqzh zrKN_Z`{S3Qi6(-(IABD1IVc{NO;`)&8sBcxe0Rk1-J6pB8^=^i`u-;EWlrq<=>J&K zJOr`&qANtfK8!jYX;-n_#a!&_0f~~TV3F~1vbJ{ayz0lA<3H*HI19VZ>`dKEv!?!V zI&1dSO!1f~96EZu6aJ-i&?)Y%e+?zwb!Vx}0uwwk(gP53tiiRh!?`K`0M&LQ_tTf7 zjs6RUI{nt@g!PEMA#xs{TNFsw3WBZ${o+GeGmbQA3*y5sD<6{*<_;GB{GmokdoT|a zx9+52(l(#N>Vx&Q=e4@k)DBE%T(^7ij|*m_?q|0F6LiG!HWnh?eRYa+Ivd4My7|?c z!>(~ErQdI6Jqs5FQG}gE=uZ^)noHh7y3JzBGfDwA6yK=e*^xGwuwboQctbIIL@Qg5 zlKrf_O?ibqO^U7r`iv%J^PA9~C_iOxa*R0Gk=~Ifm$+n(D?S)9Li0udRQ9MBo zBRB61WK)iSFhBL`Z#~JCY~%c*7y!c?EcH3|l4>Z)q&E&gokF~mb5Ne_JwOPvmm&>E zeAS{j`cYVh?yf^oyvcU=TNLK^VVozuI|tpBM1$8Fk;1ooTZGuteH^*c(QF$oE*fLi zP39W@dak~F$j+-#gye?Wxe+#B1<+-eRa1bauzGCl1H=mojyZ=*)oLExK9VewsK-;Y zGqxh=`Tbq&p}AWpf%ZE2R>IQ=EkzT+1Q^!*axWY63#>u%8R_2c%+S+WLBMy$n$_yN z6Gg1-5FTGh?<$UCf_l?Z#U*0GMQ!&se~J|1aZ29>7FO*sU@9E_C=}3Q;_&ZvhrsFe zSc{NDI71_~_YCn7G)TUX92pExlwX6(1J&T6ww~Z|N=B6_hJUE|o-TDNiL_j~Vh$vP zwf5#9I$vq;Tt=4h&O8bxg zDSbS1d=ew)fb`+yYrh>PjH_?r>YGC|*EI1MY+(!0*?=7sngXQGXH z8zv2GG1b1bW-PsVT@y>uB;i=wIFnPA7d;vzBLe2*sT>GFX+qv!kJpZ-gYkhJ_|k7Y zlI~9w%u@T#A>&>}uxJIW)^9KZwbP&H^sSnb5`Ln~71jaHw)@8|vt9Uzz z_?1u9)JQhe=gZW;CU;jpGlf#r0B(N0!u|^#tZpn4kz7)nvX}~9Xr-Zy_AL7 zgU79f0Z4}-L;4S>-~Hv5SWZF(PCpAQcka)#3H+`Q#V5rTr7Q>WSA3CvX*+7yZ{w0) z~{#ljI;m{w#kGNwR#wu*qxjA+1TN$hM%na&A5?rQdhc;rs`k&y}2f@t#>EB zH#D6b1;Vk{vvR4wK>~($(`itQ#BUkqktgKT&Vl>HTwB_H$z0vbQu8Psat=hi8mM0O z?NGiMJYerXoz^krl9{e=Avr7=QT`XY?U%Lk5rz9r4?9I?JR9D0q)5WS{rbNlKJ*$m zDglDIe3YacW+Hdy(ui}K$J)7iYv=N3C5yxj?~n|?dTz0)uAfmFg3=hS@wMc#sr+cP zKtxKz&{rYU?uKE(lg{nmGyMgZ+!96aX5omcAo#T(2-g1~`S}_WW-?jSOmFy|Ums5T zNlB$W5e?>VfN2=&9Li!t;pwdE1^bn|PRkdnp2a>&cS5|fCL=T=ByJ!jhka*4GHihj z_=w*@E<+pb9OQn7LdIdMUU}M7PDZdN2S=UASx1?O0C~7KY?4xp-Hyh7-x)7cyRypMS?MBn1F3Gvh11&{oS2so z=0^XM^WKarBBk4AM@W;Aq9mRvkO2flq}1mR)jYJVMHnnRH?nR;C`Pf}s+RVX>K3sb zQV3Qk`nBUCV)a=;$YFa>?~zD3nKE{v#DxT22w!!N>w#!;Xkr-$i$P$empx28n0C+; zre4mMA1S;K(pLIxX%`S79-XKOBRAGHpV`D0H|#S}rmG%XPa^jn7#Yq8n2dELglqGe+CK3CZ6M0r?$Ficx~nn}@T zQPzL$#cFw!|B5217X+H!aK|?)%pDc4MM;93yJx1X8ZzcPAaK8=ncTaU)&x>Z9CM|% zfR7!5fv%Nc!p@qO58ttYM$>f+c4x$OxY(5%yz$C>za2V3eeH`N{#W-}n<|R2A#TC) zqUdk*S@x2fW%a*%yvkKQnp zsnhhP{-8{pYE3{5b>(y0agw1d?Xoba>v)=EIPq6ETyxTsCp!`%>&}5d3V&Ge_m_u{ z2(VGLo!IR~jFr5+WR!Q+|NFt{H>zReICjksG~w|X*M>tD2!DCzNd>_R_j0K*CDKmc z6*N8pjgYpMmU^&r?0p4K^|~1}_qM0ACCm(wAN{gv(dQ&e6VM!e63yMI zOKJB3=f%8^j$P#c0pcg>x;E7kIHwSG@2j}UdmU&0TQT+T~-`{OY@K5usurixM?mXez!XD_)XDVQA{kLLDJbI@-F5PLucFq$UJP}ix5ozR&~Qsx?#jZ@Zg zQSOPlYu~Gevm@&QfEXYiCT+n3qLA))h23};fqG*57`j4;XTi0|80rEx|Lrn*OQ>#c4nx>VBU?^s5aENc_h6bc<&&xBQapH6NH8 zD3&jK?FhI4%^Ubny11zLyl5G|&@#TH*(DtjV!0y$Lx@c@Nn+Y11=77cPe4tfBS#Z+gh*m46vmDq%E=Jq*l;H@ZL%>IOu z%;dj-i~~TMADub?{DO#p>?t|5T0o<8W2!fog0_^up9hC4Lx5G zx^Oq%_<1;V;V{9o5{9fS>7@vAgVn9Dhq+VB3J#lB4&{YvQMw?@kK*u^PFxSJNU^Ko z5l>6#PGwjS;UA=_c3e6SI`S2WkH=pHFS)}B)f1B{b+$lm6gSHRPYFah1^)WKuhPF4 zA#RnZ$OvH60t;`(7j_|#7h+}0@|{)zjv=X}Ak+gCyVX5WlAc;?T^j$_8*S8ai=slv z>t6<{k5?~wU`$p5yMlWp$5*5N(Ix9ARct|UP3GV)go5)0rMTOX#JB_rL5{4bqs2dW zab)EOk(TPXCwxN>xfS-;{$BX^4sy>6HMZjA_i_I_-}#nlz8HF%8B-Ez>P%`WVU;oNRq9MwA0G zit{xJm{8nKo7-$Y@f#1E4N0LEeClFkcd=;BKlezuF!}=SnuXUj&2WtYFC$jLSxH`>3d;E zkvsect!(fM)a~?hy}~A9`zhrD%#J2PoDX# z+1xZ}HRLRl)dkwFcEVhHcpFT~9hwB)r7sJgezOqsp|Yp8iZl@KceH^f>lHGR`ezyF zh2$^HywC)Rh5=F8ox$d{>t8@!>Mnw^Rpwm=Hr)$Fh;7`MS)oEV9?;+Gk5qtSo60@0 zu4pdjywa?-Mqn-xW-yItKdhT3BPS#1Xa`Ww9VrNTvlk~0Go}>vUuYGctvwy(IB90O zxGa`*T+t%Kt|KReVzQb>peyLDeU~TE778rbgf0jPaY=EBozO}T_Q{Pp@wsmh}(=!+BK&eBA-J=nY74{e(d%IP=66)G( zrrBXZFX@`c&H#X(>9@t0DP{yXv2Ebqt(vWB16`XYXJf>#6P~mM+})Ow5G0<<-V2ad zWIlfP^f*_sPeZ~P&uuRv4uGlL{IpzQApoPVt<=*n3a=?}yRnKgl^cy(_w%L(h7lS~ z*mgwolof+*1i9s`>M#;CRab%rD%V_+a|>#M9?NyJza(2gBR<1c(fo;+{KvkVR?z#R z<3T^ek)u<@OD9SnFuog|`4>aam-h0GO=G-p$#?+>N;?L@ObN!X_n6F9h-FgSUDi&t z=1W`XGkk)!e?|AI57;FM>aHTu^n8)Rf0%oqg#E^v3qmnD-^)KV@TiTr7nupiHRxKd zJg=qS{_-nIvWHvD?n$CPbK3_P9F#s16hxbkX}~*)GD)N-dVwq*?P1+eI`0 z>2ol}-f!rzKDpT?G(Xfss3fjJtT)HPVwY^ly)VEn=>^8RM(yE^SzhIT!=b{9Az=aU zVdPu2eSe!wpAWt}v=VwV54(|znoeu=YL004eQoCD2_E<)M1w_=W6@)55y@lQ@n-H zbVQMU>6C}Z@_9QWG&tfCM$-)gBzuPmdl?K5b_wyL8sHRgu(xv9MVUUMS+rV3hWr%U zZLn+l(k;USMBKIg{($|~-)UAKQ7LLaVK%-k8?svO1I(H7u?V7_PU!L_srOeUd=R)F>Y)7mjZIK zEtuNOwMLf^1Zgb+6M{}EqGj{oqtE6Qz=M_O6T9^9Fg;8c2A(nX zYr~;c`UU2>Z!j++3JCTdnc&5L{HNo{OtI>@^JW=W+}n$QLHdR*?WqFD1-|WuJSMwW zL*kU(MuyR_z$@z+&1057ak9*i$LXFh10Aq2?=HQ_LBm%qwQ}0kbpTReNz{m8z=!A;)MU@PDKDYeSfn z%q|O-AS@q9-TZ5&*i1{g&0w6@lz$abIxwE~RyEW$$q6h2ugKGk^jJC6yC*H1Qe@a|CW-5kMWDmpZ^K33^Jd{=l!A%5wVV?0)gH8Z!P%N1U; zK%ft)0=qa6HpH%yF~?Pwx6WLDXrfwuYN>i_JkSrTyswwG#|w|1WLwZ0!*{a5z~YjQ zXKUbf2nVC3{?(kXSgF}zPZ0%6POmEmT?yjXx#v&!$5=I!W3Ao0d*{Pe&<Sg=zIwof(j+kb_}Hh5fDnH-8&e%bHRksj9tS$eZ#aa~ z(F@F-zt-N-REjNktAAq1jH)?~9Zq(K<#p0fX&8r~dh`qiBr^D9|AC^j|}v@%=M z6dO&kyCMz(tJv_VdeD8Y&WMpf`M2b4+!k1*7keJ5$eqE@!z2C(J;<}X*15!k$q)0&K_vdoM`Vu0(6 z5h>UXY2FNG_xUKA^kezOe+AfBZr(Q1G@pO z&Iv`}8RNteM!?5-mB8mW?tVDrf%BL5O2QouLQmI#iQ23pKd$&G53{L?81m^gqm0Ih zKXBcM(Nx9PoLzhcw^b-(zPO$1+r6RzrdMOW&s~++B%8U?Xr?b3>lz3a%-PY)lTG_H zQ_6od2;}^fzkbyY%h;gIhTzM4z{($7gqv~|@;V89Ka_GF=|=T1%^i3eR(2TlRIU}g z3z96N5p~E*r6Zq6jB(CT_jp!Tq+n}7GEu}k7tl{G_M3oaySM6fpgMK84xJ&pkyHHR z^`o4eE;c39HX6@Ul)$L2y!Y>MUQF2p?f9CZqJ$PFa)T^k1-tB>*;ZLE@M~sY5v_K! zq(HqQ{=S{5P}O8d8)%&U3uI3n{pTYD)0ve@+U5nJ7n=<7@*^K^|Q0&@!g?8EQf$;m? zm3K>y)HVgJ8di~U_G&8J;nvzfGCLia=Dnp-@ZOq%vX)-_uZ=KmUcL#6qUYs9%?fJx z1EzU`1K+L$LotD(c$>f5D`GZ8COz8WGY;$bJWe;4f+&^F|GbKE6M#TIj#M`#P48BG z`J^mjiNs?GI$A&^cF2w#ZZ2&hmplS($59D?1A0o+AaZLnW5rq@A$Z)^hC}CEGDH2sCUNAjW^msV zP{Y1zHe<6D=o+?35(1ZGgSWas96z6%%?pvQ9*Em|3ET)0(3Mhsuroy8AMuBv2LBy2 z$giH;RUGVY^bKI^7c(P0t8YOu&vqs)YX;vaHf`(KoY{GM4Lm~4)VLZ;v;Q9&5|aP7rA!oYTd<#J+TNH6Z%j#DT%n$CA!7@J!6zjI|K0L&0` zgWY*%8}F&Wz6n~b-({&xT(H)PAoD}`TPs(Lq@QhFjtSbTs`>i$+BXX!@pLd#+B@}S zRqr|EHbZcVZPs-zv~^NoXh8F8KIOYm%m5943*+8W$zty0fW}ks51naE(3uD2m6G|m zMI@gj_%1g zrsyfCAxal-V4*zjO>Pv+sa*b&n`;(iY%jql0Zlf^#vN${kVVA(5-*^cTr<((QULjp zqBXLNrOUmY$efdJ- zv0!}rHqS^%gRM+M*6y7}zyO@KnF1SmVp>AwtGH$aApH5Yr$;Q;(o#Tv-l)Jl^!;%v zq6hG*D>-pEE~nMHRMV3#1S#{`8j(Q}{?ctEvZwJaJz2AGrFGqlj41|0le(&P`Z*98a zeji)~zRA_gT=sHE_D=7&AZvRjhvk1ChyXDrvfn`Lzuo5lQ>grRzzxRBeZHIfzq|k5 z?DFSPe**YtSs2It$twUQ$UkWD2X6n6h}<6*{-Z7aUlr3f*1TgHinSYv2LI0pT{>@m K?$cSv2mcF;CBp{* diff --git a/src/front/icons/maskable/x72.png b/src/front/icons/maskable/x72.png deleted file mode 100644 index b3d7045ba455e0fa66daf50d39dd1a9f549bced8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmVPx)o=HSORCr$PnOR6xZ4`!IGsH}cvNAO<(k{}JpsYj+vLqWR9KZ^_5HF(8=t}a^ zF1#%Q5r?2qDZ$JpZE(ne$R_hrj+Q0WX7OW4%^qhS>-*cYz zuJ!Gl0DOb4UlMRqK)<&`0Zmgq3P=H&3s^Ns4N`;55m1B7@zV@agVZ2%1k@mN{4|5q zAT`Jw0X4`RKg}RDNDVSaKn*g-PcujjQiIG9P=n0z(+qO7L5mhG!us{=(bLm|XV0Gf z(Z{!W^JXkqumIPuU&ps^f5(6LT5gb!j}K0sJc+2NC}d@2;oP}%c>Ve{zI^#IEv$nF z590j!^Qfq(Kt@IenwpyM`Sa&lg=IHDNl8hvF>dnIsZ*$|tQ1fa6BEA#ML>mxh4T2j zckkri1l-=i5ApKl zOZnZ@wSD_`oIQIMw2;>kFyYXu^73-DwYA~Xr%%(GXyp?u2Q+u?T&!BP3UP69*s)^= zf`fzM=H@2vnHF5Wd>NN6UBbP4_r$0lv&P-s9kH>o*uQ^2Hg4RAWy_Yyb+oRru@S|^ z#kh0l4xT=J>M)xu2V{JD^XARN+O=!3d-rba*|P_$SFgsLIdjAu8bl+mT)BdVh6a52 z@WJ+>(;PoPKkVAIORftK568lV3+3^TA3x&m-MhGU?V4QQ)zxLCEI)lMy8$x(8oLrf zal`TP@mRimxje-1eevRj7;)jk1w4B6NPcH57Z(=<1O#B)wryhLh7B9w>gsCCr@Fd2 zvGK-@8yFZEup21512W#Zr>Ccs+kpcIWWyW>3@bW1I%K2S+1cps?w+(+mgkBUE2JwX zCnrn!(lU0%zP>&wRR(!=b+s5~^>P>>zHx4hpw_HegQG`}N=C61xrT>_rS$Ui^O2jI zJE`pW`26|v;p^)QcDmHmR9o56y8ixt+`4rOr%#_oYisL_Mv9q&bSNO>O)-nOVV2K{ z6DP24-8wisJIljkV`I2~|GtzYLw)LE>3Ms5BRxGG2?+@{zzp*D@84tR&YgJt__1Xn zJ}ZC>o48r#)0s18)vH%+WksMg zje#B!5g|Z>f`V-2G&D3MS;HetSy`DJjDL5G`Rf2NbUDXxa|~mCU%Ys+&6wWaUQ|_8 z$q}TcrUq}`ys?!9joGwmlNhsg>sD#)%quP)HCTe|TAZQSwZ_NC|L^{%z0Puj3^055 z?v>4jhK33ln#8Q&@q!aHCt;q;ILjC%!I_8o!U>v#;mVaOCxNN0t(EfQ>_cPh3C192 zIUvF~cI=phD2G9U;TjwqL{U)@N=iy(b2MgZR`9UOVUb4-qce?;j*2nN7nUM3#t~rb z2FSP{#M>QQ3{eJeUS1wLJ39r`uR`?4+Z`MLA3S&3!LSEIKXjO2W{?_W&Y=O{n`0000< KMNUMnLSTYB@Y|9A diff --git a/src/front/icons/maskable/x96.png b/src/front/icons/maskable/x96.png deleted file mode 100644 index 2972d6077830956a5ebec49f1bb2ea87f5aa3644..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1752 zcmchY`&ZHj7{XCVCYV4X<-C=U#xh6dlxg)fNW~`JqMS}m zF|%?% z7ZF;m;y>&%v?bYSQ&S9$?x@>7Bk!`L$~{@f8?i>seWj)5BzIYNrxuof+ns-0=k!m4 z{SJC^&-(%{&UoTe!Ln@_x;8vc<6gsb_u4#O%=8DAv?RYFDp{~oa~>U52{{B1Gu^8e z2;gCVe~Hx#fwiiq)W6pGzql^dmT=c_F)=Z2p`p8#8&!>)Xr!^R)_6^1a`Ezx!#}Q=n3zPZ%=&FNG2w&8IJbxhYcD!o z{H^hG;wd%VEr$>tSJ9W3mzVwQZQwSp&aSSyGMUWA!QtU7!`C1KY~s^tw1Z40no$>{ zeIQP0A!pSBJj0UP0)b|el>2^K7=tTws{$?d?Mu$uOHULEP$*PPJce2qzuEW% zHaA+Ol`j&>Mn{i1RTmWjJvRxFcE%n+Du8-Z9WnlE%G})9k^4U;Pmgf9Tt&NZ=hBCn z9|8jd`(9l}My)TMw4I-us1LB$A5MPoz|I1P(=69N%vL!DmgEh*4Z8wojv9l(aHw@~ zbksR&ZEbz1c65B4=#%*bSZshh@fd#n{jfNA>g!GoWo}bTvVkcUOEsp3 zg_S^qQr-q@E6Tlng(TL~*Kh8>Xf)PZ(jeNS${D5!=kxg)g@w4) zm7$>_%aKXfxK~e+(1$k&-WTPlg-gNJU!tQ;9UP2MXmmhpPj4@g&Hna9ZM0UU-4NG3 zSV&GshHO`F=B~^lp^F#KTm|WkCEhm@!+HzzP&FcvCPkdX@gZwR87)whqF)`7h<S@xy2L0`0(Z4sqS^0Ku4uXT4MCt( z^4`qM9P3TD{#@SXmyn%}gt%PdsZ+(k^wd=Gch|+i(zJPt3Q!W=AHJ}#5H;0_;6w=1 zEB#xNl&`vSGqSR_FqzDOB}cQq+v;a0>ioRy`}_M3bqLa}adquGaMcoN#}BJH)J_iG)4jdZJhsuh+W&$A0s@w+{|Q=DC~kG> zMp8MmvkMFUGQaQl<^epSs(c2isi~~4l5-=?N3RHlu%AIrnT+rjPaqIDH-9fED35(E z?7g)41wH%rZBmhH|1i^=ES`zCx;^{N1hhc~IhJtXm869XU$ diff --git a/src/front/manifest.webmanifest b/src/front/manifest.webmanifest index 68b64c31..7b4a239e 100644 --- a/src/front/manifest.webmanifest +++ b/src/front/manifest.webmanifest @@ -18,54 +18,6 @@ "sizes": "512x512", "type": "image/png", "purpose": "any" - }, - { - "src": "/icons/maskable/x48.png", - "sizes": "48x48", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/maskable/x72.png", - "sizes": "72x72", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/maskable/x96.png", - "sizes": "96x96", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/maskable/x128.png", - "sizes": "128x128", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/maskable/x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/maskable/x384.png", - "sizes": "384x384", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/maskable/x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/maskable/x1280.png", - "sizes": "1280x1280", - "type": "image/png", - "purpose": "maskable" } ], "share_target": { From bf1dbe1ad8cab02cff31476458424d1f2e2ecf9d Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 17 Apr 2024 00:08:45 +0600 Subject: [PATCH 09/99] package: bump version to 7.12.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8885f1f8..c5e9d214 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.12.4", + "version": "7.12.5", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From ce1ba8c92d5c987d6862c1f23f1d1b1f7bc1bc1e Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 16 Apr 2024 19:34:45 +0000 Subject: [PATCH 10/99] instagram: use different endpoint for getting posts --- src/modules/processing/services/instagram.js | 39 +++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index 5c696709..b5e2228d 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -70,31 +70,41 @@ async function getPost(id) { dtsgId = await findDtsgId(cookie); } - const url = new URL('https://www.instagram.com/graphql/query/'); + const url = new URL('https://www.instagram.com/api/graphql/'); const requestData = { - jazoest: '26297', + jazoest: '26406', variables: JSON.stringify({ shortcode: id, __relay_internal__pv__PolarisShareMenurelayprovider: false }), - doc_id: '24852649951017035' + doc_id: '7153618348081770' }; if (dtsgId) { requestData.fb_dtsg = dtsgId; } - data = (await request(url, cookie, 'POST', requestData)).data; + data = (await request(url, cookie, 'POST', requestData)) + .data + ?.xdt_api__v1__media__shortcode__web_info + ?.items + ?.[0]; } catch {} if (!data) return { error: 'ErrorCouldntFetch' }; - const sidecar = data?.xdt_shortcode_media?.edge_sidecar_to_children; - if (sidecar) { - const picker = sidecar.edges.filter(e => e.node?.display_url) + const carousel = data.carousel_media; + if (carousel) { + const picker = carousel.filter(e => e?.image_versions2) .map(e => { - const type = e.node?.is_video ? "video" : "photo"; - const url = type === "video" ? e.node?.video_url : e.node?.display_url; + const type = e.video_versions ? "video" : "photo"; + const imageUrl = e.image_versions2.candidates[0].url; + + let url = imageUrl; + if (type === 'video') { + const video = e.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a); + url = video.url; + } return { type, url, @@ -103,22 +113,23 @@ async function getPost(id) { thumb: createStream({ service: "instagram", type: "default", - u: e.node?.display_url, + u: imageUrl, filename: "image.jpg" }) } }); if (picker.length) return { picker } - } else if (data?.xdt_shortcode_media?.video_url) { + } else if (data.video_versions) { + const video = data.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a) return { - urls: data.xdt_shortcode_media.video_url, + urls: video.url, filename: `instagram_${id}.mp4`, audioFilename: `instagram_${id}_audio` } - } else if (data?.xdt_shortcode_media?.display_url) { + } else if (data.image_versions2?.candidates) { return { - urls: data.xdt_shortcode_media.display_url, + urls: data.image_versions2.candidates[0].url, isPhoto: true } } From 009f4acbb3080f2b8c6991967f7a86967b2882c9 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 17 Apr 2024 19:57:36 +0200 Subject: [PATCH 11/99] docs/example/docker: COOKIE_PATH example to compose --- docs/examples/docker-compose.example.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index 89c84642..82564057 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -21,6 +21,8 @@ services: API_URL: "https://co.wuk.sh/" # replace eu-nl with your instance's distinctive name API_NAME: "eu-nl" + # if you want to use cookies when fetching data from services, uncomment the next line and the lines under volume + # COOKIE_PATH: "/cookies.json" # see docs/run-an-instance.md for more information labels: - com.centurylinklabs.watchtower.scope=cobalt From 11d7a62b078e17d4a93ce58aba0526d31a9924cd Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 19 Apr 2024 09:41:52 +0600 Subject: [PATCH 12/99] readme: clarify free api usage terms --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ebd3067..9e1d0423 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,12 @@ this list is not final and keeps expanding over time. if support for a service y | youtube | supports videos, music, and shorts. 8K, 4K, HDR, VR, and high FPS videos. rich metadata & dubs. h264/av1/vp9 codecs. | ## cobalt api -cobalt has an open api that you can use in projects *for completely free~*. it's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/api.md) to learn how to use it. +cobalt has an open api that you can use in your projects *for free~*. it's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/api.md) to learn how to use it. -you can use the main api instance ([co.wuk.sh](https://co.wuk.sh/)) in your projects. +✅ you can use the main api instance ([co.wuk.sh](https://co.wuk.sh/)) in your **personal** projects. +❌ you cannot use the free api commercially (anywhere that's gated behind paywalls or ads). host your own instance for this. + +we reserve the right to restrict abusive/excessive access to the main instance api. ## how to run your own instance if you want to run your own instance for whatever purpose, [follow this guide](https://github.com/wukko/cobalt/blob/current/docs/run-an-instance.md). From b50ad1e4f251132af9f1d247f59efb9ec570e293 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 19 Apr 2024 09:44:13 +0600 Subject: [PATCH 13/99] readme: fix links for other branches/forks --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9e1d0423..8374817a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # cobalt best way to save what you love: [cobalt.tools](https://cobalt.tools/) -![cobalt logo with repeated logo (double arrow) pattern background](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/pattern.png "cobalt logo with repeated logo (double arrow) pattern background") +![cobalt logo with repeated logo (double arrow) pattern background](/src/front/icons/pattern.png "cobalt logo with repeated logo (double arrow) pattern background") ## what's cobalt? cobalt is a media downloader that doesn't piss you off. it's fast, friendly, and doesn't have any bullshit that modern web is filled with: ***no ads, trackers, or invasive analytics***. @@ -51,7 +51,7 @@ this list is not final and keeps expanding over time. if support for a service y | youtube | supports videos, music, and shorts. 8K, 4K, HDR, VR, and high FPS videos. rich metadata & dubs. h264/av1/vp9 codecs. | ## cobalt api -cobalt has an open api that you can use in your projects *for free~*. it's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/api.md) to learn how to use it. +cobalt has an open api that you can use in your projects *for free~*. it's easy and straightforward to use, [check out the docs](/docs/api.md) to learn how to use it. ✅ you can use the main api instance ([co.wuk.sh](https://co.wuk.sh/)) in your **personal** projects. ❌ you cannot use the free api commercially (anywhere that's gated behind paywalls or ads). host your own instance for this. @@ -59,7 +59,7 @@ cobalt has an open api that you can use in your projects *for free~*. it's easy we reserve the right to restrict abusive/excessive access to the main instance api. ## how to run your own instance -if you want to run your own instance for whatever purpose, [follow this guide](https://github.com/wukko/cobalt/blob/current/docs/run-an-instance.md). +if you want to run your own instance for whatever purpose, [follow this guide](/docs/run-an-instance.md). it's *highly* recommended to use a docker compose method unless you run for developing/debugging purposes. ## sponsors @@ -71,7 +71,7 @@ cobalt is a tool for easing content downloads from internet and takes ***zero li cobalt is ***NOT*** a piracy tool and cannot be used as such. it can only download free, publicly accessible content. such content can be easily downloaded through any browser's dev tools. pressing one button is easier, so i made a convenient, ad-less tool for such repeated actions. ## cobalt license -cobalt code is licensed under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE). +cobalt code is licensed under [AGPL-3.0](/LICENSE). cobalt branding, mascots, and other related assets included in the repo are ***copyrighted*** and not covered by the AGPL-3.0 license. you ***cannot*** use them under same terms. From 5c4dbb7112269f54ecc459052b9800fef69e9d63 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 19 Apr 2024 10:00:47 +0600 Subject: [PATCH 14/99] docs: update links and firefox troubleshooting added a message about firefox 125 supporting clipboard pasting by default. moved screenshots to their own subfolder in docs folder. --- docs/api.md | 2 +- .../troubleshooting/clipboard/config.png | Bin 0 -> 4154 bytes .../images/troubleshooting/clipboard/risk.png | Bin 0 -> 17944 bytes .../troubleshooting/clipboard/search.png | Bin 0 -> 6867 bytes .../troubleshooting/clipboard/toggle.png | Bin 0 -> 18725 bytes .../troubleshooting/clipboard/toggled.png | Bin 0 -> 17312 bytes docs/run-an-instance.md | 4 ++-- docs/troubleshooting.md | 18 +++++++++++------- 8 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 docs/images/troubleshooting/clipboard/config.png create mode 100644 docs/images/troubleshooting/clipboard/risk.png create mode 100644 docs/images/troubleshooting/clipboard/search.png create mode 100644 docs/images/troubleshooting/clipboard/toggle.png create mode 100644 docs/images/troubleshooting/clipboard/toggled.png diff --git a/docs/api.md b/docs/api.md index d67122bd..e63ee7cd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -59,7 +59,7 @@ from a successful call to `/api/json`. however, the parameters passed to it are and **unmodifiable** from your (the api client's) perspective, and can change between versions. therefore you don't need to worry about what they mean - but if you really want to know, you can -[read the source code](../src/modules/stream/manage.js). +[read the source code](/src/modules/stream/manage.js). ## GET: `/api/serverInfo` returns current basic server info. diff --git a/docs/images/troubleshooting/clipboard/config.png b/docs/images/troubleshooting/clipboard/config.png new file mode 100644 index 0000000000000000000000000000000000000000..b0c0a04803397f14957aef152d84e3449757f3d0 GIT binary patch literal 4154 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 OK>9i+=yKHc$o~VoUOguO literal 0 HcmV?d00001 diff --git a/docs/images/troubleshooting/clipboard/risk.png b/docs/images/troubleshooting/clipboard/risk.png new file mode 100644 index 0000000000000000000000000000000000000000..1948f0eb48baad049c38a83297ecf8709d94c275 GIT binary patch literal 17944 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 cYpKC;r0VcLr9Su7T2?+_Es)~Xx2?;3z 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 literal 0 HcmV?d00001 diff --git a/docs/images/troubleshooting/clipboard/toggle.png b/docs/images/troubleshooting/clipboard/toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..32060dc7f9e01dd447d66db8b6e493e47997e3a9 GIT binary patch literal 18725 zcmeIaXH-;6&^C&M5n&`q5Rj|{i6cl(qa-Eg9F+_MDgu%jBpwhXi$o=7B#mSQBqzzt zfPmyI8HRK>Cn)FW_kQ=+UF)v({+ZRx?5^&vu70ZO>DmOVD9K*Mqr$_$z_|MGfs`5s z1`Za`-oOO`pE8Gy*cce}7!RfHX}mC6o4ArlI+t{|IoSni5~KF*m;=lCJTQ1=6}pl; z#a2c8(KRRgjw)`W8`Br5C^F)ipj)ciciy)7;xgOTj5M=uf}U^pQiUxtOVMXbVu$+T z@;2dieK~W>t**+;%d5(D&&|uL7LU4KgsZ+wy|EBad5oqed)!J*o0q$M+k@T*0~7a` z52!2N>ICXmcXxr>Jqn#D8?o1|9(KQak1j@f9(#4l#=p+Kp3$U1&DvwksfSD zK(B%g4+|q0t9B@QURM54;fRjzUy`7tIBeCD(2dVkRXe^^cdyi=z)D36-~TFKQz?5{g)+TAIjnxy=_}!Ke}m49+BE zceg4U=dJTbzcx-TeX;QwJYziOeYxj;VG_2l^g!053u?la{JZY2v+qG>EPZ;eAp}_}ab=Gm^ZZWOy;&&WZqizG9joZuBc)8Dn53 z8bHcTFtwPQLC#&F=(X{WPYqd7(rFj>h2Hy8W5e;<3zADkz-UGpF9(fHnkpeFNwFAyGvk6$5HSNXA+JEN+uA_m8I3NQ zOz)CJQv6%&Dqb*WRbl(VLOB?SwWUAm3LXXYBK_ z$T=Yh#`bsU=1MOcabwYJ7}A4x>_10cuozOkDWq*Z)8rd*#iIFlpXJR?k5gtfnM_TL z>}sO38pO6YB9V2Tv+&F0?J?RtuQTV#E7y@L+Z+YYjqq=Qsrl`)@AC1# zBQsNPZ`GPpu37`rvz_UBr(ZBj=jeyDUH*LIdQ~kBC^iXQ@CHVS*aD;dVF$G*6`yIZb|by z%Z_QY-+RMjMvT2Bo!5{wR;V7aed@~7fR(a_B5C|n+Ni550*kxhlp-?txZII-nPz(a z0jh68;RH}(_D&|Soy4Nb*z?b8JX3SB85zz#8VE!OLy_q1f|MIK>5SaVsYlP$DMwup zgU?r@dvr3MpV|~Wuc=Z(PNIyOX+i3IrY4Y{LEoQ{WyltS|d$g(Y zp{YUO)Uq=9?8TuY?cWSKu4bKgcElt=^>Lvhnp<}9VgdMM7NgN?xmZYsi$8?7ntKEm%f<}^~(o!6v zI~Zo>N~f_h%*Vy8MG%VkSdZQ4DOU2M*Xh15FdpKjgrFQb%^=_+M)7iln9?Q=pJOxJSn=anbXcACI3>MYgRP{b9x&r-{AGfO-+ z%1WD#R$@WZb-7O(K7tx@-WLA>>QHOkY)v1Rue*3u_O{A-}t7SZqha*h-)+DGcxdQZ^;l&!5fLeF|;OXuQ{ zs@C`X;JJ#u;zx~N@O2bI4Fjs|pjSXsen6Iu-YtUz_Ew)GXM)01Q zm&}YPHNg;zBs~Wz`oYS_S;0ni!!I(iv12U&X5o_o!>M*~(Lk3exoU zw6r_EE2gwR4B2~lRH!`b?Xb@5F0&VH?=F(=;FPzsQ7&6mdl*{Z^|D}VM_suywqQN)ba468 zbW=dP>p}|4O&KsrIUYOevZ=|T!W~D7g9DQA?6h_BmO1TPSoa zXF{O@OZ3Q@;eh*|U-u!!q%RxjvF`2Sd|xf?4O~BNzOM-B=qCM61@iLRnJDpIWIgQs zNZ`8A>7H(h@a)e+8Bu;$Dh0dNR)gIO5(ZaMTnR^Zag$;POgUo%%(Z*v57sWZ&-v6J z>muiMdl&CEHaO13IpNPH&uDczsnd~qvOLkFurjR7^74+Mkj*Z1n-}0&6@)F-pB8@L z`A$hGKKX*V{OaL%_Hws6o?F4iHm8+5?z*`il_w+5KE-ZymR%msO4ZQMw39FIHofMG zWX{K|bY)?Y8?{4n+1inK=`!>6#9Tp>%=hL^W>uh;Q|t}c7L?zxZ%$P3qe2y@Q|g2_ zZaB5a7QNw<%Y?n*>`X~!?TpfQ=A7%4i_dV-@6jqkk0&us4r!f=!OBq6Vg-9-r4}Jw zybgg{Yy4ja#EWRY8x+w9J@qUx_W1mBgXmuyPTiP%!#r8Hm+%Y)Lv*Sui?0Q*0B$uZ z!J+QqdXjgF=uTf`{ss}qjgd1ru*)^5Z|CBX#Uy3yDzE|B4(e}JB5b|K2>^@tvL+Dy(0g1b3|<0-wMl| zT)V-R;zfUPt9dV6%sj3FUH9;)d~3B{{sdtZlgx>g;oc2h8q>`v0F86P3iRi8v$7tn zt=E=#D@TEILmr-$dq>;vinL)_Q!X$e7RVqhSO9;X+sJtE<5n%KlEglgij`GOQNZ)_T%zPgs}W z1~aR(&iZ0gBIcD#o}KY8hhuz(C&L8-;lT~u1;=ANRi^PCN=f?*Iz_~!nMlvgenlST zepI|3r4<3^%!ZN&OzzQ;$$O4Ffr+0_kg3i;f;V_SkqUgO+tEJqaNGMmj)z1$a?!Tv zB$k}kOWxl0;{+eB1`aWR4AcNJ#1MAJJLYkW+!xq2CBPoq=)RYVK515GejwSzi8Dc& zW6w{^bF0o|P7bzU5miC5U1eK@SW_YbLb^msD}yl8uP@j5A0CThf6>!EJ3SE%ggZGo zscCx$ zc`|;~zcER7eMv|sVc;ZPHear4WAbKFKZpK$G6Nr{dq!oe5#?Z(p<7+4mJO|ui8VqI z!wUPUlIhfRTxaB=-z@1cz5yW!5k^pj=jg#JDM)@_~x&mD!ctU#8x{xH1V0@8>BsloHOKmm7ZTW_ONx6 zH&A|>Gr%L9h~si+sM&3g!I6>ngU4Z9Rz%s<&nB4jdnghGpZXdIG96XoWtb@3m^s(y zotU_e(N7BxZF}*ompDsKzZ2-zPSbcJA(Xm4=4;N%J9S?TqQLs;%pHiIT+jvp4>`jvj z1vL)jtn?a$1*qlQflp+hc0`C!uRSkpA(OaAcof)3|h zG&Kkr5PsV2BzONu%Eamb2ZF$iarX^n>Rtj3OB}Bi{mJMkrNk*pUq~OX=0nksIwCB^ zE-7`^*8_EK*w;Qte@(+n2Sa!q%2lrPRMWidpl@t22t#gf2i5xKgYSY0Nijdszl6@c zXRgFMAm`ATbqfxvay$Bi-C|LIP=v-xw~hN+2Un=i2t;M*)IW^fTx0Eu+E7|tnM+>( z{dzo33epqp1hgj8dm0*68f4Ll9z6xBRQ|30a9f~v?MUY4-ooY*`*s=ry$YD8Or~|- zw%WA8OOAn;Re5HkykkA!!9A+UhzJg?UerrmkU zZ1SzEflSE~6PdJ>RqLJY{IqqjwX8)bvEEX6h5K>B6J$Jm6TL#m)3ZTBS(N0k_`>tol=>}cu&gyz68Q8IT_QK#30wfYY{F{w{_{1VXZOkUAmqKbY$)K^pzF)ni%#iD*8!guf8Vu-{ZB09)Ok3-LtN_p;2jv>~ zGthAA5mBHR9X8MrWiG29-Z@nz-h6tcO>3r~uoKpZ7V=tXQ>kqxoQprqoBMcMD3S6F zF6j`gc^KsvM_Dk)eT_2Zpa!8kO$E}kpMwqB_rQ)@9=JY~xD5Fqr*c%?Q8No`%IuWd zVNRc{N$^T2-dif;Y(pwG6Y_PGNg68|2|n_gJmojd+A0N3Yz#bCewe>kkBh|eSwb;_ zvQ_D?#3nW98K6ZtyM*iF}a_U@Wv%N(Q4?i37};l<4)zL5EVVv$+H zEkzqX#bjLlfU9ygPjBXNiRp<Li?jWzX zNvrAW=W%N@;XaSzsO??22Fm@K1O4u}W*p^}bTh-d_Y|49#=tk>F0UXmZsO2tG&Zsq zzdCsqU#4LfE8%jQRrXmAKP2)gLlEK6Sd!wRTE08QqGWYPZIE4DVzm3@65Y=@JPvzM zu3;hBTT7(HDkM8Zq&v=!A}(YVS*|Z*#g5x6Zd+2P*4KI2H1&E$zlRKtcb3&*-|Kgq zyhq~WdoHeu)z*S-Nw3+!8bTpX{_m3(XWK1TTpm$7rRyQpjOgHg@Hy#uV~5b^28KjD z7F|}gQ&(^BcWlkEmvJ_S+Dhc7x~Igr={)lugVaowzU9lAC4~y6`pwmM_x>wxNml~noGOr+gbh2{yeh7uqD*&XDDb-b^ zS|QMw6E4|dFT<3f5VRRw$S6^(#(NWy&v{DjmD)O-hNKWd7WI2Rq1RR7@Vkf?vSPPrbq^TxprZeM2jUp&eHD(_f{t7 zp0NURD#3sL+=KaO-$m~8=$*jU++4SB{ur1zD_ZRgva3nJwnrs_Mr+F2i}j()44Pt5udptmBa`Kb&$FllILO0qI^jcM90|FGIW z@zKZ17Lz?4@FD;wi39vbsskvmp@B5qC&>p7dw(6pP6LDa`6>`!7>6h*-v{qf-d|iM zHasE;=eejA+>Ao-QFD+EY!d5bVG8=9t}ZB?9-cWLZr&QEoc>5^qD=?pz{QK(mSpqW zr*REZt$URY%@*t%8yeA?*~Ww2h(eEkQcf=>{0nkw+lSp zj|YgLsSTJ|1QgLoW@_6a)+bJ+;Xk~_kAUu+I{bVsdy)Jmn1ft=_d+i7^AVzSz|>wN z2Z0^?iHrZVpv1r=LxPlt{`ulx4`JViQc$GxJN=A>E^w79QotTEepBcBjaUrKdw4`3 zP>!PEZ{?Dz0y`OWpz-Qoqx~%#`Wmp>dp7FVe=Ap-6o^k5#WjB({|i(4zb$o(083vv ziNpPL_u80C^NWZ-{$O;5=l-JohA1cp^Tb)1jN92V@zQQbX|mBY12z#WH@v2?VM2S7 zwfnjeZCqbn1Q959Bt2E&KWj8+GeNr?)P+m1o_L{~c-B}afY6Brp4bE}XMD1d=)Obr zr0)bFU%!w{w6rQk@zY}dZZ&eL=-#gGO=c7+tbAxZ%3C}wtc4rk*=0qCDULr&4YIF4 zdpfpH*xHtQG%r_gNw-}oL>l;uCjH4r#SR(AHoM3APtLSDr);@6o;yVYHpFnPO$a<* z@+|7>aZ`_ABpIB6>cQI!OyQrBd@L`){HLn9YYHU9k7rerjc4uw3h;!FH#Lk*z6ykA zdA{3mS0?=L;RwoZsOCNsq6H;C*O9_m4X_g# z=fx$#fA2Ol8nDYWQS^>j2Vf1Vw4Ug&f3J{87qGrI0x_EKkAQhMvtUg8MC3ouP*5iD z{oU3iqnl8QHfksa{5@{|f{{w*-l7it%`SI{r#he90t z?ge!C2K-2zg7I9U1ZYIixCw_rpPoOpqP%uN)Qc-vtzk6mEQwEQG_m;U`D? zaaTx`hVR#Fja512*-h3Nf(U6jngXsy0i(D7LB?)AQfTPEahs3c2bv9BTuAa!-v#|J zMtWaNfDfSL<%(lT@nGXl!sik+Cg*g66uG!wxrB|!qS6OZDZov5-Z9lkwqAqYl z^TSkps-YABX%S~1=}MviGrXfOqvWY@^fJx7=MNUM*+x~KwYYW9D=Q?q-n^3uj3hUP z1ImNls#|(pHIs#(t2P?l66az!*E*+tzdSpMcNny;4>_wG6^*iGDcGGydub;l_F5M$ zwL3UsT=1ztxy23*KBs!4`z2|u=UjSL%6iFF{Ox;S$&ZoB)!SYRjVbNurG&X}4<%wH z<_#ZE7>UYR)b8L{mi1g)w?5Hl)o$-<OSoADa$E%dTc(2|gOKY8(chKO5^87ct=Zm3kibzskNB`-1C z>?PUP3$m16%YLSH*Fb2Y_t6);1at|Pln?y;>SG;BidltcSLb#%scz#dvg4dsrP$Vb zX*|-Ffuq&4=h24t_nI1Rt8?}BU>h*ddDl#Qn7B+Us3W5O#8-5)Im&gH6Q(~ie-a3Z);J7nTHZh7LS$7? z>7&h~Bi1N~FcCsGbx%T{R%@nDlMA)qb*bqZ`RL@`R3(6{a3$S&!jxLR#nbRtA(z@Yf!0p0Z!D_2)wWjDwxouX_R#VCIX?A)r z)=s=gMJ2p+;h37F##dsbYf&=PFkHR*ZLy)5@@`4y7Q+ ziq@knf<;s5q}$Crm>lY(ah$1;9l?Pl+L$arZS* zjT4F*;=W_g;ttR8{-+kemSL)6<3>v`QIC0DW)+=7b$RIOwsdzE$4oBS`kZZx)yp_~ z{VvCe#r=+t^)P)!#~4_n;aTSmrVGJFAKCdC%^^zPWT@udy@m=}QD+FGy)~5OoBBef z-%5C-r({>hp9$bmab#s)Jh}pAsWqc}P!*dp+9Ap*e`5fX*Vf&j;(qJPQjN2yPbxg% z&T*Tyuk%twR?kQ;$CZ^5z_j{xo1nG!j>S1~d2_YFLw>`XrFkPw3s(>E_k!kA7EX2RzR5MO&X-QVab@+(rk>SNz@cJnbkQ}Ec;NaEl(REY56f7>m` z*IIhGx(o?4i|x%CTHCW{B$D%Ece~CB8)^k&aL%Xhkk-#&aDQDLEb5=YJ| zgct?r6S0||Yj(&&XL&?Y&hs6A)6 zNVx~Rb3E7f@`6*vW>8s$sBEOs+B9odW(4{Mf1l%1S9wD|m96Q;JL|bZ={#@A0T0GY zW3EMLEGrXN8W{&Ks04h45YDkQ)&z&aLN7bysC27kc4`fE#rW~(-5$V{;f7Usx9DlN z`e<-TJC z3{ay6;!27a+PcJigl_1Bo+ket(aIBRIZa@>Q4PtlfRW zEC>W90&}}Qqi-_6UDsxp@*!zt-7q1QZ>W zQifvj!|5!)*~(fI5Jn$+h6!H?(!O&L{Q>E-g0(E)P2k1qX6SpfqiLC=lpslJOX2jk zM&qnRf#<%IW`OHls%}q7Zg=f*GYS8tbXWf*Au~tR`^3ttBh2(Xfp8;aK1{s=KVsFM zq{rIa1Q|QDbNdYvnMi3gXP?^POf@4_YY$V^e^OwYx+22AeJ905wA-}6leRNiD`iwT zdFB-g0FWU_Ht!{6!D;NV(ky9!MP)`I9O3{LWhE|PXF~!?mm_g(8sqilff60TCAFuG z4b8D8Sp>HviDnFHr5?;u`OcAmn5iN}MS9lrn-17&qq-KB#{83<%>{RvTw9X(Fy!rY zd&oh0SW1^=x!JK^Ucs6;PV}wma{cB358CHZGvHd=+Twd$f*`~)oUhueBbLdmc*KIo z1nyeWrzX6RU#M8YRs7+(rTpx5GF*3I)4QbU>6b0qi}aGWuN-Nb2vXyrV=Fk z<71VUF$s+*OsCJhxFPlw536)2$|1F2XbG7tRUOH0tNpg3v)X%s^#w)t+f*&wT>Y1U zgmlb9LqjFDBV30&1BOUfU+*tXjoh9~qvKfj0x@oLHax+Lx2w3475D()b9YG1m^u=+ zs}jdekc}&7`|FKR&(*bxx4C7^o(W}c{fGR`C;%BSCV z^GtX+XRyUV`c6c6lrO*~Q7Uw?v7t;}+Q668(?8*Z=AfU2;bw+ck-raqIX;gC)u$9Z zJ?2_&`QFZr%k*pL_yM^3oQ+DFXFQY}hvGv;yTf z;LEdrr}!kYB!R_w`}W~8@!eUWK+PokWb337RAb`FjfeE6si^%DG#Xuld5=4ep&7yP zR=d?4WG3W5QXvx9qu2U=U=G3S#UW#nHuBC8n~~k2pIaMp*?@9QzHWsY7w zP;R`#*3Gz{Qs{$RFI5*MJI1RRUnTQf_Gmt|8p1Vr=|0(hg+-|UQ+SY3VRvqYl#kg? zt2#xtA$=s0mWI2O%k|xZWoE;D%aSfiTULwRQhb^6x{p00WzW`nG0T#ATRfh^;sW{2 z@UdQr3-;{p6mcRtl|Dk7*Fks=K*Unzk~9(rR4Ajxs#MvSV9)Mu5pZXQT%TOux!hz{~!T=T<*u!ks72AmyLe zF^N%|N_A1nVqfQFkuT!8h}w~~oJ59<10)Cn6oM1w60QQ=)WZ*Pd|R$o1asL~MjMU` zD$*7R1Zu=vX6qujo%z5T%yCWw_v1$;T0SnuhxUXO(WYIuP-KsckTyh9|;GLdSG*?3_>6u@Sfb)OW}Pf z)(UG3)>n@bSY__`Pz`W|emwS<>=TptLe-@$xF_{TA790)Ekt*?nwtRBLG#Bs{!L%! z>aD|#?$=oaivCfDfS$@;_{30YRLh%Y4vCD|o?Ht;Na8yv$FtAxt1;v{62+Et62u_{ z>j8DIZ{Hl~BGt3pzH3;M)=^v^fjYb)XVnNDk|M+P+6<9x=j}aan{t3_#6|hcbax2F zd^>82e^KT&b1g9mJDGIO zEfI`KNeoM%?hEAL-_%4eW=zc1I=;h7IH81V^uovsfQpnereA0Zj;K zqs5MNmhk(qE+!=BvKX0*-M6GmW)ey>j?M~VKaChreE2$Gi2a`PwIju-+p^i1&;o6Q zjsj@HyL!ic{fZW@j61;>_kW9ls}c8UVXuq3|FQaUo;l(VJj&50Njq+HV`|o7Yf>1O z=&+v=U}+{*7uBi0<*pGmswKzEb)f&X9v0e$qi{wrxxaZiV^Z0hVhc1NicR!gpS}p9 z!gs%bXneaUuC4B73ke7<#uoiwz6;8LT?Q#WN*R8zVJz18N5lOCHD*jE`qwTLI+&hm z!T(kBUw+Bt8c-f9e8ETkAWaXLAs^Eu3vI@jWH%=BPLK=wl^JS%Rm)U|@=h*?MPJKH zDl**{-zrj2R584}_p5ZMHJ(8ql0#zjBzVx-Io9H`hy8B2&E*c|qrLsg!9p@wicvm^ zuQEURr5~+6>;RXt&OgG%U#~B}rxK6ER0my+m~`2oq#|x?=mX#Tjhgf^H`rUeP_x526wCU5&e6w&gq@h=Qy#pd6=^02quy5CMjMeI!EOhx%x}a zY^||*+t*lDZOO<&spyW_*vtm|B7|Z!KDGkq(^9_uH_J4`79T|$=^3|P&56tG=xE!S z99PZz9Ap|8&;o}*?n>U1ynr`FW4BcC5ORqbw#)WaflIOeqoFZ$_!_J)Y{F`UrIgQvjdrc!-ssCz=K?L8e04UM)1 z0!l7pDgiAcosv;U`ZwAXFP3w%Iwq_3BVHv)1g)m_KmB#h81QH)(IuUn7niN?RN4Dc zmlR1Gxz3R(ivkl;&oW{rEYfZL1FGCC>i3f)`==FM4}E<}XO!<{nF>2L_YDc9ST4^| z68U7)DGl8!v~n`r6vOWZ+`5j-gMnXd(MPTxE_5FsE6(i^KteS)Oa5vN7?@+&#{>-k z*%&D<<}Rn`RBjbAiHCky*fcsh%*>hM$-K5kV_4YffR6wXgQ~7_@ULcc`qrHfAzM;Bu&N$z_ zHHRp}ZqCY@$j$r|awDqy6pPZqmcN?gC2S9p@4OfyPo{htv~j42Y@DWAMI_cDL;l}% zTJ*V8SI(Z|D~|SqK)PYe8bCf|$+$`I>kL)Sukmssb_W_eJ=)F9O(su0({F0sCcC*T z)ADHTYoP_8RUWYD%YU}(Cn`zFHuPeI#=tAP6M)P4KkV=Pb?r1Bj&CJI zLw}u0bS;#kso_f?I1aGUHX)*0;9FphBW@kxpkJSTE=>u$-b@ zpW>phylwQ>j~jC)X?6G=`BLIrMF55RylhzDj+%3!X$Zk%Ks0 zQIV&9oy!K6AN()-Axj1~J%K+!R|WpD%`iHPTGe;yUpc7eczAr>>mtP(p*)#iaH^g+uOY%=o`85xb)t4@nopLg1+wdb zOkFJ%n zk9*IO&skPe$Hj z0#4j*4++j1@Y3Mpu-6&#ac-^}Y*n9UqmrsK-bXh=#qudE@IHjrgA;rM>@N8~Ei@6O ze&}z}UN6Yy7%E2p#%euRzOiF0I?B;9Mxn&ZGqyd}na(>?g%nuD>qEzhp60C+ z-nE5yBPnBUm0S|M6omgQqJagt^dn6NkU1c$MjIr2JSVUQ=5Kv{Ta2__)V-DY?o< z{)+i@LU|8-JM`!4@oT&l+sInm?YO;AGK{^v-ND#LX!#CeLsaylisk{sQK zU34j}rf)BIKm##y1WipI$rlgoJPe?3sdNi|-NuLA$pTim%jo*`^@MKVrluRoIo*ZB zcTRVGwl9YZee5C!xz#-U(%I1fOi4f@i=NvtSXR z*k>hDIf^$Uge*q)G2lGc+kkY~t0nzRwk17m*Ro<|@=nKi%=+IU6;u^Rjv0K6BCp(; zuk?d60TpJFKGEbRg8mBp00ASJPOSxU!z;NqH&L&}Z1lM5PT%eXmmeIhQGv3_J^8S9 zD5Zor)a!h*7@|+XYGb?irDVLMK;)Dx?nFUmCa)hzLVK$eN4K~o0XnC<_eDH!p%-ln z=a<`A^1^nAXO-R85zelMZ`;X1RYOHz`H)YV_~Wf<92y(mpF%R9VH4rz`0TnPQ2Wmc zZuB6oBzRfWKlO-Bsb(lo)&1M~sqkao3xIpN??$G4J)k*}F)i=kSf-n}*(iCgS6g_I^KFt$tS^6JK*2_4W?AUM(7j z(XEo7oneqJ0r~AVco3-ng@uR3>88}E+;WO4JmS@z59K2*MGGai&$>-3>Q(M72#@tx zKCb&%!H$quSoQ75-*vBqHe2U?bePP%jSH9->zzv|NZ$@!FOOG?PGbXyL}{saFV`KG zw;_LfW+vb*J&J4-JWAYapa2fALLuuwPsM;l8E;3=q;r@k-ypS`dGb4piMr7th>B*C zC=2HoQWR(82sMYiomPv`IW?0(nazu&rOWj9vXrYWr-@BvT{93O!xR_ONanG3Q?%#% z<9g(m>lZkgqT5dEs?TzTk|Uxkc^JjRLd*_7@Jc1cTXkL8zS?J$th%AwJ5dU}VC5*9 zI?*q&;;~Bvs_HIZT`RfceDL<+@MA;k_zR>A+CfaYINdA%iw=MX4YHTAVtiU$;ln9k zf;XC$5LZyg~64#JNy5nIV9;9ftpeMZ0Ku9A5T$8EzfK2QpPKO&#SYxD{>fx zR5=0A`H}XWw}?quWhkt!<5n3Gd^5Mg#R5t%yP|l!Th1wuoww(d2bAPLQ6wX`Nn$OKxyKsxuk z{Dlb!=BSha7p4%I(@M=CAIG)3m96nSq@6{?MgDV8Rm2A!cmJ$7LEb(R62jjIh&V?Z z5gYoBB&fbpb_@&?l1KxLq2$UJntc=f%)!hN*mit{GN=#Gi-jyhwh^G`RUm}@p90R^4U}xvh{n-yg#jKCQLpIChnU@xYbo{8 zdtr?W)E_b>|0cI3`mP9szxnzIHcB1+8wbnKa<(Uy>QQ9~m*?%D|F476_~bx~m3x!b z)C~>4tEl+<9HEfubEUmi)Yxn)^_=6NU`U^0Q11n@|A#Fcun|4Sn75trCTJ8j&&A>cigbB*?ARrODD2dJ;6$0{zC2mL$C;{dX}mmtDe-b}?fiF|!-?-LX` zP;&Qv{x^wRst1lQxS_F-!p~oi_@GiUpfj%JRm}BRj#wHL!u?S@J6MA$5BeQ=g)w@% zDbTupyW)MHddQ1&DET>$zgnrY*!I<)N_xOMFgfpL(No$lUw#N`kLA<{ZVo5+sIhZT z=V(A^e@UVL*!Q1*W^jN1G`!z0eCN^VyNt;kQFo&AbY1jv%*Zd}06B~rrYS*#FKre? zA1EFZyOB-!>vUKp;gHKF_Z$Pq-aDL$_Y>!Jt?^H%Uds8+qvwT?=kj9lOL2hvpZ|}T zhW}yamuxZkZ5AJlX{)LRh&gRsGfk}~zIa9Q!}g+!8>$FzWK@*$j=e0Rds3Xg_$ANJ zJ=%ESvdi-}WW9ruyK8f?Tv7El_$JAPG>t z@f7`!AN^cJWk8#wkS9uoq}E+@{a|7+ZT>pX%CD2`k08~82LlYM1p(eP%0JL<1;)cjgTo0H#*!IhFp zt#Xf9wXtUchjs-GsQ#*W4EX<{&hAgUfer$+cpCq=A3^HHvkBzZe;2&{YGd?7d_X{+ z{Yvme`iHK5!bea(pf$6=|F@XUnY;VUi?i0AS}5AXxpgMt7+mjb=D z|D78IZZyx!tba2gJtN>jh$*eAeiNkt4$+j}@prv2;6cDlWXw{Rn1AE+F#wVOySV>u gABE}E*=6fPF%y~W_6NYfKf-t@tt3@=-^BO-0Tt5u%K!iX literal 0 HcmV?d00001 diff --git a/docs/images/troubleshooting/clipboard/toggled.png b/docs/images/troubleshooting/clipboard/toggled.png new file mode 100644 index 0000000000000000000000000000000000000000..6afa0ace793db75be3e30184f2a2a2d96ea9b4bc GIT binary patch literal 17312 zcmeHvXHZmI(=Lnyj36+eh9J3tlm6RQIf&OrNqU+z`(yJdsh_$ z0}BLv{{+DXK4*;ZaWF7o829cX)ZGo&CN8^c%r$Oo`pH7n{ADpoB(Cy8ti4jRi>2Ps zPYTh}-ORgsEz=UGY`~L%UjLQjvI85sQ4RsF!d0rOk}G}6t-X=|T+_}5>lt4qibWeg z5+)h#GgQB4=v7*^N9nfL(0nW+CeyCF@!ewze6mZ^Asr^d(l=VCb>M6;&1yu!?sqb? zf{FVa?EXXs3U4vW=sG7#gJWQVp}&3P7c4WIu|U{3H?VL(5STy4e|=obZ<%bYhk!zy zZI?*@>*|ju9$^N+nJ)eD5+e)_)dmGBkP-bh>)%4aAP)&(k!GmyaDtI~`%-7>J20ZP z^Ml`|$iAh*WTpy;u(R5$*3sE1ALTE`5b1&$&{Fh5&4d{*5Bj>;MF7*7Ss8Q`eo=f< zOcKam{<1uIdjlMX2LhvX;5oS9D=4%mvd`DJ`K&V#hmOVPSVO+rZUO@r)-y7~T<=!Z z{$cE#V%mxpaRDln1aaP?A+wL?K64gRza|)0)gSg6791u`R?t$C%6?=GCjsr1(yHW5 zv|H7?p}pnE6Z(lV94|oK$C;stwYOrFdKSP;gb8K?q)~u?%Wu7QTCng$?(WKz6LU$q zRdK&A+ibixHP3h9QE0ms>g<%TV<@MW1N>YXnA!44bojLZew0?nd6P- zd7Z7?d^$b%UUO-M7j4pE3;$iA>7k;;JZhrE=P+1kKpAubP>v&%;g~7)E)}XQTPzT4B1&5)3QS* zFyy9-X%03Y#&xBR!St%_zitZ|B&~su2DCUSMT<>QF-))uJ)Gk4i|(e= z)|%%2nxqUR{~VHg!N%INTDlU&Bkh$_Uutdkd}HMHJL^_02#0Yo@*B$CpU&Ohi1PI* zoJHY~e&icccwtblH+PgMkfp8rwIOa3=CIgvz1(KVZlux6%`o42W72?ywO(FtEG?x{ zp0PXLfd5`wxUiyhn2>~hYg^bxXVQSpT-k@XCBxw;8`#zgVEgZI+AtI9oEWBCfV+GUF0B@5>w221@|+h?pi=2P+Q zI|kG&7F`PV#A@78)LvcpteAz<@jhBY^8$*b2st9L@MvQ``0lzJ3Vw;mi+>eP&A2ky z$imJpTrrB??iSawtJ(BqU$q&KXr3##nqg`@i+Uw({dM~8l8s!JhOsv8)!T)aB3nD& zf>CQYbWCA9>yd$mx=h8V1kvkjUb7*EY0;t#(jDR!zFOE?eoU8+)?+%JD=mdOzxX`W zz)r*2%(~_`7S&2?o@*V@vU*1i??T}41zlxIx!$aQ;7`3R9zH}j@%?-Fa z;0A~!_N*r zh{xDl6=H=EB8)lAWz3haTm}339tLjRTQfeaY|wDsD~T~F72*8Q+;d}sKolWXN&zdD z*?4hjfvzW@J)h z=F-kYf#zE;&wS>g9gkX|zF1-__}J zeo|DF%+e__n0y{V$LTaE!mPG@H_7n$#g|lWc%1$LzE%6=d0yQ4-Ad@sI1gh7@wHk% zT#U@*-agupciQH!-F!l~GB@edZJ?^gb9erHXQA9>Ow;O2#%cNp0=lF&cN#b!$u4VJ zQATh71byn(7P=HSuh6i=?c?{_e2|y#CRN|q!<*Ls=rPceACnuW`r7O+*v8ADLK_R4;tf|D9 zA4&}BD>J?`O=$#xQ9-u}11kHd!G^Af)w8uzXBM0In@M^4`Zo6B?tNPWoNkwZ)@pv_| zwHJGFejn-V^CQt;8?zqR2k)#k$rN~1-+RBZ@$t+nt;*_dU`x`?4YEWJ6mPQD2@yEQ zZ+?~w(ja3O^9bKQw^=#n=`TRGK-TZSh@X{dqV(gn19OMEa7`~K`Eotn&8XJw(qyH>aisoK@fnb+E%bEFY9PB9huQsYgq9I2y7>Mmo}73H>bQ?doX5O{lQ*k zdoQ$w3x|d>Rl&ky)xnxMw~ppc*FwwLr6bi|1@E`(QQdc`Zt+xu5P>af!D82ufnN7Q z(BFLH_qs=&#G>oy8rT|jabzPpF#8HC^!2c6)Cg8bSXR<%?;fpbzUMmPYISzr#h`88hno?>)d#NFxaXMw6OX*>GELIbG3?3TRcKV z!Gd&jbndn~BB14-lX>Sce%W^=ESG-q&>#~**5nXTYUlb}$Qh2z>mo5zozxhm_9-r9aQ`uW{+xRWu3kJ~tF z{5ws&whm&amc+3Qd~2DXd-1I+m?YSf+zaN3;^!8+HTyi>(%mVCm)P%|pvDIsg6$!B zxbzUVP(f%`EDK!{&w{2-F_zu3DZ!>);y= z|70*q(6wSQIX#?e3r0yjlFoQU^u(jPY6^UB0TsMgS5SD2Ljba|vB^fShinZVRcGg0 zg&0++9eEDJdbCXvOOj1gt$8`KHDH#|J9S5X_tH73IqR$N9=lq}*qo&I(#f6s`AK9A z!H`P(@o7q^&VsG^a(+UE)QGkwkViyRILaJ|_z6arB|VQ{P|F9jD=2UIQu$k^Xrg zh3nNCOXQ5;dkY_sH)YJ|HFZ%)I;zv-inpU^&gkA)S+Y~+@04E|G#+qZxS}r4=2u9A ztpiD~tJ4+y`fi4Y62ubxS}$-QF<_=GELNU#Q%oik8fs;#{sYhQu~=^Fo}ltvt;EVb zq7irX;G(G=o2An&rX|f@d6p{J>#Y{Ce34^6xd=`loxK>E$o7_}&QsNT9a9DjW}M>I z49bScT+NLoJ*R5@=o&t41--yj^imEVSg;Q|Uze36@pvM<6Ve=K-J2 z@;5Vb)}<7Ce9gj@oO4CJ+d~Hi{mY1%IbIyp=D@QY0$ojZp@Fd=SWmuf0Nj`D`p;_3 z83CUlC=zQ!zS(C!@iK7zD+>F^%d>+~<3T3%Cw^Pv*1wG33cVnx2GTyIokQw+UGWL-uaB`&_X^h~iXhjv`AuFC>EME9#$Kc@D>^|J$whMP zOVo&erjqbScnwFvRgP0W;!dKhDyjGffpp$RN;EwDTwsK;aY2B)Lzydwae6^hoV>&; zezg&0^vCeThzh+%&*E{X{l=2(X8b_S^kF4?Tf4Ec6vB&L41L>T`^3LPO3{I2on8BN z`~F*~OXFTBMgI=flL>;-u2}(ynT0f9JbBjVsvr1Nr#7xJZ;a@TvUXIplq%q1Nu4kF zALZ)b6=qQps`T5lF)_|6mU69bVmu%BPM4fX?Vjq_nzf4evmHAsQs~v3V+^Rp*2xTZ z9_r|^X#5QMvPU)*yF55;$urRLuGDJCA?1cN`I0@|EeLOMU5Mdy?=|5|T&~j7l+`>K zsh_|1Pl_wlXPIs}7C+)xSWEo8CqGJQE6Bj5>o+0U=HWws({h2LE{(`CU(0~>CHm%v z&;UF9LC!n3hdxrE&MHE0{$dHn!C#OqrA>9xZI;)Kw#Ad?7GFg~k5qT{(KVl^EQc3j}_hN((z@6LukdvS5`J-r~F-cGI$Rb~tjw&y&c zwM%A?QA0+4rYHXG3Rlco%m^sB?=~G(^-@d`teQIx8SHn=M6f&A*5P8>H*9*IVkb+C z7^ZfR$9xku324DxVq_rHXK(eSVLUCwwniD%?kNjcZD=ylkc^5?RKiqF8EE=?3xfQk z%tv~*u3BwaI%T-jKV@+J`ayFhfN-q%%~7q-LInBy*e5Wir?mhNbWFc+YeVw zQWp7)l&@*mKRU={N^~5S-|Up?+7O5zg;xIZ6{QXjg$OHB+3%Q_0(w zPLA|j@dkVB#hXMUm76||R~ckzGCAW^YuhgovUPNT^1SgtPJBredPJ7avJ)reUPJ4l z>)T3L_B|P>(QLffa%A$p;jVheo|g<(mcaOdC2s! zA!VbNa*)NFI_^eL+fndO1bP@Tt3clm z$e*q}3*fQDG~)kgZ5 zShQ9pRZvCkB~>gGi$z(ff-|w`MuXFiFju&!G`kIRiE>+D%eQZGM)slV`UcqCj%yn}=_a!T#S)8l<){Fsr z$o?-nLhe2s%a@Oj@95JT>!^^!SESW>RDsFK$(Ks8)iO9c2n zcZp|0823=LI6<-yumKnszDBOfUE47SXQS6=&+Il7Gj9cJeHrl|TJm}lhWz-ZlM!;o zj>L!CmC4xPo2XAG_jO}!*|cBjOM3>u17Sa6rbH!RAYf{kC^p<{n$QHPgh&k{@DuRN zqYL>ZE*#*OFtDF7kv0Giy&=3#bL$2`9_@sZB8)w$nX!JBl^7^*Vi=?=w>N|RJqxfS z7cs~;Z>hFGKr1T|F?JJ`tbT`&Nf%dFW7^vGC7M6r{YFep{a}zp#VSkgfB^={lzK@D z1z8;(d1;V98});fJ1xYyCR?~&2<-e~u@TkhJk69+Hoqbg6rLdu8g+TvSL){y-!W|l zhJGhoTs+YK4+W}fIN5n_1u&f`j?0y6RxkE09q)3hlMh!L@v&aI^t>R}A0YXlAncuQ zw=e2_FXN?G_Ri8oZe)^z9?>mXK2Y*Td^M`YQJ6Pz!s9OJAr9aE@gJD!eE=&BQ;x7@QaF`5WB!U6Cy8ll- z+EbyN9fMy;L;I{U-mJp#&!S@h9_GApUkUIr`+`3!ck_pGCb_DAR*n_G#lYVB&+Bs& z#EgZygo7V#8X+icZT-YLY~J=wN+@+K{RQlQL|3RBjQ-Ony}49;s@WGb!V--iTOlwN zYEWh%KzOA&?dg>Jo{semAE55iM}3RTAp5s5T`Pm@=p=p_qT{%k=H4K3{ucLb3q7q( z-4p+0_#({V0{!>`qzo+(xqb`S%-{%abc6?o=TUF4atK>uk&nN`uZu$74 zGq| zivcZJ_u{P)(|sHfiICB5|aZ&S*vj2M(N*O;Fbw60x_+3onap!52NUbGwtuXWL(1v z0Ha<>hefL?9TqsRXwp8M!Mc8#U5)p{~u_se9+ihdA-29WSm6g>; zZ_a$o-xUD#{RTn=v{?0*#O0=W3NcoNa5l-3vU_r6yLnv31asevEQ6^OgHyOn8b~qh zUIB)2b4hgMvi4o=`dA(BR)*6x;^W% zTSvMo#|=6|5l!=AZ}pO*Dl9MEbJC6~?$c+pFs@k5t6-+WTA%L4%#5ttC#*@eNgZ7? zG#4yzOBq&;Fy>uM?t2LKyPy6SrBdCg2W%OsTurF)%N1t3JTnh>gQg;%RQC<>PI>lg z#a3lq*}bXuNztvA0uQ9oKJ?|wjg98!k*ZHJbUy0d8pc9G($EBg_p9-l5o1;>B9}fn zKXzZb#gHm=$YpdH)Gzol=2z0|5C008<%R6vKhP#h_>?$H)xI>tr%#%s3CVfo)3lrO zia3{>`%Ah4pTNW%fK0<|bVwBs#!&m-ny~(ui2HZg+LbqDTeB~ev%he5YPPXAM+@30 z$6}8SPG_yAI=qR)XCV*2=G4+3HP$AnF}H;dnwtrb5oe3YP{YXqIs@O^Mx?nU)UFK~Aa9k=&%aW1_{mxHoU z6QL{K>sE!azB6WeN~ZIwfj?ixu6*%E8M1C3Sv-jhA2b<>o~TqeZ{${*A3X%@c-r@^vi$U@zWug-hMeb)SqG`c0(pV9K81Xf^?-k#3w32ky` zvtss>iv5uWK5fDPrP!)6m?}r(J@ZDtTw1v#@edQ;&E8J$tIDRTce{&28CJ(4Kl$87 z@6}cFS+P}mn$u)fDJmG!^6<$*^FCLPZ9RPyZoE}PaXn#(2QeR-Yh*jF)DLQ@5};@+ z!}x5`#8gLztZ%%sarkon#fKW}Ae_Ur#g{jT|LFy|Jqo??j&xgJ;HZ&*d1H9+<#1ct z=Vs6Q7Z1~GueKwh2@rS^t)q}6C)%ZkgUDg=x~c&^`1z9?iaAcJwqs!$D%_{7cP$@+ zZ=zp2yp>zv>Z?8(-kW2lMOHpG+uJ*^^$UMu{_O07WTrbbuAy4V@O=`vE8`}Pqvdo3^5M8RbyyqoAiv2!- zq%j1IUO_0RLK7DCjMbe4_k_L$x$nL&Ev79zV>5~N3b}-Z+T&TX&CHo$5$NQ3Gq0e^ z%04q~d>sKq?=Kl<+JR;pu0b}E<*`%VeBIqz4;xO8U+{n~`3Y~qelkKBpb==h3p8*4 zJ|XYR!m%QhF`K)``)kCyZ>g&4?327L#VbCagtD{UM|^qQ%pP0UFJ;%GHC-E%DZmCl zEQ8sn*igO7)T39ah+V1><@3PGY-N#?uOZL+^tpR@MNW5ZfeI`XYcn}XQ)GDD;HE1@ zk;>EN5k#>4?$N5P-lrEwGec+U%a^%>Cp6z5RmvEVBpqq4g<_!qMu(?cL^7$Gzcbms zNcl)GC*bYZ)|J`A1+B-;e8Y@IPR# z4|`N(*o@9YR`0=j_Gy9-6FP)Z^REY+{LPf}Ly%;I9u=-_0dPL>@|6}fB|)jtQC+i@ zL1ek<2LcLS%Sc~@tjjLq=vL!qTk=sheWn=4*ZXz>EyMiA)dcS@L1(BNwc)c={@KLf zHcBFK*3V0}t&7VIYM&Ms1cP{$%xZQSeREPpPo5*y_HaRKUvolCBS}yrmqDi|;}Q+C z9^ZU*WWRG0>x-@7Zi$+fyAgnV=Ii#Fi0(3>jXvN475Oehc zKH=m=j^ST>soh%c-m|mqsdH-78|yCv% zn7cx7+*oIAk@AJverU~yHz3ZJRr-OA9*ubuRp)Nb0ok#X_by2cxHq)P@A-`s>^bnl zI-z)AX)H~<{u`_HP!hWR_k^CIf|_f%uQXje-Y z-kh*stBU{HN7f8ev9%!6ozLE<2yJ_X2!OAU+OH|<9n9764WB4P3eK#M8IM0mb*ztM zc%<`G{*trM1HM3dbVLj)$x0Nr{mXT7Z20Rcu z+0%o^w~jjaO}+9ie`@NJKh?#K7Z=t}r)+>Q?&lP&?ZEHY27 z(rx>+JvmAt^uy-jE$SNhhBrtT{dY%mn`Z`%p3d|Q4f=6zrt%*5ayyR{K8hX|C)}b1wmePw?c)z&Z;#)vPYzcklH+- zLCYffrWw$kiVah{ghQdRrppTO`xfOBel?^A1{}-r%UA7#*>9AmFyw+--dB}()i~d1 zVjR|dayx`t>j{=s;RN%eR)PN&8<3Py=fWaUWXU4!vZ-(`hwhVoo@Ksc zL9e~dogJskpch9L>7_u!TXXIF1P6|3{6?a?=w7Fm$h@a&4+WmvRYZ}M_u2vurBlfH zuTBG?qSCqAZUrURI~5r08=F<>cCm3-b^UUnSXbtlVh2;g%?KUyO*Z$!epDd4u@#6V zx(DN0v@Y#o1({->F|{GlWn`6lF`P$ZydFod?{X!|LG$E7o@hNoxYS{o%cXiX?tP>~ z29u7b`PL^)$%FIOuP0dO>2p{=^1B)9d9ol=p2+-qZ;RKU{8fiTlgHSp%soRes=$Ng zipYA6>D-8g--{|bZ}pe}c$(d&0)eyIB30=H#{lm{1t_qQSt zR~jY@y<`35)*7g%RIb`?F7s$$WaN5j%ZPnrnOK2sa_D*(mhWuvRw*Vo#jCUEeoV~~L(Ii7Fwk^?tn~PisV{`LMz=u9F z$;JDD(jL_S^t*M9WoDW{9;*ApxL!3}`!7!5>_>0hR6-9uxcoZ6%;1|XU*OCBlaj+C zfO>x0lFJeoq;m}zkDg!S*Wuh>Kt>V*mHZ(WZY3sa6%PtFpGu*;us!@|!1jEA4NyAF zr$(jxm@zc%`G4IJ2Hasco=<|($IF6(4H-M*#TlbyZww)GeJ>Pve^C|?W))3MroXuq z>MccD84Q#J8H5HLPA6_CcC=_Z(okF0bF6gy?^$jMqKFul=OQHu;xr01WA&$&xIoz) zP$3BQ9m~j+U&Ik6>m_*~>|Vd$XGR4;Rah}sRtV#-DvKDV`I!WHmz&BUDwCz@{ozMo z2^>^`H3>GYSJ!Qz{Yd>-L+40W^4`C>Cs^cl)Kgvo`&c)ATNt=2lIuNvu{2J z^w-(JZ^VTr;Fad!3klz^LH{?}P=6ch?Y+rkIH=YO7J{Py_& z_319slCf~NCHg<2kPR3F(RB#hr_9`;U)cQmmVF~q6?(^^2PU|eV49Uzzv87S&eW+FhzVfb9^1?*F;@CrJ zc=45#JDU%5>UO@uNdN~@iZkm>gQ(vj{I|FMwVd#IFq2}PIvywX*KU_>){C*Ho@g`(xsm ziD6W_$>D*;Pj@N;@Ge~jRR{{_vnM(M zEh&VU_`g}{-OByEU z+^YQ=8avG%R$v6Q*jlTR{lU24qd)*`y-y5&vi;5K559sDA%a1!2tdK8uMO^$f6__$ z#{l@i;rt!WvEPsXGDD#{s*3L)eC;(0Y{rQ|pY0QboaIU7(DL;bstyU z`nJ`Xt$D@~p&+6+f0`ds5jM1~IbD-%6ltdA+uPT~9-)8TqU27$5YCNQ!ADVS8fShVA4JPqp()0qHONE=>}PQ~<{ZE_m_DRKDO`ZSK%HEjUeG%W zyUhMKQG{wBmeqnTUc)|{z3H|x7St%V1OcU=hN15;5l7*1)M%F$7&SLJ&c$lAf4rG~ z>t=-5z4ONXv%33+z8+N~TkWraCg}B;4;hslcpq~$)R+l%h73bXV_5rO-=n3T4Oqkt zc#aNpimXT_+8Gfgs1kQPhj;5NZu*eYrSq18*!Daosx~|)rGagS6cFrs1TFIKQ%RI9 z7i9~0i{H$Dm({9#d~LADV!)lj@;Q<-+@4*7vx&t?+K^S* z^GZ--iKVugb#A_0Zu5z7b14gN^){|mQj#`_1^B_e(=U^KS|2Ail^$dpH>}x><)&(m zkEDg+Af!!}R|G=FDQsSh?Drj;nYNM_U$+kWhU2K{-C68J7fQUQ=T}^Ks&HB!5hR)= zH%hs>LH=$uZ_vx`P$y~3u~0S;O(RGDlCY)ZMS z;$Xvyme8(NzXXjG4ewrw@nzfk(ZLc)2U_>D8%hq(<2pTbc+sxBDwFZqfnvj#~#L*u$htUS1Z7c`q&Zqv1!Vc6P`Wk?Mi{q0?Z!&mPV~fssIJOqVDE zWC4e4K)x4RZFG7F<$~;-B_?;%SK|K@Tx3bVp+MdHn4)SCD&jmL)*umak z%9f^%&-k_)Hw+4c1815anJ*&WzNZ3HvVP*_HW@ zhjGO}3`Fg*SGARf?J??wG!o{JBs_b)K%eO1^y*Z0tWh7vCb1|#i{Nr`Mq}1HcOp1rE)ZrZu~pqspx{}N?C;;wMKQaIKnCIN?adeu~ z3e9FEZ&1(tHbG$5+ALLBYB%3bdc3k1`gT+FK%cT#OY%YD>@_g9RiMvt86)|?^P2$W z;*<_-02%(^lK}X!M-3-XoKCJD?_aii#F?>#Q2RTTfL{gdV&RibWU1e2Oia-isq1RF zz8|(HoJ$~;T3H;B+Z@o_eu~JezO%i-J9u|fn$`!V`)~mYL@tp4G$F~dZL~+%`&qQU zuX6dL`0N&8ulk)?+JqbSYA)l3R=Q#4Ml|sU5kwK?3Kk?3to6hjhx>0>Aqqvb3Lhv? zIuOu|USinZsH8>u_sAO@@oDk>4c(Iu^a`2M|ws(GZg=xbJF;$IV6!U3fn>=uw zi2+K)1F4N*sbCoM{*#ExDZwfb(>S`&JEAj)j5@x9b_N-yf$d4J8 z>_$s~^JUMThj!hOT!)xa6%Oqwo;%1H`OH<&_poOPty{qc(t>a> z>1|#XT)KW*vXR(NTw6HsF^3_U(7P$OVPYGdSZH$4M>7vBADgl#xmF; zL-+el2J$8Ks2Ack#`%5xtjwgF1@zI_I5n?tb`i>^NdX(bBm4Bp0=lz%C+ihk7i+ia z8>7hd+S+yAU50ew#<|zaft>y%e1q{ni6oC{jrFv#jg!RABf-V`@$Q^?JKaIQTeI*ydBD>wpQe3Xdr>WD?=P)Y3P8Sy49AZT$rB1; zTmzdNKscwSXQpM3NvKd#vZ>LppBtJR7n1CC)OM{4fXDZ|cjcHBaDyQ|pL%uGBz>7x2 zVC=-3jEA9u5a&AW8{#TJfp1D}iS_T~_v}aO*g0{lC;}=ACAVw)&in5>ek`gSP)L3q zetujY=|C!S;gz_b^`i>>#~s)qFs>9b<)8L_Aw!*g29*9aC?Jboc;A2F?>}<^4|*=( zWHhS0e7q9K?$*ZKW^>lB*)T#Bh{A{d!4<^?3KSZhd!j5Jhv!!P*n_<^&8MlL`J~)4 zqd{wgr*u@x^zU0UoUwogba}G~T0zX=X8ZKY%GIuW6Zx8Q(1Zf&ji2SxbujJ0!G@#s z*gDjHcHtEZSfqOX`-v@e@jn@8^wqq)yhzMT1RZAuaDb`HP4{J5TU*02Q-W%%v`U%& z9h5UvuzZJ3P}H@4_L=l9qtv>O5$#>ep1 zf9gt_lL3r5=4e-zz$(>;KjXau1? z&^m>F`Eq5zm&BIQBxCS9^v`M>K<;MMdK5NPm`F}TRY($4q3zl~4d^$cf}8wkT^V%m z{d517D$x9=@GLC;mv$Gv-C_f}SKd-`>W2JLlOMjoT($hw0jMqR3iD%Pb$^J*>nE{1z-u>87FTncG)Ysww?=lG^EB>+X zTu|k2x`7c;C}!9l=u7#pYZ$1v02XXTn=h;WIC9nvfHEYe;{53{7w=Nh2Kf{gX4z`5 zeV5H@EPAc0^k=={KvO1g3ZwQvkSRex!hK{v z?oXi(2Gs_ZnTv$*zuL-v2&Gc<8)gn(h(D_hZ&C9nK`B80IKSMFK35>XbNq1%IJBMm z=Wg;#BcSgT449a_apeC%uz^usf3Of$+9i4nj7!Qt{x1MvUH>Qig29p-`myn%5~O6hKigmJ+C0%F71W&i*H literal 0 HcmV?d00001 diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 204cff09..9e607942 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -18,7 +18,7 @@ if you need help with installing docker, follow *only the first step* of these t ``` i'm using `nano` in this example, it may not be available in your distro. you can use any other text editor. -3. copy and paste the [sample config from here](https://github.com/wukko/cobalt/blob/current/docs/examples/docker-compose.example.yml) for either web or api instance (or both, if you wish) and edit it to your needs. +3. copy and paste the [sample config from here](examples/docker-compose.example.yml) for either web or api instance (or both, if you wish) and edit it to your needs. make sure to replace default URLs with your own or cobalt won't work correctly. 4. finally, start the cobalt container (from cobalt directory): @@ -26,7 +26,7 @@ if you need help with installing docker, follow *only the first step* of these t docker compose up -d ``` -if you want your instance to support services that require authentication to view public content, create `cookies.json` file in the same directory as `docker-compose.yml`. example cookies file [can be found here](https://github.com/wukko/cobalt/blob/current/docs/examples/cookies.example.json). +if you want your instance to support services that require authentication to view public content, create `cookies.json` file in the same directory as `docker-compose.yml`. example cookies file [can be found here](examples/cookies.example.json). cobalt package will update automatically thanks to watchtower. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8241ef98..28ff874c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -5,29 +5,33 @@ if any issues occur while using cobalt, you can fix many of them yourself. this document aims to provide guides on how to fix most complicated of them. use wiki navigation on right to jump between solutions. -## how to fix clipboard pasting in firefox +## how to fix clipboard pasting in older versions of firefox +``` +🎉 firefox finally supports pasting by default starting from version 125. + +👍 you don't need to follow this tutorial if you're on the latest version of firefox. +``` you can fix this issue by changing a single preference in `about:config`. ### steps to enable clipboard functionality 1. go to `about:config`: - - ![screenshot showing about:config entered into address bar](https://github.com/wukko/cobalt/assets/71202418/9ad78612-a372-4949-aeac-99dfc41e273c) + ![screenshot showing about:config entered into address bar](images/troubleshooting/clipboard/config.png) 2. if asked, read what firefox has to say and press "accept the risk and continue". ⚠ tinkering with other preferences may break your browser. **do not** edit them unless you know what you're doing. - ![screenshot showing about:config security warning that reads: "proceed with caution. changing advanced configuration preferences can impact firefox performance or security." lower there's a pre-checked checkbox that says: "warn me when i attempt to access these preferences". lowest element is a blue button that says "accept the risk and continue"](https://github.com/wukko/cobalt/assets/71202418/02328729-dbfe-4ea4-b2ca-7bcf1998c2ca) + ![screenshot showing about:config security warning that reads: "proceed with caution. changing advanced configuration preferences can impact firefox performance or security." lower there's a pre-checked checkbox that says: "warn me when i attempt to access these preferences". lowest element is a blue button that says "accept the risk and continue"](images/troubleshooting/clipboard/risk.png) 3. search for `dom.events.asyncClipboard.readText` - ![screenshot showing "dom.events.asyncclipboard.readtext" entered into search on about:config page](https://github.com/wukko/cobalt/assets/71202418/7c7f7e3c-6a6a-40df-8436-277489e72e0b) + ![screenshot showing "dom.events.asyncclipboard.readtext" entered into search on about:config page](images/troubleshooting/clipboard/search.png) 4. press the toggle button on very right. - ![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page with highlighted toggle button on very right](https://github.com/wukko/cobalt/assets/71202418/b45db18e-f4bf-4f1c-9a8c-f13a63a21335) + ![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page with highlighted toggle button on very right](images/troubleshooting/clipboard/toggle.png) 5. "false" should change to "true". - ![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page, this one with "true" text highlighted](https://github.com/wukko/cobalt/assets/71202418/4869b4ff-8385-4cd3-ae59-aa2e03a58b5f) + ![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page, this one with "true" text highlighted](images/troubleshooting/clipboard/toggled.png) 6. go back to cobalt, reload the page, press `paste and download` button again. this time it works! enjoy simpler downloading experience :) From f48a1d9af6dcb7b68b93d5901174a900e20e762b Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 19 Apr 2024 10:01:25 +0600 Subject: [PATCH 15/99] docs/troubleshooting: update button text --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 28ff874c..4c97511f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -34,4 +34,4 @@ you can fix this issue by changing a single preference in `about:config`. ![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page, this one with "true" text highlighted](images/troubleshooting/clipboard/toggled.png) -6. go back to cobalt, reload the page, press `paste and download` button again. this time it works! enjoy simpler downloading experience :) +6. go back to cobalt, reload the page, press `paste` button again. this time it works! enjoy simpler downloading experience :) From 1ff49f0669cbaff4689024972e53f18c87a845cb Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 20 Apr 2024 20:33:34 +0600 Subject: [PATCH 16/99] instagram: use different endpoint and fallback to two other options --- src/modules/processing/services/instagram.js | 140 +++++++++++++++---- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index b5e2228d..d27a5693 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -60,39 +60,99 @@ async function request(url, cookie, method = 'GET', requestData) { return data.json(); } -async function getPost(id) { - let data; - try { - const cookie = getCookie('instagram'); - let dtsgId; - - if (cookie) { - dtsgId = await findDtsgId(cookie); +async function requestHTML(id, cookie = {}) { + const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, { + headers: { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Language": "en-GB,en;q=0.9", + "Cache-Control": "max-age=0", + "Dnt": "1", + "Priority": "u=0, i", + "Sec-Ch-Ua": 'Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": "macOS", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + ...cookie } + }).then(r => r.text()); - const url = new URL('https://www.instagram.com/api/graphql/'); + let embedData = JSON.parse(data?.match(/"init",\[\],\[(.*?)\]\],/)[1]); - const requestData = { - jazoest: '26406', - variables: JSON.stringify({ - shortcode: id, - __relay_internal__pv__PolarisShareMenurelayprovider: false - }), - doc_id: '7153618348081770' - }; - if (dtsgId) { - requestData.fb_dtsg = dtsgId; + if (!embedData || !embedData?.contextJSON) return false; + + embedData = JSON.parse(embedData.contextJSON); + + return embedData; +} +async function requestGQL(id, cookie) { + let dtsgId; + + if (cookie) { + dtsgId = await findDtsgId(cookie); + } + const url = new URL('https://www.instagram.com/api/graphql/'); + + const requestData = { + jazoest: '26406', + variables: JSON.stringify({ + shortcode: id, + __relay_internal__pv__PolarisShareMenurelayprovider: false + }), + doc_id: '7153618348081770' + }; + if (dtsgId) { + requestData.fb_dtsg = dtsgId; + } + + return (await request(url, cookie, 'POST', requestData)) + .data + ?.xdt_api__v1__media__shortcode__web_info + ?.items + ?.[0]; +} + +async function extractOldPost(data, id) { + const sidecar = data?.gql_data?.shortcode_media?.edge_sidecar_to_children; + if (sidecar) { + const picker = sidecar.edges.filter(e => e.node?.display_url) + .map(e => { + const type = e.node?.is_video ? "video" : "photo"; + const url = type === "video" ? e.node?.video_url : e.node?.display_url; + + return { + type, url, + /* thumbnails have `Cross-Origin-Resource-Policy` + ** set to `same-origin`, so we need to proxy them */ + thumb: createStream({ + service: "instagram", + type: "default", + u: e.node?.display_url, + filename: "image.jpg" + }) + } + }); + + if (picker.length) return { picker } + } else if (data?.gql_data?.shortcode_media?.video_url) { + return { + urls: data.shortcode_media.video_url, + filename: `instagram_${id}.mp4`, + audioFilename: `instagram_${id}_audio` } + } else if (data?.gql_data?.shortcode_media?.display_url) { + return { + urls: data.gql_data?.shortcode_media.display_url, + isPhoto: true + } + } +} - data = (await request(url, cookie, 'POST', requestData)) - .data - ?.xdt_api__v1__media__shortcode__web_info - ?.items - ?.[0]; - } catch {} - - if (!data) return { error: 'ErrorCouldntFetch' }; - +async function extractNewPost(data, id) { const carousel = data.carousel_media; if (carousel) { const picker = carousel.filter(e => e?.image_versions2) @@ -133,7 +193,31 @@ async function getPost(id) { isPhoto: true } } +} +async function getPost(id) { + let data, result, dataType = 'old'; + try { + const cookie = getCookie('instagram'); + + data = await requestHTML(id); + if (!data) data = await requestHTML(id, cookie); + + if (!data) { + dataType = 'new'; + data = await requestGQL(id, cookie); + } + } catch {} + + if (!data) return { error: 'ErrorCouldntFetch' }; + + if (dataType === 'new') { + result = extractNewPost(data, id) + } else { + result = extractOldPost(data, id) + } + + if (result) return result; return { error: 'ErrorEmptyDownload' } } From 2561cf168e125f5931d6dcf2d3ec6bdf3589a7a9 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 20 Apr 2024 20:44:58 +0600 Subject: [PATCH 17/99] instagram: check if cookie exists before using it in second fallback --- src/modules/processing/services/instagram.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index d27a5693..33c9f21c 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -201,7 +201,7 @@ async function getPost(id) { const cookie = getCookie('instagram'); data = await requestHTML(id); - if (!data) data = await requestHTML(id, cookie); + if (!data && cookie) data = await requestHTML(id, cookie); if (!data) { dataType = 'new'; From 018557cbcd8c452eaf02b2e1398100ef98e53062 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 20 Apr 2024 20:47:33 +0600 Subject: [PATCH 18/99] instagram: remove async tag from non async functions --- src/modules/processing/services/instagram.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index 33c9f21c..2f6900c7 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -116,7 +116,7 @@ async function requestGQL(id, cookie) { ?.[0]; } -async function extractOldPost(data, id) { +function extractOldPost(data, id) { const sidecar = data?.gql_data?.shortcode_media?.edge_sidecar_to_children; if (sidecar) { const picker = sidecar.edges.filter(e => e.node?.display_url) @@ -152,7 +152,7 @@ async function extractOldPost(data, id) { } } -async function extractNewPost(data, id) { +function extractNewPost(data, id) { const carousel = data.carousel_media; if (carousel) { const picker = carousel.filter(e => e?.image_versions2) From dd7c7dfa7603943c324cb89ff29813ac18279407 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 20 Apr 2024 20:48:49 +0600 Subject: [PATCH 19/99] instagram: clean up --- src/modules/processing/services/instagram.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index 2f6900c7..afcd6296 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -60,7 +60,7 @@ async function request(url, cookie, method = 'GET', requestData) { return data.json(); } -async function requestHTML(id, cookie = {}) { +async function requestHTML(id, cookie) { const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, { headers: { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", From aaa08830b42cfb13d8b7ab6998d0660de7662e5e Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 20 Apr 2024 21:09:39 +0600 Subject: [PATCH 20/99] instagram: fix single video downloading --- src/modules/processing/services/instagram.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index afcd6296..c8f190a4 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -140,7 +140,7 @@ function extractOldPost(data, id) { if (picker.length) return { picker } } else if (data?.gql_data?.shortcode_media?.video_url) { return { - urls: data.shortcode_media.video_url, + urls: data.gql_data.shortcode_media.video_url, filename: `instagram_${id}.mp4`, audioFilename: `instagram_${id}_audio` } From 50a98c8b6af53d2843cb6f3c71df47b00e6037fd Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 20 Apr 2024 21:09:53 +0600 Subject: [PATCH 21/99] package: bump version to 7.12.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5e9d214..3b0b3443 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.12.5", + "version": "7.12.6", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From 617e4270883c3da4e680af385548ebcf46567cfe Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 24 Apr 2024 16:44:29 +0600 Subject: [PATCH 22/99] web: add maskable icons back to manifest --- src/front/icons/maskable/128.png | Bin 0 -> 815 bytes src/front/icons/maskable/192.png | Bin 0 -> 1014 bytes src/front/icons/maskable/384.png | Bin 0 -> 1856 bytes src/front/icons/maskable/48.png | Bin 0 -> 390 bytes src/front/icons/maskable/512.png | Bin 0 -> 2828 bytes src/front/icons/maskable/72.png | Bin 0 -> 569 bytes src/front/icons/maskable/96.png | Bin 0 -> 617 bytes src/front/manifest.webmanifest | 42 +++++++++++++++++++++++++++++++ 8 files changed, 42 insertions(+) create mode 100644 src/front/icons/maskable/128.png create mode 100644 src/front/icons/maskable/192.png create mode 100644 src/front/icons/maskable/384.png create mode 100644 src/front/icons/maskable/48.png create mode 100644 src/front/icons/maskable/512.png create mode 100644 src/front/icons/maskable/72.png create mode 100644 src/front/icons/maskable/96.png diff --git a/src/front/icons/maskable/128.png b/src/front/icons/maskable/128.png new file mode 100644 index 0000000000000000000000000000000000000000..e8213cfe5828cc4435d15e4da25d5e57b3f2c472 GIT binary patch literal 815 zcmV+~1JL}5P)C0002YP)t-s00030 z|Ns5{{T3D$`T6gwv<-QBdbw3CyQgoK2BeSJ7MIN;#m$jHdIx3{aStB{b8 ziHV7HbaZ89Wll~`OG`^dMMVh-3GD3b=H}+)SUnwoKOaadSbR8&+y zKR-J=J2EmdBqSvB^YiWP?Y+IdsHmuwm6d^kfm&KxK|w*<+S%6WNt=~O%#0006W zNkl~PJx+&|Ey*O!vMtM z0l3frw1Tc_06Oak7&-Xg;QW2S#V`+SP;tw7(*UgV-4J)bEL6tXHm_6jcfe)WL#2`C z^pvCxYQZOPnYnmjEe_e1R;e}afJxF`qSEYw&U<>KR%V+}`GZQW7=tcuQS(b4F!4$* zDxC`GqGF9MM9MbdR7Pdl&bDKpT4M}c!j*R4%@WWZmegL32{75OQCUy3Z4yxH&w)uY z_33g45$FmNq*kl|mydf?-m|GNtW)~{E|o1R*Dc>xsS14TfCgL(FF%k2Y<5N8IAGFq zmQjR!I-$qjFb%Erf4HCZ}!tI^jGABIgZQgf~3>jWYa&{Am z)r<5N26}69JOE|`)Ren=`+P;>w>VzNx{WH5lAQyxuj}pI0}e=@2iX(aNa~vycsO{` ziUCXmI(Z;01Kd&!U>NZ7^Zo literal 0 HcmV?d00001 diff --git a/src/front/icons/maskable/192.png b/src/front/icons/maskable/192.png new file mode 100644 index 0000000000000000000000000000000000000000..8268d89a58c3c6334d887d403c4a9a615343c5f3 GIT binary patch literal 1014 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 nv_N+M?+@W;^HqDlV&cz;Xd literal 0 HcmV?d00001 diff --git a/src/front/icons/maskable/48.png b/src/front/icons/maskable/48.png new file mode 100644 index 0000000000000000000000000000000000000000..02a5bca0fb8b6cf17cd1327fab8f337e61c79489 GIT binary patch literal 390 zcmV;10eSw3P)%F4>Kv$L zX`vgXbYb7&|Gy8rCqy@3Si^0J|miUkRvJqun1Y39LSdjRi*%rqh2$hklVL05Wkf- z420)i%{y@obs_T;;du!lZpk_>Q=EQ(tG5PFcBDNQ@rA}Ah)A*S$gW{}g286#pGB86pToN@>`)edqj#yN*IwQ1${pG%&=eh3R_57YscbXl+Qb^#i0000&)>h`{ z0RVa+p#TDQK;0FJy$AHp?wsS91K!`?S5#D7US6)NtJ~Sx+1=fRLZKoeBD}o3Dk>^p zzI?&qa2yT?fj|ff3a+iKVX@eyr6mIcgR-(Rb#?WutgN}YIR^&^X=!N!fq+J%Q79A- z56{fZOkZDLNlA&9mzSHH+u-0}KtO=0si~2X(Za&Q$jC@hQBi(=esFNGj*bqO%Vn`x z6B85TvfoMi2z&=jY?%;!3yU9s&UWytVmh#~9XfC*|PN z07%=dlBoLUD*qB*n{1Qs*53U_F-NEWhe>+<`vwY5pqs3L^wF{)LAvq)%{xqfykfRa zrkuyjKIQy{hY_*mKi|mME1CP@b!MD@rnT$#MdWK_q}_RDxSYbP*v%*(VXF^_$^}Ku zAofi%uW)Y;0H#$&LcELuGxS6|peBibOiq$y^k2D&7bbuRA+iB>>scP8c&g5nwO&%s;-b@+mY4Fmg&9BMYKdvzemfFq8IK zjHLlkBr19f7FLseW*in)a@Pm31ECoVDq(?BMWS{aFik<6(Y4hs64ls%F_w-OnE*rUS=gkD03q>~H4X^yK|zyz z0K!^u9)$K4nJQNa2e)7PA9$J_G*1};T}EhQpv!AQ?4O01*2a1*@JL_&o%+|^E%w*iro)a=uX&g5tBf`pQLfmFm8oEpuI%7OkGObr(uoo;0*^bb zlc2XBn@4X6jP<)|-i?UQUA06K<1g|kyHBNEyVDO}LNaA0&6g_k*toDLKkD?Cd3xFTB4B>dX%jM+#w$$T`l zz*BtnxWZ_N?P(T^Do3Pig+1#}GM9KJ(s^N$DtBq+sR-e@;^@^6M!McVWj>nD+cT85 zx-z({rW+)MT>6lJIL*nqh&1~oDSbJ_IOP^mjj@!an9_$Vzde0o0)1$a7E^j2N!+Y} z4^fH7o!?A}cUBb~O^Ftgz4eKeeQ3#MrBjauM^1CvM2jhgnq`lt)~8_aB-7^HWQt4f zN!T~F0RdbOpJNGfs5`dH!j!rxm1Bv`N}4?6FmC$_|Ly6komD+HZ2u(+QaHqvc4Bge z_D<($n!1A&TXyrsz_o&vQcx@5gl5A~r{wT*nk12~uRCp!c7LMf8EKPUP!Joj)`f(knlQDN4V(Jr-cPRd1zPe%Vzi=IF?m2CqWT*!25Nkr({h zUv``(F4gqQ59RxOst?9+7T{2#&e-gNH^rwVFkba@5*m7GTfcu}OWe%gIDqPa=0A2q z%@7mYUas4|=(-@*4eb*SaE{(QIeP>XG=EF((jM{pnP;U&OuNeZbCT=wEF~>WcxzC) zT!+EaUIEL2V<+0_s$;A6(p3lj@E4i*eY_tvQO!&EE6psG9u=WEUC{8=*3is0hBaYLs@wc5fVA+~wYo_u$#{WN`2wOK@|xo%Iw9xK zy=W;_OlVkZL!SNq3m@5+Nt_Mu!uK*yeBCX>#{)E@H5JVk*Hk5z)%hI{>8m!&)W(}?;rKJ^a~x9$)Ka$b!xh7r=Pfo7HXGh zw)^WXVm$ZsElSn!(zINpEe#iuN{SFv6qJ)Ia-6 z?t2W11bxF%+i+byx^k2Hwy-~fuEX7|4&3Zfl{oAvXx1`C;{$x;tGwulu$O~e9hR{y^aGi<0rp8>MLr%9BODcrsD!gUvQky~ zG-TMdrC=P;mS>BBAvR-H6lh0G)LV(2HUPPJ6-SdYIT;tE28Q#VHG=j*%*zU*pkWdc zJWv*7JXm~GguM+$PV*eRj1Nk$glmNOUaw7#0=x~+Ml3)QR{0e-pt(67JtDO0r#)33 z0Po5QIIu7mGpNeF|7hLnxyIrBf(TN!hXv%FzGV`Rl^(|Ohcdt;tm&*NSBUMXOtuGB z;T48Rh4+(3o}Zdu5+X!f<=*m*-uPUf9+YFQl1IF=&iB8P)YonqX~g&8b_YJ`(Z5Fn l9R5AAC2@a))Bh{Gf%gF3=`Qc<7AfCH)@KOjHKtxke*@F6^aKC^ literal 0 HcmV?d00001 diff --git a/src/front/icons/maskable/72.png b/src/front/icons/maskable/72.png new file mode 100644 index 0000000000000000000000000000000000000000..903f6bd500691990bc32f79d2192de30a594dbac GIT binary patch literal 569 zcmV-90>=G`P)U1D4i4kv<4{mg+}zyK($d1h!nwJ*b8~YFMdNuC9rRiG+lN zczAdage<5vZq|SJIvoDZf)%Lvw2I#ZK=M#Qy;1d0m_2sQ#amIqLvuF=xisi6$UIuz; zb6ur&hh5M0l9dq)!pXaUZ^FLHW>XW;P5BP`aN`?s&5l?Pn9ZZe+0xgV$Q#-C<2SNj z3;m;=H+pQcu@HK-A3tRDYEO~S*LK?Rog}FBlPt2w6wIbv9YG6rk|gmb8~ZEUS3jCQtRvM z=jZ3-z?PPllarH=kB?<#Wkp3rKtMne5)uy&58B$=&CSij!^6VD!n?b>y1Kff zqoag`giA|HIyyQyI5;36AhcV!LI3~(b4f%&RA}Dq)<>IyFc5&@45AbfM5WogYu#S| z{}0 Date: Fri, 26 Apr 2024 09:27:36 +0600 Subject: [PATCH 23/99] youtube: replace innertube client --- src/modules/processing/services/youtube.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 10e813af..8a773375 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -33,7 +33,7 @@ export default async function(o) { } try { - info = await yt.getBasicInfo(o.id, 'ANDROID'); + info = await yt.getBasicInfo(o.id, 'YTMUSIC_ANDROID'); } catch (e) { return { error: 'ErrorCantConnectToServiceAPI' }; } From 0feacf0ae5b3d816a1a9b88128d1d2e7f3ab4b8a Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 12:25:22 +0600 Subject: [PATCH 24/99] youtube: use web client and decipher urls --- src/modules/processing/services/youtube.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 8a773375..63e02eb3 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -33,7 +33,7 @@ export default async function(o) { } try { - info = await yt.getBasicInfo(o.id, 'YTMUSIC_ANDROID'); + info = await yt.getBasicInfo(o.id, 'WEB'); } catch (e) { return { error: 'ErrorCantConnectToServiceAPI' }; } @@ -43,7 +43,9 @@ export default async function(o) { if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' }; if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' }; - let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter(e => + let bestQuality, hasAudio; + + let adaptive_formats = info.streaming_data.adaptive_formats.filter(e => e.mime_type.includes(c[o.format].codec) || e.mime_type.includes(c[o.format].aCodec) ).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); @@ -96,7 +98,7 @@ export default async function(o) { if (hasAudio && o.isAudioOnly) return { type: "render", isAudioOnly: true, - urls: audio.url, + urls: audio.decipher(yt.session.player), filenameAttributes: filenameAttributes, fileMetadata: fileMetadata } @@ -108,14 +110,14 @@ export default async function(o) { if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') { match = info.streaming_data.formats.find(checkSingle); type = "bridge"; - urls = match?.url; + urls = match?.decipher(yt.session.player); } const video = adaptive_formats.find(checkRender); if (!match && video) { match = video; type = "render"; - urls = [video.url, audio.url]; + urls = [video.decipher(yt.session.player), audio.decipher(yt.session.player)]; } if (match) { From 8771b7d7d4fc854c2e0d12eb10a7bc523730fb80 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 12:25:46 +0600 Subject: [PATCH 25/99] package: bump youtubei.js version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b0b3443..dddc5a03 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ "set-cookie-parser": "2.6.0", "undici": "^6.7.0", "url-pattern": "1.0.3", - "youtubei.js": "^9.2.0" + "youtubei.js": "^9.3.0" } } From 43101b604c734827e5937695ba11584b791a78a0 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 15:07:32 +0600 Subject: [PATCH 26/99] stream/types: proper headers for all http requests & refactor --- src/modules/stream/types.js | 79 +++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 2b7d7482..d3e33438 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -5,6 +5,30 @@ import { metadataManager } from "../sub/utils.js"; import { request } from "undici"; import { create as contentDisposition } from "content-disposition-header"; +const defaultHeaders = { + 'user-agent': genericUserAgent +} +const serviceHeaders = { + bilibili: { + referer: 'https://www.bilibili.com/' + }, + youtube: { + accept: '*/*', + origin: 'https://www.youtube.com', + referer: 'https://www.youtube.com', + DNT: '?1' + } +} + +function getHeaders(service) { + return { ...defaultHeaders, ...serviceHeaders[service] } +} +function toRawHeaders(headers) { + return Object.entries(headers) + .map(([key, value]) => `${key}: ${value}\r\n`) + .join(''); +} + function closeRequest(controller) { try { controller.abort() } catch {} } @@ -53,7 +77,7 @@ export async function streamDefault(streamInfo, res) { res.setHeader('Content-disposition', contentDisposition(filename)); const { body: stream, headers } = await request(streamInfo.urls, { - headers: { 'user-agent': genericUserAgent }, + headers: getHeaders(streamInfo.service), signal: abortController.signal, maxRedirections: 16 }); @@ -68,49 +92,43 @@ export async function streamDefault(streamInfo, res) { } export async function streamLiveRender(streamInfo, res) { - let abortController = new AbortController(), process; + let process, abortController = new AbortController(); + const shutdown = () => ( closeRequest(abortController), killProcess(process), closeResponse(res) ); + const headers = getHeaders(streamInfo.service); + const rawHeaders = toRawHeaders(headers); + try { if (streamInfo.urls.length !== 2) return shutdown(); const { body: audio } = await request(streamInfo.urls[1], { - maxRedirections: 16, signal: abortController.signal, - headers: { - 'user-agent': genericUserAgent, - referer: streamInfo.service === 'bilibili' - ? 'https://www.bilibili.com/' - : undefined, - } + headers, + signal: abortController.signal, + maxRedirections: 16 }); const format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; + let args = [ '-loglevel', '-8', - '-user_agent', genericUserAgent - ]; - - if (streamInfo.service === 'bilibili') { - args.push( - '-headers', 'Referer: https://www.bilibili.com/\r\n', - ) - } - - args.push( + '-headers', rawHeaders, '-i', streamInfo.urls[0], '-i', 'pipe:3', '-map', '0:v', '-map', '1:a', - ); + ] args = args.concat(ffmpegArgs[format]); + if (streamInfo.metadata) { args = args.concat(metadataManager(streamInfo.metadata)) } + args.push('-f', format, 'pipe:4'); process = spawn(...getCommand(args), { @@ -128,6 +146,7 @@ export async function streamLiveRender(streamInfo, res) { audio.on('error', shutdown); audioInput.on('error', shutdown); + audio.pipe(audioInput); pipe(muxOutput, res, shutdown); @@ -145,13 +164,11 @@ export function streamAudioOnly(streamInfo, res) { try { let args = [ '-loglevel', '-8', - '-user_agent', genericUserAgent - ]; + '-headers', toRawHeaders(getHeaders(streamInfo.service)), + ] if (streamInfo.service === "twitter") { args.push('-seekable', '0'); - } else if (streamInfo.service === 'bilibili') { - args.push('-headers', 'Referer: https://www.bilibili.com/\r\n'); } args.push( @@ -162,12 +179,12 @@ export function streamAudioOnly(streamInfo, res) { if (streamInfo.metadata) { args = args.concat(metadataManager(streamInfo.metadata)) } - let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]; - args = args.concat(arg); + args = args.concat(ffmpegArgs[streamInfo.copy ? 'copy' : 'audio']); if (ffmpegArgs[streamInfo.audioFormat]) { args = args.concat(ffmpegArgs[streamInfo.audioFormat]) } + args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); process = spawn(...getCommand(args), { @@ -196,13 +213,12 @@ export function streamVideoOnly(streamInfo, res) { try { let args = [ - '-loglevel', '-8' + '-loglevel', '-8', + '-headers', toRawHeaders(getHeaders(streamInfo.service)), ] if (streamInfo.service === "twitter") { args.push('-seekable', '0') - } else if (streamInfo.service === 'bilibili') { - args.push('-headers', 'Referer: https://www.bilibili.com/\r\n') } args.push( @@ -222,6 +238,7 @@ export function streamVideoOnly(streamInfo, res) { if (format === "mp4") { args.push('-movflags', 'faststart+frag_keyframe+empty_moov') } + args.push('-f', format, 'pipe:3'); process = spawn(...getCommand(args), { @@ -254,10 +271,12 @@ export function convertToGif(streamInfo, res) { let args = [ '-loglevel', '-8' ] + if (streamInfo.service === "twitter") { args.push('-seekable', '0') } - args.push('-i', streamInfo.urls) + + args.push('-i', streamInfo.urls); args = args.concat(ffmpegArgs["gif"]); args.push('-f', "gif", 'pipe:3'); From 13d7ca3af441248bc866ed3adf92b29e4c6e33eb Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 06:03:05 +0600 Subject: [PATCH 27/99] servicesConfig: add support for m.bilibili.com subdomain --- src/modules/processing/servicesConfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 633fa2a6..95880129 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -7,6 +7,7 @@ "video/:comId", "_shortLink/:comShortLink", "_tv/:lang/video/:tvId", "_tv/video/:tvId" ], + "subdomains": ["m"], "enabled": true }, "reddit": { From 66e58d21ec3e7bdd4f2b38e1e36cfb8d14d6ca6b Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Fri, 26 Apr 2024 11:53:50 +0000 Subject: [PATCH 28/99] feat: internal streams --- src/core/api.js | 29 ++++++++++++------ src/modules/stream/internal.js | 24 +++++++++++++++ src/modules/stream/manage.js | 42 ++++++++++++++++++++++++++ src/modules/stream/stream.js | 3 ++ src/modules/stream/types.js | 55 ++++++++++++++++++---------------- 5 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 src/modules/stream/internal.js diff --git a/src/core/api.js b/src/core/api.js index eda3c014..9dd4b1cc 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -11,7 +11,7 @@ import { Bright, Cyan } from "../modules/sub/consoleText.js"; import stream from "../modules/stream/stream.js"; import loc from "../localization/manager.js"; import { generateHmac } from "../modules/sub/crypto.js"; -import { verifyStream } from "../modules/stream/manage.js"; +import { verifyStream, getInternalStream } from "../modules/stream/manage.js"; export function runAPI(express, app, gitCommit, gitBranch, __dirname) { const corsConfig = process.env.CORS_WILDCARD === '0' ? { @@ -123,13 +123,13 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.get('/api/:type', (req, res) => { try { + let j; switch (req.params.type) { case 'stream': const q = req.query; const checkQueries = q.t && q.e && q.h && q.s && q.i; const checkBaseLength = q.t.length === 21 && q.e.length === 13; const checkSafeLength = q.h.length === 43 && q.s.length === 43 && q.i.length === 22; - if (checkQueries && checkBaseLength && checkSafeLength) { let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i); if (streamInfo.error) { @@ -141,12 +141,23 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { }); } return stream(res, streamInfo); - } else { - let j = apiJSON(0, { - t: "bad request. stream link may be incomplete or corrupted." - }) - return res.status(j.status).json(j.body); - } + } + + j = apiJSON(0, { + t: "bad request. stream link may be incomplete or corrupted." + }) + return res.status(j.status).json(j.body); + case 'istream': + if (!req.ip.endsWith('127.0.0.1')) + return res.sendStatus(403); + if (('' + req.query.t).length !== 21) + return res.sendStatus(400); + + let streamInfo = getInternalStream(req.query.t); + if (!streamInfo) return res.sendStatus(404); + streamInfo.headers = req.headers; + + return stream(res, { type: 'internal', ...streamInfo }); case 'serverInfo': return res.status(200).json({ version: version, @@ -158,7 +169,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { startTime: `${startTimestamp}` }); default: - let j = apiJSON(0, { + j = apiJSON(0, { t: "unknown response type" }) return res.status(j.status).json(j.body); diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js new file mode 100644 index 00000000..a1c99ff5 --- /dev/null +++ b/src/modules/stream/internal.js @@ -0,0 +1,24 @@ +import { request } from 'undici' + +export async function internalStream(streamInfo, res) { + try { + const req = await request(streamInfo.url, { + headers: streamInfo.headers, + signal: streamInfo.controller.signal, + maxRedirections: 16 + }); + + res.status(req.statusCode); + + for (const [ name, value ] of Object.entries(req.headers)) + res.setHeader(name, value) + + if (req.statusCode < 200 || req.statusCode > 299) + return res.destroy(); + + req.body.pipe(res); + req.body.on('error', () => res.destroy()); + } catch { + streamInfo.controller.abort(); + } +} \ No newline at end of file diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index d4cb1e68..680fd8f9 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -4,6 +4,7 @@ import { nanoid } from 'nanoid'; import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; import { streamLifespan } from "../config.js"; +import { strict as assert } from "assert"; const streamNoAccess = { error: "i couldn't verify if you have access to this stream. go back and try again!", @@ -24,6 +25,7 @@ streamCache.on("expired", (key) => { streamCache.del(key); }) +const internalStreamCache = {}; const hmacSalt = randomBytes(64).toString('hex'); export function createStream(obj) { @@ -67,6 +69,34 @@ export function createStream(obj) { return streamLink.toString(); } +export function getInternalStream(id) { + return internalStreamCache[id]; +} + +export function createInternalStream(obj = {}) { + assert(typeof obj.url === 'string'); + + const streamID = nanoid(); + internalStreamCache[streamID] = { + url: obj.url, + controller: new AbortController() + }; + + let streamLink = new URL('/api/istream', `http://127.0.0.1:${process.env.API_PORT}`); + streamLink.searchParams.set('t', streamID); + return streamLink.toString(); +} + +export function destroyInternalStream(url) { + const id = new URL(url).searchParams.get('t'); + assert(id); + + if (internalStreamCache[id]) { + internalStreamCache[id].controller.abort(); + delete internalStreamCache[id]; + } +} + export function verifyStream(id, hmac, exp, secret, iv) { try { const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt); @@ -82,6 +112,18 @@ export function verifyStream(id, hmac, exp, secret, iv) { if (Number(exp) <= new Date().getTime()) return streamNoExist; + if (!streamInfo.originalUrls) { + streamInfo.originalUrls = streamInfo.urls; + } + + if (typeof streamInfo.originalUrls === 'string') { + streamInfo.urls = createInternalStream({ url: streamInfo.originalUrls }); + } else if (Array.isArray(streamInfo.originalUrls)) { + for (const idx in streamInfo.originalUrls) { + streamInfo.originalUrls[idx] = createInternalStream({ url: streamInfo.originalUrls[idx] }); + } + } else throw 'invalid urls'; + return streamInfo; } catch (e) { diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index f254dacc..0b9ba42c 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -1,4 +1,5 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js"; +import { internalStream } from './internal.js' export default async function(res, streamInfo) { try { @@ -7,6 +8,8 @@ export default async function(res, streamInfo) { return; } switch (streamInfo.type) { + case "internal": + return await internalStream(streamInfo, res); case "render": await streamLiveRender(streamInfo, res); break; diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index d3e33438..10bd3a66 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -1,10 +1,12 @@ -import { spawn } from "child_process"; -import ffmpeg from "ffmpeg-static"; -import { ffmpegArgs, genericUserAgent } from "../config.js"; -import { metadataManager } from "../sub/utils.js"; import { request } from "undici"; +import ffmpeg from "ffmpeg-static"; +import { spawn } from "child_process"; import { create as contentDisposition } from "content-disposition-header"; +import { metadataManager } from "../sub/utils.js"; +import { destroyInternalStream } from "./manage.js"; +import { ffmpegArgs, genericUserAgent } from "../config.js"; + const defaultHeaders = { 'user-agent': genericUserAgent } @@ -67,7 +69,11 @@ function getCommand(args) { export async function streamDefault(streamInfo, res) { const abortController = new AbortController(); - const shutdown = () => (closeRequest(abortController), closeResponse(res)); + const shutdown = () => ( + closeRequest(abortController), + closeResponse(res), + destroyInternalStream(streamInfo.urls) + ); try { let filename = streamInfo.filename; @@ -91,13 +97,12 @@ export async function streamDefault(streamInfo, res) { } } -export async function streamLiveRender(streamInfo, res) { - let process, abortController = new AbortController(); - +export function streamLiveRender(streamInfo, res) { + let process; const shutdown = () => ( - closeRequest(abortController), killProcess(process), - closeResponse(res) + closeResponse(res), + streamInfo.urls.map(destroyInternalStream) ); const headers = getHeaders(streamInfo.service); @@ -106,19 +111,13 @@ export async function streamLiveRender(streamInfo, res) { try { if (streamInfo.urls.length !== 2) return shutdown(); - const { body: audio } = await request(streamInfo.urls[1], { - headers, - signal: abortController.signal, - maxRedirections: 16 - }); - const format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; let args = [ '-loglevel', '-8', '-headers', rawHeaders, '-i', streamInfo.urls[0], - '-i', 'pipe:3', + '-i', streamInfo.urls[1], '-map', '0:v', '-map', '1:a', ] @@ -129,25 +128,21 @@ export async function streamLiveRender(streamInfo, res) { args = args.concat(metadataManager(streamInfo.metadata)) } - args.push('-f', format, 'pipe:4'); + args.push('-f', format, 'pipe:3'); process = spawn(...getCommand(args), { windowsHide: true, stdio: [ 'inherit', 'inherit', 'inherit', - 'pipe', 'pipe' + 'pipe' ], }); - const [,,, audioInput, muxOutput] = process.stdio; + const [,,, muxOutput] = process.stdio; res.setHeader('Connection', 'keep-alive'); res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename)); - audio.on('error', shutdown); - audioInput.on('error', shutdown); - - audio.pipe(audioInput); pipe(muxOutput, res, shutdown); process.on('close', shutdown); @@ -159,7 +154,11 @@ export async function streamLiveRender(streamInfo, res) { export function streamAudioOnly(streamInfo, res) { let process; - const shutdown = () => (killProcess(process), closeResponse(res)); + const shutdown = () => ( + killProcess(process), + closeResponse(res), + destroyInternalStream(streamInfo.urls) + ); try { let args = [ @@ -209,7 +208,11 @@ export function streamAudioOnly(streamInfo, res) { export function streamVideoOnly(streamInfo, res) { let process; - const shutdown = () => (killProcess(process), closeResponse(res)); + const shutdown = () => ( + killProcess(process), + closeResponse(res), + destroyInternalStream(streamInfo.urls) + ); try { let args = [ From 5f1dc89c42e98be19a9472ebcbb0f6b9bbca1e70 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 10:47:09 +0000 Subject: [PATCH 29/99] stream/types: attempt to pass through headers only if they exist --- src/modules/stream/types.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 10bd3a66..b5320003 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -88,8 +88,11 @@ export async function streamDefault(streamInfo, res) { maxRedirections: 16 }); - res.setHeader('content-type', headers['content-type']); - res.setHeader('content-length', headers['content-length']); + for (const headerName of ['content-type', 'content-length']) { + if (headers[headerName]) { + res.setHeader(headerName, headers[headerName]); + } + } pipe(stream, res, shutdown); } catch { From ec746f57a738cd85d2f43d8d543cb6329e4dfbb0 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 10:48:22 +0000 Subject: [PATCH 30/99] stream/manage: pass service name to internal stream --- src/modules/stream/manage.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 680fd8f9..8b6b8c62 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -79,6 +79,7 @@ export function createInternalStream(obj = {}) { const streamID = nanoid(); internalStreamCache[streamID] = { url: obj.url, + service: obj.service, controller: new AbortController() }; @@ -117,10 +118,16 @@ export function verifyStream(id, hmac, exp, secret, iv) { } if (typeof streamInfo.originalUrls === 'string') { - streamInfo.urls = createInternalStream({ url: streamInfo.originalUrls }); + streamInfo.urls = createInternalStream({ + url: streamInfo.originalUrls, + ...streamInfo + }); } else if (Array.isArray(streamInfo.originalUrls)) { for (const idx in streamInfo.originalUrls) { - streamInfo.originalUrls[idx] = createInternalStream({ url: streamInfo.originalUrls[idx] }); + streamInfo.originalUrls[idx] = createInternalStream({ + url: streamInfo.originalUrls[idx], + ...streamInfo + }); } } else throw 'invalid urls'; From 49eaa7d4ed2df843750ba429e695072df62f4e9d Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 10:59:27 +0000 Subject: [PATCH 31/99] stream: extract headers to shared file --- src/modules/stream/shared.js | 21 +++++++++++++++++++++ src/modules/stream/types.js | 21 ++------------------- 2 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 src/modules/stream/shared.js diff --git a/src/modules/stream/shared.js b/src/modules/stream/shared.js new file mode 100644 index 00000000..2f898c52 --- /dev/null +++ b/src/modules/stream/shared.js @@ -0,0 +1,21 @@ +import { genericUserAgent } from "../config.js"; + +const defaultHeaders = { + 'user-agent': genericUserAgent +} + +const serviceHeaders = { + bilibili: { + referer: 'https://www.bilibili.com/' + }, + youtube: { + accept: '*/*', + origin: 'https://www.youtube.com', + referer: 'https://www.youtube.com', + DNT: '?1' + } +} + +export function getHeaders(service) { + return { ...defaultHeaders, ...serviceHeaders[service] } +} \ No newline at end of file diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index b5320003..c8873381 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -5,26 +5,9 @@ import { create as contentDisposition } from "content-disposition-header"; import { metadataManager } from "../sub/utils.js"; import { destroyInternalStream } from "./manage.js"; -import { ffmpegArgs, genericUserAgent } from "../config.js"; +import { ffmpegArgs } from "../config.js"; +import { getHeaders } from "./shared.js"; -const defaultHeaders = { - 'user-agent': genericUserAgent -} -const serviceHeaders = { - bilibili: { - referer: 'https://www.bilibili.com/' - }, - youtube: { - accept: '*/*', - origin: 'https://www.youtube.com', - referer: 'https://www.youtube.com', - DNT: '?1' - } -} - -function getHeaders(service) { - return { ...defaultHeaders, ...serviceHeaders[service] } -} function toRawHeaders(headers) { return Object.entries(headers) .map(([key, value]) => `${key}: ${value}\r\n`) From 6eb4af125bf0eb32506ff561f56a9016135e5cc3 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 11:00:45 +0000 Subject: [PATCH 32/99] stream/internal: special youtube stream handling --- src/modules/stream/internal.js | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index a1c99ff5..449be22e 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -1,6 +1,76 @@ import { request } from 'undici' +import { Readable } from 'node:stream' +import { assert } from 'console' +import { getHeaders } from './shared.js' + +const CHUNK_SIZE = BigInt(8e6); // 8 MB +const min = (a, b) => a < b ? a : b; + +async function* readChunks(streamInfo, size) { + let read = 0n; + while (read < size) { + if (streamInfo.controller.signal.aborted) { + throw new Error("controller aborted"); + } + + const chunk = await request(streamInfo.url, { + headers: { + ...getHeaders('youtube'), + Range: `bytes=${read}-${read + CHUNK_SIZE}` + }, + signal: streamInfo.controller.signal + }); + + const expected = min(CHUNK_SIZE, size - read); + const received = BigInt(chunk.headers['content-length']); + + if (received < expected / 2n) { + streamInfo.controller.abort(); + } + + for await (const data of chunk.body) { + yield data; + } + + read += received; + } +} + +function chunkedStream(streamInfo, size) { + assert(streamInfo.controller instanceof AbortController); + const stream = Readable.from(readChunks(streamInfo, size)); + return stream; +} + +async function handleYoutubeStream(streamInfo, res) { + try { + const req = await fetch(streamInfo.url, { + headers: getHeaders('youtube'), + method: 'HEAD', + signal: streamInfo.controller.signal + }); + + streamInfo.url = req.url; + const size = BigInt(req.headers.get('content-length')); + + if (req.status !== 200 || !size) + return res.destroy(); + + const stream = chunkedStream(streamInfo, size); + + res.setHeader('content-type', req.headers.get('content-type')); + stream.pipe(res); + stream.on('error', () => res.destroy()); + } catch { + res.destroy(); + } +} export async function internalStream(streamInfo, res) { + if (streamInfo.service === 'youtube') { + return handleYoutubeStream(streamInfo, res); + } + try { const req = await request(streamInfo.url, { headers: streamInfo.headers, From 3d3a717f3ef519223460cb86639583e06bc4330e Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 11:10:26 +0000 Subject: [PATCH 33/99] stream/internal: also copy content-length where applicable --- src/modules/stream/internal.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index 449be22e..db39fb05 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -58,7 +58,11 @@ async function handleYoutubeStream(streamInfo, res) { const stream = chunkedStream(streamInfo, size); - res.setHeader('content-type', req.headers.get('content-type')); + for (const headerName of ['content-type', 'content-length']) { + const headerValue = req.headers.get(headerName); + if (headerValue) res.setHeader(headerName, headerValue); + } + stream.pipe(res); stream.on('error', () => res.destroy()); } catch { From dd56ae60e76b37801746bad74a6a479f393dc084 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 11:30:16 +0000 Subject: [PATCH 34/99] stream/internal: don't copy Host header from request its basically always gonna be localhost:9k --- src/modules/stream/internal.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index db39fb05..75e18ece 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -77,7 +77,10 @@ export async function internalStream(streamInfo, res) { try { const req = await request(streamInfo.url, { - headers: streamInfo.headers, + headers: { + ...streamInfo.headers, + host: undefined + }, signal: streamInfo.controller.signal, maxRedirections: 16 }); From 66b3697b24a241752c8380a6be625ae87255abcd Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:05:43 +0600 Subject: [PATCH 35/99] youtube: update stub handling --- src/modules/processing/services/youtube.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 63e02eb3..a844f976 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -23,7 +23,9 @@ const c = { } export default async function(o) { - let info, isDubbed, quality = o.quality === "max" ? "9000" : o.quality; //set quality 9000(p) to be interpreted as max + let info, isDubbed, + quality = o.quality === "max" ? "9000" : o.quality; // 9000(p) - max quality + function qual(i) { if (!i.quality_label) { return; @@ -43,6 +45,15 @@ export default async function(o) { if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' }; if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' }; + // return a critical error if returned video is "Video Not Available" + // or a similar stub by youtube + if (info.basic_info.id !== o.id) { + return { + error: 'ErrorCantConnectToServiceAPI', + critical: true + } + } + let bestQuality, hasAudio; let adaptive_formats = info.streaming_data.adaptive_formats.filter(e => @@ -89,12 +100,6 @@ export default async function(o) { youtubeDubName: isDubbed ? o.dubLang : false } - if (filenameAttributes.title === "Video Not Available" && filenameAttributes.author === "YouTube Viewers") - return { - error: 'ErrorCantConnectToServiceAPI', - critical: true - } - if (hasAudio && o.isAudioOnly) return { type: "render", isAudioOnly: true, From d09e6a311059d8a699e4273a0bd802bf775e92d6 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:42:45 +0600 Subject: [PATCH 36/99] localization: update strings related to youtube --- src/localization/languages/en.json | 7 +++---- src/localization/languages/ru.json | 11 +++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 546c2841..7d468cc8 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -101,10 +101,10 @@ "FollowSupport": "keep in touch with cobalt for news, support, and more:", "SourceCode": "explore source code, report issues, 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 rendering, then data about requested content is encrypted and temporarily stored in server's RAM. it's necessary for this feature to function.\n\nencrypted data is stored for 90 seconds and then permanently removed.\n\nstored data is only possible to decrypt with unique encryption keys from your download link. furthermore, the official cobalt codebase doesn't provide a way to read temporarily stored data 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 acts unexpectedly. try again or try another settings.", + "ErrorYTUnavailable": "this youtube video is unavailable. it could be age or region restricted. try another one!", + "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality in settings!", "SettingsCodecSubtitle": "youtube codec", - "SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: poor player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.", + "SettingsCodecDescription": "h264: best support across apps/platforms, average detail level. max quality is 1080p.\nav1: best quality, small file size, most detail. supports 8k & HDR.\nvp9: same quality as av1, but file is x2 bigger. supports 4k & HDR.\n\npick h264 if you want best compatibility.\n\npick av1 if you want best quality and efficiency.", "SettingsAudioDub": "youtube audio track", "SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and cobalt) language is used.", "SettingsDubDefault": "original", @@ -113,7 +113,6 @@ "SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by cobalt into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.", "ShareURL": "share", "ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!", - "ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.", "PopupCloseDone": "done", "Accessibility": "accessibility", "SettingsReduceTransparency": "reduce transparency", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 8f66b5b0..a1695553 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -102,11 +102,11 @@ "FollowSupport": "подписывайся на соц.сети кобальта для новостей и поддержки:", "SourceCode": "шарься в исходнике, пиши о проблемах, или же форкай репозиторий:", "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется рендер, то зашифрованные данные о ней временно хранятся в ОЗУ сервера. это необходимо для работы данной функции.\n\nзашифрованные данные хранятся в течение 90 секунд и затем безвозвратно удаляются.\n\ncохранённые данные можно расшифровать только с помощью уникальных ключей шифрования из твоей ссылки на скачивание. кроме того, официальная кодовая база кобальта не предусматривает возможности чтения эти данные вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", - "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", - "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", - "SettingsCodecSubtitle": "кодек для видео с youtube", - "SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями.", - "SettingsAudioDub": "звуковая дорожка для видео с youtube", + "ErrorYTUnavailable": "это видео недоступно. возможно оно ограничено по доступу или региону. попробуй другое!", + "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество в настройках!", + "SettingsCodecSubtitle": "кодек для youtube видео", + "SettingsCodecDescription": "h264: лучшая совместимость, средний уровень детализированности. максимальное качество - 1080p.\nav1: лучшее качество, маленький размер файла, наибольшее количество деталей. поддерживает 8k и HDR.\nvp9: такая же детализированность, как и у av1, но файл в 2 раза больше. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость.\nвыбирай av1, если ты хочешь лучшее качество и эффективность.", + "SettingsAudioDub": "звуковая дорожка для youtube видео", "SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.", "SettingsDubDefault": "оригинал", "SettingsDubAuto": "авто", @@ -114,7 +114,6 @@ "SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: кобальт совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, кобальт скачает \"dash\".", "ShareURL": "поделиться", "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость ограничена. попробуй другой!", - "ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.", "PopupCloseDone": "готово", "Accessibility": "общедоступность", "SettingsReduceTransparency": "уменьшить прозрачность", From 656c0a34955aba6510db8706145ef1749944f6e8 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:51:12 +0600 Subject: [PATCH 37/99] stream: add semicolons to imports --- src/modules/stream/internal.js | 8 ++++---- src/modules/stream/stream.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index 75e18ece..412ba546 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -1,7 +1,7 @@ -import { request } from 'undici' -import { Readable } from 'node:stream' -import { assert } from 'console' -import { getHeaders } from './shared.js' +import { request } from 'undici'; +import { Readable } from 'node:stream'; +import { assert } from 'console'; +import { getHeaders } from './shared.js'; const CHUNK_SIZE = BigInt(8e6); // 8 MB const min = (a, b) => a < b ? a : b; diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index 0b9ba42c..3de1cb3e 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -1,5 +1,5 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js"; -import { internalStream } from './internal.js' +import { internalStream } from './internal.js'; export default async function(res, streamInfo) { try { @@ -24,7 +24,7 @@ export default async function(res, streamInfo) { await streamDefault(streamInfo, res); break; } - } catch (e) { + } catch { res.status(500).json({ status: "error", text: "Internal Server Error" }); } } From d27366dc8aca236985c782d422caf75a6e6a4e40 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 18:58:03 +0600 Subject: [PATCH 38/99] stream/manage: remove unnecessary variable from catch --- src/modules/stream/manage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 8b6b8c62..03821a8b 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -133,7 +133,7 @@ export function verifyStream(id, hmac, exp, secret, iv) { return streamInfo; } - catch (e) { + catch { return { error: "something went wrong and i couldn't verify this stream. go back and try again!", status: 500 From d4d2f0a6f1d31b422500242a93a84f68f29e8b5e Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 19:02:05 +0600 Subject: [PATCH 39/99] package: bump version to 7.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dddc5a03..a2c270fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.12.6", + "version": "7.13", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From c86e209e55fe21443fd83c6c27c5b7ff073e2033 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 20:29:30 +0600 Subject: [PATCH 40/99] pinterest: fix video link parsing --- src/modules/processing/services/pinterest.js | 21 +++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/modules/processing/services/pinterest.js b/src/modules/processing/services/pinterest.js index 0f14eebf..2364b729 100644 --- a/src/modules/processing/services/pinterest.js +++ b/src/modules/processing/services/pinterest.js @@ -1,22 +1,16 @@ import { genericUserAgent } from "../../config.js"; -const videoLinkBase = { - "regular": "https://v1.pinimg.com/videos/mc/720p/", - "story": "https://v1.pinimg.com/videos/mc/720p/" -} +const linkRegex = /"url":"(https:\/\/v1.pinimg.com\/videos\/.*?)"/g; export default async function(o) { - let id = o.id, type = "regular"; + let id = o.id; if (!o.id && o.shortLink) { id = await fetch(`https://api.pinterest.com/url_shortener/${o.shortLink}/redirect/`, { redirect: "manual" }).then((r) => { return r.headers.get("location").split('pin/')[1].split('/')[0] }).catch(() => {}); } - if (id.includes("--")) { - id = id.split("--")[1]; - type = "story"; - } + if (id.includes("--")) id = id.split("--")[1]; if (!id) return { error: 'ErrorCouldntFetch' }; let html = await fetch(`https://www.pinterest.com/pin/${id}/`, { @@ -25,11 +19,14 @@ export default async function(o) { if (!html) return { error: 'ErrorCouldntFetch' }; - let videoLink = html.split(`"url":"${videoLinkBase[type]}`)[1]?.split('"')[0]; - if (!html.includes(videoLink)) return { error: 'ErrorEmptyDownload' }; + let videoLink = [...html.matchAll(linkRegex)] + .map(([, link]) => link) + .filter(a => a.endsWith('.mp4') && a.includes('720p'))[0]; + + if (!videoLink) return { error: 'ErrorEmptyDownload' }; return { - urls: `${videoLinkBase[type]}${videoLink}`, + urls: videoLink, filename: `pinterest_${o.id}.mp4`, audioFilename: `pinterest_${o.id}_audio` } From 291a3c2e53c476fab1da303681bf2527b14acb1d Mon Sep 17 00:00:00 2001 From: KwiatekMiki <79092746+KwiatekMiki@users.noreply.github.com> Date: Sat, 27 Apr 2024 16:37:24 +0200 Subject: [PATCH 41/99] servicesConfig: add support for /channels/uploader/id vimeo links (#459) added support for /channels/uploader/id vimeo links closes https://github.com/wukko/cobalt/issues/458 --- src/modules/processing/servicesConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 95880129..1a51d17a 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -67,7 +67,7 @@ "enabled": false }, "vimeo": { - "patterns": [":id", "video/:id", ":id/:password"], + "patterns": [":id", "video/:id", ":id/:password", "/channels/:user/:id"], "enabled": true, "bestAudio": "mp3" }, From 70a79fdeddabe17919832ca3965516ebf049279d Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 27 Apr 2024 15:36:17 +0000 Subject: [PATCH 42/99] stream/manage: refactor internal stream handling, skip m3u8 services - fix a typo caused by refactoring Co-authored-by: wukko --- src/modules/stream/manage.js | 58 +++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 03821a8b..161fe587 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -6,6 +6,8 @@ import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; import { streamLifespan } from "../config.js"; import { strict as assert } from "assert"; +const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube']; + const streamNoAccess = { error: "i couldn't verify if you have access to this stream. go back and try again!", status: 401 @@ -73,12 +75,12 @@ export function getInternalStream(id) { return internalStreamCache[id]; } -export function createInternalStream(obj = {}) { - assert(typeof obj.url === 'string'); +export function createInternalStream(url, obj = {}) { + assert(typeof url === 'string'); const streamID = nanoid(); internalStreamCache[streamID] = { - url: obj.url, + url, service: obj.service, controller: new AbortController() }; @@ -89,8 +91,12 @@ export function createInternalStream(obj = {}) { } export function destroyInternalStream(url) { - const id = new URL(url).searchParams.get('t'); - assert(id); + url = new URL(url); + if (url.hostname !== '127.0.0.1') { + return; + } + + const id = url.searchParams.get('t'); if (internalStreamCache[id]) { internalStreamCache[id].controller.abort(); @@ -98,6 +104,28 @@ export function destroyInternalStream(url) { } } +function wrapStream(streamInfo) { + /* m3u8 links are currently not supported + * for internal streams, skip them */ + if (M3U_SERVICES.includes(streamInfo.service)) { + return streamInfo; + } + + const url = streamInfo.urls; + + if (typeof url === 'string') { + streamInfo.urls = createInternalStream(url, streamInfo); + } else if (Array.isArray(url)) { + for (const idx in streamInfo.urls) { + streamInfo.urls[idx] = createInternalStream( + streamInfo.urls[idx], streamInfo + ); + } + } else throw 'invalid urls'; + + return streamInfo; +} + export function verifyStream(id, hmac, exp, secret, iv) { try { const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt); @@ -113,25 +141,7 @@ export function verifyStream(id, hmac, exp, secret, iv) { if (Number(exp) <= new Date().getTime()) return streamNoExist; - if (!streamInfo.originalUrls) { - streamInfo.originalUrls = streamInfo.urls; - } - - if (typeof streamInfo.originalUrls === 'string') { - streamInfo.urls = createInternalStream({ - url: streamInfo.originalUrls, - ...streamInfo - }); - } else if (Array.isArray(streamInfo.originalUrls)) { - for (const idx in streamInfo.originalUrls) { - streamInfo.originalUrls[idx] = createInternalStream({ - url: streamInfo.originalUrls[idx], - ...streamInfo - }); - } - } else throw 'invalid urls'; - - return streamInfo; + return wrapStream(streamInfo); } catch { return { From 78288b8faca5d0896de8037f71e535f5c15a6482 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 27 Apr 2024 22:44:25 +0600 Subject: [PATCH 43/99] core/api: don't trigger verifyStream on premature probe --- src/core/api.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 9dd4b1cc..c7e06284 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -131,14 +131,14 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { const checkBaseLength = q.t.length === 21 && q.e.length === 13; const checkSafeLength = q.h.length === 43 && q.s.length === 43 && q.i.length === 22; if (checkQueries && checkBaseLength && checkSafeLength) { - let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i); - if (streamInfo.error) { - return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); - } if (q.p) { return res.status(200).json({ status: "continue" - }); + }) + } + let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i); + if (streamInfo.error) { + return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); } return stream(res, streamInfo); } From 331f0553672abd34bf61d4e4d7549e4b30dc2d60 Mon Sep 17 00:00:00 2001 From: jsopn Date: Sun, 28 Apr 2024 18:19:05 +0700 Subject: [PATCH 44/99] stream/manage: add missing default `API_PORT` value for internal stream URLs (#463) --- src/modules/stream/manage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 161fe587..c30beec2 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -85,7 +85,7 @@ export function createInternalStream(url, obj = {}) { controller: new AbortController() }; - let streamLink = new URL('/api/istream', `http://127.0.0.1:${process.env.API_PORT}`); + let streamLink = new URL('/api/istream', `http://127.0.0.1:${process.env.API_PORT || 9000}`); streamLink.searchParams.set('t', streamID); return streamLink.toString(); } From d780192adae4e42adb6bac081fae3701847815e2 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 29 Apr 2024 15:06:30 +0600 Subject: [PATCH 45/99] instagram: add three more ways to get post info (#469) for total of fucking SIX??? --- src/modules/processing/services/instagram.js | 105 +++++++++++++------ 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index c8f190a4..405086de 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -2,11 +2,38 @@ import { createStream } from "../../stream/manage.js"; import { genericUserAgent } from "../../config.js"; import { getCookie, updateCookie } from "../cookie/manager.js"; -const commonInstagramHeaders = { - 'user-agent': genericUserAgent, - 'sec-gpc': '1', - 'sec-fetch-site': 'same-origin', - 'x-ig-app-id': '936619743392459' +const commonHeaders = { + "user-agent": genericUserAgent, + "sec-gpc": "1", + "sec-fetch-site": "same-origin", + "x-ig-app-id": "936619743392459" +} +const mobileHeaders = { + "x-ig-app-locale": "en_US", + "x-ig-device-locale": "en_US", + "x-ig-mapped-locale": "en_US", + "user-agent": "Instagram 275.0.0.27.98 Android (33/13; 280dpi; 720x1423; Xiaomi; Redmi 7; onclite; qcom; en_US; 458229237)", + "accept-language": "en-US", + "x-fb-http-engine": "Liger", + "x-fb-client-ip": "True", + "x-fb-server-cluster": "True", + "content-length": "0", +} +const embedHeaders = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Language": "en-GB,en;q=0.9", + "Cache-Control": "max-age=0", + "Dnt": "1", + "Priority": "u=0, i", + "Sec-Ch-Ua": 'Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": "macOS", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", } const cachedDtsg = { @@ -20,7 +47,7 @@ async function findDtsgId(cookie) { const data = await fetch('https://www.instagram.com/', { headers: { - ...commonInstagramHeaders, + ...commonHeaders, cookie } }).then(r => r.text()); @@ -38,7 +65,7 @@ async function findDtsgId(cookie) { async function request(url, cookie, method = 'GET', requestData) { let headers = { - ...commonInstagramHeaders, + ...commonHeaders, 'x-ig-www-claim': cookie?._wwwClaim || '0', 'x-csrftoken': cookie?.values()?.csrftoken, cookie @@ -60,26 +87,36 @@ async function request(url, cookie, method = 'GET', requestData) { return data.json(); } +async function requestMobileApi(id, cookie) { + const oembedURL = new URL('https://i.instagram.com/api/v1/oembed/'); + oembedURL.searchParams.set('url', `https://www.instagram.com/p/${id}/`); + + const oembed = await fetch(oembedURL, { + headers: { + ...mobileHeaders, + cookie + } + }).then(r => r.json()).catch(() => {}); + + const mediaId = oembed?.media_id; + if (!mediaId) return false; + + const mediaInfo = await fetch(`https://i.instagram.com/api/v1/media/${mediaId}/info/`, { + headers: { + ...mobileHeaders, + cookie + } + }).then(r => r.json()).catch(() => {}); + + return mediaInfo?.items?.[0]; +} async function requestHTML(id, cookie) { const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, { headers: { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "Accept-Language": "en-GB,en;q=0.9", - "Cache-Control": "max-age=0", - "Dnt": "1", - "Priority": "u=0, i", - "Sec-Ch-Ua": 'Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99', - "Sec-Ch-Ua-Mobile": "?0", - "Sec-Ch-Ua-Platform": "macOS", - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "none", - "Sec-Fetch-User": "?1", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", - ...cookie + ...embedHeaders, + cookie } - }).then(r => r.text()); + }).then(r => r.text()).catch(() => {}); let embedData = JSON.parse(data?.match(/"init",\[\],\[(.*?)\]\],/)[1]); @@ -196,25 +233,29 @@ function extractNewPost(data, id) { } async function getPost(id) { - let data, result, dataType = 'old'; + let data, result; try { const cookie = getCookie('instagram'); - data = await requestHTML(id); + // mobile api (no cookie, cookie) + data = await requestMobileApi(id); + if (!data && cookie) data = await requestMobileApi(id, cookie); + + // html embed (no cookie, cookie) + if (!data) data = await requestHTML(id); if (!data && cookie) data = await requestHTML(id, cookie); - if (!data) { - dataType = 'new'; - data = await requestGQL(id, cookie); - } + // web app graphql api (no cookie, cookie) + if (!data) data = await requestGQL(id); + if (!data && cookie) data = await requestGQL(id, cookie); } catch {} if (!data) return { error: 'ErrorCouldntFetch' }; - if (dataType === 'new') { - result = extractNewPost(data, id) - } else { + if (data?.gql_data) { result = extractOldPost(data, id) + } else { + result = extractNewPost(data, id) } if (result) return result; From 5fbf35a8d3a47de31fb7f6df2e983122d56f40e2 Mon Sep 17 00:00:00 2001 From: jsopn Date: Mon, 29 Apr 2024 18:56:05 +0700 Subject: [PATCH 46/99] refactor: centralize envs and their defaults in `modules/config` (#464) * feat(config): centralized env variables and their default values * fix: fip `corsWildcard` variable check in `corsConfig` * fix(config): use already declared variables and default some strings to undefined * fix: check processingPriority against NaN --- src/cobalt.js | 8 +++--- src/core/api.js | 18 +++++++------- src/core/web.js | 8 +++--- src/modules/config.js | 30 ++++++++++++++++++++++- src/modules/pageRender/elements.js | 4 +-- src/modules/pageRender/page.js | 20 +++++++-------- src/modules/processing/cookie/manager.js | 3 ++- src/modules/processing/services/tiktok.js | 7 +++--- src/modules/stream/manage.js | 6 ++--- src/modules/stream/types.js | 6 ++--- 10 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/cobalt.js b/src/cobalt.js index 050aec46..473c9b5b 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -6,6 +6,7 @@ import express from "express"; import { Bright, Green, Red } from "./modules/sub/consoleText.js"; import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js"; import { loadLoc } from "./localization/manager.js"; +import { mode } from "./modules/config.js" import path from 'path'; import { fileURLToPath } from 'url'; @@ -22,13 +23,10 @@ app.disable('x-powered-by'); await loadLoc(); -const apiMode = process.env.API_URL && !process.env.WEB_URL; -const webMode = process.env.WEB_URL && process.env.API_URL; - -if (apiMode) { +if (mode === 'API') { const { runAPI } = await import('./core/api.js'); runAPI(express, app, gitCommit, gitBranch, __dirname) -} else if (webMode) { +} else if (mode === 'WEB') { const { runWeb } = await import('./core/web.js'); await runWeb(express, app, gitCommit, gitBranch, __dirname) } else { diff --git a/src/core/api.js b/src/core/api.js index c7e06284..8ff0b5c1 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -4,7 +4,7 @@ import { randomBytes } from "crypto"; const ipSalt = randomBytes(64).toString('hex'); -import { version } from "../modules/config.js"; +import { env, version } from "../modules/config.js"; import { getJSON } from "../modules/api.js"; import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js"; @@ -14,8 +14,8 @@ import { generateHmac } from "../modules/sub/crypto.js"; import { verifyStream, getInternalStream } from "../modules/stream/manage.js"; export function runAPI(express, app, gitCommit, gitBranch, __dirname) { - const corsConfig = process.env.CORS_WILDCARD === '0' ? { - origin: process.env.CORS_URL, + const corsConfig = !env.corsWildcard ? { + origin: env.corsURL, optionsSuccessStatus: 200 } : {}; @@ -163,9 +163,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { version: version, commit: gitCommit, branch: gitBranch, - name: process.env.API_NAME || "unknown", - url: process.env.API_URL, - cors: process.env?.CORS_WILDCARD === "0" ? 0 : 1, + name: env.apiName, + url: env.apiURL, + cors: Number(env.corsWildcard), startTime: `${startTimestamp}` }); default: @@ -194,12 +194,12 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { res.redirect('/api/json') }); - app.listen(process.env.API_PORT || 9000, () => { + app.listen(env.apiPort, () => { console.log(`\n` + `${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + - `URL: ${Cyan(`${process.env.API_URL}`)}\n` + - `Port: ${process.env.API_PORT || 9000}\n` + `URL: ${Cyan(`${env.apiURL}`)}\n` + + `Port: ${env.apiPort}\n` ) }); } diff --git a/src/core/web.js b/src/core/web.js index 7c0cbf33..626574a3 100644 --- a/src/core/web.js +++ b/src/core/web.js @@ -1,4 +1,4 @@ -import { genericUserAgent, version } from "../modules/config.js"; +import { genericUserAgent, version, env } from "../modules/config.js"; import { apiJSON, languageCode } from "../modules/sub/utils.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js"; @@ -76,12 +76,12 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) { return res.redirect('/') }); - app.listen(process.env.WEB_PORT || 9001, () => { + app.listen(env.webPort, () => { console.log(`\n` + `${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + - `URL: ${Cyan(`${process.env.WEB_URL}`)}\n` + - `Port: ${process.env.WEB_PORT || 9001}\n` + `URL: ${Cyan(`${env.webURL}`)}\n` + + `Port: ${env.webPort}\n` ) }) } diff --git a/src/modules/config.js b/src/modules/config.js index 5e079536..b774a8b6 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -12,6 +12,31 @@ Object.values(servicesConfigJson.config).forEach(service => { ) }) +const + apiURL = process.env.API_URL || '', + + // WEB mode related environment variables + webEnvs = { + webPort: process.env.WEB_PORT || 9001, + webURL: process.env.WEB_URL || '', + showSponsors: !!process.env.SHOW_SPONSORS, + isBeta: !!process.env.IS_BETA, + plausibleHostname: process.env.PLAUSIBLE_HOSTNAME, + apiURL + }, + + // API mode related environment variables + apiEnvs = { + apiPort: process.env.API_PORT || 9000, + apiName: process.env.API_NAME || 'unknown', + corsWildcard: process.env.CORS_WILDCARD !== '0', + corsURL: process.env.CORS_URL, + cookiePath: process.env.COOKIE_PATH, + processingPriority: process.env.PROCESSING_PRIORITY && parseInt(process.env.PROCESSING_PRIORITY), + tiktokDeviceInfo: process.env.TIKTOK_DEVICE_INFO && JSON.parse(process.env.TIKTOK_DEVICE_INFO), + apiURL + } + export const services = servicesConfigJson.config, audioIgnore = servicesConfigJson.audioIgnore, @@ -26,4 +51,7 @@ export const supportedAudio = config.supportedAudio, celebrations = config.celebrations, links = config.links, - sponsors = config.sponsors + sponsors = config.sponsors, + mode = (apiURL && !webEnvs.webURL) ? 'API' : + (webEnvs.webURL && apiURL) ? 'WEB' : undefined, + env = mode === 'API' ? apiEnvs : webEnvs \ No newline at end of file diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index e59385e1..ae14cd88 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -1,4 +1,4 @@ -import { authorInfo, celebrations, sponsors } from "../config.js"; +import { authorInfo, celebrations, sponsors, env } from "../config.js"; import emoji from "../emoji.js"; import { loadFile } from "../sub/loadFromFs.js"; @@ -266,5 +266,5 @@ export function sponsoredList() { } export function betaTag() { - return process.env.IS_BETA ? 'β' : '' + return env.isBeta ? 'β' : '' } diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 58bb3a97..9deb0a20 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -1,5 +1,5 @@ import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc, sponsoredList, betaTag, linkSVG } from "./elements.js"; -import { services as s, authorInfo, version, repo, donations, supportedAudio, links } from "../config.js"; +import { services as s, authorInfo, version, repo, donations, supportedAudio, links, env } from "../config.js"; import { getCommitInfo } from "../sub/currentCommit.js"; import loc from "../../localization/manager.js"; import emoji from "../emoji.js"; @@ -48,10 +48,10 @@ export default function(obj) { ${t("AppTitleCobalt")} - + - + @@ -75,11 +75,11 @@ export default function(obj) { - ${process.env.PLAUSIBLE_HOSTNAME ? + ${env.plausibleHostname ? `` : ''} @@ -169,7 +169,7 @@ export default function(obj) { name: "privacy", title: `${emoji("🔒")} ${t("CollapsePrivacy")}`, body: t("PrivacyPolicy") + `${ - process.env.PLAUSIBLE_HOSTNAME ? `

${t("AnalyticsDescription")}` : '' + env.plausibleHostname ? `

${t("AnalyticsDescription")}` : '' }` }, { name: "legal", @@ -177,7 +177,7 @@ export default function(obj) { body: t("FairUse") }]) }, - ...(process.env.SHOW_SPONSORS ? + ...(env.showSponsors ? [{ text: t("SponsoredBy"), classes: ["sponsored-by-text"], @@ -499,7 +499,7 @@ export default function(obj) { }]) }) + (() => { - if (process.env.PLAUSIBLE_HOSTNAME) { + if (env.plausibleHostname) { return settingsCategory({ name: "privacy", title: t('PrivateAnalytics'), @@ -629,7 +629,7 @@ export default function(obj) { ` : ''} - + @@ -314,7 +309,7 @@ export default function(obj) { closeAria: t('AccessibilityGoBack'), header: { aboveTitle: { - text: `v.${version}-${obj.hash}${platform} (${obj.branch})`, + text: `v.${version}-${obj.hash} (${obj.branch})`, url: `${repo}/commit/${obj.hash}` }, title: `${emoji("⚙️", 30)} ${t('TitlePopupSettings')}` @@ -562,7 +557,7 @@ export default function(obj) { }, body: switcher({ name: "download", - explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`, + explanation: t('DownloadPopupDescription'), items: `${t('Download')}
${t('ShareURL')}
${t('CopyURL')}
` @@ -656,7 +651,8 @@ export default function(obj) { 'DataTransferError', 'FilenamePreviewVideoTitle', 'FilenamePreviewAudioTitle', - 'FilenamePreviewAudioAuthor' + 'FilenamePreviewAudioAuthor', + 'DownloadPopupDescriptionIOS' ])} From c35f900f311c88593ac6fa4021801902ae524578 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 11 May 2024 01:05:59 +0600 Subject: [PATCH 70/99] package: bump version to 7.13.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a2c270fa..2f8d6981 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.13", + "version": "7.13.1", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From 526341fcae81554c917c20910a76c0ff222dc7ff Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 15:51:54 +0600 Subject: [PATCH 71/99] web/cobalt.css: optimised padding and safe area - fixed top padding on iphones with dynamic island when installed as app - unified padding with 2 variables - smaller padding everywhere - moved all safe area calls into standalone media query for mobile layout - fixed dialog bottom padding - minimised header and tab footprint in browser on mobile, allowing for more content on one screen --- src/front/cobalt.css | 195 ++++++++++++++++++++++++++++++------------- 1 file changed, 136 insertions(+), 59 deletions(-) diff --git a/src/front/cobalt.css b/src/front/cobalt.css index cf047d84..283b9d9c 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -6,7 +6,9 @@ --inset-focus: 0 0 0 0.1rem var(--accent) inset; --inset-focus-inv: 0 0 0 0.15rem var(--background) inset; --font-mono: 'Noto Sans Mono', 'Consolas', 'SF Mono', monospace; - --padding-1: 0.75rem; + --padding: 0.7rem; + --padding-small: 0.2rem; + --padding-dialog: 18px; --line-height: 1.65rem; --red: rgb(249, 47, 96); --blue: rgb(47, 138, 249); @@ -81,6 +83,7 @@ html, body { margin: 0; background: var(--background); + background-color: var(--background); color: var(--accent); -webkit-tap-highlight-color: var(--transparent); font-family: var(--font-mono); @@ -89,7 +92,6 @@ body { overflow: hidden; -ms-overflow-style: none; scrollbar-width: none; - height: calc(100% + env(safe-area-inset-top)/2); } #home { position: fixed; @@ -118,10 +120,10 @@ a { align-items: center; flex-direction: row; flex-wrap: nowrap; - padding: calc(var(--gap) - 0.1rem) calc(var(--gap)*2 - 0.2rem) calc(var(--gap) - 0.1rem) var(--gap); + padding: calc(var(--gap) - 0.1rem) calc(var(--gap)*2 - var(--padding-small)) calc(var(--gap) - 0.1rem) var(--gap); width: auto; - margin-right: var(--padding-1); - margin-bottom: var(--padding-1); + margin-right: var(--padding); + margin-bottom: var(--padding); background: var(--accent-button); } .checkbox-label { @@ -133,7 +135,7 @@ a { width: 20px; height: 20px; z-index: 0; - margin-right: var(--padding-1); + margin-right: var(--padding); border: 0.15rem solid var(--accent); } [type="checkbox"]::before { @@ -241,7 +243,7 @@ button:active, background: none; border: var(--border-15); color: var(--accent); - padding: 0.3rem 0.75rem 0.5rem; + padding: 0.3rem var(--padding) 0.5rem; font-size: 1rem; } .mono { @@ -307,7 +309,7 @@ button:active, #url-clear { height: 100%; background: none; - padding: 0 1rem 0.2rem; + padding: 0 1rem var(--padding-small); transform: none; font-size: 1rem; box-shadow: none!important; @@ -320,8 +322,8 @@ button:active, display: flex; position: absolute; width: 20px; - padding-top: 0.2rem; - left: 0.7rem; + padding-top: var(--padding-small); + left: var(--padding); flex-wrap: nowrap; color: var(--accent-subtext); } @@ -350,7 +352,7 @@ button:active, position: absolute; display: flex; justify-content: center; - padding-bottom: calc(env(safe-area-inset-bottom)/2 + 2rem); + padding-bottom: 2rem; font-size: 0.9rem; text-align: center; } @@ -412,7 +414,7 @@ button:active, max-height: 95%; opacity: 0; transform: translate(-50%,-48%)scale(.95); - box-shadow: 0 0 0 0.2rem var(--glass) inset, + box-shadow: 0 0 0 var(--padding-small) var(--glass) inset, 0 0 20px 0 var(--accent-hover-transparent); } .popup.visible { @@ -442,15 +444,15 @@ button:active, .popup.small { width: 21rem; box-shadow: 0px 0px 60px 0px var(--accent-hover); - padding: 18px; - transform: translate(-50%,-50%)scale(.95); + padding: var(--padding-dialog); + transform: translate(-50%, -50%)scale(.95); pointer-events: all; border-radius: 22px; } .popup.small .popup-content-inner { display: flex; flex-direction: column; - gap: 18px; + gap: var(--padding-dialog); width: 100%; } .popup.small.visible { @@ -507,7 +509,7 @@ button:active, width: 100%; max-height: 300px; min-height: 210px; - margin-bottom: 0.7rem; + margin-bottom: var(--padding); float: left; background: var(--accent-hover); display: flex; @@ -521,8 +523,8 @@ button:active, .changelog-tags { display: inline-flex; align-items: center; - gap: 0.7rem; - padding-bottom: 0.7rem; + gap: var(--padding); + padding-bottom: var(--padding); flex-wrap: wrap; } .changelog-tag-version { @@ -540,11 +542,11 @@ button:active, padding-top: 0!important; } .desc-padding { - padding-bottom: 0.7rem; + padding-bottom: var(--padding); } #popup-subtitle { font-size: 1.1rem; - padding-bottom: var(--padding-1); + padding-bottom: var(--padding); } .popup-desc, .desc-error, @@ -562,9 +564,9 @@ button:active, } .popup-title { font-size: 1.5rem; - line-height: 1.2em; display: flex; align-items: center; + line-height: 1em; margin-bottom: 0.4rem; margin-top: 0.4rem; } @@ -581,12 +583,12 @@ button:active, .popup-content-inner, .tab-content-settings, #picker-holder { - padding-top: calc(env(safe-area-inset-top)/2 + 4.7rem); - padding-bottom: calc(env(safe-area-inset-bottom)/2 + 4.8rem); + padding-top: calc(var(--padding) + 4rem); + padding-bottom: 4.8rem; } .tab-content-settings, #tab-about-about .popup-content-inner { - padding-top: calc(env(safe-area-inset-top)/2 + 6rem);; + padding-top: 6rem; } .bullpadding { padding-left: 0.58rem; @@ -594,11 +596,11 @@ button:active, .popup-header { position: absolute; z-index: 999; - padding-top: calc(env(safe-area-inset-top)/2 + 1.7rem); + padding-top: calc(var(--padding) + 1rem); width: 100%; } .settings-category { - padding-bottom: 0.7rem; + padding-bottom: var(--padding); } .separator { float: left; @@ -616,13 +618,13 @@ button:active, line-height: var(--line-height); } .bottom-margin { - margin-bottom: var(--padding-1)!important; + margin-bottom: var(--padding)!important; } .top-margin { - margin-top: var(--padding-1)!important; + margin-top: var(--padding)!important; } .top-margin-only { - margin-top: var(--padding-1)!important; + margin-top: var(--padding)!important; margin-bottom: 0!important; } .no-margin { @@ -668,7 +670,7 @@ button:active, cursor: pointer; } .switch.space-right { - margin-right: var(--padding-1); + margin-right: var(--padding); } .switch:focus { box-shadow: var(--inset-focus) inset; @@ -738,12 +740,12 @@ button:active, bottom: 0; position: absolute; width: 100%; - padding-top: 0.2rem; - padding-bottom: 1.7rem; + padding-top: var(--padding-small); + padding-bottom: calc(var(--padding) + 1rem); } .popup-tabs-child { width: 100%; - padding: 0 0.2rem; + padding: 0 var(--padding-small); } .emoji, svg { user-select: none; @@ -773,10 +775,10 @@ button:active, justify-content: start; flex-wrap: wrap; align-content: space-around; - padding-top: calc(env(safe-area-inset-top)/2 + 7.6rem); - padding-bottom: calc(env(safe-area-inset-bottom)/2 + 4.8rem); - padding-left: 0.2rem; - padding-right: 0.2rem; + padding-top: 7.6rem; + padding-bottom: 4.8rem; + padding-left: var(--padding-small); + padding-right: var(--padding-small); } .imageBlock { width: 100%; @@ -795,7 +797,7 @@ button:active, } #popup-picker .explanation { margin-top: 0!important; - margin-bottom: var(--padding-1); + margin-bottom: var(--padding); } #cobalt-main-box #bottom button { width: auto; @@ -807,7 +809,7 @@ button:active, -webkit-user-select: none; } .collapse-header { - padding: 0.5rem var(--padding-1); + padding: 0.5rem var(--padding); font-size: 0.95rem; display: flex; flex-direction: row; @@ -816,7 +818,7 @@ button:active, background: var(--accent-button); } .collapse-header .emoji { - margin-right: var(--padding-1); + margin-right: var(--padding); } .collapse-indicator { display: flex; @@ -836,7 +838,7 @@ button:active, } .collapse-body { display: none; - padding: var(--padding-1); + padding: var(--padding); padding-bottom: 1rem; user-select: text; -webkit-user-select: text; @@ -863,7 +865,7 @@ button:active, display: flex; justify-content: center; align-items: center; - padding-top: calc(env(safe-area-inset-top) + 1rem); + padding-top: 1rem; } .urgent-text { display: flex; @@ -943,7 +945,7 @@ button:active, display: flex; justify-content: center; flex-wrap: wrap; - gap: 0.2rem 1rem; + gap: var(--padding-small) 1rem; margin-bottom: 1rem; } .sponsored-logo svg { @@ -963,7 +965,7 @@ button:active, align-items: center; justify-content: flex-start; gap: 1rem; - padding: 0.5rem 0.7rem; + padding: 0.5rem var(--padding); } .filename-item.line { border-bottom: 0.1rem solid var(--accent-button-elevated); @@ -1079,7 +1081,7 @@ button:active, } @media screen and (max-width: 660px) { #cobalt-main-box { - width: calc(100% - (0.7rem * 2)); + width: calc(100% - (var(--padding) * 2)); } } /* mobile page */ @@ -1101,14 +1103,11 @@ button:active, #cobalt-main-box #bottom #audioMode button, #audioMode { width: 100%; } - #footer { - padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem); - } #footer-buttons { flex-direction: column; align-items: stretch; width: 100%; - padding: 0 0.7rem; + padding: 0 var(--padding); } .footer-pair .footer-button { width: 100%!important; @@ -1125,9 +1124,6 @@ button:active, flex-direction: column; gap: var(--gap); } - .urgent-notice { - padding-top: calc(env(safe-area-inset-bottom)/2 + 1rem); - } .popup, .popup-header .glass-bkg, .popup-tabs .glass-bkg, @@ -1149,14 +1145,13 @@ button:active, transform: unset; } .popup.small { - width: calc(100% - 18px * 2); + width: calc(100% - var(--padding-dialog) * 2); height: auto; top: unset; bottom: 0; left: 0; transform: none; position: absolute; - padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.7rem); transform: translateY(30rem); } #popup-download .popout-meowbalt { @@ -1199,14 +1194,96 @@ button:active, max-height: 100%; box-shadow: none; } - .popup-tabs { - padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem); - } .popup-content-inner, .tab-content-settings, .popup-tabs-child, .popup-header-contents { - padding-left: 0.7rem; - padding-right: 0.7rem; + padding-left: var(--padding); + padding-right: var(--padding); + } + .popup-content-inner, + .tab-content-settings, + #picker-holder { + padding-bottom: calc(var(--padding) + 3.5rem); + padding-top: calc(var(--padding) + 3rem); + } + #footer, + .popup-tabs { + padding-bottom: var(--padding); + } + .popup.small { + padding-bottom: var(--padding-dialog) + } + .urgent-notice { + padding-top: 1rem; + } + .popup-title { + margin-top: var(--padding-small); + } + .popup-header { + padding-top: var(--padding); + } + .tab-content-settings, + #tab-about-about .popup-content-inner { + padding-top: 5rem; + } +} +@media screen and (max-width: 535px) and (display-mode: standalone) { + html, + body { + height: calc(100% + env(safe-area-inset-top) / 2); + } + .popup-header { + padding-top: max( + calc(env(safe-area-inset-top)), + var(--padding) + 1rem + ); + } + .urgent-notice { + padding-top: max( + calc(env(safe-area-inset-top) - var(--padding-small)), + var(--padding) + ); + } + #footer, + .popup-tabs { + padding-bottom: max( + calc(env(safe-area-inset-bottom) + var(--padding-small)), + var(--padding) + ); + } + .popup.small { + padding-bottom: max( + calc(env(safe-area-inset-bottom) + var(--padding-small)), + var(--padding-dialog) + ); + } + .popup-content-inner, + .tab-content-settings { + padding-top: max( + calc(env(safe-area-inset-top) + var(--padding) + var(--padding-small) + 2rem), + calc(var(--padding) + 4rem - var(--padding-small)) + ); + padding-bottom: max( + calc(env(safe-area-inset-bottom) + var(--padding) + 3rem), + calc(var(--padding) + var(--padding-small) * 2 + 3rem) + ); + } + .tab-content-settings, + #tab-about-about .popup-content-inner { + padding-top: max( + calc(env(safe-area-inset-top) + var(--padding) + var(--padding-small) * 2 + 3rem), + calc(var(--padding) + 5rem) + ); + } + #picker-holder { + padding-top: max( + calc(env(safe-area-inset-top) + var(--padding) + 5rem), + calc(var(--padding) * 2 + 6rem) + ); + padding-bottom: max( + calc(env(safe-area-inset-bottom) + var(--padding) + 2rem), + calc(4rem - var(--padding) + var(--padding-small)) + ); } } From ecdae7fac902eb954d5e24e14dc46a9729ed295d Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 17:11:58 +0600 Subject: [PATCH 72/99] web/cobalt.js: change status bar color with theme also: - cleaned up theming logics - cleaned up switcher function --- src/front/cobalt.js | 101 ++++++++++++++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 30125421..8b24464d 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -52,22 +52,13 @@ const changeApi = (url) => { const eid = (id) => { return document.getElementById(id) } + const sGet = (id) =>{ return localStorage.getItem(id) } const sSet = (id, value) => { localStorage.setItem(id, value) } -const enable = (id) => { - eid(id).dataset.enabled = "true"; -} -const disable = (id) => { - eid(id).dataset.enabled = "false"; -} -const opposite = (state) => { - return state === "true" ? "false" : "true"; -} - const lazyGet = (key) => { const value = sGet(key); if (key in switchers) { @@ -142,15 +133,53 @@ const copy = (id, data) => { const share = url => navigator?.share({ url }).catch(() => {}); -const detectColorScheme = () => { +const preferredColorScheme = () => { let theme = "auto"; let localTheme = sGet("theme"); + let isLightPreferred = false; + if (localTheme) { theme = localTheme; - } else if (!window.matchMedia) { - theme = "dark" } + if (window.matchMedia) { + isLightPreferred = window.matchMedia('(prefers-color-scheme: light)').matches; + } + if (theme === "auto") { + theme = isLightPreferred ? "light" : "dark" + } + + return theme +} + +if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { + changeStatusBarColor() + detectColorScheme() + }) +} + +const changeStatusBarColor = () => { + const theme = preferredColorScheme(); + const colors = { + "dark": "#000000", + "light": "#ffffff", + "dark-popup": "#191919", + "light-popup": "#e8e8e8" + } + + let state = store.isPopupOpen ? "dark-popup" : "dark"; + + if (theme === "light") { + state = store.isPopupOpen ? "light-popup" : "light"; + } + + document.querySelector('meta[name="theme-color"]').setAttribute('content', colors[state]); +} +const detectColorScheme = () => { + let theme = preferredColorScheme(); + document.documentElement.setAttribute("data-theme", theme); + changeStatusBarColor(theme); } const updateFilenamePreview = () => { @@ -246,6 +275,9 @@ const popup = (type, action, text) => { if (action === 1) { hideAllPopups(); // hide the previous popup before showing a new one store.isPopupOpen = true; + + // if not a small popup, update status bar color to match the popup header + if (!bottomPopups.includes(type)) changeStatusBarColor(); switch (type) { case "about": let tabId = "about"; @@ -306,32 +338,49 @@ const popup = (type, action, text) => { } } else { store.isPopupOpen = false; + + // reset status bar to base color + changeStatusBarColor(); + if (type === "picker") { eid("picker-download").href = '/'; eid("picker-download").classList.remove("visible"); eid("picker-holder").innerHTML = '' } } - if (bottomPopups.includes(type)) eid(`popup-${type}-container`).classList.toggle("visible"); + if (bottomPopups.includes(type)) { + eid(`popup-${type}-container`).classList.toggle("visible"); + } eid("popup-backdrop").classList.toggle("visible"); eid(`popup-${type}`).classList.toggle("visible"); eid(`popup-${type}`).focus(); } -const changeSwitcher = (li, b) => { - if (b) { - if (!switchers[li].includes(b)) b = switchers[li][0]; - sSet(li, b); - for (let i in switchers[li]) { - (switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`) +const changeSwitcher = (switcher, state) => { + if (state) { + if (!switchers[switcher].includes(state)) { + state = switchers[switcher][0]; } - if (li === "theme") detectColorScheme(); - if (li === "filenamePattern") updateFilenamePreview(); + sSet(switcher, state); + + for (let i in switchers[switcher]) { + if (switchers[switcher][i] === state) { + eid(`${switcher}-${state}`).dataset.enabled = "true"; + } else { + eid(`${switcher}-${switchers[switcher][i]}`).dataset.enabled = "false"; + } + } + if (switcher === "theme") detectColorScheme(); + if (switcher === "filenamePattern") updateFilenamePreview(); } else { - let pref = switchers[li][0]; - sSet(li, pref); - for (let i in switchers[li]) { - (switchers[li][i] === pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`) + let defaultValue = switchers[switcher][0]; + sSet(switcher, defaultValue); + for (let i in switchers[switcher]) { + if (switchers[switcher][i] === defaultValue) { + eid(`${switcher}-${defaultValue}`).dataset.enabled = "true"; + } else { + eid(`${switcher}-${switchers[switcher][i]}`).dataset.enabled = "false"; + } } } } From 852c9aedd0c99346e68fc695f217b69990934b96 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 17:12:17 +0600 Subject: [PATCH 73/99] web/cobalt.css: remove redundant theming --- src/front/cobalt.css | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 283b9d9c..b7e34565 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -15,40 +15,6 @@ --gap: 0.5rem; --gap-no-icon: 0.6rem; } -@media (prefers-color-scheme: dark) { - :root { - --accent: rgb(225, 225, 225); - --accent-highlight: rgb(225, 225, 225, 4%); - --accent-subtext: rgb(110, 110, 110); - --accent-hover: rgb(30, 30, 30); - --accent-hover-elevated: rgb(48, 48, 48); - --accent-hover-transparent: rgba(48, 48, 48, 0.5); - --accent-button: rgb(25, 25, 25); - --accent-button-elevated: rgb(42, 42, 42); - --glass: rgba(25, 25, 25, 0.85); - --glass-lite: rgba(25, 25, 25, 0.98); - --subbackground: rgb(10, 10, 10); - --background: rgb(0, 0, 0); - --background-backdrop: rgba(0, 0, 0, 0.5); - } -} -@media (prefers-color-scheme: light) { - :root { - --accent: rgb(25, 25, 25); - --accent-highlight: rgb(25, 25, 25, 4%); - --accent-subtext: rgb(110, 110, 110); - --accent-hover: rgb(225, 225, 225); - --accent-hover-elevated: rgb(210, 210, 210); - --accent-hover-transparent: rgba(215, 215, 215, 0.5); - --accent-button: rgb(232, 232, 232); - --accent-button-elevated: rgb(215, 215, 215); - --glass: rgba(232, 232, 232, 0.85); - --glass-lite: rgba(232, 232, 232, 0.98); - --subbackground: rgb(240, 240, 240); - --background: rgb(255, 255, 255); - --background-backdrop: rgba(255, 255, 255, 0.5); - } -} [data-theme="dark"] { --accent: rgb(225, 225, 225); --accent-highlight: rgb(225, 225, 225, 4%); From 8bcb98f005a2b370305b09d2aa2093728326268a Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 17:13:49 +0600 Subject: [PATCH 74/99] package: bump version to 7.13.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f8d6981..9d291af9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.13.1", + "version": "7.13.2", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From 4aa5aef77564f3af594697c9abf06952f0fdf227 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 17:31:33 +0600 Subject: [PATCH 75/99] web/cobalt.js: clean up --- src/front/cobalt.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 8b24464d..82fcfc58 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -151,13 +151,6 @@ const preferredColorScheme = () => { return theme } -if (window.matchMedia) { - window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { - changeStatusBarColor() - detectColorScheme() - }) -} - const changeStatusBarColor = () => { const theme = preferredColorScheme(); const colors = { @@ -176,10 +169,15 @@ const changeStatusBarColor = () => { document.querySelector('meta[name="theme-color"]').setAttribute('content', colors[state]); } const detectColorScheme = () => { - let theme = preferredColorScheme(); + document.documentElement.setAttribute("data-theme", preferredColorScheme()); + changeStatusBarColor(); +} - document.documentElement.setAttribute("data-theme", theme); - changeStatusBarColor(theme); +if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { + changeStatusBarColor() + detectColorScheme() + }) } const updateFilenamePreview = () => { From bdcb57f9c74b2ff7f352494118a4d4d20b58f8df Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 18:04:28 +0600 Subject: [PATCH 76/99] tiktok: use highest quality when h265 is allowed --- src/modules/processing/services/tiktok.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/services/tiktok.js b/src/modules/processing/services/tiktok.js index 159cfd87..b81d57d9 100644 --- a/src/modules/processing/services/tiktok.js +++ b/src/modules/processing/services/tiktok.js @@ -48,8 +48,12 @@ export default async function(obj) { images = detail.image_post_info?.images; let playAddr = detail.video.play_addr_h264; - if ((obj.h265 || !playAddr) && detail.video.play_addr) - playAddr = detail.video.play_addr; + if (obj.h265) { + playAddr = detail.video.bit_rate[0].play_addr + } + if (!playAddr && detail.video.play_addr) { + playAddr = detail.video.play_addr + } if (!obj.isAudioOnly && !images) { video = playAddr.url_list[0]; From 05301f1cd5f3ff5603d07c7f2748643a415f89b7 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 19:03:27 +0600 Subject: [PATCH 77/99] web/cobalt.js: match status bar popup color better --- 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 82fcfc58..a3302c10 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -156,8 +156,8 @@ const changeStatusBarColor = () => { const colors = { "dark": "#000000", "light": "#ffffff", - "dark-popup": "#191919", - "light-popup": "#e8e8e8" + "dark-popup": "#151515", + "light-popup": "#ebebeb" } let state = store.isPopupOpen ? "dark-popup" : "dark"; From d57fe9323448c1478155d09ac92d9c0f9d52a876 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 19:34:48 +0600 Subject: [PATCH 78/99] web: improvements for android web app - reduced padding when installed as standalone - now attempting to color the navigation bar --- src/front/cobalt.css | 18 ++++++++++++++++-- src/front/cobalt.js | 5 +++++ src/modules/pageRender/page.js | 4 +++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/front/cobalt.css b/src/front/cobalt.css index b7e34565..5e3c0659 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -1171,7 +1171,7 @@ button:active, .tab-content-settings, #picker-holder { padding-bottom: calc(var(--padding) + 3.5rem); - padding-top: calc(var(--padding) + 3rem); + padding-top: calc(var(--padding) + 3rem - var(--padding-small)); } #footer, .popup-tabs { @@ -1191,7 +1191,7 @@ button:active, } .tab-content-settings, #tab-about-about .popup-content-inner { - padding-top: 5rem; + padding-top: calc(5rem - var(--padding-small)); } } @media screen and (max-width: 535px) and (display-mode: standalone) { @@ -1252,4 +1252,18 @@ button:active, calc(4rem - var(--padding) + var(--padding-small)) ); } + + .android .popup-header { + padding-top: var(--padding); + } + .android .popup-content-inner, + .android .tab-content-settings, + .android #picker-holder { + padding-bottom: calc(var(--padding) + 3.5rem); + padding-top: calc(var(--padding) + 3rem - var(--padding-small)); + } + .android .tab-content-settings, + .android #tab-about-about .popup-content-inner { + padding-top: calc(5rem - var(--padding-small)); + } } diff --git a/src/front/cobalt.js b/src/front/cobalt.js index a3302c10..150e8f92 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,5 +1,6 @@ const ua = navigator.userAgent.toLowerCase(); const isIOS = ua.includes("iphone os") || (ua.includes("mac os") && navigator.maxTouchPoints > 0); +const isAndroid = ua.includes("android"); const isMobile = ua.includes("android") || isIOS; const isSafari = ua.includes("safari/"); const isFirefox = ua.includes("firefox/"); @@ -167,6 +168,7 @@ const changeStatusBarColor = () => { } document.querySelector('meta[name="theme-color"]').setAttribute('content', colors[state]); + document.querySelector('meta[name="background-color"]').setAttribute('content', colors[state]); } const detectColorScheme = () => { document.documentElement.setAttribute("data-theme", preferredColorScheme()); @@ -626,6 +628,9 @@ const loadSettings = () => { if (!isMobile) { eid("cobalt-body").classList.add('desktop'); } + if (isAndroid) { + eid("cobalt-body").classList.add('android'); + } if (isIOS) { eid("download-switcher") .querySelector(".explanation") diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 7666f206..f0696843 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -69,7 +69,6 @@ export default function(obj) { - @@ -86,6 +85,9 @@ export default function(obj) { + + + From 81ef49c104408d9deab2c59215384d7875c4238b Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 19:37:56 +0600 Subject: [PATCH 79/99] web/emoji: replace high detail svgs with pngs - fixes blurry svg rendering in safari - removes unused 3d emoji --- src/front/emoji/3d/cat_crying.svg | 382 ----------------- src/front/emoji/3d/cat_flabbergasted.svg | 507 ----------------------- src/front/emoji/3d/cat_grin.svg | 345 --------------- src/front/emoji/3d/cat_smile.svg | 345 --------------- src/front/emoji/3d/film_frames.png | Bin 0 -> 8720 bytes src/front/emoji/3d/film_frames.svg | 338 --------------- src/front/emoji/3d/headphone.png | Bin 0 -> 11316 bytes src/front/emoji/3d/headphone.svg | 186 --------- src/modules/emoji.js | 2 +- 9 files changed, 1 insertion(+), 2104 deletions(-) delete mode 100644 src/front/emoji/3d/cat_crying.svg delete mode 100644 src/front/emoji/3d/cat_flabbergasted.svg delete mode 100644 src/front/emoji/3d/cat_grin.svg delete mode 100644 src/front/emoji/3d/cat_smile.svg create mode 100644 src/front/emoji/3d/film_frames.png delete mode 100644 src/front/emoji/3d/film_frames.svg create mode 100644 src/front/emoji/3d/headphone.png delete mode 100644 src/front/emoji/3d/headphone.svg diff --git a/src/front/emoji/3d/cat_crying.svg b/src/front/emoji/3d/cat_crying.svg deleted file mode 100644 index afa45138..00000000 --- a/src/front/emoji/3d/cat_crying.svg +++ /dev/null @@ -1,382 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/front/emoji/3d/cat_flabbergasted.svg b/src/front/emoji/3d/cat_flabbergasted.svg deleted file mode 100644 index 84c9feda..00000000 --- a/src/front/emoji/3d/cat_flabbergasted.svg +++ /dev/null @@ -1,507 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/front/emoji/3d/cat_grin.svg b/src/front/emoji/3d/cat_grin.svg deleted file mode 100644 index be6e29d0..00000000 --- a/src/front/emoji/3d/cat_grin.svg +++ /dev/null @@ -1,345 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/front/emoji/3d/cat_smile.svg b/src/front/emoji/3d/cat_smile.svg deleted file mode 100644 index e50cbfa0..00000000 --- a/src/front/emoji/3d/cat_smile.svg +++ /dev/null @@ -1,345 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/front/emoji/3d/film_frames.png b/src/front/emoji/3d/film_frames.png new file mode 100644 index 0000000000000000000000000000000000000000..6522c5f737612757ac117488aeffaf48fff464c5 GIT binary patch literal 8720 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 A!2kdN literal 0 HcmV?d00001 diff --git a/src/front/emoji/3d/film_frames.svg b/src/front/emoji/3d/film_frames.svg deleted file mode 100644 index 4caf8736..00000000 --- a/src/front/emoji/3d/film_frames.svg +++ /dev/null @@ -1,338 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/front/emoji/3d/headphone.png b/src/front/emoji/3d/headphone.png new file mode 100644 index 0000000000000000000000000000000000000000..b46173a2915807ea81fa6023e735596e29293e18 GIT binary patch literal 11316 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/modules/emoji.js b/src/modules/emoji.js index f4793cb5..82273ac4 100644 --- a/src/modules/emoji.js +++ b/src/modules/emoji.js @@ -61,6 +61,6 @@ export default function(emoji, size, disablePadding, fluent) { if (!names[emoji]) emoji = "❓"; let filePath = `emoji/${names[emoji]}.svg`; - if (fluent) filePath = `emoji/3d/${names[emoji]}.svg`; + if (fluent) filePath = `emoji/3d/${names[emoji]}.png`; return `` } From 0609441b1f0b78a2a99f985b97cc94a627676054 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 19:38:14 +0600 Subject: [PATCH 80/99] package: bump version to 7.13.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d291af9..c9c93ddb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.13.2", + "version": "7.13.3", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", From 229ddb3701db56497e34ab6bf166c5d96ac7af80 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 19:53:12 +0600 Subject: [PATCH 81/99] web/cobalt.css: fix page height in web app on ipad --- src/front/cobalt.css | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 5e3c0659..47df2845 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -47,6 +47,7 @@ } html, body { + height: calc(100% + env(safe-area-inset-top) / 2); margin: 0; background: var(--background); background-color: var(--background); @@ -1195,10 +1196,6 @@ button:active, } } @media screen and (max-width: 535px) and (display-mode: standalone) { - html, - body { - height: calc(100% + env(safe-area-inset-top) / 2); - } .popup-header { padding-top: max( calc(env(safe-area-inset-top)), From 840bdf35b7b6355b97f5dd8337cd5bf4e4f731d2 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 12 May 2024 20:04:12 +0600 Subject: [PATCH 82/99] web: remove broken android navigation bar coloring cuz it doesn't work --- src/front/cobalt.css | 1 - src/front/cobalt.js | 1 - src/modules/pageRender/page.js | 1 - 3 files changed, 3 deletions(-) diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 47df2845..668d0eae 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -50,7 +50,6 @@ body { height: calc(100% + env(safe-area-inset-top) / 2); margin: 0; background: var(--background); - background-color: var(--background); color: var(--accent); -webkit-tap-highlight-color: var(--transparent); font-family: var(--font-mono); diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 150e8f92..423acdf3 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -168,7 +168,6 @@ const changeStatusBarColor = () => { } document.querySelector('meta[name="theme-color"]').setAttribute('content', colors[state]); - document.querySelector('meta[name="background-color"]').setAttribute('content', colors[state]); } const detectColorScheme = () => { document.documentElement.setAttribute("data-theme", preferredColorScheme()); diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index f0696843..17643958 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -86,7 +86,6 @@ export default function(obj) { - From d7a3a7cac45d585f6ffc7d298e91175bdc0fc2a1 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Mon, 13 May 2024 16:54:00 +0000 Subject: [PATCH 83/99] youtube: ignore formats with missing content length --- src/modules/processing/services/youtube.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index ff4ea0ae..10b04a5c 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -66,8 +66,8 @@ export default async function(o) { adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats) } - bestQuality = adaptive_formats.find(i => i.has_video); - hasAudio = adaptive_formats.find(i => i.has_audio); + bestQuality = adaptive_formats.find(i => i.has_video && i.content_length); + hasAudio = adaptive_formats.find(i => i.has_audio && i.content_length); if (bestQuality) bestQuality = qual(bestQuality); if (!bestQuality && !o.isAudioOnly || !hasAudio) return { error: 'ErrorYTTryOtherCodec' }; From d06bcc3e1249bf2ab3363e5c431de73a521994f1 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 14 May 2024 12:54:07 +0600 Subject: [PATCH 84/99] update repo url in package.json and dockerfile --- Dockerfile | 2 +- package.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index b3064281..4eee25b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN rm -rf /var/lib/apt/lists/* COPY package*.json ./ RUN npm install -RUN git clone -n https://github.com/wukko/cobalt.git --depth 1 && mv cobalt/.git ./ && rm -rf cobalt +RUN git clone -n https://github.com/imputnet/cobalt.git --depth 1 && mv cobalt/.git ./ && rm -rf cobalt COPY . . EXPOSE 9000 diff --git a/package.json b/package.json index c9c93ddb..ffb78672 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cobalt", "description": "save what you love", "version": "7.13.3", - "author": "wukko", + "author": "imput", "exports": "./src/cobalt.js", "type": "module", "engines": { @@ -17,13 +17,13 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/wukko/cobalt.git" + "url": "git+https://github.com/imputnet/cobalt.git" }, "license": "AGPL-3.0", "bugs": { - "url": "https://github.com/wukko/cobalt/issues" + "url": "https://github.com/imputnet/cobalt/issues" }, - "homepage": "https://github.com/wukko/cobalt#readme", + "homepage": "https://github.com/imputnet/cobalt#readme", "dependencies": { "content-disposition-header": "0.6.0", "cors": "^2.8.5", From 7db84a15e2ac0b8327abc4b164823f1354f48adc Mon Sep 17 00:00:00 2001 From: "deepsource-io[bot]" <42547082+deepsource-io[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 06:57:09 +0000 Subject: [PATCH 85/99] ci: update .deepsource.toml --- .deepsource.toml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index d6a19e95..e5ec308c 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,11 +1,7 @@ version = 1 -test_patterns = [ - "src/test/test.js" -] - [[analyzers]] name = "javascript" -enabled = true - [analyzers.meta] - environment = ["nodejs"] + + [analyzers.meta] + environment = ["nodejs"] \ No newline at end of file From 3e5ccea23c3ebe49d4bcbf339c3df945b6b2c12d Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 14 May 2024 13:08:36 +0600 Subject: [PATCH 86/99] chore: remove unused imports/variables --- src/core/api.js | 4 ++-- src/front/cobalt.js | 2 +- src/localization/manager.js | 2 +- src/modules/processing/services/tumblr.js | 1 - src/modules/sub/crypto.js | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 8ff0b5c1..440c25c2 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -25,7 +25,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { standardHeaders: true, legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), - handler: (req, res, next, opt) => { + handler: (req, res) => { return res.status(429).json({ "status": "rate-limit", "text": loc(languageCode(req), 'ErrorRateLimit') @@ -38,7 +38,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { standardHeaders: true, legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), - handler: (req, res, next, opt) => { + handler: (req, res) => { return res.status(429).json({ "status": "rate-limit", "text": loc(languageCode(req), 'ErrorRateLimit') diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 423acdf3..ef5b448c 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -679,7 +679,7 @@ window.onload = () => { } } -eid("url-input-area").addEventListener("keydown", (e) => { +eid("url-input-area").addEventListener("keydown", () => { button(); }) eid("url-input-area").addEventListener("keyup", (e) => { diff --git a/src/localization/manager.js b/src/localization/manager.js index c22f9500..2344241b 100644 --- a/src/localization/manager.js +++ b/src/localization/manager.js @@ -8,7 +8,7 @@ let loc = {} let languages = []; export async function loadLoc() { - const files = await fs.promises.readdir(locPath).catch((e) => { return [] }); + const files = await fs.promises.readdir(locPath).catch(() => []); files.forEach(file => { loc[file.split('.')[0]] = loadJSON(`${locPath}/${file}`); languages.push(file.split('.')[0]) diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index 05c7fd84..b2866c8f 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -1,5 +1,4 @@ import psl from "psl"; -import { genericUserAgent } from "../../config.js"; const API_KEY = 'jrsCWX1XDuVxAFO4GkK147syAoN8BJZ5voz8tS80bPcj26Vc5Z'; const API_BASE = 'https://api-http2.tumblr.com'; diff --git a/src/modules/sub/crypto.js b/src/modules/sub/crypto.js index b3a0539b..35b55b38 100644 --- a/src/modules/sub/crypto.js +++ b/src/modules/sub/crypto.js @@ -1,4 +1,4 @@ -import { createHmac, createCipheriv, createDecipheriv, scryptSync } from "crypto"; +import { createHmac, createCipheriv, createDecipheriv } from "crypto"; const algorithm = "aes256" From 5b50d441eb533aa420d4e85def770c9e34d9605a Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 14 May 2024 13:35:03 +0600 Subject: [PATCH 87/99] docs: update package link in docker config --- docs/examples/docker-compose.example.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index 82564057..24212ae4 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:7 + image: ghcr.io/imputnet/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:7 + image: ghcr.io/imputnet/cobalt:7 restart: unless-stopped container_name: cobalt-web From 9332b2e19660973f54dcd2636c4409736e0a69cd Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 14 May 2024 09:20:58 +0000 Subject: [PATCH 88/99] config/env: validate platform for processingPriority on startup instead of for every stream thread --- src/modules/config.js | 4 +++- src/modules/stream/types.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/config.js b/src/modules/config.js index b774a8b6..93347ad4 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -32,7 +32,9 @@ const corsWildcard: process.env.CORS_WILDCARD !== '0', corsURL: process.env.CORS_URL, cookiePath: process.env.COOKIE_PATH, - processingPriority: process.env.PROCESSING_PRIORITY && parseInt(process.env.PROCESSING_PRIORITY), + processingPriority: process.platform !== "win32" + && process.env.PROCESSING_PRIORITY + && parseInt(process.env.PROCESSING_PRIORITY), tiktokDeviceInfo: process.env.TIKTOK_DEVICE_INFO && JSON.parse(process.env.TIKTOK_DEVICE_INFO), apiURL } diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 7054a3b2..87c9f600 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -44,7 +44,7 @@ function pipe(from, to, done) { } function getCommand(args) { - if (!isNaN(env.processingPriority) && process.platform !== "win32") { + if (!isNaN(env.processingPriority)) { return ['nice', ['-n', env.processingPriority.toString(), ffmpeg, ...args]] } return [ffmpeg, args] From cc1e9dcff88990dde35ff81c256f5d8608fd301d Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Mon, 13 May 2024 18:20:10 +0000 Subject: [PATCH 89/99] api: add API_LISTEN_ADDRESS env for specifying bind address --- docs/run-an-instance.md | 1 + src/core/api.js | 2 +- src/modules/config.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 9e607942..d89d506c 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -53,6 +53,7 @@ sudo service nscd start | variable name | default | example | description | |:----------------------|:----------|:------------------------|:------------| | `API_PORT` | `9000` | `9000` | changes port from which api server is accessible. | +| `API_LISTEN_ADDRESS` | `0.0.0.0` | `127.0.0.1` | changes address from which api server is accessible. **if you are using docker, you usually don't need to configure this.** | | `API_URL` | ➖ | `https://co.wuk.sh/` | changes url from which api server is accessible.
***REQUIRED TO RUN API***. | | `API_NAME` | `unknown` | `ams-1` | api server name that is shown in `/api/serverInfo`. | | `CORS_WILDCARD` | `1` | `0` | toggles cross-origin resource sharing.
`0`: disabled. `1`: enabled. | diff --git a/src/core/api.js b/src/core/api.js index 440c25c2..31ed7dd5 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -194,7 +194,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { res.redirect('/api/json') }); - app.listen(env.apiPort, () => { + app.listen(env.apiPort, env.listenAddress, () => { console.log(`\n` + `${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + diff --git a/src/modules/config.js b/src/modules/config.js index 93347ad4..89ac1b33 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -29,6 +29,7 @@ const apiEnvs = { apiPort: process.env.API_PORT || 9000, apiName: process.env.API_NAME || 'unknown', + listenAddress: process.env.API_LISTEN_ADDRESS, corsWildcard: process.env.CORS_WILDCARD !== '0', corsURL: process.env.CORS_URL, cookiePath: process.env.COOKIE_PATH, From 0114e686b8a8ef3de984d1e003c66aa375e2dc48 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sun, 12 May 2024 14:06:04 +0000 Subject: [PATCH 90/99] api: add FREEBIND_CIDR env variable --- docs/run-an-instance.md | 11 +++++++++-- src/modules/config.js | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index d89d506c..75b6ef7e 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -60,7 +60,8 @@ sudo service nscd start | `CORS_URL` | not used | `https://cobalt.tools/` | cross-origin resource sharing url. api will be available only from this url if `CORS_WILDCARD` is set to `0`. | | `COOKIE_PATH` | not used | `/cookies.json` | path for cookie file relative to main folder. | | `PROCESSING_PRIORITY` | not used | `10` | changes `nice` value* for ffmpeg subprocess. available only on unix systems. | -| `TIKTOK_DEVICE_INFO` | ➖ | *see below* | device info (including `iid` and `device_id`) for tiktok functionality. required for tiktok to work. | +| `TIKTOK_DEVICE_INFO` | ➖ | *see below* | device info (including `iid` and `device_id`) for tiktok functionality. required for tiktok to work. | +| `FREEBIND_CIDR` | ➖ | `2001:db8::/32` | IPv6 prefix used for randomly assigning addresses to cobalt requests. Only supported on Linux systems. For more info, see below. | \* the higher the nice value, the lower the priority. [read more here](https://en.wikipedia.org/wiki/Nice_(Unix)). @@ -86,6 +87,12 @@ you can compress the json to save space. if you're using a `.env` file then the TIKTOK_DEVICE_INFO='{"iid":"","device_id":"","channel":"googleplay","app_name":"musical_ly","version_code":"310503","device_platform":"android","device_type":"Redmi+7","os_version":"13"}' ``` +#### FREEBIND_CIDR +setting a `FREEBIND_CIDR` allows cobalt to pick a random IP for every download and use it for all +requests it makes for that particular download. to use freebind in cobalt, you need to follow its [setup instructions](https://github.com/imputnet/freebind.js?tab=readme-ov-file#setup) first. if you configure this option while running cobalt +in a docker container, you also need to set the `API_LISTEN_ADDRESS` env to `127.0.0.1`, and set +`network_mode` for the container to `host`. + ### variables for web | variable name | default | example | description | |:---------------------|:---------------------|:------------------------|:--------------------------------------------------------------------------------------| @@ -96,4 +103,4 @@ TIKTOK_DEVICE_INFO='{"iid":"","device_id":"","c | `IS_BETA` | `0` | `1` | toggles beta tag next to cobalt logo.
`0`: disabled. `1`: enabled. | | `PLAUSIBLE_HOSTNAME` | ➖ | `plausible.io`* | enables plausible analytics with provided hostname as receiver backend. | -\* don't use plausible.io as receiver backend unless you paid for their cloud service. use your own domain when hosting community edition of plausible. refer to their [docs](https://plausible.io/docs) when needed. +\* don't use plausible.io as receiver backend unless you paid for their cloud service. use your own domain when hosting community edition of plausible. refer to their [docs](https://plausible.io/docs) when needed. \ No newline at end of file diff --git a/src/modules/config.js b/src/modules/config.js index 89ac1b33..1722b795 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -1,5 +1,6 @@ import UrlPattern from "url-pattern"; import { loadJSON } from "./sub/loadFromFs.js"; + const config = loadJSON("./src/config.json"); const packageJson = loadJSON("./package.json"); const servicesConfigJson = loadJSON("./src/modules/processing/servicesConfig.json"); @@ -37,6 +38,7 @@ const && process.env.PROCESSING_PRIORITY && parseInt(process.env.PROCESSING_PRIORITY), tiktokDeviceInfo: process.env.TIKTOK_DEVICE_INFO && JSON.parse(process.env.TIKTOK_DEVICE_INFO), + freebindCIDR: process.platform === 'linux' && process.env.FREEBIND_CIDR, apiURL } From c306a944d953e6f8ab47f7bfe3f81f7627880fe8 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sun, 12 May 2024 16:12:45 +0000 Subject: [PATCH 91/99] match: add freebind support for youtube and instagram --- package.json | 3 +++ src/modules/processing/match.js | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ffb78672..1b97a04d 100644 --- a/package.json +++ b/package.json @@ -41,5 +41,8 @@ "undici": "^6.7.0", "url-pattern": "1.0.3", "youtubei.js": "^9.3.0" + }, + "optionalDependencies": { + "freebind": "^0.2.2" } } diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index a6432e49..d89dc9d9 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -25,9 +25,21 @@ import streamable from "./services/streamable.js"; import twitch from "./services/twitch.js"; import rutube from "./services/rutube.js"; import dailymotion from "./services/dailymotion.js"; +import { env } from '../config.js'; +let freebind; export default async function(host, patternMatch, url, lang, obj) { assert(url instanceof URL); + let dispatcher, requestIP; + + if (env.freebindCIDR) { + if (!freebind) { + freebind = await import('freebind'); + } + + requestIP = freebind.ip.random(env.freebindCIDR); + dispatcher = freebind.dispatcherFromIP(requestIP, { strict: false }); + } try { let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata; @@ -66,7 +78,8 @@ export default async function(host, patternMatch, url, lang, obj) { format: obj.vCodec, isAudioOnly: isAudioOnly, isAudioMuted: obj.isAudioMuted, - dubLang: obj.dubLang + dubLang: obj.dubLang, + dispatcher } if (url.hostname === 'music.youtube.com' || isAudioOnly === true) { @@ -122,7 +135,8 @@ export default async function(host, patternMatch, url, lang, obj) { case "instagram": r = await instagram({ ...patternMatch, - quality: obj.vQuality + quality: obj.vQuality, + dispatcher }) break; case "vine": From d8913472ad69d32e4148ef3d9c99fe57dca3f880 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sun, 12 May 2024 16:04:05 +0000 Subject: [PATCH 92/99] package.json: revert undici version to 5.x so that it matches with youtubei.js's version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b97a04d..2daa403c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "node-cache": "^5.1.2", "psl": "1.9.0", "set-cookie-parser": "2.6.0", - "undici": "^6.7.0", + "undici": "^5.19.1", "url-pattern": "1.0.3", "youtubei.js": "^9.3.0" }, From d5aa27f5f904f3c993691ca901546b29bc4b06e5 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sun, 12 May 2024 16:13:01 +0000 Subject: [PATCH 93/99] youtube: use the freebind dispatcher if available --- src/modules/processing/services/youtube.js | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 10b04a5c..d11b2739 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -1,8 +1,9 @@ -import { Innertube } from 'youtubei.js'; +import { Innertube, Session } from 'youtubei.js'; import { maxVideoDuration } from '../../config.js'; import { cleanString } from '../../sub/utils.js'; +import { fetch } from 'undici' -const yt = await Innertube.create(); +const ytBase = await Innertube.create(); const codecMatch = { h264: { @@ -22,7 +23,27 @@ const codecMatch = { } } +const cloneInnertube = (customFetch) => { + const session = new Session( + ytBase.session.context, + ytBase.session.key, + ytBase.session.api_version, + ytBase.session.account_index, + ytBase.session.player, + undefined, + customFetch ?? ytBase.session.http.fetch, + ytBase.session.cache + ); + + const yt = new Innertube(session); + return yt; +} + export default async function(o) { + const yt = cloneInnertube( + (input, init) => fetch(input, { ...init, dispatcher: o.dispatcher }) + ); + let info, isDubbed, format = o.format || "h264"; let quality = o.quality === "max" ? "9000" : o.quality; // 9000(p) - max quality From 9419266cd7c5b21088aaf5e00f2c7f8a570fa567 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sun, 12 May 2024 16:35:52 +0000 Subject: [PATCH 94/99] stream: use freebind dispatcher in internal streams --- src/modules/processing/match.js | 3 ++- src/modules/processing/matchActionDecider.js | 5 +++-- src/modules/stream/internal.js | 3 +++ src/modules/stream/manage.js | 14 ++++++++++++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index d89dc9d9..6bbd0d2a 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -195,7 +195,8 @@ export default async function(host, patternMatch, url, lang, obj) { return matchActionDecider( r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, - obj.filenamePattern, obj.twitterGif + obj.filenamePattern, obj.twitterGif, + requestIP ) } catch (e) { return apiJSON(0, { t: genericError(lang, host) }) diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 44a4a81b..d440cff6 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -3,7 +3,7 @@ import { apiJSON } from "../sub/utils.js"; import loc from "../../localization/manager.js"; import createFilename from "./createFilename.js"; -export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif) { +export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif, requestIP) { let action, responseType = 2, defaultParams = { @@ -11,7 +11,8 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di service: host, filename: r.filenameAttributes ? createFilename(r.filenameAttributes, filenamePattern, isAudioOnly, isAudioMuted) : r.filename, - fileMetadata: !disableMetadata ? r.fileMetadata : false + fileMetadata: !disableMetadata ? r.fileMetadata : false, + requestIP }, params = {}, audioFormat = String(userFormat); diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index 412ba546..9578da9b 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -18,6 +18,7 @@ async function* readChunks(streamInfo, size) { ...getHeaders('youtube'), Range: `bytes=${read}-${read + CHUNK_SIZE}` }, + dispatcher: streamInfo.dispatcher, signal: streamInfo.controller.signal }); @@ -47,6 +48,7 @@ async function handleYoutubeStream(streamInfo, res) { const req = await fetch(streamInfo.url, { headers: getHeaders('youtube'), method: 'HEAD', + dispatcher: streamInfo.dispatcher, signal: streamInfo.controller.signal }); @@ -81,6 +83,7 @@ export async function internalStream(streamInfo, res) { ...streamInfo.headers, host: undefined }, + dispatcher: streamInfo.dispatcher, signal: streamInfo.controller.signal, maxRedirections: 16 }); diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index e8357103..7d19354f 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -6,6 +6,9 @@ import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; import { streamLifespan, env } from "../config.js"; import { strict as assert } from "assert"; +// optional dependency +const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); + const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube']; const streamNoAccess = { @@ -46,7 +49,8 @@ export function createStream(obj) { isAudioOnly: !!obj.isAudioOnly, copy: !!obj.copy, mute: !!obj.mute, - metadata: obj.fileMetadata || false + metadata: obj.fileMetadata || false, + requestIP: obj.requestIP }; streamCache.set( @@ -78,11 +82,17 @@ export function getInternalStream(id) { export function createInternalStream(url, obj = {}) { assert(typeof url === 'string'); + let dispatcher; + if (obj.requestIP) { + dispatcher = freebind?.dispatcherFromIP(obj.requestIP, { strict: false }) + } + const streamID = nanoid(); internalStreamCache[streamID] = { url, service: obj.service, - controller: new AbortController() + controller: new AbortController(), + dispatcher }; let streamLink = new URL('/api/istream', `http://127.0.0.1:${env.apiPort}`); From 3fe60046008364d6425a95a2db02cf6cdad5a54b Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Mon, 13 May 2024 18:55:42 +0000 Subject: [PATCH 95/99] dockerfile: fix freebind building, set up everything in one step this also shrinks the image by around 40MB, since the apt/lists layer no longer sticks around --- Dockerfile | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4eee25b9..c98785d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,13 @@ FROM node:18-bullseye-slim WORKDIR /app -RUN apt-get update -RUN apt-get install -y git -RUN rm -rf /var/lib/apt/lists/* - COPY package*.json ./ -RUN npm install -RUN git clone -n https://github.com/imputnet/cobalt.git --depth 1 && mv cobalt/.git ./ && rm -rf cobalt +RUN apt-get update && \ + apt-get install -y git python3 build-essential && \ + npm install && \ + apt purge --autoremove -y python3 build-essential && \ + rm -rf ~/.cache/ /var/lib/apt/lists/* COPY . . EXPOSE 9000 From ce6bafadf9b43e04f02528cc309b58b2564a8853 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 14 May 2024 15:09:44 +0600 Subject: [PATCH 96/99] docs: fix capitalisation in run-an-instance.md Signed-off-by: wukko --- docs/run-an-instance.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 75b6ef7e..25145224 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -61,7 +61,7 @@ sudo service nscd start | `COOKIE_PATH` | not used | `/cookies.json` | path for cookie file relative to main folder. | | `PROCESSING_PRIORITY` | not used | `10` | changes `nice` value* for ffmpeg subprocess. available only on unix systems. | | `TIKTOK_DEVICE_INFO` | ➖ | *see below* | device info (including `iid` and `device_id`) for tiktok functionality. required for tiktok to work. | -| `FREEBIND_CIDR` | ➖ | `2001:db8::/32` | IPv6 prefix used for randomly assigning addresses to cobalt requests. Only supported on Linux systems. For more info, see below. | +| `FREEBIND_CIDR` | ➖ | `2001:db8::/32` | IPv6 prefix used for randomly assigning addresses to cobalt requests. only supported on linux systems. for more info, see below. | \* the higher the nice value, the lower the priority. [read more here](https://en.wikipedia.org/wiki/Nice_(Unix)). @@ -103,4 +103,4 @@ in a docker container, you also need to set the `API_LISTEN_ADDRESS` env to `127 | `IS_BETA` | `0` | `1` | toggles beta tag next to cobalt logo.
`0`: disabled. `1`: enabled. | | `PLAUSIBLE_HOSTNAME` | ➖ | `plausible.io`* | enables plausible analytics with provided hostname as receiver backend. | -\* don't use plausible.io as receiver backend unless you paid for their cloud service. use your own domain when hosting community edition of plausible. refer to their [docs](https://plausible.io/docs) when needed. \ No newline at end of file +\* don't use plausible.io as receiver backend unless you paid for their cloud service. use your own domain when hosting community edition of plausible. refer to their [docs](https://plausible.io/docs) when needed. From 4b0814a2ecd8ca9b49cf3f110918a74d41c9dd6e Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 14 May 2024 15:37:41 +0600 Subject: [PATCH 97/99] config: clean up --- src/modules/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/config.js b/src/modules/config.js index 1722b795..c66521e1 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -34,7 +34,7 @@ const corsWildcard: process.env.CORS_WILDCARD !== '0', corsURL: process.env.CORS_URL, cookiePath: process.env.COOKIE_PATH, - processingPriority: process.platform !== "win32" + processingPriority: process.platform !== 'win32' && process.env.PROCESSING_PRIORITY && parseInt(process.env.PROCESSING_PRIORITY), tiktokDeviceInfo: process.env.TIKTOK_DEVICE_INFO && JSON.parse(process.env.TIKTOK_DEVICE_INFO), @@ -49,7 +49,7 @@ export const streamLifespan = config.streamLifespan, maxVideoDuration = config.maxVideoDuration, genericUserAgent = config.genericUserAgent, - repo = packageJson["bugs"]["url"].replace('/issues', ''), + repo = packageJson.bugs.url.replace('/issues', ''), authorInfo = config.authorInfo, donations = config.donations, ffmpegArgs = config.ffmpegArgs, From e44927e5adf7f9d8477362f5a7fba678661cb442 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 14 May 2024 22:08:32 +0600 Subject: [PATCH 98/99] instagram: add freebind dispatcher support --- src/modules/processing/services/instagram.js | 591 ++++++++++--------- 1 file changed, 299 insertions(+), 292 deletions(-) diff --git a/src/modules/processing/services/instagram.js b/src/modules/processing/services/instagram.js index 68420a5d..bcbf78bb 100644 --- a/src/modules/processing/services/instagram.js +++ b/src/modules/processing/services/instagram.js @@ -41,299 +41,306 @@ const cachedDtsg = { expiry: 0 } -async function findDtsgId(cookie) { - try { - if (cachedDtsg.expiry > Date.now()) return cachedDtsg.value; - - const data = await fetch('https://www.instagram.com/', { - headers: { - ...commonHeaders, - cookie - } - }).then(r => r.text()); - - const token = data.match(/"dtsg":{"token":"(.*?)"/)[1]; - - cachedDtsg.value = token; - cachedDtsg.expiry = Date.now() + 86390000; - - if (token) return token; - return false; - } - catch {} -} - -async function request(url, cookie, method = 'GET', requestData) { - let headers = { - ...commonHeaders, - 'x-ig-www-claim': cookie?._wwwClaim || '0', - 'x-csrftoken': cookie?.values()?.csrftoken, - cookie - } - if (method === 'POST') { - headers['content-type'] = 'application/x-www-form-urlencoded'; - } - - const data = await fetch(url, { - method, - headers, - body: requestData && new URLSearchParams(requestData), - }); - - if (data.headers.get('X-Ig-Set-Www-Claim') && cookie) - cookie._wwwClaim = data.headers.get('X-Ig-Set-Www-Claim'); - - updateCookie(cookie, data.headers); - return data.json(); -} -async function getMediaId(id, { cookie, token } = {}) { - const oembedURL = new URL('https://i.instagram.com/api/v1/oembed/'); - oembedURL.searchParams.set('url', `https://www.instagram.com/p/${id}/`); - - const oembed = await fetch(oembedURL, { - headers: { - ...mobileHeaders, - ...( token && { authorization: `Bearer ${token}` } ), - cookie - } - }).then(r => r.json()).catch(() => {}); - - return oembed?.media_id; -} - -async function requestMobileApi(mediaId, { cookie, token } = {}) { - const mediaInfo = await fetch(`https://i.instagram.com/api/v1/media/${mediaId}/info/`, { - headers: { - ...mobileHeaders, - ...( token && { authorization: `Bearer ${token}` } ), - cookie - } - }).then(r => r.json()).catch(() => {}); - - return mediaInfo?.items?.[0]; -} -async function requestHTML(id, cookie) { - const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, { - headers: { - ...embedHeaders, - cookie - } - }).then(r => r.text()).catch(() => {}); - - let embedData = JSON.parse(data?.match(/"init",\[\],\[(.*?)\]\],/)[1]); - - if (!embedData || !embedData?.contextJSON) return false; - - embedData = JSON.parse(embedData.contextJSON); - - return embedData; -} -async function requestGQL(id, cookie) { - let dtsgId; - - if (cookie) { - dtsgId = await findDtsgId(cookie); - } - const url = new URL('https://www.instagram.com/api/graphql/'); - - const requestData = { - jazoest: '26406', - variables: JSON.stringify({ - shortcode: id, - __relay_internal__pv__PolarisShareMenurelayprovider: false - }), - doc_id: '7153618348081770' - }; - if (dtsgId) { - requestData.fb_dtsg = dtsgId; - } - - return (await request(url, cookie, 'POST', requestData)) - .data - ?.xdt_api__v1__media__shortcode__web_info - ?.items - ?.[0]; -} - -function extractOldPost(data, id) { - const sidecar = data?.gql_data?.shortcode_media?.edge_sidecar_to_children; - if (sidecar) { - const picker = sidecar.edges.filter(e => e.node?.display_url) - .map(e => { - const type = e.node?.is_video ? "video" : "photo"; - const url = type === "video" ? e.node?.video_url : e.node?.display_url; - - return { - type, url, - /* thumbnails have `Cross-Origin-Resource-Policy` - ** set to `same-origin`, so we need to proxy them */ - thumb: createStream({ - service: "instagram", - type: "default", - u: e.node?.display_url, - filename: "image.jpg" - }) - } - }); - - if (picker.length) return { picker } - } else if (data?.gql_data?.shortcode_media?.video_url) { - return { - urls: data.gql_data.shortcode_media.video_url, - filename: `instagram_${id}.mp4`, - audioFilename: `instagram_${id}_audio` - } - } else if (data?.gql_data?.shortcode_media?.display_url) { - return { - urls: data.gql_data?.shortcode_media.display_url, - isPhoto: true - } - } -} - -function extractNewPost(data, id) { - const carousel = data.carousel_media; - if (carousel) { - const picker = carousel.filter(e => e?.image_versions2) - .map(e => { - const type = e.video_versions ? "video" : "photo"; - const imageUrl = e.image_versions2.candidates[0].url; - - let url = imageUrl; - if (type === 'video') { - const video = e.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a); - url = video.url; - } - - return { - type, url, - /* thumbnails have `Cross-Origin-Resource-Policy` - ** set to `same-origin`, so we need to proxy them */ - thumb: createStream({ - service: "instagram", - type: "default", - u: imageUrl, - filename: "image.jpg" - }) - } - }); - - if (picker.length) return { picker } - } else if (data.video_versions) { - const video = data.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a) - return { - urls: video.url, - filename: `instagram_${id}.mp4`, - audioFilename: `instagram_${id}_audio` - } - } else if (data.image_versions2?.candidates) { - return { - urls: data.image_versions2.candidates[0].url, - isPhoto: true - } - } -} - -async function getPost(id) { - let data, result; - try { - const cookie = getCookie('instagram'); - - const bearer = getCookie('instagram_bearer'); - const token = bearer?.values()?.token; - - // get media_id for mobile api, three methods - let media_id = await getMediaId(id); - if (!media_id && token) media_id = await getMediaId(id, { token }); - if (!media_id && cookie) media_id = await getMediaId(id, { cookie }); - - // mobile api (bearer) - if (media_id && token) data = await requestMobileApi(id, { token }); - - // mobile api (no cookie, cookie) - if (!data && media_id) data = await requestMobileApi(id); - if (!data && media_id && cookie) data = await requestMobileApi(id, { cookie }); - - // html embed (no cookie, cookie) - if (!data) data = await requestHTML(id); - if (!data && cookie) data = await requestHTML(id, cookie); - - // web app graphql api (no cookie, cookie) - if (!data) data = await requestGQL(id); - if (!data && cookie) data = await requestGQL(id, cookie); - } catch {} - - if (!data) return { error: 'ErrorCouldntFetch' }; - - if (data?.gql_data) { - result = extractOldPost(data, id) - } else { - result = extractNewPost(data, id) - } - - if (result) return result; - return { error: 'ErrorEmptyDownload' } -} - -async function usernameToId(username, cookie) { - const url = new URL('https://www.instagram.com/api/v1/users/web_profile_info/'); - url.searchParams.set('username', username); - - try { - const data = await request(url, cookie); - return data?.data?.user?.id; - } catch {} -} - -async function getStory(username, id) { - const cookie = getCookie('instagram'); - if (!cookie) return { error: 'ErrorUnsupported' }; - - const userId = await usernameToId(username, cookie); - if (!userId) return { error: 'ErrorEmptyDownload' }; - - const dtsgId = await findDtsgId(cookie); - - const url = new URL('https://www.instagram.com/api/graphql/'); - const requestData = { - fb_dtsg: dtsgId, - jazoest: '26438', - variables: JSON.stringify({ - reel_ids_arr : [ userId ], - }), - server_timestamps: true, - doc_id: '25317500907894419' - }; - - let media; - try { - const data = (await request(url, cookie, 'POST', requestData)); - media = data?.data?.xdt_api__v1__feed__reels_media?.reels_media?.find(m => m.id === userId); - } catch {} - - const item = media.items.find(m => m.pk === id); - if (!item) return { error: 'ErrorEmptyDownload' }; - - if (item.video_versions) { - const video = item.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a) - return { - urls: video.url, - filename: `instagram_${id}.mp4`, - audioFilename: `instagram_${id}_audio` - } - } - - if (item.image_versions2?.candidates) { - return { - urls: item.image_versions2.candidates[0].url, - isPhoto: true - } - } - - return { error: 'ErrorCouldntFetch' }; -} - export default function(obj) { + const dispatcher = obj.dispatcher; + + async function findDtsgId(cookie) { + try { + if (cachedDtsg.expiry > Date.now()) return cachedDtsg.value; + + const data = await fetch('https://www.instagram.com/', { + headers: { + ...commonHeaders, + cookie + }, + dispatcher + }).then(r => r.text()); + + const token = data.match(/"dtsg":{"token":"(.*?)"/)[1]; + + cachedDtsg.value = token; + cachedDtsg.expiry = Date.now() + 86390000; + + if (token) return token; + return false; + } + catch {} + } + + async function request(url, cookie, method = 'GET', requestData) { + let headers = { + ...commonHeaders, + 'x-ig-www-claim': cookie?._wwwClaim || '0', + 'x-csrftoken': cookie?.values()?.csrftoken, + cookie + } + if (method === 'POST') { + headers['content-type'] = 'application/x-www-form-urlencoded'; + } + + const data = await fetch(url, { + method, + headers, + body: requestData && new URLSearchParams(requestData), + dispatcher + }); + + if (data.headers.get('X-Ig-Set-Www-Claim') && cookie) + cookie._wwwClaim = data.headers.get('X-Ig-Set-Www-Claim'); + + updateCookie(cookie, data.headers); + return data.json(); + } + async function getMediaId(id, { cookie, token } = {}) { + const oembedURL = new URL('https://i.instagram.com/api/v1/oembed/'); + oembedURL.searchParams.set('url', `https://www.instagram.com/p/${id}/`); + + const oembed = await fetch(oembedURL, { + headers: { + ...mobileHeaders, + ...( token && { authorization: `Bearer ${token}` } ), + cookie + }, + dispatcher + }).then(r => r.json()).catch(() => {}); + + return oembed?.media_id; + } + + async function requestMobileApi(mediaId, { cookie, token } = {}) { + const mediaInfo = await fetch(`https://i.instagram.com/api/v1/media/${mediaId}/info/`, { + headers: { + ...mobileHeaders, + ...( token && { authorization: `Bearer ${token}` } ), + cookie + }, + dispatcher + }).then(r => r.json()).catch(() => {}); + + return mediaInfo?.items?.[0]; + } + async function requestHTML(id, cookie) { + const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, { + headers: { + ...embedHeaders, + cookie + }, + dispatcher + }).then(r => r.text()).catch(() => {}); + + let embedData = JSON.parse(data?.match(/"init",\[\],\[(.*?)\]\],/)[1]); + + if (!embedData || !embedData?.contextJSON) return false; + + embedData = JSON.parse(embedData.contextJSON); + + return embedData; + } + async function requestGQL(id, cookie) { + let dtsgId; + + if (cookie) { + dtsgId = await findDtsgId(cookie); + } + const url = new URL('https://www.instagram.com/api/graphql/'); + + const requestData = { + jazoest: '26406', + variables: JSON.stringify({ + shortcode: id, + __relay_internal__pv__PolarisShareMenurelayprovider: false + }), + doc_id: '7153618348081770' + }; + if (dtsgId) { + requestData.fb_dtsg = dtsgId; + } + + return (await request(url, cookie, 'POST', requestData)) + .data + ?.xdt_api__v1__media__shortcode__web_info + ?.items + ?.[0]; + } + + function extractOldPost(data, id) { + const sidecar = data?.gql_data?.shortcode_media?.edge_sidecar_to_children; + if (sidecar) { + const picker = sidecar.edges.filter(e => e.node?.display_url) + .map(e => { + const type = e.node?.is_video ? "video" : "photo"; + const url = type === "video" ? e.node?.video_url : e.node?.display_url; + + return { + type, url, + /* thumbnails have `Cross-Origin-Resource-Policy` + ** set to `same-origin`, so we need to proxy them */ + thumb: createStream({ + service: "instagram", + type: "default", + u: e.node?.display_url, + filename: "image.jpg" + }) + } + }); + + if (picker.length) return { picker } + } else if (data?.gql_data?.shortcode_media?.video_url) { + return { + urls: data.gql_data.shortcode_media.video_url, + filename: `instagram_${id}.mp4`, + audioFilename: `instagram_${id}_audio` + } + } else if (data?.gql_data?.shortcode_media?.display_url) { + return { + urls: data.gql_data?.shortcode_media.display_url, + isPhoto: true + } + } + } + + function extractNewPost(data, id) { + const carousel = data.carousel_media; + if (carousel) { + const picker = carousel.filter(e => e?.image_versions2) + .map(e => { + const type = e.video_versions ? "video" : "photo"; + const imageUrl = e.image_versions2.candidates[0].url; + + let url = imageUrl; + if (type === 'video') { + const video = e.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a); + url = video.url; + } + + return { + type, url, + /* thumbnails have `Cross-Origin-Resource-Policy` + ** set to `same-origin`, so we need to proxy them */ + thumb: createStream({ + service: "instagram", + type: "default", + u: imageUrl, + filename: "image.jpg" + }) + } + }); + + if (picker.length) return { picker } + } else if (data.video_versions) { + const video = data.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a) + return { + urls: video.url, + filename: `instagram_${id}.mp4`, + audioFilename: `instagram_${id}_audio` + } + } else if (data.image_versions2?.candidates) { + return { + urls: data.image_versions2.candidates[0].url, + isPhoto: true + } + } + } + + async function getPost(id) { + let data, result; + try { + const cookie = getCookie('instagram'); + + const bearer = getCookie('instagram_bearer'); + const token = bearer?.values()?.token; + + // get media_id for mobile api, three methods + let media_id = await getMediaId(id); + if (!media_id && token) media_id = await getMediaId(id, { token }); + if (!media_id && cookie) media_id = await getMediaId(id, { cookie }); + + // mobile api (bearer) + if (media_id && token) data = await requestMobileApi(id, { token }); + + // mobile api (no cookie, cookie) + if (!data && media_id) data = await requestMobileApi(id); + if (!data && media_id && cookie) data = await requestMobileApi(id, { cookie }); + + // html embed (no cookie, cookie) + if (!data) data = await requestHTML(id); + if (!data && cookie) data = await requestHTML(id, cookie); + + // web app graphql api (no cookie, cookie) + if (!data) data = await requestGQL(id); + if (!data && cookie) data = await requestGQL(id, cookie); + } catch {} + + if (!data) return { error: 'ErrorCouldntFetch' }; + + if (data?.gql_data) { + result = extractOldPost(data, id) + } else { + result = extractNewPost(data, id) + } + + if (result) return result; + return { error: 'ErrorEmptyDownload' } + } + + async function usernameToId(username, cookie) { + const url = new URL('https://www.instagram.com/api/v1/users/web_profile_info/'); + url.searchParams.set('username', username); + + try { + const data = await request(url, cookie); + return data?.data?.user?.id; + } catch {} + } + + async function getStory(username, id) { + const cookie = getCookie('instagram'); + if (!cookie) return { error: 'ErrorUnsupported' }; + + const userId = await usernameToId(username, cookie); + if (!userId) return { error: 'ErrorEmptyDownload' }; + + const dtsgId = await findDtsgId(cookie); + + const url = new URL('https://www.instagram.com/api/graphql/'); + const requestData = { + fb_dtsg: dtsgId, + jazoest: '26438', + variables: JSON.stringify({ + reel_ids_arr : [ userId ], + }), + server_timestamps: true, + doc_id: '25317500907894419' + }; + + let media; + try { + const data = (await request(url, cookie, 'POST', requestData)); + media = data?.data?.xdt_api__v1__feed__reels_media?.reels_media?.find(m => m.id === userId); + } catch {} + + const item = media.items.find(m => m.pk === id); + if (!item) return { error: 'ErrorEmptyDownload' }; + + if (item.video_versions) { + const video = item.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a) + return { + urls: video.url, + filename: `instagram_${id}.mp4`, + audioFilename: `instagram_${id}_audio` + } + } + + if (item.image_versions2?.candidates) { + return { + urls: item.image_versions2.candidates[0].url, + isPhoto: true + } + } + + return { error: 'ErrorCouldntFetch' }; + } + const { postId, storyId, username } = obj; if (postId) return getPost(postId); if (username && storyId) return getStory(username, storyId); From 5c9ecb2781c698248386677dbefd80ea45d585b5 Mon Sep 17 00:00:00 2001 From: jsopn Date: Wed, 15 May 2024 18:56:10 +0700 Subject: [PATCH 99/99] pinterest: implement image and gifs support (#475) Co-authored-by: jj Co-authored-by: wukko --- README.md | 2 +- src/modules/processing/services/pinterest.js | 22 ++++++++++---- src/modules/processing/servicesConfig.json | 2 +- src/test/tests.json | 32 ++++++++++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8374817a..e9da513a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ this list is not final and keeps expanding over time. if support for a service y | service | notes or features | | :-------- | :----- | | instagram | supports photos, videos, and stories. lets you pick what to save from multi-media posts. | -| pinterest | supports videos and stories. | +| pinterest | supports photos, gifs, videos and stories. | | reddit | supports gifs and videos. | | soundcloud | supports private links. | | tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. | diff --git a/src/modules/processing/services/pinterest.js b/src/modules/processing/services/pinterest.js index 2364b729..23d9f4d8 100644 --- a/src/modules/processing/services/pinterest.js +++ b/src/modules/processing/services/pinterest.js @@ -1,6 +1,7 @@ import { genericUserAgent } from "../../config.js"; -const linkRegex = /"url":"(https:\/\/v1.pinimg.com\/videos\/.*?)"/g; +const videoRegex = /"url":"(https:\/\/v1.pinimg.com\/videos\/.*?)"/g; +const imageRegex = /src="(https:\/\/i\.pinimg\.com\/.*\.(jpg|gif))"/g; export default async function(o) { let id = o.id; @@ -19,15 +20,24 @@ export default async function(o) { if (!html) return { error: 'ErrorCouldntFetch' }; - let videoLink = [...html.matchAll(linkRegex)] + let videoLink = [...html.matchAll(videoRegex)] .map(([, link]) => link) - .filter(a => a.endsWith('.mp4') && a.includes('720p'))[0]; + .find(a => a.endsWith('.mp4') && a.includes('720p')); - if (!videoLink) return { error: 'ErrorEmptyDownload' }; - - return { + if (videoLink) return { urls: videoLink, filename: `pinterest_${o.id}.mp4`, audioFilename: `pinterest_${o.id}_audio` } + + let imageLink = [...html.matchAll(imageRegex)] + .map(([, link]) => link) + .find(a => a.endsWith('.jpg') || a.endsWith('.gif')); + + if (imageLink) return { + urls: imageLink, + isPhoto: true + } + + return { error: 'ErrorEmptyDownload' }; } diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 0b32016c..eaea0167 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -91,7 +91,7 @@ "enabled": true }, "pinterest": { - "alias": "pinterest videos & stories", + "alias": "pinterest (all media)", "patterns": ["pin/:id", "pin/:id/:garbage", "url_shortener/:shortLink"], "enabled": true }, diff --git a/src/test/tests.json b/src/test/tests.json index 95c95a65..a0ef8e6b 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -976,6 +976,38 @@ "code": 200, "status": "redirect" } + }, { + "name": "regular picture", + "url": "https://www.pinterest.com/pin/412994228343400946/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "regular picture (.ca TLD)", + "url": "https://www.pinterest.ca/pin/412994228343400946/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "regular gif", + "url": "https://www.pinterest.com/pin/814447913881127862/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "regular gif (.ca TLD)", + "url": "https://www.pinterest.ca/pin/814447913881127862/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } }], "streamable": [{ "name": "regular video",