various changes

This commit is contained in:
Zotify 2024-08-01 23:44:52 +12:00
parent 360e342bc2
commit b361976504
17 changed files with 573 additions and 353 deletions

View File

@ -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"
},
}

View File

@ -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}`

View File

@ -13,6 +13,11 @@ requests = "*"
tqdm = "*"
[dev-packages]
black = "*"
flake8 = "*"
mypy = "*"
types-protobuf = "*"
types-requests = "*"
[requires]
python_version = "3.11"

471
Pipfile.lock generated
View File

@ -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"
}
}
}

View File

@ -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.

View File

@ -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

View File

@ -1,7 +1,6 @@
black
flake8
mypy
pre-commit
types-protobuf
types-requests
wheel

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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,

View File

@ -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,
)
)

View File

@ -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:

View File

@ -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 = ["", "", "", "", "", "", "", ""]

View File

@ -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

View File

@ -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))

View 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)