diff --git a/.mypy.ini b/.mypy.ini index 1dc19fa..ce71e24 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -22,6 +22,9 @@ ignore_missing_imports = True [mypy-tomlkit.*] ignore_missing_imports = True +[mypy-Crypto.*] +ignore_missing_imports = True + [mypy-Cryptodome.*] ignore_missing_imports = True @@ -30,3 +33,9 @@ ignore_missing_imports = True [mypy-PIL.*] ignore_missing_imports = True + +[mypy-cleo.*] +ignore_missing_imports = True + +[mypy-deezer.*] +ignore_missing_imports = True diff --git a/poetry.lock b/poetry.lock index cb248a2..83411ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "autodoc" version = "0.5.0" @@ -44,6 +52,28 @@ soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "black" +version = "21.7b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.8.1,<1" +regex = ">=2020.1.8" +tomli = ">=0.2.6,<2.0.0" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] +python2 = ["typed-ast (>=1.4.2)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2021.5.30" @@ -63,6 +93,18 @@ python-versions = ">=3.5.0" [package.extras] unicode_backport = ["unicodedata2"] +[[package]] +name = "cleo" +version = "1.0.0a3" +description = "Cleo allows you to create beautiful and testable command-line interfaces." +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +crashtest = ">=0.3.1,<0.4.0" +pylev = ">=1.3.0,<2.0.0" + [[package]] name = "click" version = "8.0.1" @@ -82,6 +124,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "crashtest" +version = "0.3.1" +description = "Manage Python errors with ease" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + [[package]] name = "decorator" version = "5.0.9" @@ -125,6 +175,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "isort" +version = "5.9.3" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + [[package]] name = "jinja2" version = "3.0.1" @@ -155,6 +219,14 @@ category = "main" optional = false python-versions = ">=3.5, <4" +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "packaging" version = "21.0" @@ -166,6 +238,14 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "pathvalidate" version = "2.4.1" @@ -212,6 +292,14 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "pylev" +version = "1.4.0" +description = "A pure Python Levenshtein implementation that's not freaking GPL'd." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pyparsing" version = "2.4.7" @@ -228,6 +316,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "regex" +version = "2021.7.6" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "requests" version = "2.26.0" @@ -380,6 +476,14 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "tomli" +version = "1.1.0" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "tomlkit" version = "0.7.2" @@ -486,13 +590,17 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "8128cd9440b4931b23509f72a3952e1d4115a3783ded70c3b458de921ed30c56" +content-hash = "3e7ca36060a8049300d1408d6b7595c1fdf27cd5b4ed9cfab68af94d083d7935" [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] autodoc = [ {file = "autodoc-0.5.0.tar.gz", hash = "sha256:c4387c5a0f1c09b055bb2e384542ee1e016542f313b2a33d904ca77f0460ded3"}, ] @@ -505,6 +613,10 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, ] +black = [ + {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, + {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, +] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, @@ -513,6 +625,10 @@ charset-normalizer = [ {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"}, {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"}, ] +cleo = [ + {file = "cleo-1.0.0a3-py3-none-any.whl", hash = "sha256:46b2f970d06caa311d1e12a1013b0ce2a8149502669ac82cbedafb9e0bfdbccd"}, + {file = "cleo-1.0.0a3.tar.gz", hash = "sha256:9c1c8dd06635c936f45e4649aa2f7581517b4d52c7a9414d1b42586e63c2fe5d"}, +] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, @@ -521,6 +637,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +crashtest = [ + {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"}, + {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"}, +] decorator = [ {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, @@ -541,6 +661,10 @@ imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] +isort = [ + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, +] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, @@ -585,10 +709,18 @@ mutagen = [ {file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"}, {file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"}, ] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] pathvalidate = [ {file = "pathvalidate-2.4.1-py3-none-any.whl", hash = "sha256:f5dde7efeeb4262784c5e1331e02752d07c1ec3ee5ea42683fe211155652b808"}, {file = "pathvalidate-2.4.1.tar.gz", hash = "sha256:3c9bd94c7ec23e9cfb211ffbe356ae75f979d6c099a2c745ee9490f524f32468"}, @@ -674,6 +806,10 @@ pygments = [ {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, ] +pylev = [ + {file = "pylev-1.4.0-py2.py3-none-any.whl", hash = "sha256:7b2e2aa7b00e05bb3f7650eb506fc89f474f70493271a35c242d9a92188ad3dd"}, + {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"}, +] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -682,6 +818,49 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +regex = [ + {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, + {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, + {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, + {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, + {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, + {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, + {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, + {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, + {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, + {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, + {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, + {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, + {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, +] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, @@ -730,6 +909,10 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +tomli = [ + {file = "tomli-1.1.0-py3-none-any.whl", hash = "sha256:f4a182048010e89cbec0ae4686b21f550a7f2903f665e34a6de58ec15424f919"}, + {file = "tomli-1.1.0.tar.gz", hash = "sha256:33d7984738f8bb699c9b0a816eb646a8178a69eaa792d258486776a5d21b8ca5"}, +] tomlkit = [ {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"}, diff --git a/pyproject.toml b/pyproject.toml index 4fc591a..bcfc554 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ ] [tool.poetry.scripts] -rip = "rip.cli:main" +rip = "rip.cli_cleo:main" [tool.poetry.dependencies] python = "^3.8" @@ -35,6 +35,8 @@ windows-curses = {version = "^2.2.0", platform = 'win32 or cygwin'} Pillow = "^8.3.0" deezer-py = "^1.0.4" pycryptodomex = "^3.10.1" +cleo = {version = "1.0.0a3", allow-prereleases = true} +appdirs = "^1.4.4" [tool.poetry.urls] "Bug Reports" = "https://github.com/nathom/streamrip/issues" @@ -44,6 +46,8 @@ Sphinx = "^4.1.1" autodoc = "^0.5.0" types-click = "^7.1.2" types-Pillow = "^8.3.1" +black = "^21.7b0" +isort = "^5.9.3" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/rip/cli.py b/rip/cli.py index 1f37acb..c5c4c1c 100644 --- a/rip/cli.py +++ b/rip/cli.py @@ -9,15 +9,31 @@ logging.basicConfig(level="WARNING") logger = logging.getLogger("streamrip") -@click.group(invoke_without_command=True) +class SkipArg(click.Group): + def parse_args(self, ctx, args): + if len(args) == 0: + click.echo(self.get_help(ctx)) + exit() + + if args[0] in self.commands: + print('command found') + args.insert(0, "") + # if args[0] in self.commands: + # if len(args) == 1 or args[1] not in self.commands: + # # This condition needs updating for multiple positional arguments + # args.insert(0, "") + super(SkipArg, self).parse_args(ctx, args) + + +# @click.option( +# "-u", +# "--urls", +# metavar="URLS", +# help="Url from Qobuz, Tidal, SoundCloud, or Deezer", +# multiple=True, +# ) +@click.group(cls=SkipArg, invoke_without_command=True) @click.option("-c", "--convert", metavar="CODEC", help="alac, mp3, flac, or ogg") -@click.option( - "-u", - "--urls", - metavar="URLS", - help="Url from Qobuz, Tidal, SoundCloud, or Deezer", - multiple=True, -) @click.option( "-q", "--quality", @@ -27,6 +43,7 @@ logger = logging.getLogger("streamrip") @click.option("-t", "--text", metavar="PATH", help="Download urls from a text file.") @click.option("-nd", "--no-db", is_flag=True, help="Ignore the database.") @click.option("--debug", is_flag=True, help="Show debugging logs.") +@click.argument("URLS", nargs=1) @click.version_option(prog_name="rip", version=__version__) @click.pass_context def cli(ctx, **kwargs): @@ -54,9 +71,7 @@ def cli(ctx, **kwargs): from .constants import CONFIG_DIR from .core import RipCore - logging.basicConfig(level="WARNING") - logger = logging.getLogger("streamrip") - + print(kwargs) if not os.path.isdir(CONFIG_DIR): os.makedirs(CONFIG_DIR, exist_ok=True) @@ -68,7 +83,8 @@ def cli(ctx, **kwargs): logger.debug("Starting debug log") if ctx.invoked_subcommand is None and not ctx.params["urls"]: - echo(cli.get_help(ctx)) + print(dir(cli)) + click.echo(cli.get_help(ctx)) if ctx.invoked_subcommand not in { None, @@ -90,13 +106,13 @@ def cli(ctx, **kwargs): r = requests.get("https://pypi.org/pypi/streamrip/json").json() newest = r["info"]["version"] if __version__ != newest: - secho( + click.secho( "A new version of streamrip is available! " "Run `pip3 install streamrip --upgrade` to update.", fg="yellow", ) else: - secho("streamrip is up-to-date!", fg="green") + click.secho("streamrip is up-to-date!", fg="green") if kwargs["no_db"]: config.session["database"]["enabled"] = False @@ -108,7 +124,7 @@ def cli(ctx, **kwargs): if kwargs["quality"] is not None: quality = int(kwargs["quality"]) if quality not in range(5): - secho("Invalid quality", fg="red") + click.secho("Invalid quality", fg="red") return config.session["qobuz"]["quality"] = quality @@ -126,7 +142,7 @@ def cli(ctx, **kwargs): logger.debug(f"Handling {kwargs['text']}") core.handle_txt(kwargs["text"]) else: - secho(f"Text file {kwargs['text']} does not exist.") + click.secho(f"Text file {kwargs['text']} does not exist.") if ctx.invoked_subcommand is None: core.download() @@ -150,6 +166,7 @@ def filter_discography(ctx, **kwargs): For basic filtering, use the `--repeats` and `--features` filters. """ + raise Exception filters = kwargs.copy() filters.pop("urls") config.session["filters"] = filters @@ -206,7 +223,7 @@ def search(ctx, **kwargs): if core.interactive_search(query, kwargs["source"], kwargs["type"]): core.download() else: - secho("No items chosen, exiting.", fg="bright_red") + click.secho("No items chosen, exiting.", fg="bright_red") @cli.command() @@ -333,10 +350,10 @@ def config(ctx, **kwargs): config.update() if kwargs["path"]: - echo(CONFIG_PATH) + click.echo(CONFIG_PATH) if kwargs["open"]: - secho(f"Opening {CONFIG_PATH}", fg="green") + click.secho(f"Opening {CONFIG_PATH}", fg="green") click.launch(CONFIG_PATH) if kwargs["open_vim"]: @@ -347,41 +364,41 @@ def config(ctx, **kwargs): if kwargs["directory"]: config_dir = os.path.dirname(CONFIG_PATH) - secho(f"Opening {config_dir}", fg="green") + click.secho(f"Opening {config_dir}", fg="green") click.launch(config_dir) if kwargs["qobuz"]: - config.file["qobuz"]["email"] = input(style("Qobuz email: ", fg="blue")) + config.file["qobuz"]["email"] = input(click.style("Qobuz email: ", fg="blue")) - secho("Qobuz password (will not show on screen):", fg="blue") + click.secho("Qobuz password (will not show on screen):", fg="blue") config.file["qobuz"]["password"] = md5( getpass(prompt="").encode("utf-8") ).hexdigest() config.save() - secho("Qobuz credentials hashed and saved to config.", fg="green") + click.secho("Qobuz credentials hashed and saved to config.", fg="green") if kwargs["tidal"]: client = TidalClient() client.login() config.file["tidal"].update(client.get_tokens()) config.save() - secho("Credentials saved to config.", fg="green") + click.secho("Credentials saved to config.", fg="green") if kwargs["deezer"]: - secho( + click.secho( "If you're not sure how to find the ARL cookie, see the instructions at ", italic=True, nl=False, dim=True, ) - secho( + click.secho( "https://github.com/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie", underline=True, italic=True, fg="blue", ) - config.file["deezer"]["arl"] = input(style("ARL: ", fg="green")) + config.file["deezer"]["arl"] = input(click.style("ARL: ", fg="green")) config.save() @@ -481,7 +498,7 @@ def convert(ctx, **kwargs): elif os.path.isfile(kwargs["path"]): codec_map[codec](filename=kwargs["path"], **converter_args).convert() else: - secho(f"File {kwargs['path']} does not exist.", fg="red") + click.secho(f"File {kwargs['path']} does not exist.", fg="red") @cli.command() @@ -503,7 +520,7 @@ def repair(ctx, **kwargs): def none_chosen(): """Print message if nothing was chosen.""" - secho("No items chosen, exiting.", fg="bright_red") + click.secho("No items chosen, exiting.", fg="bright_red") def main(): diff --git a/rip/cli_cleo.py b/rip/cli_cleo.py new file mode 100644 index 0000000..0e15eda --- /dev/null +++ b/rip/cli_cleo.py @@ -0,0 +1,214 @@ +import logging +import os + +from cleo.application import Application as BaseApplication +from cleo.commands.command import Command + +from streamrip import __version__ + +from .config import Config +from .core import RipCore + +logging.basicConfig(level="WARNING") +logger = logging.getLogger("streamrip") + + +class DownloadCommand(Command): + """ + Download items from a url + + url + {--f|file=None : Path to a text file containing urls} + {urls?* : One or more Qobuz, Tidal, Deezer, or SoundCloud urls} + """ + + help = ( + '\nDownload "Dreams" by Fleetwood Mac:\n' + "$ rip url https://www.deezer.com/en/track/63480987\n\n" + "Batch download urls from a text file named urls.txt:\n" + "$ rip --file urls.txt\n" + ) + + def handle(self): + config = Config() + core = RipCore(config) + + urls = self.argument("urls") + path = self.option("file") + if path != "None": + if os.path.isfile(path): + core.handle_txt(path) + else: + self.line( + f"File {path} does not exist." + ) + return 1 + + if urls: + core.handle_urls(";".join(urls)) + + if len(core) > 0: + core.download() + elif not urls and path == "None": + self.line("Must pass arguments. See rip url -h.") + + return 0 + + +class SearchCommand(Command): + """ + Search for and download items in interactive mode. + + search + {query : The name to search for} + {--s|source=qobuz : Qobuz, Tidal, Soundcloud, Deezer, or Deezloader} + {--t|type=album : Album, Playlist, Track, or Artist} + """ + + def handle(self): + query = self.argument("query") + source, type = clean_options(self.option("source"), self.option("type")) + + config = Config() + core = RipCore(config) + + if core.interactive_search(query, source, type): + core.download() + else: + self.line("No items chosen, exiting.") + + +class DiscoverCommand(Command): + """ + Browse and download items in interactive mode. + + discover + {--s|scrape : Download all of the items in the list} + {--m|max-items=50 : The number of items to fetch} + {list=ideal-discography : The list to fetch} + """ + + help = ( + "\nAvailable options for list:\n\n" + " • most-streamed\n" + " • recent-releases\n" + " • best-sellers\n" + " • press-awards\n" + " • ideal-discography\n" + " • editor-picks\n" + " • most-featured\n" + " • qobuzissims\n" + " • new-releases\n" + " • new-releases-full\n" + " • harmonia-mundi\n" + " • universal-classic\n" + " • universal-jazz\n" + " • universal-jeunesse\n" + " • universal-chanson\n" + ) + + def handle(self): + from streamrip.constants import QOBUZ_FEATURED_KEYS + + chosen_list = self.argument("list") + scrape = self.option("scrape") + max_items = self.option("max-items") + + if chosen_list not in QOBUZ_FEATURED_KEYS: + self.line(f'Error: list "{chosen_list}" not available') + self.line(self.help) + return 1 + + config = Config() + core = RipCore(config) + + if scrape: + core.scrape(chosen_list, max_items) + core.download() + return 0 + + if core.interactive_search( + chosen_list, "qobuz", "featured", limit=int(max_items) + ): + core.download() + else: + self.line("No items chosen, exiting.") + + +class LastfmCommand(Command): + """ + Search for tracks from a list.fm playlist and download them. + + lastfm + {--s|source=qobuz : The source to search for items on} + {urls* : Last.fm playlist urls} + """ + + def handle(self): + source = self.option("source") + urls = self.argument("urls") + + config = Config() + core = RipCore(config) + config.session["lastfm"]["source"] = source + core.handle_lastfm_urls(";".join(urls)) + core.download() + + +class ConfigCommand(Command): + """ + Manage the configuration file + + {--o|open : Open the config file in the default application} + {--ov|open-vim : Open the config file in (neo)vim} + {--d|directory : Open the directory that the config file is located in} + {--p|path : Show the config file's path} + {--q|qobuz : Set the credentials for Qobuz} + {--t|tidal : Log into Tidal} + {--dz|deezer : Set the Deezer ARL} + {--reset : Reset the config file} + {--update : Reset the config file, keeping the credentials} + """ + + +class Application(BaseApplication): + def __init__(self): + super().__init__("rip", __version__) + + def _run(self, io): + if io.is_debug(): + logger.setLevel(logging.DEBUG) + + super()._run(io) + + # @property + # def _default_definition(self): + # default_globals = super()._default_definition + # default_globals.add_option(Option("convert", shortcut="c", flag=False)) + # return default_globals + + +# class ConvertCommand(Command): +# pass + + +# class RepairCommand(Command): +# pass + + +def clean_options(*opts): + return tuple(o.replace("=", "").strip() for o in opts) + + +def main(): + application = Application() + application.add(DownloadCommand()) + application.add(SearchCommand()) + application.add(DiscoverCommand()) + application.add(LastfmCommand()) + # application.add(ConfigCommand()) + application.run() + + +if __name__ == "__main__": + main() diff --git a/rip/config.py b/rip/config.py index 8ef27b6..fec572c 100644 --- a/rip/config.py +++ b/rip/config.py @@ -7,7 +7,7 @@ import shutil from pprint import pformat from typing import Any, Dict -import click +from click import style, secho import tomlkit from streamrip.exceptions import InvalidSourceError diff --git a/rip/constants.py b/rip/constants.py index f119132..75cabf1 100644 --- a/rip/constants.py +++ b/rip/constants.py @@ -4,7 +4,7 @@ import os import re from pathlib import Path -import click +from click import style, secho APPNAME = "streamrip" APP_DIR = click.get_app_dir(APPNAME) diff --git a/rip/core.py b/rip/core.py index 03d1125..35adee1 100644 --- a/rip/core.py +++ b/rip/core.py @@ -10,7 +10,7 @@ from hashlib import md5 from string import Formatter from typing import Dict, Generator, List, Optional, Tuple, Type, Union -import click +from click import style, secho import requests from tqdm import tqdm @@ -162,8 +162,8 @@ class RipCore(list): if not parsed and len(self) == 0: if "last.fm" in url: message = ( - f"For last.fm urls, use the {style('lastfm', fg='yellow')} " - f"command. See {style('rip lastfm --help', fg='yellow')}." + f"For last.fm urls, use the {click.style('lastfm', fg='yellow')} " + f"command. See {click.style('rip lastfm --help', fg='yellow')}." ) else: message = f"Cannot find urls in text: {url}" @@ -175,7 +175,7 @@ class RipCore(list): logger.info( f"ID {item_id} already downloaded, use --no-db to override." ) - secho( + click.secho( f"ID {item_id} already downloaded, use --no-db to override.", fg="magenta", ) @@ -248,12 +248,12 @@ class RipCore(list): max_items = float("inf") if self.failed_db.is_dummy: - secho( + click.secho( "Failed downloads database must be enabled in the config file " "to repair!", fg="red", ) - raise click.Abort + exit() for counter, (source, media_type, item_id) in enumerate(self.failed_db): if counter >= max_items: @@ -304,7 +304,7 @@ class RipCore(list): item.load_meta(**arguments) except NonStreamable: self.failed_db.add((item.client.source, item.type, item.id)) - secho(f"{item!s} is not available, skipping.", fg="red") + click.secho(f"{item!s} is not available, skipping.", fg="red") continue try: @@ -321,7 +321,7 @@ class RipCore(list): self.failed_db.add(failed_item_info) continue except ItemExists as e: - secho(f'"{e!s}" already exists. Skipping.', fg="yellow") + click.secho(f'"{e!s}" already exists. Skipping.', fg="yellow") continue if hasattr(item, "id"): @@ -366,13 +366,13 @@ class RipCore(list): creds = self.config.creds(client.source) if client.source == "deezer" and creds["arl"] == "": if self.config.session["deezer"]["deezloader_warnings"]: - secho( + click.secho( "Falling back to Deezloader (max 320kbps MP3). If you have a subscription, run ", nl=False, fg="yellow", ) - secho("rip config --deezer ", nl=False, bold=True) - secho("to download FLAC files.\n\n", fg="yellow") + click.secho("rip config --deezer ", nl=False, bold=True) + click.secho("to download FLAC files.\n\n", fg="yellow") raise DeezloaderFallback while True: @@ -380,7 +380,7 @@ class RipCore(list): client.login(**creds) break except AuthenticationError: - secho("Invalid credentials, try again.", fg="yellow") + click.secho("Invalid credentials, try again.", fg="yellow") self.prompt_creds(client.source) creds = self.config.creds(client.source) except MissingCredentials: @@ -419,7 +419,7 @@ class RipCore(list): interpreter_urls = QOBUZ_INTERPRETER_URL_REGEX.findall(url) if interpreter_urls: - secho( + click.secho( "Extracting IDs from Qobuz interpreter urls. Use urls " "that include the artist ID for faster preprocessing.", fg="yellow", @@ -432,7 +432,7 @@ class RipCore(list): dynamic_urls = DEEZER_DYNAMIC_LINK_REGEX.findall(url) if dynamic_urls: - secho( + click.secho( "Extracting IDs from Deezer dynamic link. Use urls " "of the form https://www.deezer.com/{country}/{type}/{id} for " "faster processing.", @@ -486,7 +486,7 @@ class RipCore(list): exit() except Exception as err: self._config_corrupted_message(err) - raise click.Abort + exit() def search_query(title, artist, playlist) -> bool: """Search for a query and add the first result to playlist. @@ -523,8 +523,10 @@ class RipCore(list): playlist.append(track) return True + from streamrip.utils import TQDM_BAR_FORMAT + for purl in lastfm_urls: - secho(f"Fetching playlist at {purl}", fg="blue") + click.secho(f"Fetching playlist at {purl}", fg="blue") title, queries = self.get_lastfm_playlist(purl) pl = Playlist(client=self.get_client(lastfm_source), name=title) @@ -541,8 +543,11 @@ class RipCore(list): # only for the progress bar for search_attempt in tqdm( concurrent.futures.as_completed(futures), + unit="Tracks", + dynamic_ncols=True, total=len(futures), - desc="Searching", + desc="Searching...", + bar_format=TQDM_BAR_FORMAT, ): if not search_attempt.result(): tracks_not_found += 1 @@ -550,7 +555,8 @@ class RipCore(list): pl.loaded = True if tracks_not_found > 0: - secho(f"{tracks_not_found} tracks not found.", fg="yellow") + click.secho(f"{tracks_not_found} tracks not found.", fg="yellow") + self.append(pl) def handle_txt(self, filepath: Union[str, os.PathLike]): @@ -602,7 +608,7 @@ class RipCore(list): media_type if media_type != "featured" else "album" ].from_api(item, client) - if i > limit: + if i >= limit - 1: return else: logger.debug("Not generator") @@ -616,7 +622,7 @@ class RipCore(list): for i, item in enumerate(items): logger.debug(item["title"]) yield MEDIA_CLASS[media_type].from_api(item, client) # type: ignore - if i > limit: + if i >= limit - 1: return def preview_media(self, media) -> str: @@ -795,11 +801,7 @@ class RipCore(list): for page in range(1, last_page + 1) ] - for future in tqdm( - concurrent.futures.as_completed(futures), - total=len(futures), - desc="Scraping playlist", - ): + for future in concurrent.futures.as_completed(futures): get_titles(future.result().text) return playlist_title, info @@ -815,9 +817,9 @@ class RipCore(list): :type source: str """ if source == "qobuz": - secho("Enter Qobuz email:", fg="green") + click.secho("Enter Qobuz email:", fg="green") self.config.file[source]["email"] = input() - secho( + click.secho( "Enter Qobuz password (will not show on screen):", fg="green", ) @@ -826,27 +828,27 @@ class RipCore(list): ).hexdigest() self.config.save() - secho( + click.secho( f'Credentials saved to config file at "{self.config._path}"', fg="green", ) elif source == "deezer": - secho( + click.secho( "If you're not sure how to find the ARL cookie, see the instructions at ", italic=True, nl=False, dim=True, ) - secho( + click.secho( "https://github.com/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie", underline=True, italic=True, fg="blue", ) - self.config.file["deezer"]["arl"] = input(style("ARL: ", fg="green")) + self.config.file["deezer"]["arl"] = input(click.style("ARL: ", fg="green")) self.config.save() - secho( + click.secho( f'Credentials saved to config file at "{self.config._path}"', fg="green", ) @@ -854,19 +856,19 @@ class RipCore(list): raise Exception def _config_updating_message(self): - secho( + click.secho( "Updating config file... Some settings may be lost. Please run the " "command again.", fg="magenta", ) def _config_corrupted_message(self, err: Exception): - secho( + click.secho( "There was a problem with your config file. This happens " "sometimes after updates. Run ", nl=False, fg="red", ) - secho("rip config --reset ", fg="yellow", nl=False) - secho("to reset it. You will need to log in again.", fg="red") - secho(str(err), fg="red") + click.secho("rip config --reset ", fg="yellow", nl=False) + click.secho("to reset it. You will need to log in again.", fg="red") + click.secho(str(err), fg="red") diff --git a/streamrip/clients.py b/streamrip/clients.py index 221e70b..bfe9b87 100644 --- a/streamrip/clients.py +++ b/streamrip/clients.py @@ -11,8 +11,8 @@ from abc import ABC, abstractmethod from pprint import pformat from typing import Any, Dict, Generator, Optional, Sequence, Tuple, Union -import click # type: ignore -import deezer # type: ignore +from click import style, secho +import deezer import requests from Cryptodome.Cipher import AES, Blowfish # type: ignore diff --git a/streamrip/exceptions.py b/streamrip/exceptions.py index fbf9b90..784d182 100644 --- a/streamrip/exceptions.py +++ b/streamrip/exceptions.py @@ -1,7 +1,7 @@ """Streamrip specific exceptions.""" from typing import List -import click +from click import style, secho class AuthenticationError(Exception): diff --git a/streamrip/media.py b/streamrip/media.py index 3a27828..1eaff82 100644 --- a/streamrip/media.py +++ b/streamrip/media.py @@ -15,12 +15,11 @@ import subprocess from tempfile import gettempdir from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, Union -import click +from click import style, secho from mutagen.flac import FLAC, Picture from mutagen.id3 import APIC, ID3, ID3NoHeaderError from mutagen.mp4 import MP4, MP4Cover from pathvalidate import sanitize_filename, sanitize_filepath -from tqdm import tqdm from . import converter from .clients import Client, DeezloaderClient @@ -50,7 +49,6 @@ from .utils import ( get_stats_from_quality, safe_get, tidal_cover_url, - tqdm_download, tqdm_stream, ) @@ -272,7 +270,7 @@ class Track(Media): :type progress_bar: bool """ if not self.part_of_tracklist: - secho(f"Downloading {self!s}\n", bold=True) + click.secho(f"Downloading {self!s}\n", bold=True) self._prepare_download( quality=quality, @@ -295,7 +293,6 @@ class Track(Media): if self.client.source == "qobuz": if not self.__validate_qobuz_dl_info(dl_info): - # secho("Track is not available for download", fg="red") raise NonStreamable("Track is not available for download") self.sampling_rate = dl_info.get("sampling_rate") @@ -314,7 +311,7 @@ class Track(Media): words[0] + " " + " ".join(map(str.lower, words[1:])) + "." ) - secho(f"Panic: {e} dl_info = {dl_info}", fg="red") + click.secho(f"Panic: {e} dl_info = {dl_info}", fg="red") raise NonStreamable _quick_download(download_url, self.path, desc=self._progress_desc) @@ -470,7 +467,6 @@ class Track(Media): """Download the cover art, if cover_url is given.""" self.cover_path = os.path.join(gettempdir(), f"cover{hash(self.cover_url)}.jpg") logger.debug(f"Downloading cover from {self.cover_url}") - # secho(f"\nDownloading cover art for {self!s}", fg="blue") if not os.path.exists(self.cover_path): _cover_download(self.cover_url, self.cover_path) @@ -670,7 +666,7 @@ class Track(Media): """ if not self.downloaded: logger.debug("Track not downloaded, skipping conversion") - secho("Track not downloaded, skipping conversion", fg="magenta") + click.secho("Track not downloaded, skipping conversion", fg="magenta") return CONV_CLASS = { @@ -687,15 +683,15 @@ class Track(Media): try: self.container = codec.upper() except AttributeError: - secho("Error: No audio codec chosen to convert to.", fg="red") - raise click.Abort + click.secho("Error: No audio codec chosen to convert to.", fg="red") + exit() if not hasattr(self, "final_path"): self.format_final_path() if not os.path.isfile(self.path): logger.info("File %s does not exist. Skipping conversion.", self.path) - secho(f"{self!s} does not exist. Skipping conversion.", fg="red") + click.secho(f"{self!s} does not exist. Skipping conversion.", fg="red") return assert ( @@ -707,7 +703,6 @@ class Track(Media): sampling_rate=kwargs.get("sampling_rate"), remove_source=kwargs.get("remove_source", True), ) - # secho(f"Converting {self!s}", fg="blue") engine.convert() self.path = engine.final_fn self.final_path = self.final_path.replace( @@ -814,7 +809,7 @@ class Video(Media): :param kwargs: """ - secho( + click.secho( f"Downloading {self.title} (Video). This may take a while.", fg="blue", ) @@ -949,7 +944,7 @@ class YoutubeVideo(Media): :type youtube_video_downloads_folder: str :param kwargs: """ - secho(f"Downloading url {self.id}", fg="blue") + click.secho(f"Downloading url {self.id}", fg="blue") filename_formatter = "%(track_number)s.%(track)s.%(container)s" filename = os.path.join(parent_folder, filename_formatter) @@ -969,7 +964,7 @@ class YoutubeVideo(Media): ) if download_youtube_videos: - secho("Downloading video stream", fg="blue") + click.secho("Downloading video stream", fg="blue") pv = subprocess.Popen( [ "youtube-dl", @@ -1107,18 +1102,18 @@ class Tracklist(list): except (KeyboardInterrupt, SystemExit): executor.shutdown() click.echo("Aborted! May take some time to shutdown.") - raise click.Abort + exit() else: for item in self: if self.client.source != "soundcloud": # soundcloud only gets metadata after `target` is called # message will be printed in `target` - secho(f'\nDownloading "{item!s}"', bold=True, fg="green") + click.secho(f'\nDownloading "{item!s}"', bold=True, fg="green") try: target(item, **kwargs) except ItemExists: - secho(f"{item!s} exists. Skipping.", fg="yellow") + click.secho(f"{item!s} exists. Skipping.", fg="yellow") except NonStreamable as e: e.print(item) failed_downloads.append((item.client.source, item.type, item.id)) @@ -1264,7 +1259,7 @@ class Tracklist(list): :rtype: str """ - secho( + click.secho( f"\n\nDownloading {self.title} ({self.__class__.__name__})\n", fg="magenta", bold=True, @@ -1422,7 +1417,7 @@ class Album(Tracklist, Media): self.download_message() # choose optimal cover size and download it - secho("Downloading cover art", bold=True) + click.secho("Downloading cover art", bold=True) cover_path = os.path.join(gettempdir(), f"cover_{hash(self)}.jpg") embed_cover_size = kwargs.get("embed_cover_size", "large") @@ -1446,7 +1441,7 @@ class Album(Tracklist, Media): cover_size = os.path.getsize(cover_path) if cover_size > FLAC_MAX_BLOCKSIZE: # 16.77 MB - secho( + click.secho( "Downgrading embedded cover size, too large ({cover_size}).", fg="bright_yellow", ) @@ -1473,7 +1468,7 @@ class Album(Tracklist, Media): and kwargs.get("download_booklets", True) and not any(f.endswith(".pdf") for f in os.listdir(self.folder)) ): - secho("\nDownloading booklets", bold=True) + click.secho("\nDownloading booklets", bold=True) for item in self.booklets: Booklet(item).download(parent_folder=self.folder) @@ -1783,9 +1778,9 @@ class Playlist(Tracklist, Media): kwargs["parent_folder"] = self.folder if self.client.source == "soundcloud": item.load_meta() - secho(f"Downloading {item!s}", fg="blue") + click.secho(f"Downloading {item!s}", fg="blue") - if playlist_to_album := kwargs.get("set_playlist_to_album", False): + if kwargs.get("set_playlist_to_album", False): item.meta.album = self.name item.meta.albumartist = self.creator diff --git a/streamrip/utils.py b/streamrip/utils.py index 912b39a..419c4be 100644 --- a/streamrip/utils.py +++ b/streamrip/utils.py @@ -6,14 +6,12 @@ import base64 import functools import hashlib import logging -import os import re from collections import OrderedDict -from json import JSONDecodeError from string import Formatter from typing import Dict, Hashable, Iterator, Optional, Tuple, Union -import click +from click import secho import requests from Cryptodome.Cipher import Blowfish from pathvalidate import sanitize_filename @@ -240,50 +238,6 @@ def get_stats_from_quality( raise InvalidQuality(quality_id) -def tqdm_download(url: str, filepath: str, params: dict = None, desc: str = None): - """Download a file with a progress bar. - - :param url: url to direct download - :param filepath: file to write - :type url: str - :type filepath: str - """ - logger.debug(f"Downloading {url} to {filepath} with params {params}") - if params is None: - params = {} - - session = gen_threadsafe_session() - r = session.get(url, allow_redirects=True, stream=True, params=params) - total = int(r.headers.get("content-length", 0)) - logger.debug("File size = %s", total) - if total < 1000 and not url.endswith("jpg") and not url.endswith("png"): - logger.debug("Response text: %s", r.text) - try: - raise NonStreamable(r.json()["error"]) - except JSONDecodeError: - raise NonStreamable("Resource not found.") - - try: - with open(filepath, "wb") as file, tqdm( - total=total, - unit="iB", - unit_scale=True, - unit_divisor=1024, - desc=desc, - dynamic_ncols=True, - # leave=False, - ) as bar: - for data in r.iter_content(chunk_size=1024): - size = file.write(data) - bar.update(size) - except Exception: - try: - os.remove(filepath) - except OSError: - pass - raise - - def clean_format(formatter: str, format_info): """Format track or folder names sanitizing every formatter key. @@ -345,14 +299,14 @@ def decrypt_mqa_file(in_path, out_path, encryption_key): from Crypto.Cipher import AES from Crypto.Util import Counter except (ImportError, ModuleNotFoundError): - secho( + click.secho( "To download this item in MQA, you need to run ", fg="yellow", nl=False, ) - secho("pip3 install pycryptodome --upgrade", fg="blue", nl=False) - secho(".") - raise click.Abort + click.secho("pip3 install pycryptodome --upgrade", fg="blue", nl=False) + click.secho(".") + exit() # Do not change this master_key = "UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754=" @@ -431,7 +385,7 @@ def decho(message, fg=None): :param fg: ANSI color with which to display the message on the screen """ - secho(message, fg=fg) + click.secho(message, fg=fg) logger.debug(message) diff --git a/tests/tests.py b/tests/tests.py index 279f099..76dd22f 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -2,7 +2,7 @@ import os import shutil import subprocess -import click +from click import style, secho, echo test_urls = { "qobuz": "https://www.qobuz.com/us-en/album/blackest-blue-morcheeba/h4nngz0wgqesc",