various changes
This commit is contained in:
parent
360e342bc2
commit
b361976504
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
}
|
|
@ -15,9 +15,8 @@
|
|||
- 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
|
||||
- Renamed `--liked`/`-l` to `--liked-tracks`/`-lt`
|
||||
- Renamed `root_path` and `root_podcast_path` to `music_library` and `podcast_library`
|
||||
- Renamed `root_path` and `root_podcast_path` to `album_library` and `podcast_library`
|
||||
- `--username` and `--password` arguments now take priority over saved credentials
|
||||
- Regex pattern for cleaning filenames is now OS specific, allowing more usable characters on Linux & macOS.
|
||||
- On Linux both `config.json` and `credentials.json` are now kept under `$XDG_CONFIG_HOME/zotify/`, (`~/.config/zotify/` by default).
|
||||
- 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
|
||||
|
@ -29,7 +28,7 @@
|
|||
|
||||
- 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`
|
||||
- `--library`/`-l` overrides both `album_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.
|
||||
- `--debug` shows full tracebacks on crash instead of just the final error message
|
||||
- Added new shorthand aliases to some options:
|
||||
|
@ -55,6 +54,9 @@
|
|||
- `{explicit}`
|
||||
- `{isrc}`
|
||||
- `{licensor}`
|
||||
- `{playlist}`
|
||||
- `{playlist_number}`
|
||||
- `{playlist_owner}`
|
||||
- `{popularity}`
|
||||
- `{release_date}`
|
||||
- `{track_number}`
|
||||
|
|
5
Pipfile
5
Pipfile
|
@ -13,6 +13,11 @@ requests = "*"
|
|||
tqdm = "*"
|
||||
|
||||
[dev-packages]
|
||||
black = "*"
|
||||
flake8 = "*"
|
||||
mypy = "*"
|
||||
types-protobuf = "*"
|
||||
types-requests = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "dfbc5e27f802eeeddf2967a8d8d280346f8e3b4e4759b4bea10f59dbee08a0ee"
|
||||
"sha256": "9cf0a0fbfd691c64820035a5c12805f868ae1d2401630b9f68b67b936f5e7892"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -18,11 +18,11 @@
|
|||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
|
||||
"sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
|
||||
"sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
|
||||
"sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2024.2.2"
|
||||
"version": "==2024.7.4"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
|
@ -130,11 +130,11 @@
|
|||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
|
||||
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
|
||||
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
|
||||
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.6"
|
||||
"version": "==3.7"
|
||||
},
|
||||
"ifaddr": {
|
||||
"hashes": [
|
||||
|
@ -145,7 +145,7 @@
|
|||
},
|
||||
"librespot": {
|
||||
"git": "git+https://github.com/kokarare1212/librespot-python",
|
||||
"ref": "f56533f9b56e62b28bac6c57d0710620aeb6a5dd"
|
||||
"ref": "3b46fe560ad829b976ce63e85012cff95b1e0bf3"
|
||||
},
|
||||
"music-tag": {
|
||||
"git": "git+https://zotify.xyz/zotify/music-tag",
|
||||
|
@ -162,78 +162,90 @@
|
|||
},
|
||||
"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"
|
||||
"sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885",
|
||||
"sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea",
|
||||
"sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df",
|
||||
"sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5",
|
||||
"sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c",
|
||||
"sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d",
|
||||
"sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd",
|
||||
"sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06",
|
||||
"sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908",
|
||||
"sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a",
|
||||
"sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be",
|
||||
"sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0",
|
||||
"sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b",
|
||||
"sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80",
|
||||
"sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a",
|
||||
"sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e",
|
||||
"sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9",
|
||||
"sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696",
|
||||
"sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b",
|
||||
"sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309",
|
||||
"sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e",
|
||||
"sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab",
|
||||
"sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d",
|
||||
"sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060",
|
||||
"sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d",
|
||||
"sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d",
|
||||
"sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4",
|
||||
"sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3",
|
||||
"sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6",
|
||||
"sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb",
|
||||
"sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94",
|
||||
"sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b",
|
||||
"sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496",
|
||||
"sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0",
|
||||
"sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319",
|
||||
"sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b",
|
||||
"sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856",
|
||||
"sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef",
|
||||
"sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680",
|
||||
"sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b",
|
||||
"sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42",
|
||||
"sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e",
|
||||
"sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597",
|
||||
"sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a",
|
||||
"sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8",
|
||||
"sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3",
|
||||
"sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736",
|
||||
"sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da",
|
||||
"sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126",
|
||||
"sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd",
|
||||
"sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5",
|
||||
"sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b",
|
||||
"sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026",
|
||||
"sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b",
|
||||
"sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc",
|
||||
"sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46",
|
||||
"sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2",
|
||||
"sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c",
|
||||
"sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe",
|
||||
"sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984",
|
||||
"sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a",
|
||||
"sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70",
|
||||
"sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca",
|
||||
"sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b",
|
||||
"sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91",
|
||||
"sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3",
|
||||
"sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84",
|
||||
"sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1",
|
||||
"sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5",
|
||||
"sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be",
|
||||
"sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f",
|
||||
"sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc",
|
||||
"sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9",
|
||||
"sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e",
|
||||
"sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141",
|
||||
"sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef",
|
||||
"sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22",
|
||||
"sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27",
|
||||
"sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e",
|
||||
"sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==10.2.0"
|
||||
"version": "==10.4.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
|
@ -320,95 +332,266 @@
|
|||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
|
||||
"sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760",
|
||||
"sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.31.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.32.3"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386",
|
||||
"sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"
|
||||
"sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644",
|
||||
"sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.66.1"
|
||||
"version": "==4.66.4"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20",
|
||||
"sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"
|
||||
"sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
|
||||
"sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.2.0"
|
||||
"version": "==2.2.2"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6",
|
||||
"sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"
|
||||
"sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526",
|
||||
"sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.7.0"
|
||||
"version": "==1.8.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"
|
||||
"sha256:06203c23a82b69aca9e961da675600dff19026bb22b5d042f18f9e0ff1139ed3",
|
||||
"sha256:0b0d2ffc4bafbcc4152067bfbc1a67074d23e6100e356424bd985ca8067a2bfd",
|
||||
"sha256:13beed15eed7e569fd56dbe16c7cb758f81c661d53ec253fbf9cfe7a20e28b7c",
|
||||
"sha256:1a95025f0949ed0e873e141d482fbbefa223ef90646443e4a1d6d47f50eb89d7",
|
||||
"sha256:1c932b15848ae6b8e4b2b50c65368e396d000fea95acd473611693dbe5a00096",
|
||||
"sha256:1f09b692219abf9b1ca28364d6f4eb283a4c676e30c905933d1694cbd321bc4b",
|
||||
"sha256:28b1721617ddc9bf3d2ba3e2b96234f7539e1dbdcacaf6e94ec31ff7b5ebe620",
|
||||
"sha256:31c8406f62251aa62f5b67d865007ffd1dd929eae9027166ffa6bccca69253bd",
|
||||
"sha256:390feb3e7fccdffbf66c9bcd895b1db92e501aa2789d6a8b44e6e027ab80ec14",
|
||||
"sha256:3ad2fe0cbfebe20612c9a5390075a2b3a258a78928f5b7b5163be1699cc528f0",
|
||||
"sha256:3bd0cd9435dced8c31491b3ed7c15707acedd11f00451f7fbb57ba3868dd5724",
|
||||
"sha256:3eb0e57654e139c3ef5b6421053236be4a0add9f0301b01545b11a0552c7c123",
|
||||
"sha256:4754dfba1af63545dfd0ab26c834c907e1dd3f94c8ee190c3041a6444313aaed",
|
||||
"sha256:48275e3db89a8d90ff983c3f7b0c6eee2ede3c4e5e75eaf2aa571ea8cb956d95",
|
||||
"sha256:4dd7d8fdee36cc6bde0bcb08b79375009de7a76d935d1401b6ae4b62505b9ee0",
|
||||
"sha256:4e83e18722d0bdc2e603f7ca104adf276d5728a664b9e94c99e2d8c02001429c",
|
||||
"sha256:5354c1cf83d36b2d03ee5774923d30fe838f9371963b42ca46ecba45d3507ff4",
|
||||
"sha256:5586bc773d6cee4f9a14692f5e6bc6387ddb54b2bfae0db01c0695aac20c420a",
|
||||
"sha256:56146e66774c30e238088f67be47740ffd4f669c08e76f2e470bd611d7bdae46",
|
||||
"sha256:59953e8445e69e5fee53381c437d3494f7fac8d7b51f0169d59b69eba8f95063",
|
||||
"sha256:5b6cfc2b62e6282eabbcb6c7223b0a8c05ed3a326e7b467d06b85a3eeda1bfc8",
|
||||
"sha256:5c8c2eeb838538fffaa421f9b3f9c671778886595b5aa0d4ef4d000531e721d2",
|
||||
"sha256:6732b224be7e69f7c77798e50205f8e92646ab59724151d66d8dc97f92e99a77",
|
||||
"sha256:700bae69eb7c45037deef4a729586f32205d391de38802e2ab89151a7a87d1fc",
|
||||
"sha256:76d12185c335c14b04b8706b4dd0badc16f4185caeb635419c84e575cef7c980",
|
||||
"sha256:779d81aac693e57090343ce5b18f477fec993f969aa87660a33e7ce81880ccdf",
|
||||
"sha256:82678a77e471dd3b0ad5ed47a4a42474af3150819718eff7e36dca32ae591949",
|
||||
"sha256:87b6e92a869932f4aac3076816a1b987c581b01e49a08e495bef7165be049dfd",
|
||||
"sha256:9228c512334905338f65825102e47778e5ce034bb4249c3deb22991826ed061f",
|
||||
"sha256:9ad8bc6e3f168fe8c164634c762d3265c775643defff10e26273623a12d73ae1",
|
||||
"sha256:9c295b424a271ce5022da83a1274b4cd0f696c5b8e0c190e6a28efde8b36e82d",
|
||||
"sha256:9d364a929121df5b96af53ac62abdd61fa3a931e74c7a4c80204c961c01a8667",
|
||||
"sha256:a2fa3a89f6a0cf03a56141dad158634a009a22fbe645c7c01e85edc12a0a239f",
|
||||
"sha256:a37fe4f302edb8d931a4c386d0944f996e3f54717495636113880c4492ab479f",
|
||||
"sha256:a49b13ec79edff347b1e7af65f5843719ca151ef071ac6b2ff564bb69d164331",
|
||||
"sha256:b20036ab22df2fb663f797b110fa82d4798084fcc56c8a264af50989581062be",
|
||||
"sha256:b3dd7143dfc37a20f7d1ccf32f916ac78c11d3c8bae61438ee06376b1bc535fc",
|
||||
"sha256:b60b260c70bb77d7f3b666bdd2a2a74cead5e36814f8b4295778bcdd08f65c7e",
|
||||
"sha256:c50ee0df6b0b06f1dad6261670b5be53c909b9a2b1985bcf65ea5b0d766fd10e",
|
||||
"sha256:ca46637fcc0386fdbe6bde447184ed981499c8c1b5b5fcaa0f35c3b15528162a",
|
||||
"sha256:d4bc5e43d02e0848c3174914595dfcebed9b74e65cbdfb1011c5082db7916605",
|
||||
"sha256:d6c05af8b49c442422ce49565ab41a094b23e0f5692abe1533428cbe35a78f8e",
|
||||
"sha256:d80bde641349198c8c17684692a8cc40a36a93c0cebd8f1d7c42db7ceeaa17be",
|
||||
"sha256:db8607a32347da1fd4519cfea441d8b36b44df0c53198ae0471c76fc932a86e0",
|
||||
"sha256:ddae9592604fe04ec065cc53a321844c3592c812988346136d8ee548127f3d12",
|
||||
"sha256:e1031c7c5f8516108e7c24190179e6a522183de218a954681a341ee818f8079a",
|
||||
"sha256:e36f50a963d149bb7152543db9bdbd73f7997e66b57b7956fc17751f55e59625",
|
||||
"sha256:e7e2c398679c863e810a9af2c5d14542a32d438e3bf5ba0b9d8e119326c33303",
|
||||
"sha256:f2b26c23efeded0e7fcfd0fb4d638ec4a83d120e1d455267d353090e36479528",
|
||||
"sha256:f56ec955f43f944985f857c9d23030362df52e14a7c53c64bf8b29cfadebd601",
|
||||
"sha256:f9a28b0416a36ec32273ee1ac80cc72ff9b06d1cb15a9481dcd5c92bd2bc8f03"
|
||||
],
|
||||
"markers": "python_version >= '3.7' and python_version < '4.0'",
|
||||
"version": "==0.131.0"
|
||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||
"version": "==0.132.2"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
"develop": {
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474",
|
||||
"sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1",
|
||||
"sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0",
|
||||
"sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8",
|
||||
"sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96",
|
||||
"sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1",
|
||||
"sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04",
|
||||
"sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021",
|
||||
"sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94",
|
||||
"sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d",
|
||||
"sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c",
|
||||
"sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7",
|
||||
"sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c",
|
||||
"sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc",
|
||||
"sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7",
|
||||
"sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d",
|
||||
"sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c",
|
||||
"sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741",
|
||||
"sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce",
|
||||
"sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb",
|
||||
"sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063",
|
||||
"sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==24.4.2"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
|
||||
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.7"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a",
|
||||
"sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_full_version >= '3.8.1'",
|
||||
"version": "==7.1.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54",
|
||||
"sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a",
|
||||
"sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72",
|
||||
"sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69",
|
||||
"sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b",
|
||||
"sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe",
|
||||
"sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4",
|
||||
"sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd",
|
||||
"sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0",
|
||||
"sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525",
|
||||
"sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2",
|
||||
"sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c",
|
||||
"sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5",
|
||||
"sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de",
|
||||
"sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74",
|
||||
"sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c",
|
||||
"sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e",
|
||||
"sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58",
|
||||
"sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b",
|
||||
"sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417",
|
||||
"sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411",
|
||||
"sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb",
|
||||
"sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03",
|
||||
"sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca",
|
||||
"sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8",
|
||||
"sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08",
|
||||
"sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.11.1"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
|
||||
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
|
||||
"sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==24.1"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
|
||||
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.12.1"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee",
|
||||
"sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c",
|
||||
"sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.12.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f",
|
||||
"sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"types-protobuf": {
|
||||
"hashes": [
|
||||
"sha256:683ba14043bade6785e3f937a7498f243b37881a91ac8d81b9202ecf8b191e9c",
|
||||
"sha256:688e8f7e8d9295db26bc560df01fb731b27a25b77cbe4c1ce945647f7024f5c1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==5.27.0.20240626"
|
||||
},
|
||||
"types-requests": {
|
||||
"hashes": [
|
||||
"sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358",
|
||||
"sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.32.0.20240712"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
|
||||
"sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.12.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
|
||||
"sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
README.md
15
README.md
|
@ -10,19 +10,20 @@ Built on [Librespot](https://github.com/kokarare1212/librespot-python).
|
|||
|
||||
## Features
|
||||
|
||||
- Save tracks at up to 320kbps\*
|
||||
- Save tracks at up to 320kbps<sup>**1**</sup>
|
||||
- Save to most popular audio formats
|
||||
- Built in search
|
||||
- Bulk downloads
|
||||
- Downloads synced lyrics
|
||||
- Downloads synced lyrics<sup>**2**</sup>
|
||||
- Embedded metadata
|
||||
- Downloads all audio, metadata and lyrics directly, no substituting from other services.
|
||||
|
||||
\*Non-premium accounts are limited to 160kbps
|
||||
**1**: Non-premium accounts are limited to 160kbps \
|
||||
**2**: Requires premium
|
||||
|
||||
## Installation
|
||||
|
||||
Requires Python 3.10 or greater. \
|
||||
Requires Python 3.11 or greater. \
|
||||
Optionally requires FFmpeg to save tracks as anything other than Ogg Vorbis.
|
||||
|
||||
Enter the following command in terminal to install Zotify. \
|
||||
|
@ -64,8 +65,6 @@ Downloads specified items. Accepts any combination of track, album, playlist, ep
|
|||
| 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 |
|
||||
|
||||
</details>
|
||||
|
||||
|
@ -104,7 +103,7 @@ file.write_cover_art(track.get_cover_art())
|
|||
|
||||
## Contributing
|
||||
|
||||
Pull requests are always welcome, but if adding an entirely new feature we encourage you to create an issue proposing the feature first so we can ensure it's something that fits sthe scope of the project.
|
||||
Pull requests are always welcome, but if adding an entirely new feature we encourage you to create an issue proposing the feature first so we can ensure it's something that fits the scope of the project.
|
||||
|
||||
Zotify aims to be a comprehensive and user-friendly tool for downloading music and podcasts.
|
||||
It is designed to be simple by default but offer a high level of configuration for users that want it.
|
||||
|
@ -112,7 +111,7 @@ All new contributions should follow this principle to keep the program consisten
|
|||
|
||||
## Will my account get banned if I use this tool?
|
||||
|
||||
There have been no confirmed cases of accounts getting banned as a result of 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://watsonbox.github.io/exportify/) to keep backups of your playlists.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
librespot>=0.0.9
|
||||
librespot@git+https://github.com/kokarare1212/librespot-python
|
||||
music-tag@git+https://zotify.xyz/zotify/music-tag
|
||||
mutagen
|
||||
Pillow
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
black
|
||||
flake8
|
||||
mypy
|
||||
pre-commit
|
||||
types-protobuf
|
||||
types-requests
|
||||
wheel
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
[metadata]
|
||||
name = zotify
|
||||
version = 0.9.4
|
||||
version = 0.9.5
|
||||
author = Zotify Contributors
|
||||
description = A highly customizable music and podcast downloader
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
keywords = python, music, podcast, downloader
|
||||
keywords = music, podcast, downloader
|
||||
licence = Zlib
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
|
@ -17,9 +17,9 @@ classifiers =
|
|||
|
||||
[options]
|
||||
packages = zotify
|
||||
python_requires = >=3.10
|
||||
python_requires = >=3.11
|
||||
install_requires =
|
||||
librespot>=0.0.9
|
||||
librespot@git+https://github.com/kokarare1212/librespot-python
|
||||
music-tag@git+https://zotify.xyz/zotify/music-tag
|
||||
mutagen
|
||||
Pillow
|
||||
|
|
|
@ -97,7 +97,7 @@ class Session(LibrespotSession):
|
|||
self.__language = language
|
||||
|
||||
@staticmethod
|
||||
def from_file(cred_file: Path, language: str = "en") -> Session:
|
||||
def from_file(cred_file: Path | str, language: str = "en") -> Session:
|
||||
"""
|
||||
Creates session using saved credentials file
|
||||
Args:
|
||||
|
@ -106,6 +106,8 @@ class Session(LibrespotSession):
|
|||
Returns:
|
||||
Zotify session
|
||||
"""
|
||||
if not isinstance(cred_file, Path):
|
||||
cred_file = Path(cred_file).expanduser()
|
||||
conf = (
|
||||
LibrespotSession.Configuration.Builder()
|
||||
.set_store_credentials(False)
|
||||
|
@ -118,7 +120,7 @@ class Session(LibrespotSession):
|
|||
def from_userpass(
|
||||
username: str,
|
||||
password: str,
|
||||
save_file: Path | None = None,
|
||||
save_file: Path | str | None = None,
|
||||
language: str = "en",
|
||||
) -> Session:
|
||||
"""
|
||||
|
@ -133,6 +135,8 @@ class Session(LibrespotSession):
|
|||
"""
|
||||
builder = LibrespotSession.Configuration.Builder()
|
||||
if save_file:
|
||||
if not isinstance(save_file, Path):
|
||||
save_file = Path(save_file).expanduser()
|
||||
save_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
builder.set_stored_credential_file(str(save_file))
|
||||
else:
|
||||
|
@ -144,7 +148,9 @@ class Session(LibrespotSession):
|
|||
return Session(session, language)
|
||||
|
||||
@staticmethod
|
||||
def from_prompt(save_file: Path | None = None, language: str = "en") -> Session:
|
||||
def from_prompt(
|
||||
save_file: Path | str | None = None, language: str = "en"
|
||||
) -> Session:
|
||||
"""
|
||||
Creates a session with username + password supplied from CLI prompt
|
||||
Args:
|
||||
|
|
|
@ -5,16 +5,15 @@ from pathlib import Path
|
|||
|
||||
from zotify.app import App
|
||||
from zotify.config import CONFIG_PATHS, CONFIG_VALUES
|
||||
from zotify.utils import OptionalOrFalse, SimpleHelpFormatter
|
||||
from zotify.utils import OptionalOrFalse
|
||||
|
||||
VERSION = "0.9.4"
|
||||
VERSION = "0.9.5"
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(
|
||||
prog="zotify",
|
||||
description="A fast and customizable music and podcast downloader",
|
||||
formatter_class=SimpleHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
|
@ -53,7 +52,7 @@ def main():
|
|||
)
|
||||
parser.add_argument("--username", type=str, default="", help="Account username")
|
||||
parser.add_argument("--password", type=str, default="", help="Account password")
|
||||
group = parser.add_mutually_exclusive_group(required=False)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
"urls",
|
||||
type=str,
|
||||
|
|
120
zotify/app.py
120
zotify/app.py
|
@ -8,11 +8,7 @@ from zotify.config import Config
|
|||
from zotify.file import TranscodingError
|
||||
from zotify.loader import Loader
|
||||
from zotify.logger import LogChannel, Logger
|
||||
from zotify.utils import (
|
||||
AudioFormat,
|
||||
CollectionType,
|
||||
PlayableType,
|
||||
)
|
||||
from zotify.utils import AudioFormat, PlayableType
|
||||
|
||||
|
||||
class ParseError(ValueError): ...
|
||||
|
@ -32,7 +28,7 @@ class Selection:
|
|||
def search(
|
||||
self,
|
||||
search_text: str,
|
||||
category: list = [
|
||||
category: list[str] = [
|
||||
"track",
|
||||
"album",
|
||||
"artist",
|
||||
|
@ -56,12 +52,13 @@ class Selection:
|
|||
offset=0,
|
||||
)
|
||||
|
||||
print(f'Search results for "{search_text}"')
|
||||
count = 0
|
||||
for cat in categories.split(","):
|
||||
label = cat + "s"
|
||||
items = resp[label]["items"]
|
||||
if len(items) > 0:
|
||||
print(f"\n### {label.capitalize()} ###")
|
||||
print(f"\n{label.capitalize()}:")
|
||||
try:
|
||||
self.__print(count, items, *self.__print_labels[cat])
|
||||
except KeyError:
|
||||
|
@ -109,7 +106,7 @@ class Selection:
|
|||
|
||||
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)
|
||||
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
|
||||
|
@ -149,30 +146,21 @@ class App:
|
|||
self.__config = Config(args)
|
||||
Logger(self.__config)
|
||||
|
||||
# Check options
|
||||
if self.__config.audio_format == AudioFormat.VORBIS and (
|
||||
self.__config.ffmpeg_args != "" or self.__config.ffmpeg_path != ""
|
||||
):
|
||||
Logger.log(
|
||||
LogChannel.WARNINGS,
|
||||
"FFmpeg options will be ignored since no transcoding is required",
|
||||
)
|
||||
|
||||
# Create session
|
||||
if args.username != "" and args.password != "":
|
||||
self.__session = Session.from_userpass(
|
||||
args.username,
|
||||
args.password,
|
||||
self.__config.credentials,
|
||||
self.__config.credentials_path,
|
||||
self.__config.language,
|
||||
)
|
||||
elif self.__config.credentials.is_file():
|
||||
elif self.__config.credentials_path.is_file():
|
||||
self.__session = Session.from_file(
|
||||
self.__config.credentials, self.__config.language
|
||||
self.__config.credentials_path, self.__config.language
|
||||
)
|
||||
else:
|
||||
self.__session = Session.from_prompt(
|
||||
self.__config.credentials, self.__config.language
|
||||
self.__config.credentials_path, self.__config.language
|
||||
)
|
||||
|
||||
# Get items to download
|
||||
|
@ -182,6 +170,7 @@ class App:
|
|||
collections = self.parse(ids)
|
||||
except ParseError as e:
|
||||
Logger.log(LogChannel.ERRORS, str(e))
|
||||
exit(1)
|
||||
if len(collections) > 0:
|
||||
self.download_all(collections)
|
||||
else:
|
||||
|
@ -208,11 +197,12 @@ class App:
|
|||
return ids
|
||||
elif args.urls:
|
||||
return args.urls
|
||||
except (FileNotFoundError, ValueError):
|
||||
Logger.log(LogChannel.WARNINGS, "there is nothing to do")
|
||||
except KeyboardInterrupt:
|
||||
Logger.log(LogChannel.WARNINGS, "\nthere is nothing to do")
|
||||
exit(130)
|
||||
except (FileNotFoundError, ValueError):
|
||||
pass
|
||||
Logger.log(LogChannel.WARNINGS, "there is nothing to do")
|
||||
exit(0)
|
||||
|
||||
def parse(self, links: list[str]) -> list[Collection]:
|
||||
|
@ -226,28 +216,28 @@ class App:
|
|||
except IndexError:
|
||||
raise ParseError(f'Could not parse "{link}"')
|
||||
|
||||
match id_type:
|
||||
case "album":
|
||||
collections.append(Album(self.__session, _id))
|
||||
case "artist":
|
||||
collections.append(Artist(self.__session, _id))
|
||||
case "show":
|
||||
collections.append(Show(self.__session, _id))
|
||||
case "track":
|
||||
collections.append(Track(self.__session, _id))
|
||||
case "episode":
|
||||
collections.append(Episode(self.__session, _id))
|
||||
case "playlist":
|
||||
collections.append(Playlist(self.__session, _id))
|
||||
case _:
|
||||
collection_types = {
|
||||
"album": Album,
|
||||
"artist": Artist,
|
||||
"show": Show,
|
||||
"track": Track,
|
||||
"episode": Episode,
|
||||
"playlist": Playlist,
|
||||
}
|
||||
try:
|
||||
collections.append(
|
||||
collection_types[id_type](_id, self.__session.api(), self.__config)
|
||||
)
|
||||
except ValueError:
|
||||
raise ParseError(f'Unsupported content type "{id_type}"')
|
||||
return collections
|
||||
|
||||
def download_all(self, collections: list[Collection]) -> None:
|
||||
"""Downloads playable to local file"""
|
||||
count = 0
|
||||
total = sum(len(c.playables) for c in collections)
|
||||
for collection in collections:
|
||||
for i in range(len(collection.playables)):
|
||||
playable = collection.playables[i]
|
||||
for playable in collection.playables:
|
||||
count += 1
|
||||
|
||||
# Get track data
|
||||
if playable.type == PlayableType.TRACK:
|
||||
|
@ -263,43 +253,51 @@ class App:
|
|||
LogChannel.SKIPS,
|
||||
f'Download Error: Unknown playable content "{playable.type}"',
|
||||
)
|
||||
return
|
||||
continue
|
||||
|
||||
# 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
|
||||
track.metadata.extend(playable.metadata)
|
||||
try:
|
||||
output = track.create_output(
|
||||
library, template, self.__config.replace_existing
|
||||
playable.library,
|
||||
playable.output_template,
|
||||
self.__config.replace_existing,
|
||||
)
|
||||
except FileExistsError:
|
||||
Logger.log(
|
||||
LogChannel.SKIPS,
|
||||
f'Skipping "{track.name}": Already exists at specified output',
|
||||
)
|
||||
|
||||
file = track.write_audio_stream(output)
|
||||
# Download track
|
||||
with Logger.progress(
|
||||
desc=f"({count}/{total}) {track.name}",
|
||||
total=track.input_stream.size,
|
||||
) as p_bar:
|
||||
file = track.write_audio_stream(output, p_bar)
|
||||
|
||||
# Download lyrics
|
||||
if playable.type == PlayableType.TRACK and self.__config.lyrics_file:
|
||||
if not self.__session.is_premium():
|
||||
Logger.log(
|
||||
LogChannel.SKIPS,
|
||||
f'Failed to save lyrics for "{track.name}": Lyrics are only available to premium users',
|
||||
)
|
||||
else:
|
||||
with Loader("Fetching lyrics..."):
|
||||
try:
|
||||
track.get_lyrics().save(output)
|
||||
track.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:
|
||||
if (
|
||||
self.__config.audio_format != AudioFormat.VORBIS
|
||||
or self.__config.ffmpeg_args != ""
|
||||
):
|
||||
try:
|
||||
with Loader(LogChannel.PROGRESS, "Converting audio..."):
|
||||
with Loader("Converting audio..."):
|
||||
file.transcode(
|
||||
self.__config.audio_format,
|
||||
self.__config.transcode_bitrate,
|
||||
|
|
|
@ -5,80 +5,105 @@ from librespot.metadata import (
|
|||
ShowId,
|
||||
)
|
||||
|
||||
from zotify import Session
|
||||
from zotify.utils import CollectionType, PlayableData, PlayableType, bytes_to_base62
|
||||
from zotify import Api
|
||||
from zotify.config import Config
|
||||
from zotify.utils import MetadataEntry, PlayableData, PlayableType, bytes_to_base62
|
||||
|
||||
|
||||
class Collection:
|
||||
playables: list[PlayableData] = []
|
||||
|
||||
def type(self) -> CollectionType:
|
||||
return CollectionType(self.__class__.__name__.lower())
|
||||
def __init__(self, b62_id: str, api: Api, config: Config = Config()):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Album(Collection):
|
||||
def __init__(self, session: Session, b62_id: str):
|
||||
album = session.api().get_metadata_4_album(AlbumId.from_base62(b62_id))
|
||||
def __init__(self, b62_id: str, api: Api, config: Config = Config()):
|
||||
album = 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),
|
||||
config.album_library,
|
||||
config.output_album,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Artist(Collection):
|
||||
def __init__(self, session: Session, b62_id: str):
|
||||
artist = session.api().get_metadata_4_artist(ArtistId.from_base62(b62_id))
|
||||
def __init__(self, b62_id: str, api: Api, config: Config = Config()):
|
||||
artist = 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)
|
||||
)
|
||||
album = 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),
|
||||
config.album_library,
|
||||
config.output_album,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Show(Collection):
|
||||
def __init__(self, session: Session, b62_id: str):
|
||||
show = session.api().get_metadata_4_show(ShowId.from_base62(b62_id))
|
||||
def __init__(self, b62_id: str, api: Api, config: Config = Config()):
|
||||
show = 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))
|
||||
PlayableData(
|
||||
PlayableType.EPISODE,
|
||||
bytes_to_base62(episode.gid),
|
||||
config.podcast_library,
|
||||
config.output_podcast,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
def __init__(self, b62_id: str, api: Api, config: Config = Config()):
|
||||
playlist = api.get_playlist(PlaylistId(b62_id))
|
||||
for i in range(len(playlist.contents.items)):
|
||||
item = playlist.contents.items[i]
|
||||
split = item.uri.split(":")
|
||||
playable_type = split[1]
|
||||
playable_id = split[2]
|
||||
metadata = [
|
||||
MetadataEntry("playlist", playlist.attributes.name),
|
||||
MetadataEntry("playlist_length", playlist.length),
|
||||
MetadataEntry("playlist_owner", playlist.owner_username),
|
||||
MetadataEntry(
|
||||
"playlist_number",
|
||||
i + 1,
|
||||
str(i + 1).zfill(len(str(playlist.length + 1))),
|
||||
),
|
||||
]
|
||||
if playable_type == "track":
|
||||
self.playables.append(
|
||||
PlayableData(
|
||||
PlayableType.TRACK,
|
||||
split[2],
|
||||
playable_id,
|
||||
config.playlist_library,
|
||||
config.output_playlist_track,
|
||||
metadata,
|
||||
)
|
||||
)
|
||||
elif playable_type == "episode":
|
||||
self.playables.append(
|
||||
PlayableData(
|
||||
PlayableType.EPISODE,
|
||||
split[2],
|
||||
playable_id,
|
||||
config.playlist_library,
|
||||
config.output_playlist_episode,
|
||||
metadata,
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
@ -86,10 +111,21 @@ class Playlist(Collection):
|
|||
|
||||
|
||||
class Track(Collection):
|
||||
def __init__(self, session: Session, b62_id: str):
|
||||
self.playables.append(PlayableData(PlayableType.TRACK, b62_id))
|
||||
def __init__(self, b62_id: str, api: Api, config: Config = Config()):
|
||||
self.playables.append(
|
||||
PlayableData(
|
||||
PlayableType.TRACK, b62_id, config.album_library, config.output_album
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Episode(Collection):
|
||||
def __init__(self, session: Session, b62_id: str):
|
||||
self.playables.append(PlayableData(PlayableType.EPISODE, b62_id))
|
||||
def __init__(self, b62_id: str, api: Api, config: Config = Config()):
|
||||
self.playables.append(
|
||||
PlayableData(
|
||||
PlayableType.EPISODE,
|
||||
b62_id,
|
||||
config.podcast_library,
|
||||
config.output_podcast,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -7,18 +7,18 @@ from typing import Any
|
|||
|
||||
from zotify.utils import AudioFormat, ImageSize, Quality
|
||||
|
||||
ALBUM_LIBRARY = "album_library"
|
||||
ALL_ARTISTS = "all_artists"
|
||||
ARTWORK_SIZE = "artwork_size"
|
||||
AUDIO_FORMAT = "audio_format"
|
||||
CREATE_PLAYLIST_FILE = "create_playlist_file"
|
||||
CREDENTIALS = "credentials"
|
||||
CREDENTIALS_PATH = "credentials_path"
|
||||
DOWNLOAD_QUALITY = "download_quality"
|
||||
FFMPEG_ARGS = "ffmpeg_args"
|
||||
FFMPEG_PATH = "ffmpeg_path"
|
||||
LANGUAGE = "language"
|
||||
LYRICS_FILE = "lyrics_file"
|
||||
LYRICS_ONLY = "lyrics_only"
|
||||
MUSIC_LIBRARY = "music_library"
|
||||
OUTPUT = "output"
|
||||
OUTPUT_ALBUM = "output_album"
|
||||
OUTPUT_PLAYLIST_TRACK = "output_playlist_track"
|
||||
|
@ -49,7 +49,7 @@ SYSTEM_PATHS = {
|
|||
}
|
||||
|
||||
LIBRARY_PATHS = {
|
||||
"music": Path.home().joinpath("Music/Zotify Music"),
|
||||
"album": Path.home().joinpath("Music/Zotify Albums"),
|
||||
"podcast": Path.home().joinpath("Music/Zotify Podcasts"),
|
||||
"playlist": Path.home().joinpath("Music/Zotify Playlists"),
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ OUTPUT_PATHS = {
|
|||
}
|
||||
|
||||
CONFIG_VALUES = {
|
||||
CREDENTIALS: {
|
||||
CREDENTIALS_PATH: {
|
||||
"default": CONFIG_PATHS["creds"],
|
||||
"type": Path,
|
||||
"args": ["--credentials"],
|
||||
|
@ -80,11 +80,11 @@ CONFIG_VALUES = {
|
|||
"args": ["--archive"],
|
||||
"help": "Path to track archive file",
|
||||
},
|
||||
MUSIC_LIBRARY: {
|
||||
"default": LIBRARY_PATHS["music"],
|
||||
ALBUM_LIBRARY: {
|
||||
"default": LIBRARY_PATHS["album"],
|
||||
"type": Path,
|
||||
"args": ["--music-library"],
|
||||
"help": "Path to root of music library",
|
||||
"args": ["--album-library"],
|
||||
"help": "Path to root of album library",
|
||||
},
|
||||
PODCAST_LIBRARY: {
|
||||
"default": LIBRARY_PATHS["podcast"],
|
||||
|
@ -138,8 +138,8 @@ CONFIG_VALUES = {
|
|||
},
|
||||
AUDIO_FORMAT: {
|
||||
"default": "vorbis",
|
||||
"type": AudioFormat,
|
||||
"choices": [n.value.name for n in AudioFormat],
|
||||
"type": AudioFormat.from_string,
|
||||
"choices": list(AudioFormat),
|
||||
"args": ["--audio-format"],
|
||||
"help": "Audio format of final track output",
|
||||
},
|
||||
|
@ -256,13 +256,13 @@ CONFIG_VALUES = {
|
|||
|
||||
class Config:
|
||||
__config_file: Path | None
|
||||
album_library: Path
|
||||
artwork_size: ImageSize
|
||||
audio_format: AudioFormat
|
||||
credentials: Path
|
||||
credentials_path: Path
|
||||
download_quality: Quality
|
||||
ffmpeg_args: str
|
||||
ffmpeg_path: str
|
||||
music_library: Path
|
||||
language: str
|
||||
lyrics_file: bool
|
||||
output_album: str
|
||||
|
@ -276,9 +276,9 @@ class Config:
|
|||
save_metadata: bool
|
||||
transcode_bitrate: int
|
||||
|
||||
def __init__(self, args: Namespace = Namespace()):
|
||||
def __init__(self, args: Namespace | None = None):
|
||||
jsonvalues = {}
|
||||
if args.config:
|
||||
if args is not None and args.config:
|
||||
self.__config_file = Path(args.config)
|
||||
# Valid config file found
|
||||
if self.__config_file.exists():
|
||||
|
@ -300,7 +300,7 @@ class Config:
|
|||
|
||||
for key in CONFIG_VALUES:
|
||||
# Override config with commandline arguments
|
||||
if key in vars(args) and vars(args)[key] is not None:
|
||||
if args is not None and key in vars(args) and vars(args)[key] is not None:
|
||||
setattr(self, key, self.__parse_arg_value(key, vars(args)[key]))
|
||||
# If no command option specified use config
|
||||
elif key in jsonvalues:
|
||||
|
@ -314,14 +314,13 @@ class Config:
|
|||
)
|
||||
|
||||
# "library" arg overrides all *_library options
|
||||
if args.library:
|
||||
print("args.library")
|
||||
self.music_library = Path(args.library).expanduser().resolve()
|
||||
if args is not None and args.library:
|
||||
self.album_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:
|
||||
if args is not None and args.output:
|
||||
self.output_album = args.output
|
||||
self.output_podcast = args.output
|
||||
self.output_playlist_track = args.output
|
||||
|
@ -334,8 +333,8 @@ class Config:
|
|||
return value
|
||||
elif config_type == Path:
|
||||
return Path(value).expanduser().resolve()
|
||||
elif config_type == AudioFormat:
|
||||
return AudioFormat[value.upper()]
|
||||
elif config_type == AudioFormat.from_string:
|
||||
return AudioFormat.from_string(value)
|
||||
elif config_type == ImageSize.from_string:
|
||||
return ImageSize.from_string(value)
|
||||
elif config_type == Quality.from_string:
|
||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
from itertools import cycle
|
||||
from shutil import get_terminal_size
|
||||
from sys import platform
|
||||
from sys import platform as PLATFORM
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
|
@ -22,7 +22,7 @@ class Loader:
|
|||
pass
|
||||
"""
|
||||
|
||||
def __init__(self, desc="Loading...", end="", timeout=0.1, mode="std3") -> None:
|
||||
def __init__(self, desc: str = "Loading...", end: str = "", timeout: float = 0.1):
|
||||
"""
|
||||
A loader-like context manager
|
||||
Args:
|
||||
|
@ -35,7 +35,8 @@ class Loader:
|
|||
self.timeout = timeout
|
||||
|
||||
self.__thread = Thread(target=self.__animate, daemon=True)
|
||||
if platform == "win32":
|
||||
# Cool loader looks awful in cmd
|
||||
if PLATFORM == "win32":
|
||||
self.steps = ["/", "-", "\\", "|"]
|
||||
else:
|
||||
self.steps = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]
|
||||
|
|
|
@ -22,7 +22,7 @@ class LogChannel(Enum):
|
|||
|
||||
|
||||
class Logger:
|
||||
__config: Config
|
||||
__config: Config = Config()
|
||||
|
||||
@classmethod
|
||||
def __init__(cls, config: Config):
|
||||
|
@ -50,9 +50,9 @@ class Logger:
|
|||
total=None,
|
||||
leave=False,
|
||||
position=0,
|
||||
unit="it",
|
||||
unit_scale=False,
|
||||
unit_divisor=1000,
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
) -> tqdm:
|
||||
"""
|
||||
Prints progress bar
|
||||
|
|
|
@ -7,14 +7,13 @@ from librespot.metadata import AlbumId
|
|||
from librespot.structure import GeneralAudioStream
|
||||
from librespot.util import bytes_to_hex
|
||||
from requests import get
|
||||
from tqdm import tqdm
|
||||
|
||||
from zotify.file import LocalFile
|
||||
from zotify.logger import Logger
|
||||
from zotify.utils import (
|
||||
AudioFormat,
|
||||
ImageSize,
|
||||
MetadataEntry,
|
||||
PlayableType,
|
||||
bytes_to_base62,
|
||||
fix_filename,
|
||||
)
|
||||
|
@ -40,13 +39,15 @@ class Lyrics:
|
|||
f"[{ts_minutes}:{ts_seconds}.{ts_millis}]{line.words}\n"
|
||||
)
|
||||
|
||||
def save(self, path: Path, prefer_synced: bool = True) -> None:
|
||||
def save(self, path: Path | str, prefer_synced: bool = True) -> None:
|
||||
"""
|
||||
Saves lyrics to file
|
||||
Args:
|
||||
location: path to target lyrics file
|
||||
prefer_synced: Use line synced lyrics if available
|
||||
"""
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path).expanduser()
|
||||
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)
|
||||
|
@ -60,10 +61,12 @@ class Playable:
|
|||
input_stream: GeneralAudioStream
|
||||
metadata: list[MetadataEntry]
|
||||
name: str
|
||||
type: PlayableType
|
||||
|
||||
def create_output(
|
||||
self, library: Path = Path("./"), output: str = "{title}", replace: bool = False
|
||||
self,
|
||||
library: Path | str = Path("./"),
|
||||
output: str = "{title}",
|
||||
replace: bool = False,
|
||||
) -> Path:
|
||||
"""
|
||||
Creates save directory for the output file
|
||||
|
@ -74,6 +77,8 @@ class Playable:
|
|||
Returns:
|
||||
File path for the track
|
||||
"""
|
||||
if not isinstance(library, Path):
|
||||
library = Path(library)
|
||||
for meta in self.metadata:
|
||||
if meta.string is not None:
|
||||
output = output.replace(
|
||||
|
@ -87,26 +92,20 @@ class Playable:
|
|||
return file_path
|
||||
|
||||
def write_audio_stream(
|
||||
self,
|
||||
output: Path,
|
||||
self, output: Path | str, p_bar: tqdm = tqdm(disable=True)
|
||||
) -> LocalFile:
|
||||
"""
|
||||
Writes audio stream to file
|
||||
Args:
|
||||
output: File path of saved audio stream
|
||||
p_bar: tqdm progress bar
|
||||
Returns:
|
||||
LocalFile object
|
||||
"""
|
||||
if not isinstance(output, Path):
|
||||
output = Path(output).expanduser()
|
||||
file = f"{output}.ogg"
|
||||
with open(file, "wb") as f, Logger.progress(
|
||||
desc=self.name,
|
||||
total=self.input_stream.size,
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
position=0,
|
||||
leave=False,
|
||||
) as p_bar:
|
||||
with open(file, "wb") as f, p_bar as p_bar:
|
||||
chunk = None
|
||||
while chunk != b"":
|
||||
chunk = self.input_stream.stream().read(1024)
|
||||
|
@ -127,6 +126,8 @@ class Playable:
|
|||
|
||||
|
||||
class Track(PlayableContentFeeder.LoadedStream, Playable):
|
||||
__lyrics: Lyrics
|
||||
|
||||
def __init__(self, track: PlayableContentFeeder.LoadedStream, api):
|
||||
super(Track, self).__init__(
|
||||
track.track,
|
||||
|
@ -135,10 +136,8 @@ 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:
|
||||
|
@ -154,7 +153,8 @@ class Track(PlayableContentFeeder.LoadedStream, Playable):
|
|||
)
|
||||
return [
|
||||
MetadataEntry("album", self.album.name),
|
||||
MetadataEntry("album_artist", [a.name for a in self.album.artist]),
|
||||
MetadataEntry("album_artist", self.album.artist[0].name),
|
||||
MetadataEntry("album_artists", [a.name for a in self.album.artist]),
|
||||
MetadataEntry("artist", self.artist[0].name),
|
||||
MetadataEntry("artists", [a.name for a in self.artist]),
|
||||
MetadataEntry("date", f"{date.year}-{date.month}-{date.day}"),
|
||||
|
@ -180,7 +180,7 @@ class Track(PlayableContentFeeder.LoadedStream, Playable):
|
|||
),
|
||||
]
|
||||
|
||||
def lyrics(self) -> Lyrics:
|
||||
def get_lyrics(self) -> Lyrics:
|
||||
"""Returns track lyrics if available"""
|
||||
if not self.track.has_lyrics:
|
||||
raise FileNotFoundError(
|
||||
|
@ -208,7 +208,6 @@ 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:
|
||||
|
@ -228,29 +227,26 @@ class Episode(PlayableContentFeeder.LoadedStream, Playable):
|
|||
MetadataEntry("title", self.name),
|
||||
]
|
||||
|
||||
def write_audio_stream(self, output: Path) -> LocalFile:
|
||||
def write_audio_stream(
|
||||
self, output: Path | str, p_bar: tqdm = tqdm(disable=True)
|
||||
) -> LocalFile:
|
||||
"""
|
||||
Writes audio stream to file.
|
||||
Uses external source if available for faster download.
|
||||
Args:
|
||||
output: File path of saved audio stream
|
||||
p_bar: tqdm progress bar
|
||||
Returns:
|
||||
LocalFile object
|
||||
"""
|
||||
if not isinstance(output, Path):
|
||||
output = Path(output).expanduser()
|
||||
if not bool(self.external_url):
|
||||
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, Logger.progress(
|
||||
desc=self.name,
|
||||
total=self.input_stream.size,
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
position=0,
|
||||
leave=False,
|
||||
) as p_bar:
|
||||
) as f, p_bar as p_bar:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
p_bar.update(f.write(chunk))
|
||||
return LocalFile(Path(file))
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from argparse import Action, ArgumentError, HelpFormatter
|
||||
from argparse import Action, ArgumentError
|
||||
from enum import Enum, IntEnum
|
||||
from pathlib import Path
|
||||
from re import IGNORECASE, sub
|
||||
from sys import exit
|
||||
from sys import platform as PLATFORM
|
||||
from sys import stderr
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from librespot.audio.decoders import AudioQuality
|
||||
|
@ -25,7 +23,20 @@ class AudioFormat(Enum):
|
|||
OPUS = AudioCodec("opus", "ogg")
|
||||
VORBIS = AudioCodec("vorbis", "ogg")
|
||||
WAV = AudioCodec("wav", "wav")
|
||||
WV = AudioCodec("wavpack", "wv")
|
||||
WAVPACK = AudioCodec("wavpack", "wv")
|
||||
|
||||
def __str__(self):
|
||||
return self.name.lower()
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def from_string(s):
|
||||
try:
|
||||
return AudioFormat[s.upper()]
|
||||
except Exception:
|
||||
return s
|
||||
|
||||
|
||||
class Quality(Enum):
|
||||
|
@ -94,15 +105,6 @@ class MetadataEntry:
|
|||
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"
|
||||
|
@ -111,14 +113,9 @@ class PlayableType(Enum):
|
|||
class PlayableData(NamedTuple):
|
||||
type: PlayableType
|
||||
id: str
|
||||
|
||||
|
||||
class SimpleHelpFormatter(HelpFormatter):
|
||||
def _format_usage(self, usage, actions, groups, prefix):
|
||||
if usage is not None:
|
||||
super()._format_usage(usage, actions, groups, prefix)
|
||||
stderr.write('zotify: error: unrecognized arguments - try "zotify -h"\n')
|
||||
exit(2)
|
||||
library: Path
|
||||
output_template: str
|
||||
metadata: list[MetadataEntry] = []
|
||||
|
||||
|
||||
class OptionalOrFalse(Action):
|
||||
|
@ -171,24 +168,22 @@ class OptionalOrFalse(Action):
|
|||
)
|
||||
|
||||
|
||||
def fix_filename(filename: str, substitute: str = "_", platform: str = PLATFORM) -> str:
|
||||
def fix_filename(
|
||||
filename: str,
|
||||
substitute: str = "_",
|
||||
) -> str:
|
||||
"""
|
||||
Replace invalid characters on Linux/Windows/MacOS with underscores.
|
||||
Replace invalid characters. Trailing spaces & periods are ignored.
|
||||
Original list from https://stackoverflow.com/a/31976060/819417
|
||||
Trailing spaces & periods are ignored on Windows.
|
||||
Args:
|
||||
filename: The name of the file to repair
|
||||
platform: Host operating system
|
||||
substitute: Replacement character for disallowed characters
|
||||
Returns:
|
||||
Filename with replaced characters
|
||||
"""
|
||||
if platform == "linux":
|
||||
regex = r"[/\0]|^(?![^.])|[\s]$"
|
||||
elif platform == "darwin":
|
||||
regex = r"[/\0:]|^(?![^.])|[\s]$"
|
||||
else:
|
||||
regex = r"[/\\:|<>\"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$"
|
||||
regex = (
|
||||
r"[/\\:|<>\"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$"
|
||||
)
|
||||
return sub(regex, substitute, str(filename), flags=IGNORECASE)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue