From 360e342bc2b13d672e76bc8fafe27f000b499d94 Mon Sep 17 00:00:00 2001 From: Zotify Date: Sat, 17 Feb 2024 17:59:23 +1300 Subject: [PATCH] changes --- CHANGELOG.md | 11 +- LICENCE | 2 +- Pipfile | 18 ++ Pipfile.lock | 414 ++++++++++++++++++++++++++++ README.md | 59 ++-- assets/banner.png | Bin 0 -> 112613 bytes requirements_dev.txt | 1 + setup.cfg | 9 +- zotify/__init__.py | 110 ++++---- zotify/__main__.py | 7 +- zotify/app.py | 444 ++++++++++++------------------- zotify/collections.py | 95 +++++++ zotify/config.py | 26 +- zotify/file.py | 3 +- zotify/loader.py | 8 +- zotify/{printer.py => logger.py} | 14 +- zotify/playable.py | 80 +++--- zotify/utils.py | 85 +++--- 18 files changed, 923 insertions(+), 463 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 assets/banner.png create mode 100644 zotify/collections.py rename zotify/{printer.py => logger.py} (85%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 788a032..b1830b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,6 @@ ## v1.0.0 -An unexpected reboot. - ### BREAKING CHANGES AHEAD - Most components have been completely rewritten to address some fundamental design issues with the previous codebase, This update will provide a better base for new features in the future. @@ -12,7 +10,7 @@ An unexpected reboot. ### Changes -- Genre metadata available for tracks downloaded from an album +- Genre metadata available for all tracks - Boolean command line options are now set like `--save-metadata` or `--no-save-metadata` for True or False - Setting `--config` (formerly `--config-location`) can be set to "None" to not use any config file - Search result selector now accepts both comma-seperated and hyphen-seperated values at the same time @@ -24,10 +22,12 @@ An unexpected reboot. - The output template used is now based on track info rather than search result category - Search queries with spaces no longer need to be in quotes - File metadata no longer uses sanitized file metadata, this will result in more accurate metadata. -- Replaced ffmpy with custom implementation +- Replaced ffmpy with custom implementation providing more tags +- Fixed artist download missing some tracks ### Additions +- New library location for playlists `playlist_library` - Added new command line arguments - `--library`/`-l` overrides both `music_library` and `podcast_library` options similar to `--output`/`-o` - `--category`/`-c` will limit search results to a certain type, accepted values are "album", "artist", "playlist", "track", "show", "episode". Accepts multiple choices. @@ -52,13 +52,13 @@ An unexpected reboot. - `{album_artist}` - `{album_artists}` - `{duration}` (milliseconds) + - `{explicit}` - `{isrc}` - `{licensor}` - `{popularity}` - `{release_date}` - `{track_number}` - Genre information is now more accurate and is always enabled -- New library location for playlists `playlist_library` - Added download option for "liked episodes" `--liked-episodes`/`-le` - Added `save_metadata` option to fully disable writing track metadata - Added support for ReplayGain @@ -79,6 +79,7 @@ An unexpected reboot. - Removed `print_api_errors` because API errors are now treated like regular errors - Removed the following config options due to their corresponding features being removed: - `bulk_wait_time` + - `chunk_size` - `download_real_time` - `md_allgenres` - `md_genredelimiter` diff --git a/LICENCE b/LICENCE index d3ba069..c012b87 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright (c) 2022 Zotify Contributors +Copyright (c) 2024 Zotify Contributors This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..2fc6f0d --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +librespot = {git = "git+https://github.com/kokarare1212/librespot-python"} +music-tag = {git = "git+https://zotify.xyz/zotify/music-tag"} +mutagen = "*" +pillow = "*" +pwinput = "*" +requests = "*" +tqdm = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..4eb010d --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,414 @@ +{ + "_meta": { + "hash": { + "sha256": "dfbc5e27f802eeeddf2967a8d8d280346f8e3b4e4759b4bea10f59dbee08a0ee" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.2.2" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.7.1" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "ifaddr": { + "hashes": [ + "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", + "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4" + ], + "version": "==0.2.0" + }, + "librespot": { + "git": "git+https://github.com/kokarare1212/librespot-python", + "ref": "f56533f9b56e62b28bac6c57d0710620aeb6a5dd" + }, + "music-tag": { + "git": "git+https://zotify.xyz/zotify/music-tag", + "ref": "5c73ddf11a6d65d6575c0e1bb8cce8413f46a433" + }, + "mutagen": { + "hashes": [ + "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", + "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.47.0" + }, + "pillow": { + "hashes": [ + "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", + "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", + "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", + "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", + "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", + "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", + "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", + "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", + "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", + "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", + "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", + "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", + "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", + "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", + "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", + "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", + "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", + "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", + "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", + "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", + "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", + "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", + "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", + "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", + "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", + "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", + "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", + "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", + "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", + "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", + "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", + "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", + "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", + "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", + "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", + "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", + "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", + "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", + "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", + "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", + "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", + "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", + "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", + "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", + "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", + "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", + "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", + "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", + "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", + "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", + "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", + "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", + "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", + "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", + "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", + "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", + "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", + "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", + "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", + "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", + "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", + "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", + "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", + "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", + "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", + "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", + "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", + "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==10.2.0" + }, + "protobuf": { + "hashes": [ + "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf", + "sha256:097c5d8a9808302fb0da7e20edf0b8d4703274d140fd25c5edabddcde43e081f", + "sha256:284f86a6207c897542d7e956eb243a36bb8f9564c1742b253462386e96c6b78f", + "sha256:32ca378605b41fd180dfe4e14d3226386d8d1b002ab31c969c366549e66a2bb7", + "sha256:3cc797c9d15d7689ed507b165cd05913acb992d78b379f6014e013f9ecb20996", + "sha256:62f1b5c4cd6c5402b4e2d63804ba49a327e0c386c99b1675c8a0fefda23b2067", + "sha256:69ccfdf3657ba59569c64295b7d51325f91af586f8d5793b734260dfe2e94e2c", + "sha256:6f50601512a3d23625d8a85b1638d914a0970f17920ff39cec63aaef80a93fb7", + "sha256:7403941f6d0992d40161aa8bb23e12575637008a5a02283a930addc0508982f9", + "sha256:755f3aee41354ae395e104d62119cb223339a8f3276a0cd009ffabfcdd46bb0c", + "sha256:77053d28427a29987ca9caf7b72ccafee011257561259faba8dd308fda9a8739", + "sha256:7e371f10abe57cee5021797126c93479f59fccc9693dafd6bd5633ab67808a91", + "sha256:9016d01c91e8e625141d24ec1b20fed584703e527d28512aa8c8707f105a683c", + "sha256:9be73ad47579abc26c12024239d3540e6b765182a91dbc88e23658ab71767153", + "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9", + "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388", + "sha256:af0ebadc74e281a517141daad9d0f2c5d93ab78e9d455113719a45a49da9db4e", + "sha256:cb29edb9eab15742d791e1025dd7b6a8f6fcb53802ad2f6e3adcb102051063ab", + "sha256:cd68be2559e2a3b84f517fb029ee611546f7812b1fdd0aa2ecc9bc6ec0e4fdde", + "sha256:cdee09140e1cd184ba9324ec1df410e7147242b94b5f8b0c64fc89e38a8ba531", + "sha256:db977c4ca738dd9ce508557d4fce0f5aebd105e158c725beec86feb1f6bc20d8", + "sha256:dd5789b2948ca702c17027c84c2accb552fc30f4622a98ab5c51fcfe8c50d3e7", + "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20", + "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3" + ], + "markers": "python_version >= '3.7'", + "version": "==3.20.1" + }, + "pwinput": { + "hashes": [ + "sha256:ca1a8bd06e28872d751dbd4132d8637127c25b408ea3a349377314a5491426f3" + ], + "index": "pypi", + "version": "==1.0.3" + }, + "pycryptodomex": { + "hashes": [ + "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1", + "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305", + "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c", + "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458", + "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed", + "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc", + "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c", + "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc", + "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079", + "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb", + "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa", + "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427", + "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5", + "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64", + "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6", + "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e", + "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43", + "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3", + "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499", + "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8", + "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b", + "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623", + "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7", + "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc", + "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4", + "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e", + "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a", + "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781", + "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794", + "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea", + "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b", + "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.20.0" + }, + "pyogg": { + "hashes": [ + "sha256:40f79b288b3a667309890885f4cf53371163b7dae17eb17567fb24ab467eca26", + "sha256:794db340fb5833afb4f493b40f91e3e0f594606fd4b31aea0ebf5be2de9da964", + "sha256:8294b34aa59c90200c4630c2cc4a5b84407209141e8e5d069d7a5be358e94262" + ], + "version": "==0.6.14a1" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "tqdm": { + "hashes": [ + "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386", + "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.66.1" + }, + "urllib3": { + "hashes": [ + "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", + "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.0" + }, + "websocket-client": { + "hashes": [ + "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6", + "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588" + ], + "markers": "python_version >= '3.8'", + "version": "==1.7.0" + }, + "zeroconf": { + "hashes": [ + "sha256:0251034ed1d57eeb4e08782b22cc51e2455da7552b592bfad69a5761e69241c7", + "sha256:02e3b6d1c1df87e8bc450de3f973ab9f4cfd1b4c0a3fb9e933d84580a1d61263", + "sha256:08eb87b0500ddc7c148fe3db3913e9d07d5495d756d7d75683f2dee8d7a09dc5", + "sha256:10e8d23cee434077a10ceec4b419b9de8c84ede7f42b64e735d0f0b7708b0c66", + "sha256:14f0bef6b4f7bd0caf80f207acd1e399e8d8a37e12266d80871a2ed6c9ee3b16", + "sha256:18ff5b28e8935e5399fe47ece323e15816bc2ea4111417c41fc09726ff056cd2", + "sha256:194cf1465a756c3090e23ef2a5bd3341caa8d36eef486054daa8e532a4e24ac8", + "sha256:1a57e0c4a94276ec690d2ecf1edeea158aaa3a7f38721af6fa572776dda6c8ad", + "sha256:2389e3a61e99bf74796da7ebc3001b90ecd4e6286f392892b1211748e5b19853", + "sha256:24b0a46c5f697cd6a0b27678ea65a3222b95f1804be6b38c6f5f1a7ce8b5cded", + "sha256:28d906fc0779badb2183f5b20dbcc7e508cce53a13e63ba4d9477381c9f77463", + "sha256:2907784c8c88795bf1b74cc9b6a4051e37a519ae2caaa7307787d466bc57884c", + "sha256:34c3379d899361cd9d6b573ea9ac1eba53e2306eb28f94353b58c4703f0e74ae", + "sha256:3768ab13a8d7f0df85e40e766edd9e2aef28710a350dc4b15e1f2c5dd1326f00", + "sha256:38bfd08c9191716d65e6ac52741442ee918bfe2db43993aa4d3b365966c0ab48", + "sha256:3a49aaff22bc576680b4bcb3c7de896587f6ab4adaa788bedbc468dd0ad28cce", + "sha256:3b167b9e47f3fec8cc28a8f73a9e47c563ceb6681c16dcbe2c7d41e084cee755", + "sha256:3bc16228495e67ec990668970e815b341160258178c21b7716400c5e7a78976a", + "sha256:3f49ec4e8d5bd860e9958e88e8b312e31828f5cb2203039390c551f3fb0b45dd", + "sha256:434344df3037df08bad7422d5d36a415f30ddcc29ac1ad0cc0160b4976b782b5", + "sha256:4713e5cd986f9467494e5b47b0149ac0ffd7ad630d78cd6f6d2555b199e5a653", + "sha256:4865ef65b7eb7eee1a38c05bf7e91dd8182ef2afb1add65440f99e8dd43836d2", + "sha256:52b65e5eeacae121695bcea347cc9ad7da5556afcd3765c461e652ca3e8a84e9", + "sha256:551c04799325c890f2baa347e82cd2c3fb1d01b14940d7695f27c49cd2413b0c", + "sha256:5d777b177cb472f7996b9d696b81337bfb846dbe454b8a34a8e33704d3a435b0", + "sha256:6a041468c428622798193f0006831237aa749ee23e26b5b79e457618484457ef", + "sha256:6c55a1627290ba0718022fb63cf5a25d773c52b00319ef474dd443ebe92efab1", + "sha256:7c4235f45defd43bb2402ff8d3c7ff5d740e671bfd926852541c282ebef992bc", + "sha256:8642d374481d8cc7be9e364b82bcd11bda4a095c24c5f9f5754017a118496b77", + "sha256:90c431e99192a044a5e0217afd7ca0ca9824af93190332e6f7baf4da5375f331", + "sha256:9a7f3b9a580af6bf74a7c435b80925dfeb065c987dffaf4d957d578366a80b2c", + "sha256:9dfa3d8827efffebec61b108162eeb76b0fe170a8379f9838be441f61b4557fd", + "sha256:a3f1d959e3a57afa6b383eb880048929473507b1cc0e8b5e1a72ddf0fc1bbb77", + "sha256:a613827f97ca49e2b4b6d6eb7e61a0485afe23447978a60f42b981a45c2b25fd", + "sha256:a984c93aa413a594f048ef7166f0d9be73b0cd16dfab1395771b7c0607e07817", + "sha256:b843d5e2d2e576efeab59e382907bca1302f20eb33ee1a0a485e90d017b1088a", + "sha256:bdb1a2a67e34059e69aaead600525e91c126c46502ada1c7fc3d2c082cc8ad27", + "sha256:bf9ec50ffdf4e179c035f96a106a5c510d5295c5fb7e2e69dd4cda7b7f42f8bf", + "sha256:c10158396d6875f790bfb5600391d44edcbf52ac4d148e19baab3e8bb7825f76", + "sha256:c3f0f87e47e4d5a9bcfcfc1ce29d0e9127a5cab63e839cc6f845c563f29d765c", + "sha256:c75bb2c1e472723067c7ec986ea510350c335bf8e73ad12617fc6a9ec765dc4b", + "sha256:cb2879708357cac9805d20944973f3d50b472c703b8eaadd9bf136024c5539b4", + "sha256:cc7a76103b03f47d2aa02206f74cc8b2120f4bac02936ccee5d6f29290f5bde5", + "sha256:ce67d8dab4d88bcd1e5975d08235590fc5b9f31b2e2b7993ee1680810e67e56d", + "sha256:d08170123f5c04480bd7a82122b46c5afdb91553a9cef7d686d3fb9c369a9204", + "sha256:d4baa0450b9b0f1bd8acc25c2970d4e49e54726cbc437b81ffb65e5ffb6bd321", + "sha256:d5d92987c3669edbfa9f911a8ef1c46cfd2c3e51971fc80c215f99212b81d4b1", + "sha256:e0d1357940b590466bc72ac605e6ad3f7f05b2e1475b6896ec8e4c61e4d23034", + "sha256:e7d51df61579862414ac544f2892ea3c91a6b45dd728d4fb6260d65bf6f1ef0f", + "sha256:f74149a22a6a27e4c039f6477188dcbcb910acd60529dab5c114ff6265d40ba7", + "sha256:fdcb9cb0555c7947f29a4d5c05c98e260a04f37d6af31aede1e981bf1bdf8691" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.131.0" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 923e565..a50d527 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ -# STILL IN DEVELOPMENT, NOT RECOMMENDED FOR GENERAL USE! - -![Logo banner](https://s1.fileditch.ch/hOwJhfeCFEsYFRWUWaz.png) +![Logo banner](./assets/banner.png) # Zotify A customizable music and podcast downloader. \ Formerly ZSp‌otify. -Available on [zotify.xyz](https://zotify.xyz/zotify/zotify) and [GitHub](https://github.com/zotify-dev/zotify). +Available on [zotify.xyz](https://zotify.xyz/zotify/zotify) and [GitHub](https://github.com/zotify-dev/zotify). \ +Built on [Librespot](https://github.com/kokarare1212/librespot-python). ## Features @@ -48,23 +47,23 @@ Downloads specified items. Accepts any combination of track, album, playlist, ep
All configuration options -| Config key | Command line argument | Description | -| ----------------------- | ------------------------- | --------------------------------------------------- | -| path_credentials | --path-credentials | Path to credentials file | -| path_archive | --path-archive | Path to track archive file | -| music_library | --music-library | Path to root of music library | -| podcast_library | --podcast-library | Path to root of podcast library | -| mixed_playlist_library | --mixed-playlist-library | Path to root of mixed content playlist library | -| output_album | --output-album | File layout for saved albums | -| output_playlist_track | --output-playlist-track | File layout for tracks in a playlist | -| output_playlist_episode | --output-playlist-episode | File layout for episodes in a playlist | -| output_podcast | --output-podcast | File layout for saved podcasts | -| download_quality | --download-quality | Audio download quality (auto for highest available) | -| audio_format | --audio-format | Audio format of final track output | -| transcode_bitrate | --transcode-bitrate | Transcoding bitrate (-1 to use download rate) | -| ffmpeg_path | --ffmpeg-path | Path to ffmpeg binary | -| ffmpeg_args | --ffmpeg-args | Additional ffmpeg arguments when transcoding | -| save_credentials | --save-credentials | Save login credentials to a file | +| Config key | Command line argument | Description | Default | +| ----------------------- | ------------------------- | --------------------------------------------------- | ---------------------------------------------------------- | +| path_credentials | --path-credentials | Path to credentials file | | +| path_archive | --path-archive | Path to track archive file | | +| music_library | --music-library | Path to root of music library | | +| podcast_library | --podcast-library | Path to root of podcast library | | +| mixed_playlist_library | --mixed-playlist-library | Path to root of mixed content playlist library | | +| output_album | --output-album | File layout for saved albums | {album_artist}/{album}/{track_number}. {artists} - {title} | +| output_playlist_track | --output-playlist-track | File layout for tracks in a playlist | {playlist}/{playlist_number}. {artists} - {title} | +| output_playlist_episode | --output-playlist-episode | File layout for episodes in a playlist | {playlist}/{playlist_number}. {episode_number} - {title} | +| output_podcast | --output-podcast | File layout for saved podcasts | {podcast}/{episode_number} - {title} | +| download_quality | --download-quality | Audio download quality (auto for highest available) | | +| audio_format | --audio-format | Audio format of final track output | | +| transcode_bitrate | --transcode-bitrate | Transcoding bitrate (-1 to use download rate) | | +| ffmpeg_path | --ffmpeg-path | Path to ffmpeg binary | | +| ffmpeg_args | --ffmpeg-args | Additional ffmpeg arguments when transcoding | | +| save_credentials | --save-credentials | Save login credentials to a file | | | save_subtitles | --save-subtitles | | save_artist_genres | --save-arist-genres | @@ -91,9 +90,9 @@ Zotify can be used as a user-friendly library for saving music, podcasts, lyrics Here's a very simple example of downloading a track and its metadata: ```python -import zotify +from zotify import Session -session = zotify.Session.from_userpass(username="username", password="password") +session = Session.from_userpass(username="username", password="password") track = session.get_track("4cOdK2wGLETKBW3PvgPWqT") output = track.create_output("./Music", "{artist} - {title}") @@ -113,20 +112,14 @@ All new contributions should follow this principle to keep the program consisten ## Will my account get banned if I use this tool? -No user has reported their account getting banned after using Zotify +There have been no confirmed cases of accounts getting banned as a result of using Zotify. However, it is still a possiblity and it is recommended you use Zotify with a burner account where possible. -Consider using [Exportify](https://github.com/watsonbox/exportify) to keep backups of your playlists. +Consider using [Exportify](https://watsonbox.github.io/exportify/) to keep backups of your playlists. ## Disclaimer Using Zotify violates Sp‌otify user guidelines and may get your account suspended. -Zotify is intended to be used in compliance with DMCA, Section 1201, for educational, private and fair use, or any simlar laws in other regions. \ -Zotify contributors cannot be held liable for damages caused by the use of this tool. See the [LICENSE](./LICENCE) file for more details. - -## Acknowledgements - -- [Librespot-Python](https://github.com/kokarare1212/librespot-python) does most of the heavy lifting, it's used for authentication, fetching track data, and audio streaming. -- [music-tag](https://github.com/KristoforMaynard/music-tag) is used for writing metadata into the downloaded files. -- [FFmpeg](https://ffmpeg.org/) is used for transcoding audio. +Zotify is intended to be used in compliance with DMCA, Section 1201, for educational, private and fair use, or any simlar laws in other regions. +Zotify contributors are not liable for damages caused by the use of this tool. See the [LICENSE](./LICENCE) file for more details. diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c1d63dbbedd240a7a01e499820c6f077068f3ae1 GIT binary patch literal 112613 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

-yPa#GNJ~3ZYy|#G-WdM zob~YBz9`yK?deIe(9ZkEI(F_OpGu)WrN>0Z@5ZfUfCfB)GgfjIf$vyi&GU7LFS5MV zZ?g@ZSG`jtIG15ncr+`w>Ori*-mu78OjTXZoY5!p2LmH@?JKkpp~gXBx||B)a0kml$Qs4S5t3b{AG^f+5DG|imH{R5Z)hY z&D>AEU9`$82m9;;Ui}wGj2LOsPX>#ej9m7jw=_3Wqz1MW3LkCrxa4<+@5>2)bc-azI+kcW~5Pg!4cN`v|*%6uD0afG*H; zXEF9Iuj+$=8V72Y}!Qg|PS@0ZK5= z7OuZ~xX)INM|wS_b^!ZGu$o^n&6YXwYnY{Rk6Zx4vRW(sbu(G_Ox#XwACK7QlxLob z=Ls6eLKa;MWAf*tlQEnY$8jJYruVG9ws2C|Lkq3CQ)rDIXWgD4&Uuf(>@ESDv6t|$ z2RaT$hby$=n*LRdsXBBkihL_t{^iO(&sZ;(S@5Tj*dAeh8g=jrdMFTSz>l=Pw?$z2 z7J+2SrYuj5!hM?yQ4zMa)476wyM|8;{~tO^bCSfFoz^_3S<>C%a=3tAubU5<74L0P zF6cXU#+B99WK%^Hech@qsHXmk=NfRvqjtFO1}Tru3PQY|`Ro@gA4Gfb++^EjBEl>Fp_^X=Wd9p!63J!ZBFupO{tES<=tXj&Ss9L_f zKDu5Dqtv?B?7G*v`CxniQEWA5c!w7h9@rJ^g=Dnu3%R6*F}l)?(KjdY+`2FnduJx3 zMX#Frz|ZxtBd4#%R}6cxBw(Jkj>}Xh-cM*Z>bMA;p93tFPRR+c-XX(PRVUyp^aTgR zH(r)Fs^OPnHfE43l^Huc9UJRty(o8|1|;3%0X;I4N9!2DpJ$(ELlabwl%wUNJt-rj zFf^F*Az<1!pLCjD@RyL_9^Q5L3V5JjJj^1gyg9$XH{$)Jzm<^Hbx@A~Yb}4%Ux~YK zQ!TGqE#d)v1nFySB+&^JMoOCW=^~;n<(|^J_hr~UD^8V@YG^88&VYR`zmkiTypvix z{!TsH#8&>b;_6kE0)YfDaG1Wn_D*l9cL52rM0N&{D)`?{n zb)%xGS>@I!;;WecS=PF>n)*|K8lF;83L*iew7%>1Fn;s-)R<|Xa=ePooA-(=2uQX@KwLfvLr;p`(Dky z%Afb_-MxrwyETMhn2*a-o}(@UnMP-L+3>QQ`fu2u@atPe-KWMu7s4+!aR_4^MJ4>CS%&Am8 zrmir_AJ2G#dIPSDoybYQP+8z!>>>gponA)PPsX;Nx4u?o3H+kJx=8rKkb64SKasTy zp`zztArl)-*vD*HC%Ogu1sLZn1D(B*{kSljD5ct!#yOL|PuAiq9oK*e$a0Q(k811D z=Ek+ROb^B+mOAW-_*(yFQKIVAe3Z5m9`Lz$_5N0NHvm<76xMxeQ#9gM{z}h^;>{wY z=coDu-ZM_ve8_Sv`*Sco)#!8xutwi>k)RAsmqVEbx~8KJXd@m%&IdybLak?I>w#6I zF~;J?iU$(peh?#afHLzn!W}VPwNsojEw!5OoYaX^ug;=o4*c(>}dRqlNVr-Zz z?di2Jl=^8U!&p<#HHsHKEB7e(;yZkN{&Nr}p@$a|+Ax$Wr+pKYH*@+O^z!IbEQHqO zQ|lD0a_bXKdY7A%s5w&@m1*Q6ZCB&=fg^YG0=+|OXJIv!UJbd|$kVNuopf}b*~6lI z_h<|$9Y%2+GK7oFBzPfWsvSJ|-?%*_Ser_rSQLYyL#I)ogrQ47e&KD1GfGFZQPcVegP16uP~mwJZ}fd&|DrXC z`45KpzKg8vEMEJ?k9b03V}CZY6aRthPtHz?q&1z4u;4mb+t9bQ;YV7YFtk0M@qBf_ zFUawbhwlgI31Jn`-bWeklbqHDz+dAc(zwhoc0E>h!Eg_=vDkx;1J+kw8^2Niu6k%G zL+E{Hx<1X>R$YJEM1ZU3SFX2J79$B3CGuKJwemw_Ulxy?mmb4s*zQ*oi_0=?xuP=} zrEMCLUk28MII~k_Ul?P_JBlV?haY+I+Kl-#hN2URAaRJom^1^odXq~;M0P_k_tC`V zjM=J48jGS(D3e5DX7xHcepHTy>b$1)+1jFE7`SIXx8L%OxGz{{sS@=t_8GvA_`Fo6 z(}n1XqaSb)%npOT9HXrm;*2CR-lHOKS84=)x;?+I+15y zMtkHalAA2<=)r3(wXcO~WnXQD)7aC9bai-X>-W2RyAD3~7wA-FJes$YV0&;P^mL5e zH-v#kcC;o)6)ze6x5!_}y!dBaPoGmKNs1$U!x5yq|4j=D*PX)F+hjr;b)zh0@vW|o zXN==Cf*b>NQ%Uc3UZ}3E=!-;!uGj{gRR`FO8h$w_J9lA!mieKx0p11@m8XfE|ElDs z!(`$&6VyXE?80927LY-9pgm@pV z+afoy>*g_H1E_S zhwA~#hy`j-?YeB|?R6xFu_l8r4>Ife5V~bJ#0Zt?uJchEid)TK*Q2??5_bt&P%Cz# zC@2ZFK9rPiddnB~PCXJVc8IO>?=PoLZc}gE%JS;~2W!qmYo7aiT87S@?$5XkO{ZBY zw`@YRhrKsPS4zc(Y7w)a=Q+y_QKZ}w%Q6F!MRsQC@z<&emcU6-@oOIwAWw$Qj(kMM z<1~DJ7_ph`EehclUjZE1M`TtYk;aE0^Iw6BWzY?ac@j3?(bP=1?Imyv8Ah01urWTU z^Nji=N zTIUI&_427JHaSpc|5tn|>(NY5SMVeeG$mc{SyfiflwvY(RKOhunQ)c+S!xyQq`_Sc z>@wo%NVIV38SSa5+vL84ejjjkG6nAOZ8h&cgfj{b_BJRxNJnR{=ft}3jH$i2j=jWH z7thopYjU*p!f>;CXF`^s94lREv)L{=kW;NKcA_PBHgp|uL7n@BY$^Ch&@sqS?`@Gp zgY{oD6twgBxFt@*^8&^MkHRwo4^bo_%e=!BvJJKXr;s^X4*BAIGn7bc0G5CBHFoAd zB)}86!X|yxYRM5fFY4K_#^p(wX5?sDc4Nej^WpKv+}8Qf@{jJPGOADMslWQj0a6pz zR2T0*)%eV3WPa3Iq-%0i?k)V=^rq-l%LzhFmWgyH>6S0S0qHyTQ6gEQ)n)++Y%ylJ zAlCL))yx`bf)VehXhSR&sMa0Kg<1!=x3cDTGHmULS z)pukYZX=eqq8t6Of$+jy>#6)n6Tms&Z?i+q?h&miBO>8eB3^@B(+=Fx5g10t6%90$ zR=<0%*z2~=XVw!@6B7G$H3b>a#+VZJ&g{yWfQz%YSvI9Q&#k16hkr7EdYVTbx}Q8s zu0CgPNg;lKf{gLsV2kyktelIF0w9|16J%Ip#g`LmVTKNf7X6~zzu7d#Qttjm{f|eC zCts*Baf&M%m>vzo%X7IcY1F{WH|)t}ak%C`)ZiKDuW<`n>LS&;M5dRn!d1wsQZj01 zLm*nJq1mKZx&7H*+E7{ASRPwuCu_NJpXH*idyh)lRWALupR88OqtWb1cK44>KXSS1 z;_z|%uAXp~GqES;`MB4iCdPJj)5v;F(J0DKEalc!N}os-lj`(_5(hc*%e)&;1^(W$ z;1}0U!R5;RH#H(}H5cEFq5KFnU#kEP(^$o^AyK)U#*y@FWG+19eie@A6A&gqxw?VD z3@3QcLh-3N<#z^nHsiIN7vBfpQm?+3M;iBqK4y8XXHfgRyfAmsMP>L!EvLtbo&43% zUmGC*4QRXfuCMOC?}KYuH|}mk^xxerqJO?BfP5)mb?qpERSiD2AC0@iCFy>oZAKqg@qXD+p8c6v=@U6tb#^Vo80^m-PD-OmewN?zxmh;qkCng1$b^l35a98ttN z^p-;UyPH~uCJT|e7|RIm$;`XXo#~u)YFM*H#HgBeNs3ho74|#b4xBBgk;zoiKz&jb z?eApwAMHEW5K~G=>g+1QZ5o_=B*|_CKtgA<&qnim|9MDD#T64-Hp4{NW6MQVN2b~X z?Fb$O|bc+@xRE=qvjXVrz|0J~w%W4Tfjp@u1P0uIMgP|21r9wX#f)_fr~1 zSKpI;P`#w36Zu|L6>C&`!_&v5G2> zeBAKl^lVhd#4hiDxzFo=xKF>CQ(=P&y>#8`3mQA9*79mLnNkak8ngCEA;rY7MC0R@ ztScnzGfvuIO2=>qK9G?4fdQR&TK9&W%%oY44I0K;Sl{V)bh#<4@zOWbT=@9M+=KWg z;`_?Dfjovm*7=e856*=Y&)`Q;a!Ke9^{*$st+cf^SK>2HT3Sbcb^qE}&(LF1BVwy_ z`IF{3>8CQ{$SFB_(OJBL72AErZ}uD8StT*wx?m%WFO%lnt~?MOGje<<1#|tR1fjo~ zDp6A+@&n8n5-1aofcRBo)t&C4{iX4TnTkQkMGFovO&1WX_55cRz?Wq#l|wYJXMl7L5~OD`T7{&@2}Z!wYXs1%t>OCUeX5 zRCsW^>_r&^=jD%o<*RUMckBaRVHA5?Cja27`W3BrQC(h#qpx3~zd}FWsWz_rIOJRU zi_OBnPrhTV!-A!_`@jTQa-!g#*mLuf>=@lz%#u~KE`~V5t=H$M5AkBf>>GSLfi^7C z;j?7vO;|N!uD_-!zROiYG}#rF)ApS{3?&1X1QK0?-JFbbN`Z~F?wx_EMY)@nC2mBq za#qRg%9*jU){-9!`qiWRI%5*`H6pL7cqa|+v5wwIpiAY>#;m}-R77Vnebb?p+M-yq z>Ch&PQ6Ox#>qz_T4KRNTU#@a@81u|+A1?fRrWF61P&GC59GoFd8wfzS7!LI*!vHen zZkfnL-{uzHBf+fxTLvqZFR+za)4`1dh?)=i3+$rv5qud$=^5vNUIY3@bo-t9*^$ccm zG4?iR^GE+a{KP%tRLp?|2-hv~$suLoy(wjc zR?aC047fM6LzbLi2aRSN>$5R6ll$hBqnKg2#a&P!|J=jRx)4*Z7O=z-LIu;>Ax*;J{|Fb(5_r@!?Fx z5(r1#8{~KTUp)gM=D0QKR0y7YL-6bl*?CrmN0yC)`60LZuTMYppdm~ehoA2wd>-rQ zBr>#I4CaoJ)xi!1^)FJegbK$F_HX2>i{((J4KWe-jUUebOI@bb%5~qpm4AE5aJH?- zVfqQ=JEeW~!HE+O1x%3awIRG`t4rKkCe~KmR2$e&T{aHngtj(shXOHuy+KduW23mK zwwpC6wo#gWAWo$F$va65W-Hg+&Z>;P>^PKgHLFw{$tc&9(~D|L2UI_SY{;)ZBi4xo zW-+@sVNo1Mo*SG!A{Hue>~vGY;l|m%rbxrF^{lbRD*RXgX*3YH^m271J$+SaD>Yd|>`x48HIW2A8=qsrOa8LJ85U}K(%T)DCDJ#-B z$Uw8s6ef3G`1qOfXt8WRaN^v!ns-X5s$X6I$!x$wlG`C`QQ)vMUxT;hdpW4%QM%jA zn-o+VvD(L~a>v1Eo)V>aP(*N@pBtQYrm^Jjt}YSOi*Y`e%#hVSLGRSFPEL6@(l9pL z+vttd(?Gk&4hC@b|FCIBzP}p*bPv$}iX4%^I;Z^&WK4XZ>uMc#phH;f35s-2CJCS_ zvKm#^B2)U2urLRyIdvod`!mDB_6i+%q1~jF_z2&PV*4^D(aw@(3+!}9{SfT+7qXjT z#|1D?_O*-x7lyHvQ|EKjx%y7CPF<=d))Xf&xMm`y2eQqJQeZy`Mmk@I>S}?m#tfVD zdKkC;NUJOqz4Ej#^Y1@4U+h^U?SxiEUi9LREnc`gCBR+JKx)peZbezWhKq}-C z*e=6=!{_yX!sm-BX{n+DQ`GQ?dua9c#dQ1*qoC* z#rC0((mpMJn2MQi+)+^dV%jp|agQ^xE%2A9jrTN^=~C>C4USGeJsns=V|FqvoqyH3c-y+baq`us^-sgiWyN0@lVb`nzg1f z98T+yvtie5{q?s&?Z+Mgyk$-liRGpR+t&-5P|!zlqB9#hD8;F&InBR9kDiReM3(%9?{$7m^)Mpq>@#g6FTG zGZ=0lLqAJNmA9!yJO$u*KK{<;(${`xbGC3jPB9gOTsT}otB#;PWwSGH$p8sec+Mku z`(57a7`VV7rU4?nnN zlgnHt$R^FE&M?5s1;wUo8DQlZ=&frJkCPDa0&FWE{NOkrokO>ERtDeENNZ@I zZRNi7P$y7E)D^w^k%(tHmyI&Qh@StgIe=xjST7OI$1F4ox)c zQT(bRH*Hnqr}lh@G#Wk~Dvaw3^R^Q@i*e2YmPX3nmF)Z`RaYAR?V$#C!X;RgbnN&9 zg3kTta?!O&P``L}kH^&kXzZ=${*X3sO&j<`?Qs)Az=MSrdeqQz3 zt7LPW5&Lh_QIhyii>}l8XznZ1Au)!}lw$>NZMi75aKQ$SmMoM;?IXM>@yoTGjn0F?tZ#0W%1oj(CA4N7fN1@IOKaa&Q zq1MlfqywOJm5#4!@Gc=T6CNQ!QahvZ%0vG3$CAyNV-(=Z~86kQ0Eu@7_}9xIXqbn>4Y0+E4S$ z;rUo$6AoHY^7TOGHFom9!(A$2>t3_tSldIsiOOBUV#|&D1#ST1OC1N3IVxw+*-qs% zPVyspilp=rZK9I7WX%3|fMGK_ZXxUOOkW+*`WASLb5%Ev#*uM zbC9)Z>0LfU7a*hGvPV0<9^`i%)wG|;pQJX2$`Fq3na3vlAGY2E8tV4_AJ6f6jBQ*+fqyIM< z{`4yuUgb6VmPANNmdae?HtX$=NA>(k9hd9MDmr8 ze|Gfam&?y)uO7HOlQ<G80gw8Z^cBXhe@gChw5I)0U5id^qIzGf+AP*URLFm8==p9rVH4G5N3|WW(!dV@VrOGQ7iM|YB;?)>9%&f5o+YfUgqtv zOo0WVqghV9&+IEcFWD~0W$>ei6I*Ocsgy`Y;PJw#jz#gp`-vrIgNc-p@#BxdC~P0R zysX`}P1>p%l3`Yv$!+QAKbYni1n|-omL3^FN4J(bwOtc9R`3^aGmA?VlyQhj&pcLr zVO|?$=jHTf9N2R0b;{3%cPnb|W)}=qt9Jp)$0veAAIuC2`MR!YwLFO9hT zZ@x{V_E{|}EsaSk3wylXEU|i+Y`2`~S3HZqKkBz7ahzZ;9lV&H;K)61?=Igo22?A* z_$2qWQ_oo@M&h#e&z&pX*bud!j$*w>znwgzTM`a;PQ*x;sExH-n|kRh9qoI;t><

r*4CM)@OAOO4eeb8-y5JqNuFW&Hncb#BsvZ{F(nuYr+4IkwwF; zX5%CW^I^2zL?sh@>NEk~9D~DPdQIJ_d%Fb*T!v!SX=;(o9|JN`NKXQ8SgLqg5%e*Q zVj=Guvbi`HrQk*yIiFvt?JvUu$*3t98PJO>N~MPM$8m&fAb1Lz1cs&2jt^cm3dwIL z?n+VI+%{1+2;JwBv~AqsmHErNQ;4CX{9$mRm3FaBR9@kD0|pz}ewPaQslyA)@2A=Z ztl^XpJbY>U|#S!)SMm!x__^ z114YkPMij+(@C+yuDt~2DgAu2a-_%c?zw~2Z{a-j^z$E&m%{-L+wDe!+cF0NOtwy) z{{Ojbc8-6Gu&*O+7eDBucg9CEn2sAp1d_ zGX*U4-r@`u|n*qp1$f8eHFsYM5Z1@6V69jV9%;i?NiN5k#>|A*BW z2!+WUqy>inTzlNS`Ibnk~!Da9C<39oY3cn?mkD$HKXGR^#T0ca$CO+(=} z_Ha?&poZBV6zP|)Zkq;$py?*SA5b+72UxJ~O0$hPZ#gQtoX#!Y24m(d_PsFZax6Hmlpav1bKJ zwn{YeLsnAQQ*+-fN~Dj*Dzy1c5y@XVYmRD8!iG3mu_36mdjx}7sS4@fh*-Lu3egHM zOADSgS-LjOWbZD9-wIRPv8CRizi3^2iPPq$u)BxtlWsnYx~-SYK~n3d>Va_H#2Dl= z+joJ7Y9BpxA;$WbBjs&n8l!cjgzH=OR$;hQ+Lxa%cp0@Iv@b&ez2c!6tEx;5O4hyHYgZ%COIJ@+Bq8ViqA7wZ5s*ySl-)GGP8cH74MQ`Un>(`1f2a7hkTse=f> zHLCznuFTIL-LwI#d|M0*TJO=Du^T`CLlOT{ z27UiGLaHT`A3U&Mj~b2hwJHx@kuf%w@yHhmm^*{Tlx672$i;uM@Ot?)1W<$$=cT9<>?COsU@F@9Q5hYp*DeO7q_ms15&HzG_p`4@ zmrXD_40Fap6^U(F&Is>pQ}X=caajVJ6FihJJwDy(uie-9?eS*y&5xQzE!k?_YvN}n_hH@E1nIL z***5pJIPftt~J|vdF%NcZ#rFEyX;uAe`UyfgZthOP_gi%{0}i>|6OP0UvSjxaKa`K z3sE=H;ymY65x#Tw2;XgqE$2a=psqi&`FcLc{A4wO>Np2YK($pK3$dv^k7#7cHFt6o zN7LRLc1~6k@_C(MfgFc2sKz6;g(EksZ<$w58LT&`H{0XdpRV=__5QdmU^f-z0pKF0 zQGR0Uu}EQ8DwsT8fk#g<@NK}R5n+6Di4NhHyy1+l@Vu~`9he2vUBY5wl{`pV95=+p zOFItp!B*p479mjS+Myds-eY`k<||+M+XVNwq5U{Vjtb_iQewS3KVdFjiMLw=vWnva zJHAh9KihY^&eE{=HidTfX14Kvzx3e=QOn^3KVPUhWwkec$!=WJV??;NeL-{ zvH_iAT#5!3O-Y{0zlHFf@A zt&f_>q51fkR+$9jnq=|*2Nz_^Opt4SAgOH>+BeZ8jXt=42M%+%V&wCq74*WgO( zK=@f{lXJ*~eP8s&EjS(DPEk8(ca7FCD=o)ntS`kUsc(IEQ+eFy<{8JDgFh0tjFWpO z@cD2t@VoZvTpp)2_v_}WbCey+^NQyQQ#EM9%{tnby;ur40^x4#bWaFM3E!eNoO_u@ z8{RbTYklAsiGAU?#Y>9o6FP(5QZH!kXL+bsb9BV3k`&WVlEnHtV_BF!k!yGSGxv6w zS|ko=-GKa<>1c-E5p#5rf!;a&8t*snB?yPtydj`vh!J132*m$pkbwo@RoE5(jSy*c zezBQovOWiOD{~yJO~-jpav(-LgPzuSrUkn@eA(Y;-eT)0t3*H5>y|Iz&OMg6bISA> zO;=^apKTW_kS0^9tgb0qI7KygebciK!kWKwJ)AIeKFd8#G5EStntCN4g!qU&U#v&} zwT$*Xa=9Ff;``RdDN2d9(_w?aUhMdDU@0!}Oly5B9DuH>4%hfqoh@UkdXwSklD~YH z4KdoM6=mrcG2L40z^{<_uF5CGsP90$bfgNyKg+vyF$N~gyYtN0d7^M?cKTFh0S5EX zSC>wu#tEZ;RSRF*sAjt4h??8bkmgQVe#+tN$Hq~V-(5ab2|n^Fn%Gz zCxPEDEJ5Suulz6J?VPMw-X0SpWj?UeZl)K27)efDm7#d8@ye93Q~l3rcK+~9`N?E- zl{HVH*5zTbOk3hg{MmD?7ah(#9L=dCTNfhM`pk5Q>*MEOq`o&ppO5a6ZfUSZXxM>_ zo|E*!rMeIiqJS8qVx>iEXov<8?Xg5|Cnwj0I)7$@h*DTmAG-LCX8^~!ug+w@eHi!; zD&QH2KO8pe&V8l*G5bnU6iMHWRXQ#X1(hpe{lW;6b?V5;%s*uaD>5Amx=eD?yvN;abT&a<-VRD z!i~G{%f{TI`psU604mya!|%?)vF+VMaVwTbm8h*hLb))j1sM!Mx;6I8 zpaoS>;+Nh}rH#^t3T*~~$RN}=#S{Z5dn6~UMiaJgWn*ocwj-4ht9$Wyn~Bku#44M? z5R)vxz_{s}V+B#rI;Vf!rIp-ebQ;<&&0}bR-rQhlfyc+>R!JhqlrnuwirhNW6+V@z zS_u8{A@-2)qr{J&Vc;YU_sW7~UKK9q{*h~S>#_6q(}%aKF{w}1rfxZ`RK1O7a;f@j ztDSd+Jg*>-EhN+#@{oq+anl=4+4LfAv&>5$u;W$6Le5xqpz#2L+w=NZ*aSdY&wf!w zX+D2H4gA8%dIkC>E0(zzXaUgWt~z2S1sx9$wu}M>_sGMgYI1%q=H=v>>j%JPL~7)% zl(Lck`mUlIJ^HSL%#xBKCc;cI{(JvupORgpepl!EtZB$(?rrCUZF)`CK8ZqT0iW~W zO@hfSG@~5R&rG2&L&V9LEtS8Zg~DY>ND&j380$~eIV|PN_2W4kcQ&^TW)%UKknko> zgZ$=c;d(j4u% zGh*>+H%wVUZT|3mW?*a}pF!VR+0c_^@$3TqyS<~>gQT1x(fGG2otdMB3R+4*-ZI@| zbS-=ey4aUBdJb#ZqGcfIC_X2V#pJ7P(h-`8p((pI8N1B~KIh=8oj1CsQi`SqflTf( zKIxbwAY9=U87%8{q49I+vA{BMGFes*kn|*yzdw_gKlKu{y*=Ki)YI*){Qrauv@ia4 z(Y=4$yim4K#}*8V?%4O(##D zw{QLP8_s0x@d6d7PaZvv49Di;2TJ?UcMhP6*}b%$Ds_iO?)T9W=P7m8zqkNT41%5R zDnbI=V&W73&OiUgh%0bmIO@Jr;(bmr{$W;Q7l`%f8f`+S%h&H|OpS zwCG(EGPN1W1mjUN_t2Spo_#gwZ2S5!bl zXx*E;dZqqey<2q3e^rYHloX#X&AqShvQixAj2V~{G3gU2XRM9hDzP7qLYH`BBYwJK z{8@BIQb#ney6O*h4CR7RJC1%U`{^2^w;?NnwZ=;zI+sfTMo!JHpnGqeovex^nIH(A zdEPf79#;7nO?$pp1S3q*Q#bVb&|;ZDf~X+7AJmi_pfcKcSH=<$U6_K+3r*+aHreiW zW6r!m5PK!*(NfihjYIGiOQ7`yaQ$}y6I>ap0tz5s{hgKD{^Fd`Bs5iy7G#=^1Ko1d z(nD0#acE|8fYRt5FujeHmcLCPe^BG6VBP4tZ$MIjw0$5&zN}21BnsV=pr+EzM|0RI zxMtt2YZY8%AE{{FNL4W&O(g1^5uCLr{n2t$`wAz1lfg9-pTqJ%vJu~x$2h)KF}K4J z4d4!bsLh=nC{o1866pe57O=1j#8BGg>>THJeoPpq5VO+jY)F?I>UNWazJv zeSr0^e5&j@;>t#pWlOpK9JT$S^I=HzBSSw8gB0T!q=#G1`cTSslJ4Ima~H93qZ^6l z9q8vM);q?_w(BXZ;H`sUGXEuD$P3MG#?kY#!tC;+m;e-za6_~=%AE9laL3w9hhw7- zZ?6FE!THPnE>a5G@dM_ZEmZ#a=KpjQ_&Ukyum3)Z9m1_i;KO}tod2PpW5ue2Behi( z1hkm|2~ZEUt-(&Ir|i4)P;Hr5wYTh@=1N$bKWfWwWa};TR$e;WE3Or`AiLhjxe-v6 z4s%zKpeiZapRe)71kZ@W(zIJg5F1RrdBK0_`E$x@BH;D&u|T;?d7Sf*t$!iUc|1i` z6jD&=56q)`E}hq26ntR%5VNCD-9Ok?PPpqQTYe5*pVUq;+*|vam=a%S+$gtvbp^Wj zxB|V{yZMJ4B5VkLnDO7(TzUn9D0W~t{2NjHbwqnWtfb|A&f zK0SWv)joy)f!z+SQk!&yK+Pwt_}<(Qz}XBW%d)bdN#VyOTuDBOjk0g#&63@H*H~l} zh~LwgWYvE`VKj!nt*TJ_$hnZxU7H=~K}zD#S`_=Xv9%Wnf1>G~S89tSU;r zh&SlHU(16MVh>#m)Cv30x#**^XSJ6pEPVHo0rja`V5L9P&!rdR?JLy5b$?Ev*yMGEyE;dU`|+Z=rWE+a`E zFFg8`Fn16Vf+m?A+W4>#Xp^p#Np!GtoI$i(F;BOX1<$q#a zEY)mMFZsjyY0CQgRb3d5Kf~O%(fc7bC2{i-p66z>2uf9-bKfJF=*Rt*`#T!UKluMx z)MLK?m0~eFl7F$}9PazgA$x+*g3=3J8r;tQR7l!MWf-~~s8H+_!=y%2dm*AM^IU)Q z8qjeF*v9#(mal(-oP~Pzoi!4`hyQ0i zruzkl*Isuz`uUFWaP@_%)OK#!s5>HfWjFn-$EUyj6e|RoU$ked*cI48IX>sV45`aa zu;h9B0e-(+N{BgDf%v7Ao>KYB*;haGXb8pu(jDb0t0>o%vpn|aD0~n#s3eP$_+~g# zvRF)>)Ao4Jw_;V`)Uu~_wTHgWM08+mi^Y9~*j<&ZV*{WQ{r>Zdjzil^K!Mj$+u3*P zJ2I@8C695RAOa`WWyCph$@1!au_9pr%_vZHTJ|}vEH@V+y z{4jW5G*2?0RO0FReC42Cl%@6_mQ7*Lw(9~r~-S0+oPd_nm>YY;U{fwQh?jZZwjoc`RKfp$mb zYiizU3_P-*XTiv=i*G7yuD;sZMYwmo+`feDT^?=MTC8k8sKIZKGa5%S<`|W^XpPbg z(1P3c-c^b^Tz6yeuc>KpcJYFZMo{ims+APR3HDl}afw&MJ<~CwoVu`JtBn5I-ABeKtSSnvO$=@AiebqW=T%6^=w2*#vK$s6<8;OjNgTCm3$7uIG4W?pl1l_1ugpzKGK;e5 z8^f+Eu)k6x=8jPO3r2&i5XT3G2l2|XpRh-pl6`eaz9hP#J%aJo+yDVt z=?ChaWPrY#-XI&GRW|ZMPCfMt8%k#!=q-2oQC}?8-Ujk0KQ@Ol3l13dLQtMWX5-9~l8D_t$^DCC4ah%_mN-Fu zVFG^7*!U&YUlf`}yNyPf^g_y6@+mjL^%dQ23S8gG#F|+XtNzxwHr*x3l6IHq1oo-v zo?7oxR7u@b`ys4%6&}fHhU#^_{)dy)pEqkX;1_T!FtqNRzrVk|OqC-z(Q6NaIGR!f z#I*PLM8qx8SvKe$sw^SJhm(j2m~I;a>_wLErISYX+e}jVfwD)m$M}k80Ars|QS6kp z$4c*y4QJb_K_XD=tuxz`0Ae-18#j8VCX42h%R@wM+rTmQyOAe%a8d}SwwwdgZE`^JGm`OIFj)uvV{2$mpQma4)mC1bKN;k*a>M|k}1Pd=QFAkg< zZ!%@NIke7ub}TZMzuM<<$g=O_(2ypIG_c{vjxI zoZ=ZxVz`~pKwa?yLT3GKC7sHHq5BtrAr`))vj7NnxWcv(DzP6*a@)00w*|;zk^VUR zW)s5^4|Y~p9`DdRS<;C=G#nD zU!?}3pS&?{_g+2@Ak)?z-7)wE1(SFCvsutgYVvg~+Bwaj?8eExFUOrcEhJ*n20^ zh&`SQ=mr+cd43>sy3{b~8_n6@dnXvSg-_wPg+}^U|Z)CG8QvlPlg7X7wtz^ec#l)Bs z?R=DUy*e*Vy@|U!YSOER2>8mK{wB2(Q&klKFc&fD1n{Z4L0E%sjOt1*-2U4|(pHu8#A5kPHZ*NV3f7qCX4 zc&Uu06T3ckj60U69xs|M9Sr3U?QISB;a)(t2a1%;Uf%rWTbrN!sxduo_#xzcGDHLo zw2lg|Ly30NXjT=4K+QR!i!zm5*k`bmsOKL=vRUs$V_v7p4vuus{gDRmC60;C38uou z1aCMCu;+cY4B!(T>ADsUp8NJLs>7_IFE*j~3*e~t{FE#cmzB**j*k2tsZBPe%TutA z%N;C@nt`nkQK_{s4jqPK^Fg?7!Ih}CxabrLd0@S|c+46;fc}WzFPf2rG~S$)ShUx3 zn^64&Kg)liF3724iC}Lv?KmyDwoOha>*54*c`%f(BXpw8qmjJ>KBXIw0kK`i=9^AO z#sZ0Se~F43ndILOchWS+0AcLZNg`P91ZO>|T+k$Z1S-bJ!SWrwanvwcfsJ2AiP8y_ zYytN-B9nUJ1xJb$4=KTV)p{QOnxQ9NQX(Z|>KJf?-HSe)JuaKfrL#@E_v&59{15C` z0(L(<<@B2TdV|#SqjdXt?@+?6b6SEVnM}l^#z62h%lv%MgBE=Qjejjr?W5lehtFs! zA=C!|dN?_ajjOD-Ghxuuwam6YS-}TecP9;@B7F>+*Lu!9g#1-ZCbNa&*BJB^B9y_b zWt$v&e2m9jSlu*~-`_&ZI>xvg%;QA)k;@{|WkNFfw>^%~z$UqkmlHP8Kq8_h89;G& z-0#Z(rZ9B@mtdIpD$UINFVpLWQ@KDvmAS?crE6cE5*E0$EL;i>1)tF>k6NT)}u z_i2*og`ebS_tKYGk$Q&pqlNj)vK3x=Ifj;(wD-}Ys8X-^X%ljg0X(UgJ!uM?nrQqP zeF|$t8(wNZw`tkSC~-ktW&qf_+7-HU;r4*Vz8b#32I{MgR>jKVRPotWKbd^QN9jM& zLk}^M_kOq-NC{_-9#eO~$CH!2*6oQ4hD+2VX$jwzE0q&W0Sd-WmCquC_eR*r?HwiA z|BCdr#qX$b;#=m}5OQ#b(RMo08IY&CeXmV{&Un+7=41Vj4ex=0IDcAy8fy=_19@9a zuQhb>W~;=3@eOAed9bB?exoSiQiL}7FW2|=9Y+;{Qb-Mt+Cuj|ebT8H`-9~q+96}5&^e|Vs-6zaMRnVy^f z-c{4&#McJihUy>P)`3p2?CC*IBT^LKae2#fkQ2JI#D z2mL%ay6sI0I0^?0g9KkQ&ePJQ>Ns4M&~J<)>;>#nKt7v_ELZ=P+AEXaz>ye#VuqC} zwnj-CwbI>G1(?ZB)3nLqg8m6SU-6NQ;yqL1ektQHOQKikB}*Rp^qR<538H+*cCoK4 zUL`szbn}6i8$b^#UT0Da*n5`q?O1h@%aHA({3gZkSJ>tl_{+-fpprDMmD8?)gLk z^|eRx!adgSMSkXu2wAma#@J_}Te|Y#&V3`N7|e$s*GYx0qZv&jzSk=7H)AXqoX#i* zSdVKB=y*IUv%i{lt|5D*6)K6vU(iU#0FqomO5LmbZnR%%ObsanOy>$UJ#% zZilmb_;@;?YyHQjoooz1PU{A87($|%`HSC{H0W0p@!D)?@l9IXarf;w-y=sy%CYd^o~85F(7BZx;l1ZVrb{+p zJQjtzuh11^2g`>bEcG5db575S18Abkbi{eZpDklha&%}rZ(Bp>I2tR;a7snFJ2%E} zhG1f2*;uDd*E0Jc2R_nTmTUGmDxYT{{$u>X+_t)4+5L1A6MdEp9G^YOfp0p@+SCj; zWCC=(_1DksKnX2kz1+hiNzZ(Y^&xLU;rZKDgbihhV0cVb+`&R`T!2rqLrz1;NzDK1 zBB+LZ>to?t{K(res_cr??Rp#`!FtZX;9pIub@?@jhh{>*2Oz0c`QU(nm^km(Y2W6k z51oM#Z2^kz#lXe@@6xD(&y-8X?(itO=-~9xJju>-Ca0YHMTY- zZnqgct{#{HUuVQ}w-+L+_MU_?zWT!`*mmu(*Q{3}a~d7h6o=;>)n5B%oRypd@pz#e zxg(ubWHa8kesGIZkLIUNz)u*;NF8gE1&99;|0 zmRN2ZIx?c5^JYx~d_MyW5l|J$u47rU`2OVhQHq@7@l;mnWbua2+ud=TeRKoLm*!`_WH@EE?~E^RmlOtFAq6-h)d%SZ!!(IyYrz&@+p)=2 z+ly^0cALN_G-4A;4Htkn7gNl~naA`Im2Mh+lHN=J+H3>me{HB=_*Vi#qxNkNt0auu zdEa!yTRGktV{$vW%9O${W9&x>PW}4wwn6M(S-V%T;FPVhhdIpf=cN!${YrIcEttWN zYx-jD={D<34R*UW4#d&Sg_BrM0!D|_BIPIJ)NY$qDf_Vz6{kbvw~ZdlUqM^8)!<631G@}788glq6jm9;RKu{YE4+w{hvuCKX>`|$$)GS_C3tO6cvjmiQI}%hy zRpvu5xDi+3$z4Bzw0)l! zuRWf*r6>sjr)FziE~3{1A0no!^n&Cmmn;R&Ugy+TQVa||EjnG?PyfcqTahy|KRhYG zF`V%*MhPt}gT22s;?^QALEVu-)s>WA?Xc{9pEDDfu_cQYlUwTC(i|4GVJUsr;GduIAS~y*AZif>!ycwBz8q z*vm?bmm^E7Vq8r^kFIQ2Pq&q4}QO&0ob;D*Mk4(@ob;4%(q~?>>4?SD&yq{WUhI4RVi@!}MBm4~A13 z8QZH+Y_z0}dVw+NuZ?liqx5fSWqi$1@jrZFDL%X-Vy4P_tWEGHkEY`2jEBltmxCfHH$wMvv#ioyPDZ#=53s) z@2D}Ja!4M*D$dsM=gb7j=JWU(ZC1Dx_JG*E9!(GYnWn46CA=vLj1^+bvZ5oXW1VKh zMDq9Zhx;(c&oE`rUYtkUj`dIQP_Myk*$WAZBZ$!9hP8qk3ky1!l0H&c6>Y%^BMl9Y%>F=fAnd(+-#Z6yxk15HRi8 z{ScYkVHW3v^ebQ1T6qHj{^wa_&n?2e0;1BWb+YFYf3%lAkVq(`O|Df4zH{9f;d2EqSIlGtl;NijMn##ID*}w9oG2s6l zs_nMxWZ8iChVH4>xIbIc&(k{w1Uyut%7>(;yS7Lc^UV7^DwftS!J3zZv*Mz4k)VJ% zFoU$)6S}m>WN*I&7NHCxm^Ughu_tuVO$2dwX?&sSR$YLhU$Nwr{u7Ob70!RV>!`jbZ&+Z0yaD}f)9uRggBL~GETsQ$A(v+9M!}2E5J{9u9@uR7%i(*1K%dDyR3u(6eI{qo| zg^%?^$T~`21TPDrCs!A7I75u{oBrgG#AtZ36W5+f_t&>9+Y>nneeqd5Czkb}b>lZb zx5A-sjz#zoA0n}ZSFk^>(G!8N!A+_R3$*mEk@?8d@k`rK9C zHi6o?eFS<423*x9zMRh^I`GiN$=e|D^%2 z2@(ETGXLFY6`_XdIBKW5{bJ4)vTUIWql6Lzaz9YX`1E zco%;xCPaAIkm9kNlLsH?Y6gZb;)Qt=(!dq3!JdAWq(TGi=q+N=Syb=h0hbw2tHX&E z(9eTD|Kb9W`{+5Df!D4T)LB?bIX{RaC5S2ih9Q6c2E8YWb&+~tnD^hD2RNJ-B|IR1 zOu>f$?`1?gLb!=~7ygJIWJLP^)qB9Ou277ZY$hMW`RVQSkf6Qp5|H0ig?(>K=CJ*7 zqEDYsr+<{h@YSbKKh1N)=8(OIc~^hDEUD{DjZ;Ph77ccx8c!LnXdP`qGRa4e5pPS^ zn;?0^g;uHEh^aPzoB4Epe#8UG(56?kr_A(C&IOcPl~ujP>?Q_51j7$%d^;y?mjMO{ zMIrHctP||x^o|K=qM$L4wdeSYCIRd}#qqms`0?3ag?O5_`jL@@;G<`cl#8FZW<5l!I&Z_aTUUOnTg9qFOvGh5-SY`_SO z4GYlz;AM#CO!mIhcz%+xd#+#eRlxjaa#*RFdJyZs2zHuGe(4p!rNLx9wKi@?##s76 zhKbOvLyz3zGBekV_JsOchik3qx6QOv*v-+WQTM_wom1FXPXJfAOJpnN!zmCyY;;|L zOMscNWl~FrJ7PJ1_ATZv?_D4Ony4LPZ~ z@?-oRt<^|1$GiyKW|=*V&KJlp`Nw5ZeCt&^&ro#02dtbXbe0!pV0A(;@_$6m?==4% zXGE;s466WisxgvW=^nbjJOt2$M1G=mQXMk9CN)CkqY;1PM$<*;HD?KLPTQPlwI0%s zOt}}+Mn3LQwI8n3D~Mn?zOO1$)i`I`r%PmORt{}aXF+O;T^3M*PF5)2=1= znpXsB(uNkLoc;fIX6W+PsAtUez;2{|(^CKMZiedT)1LyB!ESE#?4A83GG+?|Hz`-l zNMaO6g+lp#7GyimJVZ8J0qw_g3m-O}D-Ke*bhKpKn>${qn@BBdL4$W~jP<49vMa{U z=1@d9U+cMMCP=3EtFoe3+jm%zT@2}z5G&JA;3lvl>7UzNKzDr*#-Az+!hCPrLog$bpJpo)Cm-*piJL%Vk;h_>j z2w4}CE8J?iA^~=l`P-wFD$?bPIyq@z+j^+!=e|9;?#IF_o*`|^j-O}w#6$*6`yxcH z)F9$lMLv#;knJeN;lCrX(*_1zE}5prRagzXH7YmPrW^JYW^0 z2+}PMWQaNCkp?$+Y>NrV2R>Krgj;wUaeHcs#iJI!UUP@|zS9-+&K~p>j58|G`~jwK z&n!{rO*oT>r8D^21usZ?P?jE8u+dZ1J)pS@E_mh*p`}J{Wy?IpGH+3g+&EZK9gWZ( zg<4waAneO;CEj+kGY>e`)Se08c#DH{-!q*raO@_eU5yj=u>afUc>j?qF2Ga|xi)w5 zsfxJAIf*o{`VW4d`G4u7D)bShyI zn-Eu!a&)7@pNx`2?ouPWpv>3Gcq|vsnOzNvo(+oTi_0^VrhzWVr!qFNn26g3<%#*Q zm#?5L?dEzewish9i+L8=bnaZx&q|pok^8b12XtKQS6;uuCgZ{Wa zHNeYMI=SlUX!){E+mK9-mRh)^XfEBjkG#+d1c+;+k{I*%`sL6oHSz@}z=jx{`Rsl| zTi==UUF7H98xj*{UkPm}9dj38X0w4!zj9=ELYyvRal2Y}=N&ugMdYAoopDL&uv3qh zXoXV+OX%EW`5+iK>-4p2`*dBMr;9m}iO3?%?Y?#pxmtMlrrkz;ga5L4wP{6gg#{`! zumv9cRF9=SOtW0gVsax(Kt~IgRTHmQ-kBVCH4Z(?1cWP5 zY&yAL8$LT85Q4h313U`HnQETi)Z(qowhGg|Hm9139Mo`l=)Q(&`6Hp6yEksYq|ROT z%pyqLYi-M*;Zgfub1=u(l4kEJ?Aim_%laB-ksp_%W45;5;>H>u1Td-g%!IDk&gcsu zMZj(GEHs6S-wY2!jA>{<2SJ5DNa@GaCVAalO(tMh|E!O{W)IO6rhbdfRQJzbh~y;K zHz*N&DEzuv*5IQ9wp`UtNQC$%=SW(T0#%1>uYUTw+(bWfX(_|oyl3@k%8v=^bfhM& z7rUYZtel@2W{kEg+D(c#cq=Df-ZN!I)q$&n!51%7ouzCA)~(?y(u$xDRf!K9fN7-n z%t!RZ7}q6YP_Q!N;;RJwN|_0#$8rag+mUlp*T|mcSRU|}kFU7S3M#Jl&9U;F7@dC> z!+-DnlJAr1LcM|#{{mRj!%3@i^6>tS`#VY3$#EWbP>5=Y*`bErC4O)$tnO8-4FAO* zqwSHm%tbIcTfOM({Zo&TZDxI5tk1(rRtp{4M(#Y1cl^NKQNlBjP*u7iu~+$Br5J`7 zqN$qFDBEZ%bl?&55x6Mdg^3Fxnu&?#e&IYeqO*vs7C*_lxn`e*-P#Rsj7R;R14{$? z9UIcb{7u|uFgmQ|us7`Gjw0K2r+-0(T9m8tUr05QcD9douX3RwD0WgpxH=)F0;6BK zwDbLGou%kH5%uLPvfC)O5c6q@qWastyEmt7*=4h#5#lPaA9Br*NJGD+Ems4h@EPy9 z4xt2}xFYz%wxz8;uAut0bI;Sks$kA%B5gsTkv@_6Azm47V~3l|Ut+I{TnszijgA#( z@H2L(G`&v;VlXb#cK#F3ca-T(XUwGp^=JY0 zdWUob9tuTTM2W1!BZyIA(%qJ(XC*H_;YJD$%Bxz0zQ4K=!sr_?vYYX65*xVPQFg;`)IeL!C$%~I%LlyGL+YJ5Xv!cLudc2wlnBw+H0~Pa!*Gz98TnI zG;0pgijI8|rf$tm789f1)*`*8&qChwm7EQDyz;osN#7c)2!3Hz_j+j$VB(7RC3yVs~k$5MBbQYF$C_|^v1#B9WN;z~s z8}KiGUGJ*C;@q9csxETP`26PHq>FBIrZ9Dqwj+_Fe`N4-hyTO$y^`B;oUs#6f9oo6 z-cNoPG-|Vch-W2@#uEa@JMJ@WKb6xK_+TGPy6CQG)_ce$n@zdv9OzziQ6aZZTXxf4 z@HoNYy$EBuHD|m^)h5>$%K(1GH>S8@r$b*GwYi|k!@pb~df<4Sr2@Q~eOO`mj5ZHb z6?4q18=BWdb0Av!8~xu3+IrT#-Yiu_FUa@4LeoK6Lf%1qqxQG=c0$~iQ2_<9+N_q~ zdR%RW2C(5~-T1y5c;b*~zOp8H^9Hp6 zYVh{;j_Qp!b0N)`wThWt2m=kv@uwMB#P~m-r8_y`ifg|^unFy+O8>{$ozZNqfS4HT zd?6D0aBFyOZfL}M7@wPThmZUX8hccXSB%e_Fo@yOJ9IBsem@WdbKi?@i1yLAq=v^2 zuUI(@>eWSGZ}U1eDkeq=ceF!9yjYi;`o2``rEmt##HdE12u@ndjwX}}g%M{$5WO(h zt3oX-xuD~2Se)14Cx?{Qc-md&uT_$LURzvfM^hwziLI~35o>hT8^n%1NaM~ZV*%=6 zY1&Qew)6g~5?JN(UUb--SWIk@{vcn^ zMM@a27pX4bj0UT?7OmBI3z^cb%gj9h;WMreT4s51gs@VBe=NpTKXh#{+Ml)|UsD$| zMw%zmg2NSR=V*N)b3bOPg?$rooLdS|7%Xmg;WDCnH98lz0o;6bGOQe>GD?Kx6i~`? zphRoH0;KUo+{Z%5*cTz*&I4Z@jIZ4w{%GR{udPeQdG7%b^Qnp3uK8CXdjd6zH!oHn zgdcIpPMqDnHq^fjrN|z01=)U=kVX=ZHjgzN-NRn~Wz(VW+?z{v%D5Df2@EN4K90xz zDq*Yso*_x&Ti@!)2a+mPcN`|R=(cjMIkd@cfld>tqnU5|Q@P6glfc7|#R9??0e`CYdn;3|-D ze&PGh;C{x{23pnS8TWYywHwDs*f&yl^p8Z!9c+W z1DYS-yoTmvA5ZvSynNkz{?}O2F3bPy19#ra1e`2Wt)7A3p3;CaOQCr#fke~c{Nt}h zobFJV&w5W!C$N?wnA~DtLqF?o&1qCXT$aHftGuVhQKy}1^j-QC4Pwt?W9KUHOsA-W z<|=^=c;?{XtLK~{pS9Hr5_2qkUnlP%xR;@J(;@2s@282llVET-v(w zK%%Y(Uji@!ulLa{#l`$7!U_kNf?cUawLcnCsiEWXu7i|x7$X|*8l%w>${_x+MaN2fP#QtDN40nP-#jR1QY~B1f@LMa=||?&OUqZnQy+C z`Q|;R@tcfmtJo0t(>n|kTXK9%Li>5s2EJs_yKAi0BgH!Gx{f_STHp@lKW9xUK6HNh z8LXHb*Khlqx=(5Ozsv*x-nI<3YUlck%gQ-6umLo$601Zo2?cFCec`0MI#*_o8WB=i z#<~FnmiRBQpL7iYh2jgjtW=(9d6eug?Sf~bsYj!R&y~0K707I~ZMw+2;6POLjy5-9Ne{nLKIBlgaM$Lk==txK z64VKva909_R!x_aCA`&0(8eXH7TYV;kxKy>w5d>n@|=#LkzawF8#FIrC|v+h0$|0H!(oO`AB z&w0A=w=C<;_)7rK1({&9?S0Fi7sQmxJLs_SsuZUOXo)<+^FoZJ4-;sc36gXNb>r!g z1#N!5um52vJKRypTn^|}K%rW^Mg+f4%UdkIHak1xru!A6BG?*?M23`Tz+3S?he9mj z1*y~31?$u4K)y;{6C?h;8zY%HJaf^JDdjN{8bHfdX?*0lqK(Rme#?6WR8^h}-q8WN z>}_|~%~=9A?|=dYkWre%Md`)D7g|15Is>rCk&%jpibuv_W9)u98wW!cEd&Bhd1n%9 z_m6I|e%EO_V^(?crp*=Owbq6B9Y;Kl{@+o;d`EXK zYv8cDIemDI@vb{^(A!QjfnmcMlAx8Vq+V3n<~lRu6linUS$FD%qgh-hzExh4uP1RQ z73}$dR2FH_`*zv|*Fu#&r$%eD3nUW7B))jKQC`=*m*Y#;W)XL*soXKvU!4lK$mzjl zh+ww73yyuuZKehdE7yLKpneNU@&PF)kdeuC^84KRQv#{vt}T}Rmfm6(2;Qh05fpTw zWjkxg2?$?rX+phsc!Pi0Olyrpo?l4f)G5CgCzMr{{t3itHI0m7LUqWixC(Xal3;VM zisJ1Trf1X}YIAt5)~&h_S3S4#?JJ_DC;+fbL(nv0Ts^Prm(Q4wGiqSb^9bg@c=cce z`=Y}L9`Ia2<7L=B1X~p#b!T}B$q-UDJxrLp#yLMDymS1ygS>SGD&-#N_=56g#OIkER4OoOAa#tVC>g`A|sH%p1Ac5xDzE=9eMV> zwjb3~+}&Z7JD`m9G{={p7!N$@v+5eeG~S;dgZwH)f*StKtkpL60#I_y1g$=chFdF3 zZQd&}8Wgs4ex~PaG#I70eu?Sa^IoBb+xfu4IFcxz3yKb1hVOjSPU>tE!#5kNCvJNWnU2~|%sK71kF$0hTn&EZk@%~(a@l*{% z8hPOTch85M-{iBMSfgbyqaY5uW*7`?s-IT^*h&`P6_MXfi(NndDzcs8l*=dwz@UHD zu!sHEysxS7qJU<7W(wJc`?hQ^k*^`S_#`pb^evcvPmLIBq zZ`hHeCl`Uw{Z8J<4}o|*1%h~L(uJv`cWpb!mIL}xo(ZwNL^j_Ii-H<*O6wRqL*C=L zWP5;i$l7yFV=$*+shFdb9=Qb=#+cmy?`Kd))o2HW?&P?SNT8k%jhN%bt}95nfXd<= zq@#Vk?iO-oIY8B-IxAzTD<^srf8#TVp} zH(=IVIO(`|kRn;g_|ih(Fx5&=KV%P7Fm*BstT26esahf6_-3$mZA>C1T{nf-zbDIs z*NohKciS-AcBbf0{ynATe?~Vld$j}H@4mwkQ#s`jyFd<^ia>G1gS!bR{$UbB7B`^Zqmc~hL?u1@y#=QL< z-_|O2d1JZUIq_*4j*4}7J2Ku4y!*Onx>(Au!G9nn)P8+C^+A2f6nHi*7w%-oQ|-{5 z$k#h3E=gZL!^YZq*WUp(e1Ox<g>S48y_b!kz&)daGIrD)TC%Gk_1Afz3} z+56)Q4sf5Ut=gMbtt%c|9D^C%CLCE;d1hCW_!zg?N89FnUhi5d{^s*)sUQC-8$ZRb zfs3GJsV0+7STdB~ZeNrmzvn%=y^zQPQ^SDj{M6};)T`iZYnb6{^bo#c=;1Tlt8n-D zy{a&A6WS&X!eFX9KND96hlvptSWnC_J!FBjxS7v?xU32F+5`0yn7v9&|_JJ37*zeqwpTkGE9!PJ)RQ1$bB%)!rCx8Srvc}(tMSPqkMqN7QE zf>PScT#OQYMXBJqAmLjP9sZ_D6i*oz&D}pw|#@1Z-F8Q!k_E)ncN796^2Boei4s6W{rOe?DFE$$x%qVd_}3`6LYJ?9n&nUN!bgWP1F2-P z%A^uz4SHdD6?*5y$6gtG>W96`jg`d98}f^8gK6Q5Ob>@xBCM5-hGZICvJ#3fzXrdI zY$O!+G1`9+;&pw;CsF3TbL%0H>u|kHyl=DkL05RYizZj4v6Tl8^ zZ}R*`Ri?pz#Vk(oe%~Qg^?CPt#0(oo*@ydd?MIf3d}P>y^Ra0)P8yRR@$5nq=a^(X z7rto^ripX(GCjmG^49ZdoaIx;yjH%qVzL@r>TG;WDa-E!3-htF*j63QdT{7 zckJ4!RT+TavgCMXhs;}@C_wUSTk0lU321q>o{6aDUx=gqY1RrO-U9>o_0PF0Cha(d8As} zBETGR#iW5(N-Zk!?gf|u;i63SVqdnH!|+1cJnIY}FcPk_kyPr`?!>0tp)5&}Wyn`D ztN`+#=BaWkz2mhMX%SEN!_29o!qr8OnS37nQmCa`BmwB^Ly#aO8f-H9dfvKywzUJm z*%TZNnAz{ejXwvttgIZ@ImXHj$gvzP-nYc)gF`KlYoE=<@>!o4<5yoWd7F52sH||2 zi*A(b&7q%8y~b^O{@R1#y`Et6uEo`if7oU zT8BY-(Zg}!WERB$&_CI%rQX6Ge4}UT?)5Ya8JCQ5Rco9OvVj{{e~bad42fYE%I8} z@Wvdy&!ovj53#~Ft?Wn*l2V5ly{S7z#sf7KJe?=(0?ao_R=BIu>}67wnC5$~ndhoU zp@^(YUugc_A_Kl=CXtuAMQ5k8^)6RauD~4d49@6ASDAM+SU44#MRUCy@8tkr}K< zqw^Xcd(hz>JgvBB zcc;dCN*{*`gP1@E>h9N+E8hrg`ZX6C~)-@`gODK%)|j&RfKh&202wtn$Y@DtoCn&#+!xe`=LK8fF=?%Lk| zJDlw|R}YG7ixs^H`8oLmRxvISRQh8I5xtpe1!PV#YyY5Hvab6HLj%>wDUfGHyTS&@ z-EmsB1uridmEAiU8{V*@x9!I{q{)c@OHrRcQ$1*6ow&b>S7V1hBSNL!musSApXI7{ zSRdwG`D4~8rmDn#AC>DpNb?}d6^u$ue@&FDpnl)dz~TpW=4QDhBepmZnqtKcUWq>W z?m@T2%6%d37NbE`T(vXE<+WezV&K4T;%~h1(O2&(J_)`g-8)i}7dj+X4`08Q z@lF_!FP_O8zdqQ0d+x1vCh|Gr3+ugX7w=w(wEnot}J`ULQwH_v@MAS@CR-NV(^|!$hHX5!AKi2ekM+myWsesJa2k2T!I8S zdfNYCfl;3lk2vuL&VV=&M#ORo69{bH}-N3zXd1 zUgTD&p{rD4=p?Hm|;FY-o9Q{b;{L?VoS4{07^)aJjEX}@z%;8eh z&>lMko3Ya_xO11C8xJntqfmJiZ~|AR6ZpEn?n^@o%8u#Bb%;&c3}@R^acCao7Ua<1 z-3S@@3c$3PRwNo0??Vde?#CGzLn*P9luIhLNUc8KId=xxsD4`(@0qKAP}cXqC`)g> zHOYR_cTESmjFN?CB0-Y=(s*%sn1^ zY%pajv3h;3=ZyoDUUe>JH0Fg=;-0!?uxsgy@>jv2kJEIWnvs~)B8;y(vPB=yYt7$b zv$Z<=0hXMKV}} zex<<^xM$o|BG6Epa!G{f#M$#zw06B)t!u?bL1^UW;7O z?kvCwdo`bJTM`-57zkMn(B=bpv9;bgovv1I@>?7iIGta)Gt@Vj;Dg}3sAj!{L*(w% z^0(@xn9nI79oYIt7ibvFud#|(!#Y=guE}+3{#xN1#!qy$f7Z%7m*}%&uiaU5pv|!q zQ4hn5F;fO=@c8L%*1T6$7c#!9lHpUQ-aY&=a$ z{7z5NHIP=5a9!hv8YNaC7H+z-SMGWuiGT>PXX=_j*Ha84E^)w*#9wf5)Zp^3;uD}S zp_j#b_l~^vZm4Y5vD`sUgWy2GtPj%BMD50HY_bF*M;HI67lh~PY17{eXd&s@K5AKj0IJ^%Z@6oXt7Ck@bGsk4igI~>ZoSrlO zN0ilcTduSv=q>TK+4TKgB0`NoEy>^KF|>&4Iven{S0O|E;F>k{Kqd5i9=0at#)Ig6wh#=eGgRMsHz2u+`4i%NWDJHiXWW4jKfaBaO9%Vu!tj%q!8 zJzdbuiX{7^Wj`k5&2+89=m5@0!k9rp)Z$EMhxciVxJTt7f%vz3RqZ}Wo^TVg7cJ^> zsji}sqj&rR(4YuP6zFs1ltO%%=GIOyoWQ=#>_+?7lpW@cYXZg69|JRS{?po(Q?PM_ zi{r0*2-J*pYQCd<&ThYWH0nTI#I_33t*WG7O1%`7t69PV+4v*Jtv7k^k2q89b4>?F zZ8GzAt~-$h^;`)xP^9i8Z1ajJ<;lOmnYSAl_O^h3MUW^q6{Ae~T(E>cx;e(Prs9GF zLxqS{u*#d>Xd`j*uFWGgQd9KX2}BDfRxG{8eIU!x3_av+X5cNdv4Q4-;El0BvF^Y3 z?+Pgd6fE={`nh){qEc3lz4#?P%9Klluxuv)2LoU1I{`T19b*Z|b+rIPz}{S&t)=b` z=%+jZ<$b(slrp9j~fMfGn1F$fCLww`6u52d2tIIG4oFH>rg7FqN7M(l{c zWQ8IeS#MRI-py^SVegUrCjxQ$n@i3YKk@`+!Go%1D-B5F?nJ@JnnD{k7kBuCm(1qk z;itU*vlt-7nNf%W(t&lyXmM*+3aum~mSGh->^ZBL3x};>FV@b6V9gz&AI^~JU6lNX zq3d46YWLgBPR8u@?z^}!)(chbHa1S;LS-AYzR|r#pWRi1M!J(_Wn02>FaPqmTf9Uo)im$~s^9zi9 z_Jl+{d1C&=$8TcNNl=x=N2P}FEU61~P2>M0Z|2Y%j3}7!m4IxRjlv`k6@t>SulcSG zzE;1FOa0yh&NA<(r<>E{#97Ezb?c*+ZFX^LSA9XB(!Bq@VNxReI05C5SWH_*R&b?n zjmiwba{idbnDqwBc{hF}{L?Ox7^Z2Y82ykYLW$+H;V$hlKME5dYcQ!2nXKCxVSC7} zsKuTz6%L$sXDs9JX4X~*ruKP%G2BG=aOfy^(^DO&U+dCR;E>|d1_QVFxZ&V#y!lrRif4tfg@VRV^@OZL6>@BQk~#oH{s4u5o%P^6OAyR zqy6t1R_b^9Gg>rU)v3m^{R&*iU;p5}luOCCkF~1Yb~Ov73#r=FmQ4>!aErcVC!rH%r~+5N5BQ7*&IWI13zvKE37wCX8%kX%y4r@XZ zJ+M*!p35;j52>Bwi87^;2MpKthPo$0seT<(Q;Wa7z$pt`c(f+F80ap}ILFa?Q^hTI zPn#h-rl;1%Ot0KDKh@v%zC(8atj!2v9e|qd_o@LV*DfCLfe1xa-|A|n3nq$K`bxTA z#*W*-Q({|gFmZ4cxK56dSPu3sU?bJmfl?tLPaLu)Q21_uHt1vuGUe(O;7AE8a7aAA z%wnxSmIwFO)p+0b4X7~K=^);Dmp%+=#RPaK%AVK_fHPA63*w;KIa(T>cw0*j_CmSCg`Ke{0NYzErss=PxL zX(Wt$aeZKlP(?vhnai7i?tOBo?r0x9-1`G_+!cY@T# zUXxZe3ByV&IC)PgmhpJPID1d#~d=f$fPEJxc!m9uejyZ?Y}ve6dR<&yzX4wc23{OW&5HT)>_waa8h)< z{NC8SAsruegM_jxCVsn?AUHC0n6JOdGp-;mEzyx{SI}W}&Pw89#$jQ^1qMPc zA4eHlC(iD@6_oO0$@t1@>MdJWZI#2dY|HWemt`*j3u1xj!>5Kr_T9B9EALV|eW`?x z!}~FwpMfoXW6^oyJ(lEwfTh{pP+PxO8CdpF=#J(&CBfdGo%4iWVoPuSPTM=B0^}ZZ zjvdG014+c$*4gxHaMH0D?aDoSC^(kj6cZ8{t$^gGOAonm5|K}5IlmXJjACAN@0h73 zO?QF+IT!H{f(cosBG*R14$ZVPkegdlGb{Kn{Dg{~b>#|++|j6uJ?Gn7bXeJkn-Sc4 zznw5s?eD^$qwG>ppKtZTaZbgt#RL5Cy`hiem+g$CzCBh z>KmSiTbwsv`#iKADN|h@!D*gXiWW%f8;v*nO_{uT9Q>y%&suagLTAh03}H~7AIlT> zn*ZXPR{ddp(S-VNv^X>S1g}(gH6GJJvr<{{5H7D`(~w+=a;SLmK`>Ej)%VQ~XDnK6lqdgF=jY%rN-Br@B3q+nVH-^Il3oQNXmYw;GUqvuhYJ`h zPFU}kZpDZ{W1eQZPXEpiKh|{a$RWdteeO@HZCp9c%U!Xb%?Y^)p?F-bpL4{9WC5HG)T zYv5?#qQlarVHdm!!M@Bn!H~!(^}NhI-45$rM_B*ikb285r6w;V2(rqwHHHk8J%wq3 zu|HQO9)HcY=9MRbUp5&2^O%?YTN8)8^XRb#$Z5};nv*s&=&=7V912TM1D>;8Eg zg3@uytjd1k?%Y-?uZE;_y{PQCcf}~JC4*ERDctF1wY8Mp1aZ;oFS?7rV@(Y@D6?5( zg{57pi&Cp|A<1YUR3O5msM zVT#1B*PZ$6?z)5}Jng$`kEpb+Ekz7Z`q+b`U)!Ydm*_ak5qU>rCAsN(_Y1sBni-AS za!ie85ns4s!{)9g5>s^=*Zp{-@9}|p( z(-ZM*?H`G_iQ|Dxg78Lpr|ndKFHnvPqgYW*Z08*Iybjv_Ro^1&U=mHe~O?W)M5%7-V*Mi<;b`1~b;sus9 zEqYV5_$|zfqZZfueE0_$LflHsCjDTbxMiD!*G2``T@4t}$ab%F-MEotcJ47_;W@YH z77EhEISqy%;Z06k*PYQ95SYe(qOR9bBaYRGr)1wRMZ5%Ijjo%%Q<`MMm58@$A>=~+ zHs58cCB|a;)-3BbbX^R{YM7h2e*XQlr(yXWHHQ7`&k@j7n4Kbj9m2wO4TzBGY5T0O3 zZd0nn)qJd|xK&cG+GeA|%Z{txk`VZ6&!qy4@oD1pQSWcL25>plc`M}VVT7^36mM;o z`5J3Epl8(!4cO`v{Kf}6`@f{8g@4H4N%c88yKWpEBgj0Db`R}v)azu?3nf` zxBj?ni1^xf^m$l2_c3EII7u01g_3UqZ`!ZOV?UOtM5@HsI#}Ax*%wkGUGA;y0i$_> zUL#w)ameh}ct6Mc0_8u3^SC>04nJHmxC$cxu|k*oQ4GuOMmWQnT?Ukf5)pG?5#uXy z67lQBHgWhWmAy>6L;+MvE!LQe1@B%&<^%oL_yVAaa5FCpXoY(J+bX2oI8*!Int-Jh43IAk0^LarK2PCLqfML*>BsJe*?8vgTIr{TzzOS>A>41h;CWpZK6flhgX1 zQmN?DCt{~Owd6ANZts?uq{i_h-L7RZPNoP2XX*j_cw#7zI$VKY7n^#@MZD8a{GoA{ z$lz*qo}}aPI;7`-MLHR>Qyqfd`D!V{_T?JLqVN!?{_P)@IevA-efHF@R?NZ4oimBz zSq}h`Ol_S&iy&9BcQ{}_Jgqq)i2WBtOsI^Nq5?iP611CrCsNvka z&%4Zt=l>QQWbNdRj;TI)qnz%#s?(c}cLXoj6y{U7S4doOM3`^e9Rp{Ig z@;lTs$hU$Uz7`om*UC>HC?1f?-=dhdu5Fha)_Gt$T@DAM3}(T`U9LkP=cf5zC3Cf8 z&IxY?0g^;}{{JP_Q|@k#`vwgoh9BQ}qinZU?x+nw`x77#n-pIz)7PG0bq7H8J4&kqci{PFYOE$m^G zkOLj+(|(jj%JN|HJO?T&?LV(_&HFunZMYi<13F2nIDgn;=knrH5-m^ZZjRLa9Uk)L z$o^?u=j8_Ky~f=OBvwan;D+ug!JcoHNyGhrI4_&CFIUg4+zPkiBvxWts>9~yrHc-_ ztUraZR9QkH%V%lH%dQ3$_v?Uk&&KyG5r|u4pMh~H;^7-LwUG$gMX74FG*ZDQk<}F< zgmT7$VL@G_bwQYkc}^a`@pKpsb*Ond$t6rpIZEruRBKvBB6D$2*pWdv9AAadY!*ql z^Fz>7q_;kJeSdl{*VbG!Dt#E3VKdzeWW{nuhq#pPxqJRZ_LChepF_%xGM=6ma1Fwc z9;$G7E7QnLGXNKom$M&wVs3)QE5Z#J<{x?$@E=ZG%P~m18uxfy%8eWjijV;W#QuBY zxc_&i!#hB%eV$~#dx*u@M4t0dCPnL@OEy7|^Su?*tt=WaL+^oBKeI4&<`aWBw%$7| zL|9J!H3LfmmsM0^wBVQ5%oN^4!zue#tS(A_*EIq$@1j0wO2H+6MM=lDTc6)M-Spks z{?PC{-1)poia?Nom4&4}(8_moTmp3Dmfr1stNW)G;1`SfU2w2Kn|@UF2EUMNR){df zQjav(Bns^IyprZ}6h+ep-@`xnS2=p#hjr9AQQU!k&Chgl` zYkdDObrY$7Gn9qArATDX4&Fc4j^%T!%^^6otvgjaR=o*ny z^FAtyMjJ_FQk1k9%Osm0CAJT=dw0uDO#=zBRF%D#1)ND+=Edu=hrFqI@iRs&huW6? z>mRWgsAi_QlW3t`YqK|FUk?Sz6D1k1;D?QrvFZp@NXWC@=++h9YP~k}XwpPDQUv0q zml2^uy?(7Y7;34O+}{q)5+UthQ^EK=N9GKv9ZJwJbCo)o$@U@=smtPq4g1jMX~Uj& z*^n``Dm;elyIRsyWEEB^0}1B}*rNUqyDGTb8ANwh!&f>@S%c02 zrp+v@amz-hQAKn7zo%bA9GFzoUy7bLq9y%y7n&HWUJK<*zCR4 zN1YIKyytjn;d47#>$9gR&DSV{-^JxFT5t^`+qJeFuaI=zHtEN%2wm>QY`*Z^e5&}O z|HX?&Efvo}Z4bVtevpFcr}ekt`tRPII}`Zn()GafDuMXDB+d=qPSQ+EUfvNtVKq(; z9nanA-QkL!OPrzJh&FLx%4an<8O`}ZwUT-X_XO;qE(GWoESOXSa@*2h1w`TFMua$F z-!*?MhacvsV@S8RD}s;3*DkJ!W9<9z;3e)Qma2tFSl1|VHRqv}+5-U1K%G}Y5tmP` zyz<)7p&c#L595|eyQ&)&FS>+*Nj$SnPUzl5M5zD1#QFbAg5==`r>lf_*F=X*A@aMr z&J&`3H#FIUY4wI>cBU9_=qnRGXIkmj#)0sBysU?%9V<=74?M1#uJ{-!`Ag;4s6QYU za}qu50$OlEbD%JgEr(1&4s%5A1Ci$Nx9$!5i^GQpfI`o~Ip#fd@nBawc-3Z~*%7>r z+?fgQA5s}*<-@W+%t77acHb9B#{I-g3U4=pSo~jC*#;dY{sgCpN@S~$H`SB&)CBTl z^WH|EaUxH?o%`TQftO>#A3P;BtJ+vM7koue3z+u&I6iPEbV6S6!K1z$tZI#`T&d<% zmLQKl$&qLOWxNVq{#xaH1Ehh8J|7vSj6jFH^nHhVD6rbq)=t{ z^e(tHXy7)fLeO`=2yR8xpGSj}Mm#QX9JrjVazE!27WkHv`OZYJr`>|AVO>$| zfn%b0TFJP#QWz&JPLebE$Nq!$EvkOchIbYD2y|1Hro@g3pfX6~*vcF`FffzAheaQw zElnUM+gAQ82OAFdhhv?2WCCsCV4j^H`K$zF!ky3G<+!plc;~`|#F+0W1UzT-aE>}} zXSjNx<7iEL%%yYnN|_l-Y1K$z^G^1j+! zkwW*h5VSMKc3KW}25rwD7k_tEnOMvp?wR*ClktPrj)lau&+O0c!>=-`J;h9Rv8r|T z-dV7Z`QJWy53cAgr<7QM4H^@pB5EC;Sni%VZFD!m@E>8-NE9(K=w&JIP-bPEbCHbN zK&NNTVAp0Z!e)J}cS9j8fr6-DZ}%+WxP|%75A8Cq#rzKPgoVn{Azuz^dtA=1=zGo_ zz0H2Gn8^M-o&8~Rr1iu$t9ZBWhpqn7J$iI%Bsmg;fs?GUe2Zh`Ntyi0{n2d!^5dWp z$4ObG#Qkze9!gC0<@pH~$)l{!`AwK~NiIQnJ7%nM4{}J!&HdnD*mr+^+#e=uBrk;V zQN+3m8${ZrtqG-M?&FbjL_U=w%qOgFzQvkar;Nsh48t4X>y zSIy?XgV`tczjJ|~wTbMFKC$1oGD&A*Ph+^HMfAw^hL+EHSe!M_5pp&08dZ|L{kXCfomb}3ApcfCTqG{28K=CtC++b*luLp09KYa zG80MHvUjuK`B~4=lIvV3mVhT@BX%M<1Ps5wS>t0Zk7i`gZ+ z9oIhb&TGbtjRKQ%%>35`j%9mgq*@!Df@vKv^5&7tCu3DjvDT_soBfRPyHA;~s7XD( z3{UA+^vo6&eG1$R_LM*28b2-U=5OC#n0s-oT>Grr%I9$U=hV|B`p-m_gABS*Vm?xk z{8oHbBe*+=x5yxd8wn>`$?#t5tvAZ`j8LzhX{oGp&S7ueTKiUDbmLLxqwIX^c+bAf zTh)f{OB~vg$?)onOrc4Ud~nDEJJHUY>7b!Tad9h9rDW*$c|r~e>b>tS!aavlFI>5} zQlXs&8&|ynPsxbvSOg}(N+lgfTlFO-E-x4eXAw4AVA8-iB>?!k?1jt zwKA7~JUI2zeyCNau_Hgx@$$TVWRyxu;+J6i&{@W2t?kX2{EIjPFSc1z^)wJE-M&~p zDU`%K!XCiKQGTAy7YNYvtS%q(c{*&Q5S_|KTX9mpW7xKipF%XyeS90#`{0&$+KFAT z>Xq3$BVX}0)8tWVGn#j(|v?EPzgZpVD1m=FAI z5BvA5jByhkhIgL^7jdulqByH>I$j>E&L95*tArgGL_zsfj^}9?e;tiC%1w~&6QN4r zaT@Daq!QT=jm&oJEYN@yEPYP!h|}(J!PV1MXneW9CH$V{5nV%V40~`xf5E8L)%UdJ zB_B^Wh@$o5@E+VFPAij-C!M{kff^M@RB}!U%Gt|ruTuS)IIHC92KWipWG=mVW;07c z?10F%TNfV~if5U!DmS&_THbc3pEiK+&Efktg+lwG`V*FQm8FxO9M9qYMjx9Nl>B!w zhm3&qEYxp>Hfi4klhtI^%P=iSYd4nvCA91WEf!j~S0B=0s64to8!AF%Jeh+lE^Zaw z2$wDkl6?jR25E?NbiDkHDvbpHhKBH}B5n_x`AJEOvq#Gm-dBYg+)E0)s*PTX#Y7}o zej<#1|nj?iw`TKJcNoM0`e6X%_GNoSm)C&MRCErlWzXESEnY5 z|Auc)V|}(FDt-Y83K+C*ne)?JZ!0VO{o7E_BR=R+EXY1Avqr-zTbQPZ$voaY#Jb@# z_$nn`P%*(mPmd8blKTd{dmYdymXO%9l=JLpnchmxY`zyMML(1o9wkN3$31C4zrD(+ z@}9?~x^D6|drX>=_YhmfU?`vyJ!B-6YonEC_mA=_tb53j{5%Yn=m-OwvJbC~&?~tg zyJBa*c`tmE-uG0?6Dfc8fd2{D$C#7ss0Loy+PT|gakX+zhjtYm4&2f=BIX;wjcue1 z&fXteK}+^|_MQu_V6Z%wEfmJxVSPNP_y^m>1*`p`L@OL99Y(UhBgVN|Ie8`5Lh1$| zO+bU6N-6L8^xYnqdXq~v(o!;E?Q6~xRc1`^Y3L}`_v+7o-xtdhe_JfRnms$bzRWpF z%+?8PF=FZ&hhp|^ZElgPi(Rfdf-1V&TG>gpjS`+%g9U_IMqBNR=~{K#B}W|b=)szc z4}3kUht9MS9G!bA&M!k-U1yT}?jTGG$F~#tuC{)BFq(Qx>s#4&aEN8HbawD`)@(SF z7|lS&@O(;B@}S%e{)RqMsQW(o@O$5s@)`b60nyEM$gVFIIL3_?7g>)VfZhOev4^{% z$+%4cV0bfQzQX%04cc)vFA2}&70jBCDeCdb5cZIU*tB*g*1}t;N;8 z2G!zZ3Y!9U7rwXEg8EFBNZv2RSPF1SpF)(1RJ*nRJ6Xu7%%k(>HOfJj3Fr!o=k%TAcPwWH?==hd`i%JIo zEIi)V#wc<=u*i{PdiA*#Ew%+qS4+(d-t`>-S56-^+qiafh5bZ+Ul52W~?*~c09zkbPz zM(O%{I>atAxkIsx;cxh!&9=6M4yAcG^<`Xl%G)$6E3mj>%9s?Wea@-i{9&%$Q!sud zpXdz$E1V<#fhPs3q*xi=x>;mcHR8}6IM{Z}ld(>p#<+y{wUK3L8%o}{O|w(}27vdN zmTu;_Nq*vq{5U5cUQE~2$TfS&*jM~&aB9=*(kr@7<$?B!Z@q_zaaw&4$s2%Xo7F95Gk83{sbd+)3(tyCZ5_ z|B!;;Y1!$>)LriodkxDjl`}a8bZPJFmUW*744_=Ezy`%bo_|SY9`kF?hiV}xrg|g9zFb?$k+$G7T zO~GoYqxrocc+0WWm{o=Gu7Ui^@_SXRE8{&NLK{gHqs~zIDse4GJD-~|DTvezUmNUy zfY!B-i-cWV(Jy~h{#sNUcGg{q57*Nj2?(5+sFO;r0b~RC7SQT49CBHj$^SwJIZpQi z*;pR$;=fNm)acu*#u&hSEvqu#-ZT}dLTBLBPXWl++ke`+PGs8R0UY*L= z!_DhBXo!E9`@Og2_sSj1M`+6zmiR}D#)StL>x@*^v&tp^DM|LLl%(aM>a6i;&Gl+& zILd5h9QYAs5bV0BF&*I@${rBiBF_%h_Lr^IFbZoZ?$l>EWnl5EsKFo zM^qSIQRHLb>)gA(@Y`6lOQP@ZiV5BSDKy~Pf7Te5P8vpLqiuK>BJ$E>;8aQeRqqUA z>|mIQBw~g4C9N&f!;}n%qT=QQ-pUOwXCq|~4!7>`%Ja2{4`Qj+Z+|ImIZaA$A_NT~R`x8KZ_`k>K!C$V# z=t_`W5X%w4xZiQ)dVj-qsmA&ZR5cGg=VBy*clSPs$z>}SCvh;_`dX=0d|XwD+f0DT z2hB+`?OocsDNY>fb#j>ePSJdIIqwyEviDU=?GMd! z8UNa>2XzWVHs$`rZ#*Gtalcl6s$y}`T#jY0p)(JH!EJO=&P12zKI8HK-1dyaTIWTNMS(q7CdYUYeH8h$nu&4hn;0KfCdO*z2E$Gc*Xy7 zc#8@Ua8?KhT2pk;g#RM6|K9npgvdwddzr#5O6K@m^4UdcnV4n52vvJtc#clK!2%0F zbIMVw{l~q9URDaT5&HbpK!=LtdvA*!mi>LMvhgc^XlUN6s4M!`GvU_E)c7DyC3HHV zuDD~JlvBz5#MWJuOLQk9pv_`+W}7(a{Rzt3ETm2v6N2uwjM)COBo}#gG@xX^+#&T6 z^JEET_X7*;g~ofL^-32N^e_~6=ZE~&gGGM*a54kXML7_WV?{?v);Rlp-G3*>1Rg?n zMQu#&avxn!=W*iolX+yo1Lr-?`>oORCNEP5VKYTD0*poTwYVauejXSdgEUJKrYCs3 z^U-#guM1v@nO|RAE?CJAKiQBiJu2}vAl+CQL4nx2=wOhwWNbK6PxwT z*Ov*D1CkPopv3sN2%CK?zc)T2;yYs|whJ>>46lL#_#sld<8g9M2R=2@OZOfc0+Ctp zv7wmlE_&q7)Be>O;I#TLah!x^!MAMrgnu_KoPuXdKmWDjQxmq=1}l~1Ofb60_3tib zv3Is^2eMKsgOcc9MEPn<4E$Si{V}o5XExh7J(w>!?2#{@p;jrGwg<@$@+X=9Y-r-J zw|fG^`=!nA)^oSsLMc^>Cg@&!!*5imoeL67+@Bh~~P3bM)o5LFn@s4sz94w=Hj&wQbk)cW!gGcb1siad7YxQc|%_ zY{z_&g+fzH@{h~xz-$oH9?~>#bW5Gy!tRqm(ssQ~Yq@nZ>yPB#N(C}!W`LA;Xs~-?mOIMHf}GOSNXJHleDjD2f`1O;IcMj9EoX(Na|Gy|-AASU*(l8Zi>FR}iGJ z$NT8|zxStn;5eRpT&;vgk=cCM@7uHD|U3&=nUq!am`)6u(%{uOzo_?c!6mzWJsmw zi5fGBdLV7=VK6Qdm6lj8-}4)MJ4k|%0Pt<$elwk2`Z;bv^0ILkYMq&?luB9ZAddlf zSo}%B%vNgHZKk?C^IymMf*k_*tD976ndVG*H-49MN8zxel_jL*TcF6CL;udG8Z`fh zUelYIkFE`%n0CK|$|esto6?MJM_e%J-DQmw9p(lBDV9}1o-yw<3!QM`?oj!X>M=Aj zFdAhd#2>%u%G{GbKdULTkS6OmSeI8`Ge}kw;3vWd!?w_RIbemC$c{UZqD)!#rG3Nt ztk$^2Rl*6vz}oz3+Bxptgd|K&{UPw+c4pn+kCCH&jqzq}FFgKOg1iAPaL{xK!N7Di zt!elQEJK{Vssl(TV{bi&h`Xpb1xp>nX~(0QYg;7p_fNn=Q}?R!j@DPo`Wj8ZOgCsA z)mWYyF^S77FGzAuFErqE*DHO?LHMdQWbFoYQ(d~pv_6G}%o*#?mD@bteL}xb5X>Rh zzGX3PX0W&eG4ELyi%1rdmduXJrppx)>Ir+B)DP6{XQB8PH?@p5D2wxi${*j(#-n`~OjW?A-y5Qo+N*xHDac`?geSsc7S;w5u#WW()sx4XhSZpdM zj+$@hjY=gH0s#@30Dv-tmdjD9J=VKO;JqLudk3>Jl;P+2-n6XSGh%4Ykm#e#T#NCP zi`ESz_4{&2q;1!|Rsle&ECbElHYoaNP*#4W4&zctEt*Y1jKXSIHv@H!(55c3!;VYV z*y{)d_g-XODj^zxyF3raZM%3ams--XfB1vdR{xl@6CsQ&*MAI)5H-9B&pg&oG)Tgi zz~_V`0u+twu>!)7vRhJ+O}foH@0VJ>nGK70b#V>dugV{VVHYu9C>`=4Yqi+v?p8A$ zVQlcVHUa}*yJ;VvS*jVyr0bSoL3l_eDJ-I_pp>%wy&-p7M8q)n?p_uq8$=&gc=8scFj|9PF;F)E4u1Jv^O{!?5%3a&l^ zs$Hf%ec}wbi(O3aCf9|P(X_+Dc+c9iLxz~)E65tgJ6-!fd3W|N|Dn4zXv$I={W+hp zg;K&9pY1>eyWR0--iRA-m(+{|y<*zlDml1~RZA3K4bqnIXn8~|dqrQ~ZdhvMAqvwq zk9Sm^v{qvjdwv>LQe1u6EZB(jU{jB)RfO*DWHch&!bm@Fie8?c-JipvEy`&=nl(Nk z#9fI*84kQboo>~|1GquX!Lxfp)fUvCEf$uYA4Z`MQ?WT1V1jNz)h4}S&11&oh8*b^Xa zggCq^)vCzkuX5FSXecZ=txsE#H#8`NhmsfA@;2!cGglu$(N2(Zel`tjbARSDAZw#N zjX`_r1d#iR0MAMX*+uw3VyRRMCts=5funCr0r@$S(E1|kXK;2a2M6SnW+P0JoR zGtRG~YI#V$a@B+e?z39bTJMwm`m`Rhw6XT=>Tp5Jmh{T!+gX$k24pef4deh9ujop> z{vOt$R_Xm)Ezbvn_5^ol6M=uUCmtMZ7BK^W@!j#ZJIc~d@8f?vaSCl#RsetbHT25- z$Am2xtYNR1*S(we4bSR9IgRSS@$$R+`uNYgUNW5_uhVz7@YT{@3%PGN`t@3# zes7n1YU&7kU24mgRJ4dL5NIrAkgsF=AstJkS=000NR4?T+dB6SUFoa+gIyWpr)4I&E&4mtopW-FSduz>af^g z5F4L8qjc=k1oPw0Rsv2tW-^pU`DpuIpiZ-ADQTpIX;k63%Txd=9FglfTxJZ+>Gj<+ zr`a5i1r4?Z32qF#s-QD+PkDX#^YC90%|A$%5900o@=85Qn>;3>K!(5ecvs1qOTM9Q z#bwBy>90$CPp)jPqk0x&#|VOMXn;=2*NBfk3~aq`B7(e;<=IQRHYvqS*u>220-V#${^CS9bS|7 zABpwF+2L+K*Y(T%(Mi(8!a$C${dfGOfUy9h0U`6R=O*{?k?r;84lk* z10&YL+8+M5S9&MLCVCp&vTeBl{tmxH*`_mSEqdY9wSzCZMXk))f;%F`K;{)hckWc= z*~MKC84bz>Hsl-vFb4Ki+KwDMHcdRuFXf{GF(vk-e&F?E_4icG5!k}r|LpBp3gTKf z@f`}Wf_vqrR$Y95goP;du6S{<|C$+vU#Nid5kSN8LT1NnliT(k2s2_`hZCP58YJw!$L98JymIf^6PdERE!R=~S4~*I?u?AV zx9y}=Z^k^d@9DDg631O>`@Y9k-3_bZ8!+O)&|>Je1|W~foy<&iZrp*-2bI(75%N)Z zUo#y0Qt$ZKt9pk7*r2{f56jf(?TX#(EbMS2Q>Hue}Ei5sXDsAL?DEn^FccDlh>iVY4PY9eEv&%Zy@zs0Q zJETNK5O8#hGvHsR&iOR(jn@5G>w2~6pH~6zyEy{|+aCCywp-5K6CH_U2v7R)BX}S6 zr=5eLJz?~Fn<%&iLR4O@k zBmse=3<}F%xRekWff`#kGxd<~nAqOt4H@*Mm27S!0Bz3#COs<2goURUu zEKe-26WH2P_4y3nyjiVDhx^V~)b-7npV*MFOi}d~iO>Kn38hH!l{6v&Bsx!z^U%?m zqD6~EPe>-(jYBHze^D*~ytbus)%hrAzp0I9(9ft)1R zVNz}!PBG%AdiBG_ys?+yT3-9n^ToBvPR<{9ta&j_bP-l_4Z6P~JcU0{zrH3$=llSm zn<-@W{r|)Twnoo~lzc?%iv02c=3RgsuUD$fO|Flt^=|s?_+c~%)Hl2Rgeh6Y`VfJ) zZv@;rFeczr_|88Or8=dL(=F#H^*DCRJQiAgJJuIincfdgD^n`g7CN&!+?mhdF1Ixv zB3AvXG{RArAOIq=WJ=_(zQmFle_(!GbY6e3@@)CqS4Hw+M?jrDs{cGJvJuKAQCO4w zCzL<`X*=@#Qq04n(GPrv-~13(0IkJJxl8Rm&?b2HikUUzxM{9z)x~Qy*HQ(P>656c zT;kjZ7Y`dC-CDSU6l;&nN(S-RA^sKVx&Mmvlz0d#tFH&2J9&Tvun329f;NuKk~F6( zEQPCe+6MZtL%eoht{28hb_i2%u4sKGb!olG28&g#xzJTA(Y8jOS{9YkHQ(!YlpA9( zg}`oDl5|fkZ;7UiR>eOmRZPO4QpOlxG z$hhfrsi{)nISYy}7N&J2Vjj*9JkM=JOu8#8zO6X3ug26rF-(zHk%Q&DC!;q))%4R&6I*|HKB=-9!%K^=yJ3V6u zo)?9?k9rC(FzSj1+c-ruHzy$$$vpnLEUH)cUE*{lunA$8Tp#icl?otqS#ottu~BTO zn9SH|3aHxU8V_vYa9N1F@ucfY=kyH+srP1-i;V_R`etUG-Tq(?zEJtd8laWrMU+D2 z>=03@K#~;6FH{5eKay`fE(o{*wgrqa~Nh zgdKGQ7&89OpcQy)*fmFx$iw{9dpf*Kff8r0me*NOc`66WR&Ty8y7JgzSP2tiOTvT< zhJau@M=kEgk(khzWwRhC>VT8(D8?8df--6~hPt+zk+2!2P;^*TK$mgPglq6?{wMA0 z3zojd(PJ}h3>?lK(_N>!*anJNbv8HLdEYa^t{;ZYGbH!6#HzlTn>dePI7d8z(<#hC z>vG8Lb!^;v5$b$12x`_8L3kAysx8OX2Nuj7#cBp!}lw(m0Twi&Hx(g^_tVK z?hVmY(j+mA1vz~V>27RETO&Vc&~qLo*VWh$#Z`IX^5^bEoa$I|T#o5(30*ev(Rp^z`VefW{L?MJ^5^*Vkdkm- zpldD*vrE57?+GYef&60gk85BI-B9x^p!#F~ocU-$Y2emF`>imh>b+;YY=(Qj_}nmB zK)n&^Q6DKNyJh%$KlSAQ9qx@!;!mTw9aT9`tQokIoEdejF?{QFMb_2+q1_) zr(NO$v?VK-yxpt3i)P&W*XyR-LAf8?aziyGi{bKBHN4mEbhmEs&1aDt#k!=tpBJ_R zJ^hK3yHKX=a~(Q&C^~mm2DgHkTkkyj1rk-nCD-vL0}p-DKievdD5^=1AV$*k>SlOA zwaWR~=bQdq>v56&n-e}Z-ah1IIzaWb3rCnMdBr_0tdf|SYl4;+mF3%BF5#{1o|D!G zZ$apuscf>kRFU1mqcl1rXwoVw>7YQIL8-Me#Di0|&FOi#2=4Ck1~+ken7MO|cR@XY zrQ9v!3&;T4xVd9BHz;G3FUms-zi7W>qE(;k(dD%z)Wt4++pe-2>_?qr$Cz~vceq$O zFz^G=?=WFaZzc&?WsMHsZ~@lTdJnkw&Cylo{t>|NJeHw@&oJ9Wm^l-!J~-;W_mFk5=3vj8J4PSinlp$Y5;Z zB{SkVRm;7r%Ig^3`4^r;Kl?nH5pOWwB8=Cs!H)G5Vk5FoOu2uV$fcxY$*}4w_sG3) z@*4U4$riQeqiMBq8=uPP`<5i@Y}n`~Mn>;)H+3^~i!kF!pLwL-*(a}@te97)Z;p9^ z?(M}o*;yWLTORX`P8RqnU!+(yUld7J@cP_P`;2m8c2MNwub4ik%r44uq;$68h)=XP zyRUdvufOfVajOu@MBH&&49hZ4lKw9D=jJXU)0>Q@CiFzXMr#|-MfnzDW8C46Ns zi8JdcU|sE+TiDqTsink)m8akD)_j@0S-bNUyb|d&oMgdp%k>Podv46eaxz$6Xm9yX zhQsY`n8=uRPM^oyJ^WUIQ&*4q3^ff^-^zhM?Ie&Ru)gu~G9cL{WE`L8mBsC}S|OpM zgDuC)99?K3;SnHzjA_cIfSD8yv7pUkp>aGKZhw2A^WYx;f{Vl7pR9R))ZV9A@H8XCYJq8 z$qP$uoaEEx_EKHR`85CHXp)GM)oWw^@e~I~rj$zey8U<6(X0ufPBUC5+b2E9Yp=~a zblZRoti|p8(aCGMmBykT(y~P!uJBtw{W4Xv2kKpJ2QF+JmLvrlK7G5bEnQF#u&2Bu zVR;y3R?&h+N@)7U54*h6Otp6Tb999$KWBWHDU;Wl@T!8sqaL<7lfP#CFIPwj9$XR% z^L2<1+9ZBazCd5oBl#RY`z)XWy3Y`Sg3-^x4`Km;B4Ejh-GU zB42gU^=aw$CD8SBn-@IPO-Q-^DWg46M8f|0t$5YeIuiM{?$-8p=wyI$vne8liLJXT zt%$+Zl(sEIePnjigKao3a!}!MjR%00)bGI=q~mF8=}+l=*bTZA(kn;mzaG*{J-@>< zUqc`oI-diQ9KwH1%$$SU+mgos&Oj1EjOYz}EhMF0y}fG5%O>3%c>IVJ5KjoE!SA#Bl!O>3ps z+K2B2S+x_WHtA`v7U4^61?ch@9H!W!Z^Df(+wd8LTpnOs>EIGpWn$9)30{A|s>dgg z|5Xt?B7)7;?L9vNqBo%llx9oi*!FaF#%m7)E zE!yd+`WjA^=-Ov&`1bZF!Dg2~cB=XIubuX$FU89XZnV+m`)rx# zCLQEAJt$$Stcd>px&(ZcC3<7ow%0%;q{LG$ZE)!`gP-1IF_d&v#w3$;u;}qXM{&?A&~SUd%no(_eOnN z4sDwuDP6r~jDSB&#-g(3B=?7Ld0`|otuB%i{9x9B&*ax$Q4~=9?SuQVy<%q`*d}80 zcWs$7A-v~0&o+*RCzW_q50n>`8#s6J%4Rd1_K0Gr(CWhsO{b<)*a&CE=3I=?nI2Km7udam>PSfddgWYUwd79nb{$M)JR7Uv$`zB^ghba4ON1*8jfv@cJ@ z2DF@*%?&YkIFJ~MZhEjb#SdgGWV!*!8~bvK*fevXX|Ep4wQA+1NI78uyp&E+O&}V- z;{s|x&s}<7AB9JNtln{4G11=1gjqK%9D))A70RWhxP)~7Qoiz^uyg#LCxxdH@Fwy$ zW-I3?bnJEWf!W;w@%S-@kZ$C6fdXUg@i#5E>aC221;5qLiD7SHtnz3x=5}>54^JEM>g_%__!Ixe>`7G0&Z6^P zhfwbh+rw(~(Urn^!-A6qPI~jlJoS4(R4M2yUTHs^J?V&`k!A9k;p;+G>>}GY@h$YpsY!oz?qJGBEPxAtde)7FY5Et~Q;5NWE=i_X08r67X2 zH1F~Pv+#HqaG9;LMcylPzWp)yIpWPwzI*bdSAT0mOHV}{^hbsW9hdI#m#Vb$j_x*^ zZ3z)X`Y!ioJ%?c|rg`_yslYbf1WH1uXz={=nAhpZGx^bil-&#!K28Qss_a_Uj!DdC zsxJ%~W}nj+->U-^HFq0O)OrU z#9|oo+l}}Q2K+NZ*&z1eGyt>D*`50=>PIj*E#EcuDUTy}JgpSU01;Ap+ict*v@l9Y zBDRIjMn+E>xYc+Fs=)LB#jKXvk!oYil-H{94PiRrSpV5-ZvD@imDOiw^H*$nq}dS2 zeRz{k-Q?F1Yc>74PFa{*oG8hr@i~}t@&RK)a9)2=bKi7MkVhhl5qVB5_)*XNBcbdO zB06hltNU9#FO*?I;39>=vw*Ew1DR}#aR9P^Y!?SwyDOLTpqijX##K6K_LKrwekR*i z=UywoJ%R{-x-a?Ee-W1y`3vDRmNNv{E2PVlJeSHb7mrUD$@i)uuH9j~9SkQl&KPTL zNp>#Dnd0x+bJ6n#H{S$VaVETT#!Z`}8H%P`(oFb_btfdn2!k{Z9f=j`?JcFcFcme9 zjgkW02hs$_DxP<35~Vjj>VCNiJ+hW-`OXxWYDUNPiY4vs9NJ?75)($BT`p4D95{5SF@7VgL^xWEFPXiInWv2se z4G?t(Qk%ZT59yv zHg$4J6qNBjIeNJf$xm|%tw2%Lc``PN3rXZU-xw697hy>Hjwr)<3>a4JojAkl6W~x_ zg3ZA}y-M2cjqd<0-T;?6T~PietNh!3$SYe&jQH@>0w=_My&Cf9ov%N4CM$(2l^Qv` z>Z$f{Qt7~q)WR-XA8@X^K|+%WC-tNk3hl^7(Io zs@+1DL?X!hd+OGnGEV%g^CoA0O$nG0M(H;aMM2OhG{9`f0>T+UIRB8E^1=2iRhaa| zyneFYUBk&TuNwaH?=9UOd@7d=$>Ct&&LeY_F51Q2+H`f%bcC0Zo~?6d=*K zFY)kHwat(`aS&|-kUXLeX{&pCm#NzTgsJZUsl8aYY43Qa zOX^5#j|c?rsO0l$`u-Q1iU)}Nc0tW_K;{;FDLYw=h_|Be3UY9lv;jp=L}biT1k_?vMT zX}VY(O2?&Dn%uh|X`bw(p+rpl)sksAz9-{Z>jKj@m^n3waw^j=%*?X!J7O70^*!)a zpdk7$e5evrq$pyydaDSeeSHfetF}iH7p^W<*$y-m=~iqHwdGr=h=f~CG>%mtL5kwo zS!ZwkUGe|^x8kq7N$-P>7H$q!*~Va+>GXpd_4yAoy#4y$ShGZF4h`}jT&ZIVv!jQR zcs|7!C7bDmqF|*ydvc)|!orL;kT2t>v+V`Yu|984js2Q-h}T+qSDzk5#QKg zTXC_6v zAm9b9dw@ykB*U|_1&>B40X~2g;iP1lok)GGBHVZO9Xz?q-v8vUO)}D);;#B_4}LB= zQc^jk_~QnRup$}4c!pPeJ8ONQf>bEih!cGcFHYVh^R`*p8H3wKbDZo;!_G{5wuovT z$Tt}|YEl}g*3t*x_AtZax3lG8Wf{J9;=cLkc3s(m&-6_?vl;Zs4ol=&s|^glO->p> zNy2u9hc2zeFxBsenU9O+4mDjCp|%$igro(7g>JLpOy|d{ujTo~TOwu($t>Zn-4dR& z(5%zPc|4VVC!4#5EFDhe=4IIkZ0>H5u~1J>;amJ50T7U7TLmdRq8y)@0#@Mo5azXH z^Zfb+xQHyRe9L_TVDiI>KVBw(r>(U$#*a!mLpDdquL}k0;#+Mc_BJco1B*4o-q7oU z+tgn9Zuds*aE(T|{d`1=BtM(hsY!H+a}zzvV{1$L&yyaNl`)<;IlUC&1ru=f)cP^f zf{I3Md_28pRRgTnj{!Q`{Phu@G@2hQonxTI~Y)SV8$r3pPKkk0m zh3D$>FNA2uw9de$8!VY_u8!|i1>~F7Yffqtg<~}9E3s0_n*LKW!;)+rQKq#z*G9*= z4669%AL|d*e9H54sK(wws3i1b`|a|CY8t{$>dB4Yz+w*A)lWZx1(Gowj{r_gUm8#o z86_TNeiI^H(AUiWC|RSSg*;>*VIHpvjk;+$*KjH1&l+on{rkr73!oCY+xS{TgmN=@ zqx=yZz7;~_8&uqqXOio{KKfILSnD&t@t{CgY*fxQ2T_z5fFd0B$L*H286GSJHuYYk zdgzAl=FVG)Ch*kT#j18&uOcZCYlqnzcN=d{SfwH71&IC5?^ak7tbYY^)%c?k2ZZ^x z`%lx*9DP{AS{swfTL`D{N0L*aBckQTt zMZJd(LfJJxUQ_fO&+;}y@={~yri(g=%`X15P5PLEQGp9W*=_kD$ua*~BR6jriN?6= z6U8ynER&9X`UKB{L1|T>~ zoxd{BBjU!@QQdj?UJAt8*gNel2Bn%HUv{WXY8PYWShXZ5Fj{YO- z3ihdL)?oumgVo$y}RknU`zYCK5gB;?i)gzQ^*Y8b^g%4dQZBf{OL%tlCQh+DR4MShNf5)$>W<9b}DS$Mb^W4+lmxC^U9ZzaYRa+l`d%P0S_NpMRMbLou#ix6|K_~}Tejqy zlJG=OAV)GB^TXFiUrAp2vk0KXiP7r-?zW}SY8D*y@c(Qg-aqY!M9aPCI@YxA)i7VE zgjl{lT6ICy4_a;3qh_K)YrYWa1bipef0PPDQq%F-GgGd%ux`fXPJyZ`E2(u)UZgre}s@B^V2zs=zyIC8ZI>%LQ+wXPm;Ulf6Z zex^b~X@L4j+%hp4!1ueuXOfNMj#xoDX3;T+u6ij|$Sa0>uSuhcO1b zNFcAewHn~2G37D!*9yQ6buF%WjQA)I&JS2GW<#`zruSQJ!u7&0?4g>;_2CyCsw2vR zOW;Lwgls|bmi&OIMB99}KjA%XGGI`E);P#trKh7#IwAR305&BXxWV8gE%xjtmtKBR z-EGy?FM|d11klzLa>E1tfbRGATT}jJ5cvt2Pc-u8NxLEi1yVAMSI_7@V;YETlMxms zJ{~I#kMTfX_BlW8KDaq@3i*EeLKQi0wxer=zp&_qPhnwL|Rtk`y+o>aQ}b4^;0Tk<(uu~m1tWgKG}K(*8?(sp5&jeu&{dX>yuqudw1z}ZzD z`+n!9M#}#lMk#rAA)L;7fmy;#Bti*M!eNU&S3R+^oZmjX!_6>bs_ZB-A`qrsuzgg9 zDe$PsVZ=2TZd@qz5ca7#+N=u+-6>FtngPCcSCwCU-3yCZ+X zAPNlIRMRjG9XqJp8gb~AahohL0Q*av>|EhYVoi<!98N!ozHU*q?9O3{ zP6Z---~NYkpvcoq6BFay{i>Od7niv-R5O}&X~ZZImJ&Mjn%_%?Vm=9OuX3V@J*B_% zk=-%H@BUMh?RHl9PH?mKN}=%^yvn%Soh~-frk39zw4YG$akX_k8kmkl{sd7vWp}fy z>=3@eP`Ywdm3F-(xTAc{Zlpv6#!FpPD@l#_T$g0gTy6Ye{b z)HoR_tdy1;6KpFn#M}Izv0pVPx0>4ljnb7EfOOY?9G*KdD#^$qgmF0o>r9$U{0KUK zhnsE+zR}G8q)zKuM!FX?miC@jmd6<4mBgFnRo1cSOxjZ@uD7i_4jva9gk8cl*pzhM z3+OFR=6TT2`lbaYql<@@Z;4Qqglx}9$}KdVT>=R0w*z(B0Nn}yTh1`MH#HCq>SLJm zXTgXC5cQ~<_goF49ppEOY1Kfj>K%Ib+wZN_iTZc-CI7g9U(l}1^~HnAJ~WC}Gb0kZ z80c{h0n&}`EtLv6+A_UWZzQcfIPpfy_HL6T8w0Jr-o9oJ$Uf({;b$lCk`D?dEIcnl z*Gp$6hb4{T-R|*aFD*_v8n#nXfbNG=hK2Mmy4&+%C1h*mwnwI%CS-6}iBtXSi?%J~ zZXe-@#J-<+7WHW?j|g?icfsz?m+#Cj*rcBt^!$YcK$5$i52pZ=?3JDK_0f}Rj{WMg z&$`$+_6jfQDc4pIM*C)=6zPesJnqPa674_#wcdX(eHNIUafCl*HhxbI`q>L~- z##_8To;z67EqypBOnmX#eN0XSl>D^$Vv!q~!LIhjLd(<+-&kC;b_tJ|3OM#AcZlx0 z1k#$_C8o`LFIfWPqr5#^uv{Ic9V{WRPi%Z{3&+uBFCDVmCFW7y0-QyGu6~WnTjtuQ zpJH~1{9_9gSTbsITH&~4@r3gW3++vt0x-N#|5}tp(ZW|*LzKL5FNTCj1j>mW?B#3M zfA7-JokN;G+_)0OL6L|z3-!K_D{a$XBY6BEeP=7rE#I{?3gopYpP%G1rqm}1d|)af zHo9tMyyqwvy_ir{iwI$>Y4K?~PZ@zSN!)iYxK+s0vf_+N&yB;ZY}XH2EWWs zNi5-}mAlqvrJyuxgtg~Uz1#$d+&Enc|C|K$ida;6bJkHJh@V5(Jp%l0mT)2T!Xh{| z@wNVtS2rl?!hL;f99}Rn-$V1AD$S(pDc&>vIYChOBl{GdI*uQ)=Bq&b*cgLlz|}rB z^j_I|vuj6Z5XT=SVCjyhUFD56r)8O(0?sTj8sPsp$wu^f5p;(~%ebAW*BFoSo%xw0 z&B495<6F>_KAjN?8J^SUE>E0{oZ^b28Yz3>!P7Nt9Km{J^IMtZnD2v!53iKnmb@Lo zDH+)s*;3Nda=KbkkK!$b9L4G0y&%L^W}YK#KDZM`6q_IK%p0e|Y2qVJyK2sLIrVrD zYd+*X1Cg!vb9FT#RZkr`G7HW7ALqE)7tz>Xg(Gqjp}V<(worYCJ$)x1N~2hs#kA4{ zGyjp-3DpEUY@8@q0~E_<5LOEoblmKK=N%~NIIzr#qdIek@LF9N{Wpo z(o`v+pP`6br-WKMy6jV_TUh6PxEqVts_(eRY7XDQRJCWnGye(b>qL=-pJFe}4%BNP zaZfsBHJfm^UGOvBz(G%eEv04ox_*4}R56s&3>ZP=jT#`w8cazuJL@7_~Qs6sY>c!pb zTJs{atj=jJmJO~CK2%H=W=>nMzU8$JVh;9>ddLw6j$wE8F&d_xJBzb#P60GZQbZi8 zFWQBH=S`{OPsjsT{QC(%Adub)%o!4CDUo^);>wFkDPzxab?^flN12dc;I64GH2L~y zCTc{b$y~NKCDb{@+`6FQnzv#g%Q~03l!UnIzXhORe%FDUG+c7`dcC~ng|25igtgWz zZjC%D{wsTo9r)+cM81ydThETy8whJfzZ!_b>&T@Vc-$O*JKt%ov#i-P^{J558t&Bk9nR4f_Q8qp zvnRPz#9El8$Z72siT~+<9#{?3KMO&37rpUmcaiec>NoV&clCh--Hr3Ti%vnlcpNI} zR;P$c9v%@c{;^l=7kl?{B%~Ns*xlEE^_DuW1`>}ea+gW~(pbfw{SO4D%)YSv!AqMnB!Th@C#YkA66bUBJDsJ8t5whBP^NnLl(_$8 z+2ogw%7qtF%5Foqy6E)vx5np8C1w=8%7u0kF=ilB+21~VB1ak?KbY@2+czo@#%mo~P0Fw=PEwKo$BToO9o z8RdCra@?q!ig3rN(V@L2YJQj1FG^moNb3B&Sc#u1U3n0`nh|r)A;WQ#i*fE+LbaUPhsO`@a0&vPNSMAlmnH*S}Ss@l^%q z=jRgHjUN47qJNZuiL);a241dE!)7XZqqs6_uKaBlL0G$F_0G>_?2S(=gKbmaYYUu+6Xdxr4pH&* zLY$=YKEh5Vtp=)zM`f!0c(HrPfjRRN&|AVdwJjX=Rg-t4IKsUqN%eWp@Bxrd%q`%Q zZ{r^!khMQi2Ul-RG0_XLje43HM&awhnR2J|vVFXsqeA;_?*6t0?9YEC9hQPA!cKFi zNH=42qIh{j2Y=E< zU)}+qsF-PG&^iA3EywNNzYl6}hvVf$d>qxP+*BeUW!~ZYO zVr1}-Q@F;)72Uz)>mHgHVH*|C=sf>Eo_zOc`=-%-LQ=es7{lby4`O& z2e>fJk&2H`I!}@5Y_1;=C2~*^TR`aX(;*IUC`mM2xI2~A^g-H1*`2#Ljl_?#Te3R>(~zPpS& zwO{ErW3}mzh;~UMGmm89EXz~hnj?L-wIZ26F1{_@rEIMkuJ~LmZAjDo@YZW3H_>_Px-*kK+361pX_+U>S8LFA)6rn^hjU1|-FzdGB`HehEd8{JlEwd}^; zKUEK_XWA;#&IC`NJ!_r|TmER1CwueR8!w}B#p$|YY^Rl5p@!uSV<4mduZ#J&(ORAd ztS;dRsTV)*?g{5rWIW6_RhkwW#MfIkVhY?mBg23EJ}+Oj|3!SF|3+F+t5m5{CL+{~ z!y{L}o>+_r@!F8Ag%|4LIgFfaUn_&xFmafLAw4E3W!YlIy=mHR$#ZMsfC3g7InK2> z9N*Qqv5u9=8LaEB42Wqm_@<-Ibhlgl65ZQUVbRtN)VqFioNu?y<^gHDalz-_aHj0n zGQ~Ud3-zsMQ}sy!LuEV@)*DOK<6im~?NpFAIQe^T=vcSsr}MQ3IJx*bzKHd4&_ij# zR5ndXMTk79615`m`OUba&(TVbFih@F6>)I57@xk5v)0UusfqKyuG2Fh^?1$hs&;

}irXLjNwW}F&ky}EDEe^M(7#3XBKFCFleyxGWagrC_{^4Bh z{9{NwXj9CLyen0yG`lCFNF)7tF&Lr`j}wY@DM#FS%FS;?2z=YMKU^G`=#5&(r4XP@ z7gP+Uc|?Vm4Xi^v$DZA`NS?i8tgG?BIx0#GBW-*6-eyzPdr3K4in!gv1Goy zuTszOEJr!g3Cti?bywt^js?y)>Qtv)Pl6;%*mPa@e_YE3EJJy|)ln|6CG=VrpVx|z zi;5~k%m&073Mu-YZ5O30#OI{2rVEap%8)33!KENKTP<5b|f1z9{q{Lta(bjwOK-}8lRd{vNnG;vaaR(Q+(r^RkH z{=EpOs*ItI@2Iw#vRE3GYLWD?(@f@@C}YpFo}0O`ac5L@8a;3+&n78i>`GWLc;E4` zoV>vKpJmOBPK%k&eb~rHY*P6{7{l1EKDxRX?XdifTzyAw_(($Cgq^rN=`LK-(6MYx zD{pgFVsO6;Cba3V%q6rSc{^W^5Sj4F8_ME_Eb_srEmU?z-Y zAq`}HI}y;V`}||$p(fElWaH~-5`wEZNoNgV1%f?Aku+?We$ASqlY3wAPfzDTFcaX- z4$7A6r1ehIPOnRg}Bq`vUFxkzJ?cKDtjdl|vOLxj>@tqJN4PEmx{-@IdVPfEyAsKgxw8neI+#)h4C6j;UjF*9Yn*#M`wpA^TE>9> zb0LAvcjkwlOeE9nQ_DcV-P(FdVoz}M1I~+h)lw2ITbRtszAkAPrSu~++t9T9_sSTJ z|3my9$S*0u^Q22JQ zAE8$V5N|P%if7D2KZeU1%9+)Zj2N`G!NVU)Zb(YnCSxQ~bhc`i7)Abl>mA3Z)E9~&GMqDEO0V{?D^@V!rpEat5#-j?TWxYJ_>aW8SeJcp)+F6=4U!8Pvy6 z&t`QQ=%us`#EO;@2@GCgqd=N6-+N#9_Z#dKbFBYr43-=oXwGIJ$Wb>oTmMDmw>8D0Q_zo=tprz|h3XCIi#%daj z11^iEGG3VEdip}?^XJ=60CMEfQgc*!b6supns@fGmc5OlIr4&N{E1-?%$>v#GvD3$ z<;{VeIUnHq+NWjpV?Rl54CC^3ju%v8F3S!nLedvV)Xqh|;?A!yt;wtR3}!ewoMt+T z#^phEZfV~qJ@Y$LNNzeG@_^bjxjqzXDDvS)-gzPRF_oLXpyma6_f0T1~9X-3RFK3M1F!q+I~ar$z^p1GU2RezrOHFx>^ zREeE^h2!L;Q72Ev2+_>i^5Y|rfVT(srf8_2II&K6!PWZ5Cur&SL~c-Es7WKA^tT0v zQ0W-vO&gzs^qxmwi$h^J$Xa&jlAS$w!bq&TQ!fIZo1T^wp|4zmR9T| zcX_QZY_uCcfW2aYP{nfx)3fn(zAz6!6tZZLbM>tbhYZ4e=Y%z5!`sDt?$J|g5adq! zm8+r6KEsb?Ohx=Z^x!$7cT~e3EutM%a8=zJ=hlRPzE=6b$Wf7au&T!U=Le~Uj);ZW zd`(3w$uiqUOqQFgB5*k0X- zHSAbdO|Ba<8#-2cfK+mHao<<-Zu)Hw)?iYYrt(w3C9-yrxYb!mInPUz9{tM$;utCe4NW|3Je&TT>9D? zNiyzDXD8J~&UM!|J+>28Il3*~0RI?A=XLk?9D7ugPgV0tw9odVMTX@qn()0n$i+-n zhC^09S~vr%G*1SBYDmx~!TIAIw*Ci!!KN@KwN8%-kpoVR%(IVnqxyxEz3rs`Ku#1S zhJQ^fLPnjV-k{;w?JAECUk>YhrKsXh^K@}UTs>GYs6Vn{SwI#x^!mP@j^Bjlq09TW zbKK(FZCASt`V@Jo8s_$BB;v3&mM%xz^aw+k_E60stD<692)y1gA3H9jo_Ii3H>v?_6&?WQ0gP5*}6pNn!W9qxZ*=*nUwS84f(S_FT(iWjDp(v_-QG4%Qd+$xs zQnVB;wO4B=A+chUqNqJ1_9kXx2NC(DpSJJ!_x$x7hXY6M+}AqK^SUmH4&7MD-Y5?m zjK2an3dHLL_W^jLb#_1am}jPdw*Qe$&RdSJg4TL6g;ifc^vL^oINNjxR1A-SJ>~%E zW*Ztm2Ppfu-i)p936mPm4d2ont5;4a?jmxz8s@JZFa>3ihH}<2Qmj1x2A%)5YP)fK zVG$iZ`Jq|^jLOwiaNiK-GEGRT;$LeSCzpRc<1C<=jwAvUwU!ss$`jUkpJj8o9r6}b zXKj?6)-xB2rP;HW0!)L0oIugC4WYZcrIV%G`ySLehQblq)72zwt+ZG^#H{|CDNN`- zvDBYns566-87W#`wll4m$g?m_f|XM@#AmR03#>I18OwUKCwHtd+2dJ(m4Z6nGY`G9 z#0nvy1alG9T5rAvN`nhN24Rbo z$tOQTHauK2I0JK6oSlp0t}IWO+Gd54Je?h1T)doe(hCAgH5&2mPBaHaxBIR%H)R!` z9{NlxQATZ(#ASaU(f4{e8uQ<;v#fkcU999$)dakmAh476+*kMHVcR>LUClX>wdiFt zHedIQ)x)upTi|6Zr%w++e5A+iR0I1UAcM^#y~o2OKU*EYRT_b=w#yu#%wO3D`MgMK zJgpVC<-soAPfttJD?EXil<6#lE?t_SqyDz{Rm1`{n?lIQ4wn8HpD3c;=$hd`UOZ>G z6U(I;j2BN}#@uUXJbIn^SSw24J7w3@YUjmh@R2F4d(MMjr*ft?*))7)x1D9Vd6mjI zhz?-?D>MsWl_G`W9Y}i*!rl)bxM-IOj; z^+|7dmVn=^JFlf+bOOn8v~yq;4L=$z3ruuKJDQ4DUqLxabZH<0)oq@Uvp)S#~n7}p@ z2R#oa%IF}Gz1mkIrdmfaXz1yIDd2dD5qh*`qmvZdm9TQ*W13muOx;>z@Y@&0;U8wI z)hKu??KUUW$>>|D+B9X=O3Pj>gVI#k&7UhO$T2Eev$WzHzqov396^m@7@jNF!``E$ z%RNU5cu3t|g9VX<@g^OYGQh^>DhY zPQ!@gb=tG>0<1ETft$=>f-QE%^>wPn-%2YbSOCTc{DSh(Yt*viM@NqVXiW_AY81 zRvIA!d16X|UPVRY^T|7xy{j*|XqZHu&e1Hf>O^jHK0vTzvcX#=+RI6gjAO!rvL>t| z=+(Uhi;bSGiH=Z`(_8tO)ACn7&9KZ!b36L}-4y%(pN_i0r26JA1$!~zO;CzFSE%4N zNUfnPJJ7}d#L3j_$?Exy*f}eY#Cnkr?KXWZ)W9VUj(>rG^p`grfJ=8WnbU_14*e^6 z>0bZr4XpRuXv$e0tVuaeF_cbEMVq@(#wQq@6uNuJMa}m!o20m%7pPVpdko6k{f22; z5sS28v$tGH`%EcMC|M7)!FUnSw zf8Yl`=y~?F!*mZz9l)<)*%j~28r{UiY1ANrCmSIqD3zfz)mtD3Z5>0P%q_|wtx0@V zrX+NF>>=%2pT;*1!)>wcDG;AbN0#el(*-6 z&CBwBk`$(%QUA$#rbPb>^&Q5u#Wway4mCsx}^-cC6akv|_rLqS>R(6T_RAlq+8= z=k-lNmuIG6rT42~wXc<9o^!iAX&8!Mt3P25tDYoONjeIjiq6R2T2>shAHY8=+!=2j zo`0f3nwoMwh(=a5_3{dAAmkEV8LMS zhDg1rQ+fiRFsesAjRFI4eNPKIUYT`gk@JpPyYg{i2aDVTz6>eqXrA}32Whg9Pnf)y zS0b*mttU~+s);<{wNRHItx03^bU4i2>Pluwy^^-VeE7<-|AMJc-;f{Z3B)kY**JHn zZHkpR>f?<9YZtT3s1twQG^lH5yXHcx1@>1@kZ}2SISF#wRJ_&@s1Xm|R*H1bh$bHs zwZ|H2w^&lle7DM@;50B-EI%1~wty8x0!hf=HFin9byG5z zl}|6^_=kx7x+X-E^=5x>-&kEfeD;QD)KQ%ixlccfyGANaN%Dj{tq`&|P)sRwD3v>3 z6{ObZ3ddV_ExQJ{7b5Z*P1v+$Tf91Hp!oWHz{sJeZ{ewoLy#=tV7J{mev{fHbo%I_ zNb_a1#K^CZ3twE1qCgO!7_;m%6bQ0t9MSlaVokrE67&ChjN$73OUa#PjV9w2uXyhR zREwWJPpSOGabmAKssap_5))ZvXaAv3H;)uxm0$_n=OUcmtT-EUu6FEl`31ZDIU_nO zOGWtpd7qrY=GK9^AX2vValD9Vt!@}L#h&vHXJv?IdX_Fe)=UGtA79qHkAG$4pgnxj zEl@)!{>+--#d*ZgeTpO?lqRb%ku2-sOf56zLg>kQ(tI<#Q^X1E_%GE&u7^UUgYr@= zUVWFqi9VdEH9|_lw4r)_InB|qp`krS2F(b*x!i43jb2aelp+8gS}HhIysTBF@t#kZ zv`M0CJl|0M&zlGkxg_94Ucv%JEb@l$NPOMCwlM_pN0nc)^?xVw+~3`#r@#kVVPnjj z;31%Wt(-k2yB^(9>ET-3)JeG0+?7SZL9H_||n7UZ;g`zc0K$ zXtahD_blULBNXW@oziQTvlmHDh$iCh@{Ke0p1;S+JnOS_uGNM|h{c3Wbd<&Y>!d%n z4J}@H{QjELc9{Tmf?s$49EISNk*~zgTAYbL{Lx z?*octmrfu5T4_jH{Kelp^mR+?4}|dDnwOHH5)V}qa7OphdZuFH2)tW0RzJwm7!@KraimqLp3xKhFP9iy zbT_jk3uVEs2v!P&{X{p(Ge-3BTSc)zuYS zJ8#%|0;-s+kAGgy^^s#lSu7gca6sR3^fxb@a|Fdbs#fcPkr%z zfqe-|Wq~6UVvJVDOdK-J7;C?Su0lI{`wO+uTdc7mpr=2VS9qG23c-G9!~E-mWRj3U zS|HJ*CO~!p<&AP6z=zULmLvV4zq}gCFaE`=GDwW@6pozDzW7Bq8sf3e>kT@htN}Rk zmAFSmj7T+)L1s!or&;jY_V%H#`73_OAYF+h}Ok)I)&tCGi+u zIxs`^Q=#>H{6kyW)>F=*<}Us7A~P&{AWw8tHM1{4rLMq6kfDx&Pok#lMT`9?@FGjv z(vF=0_sAeJLP#eyPld~8%o0>BR4g-iMMON-A0Zgonm4D;;&Wa#G{4v5tvQ&Ubx>9W z#Gm2fWztReQ!%br8A9SR)gW%!|3;1di}Fharv-e84hF?vaPqrVZ?X!#=k~W>j!gT! zf_-}D9Qx5^{EE2q;j@+V&qlivt=ks9R~=1(8$KjgF%BNx4Q!j}5~;o*OKdgM0%h&d zl-cOuPJz6n&<4Z3SkC^@oHQMikuQ8#YJUX6h$Ga+mAY6AiPP^-gj?B=|p8410`L$kTL<4#8eDrQ0oDt_o(z0 z%R770DN<&Bx13CvOcnwRsj2)=PtrCR`m9g zWn#G;&OLedm|6B-iGrl}J>tha3pHDZO{E<=Dh?Um90!3kX_Hp+n+1#xYbz-=7s0b-p46u3gEz(VXBr?Y-X=gR(y@pnm1pqp#Su#fXf-D zxtBH1eO+Ado^i#ReVEm~@M6)FZ+;rS#OrFBy)%tmlk$C`)t8~}TGo=rJ}sm*p_tc^wbK5SF@NKh>S7harZ8=8?QY`gCo&%4*4uZ|(5kbcShe?4?p z!PowXXu4lmE4)PZ&G0xI+)hvoePcCxRpdnYBH5w;>Zk;_qYSLb8!|)dcNy`q1*It- zPHp*9NjxAc4FkeRw;ef00Rs8$rJXbcr@f|`Drc@Sx zP;Pn;HK)&Q>!F4QKEdfI#&Gflq``=es7ZC!8MU?bXqLU1eRx59Sm|s?k;h6nf2U4xr^I;YWf!0kRxja|zLod)X{ zaAkS(?uOnbt+o1NhOp}MllnT>$UQF|K-;Px7-m`(ReQ`Dv=&giZ;V4{n1TSx;9YNs zyi*4z7<-n&I_^gKhNIq48n3$*@snRD)4UXKF%M!AUDLIQNv{8Gs(tm}*Z_mVV83#& zc14ITPTE`+;2iG*VV3$5^vUP>eh9>U3(dy!>}^<|gy0uGz6;;mWF|x0L%#u>hEk0} zxuDw2&3mDl0^+)fS+Qilf$?~2@u-j@+CIGRzXwwEArJv6R9HKY%u4Px_(`5oqpAQ- zegcNXpi=^C&+R}_>*|lMQRQ$0LvR7qRI8xZ)+TMU)y%z$T*H= zAM1Q{WH1oaoR?=g_8uq;RAAiSX%|Q}gPUr+7ycQ}AZ8o)p+|MTK>!lHgY1xEsGF+| zF7R4~A)ubigXtUXZQ*%PkV}L8^-EYw1>rv9r@DnvN)e%)kE;N>{#HU;k|Z) zMo^e#3bm(F^Jp&@W`?*e^(iSZF$o!qr(v1S_K4%XS5vH{GhL~DE!J6=Am5G-z`d(= zHC9IqE%jW+m=Kq$hCxRF9al4z$MDac2TO*OB@QFQLqy;{I>}lqBh2VyfI*KMUU#7* zBeeJD$=(U_$*-RPP9?9bH+W|Ti=-horRt0}vryk~MqbK|Vx{d(r{8$|5MGQKy0&Bm zol3!i8@r?oi?ZTXl=#1Y6E*W+;OzgN8{bN;Yj|T_;@Y4CLWy0v=c5(rPWRYy484Mc ztuklyN7agWLhnV}H$2ZZ82%USn3zh312-Hb~d@qmsIA+Lem6r;5NAq0*w0(?CK8Z);{=B44QH-L3yaWiQk?-KL&a~ zJ(0zn)gp@lxhZSJn}Gv!Tk7VCkizS$L5ZT;$D<-Z&^jkj+85@~)wYzrKz{`@wU~9{ z4f0&zoq3kQs)p;2BQNDZr>ZPu62AuY83K;7va~7J`23uiG2hezCBJF9Mf*5cBsm49 zBYaAb=Ef9hQp5xTLwiHS2hC^uk70KPg;8!4NAT;RV>x5~zZWX=_nnk{s4)l@z{b*{ zXbKw@?zc0QLzjJmV)Xm&ebY!W{$$s4yJWfSaXL%2@H&k_yk+@$h=7uQ{uzVa<^C*l z5goeNsv@J(d)Hox!)s4-GpgW=IZl9cp_ZX;#dL|If+^NLCPSlmZsjq?H42PJL@Ks? z4R!f0^GaV;5~38v3R!b1 zxWFpo7c`hBCmY?j@ud#SY>>kqZr&g{OVbtF!1NDb z!}w|>=Y~a+ixyq2q-`{)fwXql)|7iPt^x0iYKorAn(@VdcR+JyClG4J*B@aKflu~SCT1g}tJ{AHROBt0(jg$lm^C zbS!>8`z+P+zw1W}WkOolUrPxMQM8DVKe!`v1?|1xUli3&rxGhv8fj8%ZR|lZw`b-3 zD1l35DN#8G;qer3kVx~o!@>Ue)$Q85=BwvRhxXk4S9_W+rL%d}sfaI)b00$8Kp(86 z5_^<7+&j*!9(H=OsGds;{bZB+Y42=Cnan>JgX5jk&jR!(;=2>y8P0xnar;;<{C>UF zcZfBAP8FFvVeCc@>8=zc%j zEBglLJTii4Fr9G{z4ypjQ=T1Y57Ux~nH%;hC-kqU>0#VX>Hnmco0rky1a0G@F~h-L z4Qo|`di&`3H7QSht^2(lwUX2P?mhqGAOmIsYQ)JGu>plR)8aK8dTm1j0zvudY@$G( z)5WD_feEQu)hY>7u#jsn z?3X{8&Cf4g*3qQS;z1%tA)5cq(igl(niW@Z$-P3B=O16xfZmKUTGc*4t;aRzC^z+E`r}#x%PoKHh#@ogbQp?#GVuW}{jloAQ0cmnY6r;x-jKuuV z$rDQTNat$514BS0XiRL04c8yO?~(qxH}zDLG> z^qzsGvoGojh1{!A^Rqkej~HMPOI)g$t2`eTO0kAShYGiecVpkeevP1(Ni0Kfh91h%VE&)qUv8QV-Wg=OLoxu*z zykUy4mBTs}>ncA*1K-wikTPk&NWVs$t@ zo^k+zVzv1QD4W_x#l4O3yT<`FiVFJ$D?Y}DdEMXu+Y9Hr=+O0hTzXH{`fta1x))NBjd!TI}nOKH{h8e2HO*3 z45XXOpz1+9|GK2mZT^(TQF`fT?43&10W|q%=Dpwp#wW`~`Nj?+3>-%N{!i^*6ir?N zNFd5X)C*)CD?@<)^jPNH9>0X#iRlST>mhy*^8`RGH7dm*vZ=SEJXAMb*}r7C_H=;w zi&?c+uNhf)v9|h;-Xz#)fByzq4RO5ZIwdeJ;Vx-X!LqcOK3sQ&-r(UkTfWOf0g_#d2tKjpsrT4QjYd7&`Zjp=!I5X+zHgFy<6>UULA7X*i!ocU^!WQgOyt>a*>(2xNm{&o zJOJJ`RVSBFrwj8rOwUJoKhMBWp#H{t`zrzAut!tqJv}gFd8RhPu=`3?VgB*Lgf=)R zEucUELW;MfD7lQM?%29i*Cb*DH1|*Ge_2T$dcn9}%tnhtg>=Ms{WWtfQBC6=Uw!+1 z5aF?k;3)Oizghs&YF8@Ah8#@*%I4)B-$~OUj|wE{GW)U#A`EgVk!QD&?h)1O`J;nX zBO-;*YhOR^etQM=@Q*lG|LK2X%X7#jOQ=*0g^>^tOIl)Gc%Ci1QtRNM5Z7Nb_x8tK zD~GU)E%Q{C7Tg1BFOF~Z$=G5RcT21h$HC?%V2+yI-rEUI13v~W0Lq$%P0i_D%jrf+ zD3*0|9|My-CpgI0fPQT=!iNOB#Hl2HvF`5Ywfi4Q>K>1(+?+dxI?^ZeD#iQQ2ZukG zdtN&&{(=s&i+5_Ltx<$LEo6=ON>lu%C*9aWV#Y~w49Pd`MPhd!JR_=zWjJU|_vPdp z<&$Z*W|?LT#r!iAi_4jmy1`S^*z1o$6NN_g2S^qJ4Q#QnTTR;)%%S4Cifhhh(6w<1 z9WgK620YgQWpkp2E3}1;4?em_V1ypOrH5`9D?DD-w?9f=a?NP|w4P;f$877rvVQ3z|AUaCK`+{P!*TDzq2qwAN>;i|@WpiR9A|rLv)TkFFKf z;IS$YvS>L8r%f6Fq@LuUbcyFzKzfG8dYf}SP|4Dld96YOAGly6!Iwt2WA*!$g|He9Oz+TUKqK`BzUV4R5k&}#_)y>Euth&M}1&O@JGGvD3d zile@3a@7Tyy_}thUtCLPL1|yM+K|qqeoPb_y<3r*r(3=dH%9-uB+SGDK>M$v$c4aK z8F`G==|fx6Lxt(xQ13Pwdu%;>AtnI*ke;Eg@SH1H(ZbIlbi3sd{?kDvD zxfPmz7BuU$%}5h10vipGM7mSfaWJ{j9wG8d%_WWv5#Z<(Dwf6)=e7E zwYHf4daax#;h@vf#Z6_(T(C8;EhLFRNKwLl2r2P3T(@F8E=PLNgLdbgxE^q6`?$D) zkAUtL7xY~&YpUY(v`XmWwoOEA5)akR5~oRts$KqF+-KS18HsBuUMsFzn<04CrJV4aeV#H2j524VK5 zw_199L!iP`m0%$Och7sKhFirKarg{wm}msd-+^9C(2arA2fT3Eucq_}`~)8M`HMzU zqE71R!5Lcjo8y$*;X^)8>z;>~ab&SSK?T3c2rU4T%nl%=lWj55y?@|_x=T19RI*wD zE#dN6(dEFbZ@S79M<&%crJB7zOI3Yp*vHOI^(dmWP|7&lU@d530Ad8MhsBV;u%mwFyiQyu%&LIy~^>*R%^DfA+WWPn>| z`#3V?t62+{467c;>-+*B@KXiTV-myu+bT$Kz>7`2nHm9$*`4rvP@F;W4kgxZ7~j(4 zGaDd&cKo6m5Q7W5tsHnp&MLVreHMH@h?ef-vv zB`kj7Jwmpfk9NW)!$<{mP7hoQvcrTOEjO_OoKs=9{OzMK*OW^R^#x%mBahhb(_STV zjeZh;caMFzRWP6Wt-4T014uC*dO8_ zZ;-?z7krM2Jk*Cu&I?=^ilEN{*ugVK29ITACMy4RS-%R*9Ooe(Xdpq`mE*3NN!}E( z##qYfvJ@I;gxFcmyS^YtPJQieVrZN+B9MJ!ciZ$t4FMsx`O~_mP|$IsA8znDVs~7) zL#&I&<^W*OOE06P04*6LH+8N6E#a@wpcTKCk&)`oE@~QYq^aj`pp~byz*}sROZnSS zC;i)KgbC(oyd`bfpE7XStz{)8ALaQfx^FX|S+u5Ca7H5*QTGSqz5cf{=C_M3?$SA) zl^gTZk>&7c;dZ)l(zT>N^_6HhE~ZoS_4NSOy>0Nss)PkY9|%7$snmiguw-1;64q<& z(P>bSN3e~B-iL?vxE1nA3F!IT+NaIsUMEq8rTT$RPCcoEW9TAtLjLu9+Beu*ncrAx zma%}P2nA<86l(KAc0L2j08x304<`htkC)K>^_vEA5lG=V<~&0HPiI#s@gBHV&(6w+*Qu5gOMAPAJ)E%+i} zgCfXm3{=QVu)Mxt2usqLMrtmZ@a51?XyLt>&sO^A!BfKmrgrj;J9-ZDc981RHJW+o zYm|K3Vme{wUxcPHiSsP5iNYo~ghGQI^tSMllR^2JV`_hr6e_h3e`YP2f8#~4CK1`{ ztk{?pIvAT6uLjwRYO0NT`h52%C$o7$ko>vHO40#SHYTVXyQ|`tvTH+W8ZJ0B?!vP# zTL!)Gq^N&axgEoH@zUbS-@i6MWnqc+F-@o9*YD*r_1zOkgk4l8KjT@a2?DNkvptLd z2Vml1HNFZIGh+T+ISZ7P{+Hc2r=oDI9XDA~3*Lj)5>@*Y3Pv<@vX5P<^hq93T76+Xd8+po@$xaJva=~ zKP1$;X>QI%8(*XPo?b!+pnS(4UrctJ1)u^IGFXnr{~Y<&U&ZwI5N%UUJrcxuFm=0W zCN6Bs2cU9M&R&V7AcUrGqU4eFPVJ(lo}2Pj#l4pJ(C=TFTJ-^3#@-@}iy{{u9mIbW z$g~p56 z;mfA{ePL4P#BaiQ9dDE~NLtUe0j9;XF$?pK-Veh7dEb;js5j|f6Jdxq^6BaENBShr zD9PGeVXsGcs0eJ-Z^U{2kTC_D$!|u;^L))|uwmw4j(FEVTFnkf^(Tb#>DzkgRUd*l zMyia;N}+p{L@ano04QChgx-%@>O4u13{N{&LqL5`*u$unNB1yhdW4?_wC+sef(Y?C8_q~Qzv8ltXh3;#lYNVR$LsHB_5R9I^zAw+4FFlAw*qHaAvoXZ ziV$g1e^z8m{4y9v+vXmMX+>;HZJqmU|J=hZ=yGB-R{U@j>^xT!FQ4-~dv^lH{OYWuuOFp?6zt;C zdy2V*@%UkUqB_8X^=@NuT%-+M7Lbm;JDYBNjxRMLyr?}l&Z>)z!fa@4ma0HWdn(AR z;FlQ~Rfy=+Tn}pvp+E_|Rn!K?Yf#V?f12d@2^aFi|5Y&ayxyh(MyU=;^wV|Bx2uZ2 z?QFt^k6ME+gb6KMlN11N9P2_#U+N95D2+K}n%^{xX>F`&ECREfrlP zrGGEb^>c&k{?`eInJq3M3y)wQHMX+tZqxM90LBA<@MD#&OJul1XHpRwb^K>kEgI`BgRLu44 zdPw9l;Wa#S;5pF(iUU$5C(~8U!Cps^xWwit7d`D5yBl=PYl(re7bMJLIiJ?+Ifu8% zN86#}_P!gULA$Cz(54U2r|1)GOYiW4_smlSgp9>rKbw5cr?i-IPU*vUK6;^%PQGYL zFAQzz8#L$iudO$p`TLhl-nhzba&g6*IQ^}SxtrA(f5#wm8OT&%lw#J{ElSaeZ)6iS z`AQ>8bPmn`7Sb~t)jfwP8`uI5@eia z!H`31@zO@qt2nM*@qg7YW?0HYcVyFq(4fzWfE>Z96sw9iBBY0a2icBN8^@!cB%jpL zS>!OVK6y3FXwKR?(=;qvLQ(+$ryxY0 z?GKyJ>NeOGPx>WLFq!3liMFnVzLAIpms$kNMVCL&Oc?LqJ>7J&&aKIv6vC~TJzi0O zu82W1tfkrC{^{ucIk3PA*V>{-sWD6a;9|2|(Y;&7UH4c`f>e*@#|nx_@&YptuV7CDlqnD`;52Px3ft9mAc)g}w;2w%}> zn#^j-6cpRX8AEn@@A0BM4AEQ--ylgdx~{b?X=1|rc(UOVKuO0CmHe6tBeR}HM4yUr zI3n_JO87$2u|&tMtOAzYGOhi$^bV4qZl-{)j|_9w^F1x61D{ZD&cMScdV$SQC>SJ) ztXA+cm4M(%J^$L?gXc8nzm1Ro0yktNF!SRVM`(tT{ZTa(}N)SV*^*BYI=mFknmZL_8N!dkkJf$iY5nROuogKrai zTuwlAWB>sk2f_+OlNKmQZa+m&EV`uV^^fV#A~ZTr+D|q0_GJcR$0%AkX z5sicHUaO@NT*0^hXVj#ZWx7c(Chv~VNWJIln#pp6Q>SdKXW+H`{e0*#C&G13Y4;Rhh=a|hzLQ^+xLo(mGYT)yH)+XC zVBmbxJ`ZnqSz+G4(`;Zh5f0}MvaYtwm>2jdvgRNTsdE0HXjZR-M0pmZ#yV?443>I= z‚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 A3;+NC literal 0 HcmV?d00001 diff --git a/requirements_dev.txt b/requirements_dev.txt index 624c4eb..7caa5e4 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,5 +2,6 @@ black flake8 mypy pre-commit +types-protobuf types-requests wheel diff --git a/setup.cfg b/setup.cfg index 8db7ba7..94867d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = zotify -version = 0.9.2 +version = 0.9.4 author = Zotify Contributors description = A highly customizable music and podcast downloader long_description = file: README.md @@ -33,6 +33,10 @@ console_scripts = [flake8] max-line-length = 160 +ignore = + E701 + E704 + W503 [mypy] warn_unused_configs = True @@ -43,6 +47,9 @@ ignore_missing_imports = True [mypy-music_tag] ignore_missing_imports = True +[mypy-mutagen.*] +ignore_missing_imports = True + [mypy-pwinput] ignore_missing_imports = True diff --git a/zotify/__init__.py b/zotify/__init__.py index 981d092..01148a3 100644 --- a/zotify/__init__.py +++ b/zotify/__init__.py @@ -3,24 +3,25 @@ from __future__ import annotations from pathlib import Path from librespot.audio.decoders import VorbisOnlyAudioQuality -from librespot.core import ApiClient, PlayableContentFeeder +from librespot.core import ApiClient, ApResolver, PlayableContentFeeder from librespot.core import Session as LibrespotSession from librespot.metadata import EpisodeId, PlayableId, TrackId from pwinput import pwinput from requests import HTTPError, get +from zotify.loader import Loader from zotify.playable import Episode, Track -from zotify.utils import API_URL, Quality +from zotify.utils import Quality + +API_URL = "https://api.sp" + "otify.com/v1/" class Api(ApiClient): - def __init__(self, session: LibrespotSession, language: str = "en"): + def __init__(self, session: Session): super(Api, self).__init__(session) self.__session = session - self.__language = language def __get_token(self) -> str: - """Returns user's API token""" return ( self.__session.tokens() .get_token( @@ -40,25 +41,25 @@ class Api(ApiClient): offset: int = 0, ) -> dict: """ - Requests data from api + Requests data from API Args: - url: API url and to get data from + url: API URL and to get data from params: parameters to be sent in the request limit: The maximum number of items in the response offset: The offset of the items returned Returns: - Dictionary representation of json response + Dictionary representation of JSON response """ headers = { "Authorization": f"Bearer {self.__get_token()}", "Accept": "application/json", - "Accept-Language": self.__language, + "Accept-Language": self.__session.language(), "app-platform": "WebPlayer", } params["limit"] = limit params["offset"] = offset - response = get(url, headers=headers, params=params) + response = get(API_URL + url, headers=headers, params=params) data = response.json() try: @@ -69,30 +70,39 @@ class Api(ApiClient): return data -class Session: +class Session(LibrespotSession): def __init__( - self, - librespot_session: LibrespotSession, - language: str = "en", + self, session_builder: LibrespotSession.Builder, language: str = "en" ) -> None: """ Authenticates user, saves credentials to a file and generates api token. Args: - session_builder: An instance of the Librespot Session.Builder + session_builder: An instance of the Librespot Session builder langauge: ISO 639-1 language code """ - self.__session = librespot_session - self.__language = language - self.__api = Api(self.__session, language) - self.__country = self.api().invoke_url(API_URL + "me")["country"] + with Loader("Logging in..."): + super(Session, self).__init__( + LibrespotSession.Inner( + session_builder.device_type, + session_builder.device_name, + session_builder.preferred_locale, + session_builder.conf, + session_builder.device_id, + ), + ApResolver.get_random_accesspoint(), + ) + self.connect() + self.authenticate(session_builder.login_credentials) + self.__api = Api(self) + self.__language = language @staticmethod - def from_file(cred_file: Path, langauge: str = "en") -> Session: + def from_file(cred_file: Path, language: str = "en") -> Session: """ Creates session using saved credentials file Args: cred_file: Path to credentials file - langauge: ISO 639-1 language code for API responses + language: ISO 639-1 language code for API responses Returns: Zotify session """ @@ -102,12 +112,12 @@ class Session: .build() ) session = LibrespotSession.Builder(conf).stored_file(str(cred_file)) - return Session(session.create(), langauge) + return Session(session, language) @staticmethod def from_userpass( - username: str = "", - password: str = "", + username: str, + password: str, save_file: Path | None = None, language: str = "en", ) -> Session: @@ -117,15 +127,10 @@ class Session: username: Account username password: Account password save_file: Path to save login credentials to, optional. - langauge: ISO 639-1 language code for API responses + language: ISO 639-1 language code for API responses Returns: Zotify session """ - username = input("Username: ") if username == "" else username - password = ( - pwinput(prompt="Password: ", mask="*") if password == "" else password - ) - builder = LibrespotSession.Configuration.Builder() if save_file: save_file.parent.mkdir(parents=True, exist_ok=True) @@ -136,21 +141,35 @@ class Session: session = LibrespotSession.Builder(builder.build()).user_pass( username, password ) - return Session(session.create(), language) + return Session(session, language) + + @staticmethod + def from_prompt(save_file: Path | None = None, language: str = "en") -> Session: + """ + Creates a session with username + password supplied from CLI prompt + Args: + save_file: Path to save login credentials to, optional. + language: ISO 639-1 language code for API responses + Returns: + Zotify session + """ + username = input("Username: ") + password = pwinput(prompt="Password: ", mask="*") + return Session.from_userpass(username, password, save_file, language) def __get_playable( self, playable_id: PlayableId, quality: Quality ) -> PlayableContentFeeder.LoadedStream: if quality.value is None: quality = Quality.VERY_HIGH if self.is_premium() else Quality.HIGH - return self.__session.content_feeder().load( + return self.content_feeder().load( playable_id, VorbisOnlyAudioQuality(quality.value), False, None, ) - def get_track(self, track_id: TrackId, quality: Quality = Quality.AUTO) -> Track: + def get_track(self, track_id: str, quality: Quality = Quality.AUTO) -> Track: """ Gets track/episode data and audio stream Args: @@ -159,9 +178,11 @@ class Session: Returns: Track object """ - return Track(self.__get_playable(track_id, quality), self.api()) + return Track( + self.__get_playable(TrackId.from_base62(track_id), quality), self.api() + ) - def get_episode(self, episode_id: EpisodeId) -> Episode: + def get_episode(self, episode_id: str) -> Episode: """ Gets track/episode data and audio stream Args: @@ -169,20 +190,19 @@ class Session: Returns: Episode object """ - return Episode(self.__get_playable(episode_id, Quality.NORMAL), self.api()) + return Episode( + self.__get_playable(EpisodeId.from_base62(episode_id), Quality.NORMAL), + self.api(), + ) - def api(self) -> ApiClient: + def api(self) -> Api: """Returns API Client""" return self.__api - def country(self) -> str: - """Returns two letter country code of user's account""" - return self.__country + def language(self) -> str: + """Returns session language""" + return self.__language def is_premium(self) -> bool: """Returns users premium account status""" - return self.__session.get_user_attribute("type") == "premium" - - def clone(self) -> Session: - """Creates a copy of the session for use in a parallel thread""" - return Session(self.__session, self.__language) + return self.get_user_attribute("type") == "premium" diff --git a/zotify/__main__.py b/zotify/__main__.py index adbb088..6250003 100644 --- a/zotify/__main__.py +++ b/zotify/__main__.py @@ -7,7 +7,7 @@ from zotify.app import App from zotify.config import CONFIG_PATHS, CONFIG_VALUES from zotify.utils import OptionalOrFalse, SimpleHelpFormatter -VERSION = "0.9.3" +VERSION = "0.9.4" def main(): @@ -25,7 +25,7 @@ def main(): parser.add_argument( "--debug", action="store_true", - help="Don't hide tracebacks", + help="Display full tracebacks", ) parser.add_argument( "--config", @@ -138,8 +138,9 @@ def main(): from traceback import format_exc print(format_exc().splitlines()[-1]) + exit(1) except KeyboardInterrupt: - print("goodbye") + exit(130) if __name__ == "__main__": diff --git a/zotify/app.py b/zotify/app.py index e3569e0..691dc91 100644 --- a/zotify/app.py +++ b/zotify/app.py @@ -1,47 +1,33 @@ from argparse import Namespace -from enum import Enum from pathlib import Path -from typing import Any, NamedTuple - -from librespot.metadata import ( - AlbumId, - ArtistId, - EpisodeId, - PlayableId, - PlaylistId, - ShowId, - TrackId, -) -from librespot.util import bytes_to_hex +from typing import Any from zotify import Session +from zotify.collections import Album, Artist, Collection, Episode, Playlist, Show, Track from zotify.config import Config from zotify.file import TranscodingError from zotify.loader import Loader -from zotify.printer import PrintChannel, Printer -from zotify.utils import API_URL, AudioFormat, MetadataEntry, b62_to_hex +from zotify.logger import LogChannel, Logger +from zotify.utils import ( + AudioFormat, + CollectionType, + PlayableType, +) -class ParseError(ValueError): - ... - - -class PlayableType(Enum): - TRACK = "track" - EPISODE = "episode" - - -class PlayableData(NamedTuple): - type: PlayableType - id: PlayableId - library: Path - output: str - metadata: list[MetadataEntry] = [] +class ParseError(ValueError): ... class Selection: def __init__(self, session: Session): self.__session = session + self.__items: list[dict[str, Any]] = [] + self.__print_labels = { + "album": ("name", "artists"), + "playlist": ("name", "owner"), + "track": ("title", "artists", "album"), + "show": ("title", "creator"), + } def search( self, @@ -57,54 +43,55 @@ class Selection: ) -> list[str]: categories = ",".join(category) with Loader("Searching..."): + country = self.__session.api().invoke_url("me")["country"] resp = self.__session.api().invoke_url( - API_URL + "search", + "search", { "q": search_text, "type": categories, "include_external": "audio", - "market": self.__session.country(), + "market": country, }, limit=10, offset=0, ) count = 0 - links = [] - for c in categories.split(","): - label = c + "s" - if len(resp[label]["items"]) > 0: + for cat in categories.split(","): + label = cat + "s" + items = resp[label]["items"] + if len(items) > 0: print(f"\n### {label.capitalize()} ###") - for item in resp[label]["items"]: - links.append(item) - self.__print(count + 1, item) - count += 1 - return self.__get_selection(links) + try: + self.__print(count, items, *self.__print_labels[cat]) + except KeyError: + self.__print(count, items, "name") + count += len(items) + self.__items.extend(items) + return self.__get_selection() def get(self, category: str, name: str = "", content: str = "") -> list[str]: with Loader("Fetching items..."): - r = self.__session.api().invoke_url(f"{API_URL}me/{category}", limit=50) + r = self.__session.api().invoke_url(f"me/{category}", limit=50) if content != "": r = r[content] resp = r["items"] - items = [] for i in range(len(resp)): try: item = resp[i][name] except KeyError: item = resp[i] - items.append(item) + self.__items.append(item) self.__print(i + 1, item) - return self.__get_selection(items) + return self.__get_selection() @staticmethod def from_file(file_path: Path) -> list[str]: with open(file_path, "r", encoding="utf-8") as f: return [line.strip() for line in f.readlines()] - @staticmethod - def __get_selection(items: list[dict[str, Any]]) -> list[str]: + def __get_selection(self) -> list[str]: print("\nResults to save (eg: 1,2,5 1-3)") selection = "" while len(selection) == 0: @@ -115,64 +102,40 @@ class Selection: if "-" in i: split = i.split("-") for x in range(int(split[0]), int(split[1]) + 1): - ids.append(items[x - 1]["uri"]) + ids.append(self.__items[x - 1]["uri"]) else: - ids.append(items[int(i) - 1]["uri"]) + ids.append(self.__items[int(i) - 1]["uri"]) return ids - def __print(self, i: int, item: dict[str, Any]) -> None: - match item["type"]: - case "album": - self.__print_album(i, item) - case "playlist": - self.__print_playlist(i, item) - case "track": - self.__print_track(i, item) - case "show": - self.__print_show(i, item) - case _: - print( - "{:<2} {:<77}".format(i, self.__fix_string_length(item["name"], 77)) + def __print(self, count: int, items: list[dict[str, Any]], *args: str) -> None: + arg_range = range(len(args)) + category_str = " " + " ".join("{:<38}" for _ in arg_range) + print(category_str.format(*[s.upper() for s in list(args)])) + for item in items: + count += 1 + fmt_str = "{:<2} ".format(count) + " ".join("{:<38}" for _ in arg_range) + fmt_vals: list[str] = [] + for arg in args: + match arg: + case "artists": + fmt_vals.append( + ", ".join([artist["name"] for artist in item["artists"]]) + ) + case "owner": + fmt_vals.append(item["owner"]["display_name"]) + case "album": + fmt_vals.append(item["album"]["name"]) + case "creator": + fmt_vals.append(item["publisher"]) + case "title": + fmt_vals.append(item["name"]) + case _: + fmt_vals.append(item[arg]) + print( + fmt_str.format( + *(self.__fix_string_length(fmt_vals[x], 38) for x in arg_range), ) - - def __print_album(self, i: int, item: dict[str, Any]) -> None: - artists = ", ".join([artist["name"] for artist in item["artists"]]) - print( - "{:<2} {:<38} {:<38}".format( - i, - self.__fix_string_length(item["name"], 38), - self.__fix_string_length(artists, 38), ) - ) - - def __print_playlist(self, i: int, item: dict[str, Any]) -> None: - print( - "{:<2} {:<38} {:<38}".format( - i, - self.__fix_string_length(item["name"], 38), - self.__fix_string_length(item["owner"]["display_name"], 38), - ) - ) - - def __print_track(self, i: int, item: dict[str, Any]) -> None: - artists = ", ".join([artist["name"] for artist in item["artists"]]) - print( - "{:<2} {:<38} {:<38} {:<38}".format( - i, - self.__fix_string_length(item["name"], 38), - self.__fix_string_length(artists, 38), - self.__fix_string_length(item["album"]["name"], 38), - ) - ) - - def __print_show(self, i: int, item: dict[str, Any]) -> None: - print( - "{:<2} {:<38} {:<38}".format( - i, - self.__fix_string_length(item["name"], 38), - self.__fix_string_length(item["publisher"], 38), - ) - ) @staticmethod def __fix_string_length(text: str, max_length: int) -> str: @@ -182,42 +145,48 @@ class Selection: class App: - __playable_list: list[PlayableData] = [] - def __init__(self, args: Namespace): self.__config = Config(args) - Printer(self.__config) + Logger(self.__config) + # Check options if self.__config.audio_format == AudioFormat.VORBIS and ( self.__config.ffmpeg_args != "" or self.__config.ffmpeg_path != "" ): - Printer.print( - PrintChannel.WARNINGS, + Logger.log( + LogChannel.WARNINGS, "FFmpeg options will be ignored since no transcoding is required", ) - with Loader("Logging in..."): - if ( - args.username != "" and args.password != "" - ) or not self.__config.credentials.is_file(): - self.__session = Session.from_userpass( - args.username, - args.password, - self.__config.credentials, - self.__config.language, - ) - else: - self.__session = Session.from_file( - self.__config.credentials, self.__config.language - ) + # Create session + if args.username != "" and args.password != "": + self.__session = Session.from_userpass( + args.username, + args.password, + self.__config.credentials, + self.__config.language, + ) + elif self.__config.credentials.is_file(): + self.__session = Session.from_file( + self.__config.credentials, self.__config.language + ) + else: + self.__session = Session.from_prompt( + self.__config.credentials, self.__config.language + ) + # Get items to download ids = self.get_selection(args) with Loader("Parsing input..."): try: - self.parse(ids) + collections = self.parse(ids) except ParseError as e: - Printer.print(PrintChannel.ERRORS, str(e)) - self.download_all() + Logger.log(LogChannel.ERRORS, str(e)) + if len(collections) > 0: + self.download_all(collections) + else: + Logger.log(LogChannel.WARNINGS, "there is nothing to do") + exit(0) def get_selection(self, args: Namespace) -> list[str]: selection = Selection(self.__session) @@ -240,17 +209,14 @@ class App: elif args.urls: return args.urls except (FileNotFoundError, ValueError): - Printer.print(PrintChannel.WARNINGS, "there is nothing to do") + Logger.log(LogChannel.WARNINGS, "there is nothing to do") except KeyboardInterrupt: - Printer.print(PrintChannel.WARNINGS, "\nthere is nothing to do") - exit() + Logger.log(LogChannel.WARNINGS, "\nthere is nothing to do") + exit(130) + exit(0) - def parse(self, links: list[str]) -> None: - """ - Parses list of selected tracks/playlists/shows/etc... - Args: - links: List of links - """ + def parse(self, links: list[str]) -> list[Collection]: + collections: list[Collection] = [] for link in links: link = link.rsplit("?", 1)[0] try: @@ -262,152 +228,92 @@ class App: match id_type: case "album": - self.__parse_album(b62_to_hex(_id)) + collections.append(Album(self.__session, _id)) case "artist": - self.__parse_artist(b62_to_hex(_id)) + collections.append(Artist(self.__session, _id)) case "show": - self.__parse_show(b62_to_hex(_id)) + collections.append(Show(self.__session, _id)) case "track": - self.__parse_track(b62_to_hex(_id)) + collections.append(Track(self.__session, _id)) case "episode": - self.__parse_episode(b62_to_hex(_id)) + collections.append(Episode(self.__session, _id)) case "playlist": - self.__parse_playlist(_id) + collections.append(Playlist(self.__session, _id)) case _: - raise ParseError(f'Unknown content type "{id_type}"') + raise ParseError(f'Unsupported content type "{id_type}"') + return collections - def __parse_album(self, hex_id: str) -> None: - album = self.__session.api().get_metadata_4_album(AlbumId.from_hex(hex_id)) - for disc in album.disc: - for track in disc.track: - self.__playable_list.append( - PlayableData( - PlayableType.TRACK, - TrackId.from_hex(bytes_to_hex(track.gid)), - self.__config.music_library, - self.__config.output_album, - ) - ) - - def __parse_artist(self, hex_id: str) -> None: - artist = self.__session.api().get_metadata_4_artist(ArtistId.from_hex(hex_id)) - for album_group in artist.album_group and artist.single_group: - album = self.__session.api().get_metadata_4_album( - AlbumId.from_hex(album_group.album[0].gid) - ) - for disc in album.disc: - for track in disc.track: - self.__playable_list.append( - PlayableData( - PlayableType.TRACK, - TrackId.from_hex(bytes_to_hex(track.gid)), - self.__config.music_library, - self.__config.output_album, - ) - ) - - def __parse_playlist(self, b62_id: str) -> None: - playlist = self.__session.api().get_playlist(PlaylistId(b62_id)) - for item in playlist.contents.items: - split = item.uri.split(":") - playable_type = PlayableType(split[1]) - id_map = {PlayableType.TRACK: TrackId, PlayableType.EPISODE: EpisodeId} - playable_id = id_map[playable_type].from_base62(split[2]) - self.__playable_list.append( - PlayableData( - playable_type, - playable_id, - self.__config.playlist_library, - self.__config.get(f"output_playlist_{playable_type.value}"), - ) - ) - - def __parse_show(self, hex_id: str) -> None: - show = self.__session.api().get_metadata_4_show(ShowId.from_hex(hex_id)) - for episode in show.episode: - self.__playable_list.append( - PlayableData( - PlayableType.EPISODE, - EpisodeId.from_hex(bytes_to_hex(episode.gid)), - self.__config.podcast_library, - self.__config.output_podcast, - ) - ) - - def __parse_track(self, hex_id: str) -> None: - self.__playable_list.append( - PlayableData( - PlayableType.TRACK, - TrackId.from_hex(hex_id), - self.__config.music_library, - self.__config.output_album, - ) - ) - - def __parse_episode(self, hex_id: str) -> None: - self.__playable_list.append( - PlayableData( - PlayableType.EPISODE, - EpisodeId.from_hex(hex_id), - self.__config.podcast_library, - self.__config.output_podcast, - ) - ) - - def get_playable_list(self) -> list[PlayableData]: - """Returns list of Playable items""" - return self.__playable_list - - def download_all(self) -> None: + def download_all(self, collections: list[Collection]) -> None: """Downloads playable to local file""" - for playable in self.__playable_list: - self.__download(playable) + for collection in collections: + for i in range(len(collection.playables)): + playable = collection.playables[i] - def __download(self, playable: PlayableData) -> None: - if playable.type == PlayableType.TRACK: - with Loader("Fetching track..."): - track = self.__session.get_track( - playable.id, self.__config.download_quality - ) - elif playable.type == PlayableType.EPISODE: - with Loader("Fetching episode..."): - track = self.__session.get_episode(playable.id) - else: - Printer.print( - PrintChannel.SKIPS, - f'Download Error: Unknown playable content "{playable.type}"', - ) - return - - output = track.create_output(playable.library, playable.output) - file = track.write_audio_stream( - output, - self.__config.chunk_size, - ) - - if playable.type == PlayableType.TRACK and self.__config.lyrics_file: - with Loader("Fetching lyrics..."): - try: - track.get_lyrics().save(output) - except FileNotFoundError as e: - Printer.print(PrintChannel.SKIPS, str(e)) - - Printer.print(PrintChannel.DOWNLOADS, f"\nDownloaded {track.name}") - - if self.__config.audio_format != AudioFormat.VORBIS: - try: - with Loader(PrintChannel.PROGRESS, "Converting audio..."): - file.transcode( - self.__config.audio_format, - self.__config.transcode_bitrate, - True, - self.__config.ffmpeg_path, - self.__config.ffmpeg_args.split(), + # Get track data + if playable.type == PlayableType.TRACK: + with Loader("Fetching track..."): + track = self.__session.get_track( + playable.id, self.__config.download_quality + ) + elif playable.type == PlayableType.EPISODE: + with Loader("Fetching episode..."): + track = self.__session.get_episode(playable.id) + else: + Logger.log( + LogChannel.SKIPS, + f'Download Error: Unknown playable content "{playable.type}"', ) - except TranscodingError as e: - Printer.print(PrintChannel.ERRORS, str(e)) + return - if self.__config.save_metadata: - with Loader("Writing metadata..."): - file.write_metadata(track.metadata) - file.write_cover_art(track.get_cover_art(self.__config.artwork_size)) + # Create download location and generate file name + match collection.type(): + case CollectionType.PLAYLIST: + # TODO: add playlist name to track metadata + library = self.__config.playlist_library + template = ( + self.__config.output_playlist_track + if playable.type == PlayableType.TRACK + else self.__config.output_playlist_episode + ) + case CollectionType.SHOW | CollectionType.EPISODE: + library = self.__config.podcast_library + template = self.__config.output_podcast + case _: + library = self.__config.music_library + template = self.__config.output_album + output = track.create_output( + library, template, self.__config.replace_existing + ) + + file = track.write_audio_stream(output) + + # Download lyrics + if playable.type == PlayableType.TRACK and self.__config.lyrics_file: + with Loader("Fetching lyrics..."): + try: + track.get_lyrics().save(output) + except FileNotFoundError as e: + Logger.log(LogChannel.SKIPS, str(e)) + Logger.log(LogChannel.DOWNLOADS, f"\nDownloaded {track.name}") + + # Transcode audio + if self.__config.audio_format != AudioFormat.VORBIS: + try: + with Loader(LogChannel.PROGRESS, "Converting audio..."): + file.transcode( + self.__config.audio_format, + self.__config.transcode_bitrate, + True, + self.__config.ffmpeg_path, + self.__config.ffmpeg_args.split(), + ) + except TranscodingError as e: + Logger.log(LogChannel.ERRORS, str(e)) + + # Write metadata + if self.__config.save_metadata: + with Loader("Writing metadata..."): + file.write_metadata(track.metadata) + file.write_cover_art( + track.get_cover_art(self.__config.artwork_size) + ) diff --git a/zotify/collections.py b/zotify/collections.py new file mode 100644 index 0000000..d43a3ed --- /dev/null +++ b/zotify/collections.py @@ -0,0 +1,95 @@ +from librespot.metadata import ( + AlbumId, + ArtistId, + PlaylistId, + ShowId, +) + +from zotify import Session +from zotify.utils import CollectionType, PlayableData, PlayableType, bytes_to_base62 + + +class Collection: + playables: list[PlayableData] = [] + + def type(self) -> CollectionType: + return CollectionType(self.__class__.__name__.lower()) + + +class Album(Collection): + def __init__(self, session: Session, b62_id: str): + album = session.api().get_metadata_4_album(AlbumId.from_base62(b62_id)) + for disc in album.disc: + for track in disc.track: + self.playables.append( + PlayableData( + PlayableType.TRACK, + bytes_to_base62(track.gid), + ) + ) + + +class Artist(Collection): + def __init__(self, session: Session, b62_id: str): + artist = session.api().get_metadata_4_artist(ArtistId.from_base62(b62_id)) + for album_group in ( + artist.album_group + and artist.single_group + and artist.compilation_group + and artist.appears_on_group + ): + album = session.api().get_metadata_4_album( + AlbumId.from_hex(album_group.album[0].gid) + ) + for disc in album.disc: + for track in disc.track: + self.playables.append( + PlayableData( + PlayableType.TRACK, + bytes_to_base62(track.gid), + ) + ) + + +class Show(Collection): + def __init__(self, session: Session, b62_id: str): + show = session.api().get_metadata_4_show(ShowId.from_base62(b62_id)) + for episode in show.episode: + self.playables.append( + PlayableData(PlayableType.EPISODE, bytes_to_base62(episode.gid)) + ) + + +class Playlist(Collection): + def __init__(self, session: Session, b62_id: str): + playlist = session.api().get_playlist(PlaylistId(b62_id)) + # self.name = playlist.title + for item in playlist.contents.items: + split = item.uri.split(":") + playable_type = split[1] + if playable_type == "track": + self.playables.append( + PlayableData( + PlayableType.TRACK, + split[2], + ) + ) + elif playable_type == "episode": + self.playables.append( + PlayableData( + PlayableType.EPISODE, + split[2], + ) + ) + else: + raise ValueError("Unknown playable content", playable_type) + + +class Track(Collection): + def __init__(self, session: Session, b62_id: str): + self.playables.append(PlayableData(PlayableType.TRACK, b62_id)) + + +class Episode(Collection): + def __init__(self, session: Session, b62_id: str): + self.playables.append(PlayableData(PlayableType.EPISODE, b62_id)) diff --git a/zotify/config.py b/zotify/config.py index c2d1a68..b6dcf53 100644 --- a/zotify/config.py +++ b/zotify/config.py @@ -10,7 +10,6 @@ from zotify.utils import AudioFormat, ImageSize, Quality ALL_ARTISTS = "all_artists" ARTWORK_SIZE = "artwork_size" AUDIO_FORMAT = "audio_format" -CHUNK_SIZE = "chunk_size" CREATE_PLAYLIST_FILE = "create_playlist_file" CREDENTIALS = "credentials" DOWNLOAD_QUALITY = "download_quality" @@ -64,8 +63,8 @@ CONFIG_PATHS = { OUTPUT_PATHS = { "album": "{album_artist}/{album}/{track_number}. {artists} - {title}", "podcast": "{podcast}/{episode_number} - {title}", - "playlist_track": "{playlist}/{playlist_number}. {artists} - {title}", - "playlist_episode": "{playlist}/{playlist_number}. {episode_number} - {title}", + "playlist_track": "{playlist}/{artists} - {title}", + "playlist_episode": "{playlist}/{episode_number} - {title}", } CONFIG_VALUES = { @@ -222,12 +221,6 @@ CONFIG_VALUES = { "args": ["--skip-duplicates"], "help": "Skip downloading existing track to different album", }, - CHUNK_SIZE: { - "default": 16384, - "type": int, - "args": ["--chunk-size"], - "help": "Number of bytes read at a time during download", - }, PRINT_DOWNLOADS: { "default": False, "type": bool, @@ -265,7 +258,6 @@ class Config: __config_file: Path | None artwork_size: ImageSize audio_format: AudioFormat - chunk_size: int credentials: Path download_quality: Quality ffmpeg_args: str @@ -274,13 +266,13 @@ class Config: language: str lyrics_file: bool output_album: str - output_liked: str output_podcast: str output_playlist_track: str output_playlist_episode: str playlist_library: Path podcast_library: Path print_progress: bool + replace_existing: bool save_metadata: bool transcode_bitrate: int @@ -323,14 +315,14 @@ class Config: # "library" arg overrides all *_library options if args.library: - self.music_library = args.library - self.playlist_library = args.library - self.podcast_library = args.library + print("args.library") + self.music_library = Path(args.library).expanduser().resolve() + self.playlist_library = Path(args.library).expanduser().resolve() + self.podcast_library = Path(args.library).expanduser().resolve() # "output" arg overrides all output_* options if args.output: self.output_album = args.output - self.output_liked = args.output self.output_podcast = args.output self.output_playlist_track = args.output self.output_playlist_episode = args.output @@ -338,10 +330,10 @@ class Config: @staticmethod def __parse_arg_value(key: str, value: Any) -> Any: config_type = CONFIG_VALUES[key]["type"] - if type(value) == config_type: + if type(value) is config_type: return value elif config_type == Path: - return Path(value).expanduser() + return Path(value).expanduser().resolve() elif config_type == AudioFormat: return AudioFormat[value.upper()] elif config_type == ImageSize.from_string: diff --git a/zotify/file.py b/zotify/file.py index 4cf1bfc..960f376 100644 --- a/zotify/file.py +++ b/zotify/file.py @@ -8,8 +8,7 @@ from mutagen.oggvorbis import OggVorbisHeaderError from zotify.utils import AudioFormat, MetadataEntry -class TranscodingError(RuntimeError): - ... +class TranscodingError(RuntimeError): ... class LocalFile: diff --git a/zotify/loader.py b/zotify/loader.py index 9eb3885..364a147 100644 --- a/zotify/loader.py +++ b/zotify/loader.py @@ -8,7 +8,7 @@ from sys import platform from threading import Thread from time import sleep -from zotify.printer import Printer +from zotify.logger import Logger class Loader: @@ -50,7 +50,7 @@ class Loader: for c in cycle(self.steps): if self.done: break - Printer.print_loader(f"\r {c} {self.desc} ") + Logger.print_loader(f"\r {c} {self.desc} ") sleep(self.timeout) def __enter__(self) -> None: @@ -59,10 +59,10 @@ class Loader: def stop(self) -> None: self.done = True cols = get_terminal_size((80, 20)).columns - Printer.print_loader("\r" + " " * cols) + Logger.print_loader("\r" + " " * cols) if self.end != "": - Printer.print_loader(f"\r{self.end}") + Logger.print_loader(f"\r{self.end}") def __exit__(self, exc_type, exc_value, tb) -> None: # handle exceptions with those variables ^ diff --git a/zotify/printer.py b/zotify/logger.py similarity index 85% rename from zotify/printer.py rename to zotify/logger.py index 901e1ff..46c9112 100644 --- a/zotify/printer.py +++ b/zotify/logger.py @@ -13,7 +13,7 @@ from zotify.config import ( ) -class PrintChannel(Enum): +class LogChannel(Enum): SKIPS = PRINT_SKIPS PROGRESS = PRINT_PROGRESS ERRORS = PRINT_ERRORS @@ -21,7 +21,7 @@ class PrintChannel(Enum): DOWNLOADS = PRINT_DOWNLOADS -class Printer: +class Logger: __config: Config @classmethod @@ -29,15 +29,15 @@ class Printer: cls.__config = config @classmethod - def print(cls, channel: PrintChannel, msg: str) -> None: + def log(cls, channel: LogChannel, msg: str) -> None: """ Prints a message to console if the print channel is enabled Args: - channel: PrintChannel to print to - msg: Message to print + channel: LogChannel to print to + msg: Message to log """ if cls.__config.get(channel.value): - if channel == PrintChannel.ERRORS: + if channel == LogChannel.ERRORS: print(msg, file=stderr) else: print(msg) @@ -76,7 +76,7 @@ class Printer: """ Prints animated loading symbol Args: - msg: Message to print + msg: Message to display """ if cls.__config.print_progress: print(msg, flush=True, end="") diff --git a/zotify/playable.py b/zotify/playable.py index dd312db..e2da87a 100644 --- a/zotify/playable.py +++ b/zotify/playable.py @@ -3,37 +3,40 @@ from pathlib import Path from typing import Any from librespot.core import PlayableContentFeeder +from librespot.metadata import AlbumId from librespot.structure import GeneralAudioStream from librespot.util import bytes_to_hex from requests import get from zotify.file import LocalFile -from zotify.printer import Printer +from zotify.logger import Logger from zotify.utils import ( - IMG_URL, - LYRICS_URL, AudioFormat, ImageSize, MetadataEntry, + PlayableType, bytes_to_base62, fix_filename, ) +IMG_URL = "https://i.s" + "cdn.co/image/" +LYRICS_URL = "https://sp" + "client.wg.sp" + "otify.com/color-lyrics/v2/track/" + class Lyrics: def __init__(self, lyrics: dict, **kwargs): - self.lines = [] - self.sync_type = lyrics["syncType"] + self.__lines = [] + self.__sync_type = lyrics["syncType"] for line in lyrics["lines"]: - self.lines.append(line["words"] + "\n") - if self.sync_type == "line_synced": - self.lines_synced = [] + self.__lines.append(line["words"] + "\n") + if self.__sync_type == "line_synced": + self.__lines_synced = [] for line in lyrics["lines"]: timestamp = int(line["start_time_ms"]) ts_minutes = str(floor(timestamp / 60000)).zfill(2) ts_seconds = str(floor((timestamp % 60000) / 1000)).zfill(2) ts_millis = str(floor(timestamp % 1000))[:2].zfill(2) - self.lines_synced.append( + self.__lines_synced.append( f"[{ts_minutes}:{ts_seconds}.{ts_millis}]{line.words}\n" ) @@ -44,21 +47,24 @@ class Lyrics: location: path to target lyrics file prefer_synced: Use line synced lyrics if available """ - if self.sync_type == "line_synced" and prefer_synced: + if self.__sync_type == "line_synced" and prefer_synced: with open(f"{path}.lrc", "w+", encoding="utf-8") as f: - f.writelines(self.lines_synced) + f.writelines(self.__lines_synced) else: with open(f"{path}.txt", "w+", encoding="utf-8") as f: - f.writelines(self.lines[:-1]) + f.writelines(self.__lines[:-1]) class Playable: cover_images: list[Any] + input_stream: GeneralAudioStream metadata: list[MetadataEntry] name: str - input_stream: GeneralAudioStream + type: PlayableType - def create_output(self, library: Path, output: str, replace: bool = False) -> Path: + def create_output( + self, library: Path = Path("./"), output: str = "{title}", replace: bool = False + ) -> Path: """ Creates save directory for the output file Args: @@ -68,9 +74,11 @@ class Playable: Returns: File path for the track """ - for m in self.metadata: - if m.output is not None: - output = output.replace("{" + m.name + "}", fix_filename(m.output)) + for meta in self.metadata: + if meta.string is not None: + output = output.replace( + "{" + meta.name + "}", fix_filename(meta.string) + ) file_path = library.joinpath(output).expanduser() if file_path.exists() and not replace: raise FileExistsError("File already downloaded") @@ -81,18 +89,16 @@ class Playable: def write_audio_stream( self, output: Path, - chunk_size: int = 128 * 1024, ) -> LocalFile: """ Writes audio stream to file Args: output: File path of saved audio stream - chunk_size: maximum number of bytes to read at a time Returns: LocalFile object """ file = f"{output}.ogg" - with open(file, "wb") as f, Printer.progress( + with open(file, "wb") as f, Logger.progress( desc=self.name, total=self.input_stream.size, unit="B", @@ -103,7 +109,7 @@ class Playable: ) as p_bar: chunk = None while chunk != b"": - chunk = self.input_stream.stream().read(chunk_size) + chunk = self.input_stream.stream().read(1024) p_bar.update(f.write(chunk)) return LocalFile(Path(file), AudioFormat.VORBIS) @@ -121,8 +127,6 @@ class Playable: class Track(PlayableContentFeeder.LoadedStream, Playable): - lyrics: Lyrics - def __init__(self, track: PlayableContentFeeder.LoadedStream, api): super(Track, self).__init__( track.track, @@ -131,8 +135,10 @@ class Track(PlayableContentFeeder.LoadedStream, Playable): track.metrics, ) self.__api = api + self.__lyrics: Lyrics self.cover_images = self.album.cover_group.image self.metadata = self.__default_metadata() + self.type = PlayableType.TRACK def __getattr__(self, name): try: @@ -142,6 +148,10 @@ class Track(PlayableContentFeeder.LoadedStream, Playable): def __default_metadata(self) -> list[MetadataEntry]: date = self.album.date + if not hasattr(self.album, "genre"): + self.track.album = self.__api().get_metadata_4_album( + AlbumId.from_hex(bytes_to_hex(self.album.gid)) + ) return [ MetadataEntry("album", self.album.name), MetadataEntry("album_artist", [a.name for a in self.album.artist]), @@ -155,6 +165,7 @@ class Track(PlayableContentFeeder.LoadedStream, Playable): MetadataEntry("popularity", int(self.popularity * 255) / 100), MetadataEntry("track_number", self.number, str(self.number).zfill(2)), MetadataEntry("title", self.name), + MetadataEntry("year", date.year), MetadataEntry( "replaygain_track_gain", self.normalization_data.track_gain_db, "" ), @@ -169,21 +180,21 @@ class Track(PlayableContentFeeder.LoadedStream, Playable): ), ] - def get_lyrics(self) -> Lyrics: + def lyrics(self) -> Lyrics: """Returns track lyrics if available""" if not self.track.has_lyrics: raise FileNotFoundError( f"No lyrics available for {self.track.artist[0].name} - {self.track.name}" ) try: - return self.lyrics + return self.__lyrics except AttributeError: - self.lyrics = Lyrics( + self.__lyrics = Lyrics( self.__api.invoke_url(LYRICS_URL + bytes_to_base62(self.track.gid))[ "lyrics" ] ) - return self.lyrics + return self.__lyrics class Episode(PlayableContentFeeder.LoadedStream, Playable): @@ -197,6 +208,7 @@ class Episode(PlayableContentFeeder.LoadedStream, Playable): self.__api = api self.cover_images = self.episode.cover_image.image self.metadata = self.__default_metadata() + self.type = PlayableType.EPISODE def __getattr__(self, name): try: @@ -216,23 +228,21 @@ class Episode(PlayableContentFeeder.LoadedStream, Playable): MetadataEntry("title", self.name), ] - def write_audio_stream( - self, output: Path, chunk_size: int = 128 * 1024 - ) -> LocalFile: + def write_audio_stream(self, output: Path) -> LocalFile: """ - Writes audio stream to file + Writes audio stream to file. + Uses external source if available for faster download. Args: output: File path of saved audio stream - chunk_size: maximum number of bytes to read at a time Returns: LocalFile object """ if not bool(self.external_url): - return super().write_audio_stream(output, chunk_size) + return super().write_audio_stream(output) file = f"{output}.{self.external_url.rsplit('.', 1)[-1]}" with get(self.external_url, stream=True) as r, open( file, "wb" - ) as f, Printer.progress( + ) as f, Logger.progress( desc=self.name, total=self.input_stream.size, unit="B", @@ -241,6 +251,6 @@ class Episode(PlayableContentFeeder.LoadedStream, Playable): position=0, leave=False, ) as p_bar: - for chunk in r.iter_content(chunk_size=chunk_size): + for chunk in r.iter_content(chunk_size=1024): p_bar.update(f.write(chunk)) return LocalFile(Path(file)) diff --git a/zotify/utils.py b/zotify/utils.py index 01d5236..62dfc22 100644 --- a/zotify/utils.py +++ b/zotify/utils.py @@ -7,12 +7,8 @@ from sys import stderr from typing import Any, NamedTuple from librespot.audio.decoders import AudioQuality -from librespot.util import Base62, bytes_to_hex -from requests import get +from librespot.util import Base62 -API_URL = "https://api.sp" + "otify.com/v1/" -IMG_URL = "https://i.s" + "cdn.co/image/" -LYRICS_URL = "https://sp" + "client.wg.sp" + "otify.com/color-lyrics/v2/track/" BASE62 = Base62.create_instance_with_inverted_character_set() @@ -74,30 +70,47 @@ class ImageSize(IntEnum): class MetadataEntry: name: str value: Any - output: str + string: str - def __init__(self, name: str, value: Any, output_value: str | None = None): + def __init__(self, name: str, value: Any, string_value: str | None = None): """ Holds metadata entries args: name: name of metadata key value: Value to use in metadata tags - output_value: Value when used in output formatting, if none is provided + string_value: Value when used in output formatting, if none is provided will use value from previous argument. """ self.name = name - if type(value) == list: + if isinstance(value, tuple): value = "\0".join(value) self.value = value - if output_value is None: - output_value = self.value - elif output_value == "": - output_value = None - if type(output_value) == list: - output_value = ", ".join(output_value) - self.output = str(output_value) + if string_value is None: + string_value = self.value + if isinstance(string_value, list): + string_value = ", ".join(string_value) + self.string = str(string_value) + + +class CollectionType(Enum): + ALBUM = "album" + ARTIST = "artist" + SHOW = "show" + PLAYLIST = "playlist" + TRACK = "track" + EPISODE = "episode" + + +class PlayableType(Enum): + TRACK = "track" + EPISODE = "episode" + + +class PlayableData(NamedTuple): + type: PlayableType + id: str class SimpleHelpFormatter(HelpFormatter): @@ -147,7 +160,14 @@ class OptionalOrFalse(Action): setattr( namespace, self.dest, - True if not option_string.startswith("--no-") else False, + ( + True + if not ( + option_string.startswith("--no-") + or option_string.startswith("--dont-") + ) + else False + ), ) @@ -172,29 +192,12 @@ def fix_filename(filename: str, substitute: str = "_", platform: str = PLATFORM) return sub(regex, substitute, str(filename), flags=IGNORECASE) -def download_cover_art(images: list, size: ImageSize) -> bytes: - """ - Returns image data of cover art - Args: - images: list of retrievable images - size: Desired size in pixels of cover art, can be 640, 300, or 64 - Returns: - Image data of cover art - """ - return get(images[size.value]["url"]).content - - -def str_to_bool(value: str) -> bool: - if value.lower() in ["yes", "y", "true"]: - return True - if value.lower() in ["no", "n", "false"]: - return False - raise TypeError("Not a boolean: " + value) - - def bytes_to_base62(id: bytes) -> str: + """ + Converts bytes to base62 + Args: + id: bytes + Returns: + base62 + """ return BASE62.encode(id, 22).decode() - - -def b62_to_hex(base62: str) -> str: - return bytes_to_hex(BASE62.decode(base62.encode(), 16))