Merge branch 'dev' into 429

This commit is contained in:
Nathan Thomas 2023-12-27 14:18:25 -08:00
commit 49e63f753f
17 changed files with 199 additions and 517 deletions

486
poetry.lock generated
View File

@ -1,10 +1,9 @@
# This file is automatically @generated by Poetry and should not be changed by hand. # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]] [[package]]
name = "aiodns" name = "aiodns"
version = "3.1.1" version = "3.1.1"
description = "Simple DNS resolver for asyncio" description = "Simple DNS resolver for asyncio"
category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -19,7 +18,6 @@ pycares = ">=4.0.0"
name = "aiofiles" name = "aiofiles"
version = "0.7.0" version = "0.7.0"
description = "File support for asyncio." description = "File support for asyncio."
category = "main"
optional = false optional = false
python-versions = ">=3.6,<4.0" python-versions = ">=3.6,<4.0"
files = [ files = [
@ -31,7 +29,6 @@ files = [
name = "aiohttp" name = "aiohttp"
version = "3.9.1" version = "3.9.1"
description = "Async http client/server framework (asyncio)" description = "Async http client/server framework (asyncio)"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -128,7 +125,6 @@ speedups = ["Brotli", "aiodns", "brotlicffi"]
name = "aiolimiter" name = "aiolimiter"
version = "1.1.0" version = "1.1.0"
description = "asyncio rate limiter, a leaky bucket implementation" description = "asyncio rate limiter, a leaky bucket implementation"
category = "main"
optional = false optional = false
python-versions = ">=3.7,<4.0" python-versions = ">=3.7,<4.0"
files = [ files = [
@ -140,7 +136,6 @@ files = [
name = "aiosignal" name = "aiosignal"
version = "1.3.1" version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks" description = "aiosignal: a list of registered asynchronous callbacks"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -151,23 +146,10 @@ files = [
[package.dependencies] [package.dependencies]
frozenlist = ">=1.1.0" frozenlist = ">=1.1.0"
[[package]]
name = "alabaster"
version = "0.7.13"
description = "A configurable sidebar-enabled Sphinx theme"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"},
{file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"},
]
[[package]] [[package]]
name = "appdirs" name = "appdirs"
version = "1.4.4" version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -179,7 +161,6 @@ files = [
name = "async-timeout" name = "async-timeout"
version = "4.0.3" version = "4.0.3"
description = "Timeout context manager for asyncio programs" description = "Timeout context manager for asyncio programs"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -191,7 +172,6 @@ files = [
name = "attrs" name = "attrs"
version = "23.1.0" version = "23.1.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -206,63 +186,10 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
tests = ["attrs[tests-no-zope]", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
[[package]]
name = "autodoc"
version = "0.5.0"
description = "Autodoc Python implementation."
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "autodoc-0.5.0.tar.gz", hash = "sha256:c4387c5a0f1c09b055bb2e384542ee1e016542f313b2a33d904ca77f0460ded3"},
]
[package.dependencies]
decorator = "*"
webtest = "*"
[[package]]
name = "babel"
version = "2.13.1"
description = "Internationalization utilities"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"},
{file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"},
]
[package.dependencies]
setuptools = {version = "*", markers = "python_version >= \"3.12\""}
[package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "beautifulsoup4"
version = "4.12.2"
description = "Screen-scraping library"
category = "dev"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"},
{file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"},
]
[package.dependencies]
soupsieve = ">1.2"
[package.extras]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]] [[package]]
name = "black" name = "black"
version = "22.12.0" version = "22.12.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -297,7 +224,6 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "certifi" name = "certifi"
version = "2023.11.17" version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -309,7 +235,6 @@ files = [
name = "cffi" name = "cffi"
version = "1.16.0" version = "1.16.0"
description = "Foreign Function Interface for Python calling C code." description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -374,7 +299,6 @@ pycparser = "*"
name = "charset-normalizer" name = "charset-normalizer"
version = "3.3.2" version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false optional = false
python-versions = ">=3.7.0" python-versions = ">=3.7.0"
files = [ files = [
@ -474,7 +398,6 @@ files = [
name = "cleo" name = "cleo"
version = "2.1.0" version = "2.1.0"
description = "Cleo allows you to create beautiful and testable command-line interfaces." description = "Cleo allows you to create beautiful and testable command-line interfaces."
category = "main"
optional = false optional = false
python-versions = ">=3.7,<4.0" python-versions = ">=3.7,<4.0"
files = [ files = [
@ -490,7 +413,6 @@ rapidfuzz = ">=3.0.0,<4.0.0"
name = "click" name = "click"
version = "8.1.7" version = "8.1.7"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -505,7 +427,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "click-help-colors" name = "click-help-colors"
version = "0.9.4" version = "0.9.4"
description = "Colorization of help messages in Click" description = "Colorization of help messages in Click"
category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -523,7 +444,6 @@ dev = ["mypy", "pytest"]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "main"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [ files = [
@ -535,7 +455,6 @@ files = [
name = "crashtest" name = "crashtest"
version = "0.4.1" version = "0.4.1"
description = "Manage Python errors with ease" description = "Manage Python errors with ease"
category = "main"
optional = false optional = false
python-versions = ">=3.7,<4.0" python-versions = ">=3.7,<4.0"
files = [ files = [
@ -543,23 +462,10 @@ files = [
{file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"},
] ]
[[package]]
name = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]] [[package]]
name = "deezer-py" name = "deezer-py"
version = "1.3.6" version = "1.3.6"
description = "A wrapper for all Deezer's APIs" description = "A wrapper for all Deezer's APIs"
category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -570,23 +476,10 @@ files = [
[package.dependencies] [package.dependencies]
requests = "*" requests = "*"
[[package]]
name = "docutils"
version = "0.17.1"
description = "Docutils -- Python Documentation Utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
{file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
]
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.0" version = "1.2.0"
description = "Backport of PEP 654 (exception groups)" description = "Backport of PEP 654 (exception groups)"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -601,7 +494,6 @@ test = ["pytest (>=6)"]
name = "flake8" name = "flake8"
version = "3.9.2" version = "3.9.2"
description = "the modular source code checker: pep8 pyflakes and co" description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
files = [ files = [
@ -618,7 +510,6 @@ pyflakes = ">=2.3.0,<2.4.0"
name = "frozenlist" name = "frozenlist"
version = "1.4.0" version = "1.4.0"
description = "A list-like structure which implements collections.abc.MutableSequence" description = "A list-like structure which implements collections.abc.MutableSequence"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -689,7 +580,6 @@ files = [
name = "idna" name = "idna"
version = "3.6" version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -697,23 +587,10 @@ files = [
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
] ]
[[package]]
name = "imagesize"
version = "1.4.1"
description = "Getting image size from png/jpeg/jpeg2000/gif file"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
{file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.0.0" version = "2.0.0"
description = "brain-dead simple config-ini parsing" description = "brain-dead simple config-ini parsing"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -725,7 +602,6 @@ files = [
name = "iso8601" name = "iso8601"
version = "2.1.0" version = "2.1.0"
description = "Simple module to parse ISO 8601 dates" description = "Simple module to parse ISO 8601 dates"
category = "main"
optional = false optional = false
python-versions = ">=3.7,<4.0" python-versions = ">=3.7,<4.0"
files = [ files = [
@ -737,7 +613,6 @@ files = [
name = "isort" name = "isort"
version = "5.12.0" version = "5.12.0"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
@ -751,29 +626,10 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"
plugins = ["setuptools"] plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"] requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "m3u8" name = "m3u8"
version = "0.9.0" version = "0.9.0"
description = "Python m3u8 parser" description = "Python m3u8 parser"
category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -788,7 +644,6 @@ iso8601 = "*"
name = "markdown-it-py" name = "markdown-it-py"
version = "3.0.0" version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!" description = "Python port of markdown-it. Markdown parsing, done right!"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -809,81 +664,10 @@ profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]] [[package]]
name = "mccabe" name = "mccabe"
version = "0.6.1" version = "0.6.1"
description = "McCabe checker, plugin for flake8" description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -895,7 +679,6 @@ files = [
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"
description = "Markdown URL utilities" description = "Markdown URL utilities"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -907,7 +690,6 @@ files = [
name = "multidict" name = "multidict"
version = "6.0.4" version = "6.0.4"
description = "multidict implementation" description = "multidict implementation"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -991,7 +773,6 @@ files = [
name = "mutagen" name = "mutagen"
version = "1.47.0" version = "1.47.0"
description = "read and write audio tags for many formats" description = "read and write audio tags for many formats"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1003,7 +784,6 @@ files = [
name = "mypy-extensions" name = "mypy-extensions"
version = "1.0.0" version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker." description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1015,7 +795,6 @@ files = [
name = "packaging" name = "packaging"
version = "23.2" version = "23.2"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1027,7 +806,6 @@ files = [
name = "pathspec" name = "pathspec"
version = "0.11.2" version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1039,7 +817,6 @@ files = [
name = "pathvalidate" name = "pathvalidate"
version = "2.5.2" version = "2.5.2"
description = "pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc." description = "pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc."
category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -1054,7 +831,6 @@ test = ["allpairspy", "click", "faker", "pytest (>=6.0.1)", "pytest-discord (>=0
name = "pick" name = "pick"
version = "2.2.0" version = "2.2.0"
description = "Pick an option in the terminal with a simple GUI" description = "Pick an option in the terminal with a simple GUI"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1069,7 +845,6 @@ windows-curses = {version = ">=2.2.0,<3.0.0", markers = "sys_platform == \"win32
name = "pillow" name = "pillow"
version = "9.5.0" version = "9.5.0"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (Fork)"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1149,7 +924,6 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
name = "platformdirs" name = "platformdirs"
version = "4.0.0" version = "4.0.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1165,7 +939,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
name = "pluggy" name = "pluggy"
version = "1.3.0" version = "1.3.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -1181,7 +954,6 @@ testing = ["pytest", "pytest-benchmark"]
name = "pycares" name = "pycares"
version = "4.4.0" version = "4.4.0"
description = "Python interface for c-ares" description = "Python interface for c-ares"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -1248,7 +1020,6 @@ idna = ["idna (>=2.1)"]
name = "pycodestyle" name = "pycodestyle"
version = "2.7.0" version = "2.7.0"
description = "Python style guide checker" description = "Python style guide checker"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
@ -1260,7 +1031,6 @@ files = [
name = "pycparser" name = "pycparser"
version = "2.21" version = "2.21"
description = "C parser in Python" description = "C parser in Python"
category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
@ -1272,7 +1042,6 @@ files = [
name = "pycryptodomex" name = "pycryptodomex"
version = "3.19.0" version = "3.19.0"
description = "Cryptographic library for Python" description = "Cryptographic library for Python"
category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [ files = [
@ -1314,7 +1083,6 @@ files = [
name = "pyflakes" name = "pyflakes"
version = "2.3.1" version = "2.3.1"
description = "passive checker of Python programs" description = "passive checker of Python programs"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
@ -1326,7 +1094,6 @@ files = [
name = "pygments" name = "pygments"
version = "2.17.2" version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1342,7 +1109,6 @@ windows-terminal = ["colorama (>=0.4.6)"]
name = "pytest" name = "pytest"
version = "7.4.3" version = "7.4.3"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1365,7 +1131,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
name = "pytest-asyncio" name = "pytest-asyncio"
version = "0.21.1" version = "0.21.1"
description = "Pytest support for asyncio" description = "Pytest support for asyncio"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1384,7 +1149,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
name = "pytest-mock" name = "pytest-mock"
version = "3.12.0" version = "3.12.0"
description = "Thin-wrapper around the mock package for easier use with pytest" description = "Thin-wrapper around the mock package for easier use with pytest"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -1402,7 +1166,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
name = "rapidfuzz" name = "rapidfuzz"
version = "3.5.2" version = "3.5.2"
description = "rapid fuzzy string matching" description = "rapid fuzzy string matching"
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -1505,7 +1268,6 @@ full = ["numpy"]
name = "requests" name = "requests"
version = "2.31.0" version = "2.31.0"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1527,7 +1289,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "rich" name = "rich"
version = "13.7.0" version = "13.7.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false optional = false
python-versions = ">=3.7.0" python-versions = ">=3.7.0"
files = [ files = [
@ -1542,11 +1303,36 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"] jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.1.9"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e6a212f436122ac73df851f0cf006e0c6612fe6f9c864ed17ebefce0eff6a5fd"},
{file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:28d920e319783d5303333630dae46ecc80b7ba294aeffedf946a02ac0b7cc3db"},
{file = "ruff-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:104aa9b5e12cb755d9dce698ab1b97726b83012487af415a4512fedd38b1459e"},
{file = "ruff-0.1.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e63bf5a4a91971082a4768a0aba9383c12392d0d6f1e2be2248c1f9054a20da"},
{file = "ruff-0.1.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d0738917c203246f3e275b37006faa3aa96c828b284ebfe3e99a8cb413c8c4b"},
{file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69dac82d63a50df2ab0906d97a01549f814b16bc806deeac4f064ff95c47ddf5"},
{file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2aec598fb65084e41a9c5d4b95726173768a62055aafb07b4eff976bac72a592"},
{file = "ruff-0.1.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:744dfe4b35470fa3820d5fe45758aace6269c578f7ddc43d447868cfe5078bcb"},
{file = "ruff-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479ca4250cab30f9218b2e563adc362bd6ae6343df7c7b5a7865300a5156d5a6"},
{file = "ruff-0.1.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:aa8344310f1ae79af9ccd6e4b32749e93cddc078f9b5ccd0e45bd76a6d2e8bb6"},
{file = "ruff-0.1.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:837c739729394df98f342319f5136f33c65286b28b6b70a87c28f59354ec939b"},
{file = "ruff-0.1.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e6837202c2859b9f22e43cb01992373c2dbfeae5c0c91ad691a4a2e725392464"},
{file = "ruff-0.1.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:331aae2cd4a0554667ac683243b151c74bd60e78fb08c3c2a4ac05ee1e606a39"},
{file = "ruff-0.1.9-py3-none-win32.whl", hash = "sha256:8151425a60878e66f23ad47da39265fc2fad42aed06fb0a01130e967a7a064f4"},
{file = "ruff-0.1.9-py3-none-win_amd64.whl", hash = "sha256:c497d769164df522fdaf54c6eba93f397342fe4ca2123a2e014a5b8fc7df81c7"},
{file = "ruff-0.1.9-py3-none-win_arm64.whl", hash = "sha256:0e17f53bcbb4fff8292dfd84cf72d767b5e146f009cccd40c2fad27641f8a7a9"},
{file = "ruff-0.1.9.tar.gz", hash = "sha256:b041dee2734719ddbb4518f762c982f2e912e7f28b8ee4fe1dee0b15d1b6e800"},
]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "67.8.0" version = "67.8.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1563,7 +1349,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
name = "simple-term-menu" name = "simple-term-menu"
version = "1.6.3" version = "1.6.3"
description = "A Python package which creates simple interactive menus on the command line." description = "A Python package which creates simple interactive menus on the command line."
category = "main"
optional = false optional = false
python-versions = "~=3.5" python-versions = "~=3.5"
files = [ files = [
@ -1571,165 +1356,10 @@ files = [
{file = "simple_term_menu-1.6.3-py3-none-any.whl", hash = "sha256:27bf782f3a415b93ac310f70ace8c2db403b4058411ad6f8144ed4a1f34a7e57"}, {file = "simple_term_menu-1.6.3-py3-none-any.whl", hash = "sha256:27bf782f3a415b93ac310f70ace8c2db403b4058411ad6f8144ed4a1f34a7e57"},
] ]
[[package]]
name = "snowballstemmer"
version = "2.2.0"
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
]
[[package]]
name = "soupsieve"
version = "2.5"
description = "A modern CSS selector implementation for Beautiful Soup."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
{file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
]
[[package]]
name = "sphinx"
version = "4.5.0"
description = "Python documentation generator"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"},
{file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"},
]
[package.dependencies]
alabaster = ">=0.7,<0.8"
babel = ">=1.3"
colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
docutils = ">=0.14,<0.18"
imagesize = "*"
Jinja2 = ">=2.3"
packaging = "*"
Pygments = ">=2.0"
requests = ">=2.5.0"
snowballstemmer = ">=1.1"
sphinxcontrib-applehelp = "*"
sphinxcontrib-devhelp = "*"
sphinxcontrib-htmlhelp = ">=2.0.0"
sphinxcontrib-jsmath = "*"
sphinxcontrib-qthelp = "*"
sphinxcontrib-serializinghtml = ">=1.1.5"
[package.extras]
docs = ["sphinxcontrib-websupport"]
lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "types-requests", "types-typed-ast"]
test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"]
[[package]]
name = "sphinxcontrib-applehelp"
version = "1.0.4"
description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"},
{file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"},
]
[package.extras]
lint = ["docutils-stubs", "flake8", "mypy"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-devhelp"
version = "1.0.2"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
]
[package.extras]
lint = ["docutils-stubs", "flake8", "mypy"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.0.1"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"},
{file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"},
]
[package.extras]
lint = ["docutils-stubs", "flake8", "mypy"]
test = ["html5lib", "pytest"]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
]
[package.extras]
test = ["flake8", "mypy", "pytest"]
[[package]]
name = "sphinxcontrib-qthelp"
version = "1.0.3"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
]
[package.extras]
lint = ["docutils-stubs", "flake8", "mypy"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{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"},
]
[package.extras]
lint = ["docutils-stubs", "flake8", "mypy"]
test = ["pytest"]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1741,7 +1371,6 @@ files = [
name = "tomlkit" name = "tomlkit"
version = "0.7.2" version = "0.7.2"
description = "Style preserving TOML library" description = "Style preserving TOML library"
category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [ files = [
@ -1753,7 +1382,6 @@ files = [
name = "tqdm" name = "tqdm"
version = "4.66.1" version = "4.66.1"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1774,7 +1402,6 @@ telegram = ["requests"]
name = "types-click" name = "types-click"
version = "7.1.8" version = "7.1.8"
description = "Typing stubs for click" description = "Typing stubs for click"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -1786,7 +1413,6 @@ files = [
name = "types-pillow" name = "types-pillow"
version = "8.3.11" version = "8.3.11"
description = "Typing stubs for Pillow" description = "Typing stubs for Pillow"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -1798,7 +1424,6 @@ files = [
name = "urllib3" name = "urllib3"
version = "2.1.0" version = "2.1.0"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
@ -1811,64 +1436,10 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"] zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "waitress"
version = "2.1.2"
description = "Waitress WSGI server"
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "waitress-2.1.2-py3-none-any.whl", hash = "sha256:7500c9625927c8ec60f54377d590f67b30c8e70ef4b8894214ac6e4cad233d2a"},
{file = "waitress-2.1.2.tar.gz", hash = "sha256:780a4082c5fbc0fde6a2fcfe5e26e6efc1e8f425730863c04085769781f51eba"},
]
[package.extras]
docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"]
testing = ["coverage (>=5.0)", "pytest", "pytest-cover"]
[[package]]
name = "webob"
version = "1.8.7"
description = "WSGI request and response object"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
files = [
{file = "WebOb-1.8.7-py2.py3-none-any.whl", hash = "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b"},
{file = "WebOb-1.8.7.tar.gz", hash = "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323"},
]
[package.extras]
docs = ["Sphinx (>=1.7.5)", "pylons-sphinx-themes"]
testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"]
[[package]]
name = "webtest"
version = "3.0.0"
description = "Helper to test WSGI applications"
category = "dev"
optional = false
python-versions = ">=3.6, <4"
files = [
{file = "WebTest-3.0.0-py3-none-any.whl", hash = "sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead"},
{file = "WebTest-3.0.0.tar.gz", hash = "sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb"},
]
[package.dependencies]
beautifulsoup4 = "*"
waitress = ">=0.8.5"
WebOb = ">=1.2"
[package.extras]
docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.8)"]
tests = ["PasteDeploy", "WSGIProxy2", "coverage", "pyquery", "pytest", "pytest-cov"]
[[package]] [[package]]
name = "windows-curses" name = "windows-curses"
version = "2.3.2" version = "2.3.2"
description = "Support for the standard curses module on Windows" description = "Support for the standard curses module on Windows"
category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -1892,7 +1463,6 @@ files = [
name = "yarl" name = "yarl"
version = "1.9.3" version = "1.9.3"
description = "Yet another URL library" description = "Yet another URL library"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1995,4 +1565,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.10 <4.0" python-versions = ">=3.10 <4.0"
content-hash = "2395d1cc4b03aba9e939f3bdb4f4e90ba757227c43acbad2d310ed891fe5c307" content-hash = "65ad2363e3856aed80ba967c8d53e5435c8f4230e27e1178bf832866fb91e1c3"

View File

@ -153,7 +153,7 @@ class DeezerClient(Client):
_, format_str = quality_map[quality] _, format_str = quality_map[quality]
dl_info["quality_to_size"] = [ dl_info["quality_to_size"] = [
track_info.get(f"FILESIZE_{format}", 0) for _, format in quality_map int(track_info.get(f"FILESIZE_{format}", 0)) for _, format in quality_map
] ]
token = track_info["TRACK_TOKEN"] token = track_info["TRACK_TOKEN"]

View File

@ -48,7 +48,7 @@ class Downloadable(ABC):
await self._download(path, callback) await self._download(path, callback)
async def size(self) -> int: async def size(self) -> int:
if self._size is not None: if hasattr(self, "_size") and self._size is not None:
return self._size return self._size
async with self.session.head(self.url) as response: async with self.session.head(self.url) as response:
@ -89,8 +89,11 @@ class DeezerDownloadable(Downloadable):
logger.debug("Deezer info for downloadable: %s", info) logger.debug("Deezer info for downloadable: %s", info)
self.session = session self.session = session
self.url = info["url"] self.url = info["url"]
self.quality = info["quality"] max_quality_available = max(
self._size = int(info["quality_to_size"][self.quality]) i for i, size in enumerate(info["quality_to_size"]) if size > 0
)
self.quality = min(info["quality"], max_quality_available)
self._size = info["quality_to_size"][self.quality]
if self.quality <= 1: if self.quality <= 1:
self.extension = "mp3" self.extension = "mp3"
else: else:
@ -290,6 +293,7 @@ class SoundcloudDownloadable(Downloadable):
async def _download_original(self, path: str, callback): async def _download_original(self, path: str, callback):
downloader = BasicDownloadable(self.session, self.url, "flac") downloader = BasicDownloadable(self.session, self.url, "flac")
await downloader.download(path, callback) await downloader.download(path, callback)
self.size = downloader.size
engine = converter.FLAC(path) engine = converter.FLAC(path)
await engine.convert(path) await engine.convert(path)

View File

@ -93,6 +93,9 @@ class SoundcloudClient(Client):
} }
resp, status = await self._api_request(f"search/{media_type}s", params=params) resp, status = await self._api_request(f"search/{media_type}s", params=params)
assert status == 200 assert status == 200
if media_type == "track":
for item in resp["collection"]:
item["id"] = self._get_custom_id(item)
return [resp] return [resp]
async def get_downloadable(self, item_info: str, _) -> SoundcloudDownloadable: async def get_downloadable(self, item_info: str, _) -> SoundcloudDownloadable:

View File

@ -18,7 +18,10 @@ logger = logging.getLogger("streamrip")
def remove_artwork_tempdirs(): def remove_artwork_tempdirs():
logger.debug("Removing dirs %s", _artwork_tempdirs) logger.debug("Removing dirs %s", _artwork_tempdirs)
for path in _artwork_tempdirs: for path in _artwork_tempdirs:
shutil.rmtree(path) try:
shutil.rmtree(path)
except FileNotFoundError:
pass
async def download_artwork( async def download_artwork(

View File

@ -224,7 +224,8 @@ class AlbumMetadata:
safe_get(track, "publisher_metadata", "explicit", default=False), safe_get(track, "publisher_metadata", "explicit", default=False),
bool, bool,
) )
genre = typed(track["genre"], str) genre = typed(track["genre"], str | None)
genres = [genre] if genre is not None else []
artist = typed(safe_get(track, "publisher_metadata", "artist"), str | None) artist = typed(safe_get(track, "publisher_metadata", "artist"), str | None)
artist = artist or typed(track["user"]["username"], str) artist = artist or typed(track["user"]["username"], str)
albumartist = artist albumartist = artist
@ -259,7 +260,7 @@ class AlbumMetadata:
album_title, album_title,
albumartist, albumartist,
year, year,
genre=[genre], genre=genres,
covers=covers, covers=covers,
albumcomposer=None, albumcomposer=None,
comment=None, comment=None,

View File

@ -46,7 +46,7 @@ class ArtistSummary(Summary):
@classmethod @classmethod
def from_item(cls, item: dict): def from_item(cls, item: dict):
id = item["id"] id = str(item["id"])
name = ( name = (
item.get("name") item.get("name")
or item.get("performer", {}).get("name") or item.get("performer", {}).get("name")
@ -81,7 +81,7 @@ class TrackSummary(Summary):
@classmethod @classmethod
def from_item(cls, item: dict): def from_item(cls, item: dict):
id = item["id"] id = str(item["id"])
name = item.get("title") or item.get("name") or "Unknown" name = item.get("title") or item.get("name") or "Unknown"
artist = ( artist = (
item.get("performer", {}).get("name") item.get("performer", {}).get("name")
@ -127,7 +127,7 @@ class AlbumSummary(Summary):
@classmethod @classmethod
def from_item(cls, item: dict): def from_item(cls, item: dict):
id = item["id"] id = str(item["id"])
name = item.get("title") or "Unknown Title" name = item.get("title") or "Unknown Title"
artist = ( artist = (
item.get("performer", {}).get("name") item.get("performer", {}).get("name")
@ -175,7 +175,7 @@ class LabelSummary(Summary):
@classmethod @classmethod
def from_item(cls, item: dict): def from_item(cls, item: dict):
id = item["id"] id = str(item["id"])
name = item["name"] name = item["name"]
return cls(id, name) return cls(id, name)
@ -279,6 +279,17 @@ class SearchResults:
i = int(ind.group(0)) i = int(ind.group(0))
return self.results[i - 1].preview() return self.results[i - 1].preview()
def as_list(self, source: str) -> list[dict[str, str]]:
return [
{
"source": source,
"media_type": i.media_type(),
"id": i.id,
"desc": i.summarize(),
}
for i in self.results
]
def clean(s: str, trunc=True) -> str: def clean(s: str, trunc=True) -> str:
s = s.replace("|", "").replace("\n", "") s = s.replace("|", "").replace("\n", "")

View File

@ -1,9 +1,11 @@
import asyncio import asyncio
import json
import logging import logging
import os import os
import shutil import shutil
import subprocess import subprocess
from functools import wraps from functools import wraps
from typing import Any
import aiofiles import aiofiles
import click import click
@ -158,7 +160,9 @@ async def url(ctx, urls):
@rip.command() @rip.command()
@click.argument( @click.argument(
"path", required=True, type=click.Path(file_okay=True, dir_okay=False, exists=True) "path",
required=True,
type=click.Path(exists=True, readable=True, file_okay=True, dir_okay=False),
) )
@click.pass_context @click.pass_context
@coro @coro
@ -171,8 +175,26 @@ async def file(ctx, path):
""" """
with ctx.obj["config"] as cfg: with ctx.obj["config"] as cfg:
async with Main(cfg) as main: async with Main(cfg) as main:
async with aiofiles.open(path) as f: async with aiofiles.open(path, "r") as f:
await main.add_all([line async for line in f]) try:
items: Any = json.loads(await f.read())
loaded = True
except json.JSONDecodeError:
items: Any = [line async for line in f]
loaded = False
if loaded:
console.print(
f"Detected json file. Loading [yellow]{len(items)}[/yellow] items"
)
await main.add_all_by_id(
[(i["source"], i["media_type"], i["id"]) for i in items]
)
else:
console.print(
f"Detected list of urls. Loading [yellow]{len(items)}[/yellow] items"
)
await main.add_all(items)
await main.resolve() await main.resolve()
await main.rip() await main.rip()
@ -278,22 +300,42 @@ def database_browse(ctx, table):
help="Automatically download the first search result without showing the menu.", help="Automatically download the first search result without showing the menu.",
is_flag=True, is_flag=True,
) )
@click.option(
"-o",
"--output-file",
help="Write search results to a file instead of showing interactive menu.",
type=click.Path(writable=True),
)
@click.option(
"-n",
"--num-results",
help="Maximum number of search results to show",
default=100,
type=click.IntRange(min=1),
)
@click.argument("source", required=True) @click.argument("source", required=True)
@click.argument("media-type", required=True) @click.argument("media-type", required=True)
@click.argument("query", required=True) @click.argument("query", required=True)
@click.pass_context @click.pass_context
@coro @coro
async def search(ctx, first, source, media_type, query): async def search(ctx, first, output_file, num_results, source, media_type, query):
"""Search for content using a specific source. """Search for content using a specific source.
Example: Example:
rip search qobuz album 'rumours' rip search qobuz album 'rumours'
""" """
if first and output_file:
console.print("Cannot choose --first and --output-file!")
return
with ctx.obj["config"] as cfg: with ctx.obj["config"] as cfg:
async with Main(cfg) as main: async with Main(cfg) as main:
if first: if first:
await main.search_take_first(source, media_type, query) await main.search_take_first(source, media_type, query)
elif output_file:
await main.search_output_file(
source, media_type, query, output_file, num_results
)
else: else:
await main.search_interactive(source, media_type, query) await main.search_interactive(source, media_type, query)
await main.resolve() await main.resolve()

View File

@ -1,6 +1,9 @@
import asyncio import asyncio
import json
import logging import logging
import os import platform
import aiofiles
from .. import db from .. import db
from ..client import Client, DeezerClient, QobuzClient, SoundcloudClient, TidalClient from ..client import Client, DeezerClient, QobuzClient, SoundcloudClient, TidalClient
@ -171,7 +174,7 @@ class Main:
return return
search_results = SearchResults.from_pages(source, media_type, pages) search_results = SearchResults.from_pages(source, media_type, pages)
if os.name == "nt": if platform.system() == "Windows": # simple term menu not supported for windows
from pick import pick from pick import pick
choices = pick( choices = pick(
@ -215,7 +218,9 @@ class Main:
async def search_take_first(self, source: str, media_type: str, query: str): async def search_take_first(self, source: str, media_type: str, query: str):
client = await self.get_logged_in_client(source) client = await self.get_logged_in_client(source)
pages = await client.search(media_type, query, limit=1) with console.status(f"[bold]Searching {source}", spinner="dots"):
pages = await client.search(media_type, query, limit=1)
if len(pages) == 0: if len(pages) == 0:
console.print(f"[red]No search results found for query {query}") console.print(f"[red]No search results found for query {query}")
return return
@ -223,7 +228,27 @@ class Main:
search_results = SearchResults.from_pages(source, media_type, pages) search_results = SearchResults.from_pages(source, media_type, pages)
assert len(search_results.results) > 0 assert len(search_results.results) > 0
first = search_results.results[0] first = search_results.results[0]
await self.add(f"http://{source}.com/{first.media_type()}/{first.id}") await self.add_by_id(source, first.media_type(), first.id)
async def search_output_file(
self, source: str, media_type: str, query: str, filepath: str, limit: int
):
client = await self.get_logged_in_client(source)
with console.status(f"[bold]Searching {source}", spinner="dots"):
pages = await client.search(media_type, query, limit=limit)
if len(pages) == 0:
console.print(f"[red]No search results found for query {query}")
return
search_results = SearchResults.from_pages(source, media_type, pages)
file_contents = json.dumps(search_results.as_list(source), indent=4)
async with aiofiles.open(filepath, "w") as f:
await f.write(file_contents)
console.print(
f"Wrote [purple]{len(search_results.results)}[/purple] results to [cyan]{filepath} as JSON!"
)
async def resolve_lastfm(self, playlist_url: str): async def resolve_lastfm(self, playlist_url: str):
"""Resolve a last.fm playlist.""" """Resolve a last.fm playlist."""

View File

@ -5,7 +5,7 @@ import pytest
from util import arun from util import arun
from streamrip.config import Config from streamrip.config import Config
from streamrip.qobuz_client import QobuzClient from streamrip.client.qobuz import QobuzClient
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@ -21,4 +21,6 @@ def qobuz_client():
client = QobuzClient(config) client = QobuzClient(config)
arun(client.login()) arun(client.login())
return client yield client
arun(client.session.close())

BIN
tests/silence.flac Normal file

Binary file not shown.

View File

@ -44,8 +44,8 @@ def test_sample_config_data_fields(sample_config_data):
folder="test_folder", folder="test_folder",
source_subdirectories=False, source_subdirectories=False,
concurrency=True, concurrency=True,
max_connections=3, max_connections=6,
requests_per_minute=-1, requests_per_minute=60,
), ),
qobuz=QobuzConfig( qobuz=QobuzConfig(
use_auth_token=False, use_auth_token=False,
@ -81,13 +81,13 @@ def test_sample_config_data_fields(sample_config_data):
quality=0, quality=0,
download_videos=False, download_videos=False,
), ),
lastfm=LastFmConfig(source="qobuz", fallback_source="deezer"), lastfm=LastFmConfig(source="qobuz", fallback_source=""),
filepaths=FilepathsConfig( filepaths=FilepathsConfig(
add_singles_to_folder=False, add_singles_to_folder=False,
folder_format="{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]", folder_format="{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]",
track_format="{tracknumber}. {artist} - {title}{explicit}", track_format="{tracknumber}. {artist} - {title}{explicit}",
restrict_characters=False, restrict_characters=False,
truncate_to=200, truncate_to=120,
), ),
artwork=ArtworkConfig( artwork=ArtworkConfig(
embed=True, embed=True,
@ -109,6 +109,11 @@ def test_sample_config_data_fields(sample_config_data):
non_studio_albums=False, non_studio_albums=False,
non_remaster=False, non_remaster=False,
), ),
cli=CliConfig(
text_output=False,
progress_bars=False,
max_search_results=100,
),
database=DatabaseConfig( database=DatabaseConfig(
downloads_enabled=True, downloads_enabled=True,
downloads_path="downloadspath", downloads_path="downloadspath",

View File

@ -12,10 +12,10 @@ concurrency = true
# If you have very fast internet, you will benefit from a higher value, # If you have very fast internet, you will benefit from a higher value,
# A value that is too high for your bandwidth may cause slowdowns # A value that is too high for your bandwidth may cause slowdowns
# Set to -1 for no limit # Set to -1 for no limit
max_connections = 3 max_connections = 6
# Max number of API requests to handle per minute # Max number of API requests per source to handle per minute
# Set to -1 for no limit # Set to -1 for no limit
requests_per_minute = -1 requests_per_minute = 60
[qobuz] [qobuz]
# 1: 320kbps MP3, 2: 16/44.1, 3: 24/<=96, 4: 24/>=96 # 1: 320kbps MP3, 2: 16/44.1, 3: 24/<=96, 4: 24/>=96
@ -81,11 +81,13 @@ download_videos = false
# The path to download the videos to # The path to download the videos to
video_downloads_folder = "videodownloadsfolder" video_downloads_folder = "videodownloadsfolder"
# This stores a list of item IDs so that repeats are not downloaded.
[database] [database]
# Create a database that contains all the track IDs downloaded so far
# Any time a track logged in the database is requested, it is skipped
# This can be disabled temporarily with the --no-db flag
downloads_enabled = true downloads_enabled = true
# Path to the downloads database
downloads_path = "downloadspath" downloads_path = "downloadspath"
# If a download fails, the item ID is stored here. Then, `rip repair` can be # If a download fails, the item ID is stored here. Then, `rip repair` can be
# called to retry the downloads # called to retry the downloads
failed_downloads_enabled = true failed_downloads_enabled = true
@ -141,8 +143,9 @@ saved_max_width = -1
# Sets the value of the 'ALBUM' field in the metadata to the playlist's name. # Sets the value of the 'ALBUM' field in the metadata to the playlist's name.
# This is useful if your music library software organizes tracks based on album name. # This is useful if your music library software organizes tracks based on album name.
set_playlist_to_album = true set_playlist_to_album = true
# Replaces the original track's tracknumber with it's position in the playlist # If part of a playlist, sets the `tracknumber` field in the metadata to the track's
new_playlist_tracknumbers = true # position in the playlist instead of its position in its album
renumber_playlist_tracks = true
# The following metadata tags won't be applied # The following metadata tags won't be applied
# See https://github.com/nathom/streamrip/wiki/Metadata-Tag-Names for more info # See https://github.com/nathom/streamrip/wiki/Metadata-Tag-Names for more info
exclude = [] exclude = []
@ -162,7 +165,7 @@ track_format = "{tracknumber}. {artist} - {title}{explicit}"
restrict_characters = false restrict_characters = false
# Truncate the filename if it is greater than this number of characters # Truncate the filename if it is greater than this number of characters
# Setting this to false may cause downloads to fail on some systems # Setting this to false may cause downloads to fail on some systems
truncate_to = 200 truncate_to = 120
# Last.fm playlists are downloaded by searching for the titles of the tracks # Last.fm playlists are downloaded by searching for the titles of the tracks
[lastfm] [lastfm]
@ -170,11 +173,15 @@ truncate_to = 200
source = "qobuz" source = "qobuz"
# If no results were found with the primary source, the item is searched for # If no results were found with the primary source, the item is searched for
# on this one. # on this one.
fallback_source = "deezer" fallback_source = ""
[theme] [cli]
# Options: "dainty" or "plain" # Print "Downloading {Album name}" etc. to screen
progress_bar = "dainty" text_output = true
# Show resolve, download progress bars
progress_bars = true
# The maximum number of search results to show in the interactive menu
max_search_results = 100
[misc] [misc]
# Metadata to identify this config file. Do not change. # Metadata to identify this config file. Do not change.

View File

@ -48,17 +48,16 @@ def test_album_metadata_qobuz():
def test_track_metadata_qobuz(): def test_track_metadata_qobuz():
a = AlbumMetadata.from_qobuz(qobuz_track_resp["album"]) a = AlbumMetadata.from_qobuz(qobuz_track_resp["album"])
t = TrackMetadata.from_qobuz(a, qobuz_track_resp) t = TrackMetadata.from_qobuz(a, qobuz_track_resp)
assert t.title == "Dreams (2001 Remaster)"
info = t.info info = t.info
assert info.id == "19512574" assert info.id == "216020864"
assert info.quality == 3 assert info.quality == 3
assert info.bit_depth == 24 assert info.bit_depth == 24
assert info.sampling_rate == 96 assert info.sampling_rate == 96
assert info.work is None assert info.work is None
assert t.title == "Dreams (2001 Remaster)" assert t.title == "Water Tower"
assert t.album == a assert t.album == a
assert t.artist == "Fleetwood Mac" assert t.artist == "The Mountain Goats"
assert t.tracknumber == 2 assert t.tracknumber == 9
assert t.discnumber == 1 assert t.discnumber == 1
assert t.composer == None assert t.composer == "John Darnielle"

View File

@ -4,9 +4,11 @@ import pytest
from util import afor, arun from util import afor, arun
from streamrip.config import Config from streamrip.config import Config
from streamrip.downloadable import BasicDownloadable from streamrip.client.downloadable import BasicDownloadable
from streamrip.exceptions import MissingCredentials from streamrip.exceptions import MissingCredentialsError
from streamrip.qobuz_client import QobuzClient from streamrip.client.qobuz import QobuzClient
from fixtures.clients import qobuz_client
logger = logging.getLogger("streamrip") logger = logging.getLogger("streamrip")
@ -18,12 +20,12 @@ def client(qobuz_client):
def test_client_raises_missing_credentials(): def test_client_raises_missing_credentials():
c = Config.defaults() c = Config.defaults()
with pytest.raises(MissingCredentials): with pytest.raises(MissingCredentialsError):
arun(QobuzClient(c).login()) arun(QobuzClient(c).login())
def test_client_get_metadata(client): def test_client_get_metadata(client):
meta = arun(client.get_metadata("lzpf67e8f4h1a", "album")) meta = arun(client.get_metadata("s9nzkwg2rh1nc", "album"))
assert meta["title"] == "I Killed Your Dog" assert meta["title"] == "I Killed Your Dog"
assert len(meta["tracks"]["items"]) == 16 assert len(meta["tracks"]["items"]) == 16
assert meta["maximum_bit_depth"] == 24 assert meta["maximum_bit_depth"] == 24
@ -38,18 +40,19 @@ def test_client_get_downloadable(client):
def test_client_search_limit(client): def test_client_search_limit(client):
res = client.search("rumours", "album", limit=5) res = client.search("album", "rumours", limit=5)
total = 0 total = 0
for r in afor(res): for r in arun(res):
total += len(r["albums"]["items"]) total += len(r["albums"]["items"])
assert total == 5 assert total == 5
def test_client_search_no_limit(client): def test_client_search_no_limit(client):
res = client.search("rumours", "album", limit=None) # Setting no limit has become impossible because `limit: int` now
res = client.search("album", "rumours", limit=10000)
correct_total = 0 correct_total = 0
total = 0 total = 0
for r in afor(res): for r in arun(res):
total += len(r["albums"]["items"]) total += len(r["albums"]["items"])
correct_total = max(correct_total, r["albums"]["total"]) correct_total = max(correct_total, r["albums"]["total"])
assert total == correct_total assert total == correct_total

View File

@ -3,7 +3,9 @@ from mutagen.flac import FLAC
from util import arun from util import arun
from streamrip.metadata import * from streamrip.metadata import *
from streamrip.tagger import tag_file from streamrip.metadata.tagger import tag_file
from streamrip.metadata.track_metadata import TrackInfo
from streamrip.metadata.album_metadata import AlbumInfo
test_flac = "tests/silence.flac" test_flac = "tests/silence.flac"
test_cover = "tests/1x1_pixel.jpg" test_cover = "tests/1x1_pixel.jpg"

View File

@ -3,19 +3,24 @@ import shutil
from util import arun from util import arun
from streamrip.downloadable import Downloadable from streamrip.client.downloadable import Downloadable
from streamrip.qobuz_client import QobuzClient from streamrip.client.qobuz import QobuzClient
from streamrip.track import PendingSingle, Track from streamrip.media.track import PendingSingle, Track
import streamrip.db as db
from fixtures.clients import qobuz_client
def test_pending_resolve(qobuz_client: QobuzClient): def test_pending_resolve(qobuz_client: QobuzClient):
qobuz_client.config.session.downloads.folder = "./tests" qobuz_client.config.session.downloads.folder = "./tests"
p = PendingSingle("19512574", qobuz_client, qobuz_client.config) p = PendingSingle("19512574", qobuz_client, qobuz_client.config, db.Database(db.Dummy(), db.Dummy()))
t = arun(p.resolve()) t = arun(p.resolve())
dir = "tests/Fleetwood Mac - Rumours (1977) [FLAC] [24B-96kHz]" dir = "tests/tests/Fleetwood Mac - Rumours (1977) [FLAC] [24B-96kHz]"
assert os.path.isdir(dir) assert os.path.isdir(dir)
assert os.path.isfile(os.path.join(dir, "cover.jpg")) assert os.path.isfile(os.path.join(dir, "cover.jpg"))
assert os.path.isfile(os.path.join(dir, "embed_cover.jpg")) #embedded_cover_path aka t.cover_path is
#./tests/./tests/Fleetwood Mac - Rumours (1977) [FLAC] [24B-96kHz]/
# __artwork/cover-9202762427033526105.jpg
assert os.path.isfile(t.cover_path)
assert isinstance(t, Track) assert isinstance(t, Track)
assert isinstance(t.downloadable, Downloadable) assert isinstance(t.downloadable, Downloadable)
assert t.cover_path is not None assert t.cover_path is not None