From 6465ac8d6ff8a4464b512236a40e03d049f29304 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 26 Feb 2023 22:49:25 +0600 Subject: [PATCH] 5.1 closes #62, #66, #75 --- README.md | 30 ++-- docs/API.md | 21 +-- package.json | 6 +- src/cobalt.js | 16 +-- src/config.json | 7 +- src/front/cobalt.js | 37 ++--- src/front/updateBanners/happymeowth.webp | Bin 0 -> 95404 bytes src/localization/languages/en.json | 31 ++-- src/localization/languages/ru.json | 31 ++-- src/modules/changelog/changelog.json | 27 ++-- src/modules/config.js | 1 - src/modules/pageRender/page.js | 84 +++++++---- src/modules/processing/match.js | 43 ++---- src/modules/processing/services/vimeo.js | 15 +- src/modules/processing/services/vk.js | 69 ++++----- src/modules/processing/services/youtube.js | 158 +++++++++++---------- src/modules/processing/servicesConfig.json | 38 ----- src/modules/stream/selectQuality.js | 29 ---- src/modules/stream/types.js | 32 +++-- src/modules/sub/utils.js | 18 ++- src/test/tests.json | 120 +++++++--------- 21 files changed, 388 insertions(+), 425 deletions(-) create mode 100644 src/front/updateBanners/happymeowth.webp delete mode 100644 src/modules/stream/selectQuality.js diff --git a/README.md b/README.md index ad8d689..c49adc4 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,22 @@ Live: [co.wukko.me](https://co.wukko.me/) ## What's cobalt? cobalt is a social media downloader with zero bullshit. It's friendly, efficient, and doesn't bother you with shock ads or privacy invasion "consent" popups. -It tries to preserve original media quality, and in most instances you get best downloads possible (you can set your preferences in settings). +It tries to preserve original media quality, and in most cases you get best quality possible (you can set your preferences in settings). ## Supported services -| Service | Video + Audio | Only audio | Additional features | -| -------- | :---: | :---: | :----- | -| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. | -| Twitter Spaces | ❌️ | ✅ | Audio metadata. | -| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. | -| YouTube Music | ❌ | ✅ | Audio metadata. | -| Reddit | ✅ | ✅ | | -| TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermark. | -| SoundCloud | ❌ | ✅ | Audio metadata, downloads from private links | -| bilibili.com | ✅ | ✅ | | -| Tumblr | ✅ | ✅ | | -| Vimeo | ✅ | ❌️ | | -| VK Videos & Clips | ✅ | ❌️ | | +| Service | Video + Audio | Only audio | Additional features | +| -------- | :---: | :---: | :----- | +| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. | +| Twitter Spaces | ❌️ | ✅ | Audio metadata. | +| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. | +| YouTube Music | ❌ | ✅ | Audio metadata. | +| Reddit | ✅ | ✅ | GIFs and videos. | +| TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermark. | +| SoundCloud | ❌ | ✅ | Audio metadata, downloads from private links. | +| bilibili.com | ✅ | ✅ | | +| Tumblr | ✅ | ✅ | | +| Vimeo | ✅ | ❌️ | | +| VK Videos & Clips | ✅ | ❌️ | | ## cobalt API cobalt has an open API that you can use for free. It's pretty straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself. @@ -64,7 +64,7 @@ You might find cobalt's source code a bit messy, but I do my best to improve it - node-cache - url-pattern - xml-js -- better-ytdl-core +- youtubei.js Setup script installs all needed `npm` dependencies, but you have to install `Node.js` and `git` yourself. diff --git a/docs/API.md b/docs/API.md index c42903a..3c3935a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -7,16 +7,17 @@ Request Body Type: ``application/json``
Response Body Type: ``application/json`` ### Request Body Variables -| key | type | variables | default | description | -|:----------------|:--------|:----------------------------------|:-----------|:----------------------------------------------------------------------| -| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. | -| vFormat | string | ``mp4 / webm`` | ``mp4`` | Applies only to YouTube downloads. ``mp4`` is recommended for phones. | -| vQuality | string | ``los / low / mid / hig / max`` | ``hig`` | ``mid`` quality is recommended for phones. | -| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | | -| isAudioOnly | boolean | ``true / false`` | ``false`` | | -| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. | -| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. | -| isAudioMuted | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. | +| key | type | variables | default | description | +|:----------------|:--------|:----------------------------------|:----------|:-------------------------------------------------------------------------------| +| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. | +| vCodec | string | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. | +| vQuality | string | ``los / low / mid / hig / max`` | ``720`` | ``720`` quality is recommended for phones. | +| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | | +| isAudioOnly | boolean | ``true / false`` | ``false`` | | +| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. | +| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. | +| isAudioMuted | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. | +| dubLang | boolean | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. | ### Response Body Variables | key | type | variables | diff --git a/package.json b/package.json index 791b8a4..d151875 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "5.0", + "version": "5.1", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", @@ -23,7 +23,6 @@ }, "homepage": "https://github.com/wukko/cobalt#readme", "dependencies": { - "better-ytdl-core": "^1.0.1", "cors": "^2.8.5", "dotenv": "^16.0.1", "esbuild": "^0.14.51", @@ -33,6 +32,7 @@ "got": "^12.1.0", "node-cache": "^5.1.2", "url-pattern": "1.0.3", - "xml-js": "^1.6.11" + "xml-js": "^1.6.11", + "youtubei.js": "^3.0.0" } } diff --git a/src/cobalt.js b/src/cobalt.js index f21835a..b5fde3a 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -80,24 +80,26 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && app.post('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), async (req, res) => { try { let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt); + let lang = languageCode(req); switch (req.params.type) { case 'json': try { let request = req.body; + request.dubLang = request.dubLang ? lang : false; let chck = checkJSONPost(request); if (request.url && chck) { chck["ip"] = ip; - let j = await getJSON(chck["url"], languageCode(req), chck) + let j = await getJSON(chck["url"], lang, chck) res.status(j.status).json(j.body); } else if (request.url && !chck) { - let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorCouldntFetch') }); + let j = apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') }); res.status(j.status).json(j.body); } else { - let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorNoLink') }) + let j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') }) res.status(j.status).json(j.body); } } catch (e) { - res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') }) + res.status(500).json({ 'status': 'error', 'text': loc(lang, 'ErrorCantProcess') }) } break; default: @@ -114,12 +116,6 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && try { let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt); switch (req.params.type) { - case 'json': - res.status(405).json({ - 'status': 'error', - 'text': 'GET method for this endpoint has been deprecated. see https://github.com/wukko/cobalt/blob/current/docs/API.md for up-to-date API documentation.' - }); - break; case 'stream': if (req.query.p) { res.status(200).json({ "status": "continue" }); diff --git a/src/config.json b/src/config.json index fe59d60..34f6ec9 100644 --- a/src/config.json +++ b/src/config.json @@ -28,11 +28,6 @@ "boosty": "https://boosty.to/wukko" } }, - "quality": { - "hig": "1440", - "mid": "720", - "low": "480" - }, "celebrations": { "01-01": "🎄", "02-17": "😺", @@ -64,7 +59,7 @@ "webm": ["-c:v", "copy", "-c:a", "copy"], "mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"], "copy": ["-c:a", "copy"], - "audio": ["-vn", "-ar", "48000", "-ac", "2", "-b:a", "320k"], + "audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"], "m4a": ["-movflags", "frag_keyframe+empty_moov"] } } diff --git a/src/front/cobalt.js b/src/front/cobalt.js index dbbb950..0d96493 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,7 +1,7 @@ let ua = navigator.userAgent.toLowerCase(); let isIOS = ua.match("iphone os"); let isMobile = ua.match("android") || ua.match("iphone os"); -let version = 23; +let version = 24; let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); let notification = `
` @@ -9,13 +9,14 @@ let store = {} let switchers = { "theme": ["auto", "light", "dark"], - "vFormat": ["mp4", "webm"], - "vQuality": ["hig", "max", "mid", "low"], - "aFormat": ["mp3", "best", "ogg", "wav", "opus"] + "vCodec": ["h264", "av1", "vp9"], + "vQuality": ["1080", "max", "2160", "1440", "720", "480", "360"], + "aFormat": ["mp3", "best", "ogg", "wav", "opus"], + "dubLang": ["original", "auto"] } let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"]; let exceptions = { // used for mobile devices - "vQuality": "mid" + "vQuality": "720" } function eid(id) { @@ -216,17 +217,14 @@ function popup(type, action, text) { eid("popup-backdrop").style.visibility = vis(action); eid(`popup-${type}`).style.visibility = vis(action); } -function updateMP4Text() { - eid("vFormat-mp4").innerHTML = sGet("vQuality") === "mid" ? "mp4 (h264/av1)" : "mp4 (av1)"; -} function changeSwitcher(li, b) { if (b) { + if (!switchers[li].includes(b)) b = switchers[li][0]; sSet(li, b); for (let i in switchers[li]) { (switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`) } if (li === "theme") detectColorScheme(); - if (li === "vQuality") updateMP4Text(); } else { let pref = switchers[li][0]; if (isMobile && exceptions[li]) pref = exceptions[li]; @@ -295,7 +293,6 @@ function loadSettings() { for (let i in switchers) { changeSwitcher(i, sGet(i)) } - updateMP4Text(); } function changeButton(type, text) { switch (type) { @@ -336,16 +333,22 @@ async function download(url) { let req = { url: encodeURIComponent(url.split("&")[0].split('%')[0]), aFormat: sGet("aFormat").slice(0, 4), + dubLang: false + } + if (sGet("dubLang") === "auto") { + req.dubLang = true + } else if (sGet("dubLang") === "custom") { + req.dubLang = true } if (sGet("audioMode") === "true") { - req["isAudioOnly"] = true; - req["isNoTTWatermark"] = true; // video tiktok no watermark - if (sGet("fullTikTokAudio") === "true") req["isTTFullAudio"] = true; // audio tiktok full + req.isAudioOnly = true; + req.isNoTTWatermark = true; // video tiktok no watermark + if (sGet("fullTikTokAudio") === "true") req.isTTFullAudio = true; // audio tiktok full } else { - req["vQuality"] = sGet("vQuality").slice(0, 4); - if (sGet("muteAudio") === "true") req["isAudioMuted"] = true; - if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req["vFormat"] = sGet("vFormat").slice(0, 4); - if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req["isNoTTWatermark"] = true; + req.vQuality = sGet("vQuality").slice(0, 4); + if (sGet("muteAudio") === "true") req.isAudioMuted = true; + if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req.vCodec = sGet("vCodec").slice(0, 4); + if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req.isNoTTWatermark = true; } await fetch('/api/json', { method: "POST", body: JSON.stringify(req), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }).then(async (r) => { let j = await r.json(); diff --git a/src/front/updateBanners/happymeowth.webp b/src/front/updateBanners/happymeowth.webp new file mode 100644 index 0000000000000000000000000000000000000000..0c8f78b8b1071df8014e569ba71b23177ee3bf9c GIT binary patch literal 95404 zcmYJZ19T=$v^D(1#>BR5+qP}nb~3STXJXs7Z9AFR`scm(uJ7Ngd!4RYRlU0FRG&J% zcPUAVi50s50n|l>6x0+rG@tf*XjKkWd8n4q~G5q z*SR_Uj*eejv-5k`PJ~Q@2g*F?L)(Yi7*U3@;!C#bO7u9C;m)8}(@wc1B`QuPa0%tV z498khf*@!-N-d_HGogjlublQ`@PNOOGJ%zn6hE!yOdYUwyq ziL!0DTl^mI*IQ#Krd1~FjYd)UR_=cnndd`=@Y(p+Tsq5SO6&l_*swI2st7a*A2f9lvS%$UcT z({3`cS`yC!>ID;kMm;u&MJzmPp3gX#7TY?Jo^}2#rtz%^Wwmam~QE~oWTCZ^@C zZ-2qlLCID6&|~pgw0cOfk^FASh_TcWZs36$Jf;eIy!cq z$RC-%Dm>pujuzHKXxG+zxGk6&l|vy@OV!`3n(`f9f9J~`&6^`G*b|Bz!C@1@V+Oi z(iNIBD@Y15ms4%Fc9JWUS?&-5qRQ`#kJ-YCB$#y|Ruv9MS#Qz|^&fOTk>3B(?_Yr!V{pG}E!{d@O0HjDJ7I7^lhUa3w1*Kru)5Br_NgOsOaoONU)%i({3B&h(ne`=M zdmV-a2E`CIK_K1e*zK$K(+yTj=gKJfz2&{>n4E~)7kO%{fB(VpMdHi)w6Us5=>s*f zhz>bo4dE3DiEzE9_IVp@Q5`Bz(T={|F&^7RP1bIYcyetmKXZ58uosS(S%a}-y?(4;& z-Ks?e$xV$QOZl*1!`hfoUSPTSL=qZ5oKqUIz$6`5~ZK8II>$> zOzWy~)IfP0)Z3Lw`e6@T_GI}sjM=${Uj@%1i2ztcXs~V9UcUK?3t}3)R|0a^@ z0YA4=*TdS!#7}6jz%@q^YR{RHJ$mSNo#X6hjRP;-jr?hcC}{!V#y~585?H5w{;f_0TnVK@tdVb1{MXg}F2V(I9w4-A2|6k# z001JK`?h>J&Y;B@a#sxvVJgdVs1_OsCh`DPEj{fed_)=ZLb?{P1<`WGbAIoY3}a}` zY(8jvPq!UV8PVBaLnl-3)cr`(1GX$x>R;PMI7l6BXYqCx$2xN1 zM}4cqYnoR1ABur08UwzqWh5o-)i}&(Jik*A9aveBw$;({xZMQt1ftzTd^X`fZ?SMU4Ok?eV{j?^3_NTWl1TQStYAGOWB}gC4LRj zV2U!Lj*PkMExsa>1^kyd===>OF(V7jZwbZ0w{e0IH(dE;=hx+%@FZ*=yrVV|-;P1Z zioaC2HRHJz)?5UWqbWKxfSAt0=I!`R9e+eBcB~XAtoAZIJ)!%x9B2r-mpEs~Nsz=E z&IYYwfFDJ@bcKWGTv@d=y3*MRgfTGZ9oHsX@`l||+c2FxaX6jQC|Pt1M}me?D-=!% z=7)v8JaK)q^Uf!t3IUMJ0T>=5HqX6KUv!|Zi94rO@c@0?|J3^(FQKf)({HGRl9bU) zp8rA~Wy45++Ujf%rtue++1=Du3-Dlz@;`@>b9dv3LBB!@!FBiNL6{1fff)@} zAr9iuN&_(^iZQ-s`hFd01x5JjZeD;u#wSx|Cve#inqhd}o*__<^*45L|IX<@%AJkJ z*Eqa+DHw)e`4H5w0d<3syzkRYMrOc;8%gkG6$~QzEj9Hg=c~8G?wDaqGt#&7KFz*z zm<;`;5#E_VhKZzu@>z+#o=fmSGujqQ-#a!gCo0V#?|a)AX4 zFep}1A{5yv<$T{(1H{k;fZXy^{WU@=|7;xOLaHx%AaU|dMLp*aAgyoD`mFAO!M(f~ zgm?m3|48EZG3?@ZQ2-LtyiyO$#-G2#67smTnOsY zS{fn}g}Bfzy7j|zp50pc;c_?90NPeNcmn<&5!9r7<#NC``(%JF@;f)AmYmrZQXg=< z+h49rgjPJtrQJ*W=TFGJbPivvUkC(|HXt;eB-ceHt0n?-d!G zE&K|9?0c$!T=_Zrzd8sB--U1rgcpaI3+K*dO`=snpSD*RTW`$d zb^XIUSvpciUURh>k||;Vcex6$?+Z_-Q~vSshIcY|=x!;-{XTTb*6ljGKPhm8D11t)_VYN8V{JFi{gwc2u;V=(*_%Dnv1<0=pDR_MY|HevkZ!~S^A^H%N ze6MtZVW3QK``(24H6Y4DCg(w9uE|U8=Q*?YUsCD}+jd4>+A5V8cIT`tIjCcV=&iDv zVAabWe(9!q3GJ#P@{91fDOr%mR_m#(q>)WHkohP=w6^Wk`Njh68|xPSz$rwInAf@+!Tn*46l z_8TIKgGDv+tuB!W#a~xnvPr{Do+0G51`=`5H12?nkR|C@hBjBmIi!m z-;pFu#@;~!w!7ot{R|uuoPZK}GME7kv)lqrMc-Q)r-1xeHK zn*qpuM~>p9@fY|P6Hlt zaHdUZbEmY8mx{?&rJ*@%0xbr(E%*J0A0?-K@5%1*5G3~*)ggWbtx;N$_~7PH8N-dX55k=%4Bc5yFd}(Rxto`IY=i4Z3Cpv>(;z4; z7hZsn7)1!Q8ZRC5hp8Ed>}sQh(;BcM|ifPa=MRzOCO(P*TMBh@!P#~EU^jT<>;5t9(9 z(s)tk!OMaG>Izkmi+jK2Oz-p3U-oHJdXPQ83gMy|R!IhVJ?w*cY$bVvhJ&-&2}-kX z9z1}jVMx3@T*P-VMAJsms{uNCNS+Z#t#`0w!71J(>+%br0^tqhT0Q`#Unsb{AUoo% z^a}sSq2&8ibr5f1Y;}ZoT}&gE#>xRSTvwN{yxpihuVnL3fE(wiGcx5doXYPBHoujuHH9PaH@5wjw7>f|e7yrHuPot=NnW0pu{EENzXu^}vn4?RPK4fBX^Xzh_A<{{Q(Su{>#D zS=x*MAc9o2YtQAZmf+8f@7$Df`??#pyaoQt_byFgzw2Yp&&yAB zAGr~K-LKUw-;3|TYPR@J#bIAyePzrK8v06d_8F9?-FHv6A9NppZ11IovC)MdEoDO5 zjohh>(}VG4@+O;{fu9VD`+Wxk;ZHW!SC!!u(o2vDY76T3#wfv-r0F9TuIl$2v#1s7 zcjTi5eN3#Bnr!iwDkvw*irN3ca6G!u3HvQ0^w?W)!jXm-ldGUT?MUSRz*LBH35A)p z?DQc~J#HO`5n}KI>>M#e&XIlapMf?}rH7Fy^W0T=@ymF{owdk_7J+iaQQB&4 zZYg5qylT=3MU3n(OFE_Sp;LtndkE9|DpgFy#3-Qzdv11N`jo_DifB2%rZh?sBj=~$ zb}6D3k^Oq-&P$s^5wd*)Z;)8W&n+c(1DMYLhb(Mp9@7N(T@%y0WiCT!9&cGP6l$BFLu#!l`34V>n8!4|Fk3JP~H(0s9$j%v>H!&%V{ zt#;5lXxeL&`OOA#fYzrc^s*A(#Z^vjB(LNC?X!T%#8%w@d4~OnkV}O1(UbsE(>M4| zw$pico&UF?j|01^1g~KTnqM>Z-iNj#Qfftcjw^Dp+Ie*<9cy&dnkIM@`akD0jYr51 zaQPG5%~^HtxjW$O>FZl_aa&1~z~Ez@^ttUiuQ3r|lHw6!2#bh{Y>$ZpTi%c$%HFYF zw*xB*glL$>RbAnmhLcsJCb;r2i4u>h#>Im{1gya)hw_{&d^7K8H_(-fL+qD>^DrH; z#&}_r*f2b5JFivE{I6T=%})-`-zdhCQeJ@Y#u6Cc7L_~C2J`@w5nqiF?hiQJzdXy! z0_L(ht83$*%k~^htTe9=fIVg_lvBf+dA!$HRiHlv zm}1z3CD$1E$#x0Be7q56VlvL=<=h+SH>3P~XDL8bmZE4VWuM2roEDUNHqeo3n44lqju#iv?S9+3x(u>+X#|Sds>3G%UUmTw z3Pkdn4D50D$P6IkANU}+MMiFb`mC&ycRTJ^9k2VS&srcT?%Br1D(L7ltR1Y5drmCS zKFa!eecI?faxA>Y_eW6<7n0fokvs50bo#w|8P0PRJ15WX>lzOuzTW=#=243t9A(c; zTY?lem&x{?a|uQLqNSj6EZWSo513r2C6Z}%`AnM|Bv5u^T^^ksGzjR0;I}@4EIUVC_4^{MyNeBSAYbE=aPZk<6~X)Nja zVc)haY#XDu(Kz5qL54=QvEI;=czmuQY4-2qk)ErBm zds>S?Ty%PPTm_Innv+$H=t89dHUlY8Bo_orYwo_RI(xVAW_d};M>Ed~G-nBc8%JYRaX2ckIeR)Op6sisWM;p- zQ5W$fV?+5l0M2Z*pWg+fL*c^73?GU+S_zpn+m6*0I+gYe%Z1xv zJTVhrOE^j#jn5QQ>@s-X`J|U*`2n|No)R}iX1YJ-M4Wi_+rv+jbQR6l1I5>=4%*+5 zeZXHV*c<;dd{B00rNLz$z`K zZ`WOrpr08mkE*FFDC4x0T4xEtiX2<7kEyh7NjT*Kx!0{0FVnnv;jSXqg*gKL19fq; zz{n`a<1iMilt3V!RMAipwGq)|n39LxBN&_gPhhAYl``x7i}=`qJ8R`4Xg>5so_x(U zD~KznLZHqe5ID*8%dY?uVGr2f{V(`=Pb=KY{0Sasm@W4;rjaD@Bs0dB68%R9Dj9`C zKTbb4WPMoZdx8pj&qx4skbC&&N3hbLPPzLf1nSBBAjBTaS#xuQ*>GU^1``o_`Sjc) zcZZs2aKt`I$Ud>qYBt|4zO?s#;EVo07%9YkOxDu6p`xAURIptm>4W$N{{{s7uUx5l zOj?G9Wv1&FEE}h$(%~H6b@H@z0j$!Bsd{5Fegx<)C{`C3P z3GxRcF-563HKke#wirqnAQ*ZG?vh21JS)jT0SNN3 zAwA}c%GMon3Mg=AE`NZ=GG>@n`hzG$KvJ-MVB!TTPz@eLwaC9>)u@g&It!x(*OQDC z`H%1@w=3D)wDU+SPkDn z0J151!2F^^;wcUj&_- zQQIFXHwhrSC+>AZkY8S|UdWz-bLaLr{lF3F!*Hetmak)x0gS~s%UCcDbca+oX z1~2WyD&u{nQin#Z0cC5jT2y(h-M%d(SI!WPp7i0xkE2(A!4X*?sV+oc!sws?+b%eW|41Mp zWv|pMkiDH$qGYhgHC0{xUSDSF%Oy3@T5tW|jI!>V`$au*7wMsf7Orm(0lzFfJL5dR zbI|9kl`T>VE-6pt)ih0%3Kg>8bMF#c>BK2m{@CYcSiTlSM<>@dJeI=gubJ)M-fhio z6>4IK$H6(M%3z^CxcwsHRK$)!(15|*!{M*>{YMHUwyG z{9rypG+}XfNhi2&#!{>W1csS4>ogImKC9I?5jY;U(}ZyqE^f@S+{REy@<%LN#}fz% zab8`Zc)_wxm_zVPvdXt-I4XL_DW?wToZJOzc|?0$N)PWQPkp?dZZ-~b-{pa)^hS;L z2Uq6go%-D}k#&aY;!$n?H;)wd2867vzuD9d*!&nh9Mm#KtB6H1vhOo%44N&{9du;*VvoDGE>j?~Gt6=804*%{BRpFR9gDox{lxFB8Geb&@f zZPw^v$T7oNmG9s=W0DG#GE_q3AG{0P*Xr%`VT|c6`1mC8!`P{~wK-l*BYiGvObDif zJWySwH|cv^$ZOBrrkKgkaWoedaGH2F5pE*Tk8Fsr5(H4j3(4Hwo>Ll-EX)jnwHD!% zlwNS1^Vl7GWSWI$(lGLVcv34c#|0hJ51c6}2WKo{-hJ;)Xewb+`0to-ha46|K#4gk`5%5x#I$ho^ZWhpk*k8Xl-M}GU`WER|6rk8A_zDDtXiKGHtVmcB%T2EeVyN2qdG5Hj2prdU z>A_t_dsJYI*q1)J1jP32CqHjhY>{KN_f%4s?8`bM^AL}a^*gjIR)SMMAIzds7u%Xt z++n(%U*DSH*$*8f7ixw0))zoy*y=X@QhC=agor*Qha0;ZJyqF4cL>TVN%`;h3*1}41bmc)J}v7JvX}z1 z`a0N(OK45mfO=F&VD%qcew$inz8czZvS@^*f`$- zz{K?DL%uaPd!xCZbb2EJRFTiTgd7mB8a)4-4{E=xje)E50DyI zI-FU#ghmCRB}6Qnih4hBt&sp)nHGZNJ*jDQs<|@&1`Lre27yhUm6+m}Hj2J%!9S{7 zSkqr?yJf=Yd>MD#mE7s+;mp_szD$!gmV$aGUml20OWzM$KtzV+*xZzp zC9uPbf7;n9n%yVp<(=C*p#SB@-e@m6-LHG7j!$~wesgQL#{jylX-3uovoE{#!f=oH zA<9;*cEZLk@psWd~(iv^+ z50EW0xB+{a96ze{^Wgz$HvcSMo5v#usVuX#M3x&ypZ9?o^UGn{Khi@>|I8hc{E3OE z6>gT4-LP|{tk+)D%UCOd+q5m$8<%*#VhFRqhsDO3)+ChjRz)>H+FoivX+~fy1TLF1 z%OHK59LYFJ^0V-Os;{qFgNp4NuP0g|c4%FFDP037_Fa8hmHJFDr)uJ`JG^ps`(Lw}@IPVAfeiGc?sRYe>Z>MBrGZY3K!R9OO}? zI<7EBpb*zMWvk>zKQn@WDE1y=^E>ytTg}mzg%u>5fzV`CMDLFUgcWm9C##w&g2!yP zFsFu;Iiw4O&vxQ()$8N@B{%P2|8ihD1m`)xCWr39Hv2^RwUUt zX4S*}UCvbZ>njggF8es5an>6R5qeJqN909@d$#sO6;1pa{@54h&Xx)36#hGS zK<^Kt#_q2R7&z}j=3?{PteUagDQmJ+kh}K1$mgP07J6UbSucqZ)0`hB8p4_#Bja_=!P2tg{#l%tSI4U-lc);5qU%^1!A_|=ey;C(XVJ8 zEKqW^Ks`IBo<3ocI6ilegV%nu9HQ}0y~sJVabU$)VgWU2$gs_6pqbKrT)pq_Sh)G4lh-l_#XqilQ26(tDUk#R>HN-$?p%=_)wZ_=IW12|rsq z<<&hT{F^rTxBQl1^^@$2=sV=;yr`ccTtc6IFB_0(i_(NV_2F`s)hudfz2Zt73Umc1 z(KzXnsm{(-dB7dGa;h)Om)O;Lc%SGRVtsolSN#lY(dYwF20jszA3y(e3^v77`{qHA z$=5Xihtr95axhngaczX=s^b+{f1_aT_>+Dw_HnILmO@j##kJYE_M`w3^byp=5ZW6g zBNA#G)r$!&g>8tv&e~b+%oCx_jTS1?=}qN-n0`?8|D9a$t+!xzmfe&S}}Nw7-vRkiJ70i>mtaI4}#H#96~U$T^~_1j?S zzbGnwVx_6UU}#(Q(wuA|3n4(rE&2&9RYMM$k-6vVNXb-gf{hmHF=g~Ji~le?F<7UB zzZimD-qqi8r6pg}@UxY5MqCXqqqg0*38(J=TTI=X_S#zSf)zPtQiKW-dNr z@*C-8A_RB)1s44aa!}3cq@JJa0!K>tL-wWBMOSy&Q7ck& zm{mD^HldKKB@r;WxE8A~ho|GyWS* zoS%s-%hXd3jegoJP^O-O{j4ChF-%sy;D6$n*Zqd<9cE+qce~bo**_CEGAuaJF?^;T z4r5Y3NQkkq+5_0fqgO%dz0ts6D#a>?N5h}Q^ai{-5vaLTWOL#AZoDFEg{hi zZMD>UgU?UklAqxSTt!Lm#<0QsnOHVAfw0x)F1nw8VRxXwQC+?d13!=A)I(%}?7tqz zW3C-_h#kAP3m8bjW#?MSY!FtR72Dy@Y(aRE5BB9qOkV zn^w^p^iH7^au4zCa1*4QdJ`fqhTu@awOgx5rH~9ST;Jh^qzz`Bdr6kkj7ROF;xslA z{9CWXCCkI!hs1<9870ihvm@u`XrSpJE?Iq-9uMG!S9V4tOMKd;q`Jh^-OX0us4T<4 zH>(9D({e?Gr!RtneRX~PB;zck2LY6?b6I6Nz(XXlt1j)91-5~}hV@-)xP(=)z2S1W zifJfn7PFbAMvr(n^o-Yxh-Ld@io&un0>w;$C^fa>i^7rt>0+{`$JF%gq?&F>--~RZ zP)e_ZRvuqSaUC;Ah&}uWt#{6HuZKlz0(qu*PqYsw9JhAtm$&g5!gE}hgA_@`1_a$a zt(KRem>Vo(j2ckgAdApdfnRLr7!{vi&o+r`JQ*K(R&q@YC_GPc(Tt&kd9t&aY+FYi;y)*KVYNX5G;k#=9xW{bSG?2ZJ?>}YiA;QvMC4_TJz-Qhe)JEvZNsf%K*F*n&;Df#%z#KVxdy?Q9HAx`{}@whK6+BKFuY!mLF{CD)tp z%eZ_jQ*+&u)BBdlSpwqN4j3zsf{uhn&Pr`pmNw=TZ&BaNnp0kM<3}A?ji#VqsAmhC z%pvb3Lx%Ep4l@;xYlurgGCjw?O=3M*h>~!ioO-dHWClZ;$o==yD$aYW`1` zb0n6GOvcOpSvg^J>Ajcu^_2%m!@t!2Ci+S5=T2mgtKx|GGTv$>)Yrm zd?xc~##P(06K~MQ{k3f9q5^>??w0C~8YHYhz^1YXocIv50g8h^KjHW6=kNY@Ck+@s#7=p5nIFYhz+yHwm= z%G~=?@8cD$%Wr-Q`3HYa|EPI%7Ih!{h4wnYKlC{C4S99Xu|MApNTe|an`XI;X-e|w zAC+@B4x`cxp);Lwx8~3IY>>HQP_)(nl`u1dRoT1PZw)1$bare$&+a*y{BZ(Xo7492 zS?OpMx&8CnANI^LFF_8bawRCWh^0q6EfSe&EAB zWXjEpPG}ac&aQ50@lpUdlLt&2oX)yUsj)X)a6mJSwwm8iMlI^~F5{iz#VEgSp_Yq| zPYI>AkfZAO^?K1~G0;4{2v8>H;Y5v5Y}u6zjj}LaaH;>MSGn?wy>5COyuD}Xd8D!= zpfkg2D=Lyz`nxl=g&-j+K9QM zd?Yiw$W*z4W`|zo&OSMQN#0}%^w0{j`R`f_ zOboQ&Gat$QpA#Fy(r5p85__hxynV@hOY5mhI{vJuXO*B#ysFC<>Amr~^mjEaD`_{l zo$?6SN!EOV?Ol*8=0LnLRdKzyTJ<-1mc zB{GH!82Y_@=-Y_^{0hjn50uP&{knuxbDP6iIGVF%IR%^=kH|EhNT6>dh;>b7tBpra zD_h=m0xHbu*dz0$BDx^E1J190ob4aXAa1ss9}%=e^{aH_5{$eBzg(HBT=2PhOPZIN z@c4bs2>Sy<1Hlh@;qR!+VmBkL(#KxA_UzFj=j@wRlSnVU8!I&&^RV}%ejqh%=28;; z@#fU8a@m`WIiS%^4>?Q~`Nr_G;3o_#9c@T!ifIywV@*UDKPcx=6&~(6iEr=p*gsJG zlAsPa{b8hkmE67W*(LHw<%BR_RElT?4jD+>#pY+<35?TP0DIQ~Im=T5J}`o0t7f_V z1uO24tClj$G)w3)e@>LT;kF!CBStvOk~8Li*t5tB z$@IPG8!sLF$AB;+%^-TKnw$x*{z|`AKoc9Y5=~K;5V#h76C|yN7)waoo3}1)veAN6 z9U7ii!4cX_B$p!^jT`Rx%gRR4_?}Vkzr!)fIj!C|L9bJS=fmzSYI=mvgh`0a0_nBW ze+iJ9`CL)Dk3m(uI!jUW8Qq$uENe^H z^y~!0>xMnAEA+8v(V zMNtPiri64|pLM?tHrd>mc+WQ#&7Sl^k8~~xyIN2s-c%iBHow5Ix|JR;1gF0Aaf zv0Id(E2%V0AuO>$3Pn3txxN@E1ZTr#>Y#e~4wM+JE|YDG+|m(5Z+wVaqwZz!LFRR? zLWN!zxJ%4k+RwE^kp%l$B~RgB0k+ADoMj6*;=*VL7%vYN4}ur+M;6Y-KcLFR62IEP z4rrM%K}qKr!Io{!!4N<7**r=uM74Ygx3S5it{*NWEwlLok-E7VYW%2ynL?$EWOX{JV!beip)h4FE_lV&DtG^rI%IND)^s zF-t}P-EjD4aR$1#%i4R>Iucu)?izril{J-d|vGOUb#$H#!nb zT`-bTR({<#Xjd=$h@Uh5yc2TeV5QEWMy157Iaki{40^9XFhYMi^PQgsPe}OqTU5W* zT!-U?;cXCUq9heJRX}?Vb&Tc?D^k={V>jdIU9y0sdr&+cbS_G*YU9TcRv|nvNzdP8 zB>0znjR4uCG}F~&GL)|$a$tI~3DD=FfmU2bnGf~*K6?K$ZqeY+xMtbw zurKheu&%e&!SA>eaw1ML!~1&H#|}S58|C>|@*M1%b013zV0cWQOnscSc)pk|ji=e} zr7f1XL4-3+gjTV;$PdEJ95LOB*QVl3+nT|NcGF~-4|4i3*?jZJi2|keT zf`47;a7`ZV3@Y~k3BhQaW>ySDvixmUQJ-l^b{pww!g6X*Hdl8i5=5E&0!tFxlj4vO zxLCMl7sHFIIgQoR*Tw>MNFi)}&U9BpNOx2EYG67a`T~y319t~UZuW7_Ndr{-t`fnc z&aj(fQ5fOdvSpu@y7ZV<()$u4Z(b{C3;ICq${;Nb$u?Wtyze~NB;)FRnEO7U`P*O* zT0?X*BCF8EKhw zIn9@uB{@C*cD`8QaA|QVn&hbsh*AQ4K_QciVKZGY8>xT0U0AiW%Xi%vB!tyNI%jy~vu%c5%dNtP_9w0XMQ$GKr)oY_% zPpdpjkp2P$`vy;*-)ngfQDJHOOS3;?rO+Z`_3ib$C-rB=4F?~xP*tiRzfTPKwaBizATa)ud=J#=GZ1}F<2erGJLL|3A(nd*;wz>=S zsO>w9i99r8#R6Zd&3QbPLTPA&zL~3mCqKUl=x?~otM&IgJ1pzR3Y7B8L2XhAhSDcaJ&6xWG31{Sb z>Z>&3?;*#Vm7@Je(`S1iZiJ4*tUHLI#=q3Y8f3)>Q#lr|Ph7uhVZVj~FY1#NI-O-h z1M$kum{Q`Vd0=)AB|eG73^OHgLpe)DQ=3?U3ca-zv(*L5`}l5=G4%4qMojP|OhkXc zfV8kB!zVYqjF$=5D)>&mXWT%+$6yC+nTYPXCpxl+KobGLys%>Ca>co{)@+c*ZmnvN z!LJa%l&`Qr#?C{{iyKBswa*yIGpD+FyuDZcZFEkxq}SWvD0S4Zy@^@Nhs?tB zj;~G3xsf_>>9rdx;R}a+xYO-T*;(q8SU#hzG@Js2BZ-dNq2g@#u{W$966(|=Pl=OR zd4;wYcW-;*>QDk3!bXYLz6Qsek2BnhKjr2#?~lY@WS69#s2V9voSTn**~RWstCVYv zuFWia`lYYuCZ+w$1=#ioF&`GG1z?^d6!n-Do#BgUo6iB^eaU74oIc7rjTS^>q0$+K zN)aif=+#ikOzXD~k${i))PO*8eH(uf0|A;!NY%rNgbK6jQJ>?h)OsIC6W=KF8<{#P zpRpGu`H^WeScOqB`NCX8BX(!F&GO}CcYy6=iYHDbM~}CuB6d7*q8@Fl?zC=FNXZDX z;Yli z3H}MwOYg_XfySgGm-ho+&;Y32uIZu4`6;%og5fy=Jszt$n3?k0?>JL?hMMjGQZr*x zUD2E-`@Y^rv*+b9Lk}(3gf4j!XKgbrzd?@Bmd8|rNL$OKqHgAw7b6;f!IQsMrz|K! zy)a0z!3p#&kJkgsFE#@bg_!L8F4<|*+xLzqp{7dPyiV8FkMEZhL*y#=Oe4wNb;5iu zT@=f9=r|a!ed$X(xvEtfJomJ(7aWhg%+Do#(-GWS50Tt*WRdv*UMSSd5X?b(P5@I~ zlkdEV}4lM1U?)~H!e#6yNh+2IpYk;FCh)gm`lX=( z-3^?Wf(^>Bw-H?@l=~;HR!f{d*ca|3_b$!5qual}Y6dMt16$i+)M%9{Qs3==o$~NW z%2PQF2w%Qx!@rGT90Ly#nDt@0C)1|ocsbd;JSsar)w!<2F3W^(L&$*DMUC!qaT*Ud z^FiE}a?r$O2?|V5%$P0k+pzWKNE@4J;)c&r+X{7fxi$aTf)O2VnT_5YG@K=i+gid> zz;()84~H)HPEV0e#q|hzr(@fceHIwc4)f+x8o-wYo=z@MIMc+zf2U%+R~rIeWhp20 z+{2x#2>D?PzB!OSS^L*|+=y6Jv&bpZet3t+B3lSCbUD*5%14k3wF&{?%Lz#8`(kgI&?;x` z#%#y3b@}*KVXOLEN(Bv1NPsDRyDKgajs0v}GK zt@`F!(TKA;M6AzLo>_K4{rDDKz!?PYj<3JbU3HDfnT5Phl*wVXmYD6M1kAU322VLj zDmQ3B_BEn${b?f*CCMLH;y>-8{VgQ0KrXXr3w#yp^DOZn(_yf5s%bKx z;ll;MkAcM_wY>_446!L9V+CHS{hqHr@}R(vt;E57B7`H7*O?Ynd>uAWKYdkP#@?Gk zYV$$S3dbdN<;v%-*zYG??g4GPoSv#^*Z+tQY3POc(9G^j_H?H$(K}&N_YgK?vDIcJ zQd2?tOE9Q)z@-DMv5vm_d|)1aQG*lL^0Gckaq%RS2Hmhw__G+L4RZbR>sD+h6IN*Y+M zerIeq=nxDQkLZ=+8&Sr(Rz1Ob_`wLr?+0dLHQCku5GTl>J8Kl8{Pu!%F>3!Q(nqFmC_=%3yU~%ya7q zGkx)t4-k$&DH@(B;!L{B6dnE-N(8s6!yJyTS`cIbmukp`l$BgR8@Y3dXfYpE*Xc)? zl`*nlY9mGtUk3<~#z+jl)O2VRoWsW!3lSh!9DTNAFGKH<0`Yo=Fj)e0Gao~t&Ct^r z5jurZx*Lt88~S^H%C5r3f?zTah|ouMaG&gwIZlT1uhn=v$R=& zTeB34x2St626!6@!RyM>@!$jPmxKa6W7m5u9zy|=dx+pZJT zsqfDPQ=MZ`o^h%j!}!|`Ko3Pz==7T76-IVy1gVLkGibrS+c$cOhgu7s%N8w%;(JW2m;GqrH_xfGY&$M-7P+yxOZZ-!0_Vbu=Z zrltN}kBq3Y#{LUFNJqjuL!c_H=&+vdlk~ zK39s(@pdSWC3 z@h*7gbZ7#S>9(^vS;oPh1#g*RLd`RRu>s&o->%d}IvV|Od#G|-5hjPKgrKJmSf!3X;Mh9IDyRtYjaS)+S|Z7?EWc9 z)#ey)8(we}q~YkzQz}JZENfY8m|>@C@Ry7)0P%O`vm*Tn>u_K)6DL@aw`s z{;>d}q`KP}ZF9F~TNsShluU9Na%Lw|cPsx^VUKP5cgmlWs9-)5hTOWsoMzcmgk1fcR!CM)sft&ho7wWTNG9 z&0G$*0LTI9mMF%AdlO+%*9;NiNh|g!=GG}n2Oy5gTt{H&xD&nNy_IhNyUe6?QUc;} z?$8%m^5`aOP+6m7L*?VM-5+xB25~EUqdlxYW8Nko;pPQp1yC&KzrMqZf!#TlliS_+ zRqU1gu1HoWzyt8`&M*BLMv~jCc*cnbzY0eFffKqAMkle1}o3-Rqb-V!y|5bSuGuZMLKn1e5iP9JIl8P1<6l8`kMfa%S)IJ z)3_bNn>>8laH~zo9nyC1B8Jr~mB<%YP!|LRrS8Z9iihT!V443iPQRtq6=Xu0^}*TW zpuR^?JEhGC0R$wCIS*NtjsO9jPge(pipxfzh=PI~3n|bmd+^QRNLaGz^{nBM-L-?) zK?Y&P2aBmg6dZ^CqytDzW#P75XQU-{?8W2ZZAv6>A;XQ6Q05}BLE{*%&x}%~QN!2fI(2CjU_FyowA6GM=-M zf)mO<8}C@|lm7IJ+*V&aRdI}y3QxJT*+Wj)8Cv#{B1pzL&GowE_3LKe#>g-N^Z?Oq zY(wHP7TTtyMJ<}E-j^Ja1@ZVE&s>6Zm98cUG$ieFck3R-NvwJ=_;l5ziP@h+n&;5K zSb!1htC!bt05*7DG!Poj9L7Ro!2olrv`(Vm|2R~cVc$mG&;tX2)B!4EAQs&K2~_(S zt;eOX%%#uIp}bRL=PrE5Ye^KY*G>>WHef>VB+#Pt-?{+1aET-leOK}laKsdraw{mZ zI(oI@Bx*q~dE1$BE>&adhV*>gR`yS5%ETg@aHGy82QocSd537xyk7E(%k=7ZT%1AN zG87mvhX4Qrf6Sg4LIDy9JV(f<*(zFM2Qwej#X!dSw{cdZ0Z@3;&ppy!2MJYlBX3~Q zCTVEv9Z2aQZiHiHD@&r^7l>}D$JLLHpkUZa%!5Q_qf{ zdiE=TgLhmGL(Z4tN5jhdzMsRcnM_dB4V0BqkFBgyAVbLe0~hxEtGw zSck5NqV?iM?FA+j3V1Q5Aks+tljpB*C#LQb63Fucxar^HE|MnKxY>`YcI@e45H z!8olji@JEi9aNPS+5*KGu=2WRC!OKgY92Od{QZnA&OJZe(bU&wng+S5_v!sfjtA3W z4hw+avhO-|OiBS5l~4?&jiuBSZoX)9Qp~>I6M0sqkHPbfy9I%st+fy132CsE6u2VQ zdF8m z2^#ndpd!GH$(3$SnO%Lmj2Xgkb=m|wvyW|f)Wh+_xBx`x8Y)61pT+%icip{PvYx=Oj($3{ zysCwk6DiVFxpDMuSJ)_)1DhmJhpa>^RrdgUUzh)PpXq&b-F%Z7PeYcMfDANlvuz%G zAy30zeDLg3Yo|Mm{5+x`Nc?|g$lc!|;N?v?kg9{c_-MZzku)_Wy{4XX`HEW9^HAvO zumRan>Q(->(;cbf^^vmhzB;Z~I8AwGGh7nx=+9(oWK3Dx|7!xZ-#MC5fH^wks&b z!II)8^vSOGV|eLo(IG!micp!*bs!ued}8s^a0iOSh4UQnVlC6r0$p(SkY2G$64LJw zyF4Rl>pk7oIbMkUthb}mMZhzHa&~HWTK_;FMFfy50G`5pcS$eV?l~~_2{q#7ER>94 z*aLXJWTW*5!}F9RKvs*#8dmA#?mzR~?i+-0TPzAFP%gXo0dMWRa4YB(59cC5d7LYpq5Va~)@7EGBAf z){%?!ZtMEwZfnzaiZmg3dl~VDMMCxn@qWYhQucZzcOprr&rxGjxcGh=+yBK6wAPRj z{6!N}QHUh#at0n(en}l(#Sf6IMwYJTGIDxmlU9-xcc^4GzQqbML@y@BnB0D228pg zT+XQLZ;=lHx#*kYS?4F3afxBVh7+|+I|xDQ=c#(5NP-0I@IIVSL!@y%F*VNyAQa|B zAjI|+h}YId(;L=@!%lW(PYtt@b&9~?V5$Dve7b|$Ljrj|^>c0e zfR}Kg_by4G~-C+sG5Ovalb@;vA>mk+4BE1tH%vZ3+so!;TaBv>G8}nC8HUaEJA3HHySSq zNu|Z=OehHHq*iPEAHwDF6Tf000000P_I=NdW+-0000%Oi)OY z000014>11`5du;qNsgpou>8@+?%uV$SDdnl{!f5k{E-j9Ikq-tQhJ5mX;EIHL0y~_ z2Pg|X2ux^A0{d1ctVm!24)}>05q^Zv01gO#+R&IQ#w~r`oz@Dq9C8sLIX2Xm>vcyF31`9I{*Nj30NaP@Vq109H^qAf_k) z0QAfNodGKJ0ZIWrkv^MC0*?f$fB_(sbz*L=PB+l?6R&qm|4u;c12$!l+`QSuRhSdc zib;P@+Ev?pxAw2yFYzAtw%_mH-TzX5ap@W0yrcZj_eY9OvicV`}}_gy{tWd z>NF*f(=&^-2`vOFb%9>UVSMr(!G^$gWeo@w@j2EhICIm}OAT|fxV1!a5P4J&WUQKG z#7#m7U8@+@t*h{$Q!h~L+%dBCSi3g7xaw#?P}P4 z?2RiTWaDJ(N#7JsIUC6yMMRgvWz9TMU~PH*?Ttpg6?***^t{r6?;l{-pgO1o0h_~G z$P{y;rYUG`e&}{YVhx!Sw}uv;1}PPNC$5sYrEK$YPa$l1GG1p0F{DoEkQn46H3F_~ zn$IhwYE^AL4vFK{1ky}_65f)2Ao#M~Qe^@|`+4AnooOa=9L5s;Ifsj$gtiC5Aj9HK zQa1y>`;E~Xq*c7gxC5KJ#m`sT#!|iDw2sO}!%l5N@8LZG-{|@!?T;Kpm07hMa?3C@ zj`xg^?JuFp-UK)9T99hQtNG+rP4jtRT&VkXMcV^wmQnYAvBmd6vzH@X17-RJ>Y*0Z zFUC$p0Ph=BXGdquPR`-tn2zbi}R^la@`b~)o@o@Ckox&QNf-Sv)Ip&?WA zfm^PTnYbCo=E-DdDC6$fUiUn`V-=dbXJzQII=?*iX+Pg~9=GZF&t z?so55jF~?6SX}-`4u22&I6`dm;`C|OA2Gq8p2d`(q*`Cyd)C#c<@KsadDBxPqx7qg z+q2Tg2%&*=^rsjnxf(B)NXI}uyC&FOlOGL7d_A9`Zp;I8PaAt-gM|b$gSfyve~Bb8 z&#?D}(UKq47yDBs9PvB$oH|Mv6Ua{*S!dE8f7?HDK^(s)mdp+to7O)uxX*_vJ|sWw zLq+qCJagl9@#_?22e6hUV2-#|LXDLyYf4?`LzL8d4{Tdt2(`;>ZCrY0)eqr-H!%*9 zOJT^bHIMbI{Jupu={kee0M$n&VR8GI>$pyRH3{;o#P;`m18Nx9OIsJ%IiaU)m4z#Y z>$Re1y9faJVR5kLTV1T3yXzr;-Wa?b(at{@)Z8p^H@;U-0_1QJrOZ3Rh3aR`b`LL>uzE7t-J#st_9g zo7ou!VK3nMX6Y6g_*RoToYA!Qe<dN?EM_{d|mge_c-@NOV&VvOWzyC<7iRUx&$h?Qp+P^Shx z&M{ogYgfB`6W%{MG+Wn$L9VoT#AsuSP|x|O`u%>v4Q%~aKmh*UTY{3%;cPP4e(~Cd z3IYr|2=t7C-;~BQ!b&TFqQ4hPE zvWBI4F;*@hqN<~np{jZc+cKRq_bws0R5qn4GdLGwC>D%|*#T;`)|uwgC)wK!Jb6Kq zukO_6I(H~S`)xC{e%4Y3?RubW6*EdI^(@xc1xnGj*J)mymMCu#-@@=n4eJ3sBPZPN8} zchjhqRX^C56_sb!$%`5rTQt$HBW4=$hwHG{;?q~Ak{>E zA2b2zY+~RZb48yXi)G(vB@57hmy^+Wn7p3^y#I?rO$rbEKI59A#IzHFpze$Gp8L$- z>-91PdSEJ)`UF{9u#3jvDz$ZU&C z4loKf@zBGe$^fCyY9b}Hd!!Y=p;5@A?C6*<1E+ah!=$CR@}XJN)IKlP3VeD)*OSxQ zvX9qtOyk(!EyYf_mTuA6yXo1#xhTN1R;Wm61#hr7PkM=Zg52jKojN`!Zu zlfno&TiLfzY#m74KZO?+^4M2JY44;(05DA@0|$uIvPWIe=fBA;m@^&mB(rVzh39|8 zn61WIRvJAj$>Z!mYT??eAMaR&1vG!xbibyZlqIMKh~uFw|d9h8z*< zClH!9W;9cd4R+CY3DUVwh`+H+at7S76f5a!G7nxH48PI*j`Pg(iy(QZcUbN*zG?AJ z5CZou`KSEKyH2@`X3(q!3%S&<=)_5T4?5m#qFXxWXOyNEuop34^KQeE@`4zg_k`wx zF{2S((gZnr%QP8*!;ZoH0*q<#Hv~P`P7D4_D?=+T`j!uLX4*@phY|UYqynJWr`Ykz znja`yM|;*p5R7tsDK$FAj(W|PWM)iI3eLa*+ZeyH7K2 z3i4nq&j7WMfVRMcM~D>>`rzeNlFg3WNiOt4~$J+XC&j?$E@-Vwz=nlr)tZ#(?t7Ilm`-E3v zwM2}ymu+1P;Y$UKyK_s>820LRTACEh^bDzAHeiv|3o#jq$Mmnq-x^gRzI#Xw>@@-c zVG0GsE**4v1MzDS*OBn!N$vFqGb#4iCMMi_|BrJgUwiF|)Kn4CAzrE&9~;31F?2!9 z6a}BfuJa2c`ViNxGhR}nkml{ZHQXW*w907HjSJRRz7YvuGio=sX_p181wi>b1O)X=k(VAJy zgzUlXfh&`TKpF&$i;VQKStZna6iht24ltg!iWv-~wJ@yl252EWwXQZLiwuEmT(;-w z^&Wd1@9LXxW>Q4-zJ9!+m7CZ#3EJK4m=IQkm;l$*3;r=(7kW_)PypNU%I7|gO=G;J z$_%>(HOKN|DAbPg9-_&1bv5_IEA)uZX%VP*@@dqJ>Ta8(5;B(IRSW59z~5r3m1?)h zOKl^dA`e3PD#AdTQ!jM*n%8F`zC3DqX8~(}1*0d+0-p`&7!Am|v{yrUtnNjb-b7ci zT2UWrB_<9h(<69Yeei#>pE#?UI=$^-Yse>nd$Gnd);hx&!D|g*U1#QwN*WOHskBIY zho?`YTGAoj;IGm}db`hKtmgFsYwaX*&}lw;cl6Neh2a!EGU5n50h8&tkm{jS+~kCn z?*A2wGDOvDkO%&s^&07NjBL=Yw)Glb+~2Ri^7i*G?tF<(OUOoAE9_aIodez_2d%~9 z0R8Ifc5~O^J)p*2bnuk$92OHWP(^2!M8n#4KYiYMsyT^#Bay@?m*?ZS-e@+jqljpZ zoM3v@s^U8@5mL-==rs7~)yCV=)AYN#nZfi&^Y!r^sZHKp{tR4ZYwbrvamQHw?Ya*% zoM66butAB~JJm5?JV(ulZ?9uTvMse0*{zNs3d+7WCgXWm0zFqD3RZs^Z9t33`0wqK zcW^f?*ontb^)U^ktI194<^`_aFaP)A6n8R3#hYVAGiI@ZiIZsVD#xQ}6W@a8h)|@1 z`fzVsBPd3i7eD?K?ddvOdZ#X6bcR}|Snv(!1LeaGczlq%+1{SwVT#;qx9lSliQ?ua zR-Uh&0V=y`f{3rbYXV#?q7and3Gw%6@j!XOSXAo1IJ_sP`8lXTB0na0;Kz*f8GZvu z+3c%Tx(+9e@{*4H4pXHSOqdP*A4UL#ZfdE1{d6 z;xRngBi)!+JHQYwujQ<|pxVF2vXiF43fuM8GoUS-4OCGMqAlrpQL;b690jqQs~Ua@ zwu(*wE4e9PfDpVJlh>ezc(as@4t}t|fqL+_e#%nV#}od9_uKcLG{t6)d+~pi{;FQ- zM!Cf4DY9mat%IZ^tF?MA5TDkkYJ>D{D!5newB3f^B5gp^#l(l8FlwSTET zFTk1pzITyR+x#z%-rU|kK1i*+I;+4y@~$dYixGm=$+FLMld4kT$IuTx8IttXMJ9`6 za16o(lFFtu8)!jA@m)}6U*nQgJX9;wt3V4?YCiT6Y$|A8C1Nk&Y)K-fd0b3d6vj+r z$1_x$XkU_LLg}ewmUV_97bM>Cd$4sUMj12A>`e-+)r9uSneo`vgT?tVFDSqJz9xFdr+) z=#%(<(S(KXble?H-ZRz}i4d+<6@}kl=!0(u#McEu`ab7hBA#dZj(MYH3Z!OWaBm_X2>&m=MMWr=L2?l7Rfv+2h2$0^s?A!-V@ z)P9oBiaNf@8q-X?+9RuQnw=l`8F0F1J-qFSKYa)^d7fKj<)3|bh74@v+Xq_H;6pj>fHXxfLn9QCM=8 zb$0^D@lgA^J7cbKgnJKuhq~&l0fAqGD7Funb)ilB8 zV47btz9o6+Eh8Y59n2yW^a&HuyTlDn(R_PY>}~}ej7U2ZWvrPZ(eVvZA-SbSHgt)u zJ@1F5Br6|UV^&v=v5wm=gK zCV&`+CHL!J7`@btOpcLT*XAQYKTZuz*jyjHGpJQ0pMD&yzbQ-B(ZQ804b1X zAX2pF`}&|&t1fm2N*>|ubSh ztv*ZM6o9{VJKh~k?I5z=q~Tw1FRNK}3kjd?!V1DPD#_p_#_PF;0huZ9P=L$N$xqar z0}%X!@Iz483JrApumbZH3R%OdFOw;@!T4A6=a^n36~}-?(VynMcoMz@h}BIG7)hz4e6raS zgGDc%z>W{z?}1R(+%ODDDP_fVHq^%}7N1X16fxXa`PsL^IzWPT;l z>jO?ZO0JOwmlAb^U>0Q2`E5}BiMJ#wqp0YM8teR*Sg2e>GfRSPKMpWGWeVM<_EnE! z@_slRh)U9yw22e~!bBV}^6w$`3`4Q)kq?BMxljrOiAbQ7;|vLuBZH^QPqEA_yIGCR zDY69Ffze1Y;UDgm9@ir~pud{XLoy^;=Xh3fabB%8LEmDnMW(;>r*(0yiGYZR7?>{w z1S_1TjVR_u4BoKftrDT(S=m*~X0F3TMM>1r+A@Q+7L~z6yZy zUubC_n~ja>O}}~t`(so`(0E6SEeekQk7`E!YDwZi)U<6AoL+7 zx^^eS&B@G|U%z#Mqmf&20)Y_=m;c{NSN~-4PQSCg&iqTg^-t=q-s4Lgpp8TCxo4pH ze|Fc>=^puJ;7g;ZsscO>HctK?u(mv>>}D2hx)wjZ!cId-7ap#^-Dnoz0Wis~;T=rY zh8fm#eC@%=VwC^#t7KGkjrhAn#x_lU!~)3IHN4Oq>MOc8YN%rbHNsH^=xJ{W#v9+4 zlK8W`j1Q99^-E~LCR(*4{YadUIwLKcELe#}&9_S-t{0(@#o@Vqu z5ML(p`j^zK;;>er00NBbNK(ek7vKUK)EDg_KeMbh@{W25h(|@5P@)Ejc#D0G3Cu0< ziiBH^xL_CqKt2nQFio_Ya)DKC9V7vq@vf5|p2IU`;oGE^jJiro6q*Bi>9v1>H+;G- zG4HGVewqMgD>k;?XaHAAVKTX@fnX>CKsK4|PK?~WwWc>{{|po1k9S*+V`#myvf&C& z=usT8M4d8u`#d@=Jqeh>vW$oS?hqne;GmNS`fCD*@MhL3*cz7~6ZdQfCP3GrZO4*pDjE+Z4MGQ$kdbBa&EbU#B70kOEO~RREo7#A$J{CtIgo~eKS@S zSE^S@CLe%0AzVmE5X?VJz?W0rq|!vw^Ta6*15rT39ss#6^DNBsfAP)ligDv{YSy#2 zvi>RZ;P}GFyT{B!=ufTPP#eT2U(o9ZHI^-1r&23 zVM5_#Ic_-v6L{$Szh%-?4%clXplY5)MOo1*fAhk~)?I6;f_av91r#M%i7U!v!vp#@ zQ<-T{^8_$d7z6koucuCpPPS><%KBgAg-LlTF%@=$jzWR?DCK~AMduZ4T&i>HaLH=WYl7a7FN*JYWvJ2;2l6%_-uW9cfn|AX zD{R8>>5%J3C9AC4ub7dr9@P>F8F+a5KNXNSBOCu&nH)e+lWhio9+rwvBS&1&(c2k8 zeL-tiCnGnh2iX2e{Uni!%RA`40%pjrElEgjvS_YCqDTvys01ZZw#Y6IuPAzZ&bsH7 z3gL9m<8u8I0$8d}^vJzvxPA{N`Mc#lcsKKv1_XN0cj@#y*!`q8uAU!AGrkqEyzZqE z$ZU>~Z0&R*0j{(>rKzt>77?RrG%>pDT}sCHiDR@4jI%&x-seDAu%T~=21-1^^h#CT zSC+Z*VLUhyF=wNQdO}Z7GdghxYAW3Q20pURs+X6=W3KXhgdwCFox&OV;OP9+=cU67 zTboqoZ4f+yH$@z2v1*@%G4z}7Yw|WvFcXxAFYs#=oImFEvPzeSR%h1*Kfb>b|1Mp? zc3Eu{>>x$EHHth%UcECjKF^r+6CtX)8i-^5$Cqv94f{@l?qDS(1wqS^J`E}rOjt@xhh`VJ|%NfwkMEDoU>rlz=6V$Mn~5O zF&h3Dqe&yMHK-{UC@5CAu5QXuCi?~43KO6xU!Xvgw8fl0HZUCsVZ)YoWXue?skZaX zvWe<+(5;O_{fF*Ru-N;h>ZkU~@b8$H8h+sz`mhIiHU31&O%t&mV*LYtGjAeWnpu&B ziwYZiFzs(+KM5IOPlX|q^e?G*5PKOhl>$yd-@QYbP1vpMvUVE^dHP@7IiK9tj5a0_dKF!f2)KgNlE2aXNZL_yxFw*eX-XAh>-_x4iKZB>SiTfm*4j|DgF9 z4X!&n>Y22cBAwIs_r8W`aSMRF)37qJ_NKY#H|A2$4gn))d#D`zr(%t<-J_UH1xw`h zLSh@8YG6vM%TnK>JR>rbT$gSVCAJ{}E1cU1xyF-h&dh`62HirPsF3EE?oWjIO@1x!arr z{#?3lB%cDM`+o-_=>~qm$HFcsX9Xi|5lr9Vmd3*{i_L1Tih;WGSzntc#>Y#y`Q0~n zFS_6Pa+(1Ur@7K_rNlHkRdw(l6me(sfmlE|-m4Zdf-`x+fSmxqER|AV{( zAL@IE#<39LKI~BYi|Hd!Eb0^XIuw47FFD6D0SRH$xRlt=z%h z`6xRXK+SiXZZ%PpT)ex74^kq$X{t5C%sB)ZcmZXme1{A-^Jkx+VauX?jhhdd6X>{B zp0P>=G0g$_oPBP%09ZXqxV*AY$;3D2q&^sx3&cT+^|F`3UnyR=kMfI&w&f~Ar~S`W zBh|)Ni_oTH$xFMymKe<|eoCu_V?7Mxd+|9bcOpf0n`xHeGPm6CLiL7KWCn@inBR3f zaVC=IE|G|77y9tasG4WsDgBb}6w%_ZNsfJX2VKWFPj56VP&Hl@ZF+izNQcgh0}XD9 z81UIQFWE|BXns?2tEJ42t;z-5ihmPt#uKp5fOM&M)H1zm#=g06pp1CWatdvS@PZf) zB*fhM1Tx-xgJzH}uId3G|HVNrC2AS_q1#U)GFKnb)f5j!Iu1aJNq52?lNi=a4uOe3 z`rRME7+iWrX*5ewcwfv#GM;1f2)5+bqM|+m8yGKjD*&qIDZ4(R@ny!5Dv0A89e}m2 zj8UNekCJU~z-^iU|9D^38eW)o=s-?mGRyzg7fm2VMChf}%n#`$_*C`8$A2y(xNi*V zoX8L-`9$@K#cmPiv)jpGKruutf1KXxLKvO2d$i%$%|j3JucU~uG8`5F6Fl1J{;}U9 z!l`2LL_1z90TC1?&+wN|Q@YxRDog7X`=WXaDA`xV9onThC>cXGt}zx~&$GCHKhswHkVT%k zU#{+pD}`4$G+N-rXZR|v_xxejq5VYSx)re%9O@4%3Pgf`cKn;QwztN}!pAAQn0)*F zhc*=7j)jN&-$nIFD5FmSoQAgNyIO38$ zP3+Q-QEP7qYTp2C=K;1^J0fI_1gE>VhByq+2pt9k=hS@;s8GwT)wL3IxxMOkvgn#w zYmY(i(C9>~e z3<3)SDcWe)W) zgQ1BN`38C@D7RlaT#N11Wte(&1g80CV<~cTr>-nj6T{NTFD&zmHMbWF-Rk;ilkjEk z3i?Q5$|R53pQ^$NyBF=jO~KohMOZyFtD(0$d2?L;0P08oFl`L;J7r450#4`yZW?O1 z4ZVQ%s6B^g02!SIaVnSrA>?!=3HbUiJUWLWqG#X0S6BcAG@-0PP@l$*nUWlZDjM9w z2}!YI9cSlLW|{ssP@XwUc6~QRX<$TcyyuVL+W$8n#teshz%d@2FI+FyKvR@?KT$0G zL}+vgMt}Jqc5(o-N+v4SRWA_^OI6(i`g#ca(rY*}|FpbKmr|wyL}%ELMWrES=2$RR zTrMz<@4nx_U(+k`z!u;oe$M{a7hvND>G3_gnvBcd@(a<(7aWwxg|tAs&mlC;d( zDR^EINW^GBX@PwdRU?mIQx&o--bDN~ECmthWIO-mV|UeOORl&f1FD|S=U-$rnLr;y zwAQES0000%PEAH+B>(^b000620P_I=MF9X)0000%Oi)Pk000014>11`5khVxNsc6i z2B*KYL3E*m|H(!4e**mGAAdc@^5rw6ECsbNX&&Zkd!eyXjhj9AnTetk1R4mMl+`|4 zUvOl8{Ne-hgUSLpLCBJa6|hj5XGegu7no^`>YYHk=Kp_;A12sf*YFn5PEov9!i`p&2RDp#p+z=*^)`2_hD+BZv4(Xh*c|+tBY{7VW zB$%x&j)o1n0$K{_*b@!dVIToBlsYU$UO=KS6s2ex&}sf0FG9=BxLI)#sv1)Bkys)>j}a z)PLr?f&Y_~3)C4C#NYFa<>`>Go_0KkhRrbV$zC?i%M zJi9y|y-6lHW#NV<=kj->y~K{=hYz&Ez_CY5&luQh@q>jp>T~}dU7ilyezIc6#xYg! zN>ShN6OQ%o2~y>`tYdK{z{!4&@UXSihw*4&Sj_dlrQ#&atg9`8WiZBlwJOVm6b8F=}7%IF(Xa z*H^BYJQ`wWQBp)LHJU!ktm#Nh)n5f_;Fq)(xLjA&V8LqOO+eFQiCRrr`m!)VRlq&w zG%!89d&!ae39bN#-(Qz5-mNRM!Ryp@hNhR>W$agvFmzFC)8e=6r0T}8=Q3PCat>_q z4?D>jcmOA8W20_g>|`kPK(hhNuM0=hPviPcz_aZq!F1=HKh?c#K7T5K2thK8a_CSR zsSEx-?6HY_19$V>y{PWD=5-pfC3Nd6P^24z4M-J}*1?Ot2^MYJvSC#}6!lZ#WJ3!i z6G7ZHr$}7~@w2L+Ozvz!9yEWhM>T?R4ZHQSUktK~!@8O1eDtEhkPigQ#j{3mqM z+13lDqr51FI6=cAhebFFlEx>y{4^i@VbB&!if%8mKD^85SY&Yp_z zUeDURck}l&&c$;v1+vDp6tW4RM z`ppv1anaLJ-r*a~X{DyI{8|7Q^FBTfsGaqkxB92gSDE2EGMuO#3aI>oU)5B>R`gYa zn)U}-ieY1xz{!4&>pSM}o5(l-0RD#`YY%n6KxH|Dc5$(NvQ+ipCUi@6wt>aL_YOVXt4n|#@uQJpJt2|0|azXBJn$K zQ?`2PPpWmndL9h#si+7ukz(mc76LMLgb!>0V9{+HAZ=_$|4svlF{1eI-DD*ie4PJ? zZ3#U&bnoF)+Y@+)j&mls26Rh*W*ygLe`!k9%6C4yZd<3wOmhU{4)do0yn>9O&Faux zI0H$2FL+M0^1r=LUE~0WKzF~Oqut;f41Bo(_yE$zDTDLUahOVO4-93`n63^DhV|VQ z$1d^bG>u%YXzWD}dYX=F68C!*J$Ge4y6SzO_hMhfzByuHf>IQ&w6W3OB}&~f zS+MrZ-3iXW6LUH9frmd=anACAyWh=97HmI|+jO7opK<^A>ns}~q~ZVow4gaK(R%QZ z#(L=Y&ux&Us5JsWMNvG`XBk~rtVQh0Xh*(VXE}lFG^)>ILv!Dv2w(zFQw>PDG)lQ> z`dEuX9T<8W(qA3JX6LV8Pke&!RY^eA;thAf>53QfLBiJWAZ&r63Hd~h*5CfvZdcDt z7!8FV*la<22-+cxm3}y?bjb}?&YnL`am-~Lp%GV`A#5{b^q1!6bi1aKaR5}oNWWO> z$p;{yZ^O(_T|g*xxNFJM5c=+5f8JUKNX;uO5F0Bm2Gj5mu)toCCsd+TZ6f>E&(Mf& zb3DoSdh5xT7$K)hn`|*F*VCv0^&Kp$nXsHmk3eGdz&)6+7=9BI=RvTDdzY!8Jj;+3 z+z^qKbrq=DwKQsL@0GP_7Wo1wbNbjD15KZ!n&kHmqesT_j@}cyypw6ty~WL)h3zt$ zK~ps~kP%~_564he_Ye@?9ZYLzUls)Gi(Ng7@kdI4`nL8E`zk&7c&QeX=xRaN$T0T= zCIBfST~a@OG_8kEDp|T1M<=B7UwBmeb0oZVN-4XE&+q=Ce@h(Taf5?YiEhi@7rrm4 zXdSI%PiQId3wJU&D7RaCv}F5)2UhSq>NHRZOsMk&vtK|72EpFhd!)oTI=N#CE$Fj; z(DV5+YXK3AWq@ig*MI{4%!*sYKei~cBqP-9O9VV~Nj>4!k1={Mgv%Z#*!_lVoSJ>o z0eSlA)4b8o9eyk`(cI3=3}mAjUDqOowwQU=`q{~SlxK|)-Y1o+xNC}YkCoP`XSF8r!G8+l!rZFTk`_xF}G`ZuBA_oD>{qBv7H1)Cv06c z{rx`DSCU*AQ6a=$3e4LA?N-e4W3)``H)_(gq!@d4u5l_c-!843-Wm4@yerVhF%v@L zE&g#Qxn4Ck#7Ri}Z!KGTBM5w4pTooyBYP>a(M^vXVuhwZQ@45$0sU>A3|ud5{@@qF zE3TnzIp#ORBWv(Lgo1PtLxnCy{4+d|k`}PNNc=nyZ{1{JOM}f&{fH62o&S>Z9M!-i z6jZz?>tz`Zj0V(Jtd^l_54@_`khV^J@C9$@`T5~N^DT=h-{FCXBfKS)n8ug^6=3j7 za1jlt86gWsvCyhO9{pu^0l$%-uFw<)J32b$={0K~$GmL;>?@dSiauGi3qgagI*K?g z2~Gcp;d2tIlP9QBR*^8-q-LJPc$oy=aaQ*E7mV3yvd6Dik=LctEI+aE4gW6?00A58 zNYvQom>hmTBUR~mLZo%WD)Lz~s=nNd>P@IdiyTx^WW4b6E#bBk4_)BvjqJ}5eCSU! zghUol(taD-!gbS8a@MA1+DgRPJ~XVX|Er`JQR3>4RqC5PJ21QpTeu7dLlpNI@`sY= zy{L1gtbA|c0w}YCSGL_SD6g_@9I`*@ZqxtOHo7QVIa1)F(ViD$a$l; zcscAA+TVh)m-_08DOIj2GIJL-+PJ5&69Y{ln>h=?K+??|U)g&j>;N%Y&~XHUU!qoo z72{RPTEjCsIw;4gT1#AqEir!gq50LY!3+Wb!#?i+0ZGVoO1bF~oSl{u$~7Lk>_X|hWpBZ>fj&7QOSyPP-#kLf?mQ{t)qa7QP18?3nv?eonn$DV0hWS=USXv z{Xc2^o*5o`8Kkum%qbtk#UU(CvK%ETCJs?XB}F-iHPQ%HfEzk1`?#6~mm{2|lwVSGGB!{P7y`Xv*}< z0Sl6#S^ovKFhTZ2jN!)I#dXc^Z^aYyxZ=p;QP!L9)7vtEf%0~rX!uG?N*!iR4nN}+ zFRhrP>HDY=Ht>tb+e;l`G*_HG`jxG0GO_2BN-8;FqUW`)z^+Oo5FExuN;tzOva9|5 z>uj2+TrQ{A?ZIEqZ2&$Et{*M?vMCsS8Q$+>xHxC`>pJUpmQph-V<=0OvP#xiXzA1MepGM5P?l&gz@IVoi>K3#{s6z=TJbhnvJUz*jYfSM!=$++e(5alm%(o$GY7GfSCg4k9;X3zjPSs=4931m=s)4yR%u zt)<4G@tX)Oxwph`F+Ihb@NiUM4E5F{^r!HVZn30GFjHbhy+0GO+xdM@x@pUVPLI~9qys5hlyG+0Y8hR zvllH+Av@AynuBIW|9J=~tJ(*@#A?(C!qB>QnoGCPNk|=0E+XiP1D)`DvRMtSoaNLU z;iK3(zpN>!ov_9E&XK{@t8?RqCoXeiC8LoS^{Dmw6TW*Zy<@gz5`wf2E8UwPbyO_k zrGLX?DXi!$;MAT~9Z}++n@Iow0G|02Y?px}M z49i!g0Bv9zZQrt1FtK(7rHc@0=)Z@vIc}mGq)LR5(;9e3VwB9B1&=R~;0S_xGn{Fe z58=rwb%DBUJ!r1)S@JtEzL-D)-I0eNbMK8VdzL{TPQewG1uLD5WXkL z*3rFd-E9xB-6Y4Z4C3g39V(zK*uVe@Z^TMZo_tg5Grrku7o6rL`Cd590(v}u>~-ow z+fxoQ7Xm06MuIw(6lfUXYfLdw9W;4keC5>%a>`)Rq?o?+T-8va{l0L=Fvo56m$Ww$ zQ;F}=611)x?tRjZ`iDKl(eeb0ceDN2I($GF{Yh$w&@>{S(TF7EenLqzP?3PuA zVymFGbRDcmSL?gDeOWUoPI_Y)g;1oQye(_M+raO2#4XP4_TEA7AJq%uS0I3urKbw1 z9^_`8RvD6t)fX=Xn0EX=f5~mJLiKs#ur$F{!Q#X#y-{h$^d_qcX4%DIwsr8~UNq@t z(>*p05p%z!rX;D9*6kbAYPAEwH=Oxz##tphJ4ze3%!^jh0oJAjbht@@9)`|ZGfb9( z4Tn*fB@Z38+IDm@kQ)h={BC*N!BYkxjh7!>KDrOvppJHZnaa|s)M7S8VarUa`9Bbw zWz?ALh8ssK6t2I%q>o}1Qra*l>d(f+x7RsY(iaOSBG*LmY9Wq&_G~$~PVrdtI`0Zm z?p7)38t#0T>qSy?b`!%-g%3QG44yo7N-F;lm4jfF)jw}+=0I_n96JeH?Y9*d7ivU!8t^09sJn;Hw?y$$B2~O8hS1b{7?{ z&|c9?H_z!l_a}j{w}OqdU)EiOSO&wsa>un`0eBx>O>t2c#kgi zMk9t(i?k7eo{7?zBZ%>P7X~P@sHP$!y8adKKG?2t)cj8Qsmp4{Sxks7Po954Mhie} z&`|gjjhTEFdgqGh^7v$EO!L34WZOsRT-T_>lK;e@0*?ENqj55aLE#tMAZi>kfYwRi z3Bt1m0DS5;5xBp=63yKa^sfGUH+C$;r_A_acK|6mbslt?^!cqesm?OAs8Jm1*2 zS&Jlf$R2B%C>7h=!iAhD*90v);0ppM&*m=uT4PVLIbrdbDVx$FshU z?7*?rdV*b*`@G$la~Rle9$)n=2*-m#PbYKOrxZjB88CrPg|Gl#E*BvB`ITXR4LW?G z^+O<3)A}1QeEb`Y(q2zDR)#-Hne3lAv^gvS>zp+evdem;N!CnztcZ$M+1*FC=I2V0`%s*!bV_wRszIS0uj>LZKc zY|041;?o5FbS5d9GrJp3@z9LZb}o8?zqIDi+$%-gU0BJTBsE7LA#?#18M{RHBVSt5 z#;b2C9=QO@YQuS&hZOoWu4RDcDh3`PH;gAz<%ljXj3I4s#* z4)m|>-;>X6-;g#<<*$n;=`!BJ(l0y1+L0K!;6}evqVuF$|E~ihk(h&%GR|0U5ZFJ9 zPY~aMd@l5kUz|1D$@l=?Rcay(lfA8|a@7eKESl5G{5_+avGOFtkwI840_Z5Rq1kqx zWJL){cF|Gk{5QcF+^uWQ1hu}jX+K}35D-bs6^C0Y8AFlPCkYPUe4qQPtcENOs|N=l z-eFk)ONkWQhsMZU?|2@$X@V0T65va2{ng|}SM8FKASl$ja(a#`{cWmQQ4;VmvT{6n zM4*&Cpj=_eFa6ko1Kju()68yi#AeYphc*_6O5q5gC0>1H8j9q5$)CD#PWI|Ti1eVdL8S68=gTha7RzgGJb-nGy@U<^vMQ;sRP-r~tn zyCigNz-ck%RDu1D@-I-RHoqXP$DR;J3DDw0Rb-DA*%c^8H6oY#Ajv%k zB33t#DA(Jg1fLLvSFia=sx{%~12tOaX2XP#ct7Yl?u=(C00By{v|-!Iqr4A_YvU{{ zA!?TF3m1n0WJMqrXz=>v(S{h_fGUK^i2t33n_hT+O>p839R)%c!1~5nS0HX zQNzmOGaA0ksPLJ1*(o3aKu`QIb}ghZ%42RfPYunVaUh;r-D0)sx?oKmD*TGTHimqV zc2wlsn9z721m+^09$*43|2rXG{y6G4NywQQg^O5K{U|xm+iF1sPOb7P`yL>ozz?dIQ5ul=%|O1#8+^$wJfio=vXNO zE)GPT!1e~MENrz<(SXk{gNPO+=KDs_F}pXfI;B>(Y6?$bs8oaeh*aO;+P^y|u3fW|Mmw`p2zh-PJJWR0lmhj!2WpsXJc&>W>a>bu131}o zOjPt$SO+Mn#0G=8@T-O{bQK^m%i?Jcd~J`j119__S56ihiRew%9}J3!3l$W#{W3t6 z2SfZG+S$lT4Y#t5;m8Cd|Jkv{tcemjRLLX`To;lvM!$P38K_z{!mghjfG3St`huB> z;ou@6x)*suBwYYiub3MzI9S_y<-k6OItA!d@Jsp2m~vFvb}A_Jco*Pn{~?a+ME60T zD~8B`)1>>7C%nqiS%h=#YV2~|hkIEzPN;y057hj|NhOq~(l^XI#SWS&nf~eBhSF40lNyos*Z8tM(QVM zmw*ncRR#a15KhVXnMVGU>s7DQ%cGrmyWH&e@NHDnzRi6s18BadG;+4C35FicNL$UPhXY8$yDKQ{98gE?FiK1Ii9_ImYo3RE~&6#ECY;E)ohwYzzyNfQ>q+LUo=rrhA z>Lnaqiury)|L5F2RrR9USU&;eouzS8AVWUHt29ytU*lX6eUNqOzI?Q?MSsc2E{V2Wn(%|2bT)&~ z-Ip|vek$m0m(eDW;JP{`3JL6g6_V6YsRdCxPS4lm(WA&EAKfdtcde#Ys`m2`6@S3E zQ6Ih#we_;oT3zp_c5?lu%MSXpWn65l$1=RG<&{li!i{Ztb*-AwcVsOZPfrU06pX^l zXXr!aZ(|yiT0RCnMG{LQW4%%B8V?0d;wBZ9{Tzpcn^b$VrZO@;XM0BPvvMM5ew;0j zfMD1UDJcosu3m<5{b`L zNYJ$c2JH@s6`@+0xpU&Q7hO(T=kw~Il_5K9tO>2FOFLLss)zpK`eh}P8B%;)Vi7&U zDGk|79(Q`&fC0%g81|e>b2$V;UykMs0q#aQ5E%(5Z@)|7jb0~Zw;H2z)%H3l9>3i9 z@6O6ShB*^E#R@CY>n{Bnl@j ztaqEhBtubP1;4vh;ORp$g4dxcL)F-m-rnyW6%6?B3eWb4I*i4zVPJ1gidy5uU8ALx znek;-xbq#V6Ho*yV_y8L1pE4~ar~^~iE5>7D=~StT_#kCTvy9T$;5@z39??Xq9CUtQR!BmQX%pCiL$rL5%Ij^1tY0I(5u29>$2n7DS}RR6b5qb|tQV!|8wn%PFXHUL zr0eu`ikOx?7v``Y#uK)c+4lq#2tWV;K~7CZRw4iZ00009008F!05<^urvLx|K}=9c z)&Kwi0S_?$5fMUeBuS1Wh5kwhQC(mU{U;aE{|WGaJNFGA&5+w%c^D|qb7TV?nIhE= zUIfZFI0cX_*#nb(g6NcbWq_*Zx zp@5F|`RaYF$dTl{7k{Li&r#2Y0N3-x;$f&I$$9$5 z-E~=n%c+Y?)X@97*sWRc;39Lk3A?#&a7SBAg1G}#CQwoXI_F7}L zU`v}PDNPb0q4HlUrdZ&*Qnn&dD3n&Ea5mlqx3U*@_T|oWK5)J94qE{wD95_eWk2s$w9Y>XM?SpYhYj${9t1~Qfdv+%}|w>#TunXFtr9yVw}fcc9#W}`rt`DO3WC8e$V$t&Vo z)9<%p1hphL#S#y)3p^VFO$2$rWhW&~1$!EhjV|>yKhHEER zgO zHA0gt=>G!hcP*owAC5i*T(t=13!E>Q%%>F;pC~g_{y*+D>sRhOH@PsP%F?{1k|{aa z@>GE`M{W$(d?gZ6Q?7}tKx&*cvq`p3y+D1@a2VG7LH|V%^cw&-p6o=TQe{dL?Iz5; z)7)PKq2jLH2);_Xd_iT4ft3)-EZ$^WXVdF-zarQj?{}~)p#v?Cw`vzoRwxzG!w|vK ze3vljpxyW;c1!qR3g*|(J;1FF?(`3wBTYS|e6flb(Pkb*1Rgj#B`tIa!9U3)rLw|3 zg&0NcoFT;Y2maVk_eeeBmVM=$*I#T~vl{%v#gQ$e)!##RT37}1mlI2QZ{%_$1OPxWyA+OF<_Aq zCm&zIwg3E4+2M;ZK1--z75pqBA#bK)VKYK(16oDEB1uKl&P;U!nqY1%A5RO78`gfR z0BpD%Dz?1i7ei{to3ZtNXt(6ENl9fZ)mm+K>dOwdbY7w&TLlG$9V96sC-5y}Aw#s3 z!Wj_g73N*LU_+j+ zhSFPUfQaLL{K|;>Kj3^2T3>T zjvm5W>fXwsr77KL5((y*kn9S_%F%ebH^A-+g$39`!Tw(xYZQ$_GglZFJ;^f z2URXLwzOEE+w0Bswb$`Aieoe{tg)o}C?poI-5Ce73YrMw)*Heqdnl z!rx5ox&dWX6nTRCcad_JAa?fMlH3!Fe)yRFZ;x~Y=|O=s^?=%0Xkl_k5NQ&L6-w-C z2j(yXt@RlFF~oUi;YA;+#(k`kw@;xZ!G~@duX_^mLp> z@iPxq$q$`LVx?OO>(Y?n*9#yJeyK8<(I*ajHlxjT<4-7c6Oa5< ziM?e}&k+u>@5c*5jD*O}#sXSp2*U+NB25<^KuJZ$Oc(LkV@)|7Md!l$2qf(8xmdsR zmb4g98((o%ME+?iY77Hr4$ptw-sw;j;#U7_p^ihaA_$b?d^;N^21YPqnF5TFA5)cC z8`^{aysmxHIu8qVUu7sZi&@t$DaGBXt27DjZMLz3JyhCP5;fmkAtp_`Mu;X^W&mn- zg7pEvYx1i3=0p5j9P3?|aP}qzrS4y%j@+{f}(?*xM z&DtK9n8dH{B?^tME>G2xH`aE`!_lQ>$>2YbY+oA~fn0na z=8$H%in)T&G%}{Ol6cMiUR>39p{(DPaZe2IYy-hj zAJ%OOjIa}&fu6qehffp>L~IFMNLFfk7w1V{;85NJr$x=^ol>(7A$`ej`*tv=SKHK2 z=!XmlPF&z&g>U4iUCdq}Aitjq*v=gjp|L_B92;K4{;RICb$Qk9eX{rGhPbcS0bJiq`-K&W;}>cX~AW8*Jh%1w?P{vBb4W)1JEj4Eao zmZoZPG^Ue!Fh?Xp#Dw58RyS#@`PEvPsgF11>o&mcn~!kvuhIM20-U-2PIE8pCRZJ8 zBxQQYhWd%ftCWl_(5EwEt%#XvC$C#o&^j~WKQLKiSOMs!4yqwrRKAntR0BseP9M2C zvZsS8?=;fmawH-Z*xjK(K7{?2I8rfU<>qdHZbV*|T)oZf-_-~dfSC^w;$2;;CFei% z>|C&r5m8VY%D>cKyN_EB->LdENAVC%r}M`zu(F?+XW0C)DCg<&dd!5k5X(lZq*3i+A8{eZ>;>T*%>%Tqg~ zG;B=KP{=4$S#<1Kl~LC1!pJonn{0f#;ofR&?fOG#d}Z0Q$oe}F0S9VX>0jb~+p88v zNZET1lu7#(oBBp^)O8TIEk&ODJqCIrECYc(cvP7od7ygsKkD`ZrPPWrdm-Ak3Mof> zhlvY~rE{ViG|&y*(e_sSL<{9Ylo`K_(;8ve)(m}>(ya0z1-YTN7DxixJUnwyZg10y zQ42vaOjR7HmF1@bu4g;vXL|F7J!BvM>Pf<28Uro zS)$UE`#S3vq=o;H%?F1_RE_s+>O3t1Qrd5>u_S?>|?x*kj=o+Drz<{FW3J2;pTt*BM>$q1{3y`I<1UYP8cJKZ>mf9;|;!1z= zFM}W(7L^GJ#3KEaDD7)xy>j0IgTXLe)7}j+X}^hvN-ReV#R znG;kuTqT43b1QE(?ucnNf5o*F7ETRp4J9oJ_HTTWV3*GmfQ(jK!liXmzb&Em8WpgM zo8x39UkYGJjf#QU8F7PZ>$b6^lq~>$;p=oqS2&)wr{`NTc)zLogkT$Dm;dqH1JO&d zH6pfRa;8NEr8WOA!DjRI}a;l~^yz9Uz3rC4;GMZ`P=OwNWI)K4+wg`}`A0 za}>3{5ha}&EoYJO^GV0V)+j-b)El3xfxerCEk7W#H$GWbnyE*R>v9f{d*JmARud5`=j2+uv*1<~7)x z$ulc4H*F_Ha0}8>tuP3O%)gRW8MOkpHKh!@Xc{>~JDH11nGVS=O|T}B>TC`S>~2Bi zIfe=pv%G-z?VSOTiSRUPXB^ERH5F&|Byf$F@SJVh_K=H`1TQQ)7VO~KB5Dj_5VH0H z$vl$HV4_Hl&CqI@pnaGT-ya4R=ghOVFJsuAJl@I~U(N4wW0X@FhB{RWHxFeVJcH!Q zvy=K-*!MLiNN;RPW|@B9fWlw0arg!eZB7HidPPC-CJ7N(L*wIN_0bpr5g4J@rb|w6 z?&4ve5es_DC);t*>R%3R<{Oupg|V5n6BzU3Bx-B2>!uG|kdwiXC?0tE^}jVz$GZo( zmZVpwUbMw=Vw`Mbimoxu#C(Up&z;3!wL5Id&cOsOGhR}EO2?+JR}f>Ze%Uat<)=+$ zDUA;h+Xl+UolJeDG_5`5L)nZBD-UN!)5Z6g^bjau7yuHl<(~`5_ohGnXk-q+m=xad z9aB_{r;&HBt5v{EKVr_L0km;@CIeD!&R|X4ThTO)&CMYqY%HsO(;|1dXERfUnqiATuJJMp1WzQoq@n-dT{%@!Y2-b>SF`Hmp+vBFb%{z7YZL?LLF3@ z^rd63n5LpBNc10X#x7ARhBwo=gvP9>g&b%TlYd1=+RR=|AhQNw+neHz0aNYeyVdSa+TBZ$5uPnFok~o=FsA zh_@9PS#E4@b31N%0S`^&X4vob6hKuv1p0}<`Va);PQk;n2_>N34~lu~8+tG*@Bb8WJdP*wtB^s^DEmW>q|;>N`nsi0-`@a13V5g4d|HrS-#Vz_|p{kG@sa zP8Mv!ZubTw>R4VQ7Zu{Xv|cD$eLC?225~CYz)r)jM$Olpw17mBHd*C8L7Pf!P+$N6 z1~3XnBF_GVczp?LU4QLH?{*0_L>RkZ!3c0teT{%2gJvm)mW6@V>%*-{5gaGDC7{)U zLiL0WoF1)6fVNkvarY?Vmt$3p7wFnka;$-n_jH^%2nN|^{5$s9(2c=dxH60$Ocy54 z+=GpAepfEp=}cQ1~J)FugV!E3;|qb_qs++{p$UU;Lm={IBAx1 zl~Slm)%~Bo3O36{SfCk*PtC#hp2QOyIg~!|+vCr-ZPQ_Dg1k{VRtXt?(@AVM6adU8 zwZsG_5)r7vn4Bjm$KC|83xWd&maK9BsC*D8s&~^Ju^d_t+50)NHCapP6YSzCi1>Yf z$c#lo^amgz4<6N^!@8sp7*f#-n_+olA#ybOfB|I4pbU9!Q}+O5UGlv>*KBY@;!!$p z_8V}@QOGYr-K;8?&2)7&#~ndjDcjZYOvQ4bHtjN7ZXognph99gLGh=ncR77m^3HyD zx8zys(0I-{jUQQ;y$a+-%ID?0JM@f@(IF{<9beJLUIKI^sKC)RxNftjX&Wm;5X&6L z4pc^10Is#l$VI>cwT{7}J<0yn*D~b=v-^I>G=`v!Lzf^Mb$+{Ni8%alnESLR2)p$l z`6`-MQKO2IG^+oQe|bVymM1`J-l5Kc`(_{HE64byxq?MZ+W6be7~`2dYvc ztxVI&l+QCkaDgBk_Hi2^0aGNqx z`CqL+&+UqfV)hUEv)Cr4zRCAGT-{xMQx!B^miA<;Yj%bqgpx@9Q(sn~>v^9yq%QHM zuN9{sbu7=f;=AivK?Mq$8mA^$07Ph@{u}lqX2b>PqbtM?oh2~P#H-)h%z0x|Ro8&K zX)2Is8H5n6Cwher?)Tp^sa81EsPkj(eXU=;?U79qW$0tPxrOULI8Kl^HoVAz;tJa+C$0_j3a(H|C&~(5 zB;F&~4NuPo{E7$m31_l%)*D&Lq>|7W!m|{=iW={FQUJ0ixQ%ghJ(X}B_FGrLyBh7& zQc}n91jJGS03|HUS%#c>;D`=E=Dv6L5URXL82{8+JGUU(2Q2emW0ID)0*eG+3hJk; zGL@ouRLHzY6+_|(dA%8a*W@SpvT^$^7pgagWvd_e3OIWrW@ZK9!WTU~_6d~~3xyV< zKnbqqXV0M)=U7fPwu>!z4Pv0*d>f^BU9?Ki)0th2BnfCe0j^@ia8`v|j*9>zJx-g+ zt~dFrmZ9SBf96tAV|1x`Q}tvRa6I;aykE&mkA=JNxS-a^9!MEVZauO;dPnz0)W4Gq zez|oCdG_G(xaJRp0;HgI^JGaqWH)*9*l_dn!0P!NG4WTT>ZKvzsELtWdRPbQ{_fiQ z9iZ6@*ZBUv9M@UmkZK+^-{%yg%7#;vk;r&^atO>g_n{E4$EAREC!WqbTds>(k1aN6 zmh+KW^KoQwG&j+(T}&_xzQ<<1unD?;3=b{*=^<-|Xp~rDjSx_7EEfsPjETNA$lyuHK+yLJ!h8=$O2y)P9c0F(=W1*Fp7&Ov^zcMSD7E;ZchytQiDx|wTEqri z(p1elAQ-skD%SWMe`5LS+R=G2`)t}V^jC(>PqmU9pA7oCeY@9ZJaqlRc$k{>4*;%l zxio&ocRB%7j&@hKNyAK)t>^jh(_}5EE-oyQKGY-|;gNGz&E?l_lVRY|>hI?=2HBHpCzF znG2T1s~5XW>vLTRq+huc>|YpDe>Dm4R{CB;6Kq@vj=?}^_n2H0fFv9gkbj52IhhPj zRy89SEs+nc;)Gt)Zp7nX;%rKH;BzlcG0*p$NF->t?AW4RFe##weMSHU9I(3UA*l1! zceucEcSCE}V1t!mI?U2Nl?}FtD5G=mI+Vj%7x@c<1sDK7Lvf8an{4gK`fGjw!ufd= zxf#sQf)3L_I5_Ed`1fCIGCL`dbe~5}&*8S)=PNl}Zq^F=k3J?0_G+kkQ&}LTeyhUt zCBGAm$>U&j1(i4Zkn&3&7dmzF3j)+$r%*4NpJC<}55ywCkn(S%m+WgT51)$$05>~B z0E`pWIvX3$AE*?FDxK!a!q^zoW+pUQ%7##cA2xu}yj{IOwYu=+3)$HVnUHRK^3&=& z!)ZMBn6y0$y5j) znoluh^o*P4q23pO)ZG7}B$};VVQKy}qOwJEasoRm87If1e5f|e_HNrM)}(5cIya$s z1N_!*(804<^s5TXD6|?r>(c^X^Mn`ylRpgl;w_5wfESDQ8EgR!IVF)P63xy<@bKt% z+A5Grr@@k|z?ZFN1rMFXRMp(YpJS3G4O%;c-sz)3!!oyMBa2w!ESnF#RU8nD0D;cT zf8{OZ^#|chZ_mSANEAjX_1ln z1!~(oY>jj@Uj^T|PK4|aA4_^QJzqP@`n|%sw4Ozm*>{Z^@6wvcHSS*skrxIB5+<8n1@YnPl>Xm(O)9*PbZVpFp( zD*|!^gab6Q7kHwl6wKY8Bbcn%&G%pYgyV+wgX`!e+WnWdfV5y z=4NdS1UdQ*+OD=-F&|7O^qddN28unSoLUY&jm7w*Op$}dcFE=wb~0o{YlaQNw2nrsXXnG3;vFrKNMt{jSRuQip%9=<(1waU5R?wBd&40JD*#&_)x5Wq z>})q-VnL)k+4nm~`{^xOiw8~Ndcf8%#^~LdoVrDQA{a$5F|Lf;rP-%1J9VJz<^S$} zh)7h@e@FcU=lxeD(E&01XO)q^WJM%+L&IoAikzcSYz{a6Awk;eH#I>|o60%eg3o?L z1RB`Ko++M;UJ8WxUchmS^lV^V>PIxa)zLu0EqW68JA?Iy3dJ;u{&4&F#VRB!{QX?M zJphO&Jv(%uM_$2@gp6Z&p{k*s<(bD}?C`Um%}A3q|EbS#XF6erYUmjgKP3cXzo=Jl-uwJ{kSiG0YrkaGLYj|vYn#P`b6uJKdnZ%YD7)jdbV;Ky9 z000C_VAx0jfnou*-OnM=50y6>Yy;E*s}N!amNESitUn?Z{T=-ISuKTAyh5-rLK(k5 zA1rB>RH_ZSA*{N{Fx;K^o#ZXpE!d}+UP!`crVS9+x_gh|{wseY-&g$+>9e?)5yDVr zBMm}zVQO-YEGhO_O#I1_?Q%98Vhn!*C`)c)T2`za#W@yyum1J z54wi(prADg$sds`cepkeHZza3_yLDfxCv+`WT0VI58IV6fe@|_bIEiiNxxdqz-nLx zxRvg7HP2w+RW5k|3#biJW8|?Si*Akx)@BybTTQqP?2rzI6ye8TrItpF#UoO*+k zKFdtO;mWqtANA^h00&chhaihMB^nCB-AO^;)}|#~xVf)}bRu?_`Ve>L8DNY~la9S_ zs-`7-fqc>-GMghU(BZ`#3}ODRZ>OPUW2xAuVP!-h39BjTJvWR{0;W1dufcVb*TYVB z?l+I}F%0`ntDXMD7a=_J7}L-5q*y&j>sN81`35=mN~l!sR>eYKHU<6?_8k+=Bq<8g z=(sT=5GQ=OrY)z-k99rKX7Zz7`>-2&7*3{DVlCt%FiQYoUwybWdH|`CSQTO~$T@i4 z#3#}(@AznCxwK|YcS!NWP;Oh`24!9u{2L%H#PiX~IY{^zt@TmWs1T$hA-QK`ui)f? ztsKXNqGv|7dugk{^*KpF56Y(9<3&{#Tm)d@lQLi47f|bgOPUw($I*);YMsd^u=VQy z%ASpU5XDt^62`aX>e(x?3UG|O8Gx?mgbC459}L}mzL8yO!?Tm9V zu2OTKZk2UsFBULAdfi9oqpS>&R#U{*t$XF=%bzSB002QwO-8sR0000000sa6^8o-o z0RU3~002QuP)PFt0003GF#iz|0!}1JlB8g8{LzL`)m7d1rxwxw3Gj=5{Q7t)&!dC) zRB>RO?t468zI3L7R1@XmW^*x3Tv0={zGIqnJi1UpUB zR+YX0c|eB05IbR_v2ss8urWdooO05Jf#sZ?YH>;xiVYk9GbGaba3SO9(E2u4?zsah zNpcSzCj}89No%Sd1Lc@44=%f@aq@`!O$`#Ylbwx;qV<7g9vvpM9?2I_z_6WN1KIlG zUk?CQP&go@BLDyty#Sp7D)a$90X~sJoJoR(7PSBYAe7yzf&0Jz`7htE*^b-Y*Z;{C z^O^d2XM4ZJedqr(|8wzI#glhFmCd(szwv+DKWqExxaXGt1N&|L&)*8c{m1dY_YQxw zMczJ5KWV+#e;5Ao>>v8w)9=lXoHv@+qz6`?KXwtW7Pa*!ycSAjEXwOsUZU z$OQjZI8uJQF<9;y>8|lwl+2m%@6(^Eo#V7Gm`oTP9P4xPj%=C$xmt8|bSt_GH}dLS zL;OnPvOo91EI3nj7J2%I8$O^X;6XKL4Cu9UEYQPG+qyS<+a3oIr8sDuX{`T|POC@* z+&%LIadv?fWs^$E+7x!T@~(Els{bdh4M5lDD&>MWNc!2f;?Q^>9#aZL#}(mZIYIo@ zi7_e;Z47PY{B+PMSayRASgh+6BU?R$c3mHd>kB?n;43PSk~+zzd7= zcHfP#k^BG%S9ImdQ zgUJg{0GwxwPvg|puvPNv`fxv0co{HVNW-9j>B|a&M8TcE+$;8jeim8_Giu$W3L&P6 zb@has3}DjJdFy2mzr21smU620<-n0eN(;dmXhFlus0|Gh=W|{E?^59b$%hR{-g~`D zE@I|w2Hmfc`n}luliGX|9}tE=rn%h@js~FL3lzpNEh6SD*s(4YQ?0LjwHyinycSElQn%a=S6?+xmb%0Q9p&;Cj$=8_DIc$>N9yW8Ic~>V+Zr zxKWr$Y4D@M*0?_DXj^i}MR}iEixyBk@W!b*4|{9OO@R2q=U+x(Mn0uDl#CC$0@3+F zE~(cUq0uTf61d~2l7;YPX5HQXV&DHaR}r%~d+j+srXg%_C?DW(BI4zDu!%!&Z)|?7 z(3zh|&7f|UT(@1*W@TnZ^#v~Gy#1ZX8R+_WdPK$vANzKQ{&LZJf=SgmYs&H>5;_OA zpoGdl?KQ8Yv}o1nTr%&FIpKvE-$TcD74M{KpTnQx?+iaPH7Z-wna2xfBSU?cQGt+2 z;;7tz^c&fX6I#aiQ&5F0k39pXA&4d&G;ojmJwbneOmX?$dk{v3A ni1!ezV+>+} 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 sl!n7HEXo#zZKhire me.", "SettingsVideoMute": "mute audio", - "SettingsVideoMuteExplanation": "disables audio in downloaded video when possible. ignored when audio mode is on or service only supports audio.", - "SettingsVideoGeneral": "general", + "SettingsVideoMuteExplanation": "disables audio in downloaded video when possible.", "ErrorSoundCloudNoClientId": "couldn't find client_id that is required to fetch audio data from soundcloud. try again, and if issue persists, {ContactLink}.", "CollapseServices": "supported services", "CollapseSupport": "support & source code", @@ -115,6 +108,14 @@ "FollowSupport": "follow {appName} on mastodon or twitter for support, polls, news, and more:", "SupportNote": "please note that questions and issues may take a while to respond to, there's only one person managing everything.", "SourceCode": "report issues, explore source code, star or fork the repo:", - "PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, salted sha256 hash of your ip address and information about requested stream are temporarily stored in server's RAM for 2 minutes. after 2 minutes all previously stored information is permanently removed. hash of your ip address is used for limiting stream access only to you.\nno one (even me) has access to this data, because official cobalt codebase doesn't provide a way to read it outside of processing functions in the first place.\n\nyou can check {appName}'s github repo for yourself and see that indeed nothing is stored permanently." + "PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, salted sha256 hash of your ip address and information about requested stream are temporarily stored in server's RAM for 2 minutes. after 2 minutes all previously stored information is permanently removed. hash of your ip address is used for limiting stream access only to you.\nno one (even me) has access to this data, because official {appName} codebase doesn't provide a way to read it outside of processing functions in the first place.\n\nyou can check {appName}'s github repo for yourself and see that indeed nothing is stored permanently.", + "ErrorYTUnavailable": "this youtube video is unavailable or age restricted. i am currently unable to download videos with sensitive content. try another one!", + "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nnote: youtube api sometimes acts unexpectedly. blame google for this, not me.", + "SettingsCodecSubtitle": "youtube codec", + "SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: low player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\nif you want best editor/player/social media compatibility, pick h264.", + "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 {appName}) language is used.", + "SettingsDubDefault": "original", + "SettingsDubAuto": "auto" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 2a31206..e448398 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -29,29 +29,23 @@ "ErrorCouldntFetch": "мне не удалось получить инфу о твоей ссылке. проверь её и попробуй ещё раз.", "ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".", "ErrorBadFetch": "произошла ошибка при получении инфы о твоей ссылке. ты уверен, что она работает? проверь её, и попробуй ещё раз.", - "ErrorCorruptedStream": "этот файл сломан на стороне {s}. ты можешь попробовать ещё раз, но если не получится, то попробуй другой формат и разрешение.", + "ErrorCorruptedStream": "этот файл сломан на стороне сервера. попробуй ещё раз чуть позже!.", "ErrorNoInternet": "кажется, нет подключения к интернету. возможно лежит сервер {appName}. в любом случае, проверь подключение к интернету и попробуй ещё раз.", - "ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу {s}. скорее всего {s} лежит, или же ip адрес {appName} добавили в чёрный список. попробуй ещё раз чуть позже.", + "ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. скорее всего он лежит, или же ip адрес {appName} добавили в чёрный список. попробуй ещё раз чуть позже!", "ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!", - "ErrorLiveVideo": "я не гадалка, и не умею заглядывать в будущее. дождись окончания прямого эфира и попробуй ещё раз чуть позже.", + "ErrorLiveVideo": "я не гадалка, и пока что не умею заглядывать в будущее. дождись окончания прямого эфира и попробуй ещё раз!", "SettingsAppearanceSubtitle": "внешний вид", "SettingsThemeSubtitle": "тема", - "SettingsFormatSubtitle": "формат загрузок", + "SettingsFormatSubtitle": "формат", "SettingsQualitySubtitle": "качество", "SettingsThemeAuto": "авто", "SettingsThemeLight": "светлая", "SettingsThemeDark": "тёмная", - "SettingsQualitySwitchMax": "макс", - "SettingsQualitySwitchHigh": "высокое", - "SettingsQualitySwitchMedium": "среднее", - "SettingsQualitySwitchLow": "низкое", - "SettingsQualitySwitchLowest": "худшее", "SettingsKeepDownloadButton": "оставлять >> на экране", "AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране", "SettingsEnableDownloadPopup": "спрашивать, что делать при скачивании", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", - "SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. у webm видео битрейт обычно выше, но устройства на ios не могут проигрывать их без сторонних приложений.", - "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.\nесли ты хочешь опубликовать видео с youtube где-то в соц. сетях, то выбирай комбинацию из mp4 и 720p.", + "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.", "LinkGitHubChanges": ">> смотри предыдущие изменения на github", "NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.", "DownloadPopupDescriptionIOS": "зажми кнопку \"скачать\", затем скрой превью видео и выбери \"загрузить файл по ссылке\" в появившемся окне.", @@ -70,7 +64,7 @@ "AccessibilityModeToggle": "переключить режим скачивания", "DonateLinksDescription": "ссылки на донаты открываются в новой вкладке. это наилучший способ отправить донат, если ты хочешь, чтобы я получил его напрямую.", "SettingsAudioFormatBest": "лучший", - "SettingsAudioFormatDescription": "когда выбран \"лучший\" формат, ты получишь аудио лучшего качества, так как оно не будет сконвертировано. если же выбрано что-то другое, то аудио будет немного сжато.", + "SettingsAudioFormatDescription": "когда выбран \"лучший\" формат, ты получишь аудио наилучшего качества, так как оно не будет сконвертировано. если же выбрано что-то другое, то аудио будет немного сжато.", "Keyphrase": "сохраняй то, что любишь", "SettingsRemoveWatermark": "убрать ватермарку", "ErrorPopupCloseButton": "ясно", @@ -105,8 +99,7 @@ "DonateVia": "открыть", "DonateHireMe": "или же ты можешь пригласить меня на работу.", "SettingsVideoMute": "отключить аудио", - "SettingsVideoMuteExplanation": "убирает аудио при загрузке видео, когда это возможно. игнорируется если включен режим аудио или сервис поддерживает только аудио загрузки.", - "SettingsVideoGeneral": "основные", + "SettingsVideoMuteExplanation": "убирает аудио при загрузке видео когда это возможно.", "ErrorSoundCloudNoClientId": "мне не удалось достать client_id, который необходим для получения аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.", "CollapseServices": "что поддерживается?", "CollapseSupport": "поддержка и исходный код", @@ -115,6 +108,14 @@ "FollowSupport": "подписывайся на аккаунты {appName} на mastodon или twitter для новостей, поддержки, участия в опросах, и многого другого:", "SupportNote": "помни, что ответ на твой вопрос может занять время, так как только один человек занимается и разработкой и поддержкой.", "SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", - "PrivacyPolicy": "политика конфиденциальности {appName} довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - не моё дело, а только твоё.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы данной функции.\n\nв этом случае, sha256 хэш (с солью) твоего ip адреса и данные о запрошенном стриме хранятся в оперативной памяти сервера в течение 2-х минут. по истечении этого времени всё стирается. хэш твоего ip адреса используется для предоставления доступа к стриму только тебе. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как код {appName} специально не позволяет читать такие данные снаружи.\n\nты можешь посмотреть исходный код {appName} и убедиться, что всё так, как описано." + "PrivacyPolicy": "политика конфиденциальности {appName} довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - не моё дело, а только твоё.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы данной функции.\n\nв этом случае, sha256 хэш (с солью) твоего ip адреса и данные о запрошенном стриме хранятся в оперативной памяти сервера в течение 2-х минут. по истечении этого времени всё стирается. хэш твоего ip адреса используется для предоставления доступа к стриму только тебе. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как код {appName} специально не позволяет читать такие данные снаружи.\n\nты можешь посмотреть исходный код {appName} и убедиться, что всё так, как описано.", + "ErrorYTUnavailable": "это видео недоступно или же ограничено по возрасту на youtube. пока что я не умею скачивать подобные видео. попробуй другое!", + "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", + "SettingsCodecSubtitle": "кодек для видео с youtube", + "SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nесли тебе нужна максимальная совместимость с плеерами/редакторами/соц.сетями, то выбирай h264.", + "SettingsAudioDub": "звуковая дорожка для видео с youtube", + "SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера (и {appName}).", + "SettingsDubDefault": "оригинал", + "SettingsDubAuto": "авто" } } diff --git a/src/modules/changelog/changelog.json b/src/modules/changelog/changelog.json index 9013d60..b893c34 100644 --- a/src/modules/changelog/changelog.json +++ b/src/modules/changelog/changelog.json @@ -1,11 +1,16 @@ { "current": { + "version": "5.1", + "title": "the evil has been defeated", + "banner": "happymeowth.webp", + "content": "hey, ever wanted to download a youtube video without a hassle? cobalt is here to help. this update fixes all issues related to youtube downloads.\nnot only that, but it also introduces features never before seen in a downloader, such as youtube dub downloads! read below to see what's up :)\n\ntl;dr:\n*; audio in youtube videos FINALLY no longer gets cut off.\n*; you now can pick any video resolution you want (from 360p to 8k) and any possible youtube video codec (h264/av1/vp9).\n*; you now can download youtube videos with dubs in your native language. just check settings > audio.\n*; youtube processing has been vastly sped up.\n\nok, now onto the nerdy part of changelog. this update is pretty huge and includes improvements across the board.\n\nservice improvements:\n*; all youtube functionality has been reworked. cobalt now relies on innertube apis, not web scraping.\n*; random audio cut off issue has been fixed, let me know if it ever occurs again. (closes #62, #66, #75).\n*; added support for youtube dubs. currently it's using your browser's default language when enabled, but i have plans on making a picker. i'll ask people on twitter and mastodon if this feature is needed, and add a picker in next updates.\n*; instead of adding more quality presets, i added granular quality options. pick whatever you like, from 360p up to 4320p (for all services, not just youtube).\n*; replaced a format picker with codec picker for youtube. you can pick h264, av1, or vp9. all of them should work as expected.\n*; youtube audio files are now properly matched to corresponding video files.\n*; it's now always possible to download pristine h264 720p/360p videos from youtube. these videos will work ANYWHERE, so they're default for mobile.\n*; youtube requests are no longer permanently cached, ram usage should drop even further.\n*; youtube video and audio file names now include codec and dub language when applicable.\n*; general performance of entire youtube download process has been greatly improved.\n*; vk module has been reworked to be more compact and not make use of outdated technique of quality picking. should also be way more reliable.\n\ninternal improvements:\n*; cleaned up services config, all constants have been moved directly to modules for quicker access.\n*; matching module has been slightly cleaned up.\n\ninterface improvements:\n*; many descriptions and error messages have been slightly tuned to be less wordy.\n*; unnecessary title duplications in settings have been merged into one.\n*; added more clarity to quality and codec descriptions.\n\nif you use cobalt api, please note that you have to update your creation to support new features.\n\nthis is the second batch of 5.x improvements, there's way more to come. thank you for being here, i really appreciate your support.\n\nif you want to thank me (the developer), there's a nice tab under this changelog that has \"donations\" text on it. anything helps me continue developing and hosting the friendliest media downloader :D" + }, + "history": [{ "version": "5.0", "title": "it's all about attention to detail!", "banner": "valentines.webp", "content": "happy valentine's day! i have an update for you, as a gift :D\n\ntl;dr: added support for reddit gifs, fixed douyin downloads, fixed vimeo quality picking, revamped entirety of codebase, and many other fixes.\n\nhere's more info:\n\nthis update is mostly about cleaning up and polishing the codebase, but it also has some new features. here's what's up:\n\nservice-related improvements:\n*; you now can download gifs from reddit!\n*; attempting to download a video from douyin no longer throws an error (bytedance changed the api endpoint, yet again).\n*; fixed quality picking for vimeo downloads.\n*; fixed length limit check in vimeo module.\n*; fixed support for \"user view\" vk clips links.\n*; various twitter errors are now displayed correctly instead of falling back to the default error.\n*; state of all services is now tested on each commit.\n\nui improvements:\n*; cobalt social links no longer disappear if you have an aggressive ad blocking extension installed.\n*; various localization improvements for both english and russian.\n*; changed some service aliases to display full list of supported downloads.\n*; added current branch information to version text (in settings).\n*; fixed typos in older changelogs.\n\ninternal improvements:\n*; everything has been sanitized, improved, and refactored. code is now much easier to read and maintain.\n*; rewrote and/or optimized all modules that were messy or inefficient.\n*; all git interaction functions now store info in cache instead of fetching it every time the function is called.\n*; added a test script that checks functionality of all supported services.\n*; updated deepsource config. checks are more accurate now.\n*; requests from internet explorer are now dropped entirely instead of redirecting people stuck in 90s to a proper browser download page. this was done to avoid (my) personal bias towards browsers.\n\ni put a ton of effort into this version, and i hope you like it as much as i do.\n\nthank you for using cobalt. there's so much more to come :)" - }, - "history": [{ + }, { "version": "4.8", "title": "prettier than ever", "banner": "catmakeup.webp", @@ -24,7 +29,7 @@ "version": "4.5", "title": "better, faster, stronger, stable", "banner": "meowthstrong.webp", - "content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on github or npm!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, please feel free to do it on github!\n\nthank you for reading this, and thank you for sticking with cobalt and me." + "content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; cobalt now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on github or npm!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, please feel free to do it on github!\n\nthank you for reading this, and thank you for sticking with cobalt and me." }, { "version": "4.4", "title": "over 1 million monthly requests. thank you.", @@ -33,12 +38,12 @@ }, { "version": "4.3.2", "title": "twitter improvements & changelog overhaul", - "content": "- you can download explicit content from twitter.\n- direct video links from twitter are properly supported (video/1, video/2, etc.).\n- changelog history got support for banners.\n- changelog categories are not messy anymore.\n- {appName} version in changelogs is now highlighted.\n- changelog history got separators to make text easier to read.\n- changelog history can be collapsed after loading.\n- download button takes less time to change back to pressable state.\n\nif you're a developer and would like to play around with cobalt's api, then read more about it in older changelogs below!" + "content": "- you can download explicit content from twitter.\n- direct video links from twitter are properly supported (video/1, video/2, etc.).\n- changelog history got support for banners.\n- changelog categories are not messy anymore.\n- cobalt version in changelogs is now highlighted.\n- changelog history got separators to make text easier to read.\n- changelog history can be collapsed after loading.\n- download button takes less time to change back to pressable state.\n\nif you're a developer and would like to play around with cobalt's api, then read more about it in older changelogs below!" }, { "version": "4.3", "title": "developers, developers, developers, developers", "banner": "developersdevelopersdevelopers.webp", - "content": "this update features a TON of improvements.\n\ndevelopers, you now can rely on {appName} for getting content from social media. the api has been revamped and documentation is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- {appName} web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using {appName}'s api, make sure to mention @justusecobalt on twitter, i would absolutely love to see what you made." + "content": "this update features a TON of improvements.\n\ndevelopers, you now can rely on cobalt for getting content from social media. the api has been revamped and documentation is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- cobalt web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using cobalt's api, make sure to mention @justusecobalt on twitter, i would absolutely love to see what you made." }, { "version": "4.2", "title": "optimized quality picking and 8k video support", @@ -46,15 +51,15 @@ }, { "version": "4.1", "title": "better tiktok image downloads", - "content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, {appName} will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in {appName} 4.x yet, check out the previous changelog down below. there's a ton of stuff to like." + "content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, cobalt will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in cobalt 4.x yet, check out the previous changelog down below. there's a ton of stuff to like." }, { "version": "4.0", "title": "better and faster than ever", - "content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- {appName} now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when {appName} is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, {appName} will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, you can do it on github." + "content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, you can do it on github." }, { "version": "3.7", "title": "support for multi media tweets is here!", - "content": "{appName} now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and {appName} will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for {appName}, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- {appName} is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through {appName}'s code." + "content": "cobalt now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and cobalt will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for cobalt, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- cobalt is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through cobalt's code." }, { "version": "3.6.2 + 3.6.3", "title": "less disturbance", @@ -62,7 +67,7 @@ }, { "version": "3.6", "title": "improvements all around!", - "content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- {appName} is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up." + "content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- cobalt is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up." }, { "version": "3.5.4", "title": "tiktok support is back :D", @@ -70,10 +75,10 @@ }, { "version": "3.5.2", "title": "vk clips support, improved changelog system, and less bugs", - "content": "new features: \n- added support for vk clips. {appName} now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, {appName} now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- {appName} should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side." + "content": "new features: \n- added support for vk clips. cobalt now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, cobalt now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- cobalt should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side." }, { "version": "3.5", "title": "ui revamp and usability improvements", - "content": "new features:\n- {appName} now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, {appName} won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in {appName}. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved." + "content": "new features:\n- cobalt now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, cobalt won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in cobalt. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved." }] } diff --git a/src/modules/config.js b/src/modules/config.js index 82a109e..a6767da 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -14,7 +14,6 @@ export const genericUserAgent = config.genericUserAgent, repo = packageJson["bugs"]["url"].replace('/issues', ''), authorInfo = config.authorInfo, - quality = config.quality, donations = config.donations, ffmpegArgs = config.ffmpegArgs, supportedAudio = config.supportedAudio, diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 0c90a75..4c40593 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -1,5 +1,5 @@ import { backdropLink, celebrationsEmoji, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js"; -import { services as s, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js"; +import { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js"; import { getCommitInfo } from "../sub/currentCommit.js"; import loc from "../../localization/manager.js"; import emoji from "../emoji.js"; @@ -196,23 +196,31 @@ export default function(obj) { title: `${emoji("🎬")} ${t('SettingsVideoTab')}`, content: settingsCategory({ name: "downloads", - title: t('SettingsVideoGeneral'), + title: t('SettingsQualitySubtitle'), body: switcher({ name: "vQuality", - subtitle: t('SettingsQualitySubtitle'), explanation: t('SettingsQualityDescription'), items: [{ "action": "max", - "text": `${t('SettingsQualitySwitchMax')}
(2160p+)` + "text": "4320p+" }, { - "action": "hig", - "text": `${t('SettingsQualitySwitchHigh')}
(${quality.hig}p)` + "action": "2160", + "text": "2160p" }, { - "action": "mid", - "text": `${t('SettingsQualitySwitchMedium')}
(${quality.mid}p)` + "action": "1440", + "text": "1440p" }, { - "action": "low", - "text": `${t('SettingsQualitySwitchLow')}
(${quality.low}p)` + "action": "1080", + "text": "1080p" + }, { + "action": "720", + "text": "720p" + }, { + "action": "480", + "text": "480p" + }, { + "action": "360", + "text": "360p" }] }) }) @@ -222,17 +230,19 @@ export default function(obj) { body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'), 3) }) + settingsCategory({ - name: "youtube", + name: t('SettingsCodecSubtitle'), body: switcher({ - name: "vFormat", - subtitle: t('SettingsFormatSubtitle'), - explanation: t('SettingsFormatDescription'), + name: "vCodec", + explanation: t('SettingsCodecDescription'), items: [{ - "action": "mp4", - "text": "mp4 (av1)" + "action": "h264", + "text": "h264 (mp4)" }, { - "action": "webm", - "text": "webm (vp9)" + "action": "av1", + "text": "av1 (mp4)" + }, { + "action": "vp9", + "text": "vp9 (webm)" }] }) }) @@ -241,18 +251,32 @@ export default function(obj) { title: `${emoji("🎶")} ${t('SettingsAudioTab')}`, content: settingsCategory({ name: "general", - title: t('SettingsAudioTab'), - body: switcher({ - name: "aFormat", - subtitle: t('SettingsFormatSubtitle'), - explanation: t('SettingsAudioFormatDescription'), - items: audioFormats - }) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation')) - }) + settingsCategory({ - name: "tiktok", - title: "tiktok & douyin", - body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + `
${t('SettingsAudioFullTikTokDescription')}
` - }) + title: t('SettingsFormatSubtitle'), + body: + switcher({ + name: "aFormat", + explanation: t('SettingsAudioFormatDescription'), + items: audioFormats + }) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation')) + }) + settingsCategory({ + name: "dub", + title: t("SettingsAudioDub"), + body: switcher({ + name: "dubLang", + explanation: t('SettingsAudioDubDescription'), + items: [{ + "action": "original", + "text": t('SettingsDubDefault') + }, { + "action": "auto", + "text": t('SettingsDubAuto') + }] + }) + }) + settingsCategory({ + name: "tiktok", + title: "tiktok & douyin", + body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + explanation(t('SettingsAudioFullTikTokDescription')) + }) }, { name: "other", title: `${emoji("🪅")} ${t('SettingsOtherTab')}`, diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index ccd24df..5f8f62c 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -27,8 +27,7 @@ export default async function (host, patternMatch, url, lang, obj) { case "twitter": r = await twitter({ id: patternMatch["id"] ? patternMatch["id"] : false, - spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false, - lang: lang + spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false }); break; case "vk": @@ -36,34 +35,27 @@ export default async function (host, patternMatch, url, lang, obj) { url: url, userId: patternMatch["userId"], videoId: patternMatch["videoId"], - lang: lang, quality: obj.vQuality }); break; case "bilibili": r = await bilibili({ - id: patternMatch["id"].slice(0, 12), - lang: lang + id: patternMatch["id"].slice(0, 12) }); break; case "youtube": let fetchInfo = { id: patternMatch["id"].slice(0, 11), - lang: lang, quality: obj.vQuality, - format: "webm" - }; - if (url.match('music.youtube.com') || isAudioOnly === true) obj.vFormat = "audio"; - switch (obj.vFormat) { - case "mp4": - fetchInfo["format"] = "mp4"; - break; - case "audio": - fetchInfo["format"] = "webm"; - fetchInfo["isAudioOnly"] = true; - fetchInfo["quality"] = "max"; - isAudioOnly = true; - break; + format: obj.vCodec, + isAudioOnly: isAudioOnly, + isAudioMuted: obj.isAudioMuted, + dubLang: obj.dubLang + } + if (url.match('music.youtube.com') || isAudioOnly === true) { + fetchInfo.quality = "max"; + fetchInfo.format = "vp9"; + fetchInfo.isAudioOnly = true } r = await youtube(fetchInfo); break; @@ -71,8 +63,7 @@ export default async function (host, patternMatch, url, lang, obj) { r = await reddit({ sub: patternMatch["sub"], id: patternMatch["id"], - title: patternMatch["title"], - lang: lang, + title: patternMatch["title"] }); break; case "douyin": @@ -81,7 +72,6 @@ export default async function (host, patternMatch, url, lang, obj) { host: host, postId: patternMatch["postId"], id: patternMatch["id"], - lang: lang, noWatermark: obj.isNoTTWatermark, fullAudio: obj.isTTFullAudio, isAudioOnly: isAudioOnly @@ -91,15 +81,13 @@ export default async function (host, patternMatch, url, lang, obj) { r = await tumblr({ id: patternMatch["id"], url: url, - user: patternMatch["user"] ? patternMatch["user"] : false, - lang: lang + user: patternMatch["user"] ? patternMatch["user"] : false }); break; case "vimeo": r = await vimeo({ id: patternMatch["id"].slice(0, 11), - quality: obj.vQuality, - lang: lang + quality: obj.vQuality }); break; case "soundcloud": @@ -109,8 +97,7 @@ export default async function (host, patternMatch, url, lang, obj) { song: patternMatch["song"], url: url, shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false, accessKey: patternMatch["accessKey"] ? patternMatch["accessKey"] : false, - format: obj.aFormat, - lang: lang + format: obj.aFormat }); break; default: diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js index 7f40259..8f33ad2 100644 --- a/src/modules/processing/services/vimeo.js +++ b/src/modules/processing/services/vimeo.js @@ -1,4 +1,11 @@ -import { maxVideoDuration, quality, services } from "../../config.js"; +import { maxVideoDuration } from "../../config.js"; + +const resolutionMatch = { + "3840": "2160", + "1920": "1080", + "1280": "720", + "960": "480" +} export default async function(obj) { let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => { return r.json() }).catch(() => { return false }); @@ -14,7 +21,7 @@ export default async function(obj) { try { if (obj.quality !== "max") { - let pref = parseInt(quality[obj.quality], 10) + let pref = parseInt(obj.quality, 10) for (let i in all) { let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10) if (currQuality === pref) { @@ -51,9 +58,9 @@ export default async function(obj) { switch (type) { case "parcel": if (obj.quality !== "max") { - let pref = parseInt(quality[obj.quality], 10) + let pref = parseInt(obj.quality, 10) for (let i in masterJSON_Video) { - let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10) + let currQuality = parseInt(resolutionMatch[masterJSON_Video[i]["width"]], 10) if (currQuality < pref) { break; } else if (String(currQuality) === String(pref)) { diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js index b7370af..58f20cd 100644 --- a/src/modules/processing/services/vk.js +++ b/src/modules/processing/services/vk.js @@ -1,45 +1,50 @@ import { xml2json } from "xml-js"; -import { genericUserAgent, maxVideoDuration, services } from "../../config.js"; -import selectQuality from "../../stream/selectQuality.js"; +import { genericUserAgent, maxVideoDuration } from "../../config.js"; -export default async function(obj) { +const representationMatch = { + "2160": 7, + "1440": 6, + "1080": 5, + "720": 4, + "480": 3, + "360": 2, + "240": 1, + "144": 0 +} +const resolutionMatch = { + "3840": "2160", + "2560": "1440", + "1920": "1080", + "1280": "720", + "852": "480", + "640": "360", + "426": "240", + // "256": "144" +} + +export default async function(o) { let html; - html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, { + html = await fetch(`https://vk.com/video-${o.userId}_${o.videoId}`, { headers: { "user-agent": genericUserAgent } }).then((r) => { return r.text() }).catch(() => { return false }); if (!html) return { error: 'ErrorCouldntFetch' }; if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' }; + let quality = o.quality === "max" ? 7 : representationMatch[o.quality]; let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); - if (Number(js["mvData"]["is_active_live"]) !== 0) return { error: 'ErrorLiveVideo' }; - if (js["mvData"]["duration"] > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (Number(js.mvData.is_active_live) !== 0) return { error: 'ErrorLiveVideo' }; + if (js.mvData.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 })); - let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]; - if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"]; + let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })); + let repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"]; + let bestQuality = repr[repr.length - 1]; + let resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height' + if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality]; - let selectedQuality, - attr = repr[repr.length - 1]["_attributes"], - qualities = Object.keys(services.vk.quality_match); - for (let i in qualities) { - if (qualities[i] === attr["height"]) { - selectedQuality = `url${attr["height"]}`; - break - } - if (qualities[i] === attr["width"]) { - selectedQuality = `url${attr["width"]}`; - break - } - } - - let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1); - let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]); - let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"]; - if (!(selectedQuality in js["player"]["params"][0])) return { error: 'ErrorEmptyDownload' }; - - return { - urls: js["player"]["params"][0][`url${userQuality}`], - filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4` - } + if (bestQuality) return { + urls: js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`], + filename: `vk_${o.userId}_${o.videoId}_${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4` + }; + return { error: 'ErrorEmptyDownload' } } diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index d96af51..2690a0d 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -1,88 +1,92 @@ -import ytdl from "better-ytdl-core"; -import { maxVideoDuration, quality as mq } from "../../config.js"; -import selectQuality from "../../stream/selectQuality.js"; +import { Innertube } from 'youtubei.js'; +import { maxVideoDuration } from '../../config.js'; -export default async function(obj) { - let isAudioOnly = !!obj.isAudioOnly, - infoInitial = await ytdl.getInfo(obj.id); - if (!infoInitial) return { error: 'ErrorCantConnectToServiceAPI' }; +const yt = await Innertube.create(); - let info = infoInitial.formats; - if (info[0]["isLive"]) return { error: 'ErrorLiveVideo' }; - - let videoMatch = [], fullVideoMatch = [], video = [], - audio = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] === obj.format) return true - }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - - if (audio.length === 0) return { error: 'ErrorBadFetch' }; - if (audio[0]["approxDurationMs"] > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; - - if (!isAudioOnly) { - video = info.filter((a) => { - if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] === obj.format) { - if (obj.quality !== "max") { - if (a["hasAudio"] && String(mq[obj.quality]) === String(a["height"])) { - fullVideoMatch.push(a) - } else if (!a["hasAudio"] && String(mq[obj.quality]) === String(a["height"])) { - videoMatch.push(a) - } - } - return true - } - }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - - if (obj.quality !== "max") { - if (videoMatch.length === 0) { - let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim()); - videoMatch = video.filter((a) => { - if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() === String(ss)) return true - }) - } else if (fullVideoMatch.length > 0) { - videoMatch = [fullVideoMatch[0]] - } - } else videoMatch = [video[0]]; - if (obj.quality === "los") videoMatch = [video[video.length - 1]]; +const c = { + h264: { + codec: "avc1", + aCodec: "mp4a", + container: "mp4" + }, + av1: { + codec: "av01", + aCodec: "mp4a", + container: "mp4" + }, + vp9: { + codec: "vp9", + aCodec: "opus", + container: "webm" } - if (video.length === 0) isAudioOnly = true; +} - if (isAudioOnly) { +export default async function(o) { + let info, isDubbed, quality = o.quality === "max" ? "9000" : o.quality; //set quality 9000(p) to be interpreted as max + try { + info = await yt.getBasicInfo(o.id, 'ANDROID'); + } catch (e) { + return { error: 'ErrorCantConnectToServiceAPI' }; + } + + if (!info) return { error: 'ErrorCantConnectToServiceAPI' }; + if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' }; + if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' }; + + let adaptive_formats = info.streaming_data.adaptive_formats.filter((e) => { + if (e["mime_type"].includes(c[o.format].codec) || e["mime_type"].includes(c[o.format].aCodec)) return true + }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); + let bestQuality = adaptive_formats[0]['quality_label'].split('p')[0]; + + let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec)); + let checkBestAudio = (i) => (i["has_audio"] && !i["has_video"]); + let checkBestVideo = (i) => (i['quality_label'].split('p')[0] === bestQuality && !i["has_audio"] && i["has_video"]); + let checkRightVideo = (i) => (i['quality_label'].split('p')[0] === quality && !i["has_audio"] && i["has_video"]); + + if (!o.isAudioOnly && !o.isAudioMuted) { + let single = info.streaming_data.formats.find(i => checkSingle(i)); + if (single) return { + type: "bridge", + urls: single.url, + filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}` + } + }; + + if (info.basic_info.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + let audio = adaptive_formats.find(i => checkBestAudio(i) && i["is_original"]); + if (o.dubLang) { + let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang); + if (dubbedAudio) { + audio = dubbedAudio; + isDubbed = true + } + } + if (o.isAudioOnly) { let r = { type: "render", isAudioOnly: true, - urls: audio[0]["url"], - audioFilename: `youtube_${obj.id}_audio`, + urls: audio.url, + audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`, fileMetadata: { - title: infoInitial.videoDetails.title, - artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(), + title: info.basic_info.title, + artist: info.basic_info.author.replace("- Topic", "").trim(), } - } - if (infoInitial.videoDetails.description) { - let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by"); - if (isAutoGenAudio) { - let descItems = infoInitial.videoDetails.description.split("\n\n") - r.fileMetadata.album = descItems[2] - r.fileMetadata.copyright = descItems[3] - if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim(); - } - } + }; + if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) { + let descItems = info.basic_info.short_description.split("\n\n") + r.fileMetadata.album = descItems[2] + r.fileMetadata.copyright = descItems[3] + if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim(); + }; return r - } - let singleTest; - if (videoMatch.length > 0) { - singleTest = videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]; - return { - type: singleTest ? "bridge" : "render", - urls: singleTest ? videoMatch[0]["url"] : [videoMatch[0]["url"], audio[0]["url"]], - time: videoMatch[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` - } - } - singleTest = video[0]["hasVideo"] && video[0]["hasAudio"]; - return { - type: singleTest ? "bridge" : "render", - urls: singleTest ? video[0]["url"] : [video[0]["url"], audio[0]["url"]], - time: video[0]["approxDurationMs"], - filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${obj.format}` - } + }; + + let video = adaptive_formats.find(i => ((Number(quality) > Number(bestQuality)) ? checkBestVideo(i) : checkRightVideo(i))); + if (video && audio) return { + type: "render", + urls: [video.url, audio.url], + filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}` + }; + + return { error: 'ErrorYTTryOtherCodec' } } diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 98384e1..a12d800 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -4,7 +4,6 @@ "bilibili": { "alias": "bilibili (.com only)", "patterns": ["video/:id"], - "quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"], "enabled": true }, "reddit": { @@ -20,43 +19,12 @@ "vk": { "alias": "vk video & clips", "patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:duplicate?z=clip-:userId_:videoId"], - "quality_match": { - "2160": 7, - "1440": 6, - "1080": 5, - "720": 3, - "480": 2, - "360": 1, - "240": 0, - "144": 4 - }, - "representation_match": { - "2160": 7, - "1440": 6, - "1080": 5, - "720": 4, - "480": 3, - "360": 2, - "240": 1, - "144": 0 - }, - "quality": { - "1080": "hig", - "720": "mid", - "480": "low" - }, "enabled": true }, "youtube": { "alias": "youtube videos & shorts & music", "patterns": ["watch?v=:id"], - "quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"], "bestAudio": "opus", - "quality": { - "1080": "hig", - "720": "mid", - "480": "low" - }, "enabled": true }, "tumblr": { @@ -76,12 +44,6 @@ }, "vimeo": { "patterns": [":id"], - "resolutionMatch": { - "3840": "2160", - "1920": "1080", - "1280": "720", - "960": "480" - }, "enabled": true }, "soundcloud": { diff --git a/src/modules/stream/selectQuality.js b/src/modules/stream/selectQuality.js deleted file mode 100644 index 5624448..0000000 --- a/src/modules/stream/selectQuality.js +++ /dev/null @@ -1,29 +0,0 @@ -import { services, quality as mq } from "../config.js"; - -// TO-DO: remake entirety of this module to be more of how quality picking is done in vimeo module -function closest(goal, array) { - return array.sort().reduce(function (prev, curr) { - return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); - }); -} - -export default function(service, quality, maxQuality) { - if (quality === "max") return maxQuality; - - quality = parseInt(mq[quality], 10) - maxQuality = parseInt(maxQuality, 10) - - if (quality >= maxQuality || quality === maxQuality) return maxQuality; - - if (quality < maxQuality) { - if (!services[service]["quality"][quality]) { - let s = Object.keys(services[service]["quality_match"]).filter((q) => { - if (q <= quality) { - return true - } - }) - return closest(quality, s) - } - return quality - } -} diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 6c7fd69..5ded65b 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -6,8 +6,8 @@ import { metadataManager, msToTime } from "../sub/utils.js"; export function streamDefault(streamInfo, res) { try { - let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1] - let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}` + let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; + let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`; res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`); const stream = got.get(streamInfo.urls, { headers: { @@ -31,26 +31,39 @@ export function streamLiveRender(streamInfo, res) { res.end(); return; } + let audio = got.get(streamInfo.urls[1], { isStream: true }); + let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ '-loglevel', '-8', '-i', streamInfo.urls[0], - '-i', streamInfo.urls[1], + '-i', 'pipe:3', '-map', '0:v', '-map', '1:a', ]; args = args.concat(ffmpegArgs[format]) if (streamInfo.time) args.push('-t', msToTime(streamInfo.time)); - args.push('-f', format, 'pipe:3'); - const ffmpegProcess = spawn(ffmpeg, args, { + args.push('-f', format, 'pipe:4'); + let ffmpegProcess = spawn(ffmpeg, args, { windowsHide: true, stdio: [ 'inherit', 'inherit', 'inherit', - 'pipe' + 'pipe', 'pipe' ], }); res.setHeader('Connection', 'keep-alive'); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`); - ffmpegProcess.stdio[3].pipe(res); + res.on('error', () => { + ffmpegProcess.kill(); + res.end(); + }); + ffmpegProcess.stdio[4].pipe(res).on('error', () => { + ffmpegProcess.kill(); + res.end(); + });; + audio.pipe(ffmpegProcess.stdio[3]).on('error', () => { + ffmpegProcess.kill(); + res.end(); + }); ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); ffmpegProcess.on('close', () => ffmpegProcess.kill()); @@ -73,8 +86,8 @@ export function streamAudioOnly(streamInfo, res) { '-i', streamInfo.urls ] if (streamInfo.metadata) { - if (streamInfo.metadata.cover) { // doesn't work on the server but works locally, no idea why - args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0', '-filter:v', 'scale=w=400:h=400,format=yuvj420p') + if (streamInfo.metadata.cover) { // currently corrupts the audio + args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0') } else { args.push('-vn') } @@ -82,7 +95,6 @@ export function streamAudioOnly(streamInfo, res) { } let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"] args = args.concat(arg) - if (streamInfo.metadata.cover) args.push("-c:v", "mjpeg") if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]); args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); const ffmpegProcess = spawn(ffmpeg, args, { diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 64ccb52..0581863 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -2,11 +2,11 @@ import { createStream } from "../stream/manage.js"; let apiVar = { allowed: { - vFormat: ["mp4", "webm"], - vQuality: ["max", "hig", "mid", "low", "los"], + vCodec: ["h264", "av1", "vp9"], + vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"], aFormat: ["best", "mp3", "ogg", "wav", "opus"] }, - booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted"] + booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang"] } export function apiJSON(type, obj) { @@ -84,8 +84,11 @@ export function cleanURL(url, host) { } return url.slice(0, 128) } +export function verifyLanguageCode(code) { + return RegExp(/[a-z]{2}/).test(String(code.slice(0, 2).toLowerCase())) ? String(code.slice(0, 2).toLowerCase()) : "en" +} export function languageCode(req) { - return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en" + return req.header('Accept-Language') ? verifyLanguageCode(req.header('Accept-Language')) : "en" } export function unicodeDecode(str) { return str.replace(/\\u[\dA-F]{4}/gi, (unicode) => { @@ -94,13 +97,14 @@ export function unicodeDecode(str) { } export function checkJSONPost(obj) { let def = { - vFormat: "mp4", - vQuality: "hig", + vCodec: "h264", + vQuality: "720", aFormat: "mp3", isAudioOnly: false, isNoTTWatermark: false, isTTFullAudio: false, isAudioMuted: false, + dubLang: false } try { let objKeys = Object.keys(obj); @@ -117,6 +121,8 @@ export function checkJSONPost(obj) { } } + if (def.dubLang) def.dubLang = verifyLanguageCode(obj.dubLang); + obj["url"] = decodeURIComponent(String(obj["url"])); let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), host = hostname[hostname.length - 2]; diff --git a/src/test/tests.json b/src/test/tests.json index e493517..db05278 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -246,69 +246,55 @@ } }], "youtube": [{ - "name": "4k video (mp4, hig)", + "name": "4k video (h264, 1440)", "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", "params": { - "vFormat": "mp4", - "vQuality": "hig", - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false + "vCodec": "h264", + "vQuality": "1440" }, "expected": { "code": 200, "status": "stream" } }, { - "name": "4k video (webm, mid)", + "name": "4k video (vp9, 720)", "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", "params": { - "vFormat": "webm", - "vQuality": "mid", - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false + "vCodec": "vp9", + "vQuality": "720" }, "expected": { "code": 200, "status": "stream" } }, { - "name": "4k video (mp4, max)", + "name": "4k video (av1, max)", "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", "params": { - "vFormat": "mp4", + "vCodec": "av1", + "vQuality": "max" + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (h264, 720)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vCodec": "h264", + "vQuality": "720" + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (vp9, max, isAudioMuted)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vCodec": "vp9", "vQuality": "max", - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "4k video (webm, max)", - "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", - "params": { - "vFormat": "webm", - "vQuality": "max", - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "4k video (webm, max, isAudioMuted)", - "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", - "params": { - "vFormat": "webm", - "vQuality": "max", - "aFormat": "mp3", - "isAudioOnly": false, "isAudioMuted": true }, "expected": { @@ -316,10 +302,22 @@ "status": "stream" } }, { - "name": "4k video (mp4, max, isAudioMuted)", + "name": "4k video (h264, max, isAudioMuted)", "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", "params": { - "vFormat": "webm", + "vCodec": "h264", + "vQuality": "max", + "isAudioMuted": true + }, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "4k video (av1, max, isAudioMuted, isAudioOnly, mp3)", + "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", + "params": { + "vCodec": "av1", "vQuality": "max", "aFormat": "mp3", "isAudioOnly": true, @@ -330,24 +328,10 @@ "status": "stream" } }, { - "name": "4k video (mp4, max, isAudioMuted, isAudioOnly, mp3)", + "name": "4k video (av1, max, isAudioMuted, isAudioOnly, best)", "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", "params": { - "vFormat": "webm", - "vQuality": "max", - "aFormat": "mp3", - "isAudioOnly": true, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "4k video (mp4, max, isAudioMuted, isAudioOnly, best)", - "url": "https://www.youtube.com/watch?v=vPwaXytZcgI", - "params": { - "vFormat": "webm", + "vCodec": "av1", "vQuality": "max", "aFormat": "best", "isAudioOnly": true, @@ -407,10 +391,10 @@ "status": "stream" } }, { - "name": "clip, low", + "name": "clip, 360", "url": "https://vk.com/clip-57274055_456239788", "params": { - "vQuality": "low" + "vQuality": "360" }, "expected": { "code": 200, @@ -657,7 +641,7 @@ "name": "4k progressive", "url": "https://vimeo.com/288386543", "params": { - "vQuality": "max" + "vQuality": "2160" }, "expected": { "code": 200, @@ -667,7 +651,7 @@ "name": "720p progressive", "url": "https://vimeo.com/288386543", "params": { - "vQuality": "mid" + "vQuality": "720" }, "expected": { "code": 200, @@ -677,7 +661,7 @@ "name": "1080p dash parcel", "url": "https://vimeo.com/774694040", "params": { - "vQuality": "hig" + "vQuality": "1440" }, "expected": { "code": 200, @@ -687,7 +671,7 @@ "name": "720p dash parcel", "url": "https://vimeo.com/774694040", "params": { - "vQuality": "mid" + "vQuality": "360" }, "expected": { "code": 200,