From 009f4acbb3080f2b8c6991967f7a86967b2882c9 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 17 Apr 2024 19:57:36 +0200 Subject: [PATCH 01/39] 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 02/39] 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 03/39] 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 04/39] 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 zcmb7Ic|4R|{~lv76vmo(Fhlk&vP5Bw!C145eJdeZ5-~klvdu^(TV$QcK1lX8BbkaU zWhYyaHOu3%Z~5J$-}~45*Za?W&gY)*a-DOZb6wZDADf!!F*ETpfj}T;eGJ+h1frn; zV;3kr@Z0N?X9EJkX!Oxq*uR}tiNR%@L;T@kM{XRAg}=?Prr3}u`0McSN*&Cr3@e%f zGgxVzrJOEhCQ69p6~&#BDWnT~UXt>t&Wgq>3h7One%*UE^n+7$&wFr^>0` z^vO%X?l|1~+SFwFrRr%>d>1i&bFX*ocxU&@t!RNyJ4+)HkAp@zV{%r#-R#lae2fJd5i7@C+(XH}FUn2?E0~tPxF&l{QU-$Dy*G7f?oD(79sN z5=kaGWRx@}bv{p z7WlNqM(tVZI_Ez@;!#^ghHNe+^K8TjU`Z4H1_!da>_zk9)Aq*9b7|$?*via)bjq5S z?N+aY0z`rLc-ordB>r-&vg)wunMrU<2KOuuGst-aa=`Gb>d3BF^OA zH$zu-^HUg#V0$LxHut-m@tV>t!Ggl}^<;@ChHMd)FAa`Up}Q?fqy61^+?xai;Ef(K zUNDa_z=hdwJ}Gn9cQAWzQRF1ru2(F+G2ax6YN5OPgD~s*vvHv#>&ip>)SYuuHI4fV z{R^WSTqE|r0*R;`1po+w5iLsjZoEG^hRxFm@ofo=^A6s8of}oC{B0uaWTI)3$<&vI zB7vq0+?dhCZbcWX#6RI}@1(x-5_kdzq0#J5kPe+8$qJ)u+X}IEwNg$71}Z&i^F;ST zmRiu@^Z}QV%<^EK+SC-;zMQ-qv_6r8G@>aH_c_8peS1&PrUEk+7FKlhu&sr>RbWe2 zvz=zJDRBy1`F+UU;I?Ev8n~QRV!b{6w)N}Ex}+FN5Z7y3?+mRh9i#X`k?(Ne$DFx# z+AAaL_@pJ%6su6LaeFR3wfS%f`6Y}#qgVemvA@j+T)tL){a10W%FDVrH3;i=`$4gq z0izi|t8UEkVfGqFF1vk2$k1@bbmq3F?J};nfYY(o(OQof6ncw2X zN)GRFIG(km1p?2(F!`_{SqqW3BhKUEGBRh*9FXv;xD}86v6R-bC0Pp#mgR+tn17Zu z-)!YJet&Ks5KyRGjFm)JKiAq{zG;_Hu&gwmQ*JRb5j1}G+SUg=k5%0R88#PHUzJ{9 z?|X~FvSCCW%5U0|lWEyO-W#HuUSlo6@!q2qosQSAt$gw>X~n^uL#1xNQMT3YK0`;f zkZy2|_j^cA?rTTEwIt#d%`Khh2M2aceX&-7`)%UNWmBF69xG*3S&*k60s_BI-S1|V z0omAlALjhatNAwnX=lCfTWuQlLN3^xo|#wtl5e_GEoXafg}*R*sP6l>%mYVG)Jn+| zP%`-fTP|V!P6mVqibccwfTu+_KgE&kp0vead;{c#DZ|4uIwR2b;eWfw&%Ym@?N(|l zABLrb4;FH_3>S@D=^|PCl=SU#y~tFsEQ<=PiXI(0`|~nz;xB+n+_}6lYnZ-?*QB3$ zxQ7|2!RCwG;S3{d-Tdioj)iRjWs;hIpJbECqp|}obEzfu-=JpO@}urUvcx74lu`fq z%Q_SYd;?-GhnD&9i#Yk5L(IsXdoUsV{0AWlZ)AZuz#QCQW(38iLm63M5GiR8@`I4t z$XZGBF;f-=UxmP2zUehYAD_{HBD+j2weU9DUpeV$pcE7c@iCHUK&2|=J7qAmua51N z`YC(D&d}g_&fp{o0B3<{#-VWZxGsK&%9i0sX`tR8f&mVUw{SSjhmY+WmFz*_N*FSx z@_LaubquR`213>NpgmYm<>P;7wzW7`-MQcTD=#R}pvY{z!H>i3gdF-RCG4<6IM0)F z`xFKUf`=yRQ;`F$?g9V{X-@kfK?nr~sx|=6ZNcc6ZpdjwIKXTy|DPBGOQ$`ZC5R?I zd`M#84J&_ARJLG3+#5B@{Z@Kf=%1p+@-$wAV%NP+ay;9OXAD_0r&Zxu;kBrV))R}R z#l0lIsMGabB#@NU@G6yn7 zIiAJmG(VyP3a50KTCkl)jv9dw->$0FFav81f(QvGFd^auPdHS=wpd+9E2;}fBz=~@ zF2ggGeG;vQ$&#hknu6zzBqOOnB7U-CYG8+3OERp|S#rz1B5hMG@rgaJPZAK?tzxUI z&5o*T--cvWj9Wh(XP*0mD~E@AX!QS)`y!ocr5FBGz)mP$os51V8`{6W&z6pBuT=vVk=G$eoR%F47MMGQes_u8=5->hCMY)C6-^h`o*j+FZ>k# z_^+<0T<+2SueoxX0TDiE8p=WPlbrQZ`-;@w4YxwxYOx97TTUn5J3l8FYKyl<=wzxa z|J^R{auB}qrt|aHk!Xo4bIWt7hSSI1%1$=?}kw50SjY_;!x z_r5z8hR6scZ?&(SW43rZpPIwIO*c+zb*1|?;#}PQ&|^uLzh&5SRGb0q#|CJfCBEUC zt(?QJiCdT5yTx|}(6t&oAtXgo(>b&3`;{CbaXcC&2E%Qfw>5Y_>$&VtHws>FWVKMS z>*w>0jcL1Y;(;$7F8GW|59-tHpwF(j*`|DCs28Kw#JttM_g!wqtGw5uYUZEqq0;f# zvQmn3r`ruPTazHC!)?{LtF1T?HsAfVpV1x|5w%@A^~ZWHk_WToL_9Ioe5kE{*ES60 zHf~Qo>`JCO!qbn-py4ia-}Fi9L(YEdOI-GfZU>I(NsUO8^1YsV?U!GU^y?tyo3#VU z*JDxAFHmwK*YP$R`0rq7KqgX%VDnpcApH*7rbl)l(sx(aPM&X-OV`X?`rwj9besR% z4oR`R>*p=?VmRD}%T^Vx710{jYQ-v1kR>W<4}Ynly7i*=x62lVD7JEUaKjGO&UQ8V zp||@cQt62bN_hd3^%cn^ZtTS-sr2_NLT+{@p(*H?YNCxwE1zn%OrpSw$i(n6{>CHKVh*jZG*FqcbIzn7a%GT zmzOMC;?5a$xpiGsAurYqdNNlYK>Lm&O)(eL5J8#AS-Lf6+1rzg+Khb>=F`ov@Lg(b+3=Zp2`-#*26>i^b2)c5HUMn-wJ@(DGdIg4^@x)H0Ig9 z2Ft3FSBw!o6srrbL}4ef<_>$`Lw3&>W{2Y59p5kAG+lqyt4&Thio2ZK8Ctls#rNRF zZMT73?pu89EVkoiI?TytPmr?y1oZ(3*o^~Fi9orbj2Vwx^`s{KRuZY`Ues@`UH6dM z_Q8-K<1y^zN+$3J-{$MgrA%3tj_jk*=(DA{9Xihp;an*1i67OZ1xNLccZGHR+*CCq zKwv`E;8Q_>e+Mjp5$%6zZWUzZ8EkirdskQ6>5ukCK%q8Ll;cCql(;xKVI=bON(o5p zQ(+2Kkgx588YOo~pBj(|4Ah1XQ8FbZA#ijQ9uP7V!6Tf`f~DTOa{L%=WGEl`xlb># zAg~!AtY#cVfYYHmkR-r?_}h4Jo=P3s7=+L@&N?`i@plG*-T8dumDwp6m=a)jl>C%X{p0rvzn_#c{`FbI;_Bj4)>+XLSO 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 05/39] 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 06/39] 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 07/39] 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 08/39] 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 09/39] 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 10/39] 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 11/39] 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 12/39] 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 13/39] 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 14/39] 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 15/39] 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 16/39] 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 17/39] 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 18/39] 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 19/39] 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 20/39] 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 21/39] 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 22/39] 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 23/39] 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 24/39] 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 25/39] 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 26/39] 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 27/39] 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 28/39] 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 29/39] 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 30/39] 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 31/39] 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 32/39] 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 33/39] 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 34/39] 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 35/39] 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 36/39] 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) {