From 262abde2fcb5a13734d6338be70a9bbf9e988c92 Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:27:39 +0000 Subject: [PATCH] feat(extractor): implement KinoGer extractor --- src/extractor/ExtractorRegistry.ts | 2 + src/extractor/KinoGer.ts | 58 +++++++++++++++++++ .../__snapshots__/KinoGer.test.ts.snap | 44 ++++++++------ .../https:kinoger.reapiv1videoidep1tcf | 1 + .../https:kinoger.reapiv1videoidge5fhb | 1 + .../https:kinoger.reapiv1videoidx6tsx9 | 1 + 6 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 src/extractor/KinoGer.ts create mode 100644 src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidep1tcf create mode 100644 src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidge5fhb create mode 100644 src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidx6tsx9 diff --git a/src/extractor/ExtractorRegistry.ts b/src/extractor/ExtractorRegistry.ts index d6883bd..a3ec959 100644 --- a/src/extractor/ExtractorRegistry.ts +++ b/src/extractor/ExtractorRegistry.ts @@ -7,6 +7,7 @@ import { DoodStream } from './DoodStream'; import { Dropload } from './Dropload'; import { Fsst } from './Fsst'; import { SuperVideo } from './SuperVideo'; +import { KinoGer } from './KinoGer'; import { Soaper } from './Soaper'; import { ExternalUrl } from './ExternalUrl'; import { NotFoundError } from '../error'; @@ -23,6 +24,7 @@ export class ExtractorRegistry { new Dropload(fetcher), new Fsst(fetcher), new SuperVideo(fetcher), + new KinoGer(fetcher), new Soaper(fetcher), new ExternalUrl(fetcher), // fallback extractor which must come last ]; diff --git a/src/extractor/KinoGer.ts b/src/extractor/KinoGer.ts new file mode 100644 index 0000000..618bb79 --- /dev/null +++ b/src/extractor/KinoGer.ts @@ -0,0 +1,58 @@ +import crypto from 'crypto'; +import { Extractor } from './types'; +import { Fetcher, guessFromTitle } from '../utils'; +import { Context, Meta, UrlResult } from '../types'; +import { Decipher } from 'node:crypto'; + +/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/kinoger.py */ +export class KinoGer implements Extractor { + readonly id = 'kinoger'; + + readonly label = 'KinoGer'; + + readonly ttl = 900000; // 15m + + private readonly fetcher: Fetcher; + + private readonly decipher: Decipher; + + constructor(fetcher: Fetcher) { + this.fetcher = fetcher; + + const key = Buffer.from('6b69656d7469656e6d75613931316361', 'hex'); + const iv = Buffer.from('313233343536373839306f6975797472', 'hex'); + this.decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); + } + + readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/kinoger\.re|shiid4u\.upn\.one|moflix\.upns\.xyz|player\.upn\.one|wasuytm\.store|ultrastream\.online/); + + readonly normalize = (url: URL): URL => new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`); + + readonly extract = async (ctx: Context, url: URL, meta: Meta): Promise => { + const hexData = await this.fetcher.text(ctx, url, { headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' } }); + + const encrypted = Buffer.from(hexData, 'hex'); + const decrypted = Buffer.concat([this.decipher.update(encrypted), this.decipher.final()]).toString(); + + const { cf, title } = JSON.parse(decrypted) as { cf: string; title: string }; + + const height = guessFromTitle(title); + + return [ + { + url: new URL(cf), + label: this.label, + sourceId: `${this.id}_${meta.countryCode.toLowerCase()}`, + ttl: this.ttl, + meta: { + ...meta, + title, + ...(height && { height }), + }, + requestHeaders: { + Referer: ctx.referer?.href as string, + }, + }, + ]; + }; +} diff --git a/src/handler/__snapshots__/KinoGer.test.ts.snap b/src/handler/__snapshots__/KinoGer.test.ts.snap index 6881d15..534256e 100644 --- a/src/handler/__snapshots__/KinoGer.test.ts.snap +++ b/src/handler/__snapshots__/KinoGer.test.ts.snap @@ -49,15 +49,17 @@ exports[`KinoGer handle imdb blood and sinners 1`] = ` ], [ { - "isExternal": true, - "label": "kinoger.re", + "label": "KinoGer", "meta": { "countryCode": "de", - "title": "Blood & Sinners (2025)", + "title": "Blood.and.Sinners.2025.mkv", }, - "sourceId": "external_de", - "ttl": 3600000, - "url": "https://kinoger.re/#ge5fhb", + "requestHeaders": { + "Referer": "https://kinoger.com/stream/18809-blood-sinners-stream.html", + }, + "sourceId": "kinoger_de", + "ttl": 900000, + "url": "https://scvo.mazedukhkortfj.sbs/v4/bgm/ge5fhb/cf-master.txt?v=1748766126", }, ], ] @@ -79,15 +81,18 @@ exports[`KinoGer handle imdb dead city s2e5 1`] = ` ], [ { - "isExternal": true, - "label": "kinoger.re", + "label": "KinoGer", "meta": { "countryCode": "de", - "title": "The Walking Dead: Dead City 2x5", + "height": 1080, + "title": "the.walking.dead.dead.city.s02e05.german.dl.1080p.web.x264-wvf.mkv", }, - "sourceId": "external_de", - "ttl": 3600000, - "url": "https://kinoger.re/#x6tsx9", + "requestHeaders": { + "Referer": "https://kinoger.com/stream/12971-the-walking-dead-dead-city-stream.html", + }, + "sourceId": "kinoger_de", + "ttl": 900000, + "url": "https://sskt.amberwels.sbs/v4/vz1/x6tsx9/cf-master.txt?v=1748957184", }, ], ] @@ -129,15 +134,18 @@ exports[`KinoGer handle imdb dead city s2e6 1`] = ` ], [ { - "isExternal": true, - "label": "kinoger.re", + "label": "KinoGer", "meta": { "countryCode": "de", - "title": "The Walking Dead: Dead City 2x6", + "height": 1080, + "title": "the.walking.dead.dead.city.s02e06.german.dl.1080p.web.x264-wvf.mkv", }, - "sourceId": "external_de", - "ttl": 3600000, - "url": "https://kinoger.re/#ep1tcf", + "requestHeaders": { + "Referer": "https://kinoger.com/stream/12971-the-walking-dead-dead-city-stream.html", + }, + "sourceId": "kinoger_de", + "ttl": 900000, + "url": "https://slob.feistjmyrodne.sbs/v4/vz1/ep1tcf/cf-master.txt?v=1749488554", }, ], ] diff --git a/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidep1tcf b/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidep1tcf new file mode 100644 index 0000000..3cde5a4 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidep1tcf @@ -0,0 +1 @@ +2790f7d2c5debd75aa5c1dcf118bed768d89c315096fc8a4575fa85d8bdcea9be0fa2e02d40bbb58929075c2cea9116d4d92c7d90e53b58e585e65406413c01bdb8365fc215de21b5da8c52e7592df299262218066b324d2919269f6870dabe0f462917e7f94e860698bd5334129aa6a125873766eff2a6d7b400f82ed6622a78428ab87ccf6072a9bf7270119e59fe1f5100c671c9420a3e407b250da612a02aab3e23bd8715991256afe040b3741016ff163e64c656b8fabb873760953a71335adc3eab63e5d75f05f96c26a1859346abcca5c5f673c3d1641585df9081e5414663eed84400d144b86884406d0b98b217d30c37224901d736fab81a5f55997a0430cc29e15295525dc8c8ce98c63f1c7b1a25e6db9f1405ad2f8f8dfdb21e7aafb877bb26a82d6fcac38a92c7420daeca11930131b502295debf54eba16b6d709c74d77f35206a8ae8eb5f0b89edae8d118bc60872c4a9f5830d3f5c639fbd789f00fe4d2a4b27a97f674831ece3fcd8776812bca60115c49c206a3d43bd5394d4361a18d55bebb572902535023e29c84da44b21e3dddfaa7659fda6ee3894621f761ba6f156a2ff573b0a3772fdf2d8b064fd353629450f605393ff5a09cb74b95489c012824d8cadaf97806bd57f2d32a2292641299db51f071723b0f35f0569a01cbcb74730987085438e65fc7127c2ed4b7991b08bdf52139a03170dadbea7e0756e3e306e8c8925a8d227133d706eb2c82530087d88a65ab8c36996f86afbd65f959b41c3a8b874079657be9e0ba6e655853a11f03654b5a2d52247596662f9da2824a80382b27a63900f8d537d0352c2f92905a426db2038724efe345be2f688d6c1ac5c0d33bc95dacb3f7b15124e1fdfcd9280fa8cfcb7d0c4418adc935c53b563505e86c9c426950e74f5c551384911d03319efdf8fd2285e87e6d0df69b89ab67dc932714900e0bfbf9fc2c32648feef67ae6b0699c1ced90372e526e26e5ab94b80a59c67ffb1e82723946d837138e357f3e26569c02fb5bcb8328355e795f60ae76a5e81a07da7b90e33c1c985dabd9d51c55d204eb1c3b1cb80399210ad3cd3bd403f753c7faabb1d60994a238a20932c504e97afaf263d2adc36dfd7fa677f629f6719081f7d0e08b6f74a366ae48240047374d60645a734153051af08b2fc465846bdedefff753ddeb8242f973a58a8038190dedf53699ce4a0e7f5a6d0f7ca223387b133aac0fc16bb0c7928f90008200b96259b1e13d612f4349a7117805c48fbe8a027d1deabeda157236bd9f2e48338a2b76a411fde3147bb9dbe878144e7de73b6c0aa7088212cb50b9129e7ce21aed2b2bccd9685581778053c62b51adc6170627f3a80862c1969403362fcc2d742814dcad8fc7a540771a1987fbd35dff46b96d98d7811058f44c7d38a450f1a4fd47bbec76e59c637def9ef7016bd0b94b41c3b74bfb4720ab3699254f05eb3dd7a1bb2ee896d91df936c48585425f8bb2fd5be8fbc74573d5102f93c92e4b4e641418c1dd293647486b18377a972bd0178f503305b0274f985a36af5e6a61ea24c29c7c41370c04a6161d8d4ea223824fe0953a3518eb325ae63e6ceb74c956209038909a64eb549fcdae1ba8c533b71d5229b7b08cadd5a8a0f975b0a7232ee0250852d943ca6af88d0ca5dfb2a8ab17fbdda8e3ce173547fef321bc1e76e9f68d45fef6c6248e4b530fb3c1acb1ac8ad5d931ed34a881f6cb46ce29ada106b2e22883f63725a4ad445cb810b21cfc6fe25b197298dbcf548e4a233b8ee65fb66d04a9101ed5ad32118aace53e8956eac833b0e43b737827c5fe1bc305fe690b652e30d5844fe76e0a56306c27f0b3e7661b28cfa86e96c2c5f9e80946cac2250f1baad645074ca5f492ee1c882eb2b2dc595092e8610174fd8d0cdd3dae605d5fd75acf3368a98718668175bef72e7fe14c4e2932c5dfa6e7a6c9cbda6547ed21a0065ec7fd1ddcb20b1c448eb883bfe783b53d21a445a3048a9930189b158b4fefb88bbb831ca05c9ac57d6ddd6f10c61c87dbe949d371626eb67df0ae0ac0bb7e6abe00a3b43aa692d1af7849dda82c6fdcbf4482420a1eaf4d6e8532d68a41266c8ae8fbd9af9735b41d4bc825041a775325854853b78e1b954dc8d48df11530e7d110b1ca5235e45eacee82a585af44b42c0c715e691da514fd2ce05df565eca66477d6697f409baaf5f42e505095b89f10164dad4af2bb0473d7794720d8035e424f77f9dd666464baf74139b5a8aacfe73eb6743647c6d083c98f52da84ca5e7b25878c87a29556a55c3e4b5685c29e286905d910cbf2cdb1b4fa7d4509cea439018fd028cd53e887b5575947f84e32c572de28d47e01fd07cb2cda65fbd32e452367e5de9c5bcf2f1e3ccfe361ee494c392f9515333597eb0351ed4b3a4e78b71256a0e5781a60df742a0b2610a9b99590de5a04450d50337b33a314b106c7d62b100a510109011d9c8f7eb8a37fd68c0a024eb2b938f44df2ba12410595dcdf3ff27eb9addf69751ac1bb487d3a5d22a350467941f154fcb6b5c72ed55363189ca37f9aba68dabd5a492f359ca916484db7d296045fcf53b6b5309a89b9af1fa561aa6011704db32cfb1b8b67ce523420924cf65f4d42b39ebebf80af6bc6802c17c2fae9f5d7de849cd0795c6925ea159528ed77fb740e5737601135e46573a7f37b8831c50b47bbf2fc1dbac359df656a30190020727da9d845265e12ece5e99eaa90891bb2b714d4650b210ffd653d67abd1074b8cfd15632078a7a55b4142ecc07d3f918779c3aafa8861bf7f425cebdbd267e08ccc35117fe378a05801f2b8f7de3c013d9ef044fcd6d1609c3899588e46d576da164559f349ea2e3b89679de1193632812bc011fc2fea4dfae934a26126722cfe2f6e4e1e9cb599f71fe9162d6aa2177755c2e625e6f3b16da5ad5c02e44358e5f4eea8c307f30670ad80f228027b18ff5e71593795493f65b43f8ad112c7422932d1cbd9f5eadb06f26124d6538b5a041b864251d74a281e84dff9b2df98cd4de3fe37ec0e0762fbaa2c25a7d6c185cafec8a828e7bf95095bacd52aa407d6fd382c50fc76b4a73ba5335bf5003281f0a21d86a95ec943c1d6ad5398481ed52371d8c9653bc9b0d692b9a7f7b73ec9bf37e2a8bcc34164f113771c6c64c8a9149ee9e9ed9e4d8af431bc65371a2a7bc951df12d4d9f7d9cc59c862c9bbf6aa9d8ee6f5220e339611f2b08567fdd8826ab24240ba051d3dc7519a8a3f020cbfce1dafab1fe3ef330d5bbb5233a415f4d823fa91dcde753ed85277b03df99bdec3b4a1987a17e2a0ec3de74225c04fc7e180d46d376b06f95ca4a8467b8083871a657d827234c5e3c57bd4938ae49aef844d159426a5c189d0f50bc8a215824b70049095fab823221d058cf01ca69cdb81171f9efb86ee2cf72fcaa12c0351684a7ccc05705fb1a3e96d740 diff --git a/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidge5fhb b/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidge5fhb new file mode 100644 index 0000000..836e5df --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidge5fhb @@ -0,0 +1 @@ +801b1b9b443cfe022a6f1deeee34d9576d9e3d9cf827ffd683db99588ea07491dfdab62662dec76e725e7535024b23fedea22a664744d89225576238f5cd2ecc754cea35c8328b0450376c8e2fee52e2ad4d0975675a38b3e0d908976512a00aa07ca464cf084371f8db0ffebe3d8750c228f95185bedbf6f0ae2a05bc09923ae2ab6e27eae235f9cd5e09bffdf3012de2665c02d8c2e6048301327fca26ee835c58c0a63239fb0153dac5d7033cee2343788a18258702015dfe3c7b908d9f3bb720f82844979ee6a053186fc681622393bb8ba762012ffd004ab27585123b6294b09b3cb213a9fad0f9cf2e188f96c7334811b0ce4c3b774921a5bbc72986593e18b0e9db687b89b3cc81dd075a86e83547d21de8ac294de391120d832b0325e51e07452e2a4f29dc429a4a5793f10931ffb93ce7672b7fb285888ad4e80ed134fa213ec202b3adfc6257a1cf3cf34556868b71d486f8eb1bf751a41c3c0ad04c05d9fb4fc2a25c1c16923a0a9e56d07734feeb64dbf9cc9569e1a4324d0004aa9f09ea56b4e9adb176d0cc7e478de7161daad85b1ba43f2ffec2bbdeed5c3270d9c66794a3628ec4c537f08551453e9a69405c4c73ea0760a9335ff8baedfe4b297a6aeb0c12debcbf352ee13131f24d229c7c3c03b1fb87e8136c75a67b292b1fe6fcaf5dbcb4584688e30edab22cb4c5ad62e1f5aedd35c2e72727296126f05579d6c664b0cec3875db448926d6ae89a6a75f4282715a13c78e3dd8fc12279abbb9833fea44b180262d0f83662569eda01202ed1e0a528791f05f41b802f1bbe371335f7358f61891be4c4f5e078a40650a4c871b83301d19cdf00d8625b1d470af9ad677fb67cd581e326b25dbabb26778e73658944aa194cf6d5a5bf9f8739deefa51c8b3ed396486dfd27655367ba94520f18387afb834a7fc0d943db68cef2bccf81b51749429534b854464201c136bd582b9d79adb92caed9ebadc16b112d566fbddb322221c51adbb468369a96c9c17fe8920e43893da6c57bba527d40d3089cfdd50bd8eb18c70c0d0455e0af73144d69a847d7e9f12c4387c67ce34586355eb62b26070de623d975740dfd1b042300028e951cd802d40122c342cd1bbb91bf3c45a3229b7115cb043e54af430d4168444a5292062cf01490ff7016cfab2dbad5cc701f95d7a0a1b606967ab90b3ef495bf154f5fa87c16e1727953cd6ccd72db5bdae278e109921bafdd7285a23ff83f195309f273135d624514b33b8a8ec8b0e0f828552405aa9927f00f9ff3a117899c227453bef351ec0cd6ae26fbed3aba434cdfe8baeb368006a42d2f36be95418eca78ef9660681598de186e6b92845471a1c074e772ce16175b5bcc8bbd70c640d6c482febdd71e4f28fca9da888f3cb2efa2b455dd56b9597d77ca1573f3f90f700c6bf8b1d9ae5ddfc39795b145338e7855c530e4240484c227281f345444f15df0c15d37a4bdec24033a7d3c576bf77f87d40730574a9436ef89f365faf99ad8d1d5cefe98dda053d5d54bb976f3c8dd1ecf7bf9bd5a3e8d8f3a5cd4bd2ba8a56f0b1e9cfaccbae4f0d13888ef3f6fe880fd886fa2744289adc527165a56144bab9ae4d77b1d2525323f2d3673bf8d0d58608e274425ccd3a79c68752008ffe6799e1bafae904241ed84dc2e10f83f4837179415b44c9b3b48d87801863452bde7c4c9256f20c6a5f7c19e220d6790b4d064d77e37748519e0112e88b9cff9ce97a66abe709a84039d26b17cb9226b87462b2575c1a8b484beb05d0636085e1a57ac38fce572dc7957c75ca3b568dfba5cc117f26ca6a3c3b96d7d9479cf44a4d866474dd9cc4334f7807a82327dd918bfceb27e13d826d803606bed18efd2b0f8eeaf2ab9c6966f0e083607cfa66e1c1cb8b99dc593f99657ffe5f0c075cf63b1bad6acf0265d97e6dff31b531c92e6d9f4b5657817f72348d4dc4b7405a4928488bae83c47eb23776b7c1e33c546ad54b45d7ad7d99e63bfcaa30c30ce57c4b1cf6ba5d6cf3ac69df76a0229bc24a18f8293ce6d1e676061e5a3f4e5483ea494a04eaea5351f00fa25952e2c45878f4f8c442c282c80ae874392b87f52b0cd8b0a3e77374cf0de8e4ce99ef179b12ae87a6f36914778952ff745c8cbb16610cef7d6870d76ef43ff5f0618e218860832a3d203d2094716bd1c6ea985645ba26fb3238ea6d12adba71f0b191f9bfcc47d6f3be10271becac8d10a9b41ea224c71eda66e6feb69d3306fca11c4b8472d65f0639fc5a00530320462230cee852da03812536762cbc9c0249fe6521d4627db6df4f39ff61248fbcfad18e7401d6c5e589013da47480cc62f8d1d0029ed6b88716a46b5aef1d289d693433eeb942d63ea5ea4aa8b5d89bd2b8d66097ac5aed5ad2f484b231654b0b4b11c99b210b927f29dbda18ee92a8dc5a37691ba14535f760d8a0be2f3fddd448ffdc20935eb9a9fd4088bf868f62dfc3edc07bb8649fb15c76992fae6039bdf19b16bffc567141abc4d03f9f3bedb0170df75d84a2f7b604cc4e14e6bcfb2daea33ecea3b842fe01c3107f0f92d6aa1874f301488edf6fabdaedef027bb8ccb9b5c91a092a7df245ed793aac536791ed7290485ec51898d103fa237ef424f11807151bbfd87cb7a10fd75b57fd172c23607a6e086d4cff226a333d7065f9bf540578ab44cfca35cc1ef74d6c40b940e4c793e27c13aa89cc7dab39cef172de8eba6f54f54abb517809bf0bb66e78feaf0cc7a071a51ae52097bdc7984d02f4b0426f7661ab45809f25dc6f1a5b503d803c656567417ee4f2cf27375a8f646cc0c752c65366dea2d2f95e4e1e5e59dd7fb6c0841f230d9419340551539ad3307f1a418e651efaf9eec0f0cf20785d96887b31399794826f0ba81d589fc1b14c93d26cdc1618f7084880db8affb447637cfc18684ac28d433ec513a3a9fd92ecf9edcf6d79f256ee4745d353c8e4ddf9996a086618095010bd7e570e9bfb563049a8a7528ae1a33e47b3ce5c3b0671b3a9756cc08166bbedb930f5323157bbc12ec7b096dea9dd9bb07aecc168f1931d4210a2605ae2b336e33884193f4464c0447969571ee4addfcb2bc4295c5f38ac4d414539ddcfa15b1f595ee88e74a80cf1e53642decf3a02b91093e9d14cab215139f6f0dd9cdaf5c0b46db151bcdd848daa2b25dfcd97ec108852e89dd72e74abef9617d0667cf33de77f1268d0456b2ce22753b576f5b057ce29ca2109a0e2bf5c96a9f2219c7a85bd7b444f28e6f6a827d3fcbb356621a4ecde92afcfed88dbc6a579459c7eb8235d819b21ede9e513226a9b64de84c8b9ed4c3605e1c diff --git a/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidx6tsx9 b/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidx6tsx9 new file mode 100644 index 0000000..4a964ea --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:kinoger.reapiv1videoidx6tsx9 @@ -0,0 +1 @@ +2790f7d2c5debd75aa5c1dcf118bed76977219942b5e4a54b75399b9258be0fa5c063a6ae686b7a92ed2504a8eb2f01784fa7a58dfe9fafaa059e7216d25359e25e28535f5421947f74aa6fdc3dccd5f046818f33c0b3f2a5263b886efbdfdef601c7de2cc4702d0e0db79dc4c00f13d542af67c3f4e8e9b4ab8a8acb9efe50e37d85b484230cbd55d95f4ba0f0ea8a1fb9237e4bc446ae28b8328be2555e5799ed0c51bbe42794418b60786b0ca2c9b5e275ce6fa0c913d234c9af2b6876b37bc781238c3ba102884b112088b08ce8c94a13bbb1cc93d67c6e7042134365e0bc5e371de7159e8c04e28de726dc68f38a3d610f93e1cebeac5e18bff097b2cd551e94c25333f39f73efa23399a70f91deef0c9cbb32e0fe1ad693835436326e60c7cd827db430c5cf8b977d28b800a7b90f55b3ec989637d952dae51cc5be4f497604438c48c5e40645bf60a84ff7a377e83eae3c0d47b11628a975ec24fc72444b3c0baf07be5be0249956c9775c6e759c75a6cee463a20a4fae691b560db9b2ca956834c057f751c475e454d0b26ffcc9b72b0604990fad12899424bc84fcb650c8856645da5522c92cdbd1150c570d2ec67a1f8ba1c43b36feae916311e182e63e4914db194913d6299c42ac4dd90fc62d1c8da857a8dfdb17b4e48f9a1734c4e42f826ee31c6d5064964f018a94c8cccd123f2762e212ba7963d34b5ed8023ec7761ddde25eb0a1b9da05bd79c028752f8d875c25b42bc955b8142f56dc750db2c4ba2b69bb15f4a9f8da29156630a74edd239603b0e3c7cdddf70a5182bcd127b973f4e244f8eee4e9327224130ded11f5b009e1337fe8b477d79ffd32f82265174711b5a9abe4b76897ce5f48c87be1c0f56d07ac9aad42d14064fff84089418765eeeb5b136e524a8415cbfdb497c6ec0059fd0170430ec292ca3ea2e6fad0574bfa258a25e507a6ce7b3afda9d52cbcbd365c70a313ac23af5549adfd87f18b8309652f3675eb7402558d2d7d3996b8818d5f6e46ccf7c4122c97aea177c03ddef58f43f9b32858663708943f5cb62f80191e45df1a9203243c9bc89806a95d547606bacd37cf7ab9bcf31e714e77f89a01379372f581ff31f245ecb6665ffa44153eea064e1e478a037f878b66a1ca7ee2d895181b3137f32368abc0436903d9b8365bbdcc381377b04c70d43ceb5512c717481f7e92baf060b27dba82fc770482f3b71e5a405161b8d4e20fa9947c1c8872ee3bff121072b3ec9995e51bd1a4c4cd6719ac70a404f23844c9b5555d5382e36ce296b3183cc4bc3aa272e4f99fd50a4abbc979ae8937e8e3eb8c9a1bacaf8842b0582785b4b2d1ab1d03994c8774e7153a48fa5f036c21bd17f4541f45b83b97a0a06d04d9527497ef2135baa75197f352ee7665191baa5930c4c7046f1db6901e901805bd97b6ae1c069cf855a10cffe3e731aa96bcd8ca0600cbdfba45692736c162d7ab93f7be9b0b2b2ad176d02674a3e0ee80682eda99f602c0aa537a98c2d3f283aa7db19913df6e3492e387c3d8dc853ccac19925ee60465df670d8b02fab8ee3a7a0fd0acf4f7a405cf6a115af2f0e0932fad535edb6dd8b2ea0d18929b914f809ab35ba527c0f80a067d3be59617047e1445635b6fac39697a445262a97033078ff29e6a998db67178ae998a387eeddab601b51e7cfc974b3f219162e1000f77dc066e2425a44c93a898cdf76ddfd113c741e759709b5a0c543c01e277e9601d2673186c70ac11c46fba4b24cd31ed52ee2e8fa75ddad0439479ba5252d4d610373adf8ab839d09f373735625879f14f243c03f817760380eaf9f64c115c25b8b18256399f59daa042d6398a9c04efddc636c75d7256286a7fd717f86f867d69c7c167ba03a2f9dcacc4b1f03c20567d6142dc60aeeea63a71366cef83a604797654d2a496a12d300965369f967c049fee46a5536036f97ffb7a0b3ca74a66991aaad170682fe72aaf2d8ed60e026e489d9b8f03f4f2a762626b2bdb41c78fdde6065ac2baf1c966e5cae2bcea93172a675ca4694a3352d461a34af1d0ae3e04fc5c4f8d44d2433b442e0ad01c7c0b6c2bbc91e58bd3e67682009311386f2a176014c6d37c6104547d9e70c41afd1eb24735c750f947ed6d37ffb8b36873e096867c96f518d062e5649e821733c679f61c9f198a6e7211f8b88d102c22d18ff07bd542cff6f393133ea690fedeff2cb49c05dab51da95c7d7bab4f226a229792bda02a3dac8ee3febaac491b608abb26d3b50250c8dadb4a847f45487aee8cb7bbc523ec0a1cfd6a0daa3454a2e45e34d1d4325927bc3b8784f7204ccd1cd5b0566fe47a770053145a2fac9af2c33aad1d95d81367ac8d09fc512f71ec9b1ea7e5ee8764e8c95d99a539ff6fa7707228996094656eeffdf04d3fe2a487798677ad6a889ef534d5fcd9da51d4f68979cdd92db464c86239bd553331bf7c9445f4c20a67f86fb06360c5b6b8c48f6bdde41d8e3668a58a0169f529884e4882f3595e3aa10a9da1f1868aed3cf81b0dad58f7d549a6697b70468c7236aa110ee97b5e4dfc117313afe43bad24fa05874bb50413cb8f063dd4b98cb54bd612041cfd51d7d6c9ac0ae140071c35b807472769afccdc6369725b5ec773a2de93600c67eef33c9a3a34fe760c4303c6554c547cc94d4f62fe473eb883c7405ef56c704443f8862293e3f2f93ba91103eff5eda1d929e99cee6370cd85668090cc8728d8c358673f7ff0e994ba52bbaa19e3e140f3e12ced069f50f2a679e4cbf0f9fe4e82ee6ef3290482cb0286a6d6eac9604baa0a4f151757fb56ac2c0172c71242a9c68a3b8637396b03223539c7e5d1a36130edf779b75fb8b44339eb11c3c5fd9630fc6e1b8f52b7d9516c343379aac46a985a77cd7dc6b8de2389d36107a2fd8ebdf6ce3398e89d6358ecf6a4ab3ddbb152c1c088135236dee24a6685ea378d75f8dfec938fb4d2de88203bba44a953c7e90d8917b8c6266d4bd628cc52204bff1f1a494610986c30b9a2e5e2ebc0fcb652b764f3bc48705020ba5d8e601669ff9ce6fa7ae66d6cb9d875ecff7697e3213630541f0115732a144646e6d38612b5bc34c86acebcba75292dd962f80c3607c49cea78b8cab3bddbd059e74cba91581b18aa94267ff4eae6ba276924379c261d56ef13bdce7cef94c0ab0d1d0e6147bfa2cdb124f97045040a97a58ca9478ffa036d9736818cbfdb6c8093c5f37a148ec23110d1fac4cc9b8e2294aa3cf87693e3669e269f43cd7f6160142d316159d42427f52c6bfadde8ee847198185f14f1b2c257ab0d86e4e1c5fa994d511dcc974bfc1ccff4b7f3ac84a2baa0796bb8898bf49df74432b37b926326f041d01604f7396667e15f88d802454c16dae583164e858bb879d6dd752ddb1a3451b3ddf092aaa1dc5e6ccc6c72a4f311cac2fbccae678ba13f44751417652c788086eb73b5d73d