mirror of https://github.com/nathom/streamrip.git
Compare commits
49 Commits
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | f855572ebf | |
Roman | cc9bbddfff | |
Nathan Thomas | 45bf6f6b65 | |
Nathan Thomas | 3e99dad408 | |
Nathan Thomas | 54d05e1330 | |
Nathan Thomas | 178168cc68 | |
Nathan Thomas | c646c01789 | |
Nathan Thomas | ad73a01a03 | |
Nathan Thomas | 22d6a9b137 | |
Nathan Thomas | 527b52cae2 | |
dependabot[bot] | 868a8fff99 | |
dependabot[bot] | 079cef0c2a | |
dependabot[bot] | a677ccb018 | |
dependabot[bot] | 61397d616d | |
Tanner Hoisington | 6940eae650 | |
yodatak | ab08e54e37 | |
Nathan Thomas | 8757956636 | |
RealStickman | affdaa8fab | |
dependabot[bot] | 3443331501 | |
disconnect78 | 4353c84837 | |
Nathan Thomas | b01382f267 | |
disconnect78 | 9d6a2be49e | |
Nathan Thomas | 39aada312c | |
Nathan Thomas | 87d59648cf | |
Nathan Thomas | 24d23ad230 | |
Nathan Thomas | 1c2bd2545c | |
Nathan Thomas | bd3bff1f0d | |
Nathan Thomas | 01c50f4644 | |
Nathan Thomas | 99578f8577 | |
Nathan Thomas | c2b4c38e2f | |
Nathan Thomas | c6b29c2fab | |
dependabot[bot] | 070402eb1e | |
Nathan Thomas | 56f9aac92a | |
Nathan Thomas | 04f6881131 | |
Aria Stewart | 2175231bc1 | |
Alex Camilleri | 669ceee48a | |
Nathan Thomas | 1704406cdf | |
Nathan Thomas | fa65929c97 | |
Nathan Thomas | 8bc87a4b74 | |
dependabot[bot] | 4c210b9e52 | |
Nathan Thomas | 2a8bb7cf28 | |
Nathan Thomas | 52dc84cd13 | |
Nathan Thomas | df80d2708b | |
Nathan Thomas | 4c04188ade | |
Marek Veselý | 1271df5ca7 | |
Nathan Thomas | 577d914e93 | |
Nathan Thomas | 63f3901eaf | |
Nathan Thomas | 99f3220048 | |
Nathan Thomas | d4b45a7a99 |
|
@ -0,0 +1,13 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [nathom]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@ -36,9 +36,9 @@ body:
|
|||
attributes:
|
||||
label: Debug Traceback
|
||||
description: |
|
||||
Run your command, with `-vvv` appended to it, and paste the output here.
|
||||
Run your command with the `-vvv` option and paste the output here.
|
||||
For example, if the problematic command was `rip url https://example.com`, then
|
||||
you would run `rip url https://example.com -vvv` to get the debug logs.
|
||||
you would run `rip -vvv url https://example.com` to get the debug logs.
|
||||
Make sure to check the logs for any personal information such as emails and remove them.
|
||||
render: "text"
|
||||
placeholder: Logs printed to terminal screen
|
||||
|
@ -49,7 +49,7 @@ body:
|
|||
attributes:
|
||||
label: Config File
|
||||
description: |
|
||||
Find the config file using `rip config --open` and paste the contents here.
|
||||
Find the config file using `rip config open` and paste the contents here.
|
||||
Make sure you REMOVE YOUR CREDENTIALS!
|
||||
render: toml
|
||||
placeholder: Contents of config.toml
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
name: Python Poetry Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
name: Check out repository code
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10' # Specify the Python version
|
||||
|
||||
- name: Install and configure Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: 1.5.1
|
||||
virtualenvs-create: false
|
||||
virtualenvs-in-project: true
|
||||
installer-parallel: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
|
||||
- name: Run tests
|
||||
run: poetry run pytest
|
||||
|
||||
- name: Success message
|
||||
if: success()
|
||||
run: echo "Tests passed successfully!"
|
|
@ -0,0 +1,11 @@
|
|||
name: Ruff
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: chartboost/ruff-action@v1
|
||||
- uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
args: 'format --check'
|
34
README.md
34
README.md
|
@ -35,6 +35,16 @@ rip
|
|||
|
||||
it should show the main help page. If you have no idea what these mean, or are having other issues installing, check out the [detailed installation instructions](https://github.com/nathom/streamrip/wiki#detailed-installation-instructions).
|
||||
|
||||
For Arch Linux users, an AUR package exists. Make sure to install required packages from the AUR before using `makepkg` or use an AUR helper to automatically resolve them.
|
||||
```
|
||||
git clone https://aur.archlinux.org/streamrip.git
|
||||
cd streamrip
|
||||
makepkg -si
|
||||
```
|
||||
or
|
||||
```
|
||||
paru -S streamrip
|
||||
```
|
||||
|
||||
### Streamrip beta
|
||||
|
||||
|
@ -61,17 +71,13 @@ Download multiple albums from Qobuz
|
|||
rip url https://www.qobuz.com/us-en/album/back-in-black-ac-dc/0886444889841 https://www.qobuz.com/us-en/album/blue-train-john-coltrane/0060253764852
|
||||
```
|
||||
|
||||
|
||||
|
||||
Download the album and convert it to `mp3`
|
||||
|
||||
```bash
|
||||
rip url --codec mp3 https://open.qobuz.com/album/0060253780968
|
||||
rip --codec mp3 url https://open.qobuz.com/album/0060253780968
|
||||
```
|
||||
|
||||
|
||||
|
||||
To set the maximum quality, use the `--max-quality` option to `0, 1, 2, 3, 4`:
|
||||
To set the maximum quality, use the `--quality` option to `0, 1, 2, 3, 4`:
|
||||
|
||||
| Quality ID | Audio Quality | Available Sources |
|
||||
| ---------- | --------------------- | -------------------------------------------- |
|
||||
|
@ -81,14 +87,13 @@ To set the maximum quality, use the `--max-quality` option to `0, 1, 2, 3, 4`:
|
|||
| 3 | 24 bit, ≤ 96 kHz | Tidal (MQA), Qobuz, SoundCloud (rarely) |
|
||||
| 4 | 24 bit, ≤ 192 kHz | Qobuz |
|
||||
|
||||
|
||||
```bash
|
||||
rip url --quality 3 https://tidal.com/browse/album/147569387
|
||||
rip --quality 3 url https://tidal.com/browse/album/147569387
|
||||
```
|
||||
|
||||
> Using `4` is generally a waste of space. It is impossible for humans to perceive the between sampling rates higher than 44.1 kHz. It may be useful if you're processing/slowing down the audio.
|
||||
> Using `4` is generally a waste of space. It is impossible for humans to perceive the difference between sampling rates higher than 44.1 kHz. It may be useful if you're processing/slowing down the audio.
|
||||
|
||||
Search for albums matching `lil uzi vert` on SoundCloud
|
||||
Search for playlists matching `rap` on Tidal
|
||||
|
||||
```bash
|
||||
rip search tidal playlist 'rap'
|
||||
|
@ -114,9 +119,7 @@ For more customization, see the config file
|
|||
rip config open
|
||||
```
|
||||
|
||||
|
||||
|
||||
If you're confused about anything, see the help pages. The main help pages can be accessed by typing `rip` by itself in the command line. The help pages for each command can be accessed with the `-help` flag. For example, to see the help page for the `url` command, type
|
||||
If you're confused about anything, see the help pages. The main help pages can be accessed by typing `rip` by itself in the command line. The help pages for each command can be accessed with the `--help` flag. For example, to see the help page for the `url` command, type
|
||||
|
||||
```
|
||||
rip url --help
|
||||
|
@ -128,7 +131,6 @@ rip url --help
|
|||
|
||||
For more in-depth information about `streamrip`, see the help pages and the [wiki](https://github.com/nathom/streamrip/wiki/).
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
All contributions are appreciated! You can help out the project by opening an issue
|
||||
|
@ -153,7 +155,7 @@ Please document any functions or obscure lines of code.
|
|||
|
||||
### The Wiki
|
||||
|
||||
To help out `streamrip` users that may be having trouble, consider contributing some information to the wiki.
|
||||
To help out `streamrip` users that may be having trouble, consider contributing some information to the wiki.
|
||||
Nothing is too obvious and everything is appreciated.
|
||||
|
||||
## Acknowledgements
|
||||
|
@ -167,8 +169,6 @@ Thanks to Vitiko98, Sorrow446, and DashLt for their contributions to this projec
|
|||
- [Tidal-Media-Downloader](https://github.com/yaronzz/Tidal-Media-Downloader)
|
||||
- [scdl](https://github.com/flyingrub/scdl)
|
||||
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
I will not be responsible for how **you** use `streamrip`. By using `streamrip`, you agree to the terms and conditions of the Qobuz, Tidal, and Deezer APIs.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiodns"
|
||||
|
@ -27,87 +27,87 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.9.1"
|
||||
version = "3.9.4"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-win32.whl", hash = "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489"},
|
||||
{file = "aiohttp-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-win32.whl", hash = "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544"},
|
||||
{file = "aiohttp-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed"},
|
||||
{file = "aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-win32.whl", hash = "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4"},
|
||||
{file = "aiohttp-3.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-win32.whl", hash = "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7"},
|
||||
{file = "aiohttp-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf"},
|
||||
{file = "aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:76d32588ef7e4a3f3adff1956a0ba96faabbdee58f2407c122dd45aa6e34f372"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:56181093c10dbc6ceb8a29dfeea1e815e1dfdc020169203d87fd8d37616f73f9"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7a5b676d3c65e88b3aca41816bf72831898fcd73f0cbb2680e9d88e819d1e4d"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1df528a85fb404899d4207a8d9934cfd6be626e30e5d3a5544a83dbae6d8a7e"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f595db1bceabd71c82e92df212dd9525a8a2c6947d39e3c994c4f27d2fe15b11"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c0b09d76e5a4caac3d27752027fbd43dc987b95f3748fad2b924a03fe8632ad"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689eb4356649ec9535b3686200b231876fb4cab4aca54e3bece71d37f50c1d13"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3666cf4182efdb44d73602379a66f5fdfd5da0db5e4520f0ac0dcca644a3497"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b65b0f8747b013570eea2f75726046fa54fa8e0c5db60f3b98dd5d161052004a"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1885d2470955f70dfdd33a02e1749613c5a9c5ab855f6db38e0b9389453dce7"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0593822dcdb9483d41f12041ff7c90d4d1033ec0e880bcfaf102919b715f47f1"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:47f6eb74e1ecb5e19a78f4a4228aa24df7fbab3b62d4a625d3f41194a08bd54f"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c8b04a3dbd54de6ccb7604242fe3ad67f2f3ca558f2d33fe19d4b08d90701a89"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-win32.whl", hash = "sha256:8a78dfb198a328bfb38e4308ca8167028920fb747ddcf086ce706fbdd23b2926"},
|
||||
{file = "aiohttp-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:e78da6b55275987cbc89141a1d8e75f5070e577c482dd48bd9123a76a96f0bbb"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c111b3c69060d2bafc446917534150fd049e7aedd6cbf21ba526a5a97b4402a5"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efbdd51872cf170093998c87ccdf3cb5993add3559341a8e5708bcb311934c94"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bfdb41dc6e85d8535b00d73947548a748e9534e8e4fddd2638109ff3fb081df"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd9d334412961125e9f68d5b73c1d0ab9ea3f74a58a475e6b119f5293eee7ba"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35d78076736f4a668d57ade00c65d30a8ce28719d8a42471b2a06ccd1a2e3063"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:824dff4f9f4d0f59d0fa3577932ee9a20e09edec8a2f813e1d6b9f89ced8293f"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52b8b4e06fc15519019e128abedaeb56412b106ab88b3c452188ca47a25c4093"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eae569fb1e7559d4f3919965617bb39f9e753967fae55ce13454bec2d1c54f09"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:69b97aa5792428f321f72aeb2f118e56893371f27e0b7d05750bcad06fc42ca1"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d79aad0ad4b980663316f26d9a492e8fab2af77c69c0f33780a56843ad2f89e"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:d6577140cd7db19e430661e4b2653680194ea8c22c994bc65b7a19d8ec834403"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:9860d455847cd98eb67897f5957b7cd69fbcb436dd3f06099230f16a66e66f79"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69ff36d3f8f5652994e08bd22f093e11cfd0444cea310f92e01b45a4e46b624e"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-win32.whl", hash = "sha256:e27d3b5ed2c2013bce66ad67ee57cbf614288bda8cdf426c8d8fe548316f1b5f"},
|
||||
{file = "aiohttp-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d6a67e26daa686a6fbdb600a9af8619c80a332556245fa8e86c747d226ab1a1e"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c5ff8ff44825736a4065d8544b43b43ee4c6dd1530f3a08e6c0578a813b0aa35"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d12a244627eba4e9dc52cbf924edef905ddd6cafc6513849b4876076a6f38b0e"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dcad56c8d8348e7e468899d2fb3b309b9bc59d94e6db08710555f7436156097f"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7e69a7fd4b5ce419238388e55abd220336bd32212c673ceabc57ccf3d05b55"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4870cb049f10d7680c239b55428916d84158798eb8f353e74fa2c98980dcc0b"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2feaf1b7031ede1bc0880cec4b0776fd347259a723d625357bb4b82f62687b"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939393e8c3f0a5bcd33ef7ace67680c318dc2ae406f15e381c0054dd658397de"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d2334e387b2adcc944680bebcf412743f2caf4eeebd550f67249c1c3696be04"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e0198ea897680e480845ec0ffc5a14e8b694e25b3f104f63676d55bf76a82f1a"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e40d2cd22914d67c84824045861a5bb0fb46586b15dfe4f046c7495bf08306b2"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:aba80e77c227f4234aa34a5ff2b6ff30c5d6a827a91d22ff6b999de9175d71bd"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:fb68dc73bc8ac322d2e392a59a9e396c4f35cb6fdbdd749e139d1d6c985f2527"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f3460a92638dce7e47062cf088d6e7663adb135e936cb117be88d5e6c48c9d53"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-win32.whl", hash = "sha256:32dc814ddbb254f6170bca198fe307920f6c1308a5492f049f7f63554b88ef36"},
|
||||
{file = "aiohttp-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:63f41a909d182d2b78fe3abef557fcc14da50c7852f70ae3be60e83ff64edba5"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c3770365675f6be220032f6609a8fbad994d6dcf3ef7dbcf295c7ee70884c9af"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:305edae1dea368ce09bcb858cf5a63a064f3bff4767dec6fa60a0cc0e805a1d3"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f121900131d116e4a93b55ab0d12ad72573f967b100e49086e496a9b24523ea"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b71e614c1ae35c3d62a293b19eface83d5e4d194e3eb2fabb10059d33e6e8cbf"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:419f009fa4cfde4d16a7fc070d64f36d70a8d35a90d71aa27670bba2be4fd039"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b39476ee69cfe64061fd77a73bf692c40021f8547cda617a3466530ef63f947"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b33f34c9c7decdb2ab99c74be6443942b730b56d9c5ee48fb7df2c86492f293c"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c78700130ce2dcebb1a8103202ae795be2fa8c9351d0dd22338fe3dac74847d9"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:268ba22d917655d1259af2d5659072b7dc11b4e1dc2cb9662fdd867d75afc6a4"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:17e7c051f53a0d2ebf33013a9cbf020bb4e098c4bc5bce6f7b0c962108d97eab"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7be99f4abb008cb38e144f85f515598f4c2c8932bf11b65add0ff59c9c876d99"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d58a54d6ff08d2547656356eea8572b224e6f9bbc0cf55fa9966bcaac4ddfb10"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7673a76772bda15d0d10d1aa881b7911d0580c980dbd16e59d7ba1422b2d83cd"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-win32.whl", hash = "sha256:e4370dda04dc8951012f30e1ce7956a0a226ac0714a7b6c389fb2f43f22a250e"},
|
||||
{file = "aiohttp-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:eb30c4510a691bb87081192a394fb661860e75ca3896c01c6d186febe7c88530"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:84e90494db7df3be5e056f91412f9fa9e611fbe8ce4aaef70647297f5943b276"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d4845f8501ab28ebfdbeab980a50a273b415cf69e96e4e674d43d86a464df9d"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69046cd9a2a17245c4ce3c1f1a4ff8c70c7701ef222fce3d1d8435f09042bba1"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b73a06bafc8dcc508420db43b4dd5850e41e69de99009d0351c4f3007960019"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:418bb0038dfafeac923823c2e63226179976c76f981a2aaad0ad5d51f2229bca"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a8f241456b6c2668374d5d28398f8e8cdae4cce568aaea54e0f39359cd928d"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935c369bf8acc2dc26f6eeb5222768aa7c62917c3554f7215f2ead7386b33748"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4e48c8752d14ecfb36d2ebb3d76d614320570e14de0a3aa7a726ff150a03c"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:916b0417aeddf2c8c61291238ce25286f391a6acb6f28005dd9ce282bd6311b6"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9b6787b6d0b3518b2ee4cbeadd24a507756ee703adbac1ab6dc7c4434b8c572a"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:221204dbda5ef350e8db6287937621cf75e85778b296c9c52260b522231940ed"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:10afd99b8251022ddf81eaed1d90f5a988e349ee7d779eb429fb07b670751e8c"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2506d9f7a9b91033201be9ffe7d89c6a54150b0578803cce5cb84a943d075bc3"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-win32.whl", hash = "sha256:e571fdd9efd65e86c6af2f332e0e95dad259bfe6beb5d15b3c3eca3a6eb5d87b"},
|
||||
{file = "aiohttp-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:7d29dd5319d20aa3b7749719ac9685fbd926f71ac8c77b2477272725f882072d"},
|
||||
{file = "aiohttp-3.9.4.tar.gz", hash = "sha256:6ff71ede6d9a5a58cfb7b6fffc83ab5d4a63138276c771ac91ceaaddf5459644"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -188,35 +188,47 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
|
|||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.12.0"
|
||||
version = "24.3.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
|
||||
{file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
|
||||
{file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
|
||||
{file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
|
||||
{file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
|
||||
{file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
|
||||
{file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
|
||||
{file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
|
||||
{file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
|
||||
{file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
|
||||
{file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
|
||||
{file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
|
||||
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
|
||||
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
|
||||
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
|
||||
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
|
||||
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
|
||||
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
|
||||
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
|
||||
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
|
||||
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
|
||||
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
|
||||
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
|
||||
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
|
@ -394,21 +406,6 @@ files = [
|
|||
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cleo"
|
||||
version = "2.1.0"
|
||||
description = "Cleo allows you to create beautiful and testable command-line interfaces."
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"},
|
||||
{file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
crashtest = ">=0.4.1,<0.5.0"
|
||||
rapidfuzz = ">=3.0.0,<4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
|
@ -451,17 +448,6 @@ files = [
|
|||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crashtest"
|
||||
version = "0.4.1"
|
||||
description = "Manage Python errors with ease"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"},
|
||||
{file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deezer-py"
|
||||
version = "1.3.6"
|
||||
|
@ -578,13 +564,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
version = "3.7"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -843,82 +829,89 @@ windows-curses = {version = ">=2.2.0,<3.0.0", markers = "sys_platform == \"win32
|
|||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "9.5.0"
|
||||
version = "10.3.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"},
|
||||
{file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"},
|
||||
{file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"},
|
||||
{file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"},
|
||||
{file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"},
|
||||
{file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"},
|
||||
{file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"},
|
||||
{file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"},
|
||||
{file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"},
|
||||
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"},
|
||||
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"},
|
||||
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"},
|
||||
{file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"},
|
||||
{file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"},
|
||||
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"},
|
||||
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"},
|
||||
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"},
|
||||
{file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"},
|
||||
{file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
|
||||
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||
fpx = ["olefile"]
|
||||
mic = ["olefile"]
|
||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||
typing = ["typing-extensions"]
|
||||
xmp = ["defusedxml"]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
|
@ -1040,43 +1033,43 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "pycryptodomex"
|
||||
version = "3.19.0"
|
||||
version = "3.19.1"
|
||||
description = "Cryptographic library for Python"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
files = [
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff64fd720def623bf64d8776f8d0deada1cc1bf1ec3c1f9d6f5bb5bd098d034f"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:61056a1fd3254f6f863de94c233b30dd33bc02f8c935b2000269705f1eeeffa4"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:258c4233a3fe5a6341780306a36c6fb072ef38ce676a6d41eec3e591347919e8"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e45bb4635b3c4e0a00ca9df75ef6295838c85c2ac44ad882410cb631ed1eeaa"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:a12144d785518f6491ad334c75ccdc6ad52ea49230b4237f319dbb7cef26f464"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27m-win32.whl", hash = "sha256:1789d89f61f70a4cd5483d4dfa8df7032efab1118f8b9894faae03c967707865"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27m-win_amd64.whl", hash = "sha256:eb2fc0ec241bf5e5ef56c8fbec4a2634d631e4c4f616a59b567947a0f35ad83c"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c9a68a2f7bd091ccea54ad3be3e9d65eded813e6d79fdf4cc3604e26cdd6384f"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:8df69e41f7e7015a90b94d1096ec3d8e0182e73449487306709ec27379fff761"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:917033016ecc23c8933205585a0ab73e20020fdf671b7cd1be788a5c4039840b"},
|
||||
{file = "pycryptodomex-3.19.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:e8e5ecbd4da4157889fce8ba49da74764dd86c891410bfd6b24969fa46edda51"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-win32.whl", hash = "sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002"},
|
||||
{file = "pycryptodomex-3.19.0-cp35-abi3-win_amd64.whl", hash = "sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc"},
|
||||
{file = "pycryptodomex-3.19.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2"},
|
||||
{file = "pycryptodomex-3.19.0-pp27-pypy_73-win32.whl", hash = "sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c"},
|
||||
{file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2"},
|
||||
{file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975"},
|
||||
{file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb"},
|
||||
{file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec"},
|
||||
{file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53"},
|
||||
{file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905"},
|
||||
{file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40"},
|
||||
{file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644"},
|
||||
{file = "pycryptodomex-3.19.0.tar.gz", hash = "sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b5c336dc698650283ad06f8c0237a984087d0af9f403ff21d633507335628156"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c9cb88ed323be1aa642b3c17cd5caa1a03c3a8fbad092d48ecefe88e328ffae3"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:0b42e2743893f386dfb58fe24a4c8be5305c3d1c825d5f23d9e63fd0700d1110"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10c2eed4efdfa084b602ab922e699a0a2ba82053baebfc8afcaf27489def7955"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e94a7e986b117b72e9472f8eafdd81748dafff30815401f9760f759f1debe9ef"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27m-win32.whl", hash = "sha256:23707238b024b36c35dd3428f5af6c1f0c5ef54c21e387a2063633717699b8b2"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27m-win_amd64.whl", hash = "sha256:c1ae2fb8d5d6771670436dcc889b293e363c97647a6d31c21eebc12b7b760010"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:d7a77391fd351ff1bdf8475558ddc6e92950218cb905419ee14aa02f370f1054"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c9332b04bf3f838327087b028f690f4ddb9341eb014a0221e79b9c19a77f7555"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beb5f0664f49b6093da179ee8e27c1d670779f50b9ece0886ce491bb8bd63728"},
|
||||
{file = "pycryptodomex-3.19.1-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:d45d0d35a238d838b872598fa865bbfb31aaef9aeeda77c68b04ef79f9a469dc"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ed3bdda44cc05dd13eee697ab9bea6928531bb7b218e68e66d0d3eb2ebab043e"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ae75eea2e908383fd4c659fdcfe9621a72869e3e3ee73904227e93b7f7b80b54"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:371bbe0be17b4dd8cc0c2f378d75ea33f00d5a39884c09a672016ac40145a5fa"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96000b837bcd8e3bf86b419924a056c978e45027281e4318650c81c25a3ef6cc"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:011e859026ecbd15b8e720e8992361186e582cf726c50bde6ff8c0c05e820ddf"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:76414d39df6b45bcc4f38cf1ba2031e0f4b8e99d1ba3c2eee31ffe1b9f039733"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1c04cfff163c05d033bf28e3c4429d8222796738c7b6c1638b9d7090b904611e"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:de5a43901e47e7a6938490fc5de3074f6e35c8b481a75b227c0d24d6099bd41d"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-win32.whl", hash = "sha256:f24f49fc6bd706d87048654d6be6c7c967d6836d4879e3a7c439275fab9948ad"},
|
||||
{file = "pycryptodomex-3.19.1-cp35-abi3-win_amd64.whl", hash = "sha256:f8b3d9e7c17c1ffc1fa5b11c0bbab8a5df3de8596bb32ad30281b21e5ede4bf5"},
|
||||
{file = "pycryptodomex-3.19.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:ac562e239d98cfef763866c0aee4586affb0d58c592202f06c87241af99db241"},
|
||||
{file = "pycryptodomex-3.19.1-pp27-pypy_73-win32.whl", hash = "sha256:39eb1f82ac3ba3e39d866f38e480e8fa53fcdd22260340f05f54a8188d47d510"},
|
||||
{file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bc4b7bfaac56e6dfd62044847443a3d110c7abea7fcb0d68c1aea64ed3a6697"},
|
||||
{file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dffe067d5fff14dba4d18ff7d459cc2a47576d82dafbff13a8f1199c3353e41"},
|
||||
{file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab7941c2ff53eb63cb26252770e4f14386d79ce07baeffbf98a1323c1646545"},
|
||||
{file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3f3c58971784fba0e014bc3f8aed1197b86719631e1b597d36d7354be5598312"},
|
||||
{file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5ca98de2e5ac100e57a7116309723360e8f799f722509e376dc396cdf65eec9c"},
|
||||
{file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8a97b1acd36e9ce9d4067d94a8be99c458f0eb8070828639302a95cfcf0770b"},
|
||||
{file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62f51a63d73153482729904381dd2de86800b0733a8814ee8f072fa73e5c92fb"},
|
||||
{file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9919a1edd2a83c4dfb69f1d8a4c0c5efde7147ef15b07775633372b80c90b5d8"},
|
||||
{file = "pycryptodomex-3.19.1.tar.gz", hash = "sha256:0b7154aff2272962355f8941fd514104a88cb29db2d8f43a29af900d6398eb1c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1163,116 +1156,14 @@ pytest = ">=5.0"
|
|||
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "rapidfuzz"
|
||||
version = "3.5.2"
|
||||
description = "rapid fuzzy string matching"
|
||||
name = "requests"
|
||||
version = "2.32.0"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1a047d6e58833919d742bbc0dfa66d1de4f79e8562ee195007d3eae96635df39"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22877c027c492b7dc7e3387a576a33ed5aad891104aa90da2e0844c83c5493ef"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0f448b0eacbcc416feb634e1232a48d1cbde5e60f269c84e4fb0912f7bbb001"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05146497672f869baf41147d5ec1222788c70e5b8b0cfcd6e95597c75b5b96b"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f2df3968738a38d2a0058b5e721753f5d3d602346a1027b0dde31b0476418f3"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5afc1fcf1830f9bb87d3b490ba03691081b9948a794ea851befd2643069a30c1"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84be69ea65f64fa01e5c4976be9826a5aa949f037508887add42da07420d65d6"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8658c1045766e87e0038323aa38b4a9f49b7f366563271f973c8890a98aa24b5"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:852b3f93c15fce58b8dc668bd54123713bfdbbb0796ba905ea5df99cfd083132"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:12424a06ad9bd0cbf5f7cea1015e78d924a0034a0e75a5a7b39c0703dcd94095"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b4e9ded8e80530bd7205a7a2b01802f934a4695ca9e9fbe1ce9644f5e0697864"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:affb8fe36157c2dc8a7bc45b6a1875eb03e2c49167a1d52789144bdcb7ab3b8c"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1d33a622572d384f4c90b5f7a139328246ab5600141e90032b521c2127bd605"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-win32.whl", hash = "sha256:2cf9f2ed4a97b388cffd48d534452a564c2491f68f4fd5bc140306f774ceb63a"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:6541ffb70097885f7302cd73e2efd77be99841103023c2f9408551f27f45f7a5"},
|
||||
{file = "rapidfuzz-3.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:1dd2542e5103fb8ca46500a979ae14d1609dcba11d2f9fe01e99eec03420e193"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bff7d3127ebc5cd908f3a72f6517f31f5247b84666137556a8fcc5177c560939"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fdfdb3685b631d8efbb6d6d3d86eb631be2b408d9adafcadc11e63e3f9c96dec"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97b043fe8185ec53bb3ff0e59deb89425c0fc6ece6e118939963aab473505801"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a4a7832737f87583f3863dc62e6f56dd4a9fefc5f04a7bdcb4c433a0f36bb1b"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d876dba9a11fcf60dcf1562c5a84ef559db14c2ceb41e1ad2d93cd1dc085889"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa4c0612893716bbb6595066ca9ecb517c982355abe39ba9d1f4ab834ace91ad"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:120316824333e376b88b284724cfd394c6ccfcb9818519eab5d58a502e5533f0"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cdbe8e80cc186d55f748a34393533a052d855357d5398a1ccb71a5021b58e8d"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1062425c8358a547ae5ebad148f2e0f02417716a571b803b0c68e4d552e99d32"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66be181965aff13301dd5f9b94b646ce39d99c7fe2fd5de1656f4ca7fafcb38c"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:53df7aea3cf301633cfa2b4b2c2d2441a87dfc878ef810e5b4eddcd3e68723ad"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:76639dca5eb0afc6424ac5f42d43d3bd342ac710e06f38a8c877d5b96de09589"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:27689361c747b5f7b8a26056bc60979875323f1c3dcaaa9e2fec88f03b20a365"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-win32.whl", hash = "sha256:99c9fc5265566fb94731dc6826f43c5109e797078264e6389a36d47814473692"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:666928ee735562a909d81bd2f63207b3214afd4ca41f790ab3025d066975c814"},
|
||||
{file = "rapidfuzz-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:d55de67c48f06b7772541e8d4c062a2679205799ce904236e2836cb04c106442"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:04e1e02b182283c43c866e215317735e91d22f5d34e65400121c04d5ed7ed859"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:365e544aba3ac13acf1a62cb2e5909ad2ba078d0bfc7d69b1f801dfd673b9782"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b61f77d834f94b0099fa9ed35c189b7829759d4e9c2743697a130dd7ba62259f"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43fb368998b9703fa8c63db292a8ab9e988bf6da0c8a635754be8e69da1e7c1d"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25510b5d142c47786dbd27cfd9da7cae5bdea28d458379377a3644d8460a3404"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf3093443751e5a419834162af358d1e31dec75f84747a91dbbc47b2c04fc085"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fbaf546f15a924613f89d609ff66b85b4f4c2307ac14d93b80fe1025b713138"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32d580df0e130ed85400ff77e1c32d965e9bc7be29ac4072ab637f57e26d29fb"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:358a0fbc49343de20fee8ebdb33c7fa8f55a9ff93ff42d1ffe097d2caa248f1b"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fb379ac0ddfc86c5542a225d194f76ed468b071b6f79ff57c4b72e635605ad7d"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7fb21e182dc6d83617e88dea002963d5cf99cf5eabbdbf04094f503d8fe8d723"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c04f9f1310ce414ab00bdcbf26d0906755094bfc59402cb66a7722c6f06d70b2"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6da61cc38c1a95efc5edcedf258759e6dbab73191651a28c5719587f32a56ad"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-win32.whl", hash = "sha256:f823fd1977071486739f484e27092765d693da6beedaceece54edce1dfeec9b2"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:a8162d81486de85ab1606e48e076431b66d44cf431b2b678e9cae458832e7147"},
|
||||
{file = "rapidfuzz-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:dfc63fabb7d8da8483ca836bae7e55766fe39c63253571e103c034ba8ea80950"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:df8fae2515a1e4936affccac3e7d506dd904de5ff82bc0b1433b4574a51b9bfb"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd6384780c2a16097d47588844cd677316a90e0f41ef96ff485b62d58de79dcf"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:467a4d730ae3bade87dba6bd769e837ab97e176968ce20591fe8f7bf819115b1"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54576669c1502b751b534bd76a4aeaaf838ed88b30af5d5c1b7d0a3ca5d4f7b5"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abafeb82f85a651a9d6d642a33dc021606bc459c33e250925b25d6b9e7105a2e"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73e14617a520c0f1bc15eb78c215383477e5ca70922ecaff1d29c63c060e04ca"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7cdf92116e9dfe40da17f921cdbfa0039dde9eb158914fa5f01b1e67a20b19cb"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1962d5ccf8602589dbf8e85246a0ee2b4050d82fade1568fb76f8a4419257704"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:db45028eae2fda7a24759c69ebeb2a7fbcc1a326606556448ed43ee480237a3c"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b685abb8b6d97989f6c69556d7934e0e533aa8822f50b9517ff2da06a1d29f23"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:40139552961018216b8cd88f6df4ecbbe984f907a62a5c823ccd907132c29a14"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0fef4705459842ef8f79746d6f6a0b5d2b6a61a145d7d8bbe10b2e756ea337c8"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6b2ad5516f7068c7d9cbcda8ac5906c589e99bc427df2e1050282ee2d8bc2d58"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-win32.whl", hash = "sha256:2da3a24c2f7dfca7f26ba04966b848e3bbeb93e54d899908ff88dfe3e1def9dc"},
|
||||
{file = "rapidfuzz-3.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:e3f2be79d4114d01f383096dbee51b57df141cb8b209c19d0cf65f23a24e75ba"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:089a7e96e5032821af5964d8457fcb38877cc321cdd06ad7c5d6e3d852264cb9"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75d8a52bf8d1aa2ac968ae4b21b83b94fc7e5ea3dfbab34811fc60f32df505b2"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2bacce6bbc0362f0789253424269cc742b1f45e982430387db3abe1d0496e371"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5fd627e604ddc02db2ddb9ddc4a91dd92b7a6d6378fcf30bb37b49229072b89"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2e8b369f23f00678f6e673572209a5d3b0832f4991888e3df97af7b8b9decf3"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c29958265e4c2b937269e804b8a160c027ee1c2627d6152655008a8b8083630e"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00be97f9219355945c46f37ac9fa447046e6f7930f7c901e5d881120d1695458"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada0d8d57e0f556ef38c24fee71bfe8d0db29c678bff2acd1819fc1b74f331c2"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de89585268ed8ee44e80126814cae63ff6b00d08416481f31b784570ef07ec59"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:908ff2de9c442b379143d1da3c886c63119d4eba22986806e2533cee603fe64b"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:54f0061028723c026020f5bb20649c22bc8a0d9f5363c283bdc5901d4d3bff01"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b581107ec0c610cdea48b25f52030770be390db4a9a73ca58b8d70fa8a5ec32e"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1d5a686ea258931aaa38019204bdc670bbe14b389a230b1363d84d6cf4b9dc38"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-win32.whl", hash = "sha256:97f811ca7709c6ee8c0b55830f63b3d87086f4abbcbb189b4067e1cd7014db7b"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:58ee34350f8c292dd24a050186c0e18301d80da904ef572cf5fda7be6a954929"},
|
||||
{file = "rapidfuzz-3.5.2-cp39-cp39-win_arm64.whl", hash = "sha256:c5075ce7b9286624cafcf36720ef1cfb2946d75430b87cb4d1f006e82cd71244"},
|
||||
{file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af5221e4f7800db3e84c46b79dba4112e3b3cc2678f808bdff4fcd2487073846"},
|
||||
{file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8501d7875b176930e6ed9dbc1bc35adb37ef312f6106bd6bb5c204adb90160ac"},
|
||||
{file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e414e1ca40386deda4291aa2d45062fea0fbaa14f95015738f8bb75c4d27f862"},
|
||||
{file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2059cd73b7ea779a9307d7a78ed743f0e3d33b88ccdcd84569abd2953cd859f"},
|
||||
{file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:58e3e21f6f13a7cca265cce492bc797425bd4cb2025fdd161a9e86a824ad65ce"},
|
||||
{file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b847a49377e64e92e11ef3d0a793de75451526c83af015bdafdd5d04de8a058a"},
|
||||
{file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a42c7a8c62b29c4810e39da22b42524295fcb793f41c395c2cb07c126b729e83"},
|
||||
{file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b5166be86e09e011e92d9862b1fe64c4c7b9385f443fb535024e646d890460"},
|
||||
{file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f808dcb0088a7a496cc9895e66a7b8de55ffea0eb9b547c75dfb216dd5f76ed"},
|
||||
{file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d4b05a8f4ab7e7344459394094587b033fe259eea3a8720035e8ba30e79ab39b"},
|
||||
{file = "rapidfuzz-3.5.2.tar.gz", hash = "sha256:9e9b395743e12c36a3167a3a9fd1b4e11d92fb0aa21ec98017ee6df639ed385e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
full = ["numpy"]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.31.0"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
||||
{file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"},
|
||||
{file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1378,26 +1269,6 @@ files = [
|
|||
{file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.1"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
|
||||
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
|
||||
notebook = ["ipywidgets (>=6)"]
|
||||
slack = ["slack-sdk"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "types-click"
|
||||
version = "7.1.8"
|
||||
|
@ -1420,6 +1291,17 @@ files = [
|
|||
{file = "types_Pillow-8.3.11-py3-none-any.whl", hash = "sha256:998189334e616b1dd42c9634669efbf726184039e96e9a23ec95246e0ecff3fc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
|
||||
{file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.1.0"
|
||||
|
@ -1565,4 +1447,4 @@ multidict = ">=4.0"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10 <4.0"
|
||||
content-hash = "65ad2363e3856aed80ba967c8d53e5435c8f4230e27e1178bf832866fb91e1c3"
|
||||
content-hash = "5189d2cd4355c254b15f0f038d4584b146a6297726ccd6d3a0d0174b8cf5616f"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "streamrip"
|
||||
version = "2.0"
|
||||
version = "2.0.5"
|
||||
description = "A fast, all-in-one music ripper for Qobuz, Deezer, Tidal, and SoundCloud"
|
||||
authors = ["nathom <nathanthomas707@gmail.com>"]
|
||||
license = "GPL-3.0-only"
|
||||
|
@ -10,12 +10,10 @@ repository = "https://github.com/nathom/streamrip"
|
|||
include = ["src/config.toml"]
|
||||
keywords = ["hi-res", "free", "music", "download"]
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: GNU General Public License (GPL)",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
packages = [
|
||||
{ include = "streamrip" }
|
||||
"License :: OSI Approved :: GNU General Public License (GPL)",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
packages = [{ include = "streamrip" }]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
rip = "streamrip.rip:rip"
|
||||
|
@ -23,16 +21,14 @@ rip = "streamrip.rip:rip"
|
|||
[tool.poetry.dependencies]
|
||||
python = ">=3.10 <4.0"
|
||||
mutagen = "^1.45.1"
|
||||
tqdm = "^4.61.1"
|
||||
tomlkit = "^0.7.2"
|
||||
pathvalidate = "^2.4.1"
|
||||
simple-term-menu = {version = "^1.2.1", platform = 'darwin|linux'}
|
||||
pick = {version = "^2", platform = 'win32|cygwin'}
|
||||
windows-curses = {version = "^2.2.0", platform = 'win32|cygwin'}
|
||||
Pillow = "^9.0.0"
|
||||
simple-term-menu = { version = "^1.2.1", platform = 'darwin|linux' }
|
||||
pick = { version = "^2", platform = 'win32|cygwin' }
|
||||
windows-curses = { version = "^2.2.0", platform = 'win32|cygwin' }
|
||||
Pillow = ">=9,<11"
|
||||
deezer-py = "1.3.6"
|
||||
pycryptodomex = "^3.10.1"
|
||||
cleo = "^2.0"
|
||||
appdirs = "^1.4.4"
|
||||
m3u8 = "^0.9.0"
|
||||
aiofiles = "^0.7"
|
||||
|
@ -51,7 +47,7 @@ click-help-colors = "^0.9.2"
|
|||
types-click = "^7.1.2"
|
||||
types-Pillow = "^8.3.1"
|
||||
ruff = "^0.1"
|
||||
black = "^22"
|
||||
black = "^24"
|
||||
isort = "^5.9.3"
|
||||
flake8 = "^3.9.2"
|
||||
setuptools = "^67.4.0"
|
||||
|
@ -60,7 +56,7 @@ pytest = "^7.4"
|
|||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
addopts = "-ra -q"
|
||||
testpaths = [ "tests" ]
|
||||
testpaths = ["tests"]
|
||||
log_level = "DEBUG"
|
||||
asyncio_mode = 'auto'
|
||||
log_cli = true
|
||||
|
|
|
@ -2,3 +2,4 @@ from . import converter, db, exceptions, media, metadata
|
|||
from .config import Config
|
||||
|
||||
__all__ = ["Config", "media", "metadata", "converter", "db", "exceptions"]
|
||||
__version__ = "2.0.5"
|
||||
|
|
|
@ -129,7 +129,9 @@ class DeezerClient(Client):
|
|||
raise Exception(f"Invalid media type {media_type}")
|
||||
|
||||
response = search_function(query, limit=limit) # type: ignore
|
||||
return [response]
|
||||
if response["total"] > 0:
|
||||
return [response]
|
||||
return []
|
||||
|
||||
async def get_downloadable(
|
||||
self,
|
||||
|
@ -137,6 +139,10 @@ class DeezerClient(Client):
|
|||
quality: int = 2,
|
||||
is_retry: bool = False,
|
||||
) -> DeezerDownloadable:
|
||||
if item_id is None:
|
||||
raise NonStreamableError(
|
||||
"No item id provided. This can happen when searching for fallback songs.",
|
||||
)
|
||||
# TODO: optimize such that all of the ids are requested at once
|
||||
dl_info: dict = {"quality": quality, "id": item_id}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ from typing import Any, Callable, Optional
|
|||
import aiofiles
|
||||
import aiohttp
|
||||
import m3u8
|
||||
import requests
|
||||
from Cryptodome.Cipher import AES, Blowfish
|
||||
from Cryptodome.Util import Counter
|
||||
|
||||
|
@ -36,13 +37,38 @@ def generate_temp_path(url: str):
|
|||
)
|
||||
|
||||
|
||||
async def fast_async_download(path, url, headers, callback):
|
||||
"""Synchronous download with yield for every 1MB read.
|
||||
|
||||
Using aiofiles/aiohttp resulted in a yield to the event loop for every 1KB,
|
||||
which made file downloads CPU-bound. This resulted in a ~10MB max total download
|
||||
speed. This fixes the issue by only yielding to the event loop for every 1MB read.
|
||||
"""
|
||||
chunk_size: int = 2**17 # 131 KB
|
||||
counter = 0
|
||||
yield_every = 8 # 1 MB
|
||||
with open(path, "wb") as file: # noqa: ASYNC101
|
||||
with requests.get( # noqa: ASYNC100
|
||||
url,
|
||||
headers=headers,
|
||||
allow_redirects=True,
|
||||
stream=True,
|
||||
) as resp:
|
||||
for chunk in resp.iter_content(chunk_size=chunk_size):
|
||||
file.write(chunk)
|
||||
callback(len(chunk))
|
||||
if counter % yield_every == 0:
|
||||
await asyncio.sleep(0)
|
||||
counter += 1
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Downloadable(ABC):
|
||||
session: aiohttp.ClientSession
|
||||
url: str
|
||||
extension: str
|
||||
chunk_size = 1024
|
||||
_size: Optional[int] = None
|
||||
source: str = "Unknown"
|
||||
_size_base: Optional[int] = None
|
||||
|
||||
async def download(self, path: str, callback: Callable[[int], Any]):
|
||||
await self._download(path, callback)
|
||||
|
@ -57,6 +83,14 @@ class Downloadable(ABC):
|
|||
self._size = int(content_length)
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def _size(self):
|
||||
return self._size_base
|
||||
|
||||
@_size.setter
|
||||
def _size(self, v):
|
||||
self._size_base = v
|
||||
|
||||
@abstractmethod
|
||||
async def _download(self, path: str, callback: Callable[[int], None]):
|
||||
raise NotImplementedError
|
||||
|
@ -65,30 +99,31 @@ class Downloadable(ABC):
|
|||
class BasicDownloadable(Downloadable):
|
||||
"""Just downloads a URL."""
|
||||
|
||||
def __init__(self, session: aiohttp.ClientSession, url: str, extension: str):
|
||||
def __init__(
|
||||
self,
|
||||
session: aiohttp.ClientSession,
|
||||
url: str,
|
||||
extension: str,
|
||||
source: str | None = None,
|
||||
):
|
||||
self.session = session
|
||||
self.url = url
|
||||
self.extension = extension
|
||||
self._size = None
|
||||
self.source: str = source or "Unknown"
|
||||
|
||||
async def _download(self, path: str, callback: Callable[[int], None]):
|
||||
async with self.session.get(self.url, allow_redirects=True) as response:
|
||||
response.raise_for_status()
|
||||
async with aiofiles.open(path, "wb") as file:
|
||||
async for chunk in response.content.iter_chunked(self.chunk_size):
|
||||
await file.write(chunk)
|
||||
# typically a bar.update()
|
||||
callback(len(chunk))
|
||||
async def _download(self, path: str, callback):
|
||||
await fast_async_download(path, self.url, self.session.headers, callback)
|
||||
|
||||
|
||||
class DeezerDownloadable(Downloadable):
|
||||
is_encrypted = re.compile("/m(?:obile|edia)/")
|
||||
chunk_size = 2048 * 3
|
||||
|
||||
def __init__(self, session: aiohttp.ClientSession, info: dict):
|
||||
logger.debug("Deezer info for downloadable: %s", info)
|
||||
self.session = session
|
||||
self.url = info["url"]
|
||||
self.source: str = "deezer"
|
||||
max_quality_available = max(
|
||||
i for i, size in enumerate(info["quality_to_size"]) if size > 0
|
||||
)
|
||||
|
@ -119,11 +154,9 @@ class DeezerDownloadable(Downloadable):
|
|||
|
||||
if self.is_encrypted.search(self.url) is None:
|
||||
logger.debug(f"Deezer file at {self.url} not encrypted.")
|
||||
async with aiofiles.open(path, "wb") as file:
|
||||
async for chunk in resp.content.iter_chunked(self.chunk_size):
|
||||
await file.write(chunk)
|
||||
# typically a bar.update()
|
||||
callback(len(chunk))
|
||||
await fast_async_download(
|
||||
path, self.url, self.session.headers, callback
|
||||
)
|
||||
else:
|
||||
blowfish_key = self._generate_blowfish_key(self.id)
|
||||
logger.debug(
|
||||
|
@ -133,29 +166,24 @@ class DeezerDownloadable(Downloadable):
|
|||
blowfish_key,
|
||||
)
|
||||
|
||||
assert self.chunk_size == 2048 * 3
|
||||
buf = bytearray()
|
||||
async for data, _ in resp.content.iter_chunks():
|
||||
buf += data
|
||||
callback(len(data))
|
||||
|
||||
# Write data from server to tempfile because there's no
|
||||
# efficient way to guarantee a fixed chunk size for all iterations
|
||||
# in async
|
||||
async with aiofiles.tempfile.TemporaryFile("wb+") as tmp:
|
||||
async for chunk in resp.content.iter_chunks():
|
||||
data, _ = chunk
|
||||
await tmp.write(data)
|
||||
callback(len(data))
|
||||
|
||||
await tmp.seek(0)
|
||||
async with aiofiles.open(path, "wb") as audio:
|
||||
while chunk := await tmp.read(self.chunk_size):
|
||||
if len(chunk) >= 2048:
|
||||
decrypted_chunk = (
|
||||
self._decrypt_chunk(blowfish_key, chunk[:2048])
|
||||
+ chunk[2048:]
|
||||
)
|
||||
else:
|
||||
decrypted_chunk = chunk
|
||||
|
||||
await audio.write(decrypted_chunk)
|
||||
encrypt_chunk_size = 3 * 2048
|
||||
async with aiofiles.open(path, "wb") as audio:
|
||||
buflen = len(buf)
|
||||
for i in range(0, buflen, encrypt_chunk_size):
|
||||
data = buf[i : min(i + encrypt_chunk_size, buflen)]
|
||||
if len(data) >= 2048:
|
||||
decrypted_chunk = (
|
||||
self._decrypt_chunk(blowfish_key, data[:2048])
|
||||
+ data[2048:]
|
||||
)
|
||||
else:
|
||||
decrypted_chunk = data
|
||||
await audio.write(decrypted_chunk)
|
||||
|
||||
@staticmethod
|
||||
def _decrypt_chunk(key, data):
|
||||
|
@ -199,8 +227,9 @@ class TidalDownloadable(Downloadable):
|
|||
restrictions,
|
||||
):
|
||||
self.session = session
|
||||
self.source = "tidal"
|
||||
codec = codec.lower()
|
||||
if codec == "flac":
|
||||
if codec in ("flac", "mqa"):
|
||||
self.extension = "flac"
|
||||
else:
|
||||
self.extension = "m4a"
|
||||
|
@ -217,7 +246,7 @@ class TidalDownloadable(Downloadable):
|
|||
)
|
||||
self.url = url
|
||||
self.enc_key = encryption_key
|
||||
self.downloadable = BasicDownloadable(session, url, self.extension)
|
||||
self.downloadable = BasicDownloadable(session, url, self.extension, "tidal")
|
||||
|
||||
async def _download(self, path: str, callback):
|
||||
await self.downloadable._download(path, callback)
|
||||
|
@ -276,6 +305,7 @@ class SoundcloudDownloadable(Downloadable):
|
|||
def __init__(self, session, info: dict):
|
||||
self.session = session
|
||||
self.file_type = info["type"]
|
||||
self.source = "soundcloud"
|
||||
if self.file_type == "mp3":
|
||||
self.extension = "mp3"
|
||||
elif self.file_type == "original":
|
||||
|
@ -291,7 +321,9 @@ class SoundcloudDownloadable(Downloadable):
|
|||
await self._download_original(path, callback)
|
||||
|
||||
async def _download_original(self, path: str, callback):
|
||||
downloader = BasicDownloadable(self.session, self.url, "flac")
|
||||
downloader = BasicDownloadable(
|
||||
self.session, self.url, "flac", source="soundcloud"
|
||||
)
|
||||
await downloader.download(path, callback)
|
||||
self.size = downloader.size
|
||||
engine = converter.FLAC(path)
|
||||
|
|
|
@ -197,14 +197,14 @@ class QobuzClient(Client):
|
|||
|
||||
self.logged_in = True
|
||||
|
||||
async def get_metadata(self, item_id: str, media_type: str):
|
||||
async def get_metadata(self, item: str, media_type: str):
|
||||
if media_type == "label":
|
||||
return await self.get_label(item_id)
|
||||
return await self.get_label(item)
|
||||
|
||||
c = self.config.session.qobuz
|
||||
params = {
|
||||
"app_id": c.app_id,
|
||||
f"{media_type}_id": item_id,
|
||||
f"{media_type}_id": item,
|
||||
# Do these matter?
|
||||
"limit": 500,
|
||||
"offset": 0,
|
||||
|
@ -302,9 +302,9 @@ class QobuzClient(Client):
|
|||
epoint = "playlist/getUserPlaylists"
|
||||
return await self._paginate(epoint, {}, limit=limit)
|
||||
|
||||
async def get_downloadable(self, item_id: str, quality: int) -> Downloadable:
|
||||
async def get_downloadable(self, item: str, quality: int) -> Downloadable:
|
||||
assert self.secret is not None and self.logged_in and 1 <= quality <= 4
|
||||
status, resp_json = await self._request_file_url(item_id, quality, self.secret)
|
||||
status, resp_json = await self._request_file_url(item, quality, self.secret)
|
||||
assert status == 200
|
||||
stream_url = resp_json.get("url")
|
||||
|
||||
|
@ -319,9 +319,7 @@ class QobuzClient(Client):
|
|||
raise NonStreamableError
|
||||
|
||||
return BasicDownloadable(
|
||||
self.session,
|
||||
stream_url,
|
||||
"flac" if quality > 1 else "mp3",
|
||||
self.session, stream_url, "flac" if quality > 1 else "mp3", source="qobuz"
|
||||
)
|
||||
|
||||
async def _paginate(
|
||||
|
|
|
@ -8,6 +8,7 @@ import time
|
|||
import aiohttp
|
||||
|
||||
from ..config import Config
|
||||
from ..exceptions import NonStreamableError
|
||||
from .client import Client
|
||||
from .downloadable import TidalDownloadable
|
||||
|
||||
|
@ -121,7 +122,9 @@ class TidalClient(Client):
|
|||
}
|
||||
assert media_type in ("album", "track", "playlist", "video", "artist")
|
||||
resp = await self._api_request(f"search/{media_type}s", params=params)
|
||||
return [resp]
|
||||
if len(resp["items"]) > 1:
|
||||
return [resp]
|
||||
return []
|
||||
|
||||
async def get_downloadable(self, track_id: str, quality: int):
|
||||
params = {
|
||||
|
@ -319,5 +322,8 @@ class TidalClient(Client):
|
|||
|
||||
async with self.rate_limiter:
|
||||
async with self.session.get(f"{BASE}/{path}", params=params) as resp:
|
||||
if resp.status == 404:
|
||||
logger.warning("TIDAL: track not found", resp)
|
||||
raise NonStreamableError("TIDAL: Track not found")
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""A config class that manages arguments between the config file and CLI."""
|
||||
"""Classes and functions that manage config state."""
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
@ -13,8 +15,13 @@ from tomlkit.toml_document import TOMLDocument
|
|||
logger = logging.getLogger("streamrip")
|
||||
|
||||
APP_DIR = click.get_app_dir("streamrip")
|
||||
os.makedirs(APP_DIR, exist_ok=True)
|
||||
DEFAULT_CONFIG_PATH = os.path.join(APP_DIR, "config.toml")
|
||||
CURRENT_CONFIG_VERSION = "2.0"
|
||||
CURRENT_CONFIG_VERSION = "2.0.6"
|
||||
|
||||
|
||||
class OutdatedConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
@ -180,6 +187,8 @@ class DownloadsConfig:
|
|||
folder: str
|
||||
# Put Qobuz albums in a 'Qobuz' folder, Tidal albums in 'Tidal' etc.
|
||||
source_subdirectories: bool
|
||||
# Put tracks in an album with 2 or more discs into a subfolder named `Disc N`
|
||||
disc_subdirectories: bool
|
||||
# Download (and convert) tracks all at once, instead of sequentially.
|
||||
# If you are converting the tracks, or have fast internet, this will
|
||||
# substantially improve processing speed.
|
||||
|
@ -213,6 +222,7 @@ class CliConfig:
|
|||
@dataclass(slots=True)
|
||||
class MiscConfig:
|
||||
version: str
|
||||
check_for_updates: bool
|
||||
|
||||
|
||||
HOME = Path.home()
|
||||
|
@ -257,7 +267,7 @@ class ConfigData:
|
|||
# TODO: handle the mistake where Windows people forget to escape backslash
|
||||
toml = parse(toml_str)
|
||||
if (v := toml["misc"]["version"]) != CURRENT_CONFIG_VERSION: # type: ignore
|
||||
raise Exception(
|
||||
raise OutdatedConfigError(
|
||||
f"Need to update config from {v} to {CURRENT_CONFIG_VERSION}",
|
||||
)
|
||||
|
||||
|
@ -362,6 +372,26 @@ class Config:
|
|||
self.file.update_toml()
|
||||
toml_file.write(dumps(self.file.toml))
|
||||
|
||||
@staticmethod
|
||||
def _update_file(old_path: str, new_path: str):
|
||||
"""Updates the current config based on a newer config `new_toml`."""
|
||||
with open(new_path) as new_conf:
|
||||
new_toml = parse(new_conf.read())
|
||||
|
||||
toml_set_user_defaults(new_toml)
|
||||
|
||||
with open(old_path) as old_conf:
|
||||
old_toml = parse(old_conf.read())
|
||||
|
||||
update_config(old_toml, new_toml)
|
||||
|
||||
with open(old_path, "w") as f:
|
||||
f.write(dumps(new_toml))
|
||||
|
||||
@classmethod
|
||||
def update_file(cls, path: str):
|
||||
cls._update_file(path, BLANK_CONFIG_PATH)
|
||||
|
||||
@classmethod
|
||||
def defaults(cls):
|
||||
return cls(BLANK_CONFIG_PATH)
|
||||
|
@ -379,9 +409,65 @@ def set_user_defaults(path: str, /):
|
|||
|
||||
with open(path) as f:
|
||||
toml = parse(f.read())
|
||||
|
||||
toml_set_user_defaults(toml)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write(dumps(toml))
|
||||
|
||||
|
||||
def toml_set_user_defaults(toml: TOMLDocument):
|
||||
toml["downloads"]["folder"] = DEFAULT_DOWNLOADS_FOLDER # type: ignore
|
||||
toml["database"]["downloads_path"] = DEFAULT_DOWNLOADS_DB_PATH # type: ignore
|
||||
toml["database"]["failed_downloads_path"] = DEFAULT_FAILED_DOWNLOADS_DB_PATH # type: ignore
|
||||
toml["youtube"]["video_downloads_folder"] = DEFAULT_YOUTUBE_VIDEO_DOWNLOADS_FOLDER # type: ignore
|
||||
with open(path, "w") as f:
|
||||
f.write(dumps(toml))
|
||||
|
||||
|
||||
def _get_dict_keys_r(d: dict) -> set[tuple]:
|
||||
"""Get all possible key combinations in nested dicts.
|
||||
|
||||
See tests/test_config.py for example.
|
||||
"""
|
||||
keys = d.keys()
|
||||
ret = set()
|
||||
for cur in keys:
|
||||
val = d[cur]
|
||||
if isinstance(val, dict):
|
||||
ret.update((cur, *remaining) for remaining in _get_dict_keys_r(val))
|
||||
else:
|
||||
ret.add((cur,))
|
||||
return ret
|
||||
|
||||
|
||||
def _nested_get(dictionary, *keys, default=None):
|
||||
return functools.reduce(
|
||||
lambda d, key: d.get(key, default) if isinstance(d, dict) else default,
|
||||
keys,
|
||||
dictionary,
|
||||
)
|
||||
|
||||
|
||||
def _nested_set(dictionary, *keys, val):
|
||||
"""Nested set. Throws exception if keys are invalid."""
|
||||
assert len(keys) > 0
|
||||
final = functools.reduce(lambda d, key: d.get(key), keys[:-1], dictionary)
|
||||
final[keys[-1]] = val
|
||||
|
||||
|
||||
def update_config(old_with_data: dict, new_without_data: dict):
|
||||
"""Used to update config when a new config version is detected.
|
||||
|
||||
All data associated with keys that are shared between the old and
|
||||
new configs are copied from old to new. The remaining keep their default value.
|
||||
|
||||
Assumes that new_without_data contains default config values of the
|
||||
latest version.
|
||||
"""
|
||||
old_keys = _get_dict_keys_r(old_with_data)
|
||||
new_keys = _get_dict_keys_r(new_without_data)
|
||||
common = old_keys.intersection(new_keys)
|
||||
common.discard(("misc", "version"))
|
||||
|
||||
for k in common:
|
||||
old_val = _nested_get(old_with_data, *k)
|
||||
_nested_set(new_without_data, *k, val=old_val)
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
folder = ""
|
||||
# Put Qobuz albums in a 'Qobuz' folder, Tidal albums in 'Tidal' etc.
|
||||
source_subdirectories = false
|
||||
|
||||
# Put tracks in an album with 2 or more discs into a subfolder named `Disc N`
|
||||
disc_subdirectories = true
|
||||
# Download (and convert) tracks all at once, instead of sequentially.
|
||||
# If you are converting the tracks, or have fast internet, this will
|
||||
# substantially improve processing speed.
|
||||
|
@ -161,7 +162,7 @@ add_singles_to_folder = false
|
|||
folder_format = "{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]"
|
||||
# Available keys: "tracknumber", "artist", "albumartist", "composer", "title",
|
||||
# and "albumcomposer", "explicit"
|
||||
track_format = "{tracknumber}. {artist} - {title}{explicit}"
|
||||
track_format = "{tracknumber:02}. {artist} - {title}{explicit}"
|
||||
# Only allow printable ASCII characters in filenames.
|
||||
restrict_characters = false
|
||||
# Truncate the filename if it is greater than this number of characters
|
||||
|
@ -186,4 +187,6 @@ max_search_results = 100
|
|||
|
||||
[misc]
|
||||
# Metadata to identify this config file. Do not change.
|
||||
version = "2.0"
|
||||
version = "2.0.6"
|
||||
# Print a message if a new version of streamrip is available
|
||||
check_for_updates = true
|
||||
|
|
|
@ -121,27 +121,35 @@ class Converter:
|
|||
command.extend(self.ffmpeg_arg.split())
|
||||
|
||||
if self.lossless:
|
||||
aformat = []
|
||||
|
||||
if isinstance(self.sampling_rate, int):
|
||||
sampling_rates = "|".join(
|
||||
sample_rates = "|".join(
|
||||
str(rate) for rate in SAMPLING_RATES if rate <= self.sampling_rate
|
||||
)
|
||||
command.extend(["-af", f"aformat=sample_rates={sampling_rates}"])
|
||||
|
||||
aformat.append(f"sample_rates={sample_rates}")
|
||||
elif self.sampling_rate is not None:
|
||||
raise TypeError(
|
||||
f"Sampling rate must be int, not {type(self.sampling_rate)}",
|
||||
f"Sampling rate must be int, not {type(self.sampling_rate)}"
|
||||
)
|
||||
|
||||
if isinstance(self.bit_depth, int):
|
||||
if int(self.bit_depth) == 16:
|
||||
command.extend(["-sample_fmt", "s16"])
|
||||
elif int(self.bit_depth) in (24, 32):
|
||||
command.extend(["-sample_fmt", "s32p"])
|
||||
else:
|
||||
bit_depths = ["s16p", "s16"]
|
||||
|
||||
if self.bit_depth in (24, 32):
|
||||
bit_depths.extend(["s32p", "s32"])
|
||||
elif self.bit_depth != 16:
|
||||
raise ValueError("Bit depth must be 16, 24, or 32")
|
||||
|
||||
sample_fmts = "|".join(bit_depths)
|
||||
aformat.append(f"sample_fmts={sample_fmts}")
|
||||
elif self.bit_depth is not None:
|
||||
raise TypeError(f"Bit depth must be int, not {type(self.bit_depth)}")
|
||||
|
||||
if aformat:
|
||||
aformat_params = ":".join(aformat)
|
||||
command.extend(["-af", f"aformat={aformat_params}"])
|
||||
|
||||
# automatically overwrite
|
||||
command.extend(["-y", self.tempfile])
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from string import printable
|
||||
|
||||
from pathvalidate import sanitize_filename # type: ignore
|
||||
from pathvalidate import sanitize_filename, sanitize_filepath # type: ignore
|
||||
|
||||
ALLOWED_CHARS = set(printable)
|
||||
|
||||
|
@ -11,3 +11,11 @@ def clean_filename(fn: str, restrict: bool = False) -> str:
|
|||
path = "".join(c for c in path if c in ALLOWED_CHARS)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def clean_filepath(fn: str, restrict: bool = False) -> str:
|
||||
path = str(sanitize_filepath(fn))
|
||||
if restrict:
|
||||
path = "".join(c for c in path if c in ALLOWED_CHARS)
|
||||
|
||||
return path
|
||||
|
|
|
@ -7,6 +7,8 @@ from .. import progress
|
|||
from ..client import Client
|
||||
from ..config import Config
|
||||
from ..db import Database
|
||||
from ..exceptions import NonStreamableError
|
||||
from ..filepath_utils import clean_filepath
|
||||
from ..metadata import AlbumMetadata
|
||||
from ..metadata.util import get_album_track_ids
|
||||
from .artwork import download_artwork
|
||||
|
@ -49,8 +51,20 @@ class PendingAlbum(Pending):
|
|||
db: Database
|
||||
|
||||
async def resolve(self) -> Album | None:
|
||||
resp = await self.client.get_metadata(self.id, "album")
|
||||
meta = AlbumMetadata.from_album_resp(resp, self.client.source)
|
||||
try:
|
||||
resp = await self.client.get_metadata(self.id, "album")
|
||||
except NonStreamableError as e:
|
||||
logger.error(
|
||||
f"Album {self.id} not available to stream on {self.client.source} ({e})",
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
meta = AlbumMetadata.from_album_resp(resp, self.client.source)
|
||||
except Exception as e:
|
||||
logger.error(f"Error building album metadata for {id=}: {e}")
|
||||
return None
|
||||
|
||||
if meta is None:
|
||||
logger.error(
|
||||
f"Album {self.id} not available to stream on {self.client.source}",
|
||||
|
@ -84,6 +98,12 @@ class PendingAlbum(Pending):
|
|||
return Album(meta, pending_tracks, self.config, album_folder, self.db)
|
||||
|
||||
def _album_folder(self, parent: str, meta: AlbumMetadata) -> str:
|
||||
formatter = self.config.session.filepaths.folder_format
|
||||
folder = meta.format_folder_path(formatter)
|
||||
config = self.config.session
|
||||
if config.downloads.source_subdirectories:
|
||||
parent = os.path.join(parent, self.client.source.capitalize())
|
||||
formatter = config.filepaths.folder_format
|
||||
folder = clean_filepath(
|
||||
meta.format_folder_path(formatter), config.filepaths.restrict_characters
|
||||
)
|
||||
|
||||
return os.path.join(parent, folder)
|
||||
|
|
|
@ -7,6 +7,7 @@ from ..client import Client
|
|||
from ..config import Config, QobuzDiscographyFilterConfig
|
||||
from ..console import console
|
||||
from ..db import Database
|
||||
from ..exceptions import NonStreamableError
|
||||
from ..metadata import ArtistMetadata
|
||||
from .album import Album, PendingAlbum
|
||||
from .media import Media, Pending
|
||||
|
@ -180,9 +181,23 @@ class PendingArtist(Pending):
|
|||
config: Config
|
||||
db: Database
|
||||
|
||||
async def resolve(self) -> Artist:
|
||||
resp = await self.client.get_metadata(self.id, "artist")
|
||||
meta = ArtistMetadata.from_resp(resp, self.client.source)
|
||||
async def resolve(self) -> Artist | None:
|
||||
try:
|
||||
resp = await self.client.get_metadata(self.id, "artist")
|
||||
except NonStreamableError as e:
|
||||
logger.error(
|
||||
f"Artist {self.id} not available to stream on {self.client.source} ({e})",
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
meta = ArtistMetadata.from_resp(resp, self.client.source)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error building artist metadata: {e}",
|
||||
)
|
||||
return None
|
||||
|
||||
albums = [
|
||||
PendingAlbum(album_id, self.client, self.config, self.db)
|
||||
for album_id in meta.album_ids()
|
||||
|
|
|
@ -94,7 +94,11 @@ async def download_artwork(
|
|||
if len(downloadables) == 0:
|
||||
return embed_cover_path, saved_cover_path
|
||||
|
||||
await asyncio.gather(*downloadables)
|
||||
try:
|
||||
await asyncio.gather(*downloadables)
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading artwork: {e}")
|
||||
return None, None
|
||||
|
||||
# Update `covers` to reflect the current download state
|
||||
if save_artwork:
|
||||
|
@ -131,7 +135,7 @@ def downscale_image(input_image_path: str, max_dimension: int):
|
|||
# Get the original width and height
|
||||
width, height = image.size
|
||||
|
||||
if max_dimension <= max(width, height):
|
||||
if max_dimension >= max(width, height):
|
||||
return
|
||||
|
||||
# Calculate the new dimensions while maintaining the aspect ratio
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from streamrip.exceptions import NonStreamableError
|
||||
|
||||
from ..client import Client
|
||||
from ..config import Config
|
||||
from ..db import Database
|
||||
|
@ -8,6 +11,8 @@ from ..metadata import LabelMetadata
|
|||
from .album import PendingAlbum
|
||||
from .media import Media, Pending
|
||||
|
||||
logger = logging.getLogger("streamrip")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Label(Media):
|
||||
|
@ -57,9 +62,17 @@ class PendingLabel(Pending):
|
|||
config: Config
|
||||
db: Database
|
||||
|
||||
async def resolve(self) -> Label:
|
||||
resp = await self.client.get_metadata(self.id, "label")
|
||||
meta = LabelMetadata.from_resp(resp, self.client.source)
|
||||
async def resolve(self) -> Label | None:
|
||||
try:
|
||||
resp = await self.client.get_metadata(self.id, "label")
|
||||
except NonStreamableError as e:
|
||||
logger.error(f"Error resolving Label: {e}")
|
||||
return None
|
||||
try:
|
||||
meta = LabelMetadata.from_resp(resp, self.client.source)
|
||||
except Exception as e:
|
||||
logger.error(f"Error resolving Label: {e}")
|
||||
return None
|
||||
albums = [
|
||||
PendingAlbum(album_id, self.client, self.config, self.db)
|
||||
for album_id in meta.album_ids()
|
||||
|
|
|
@ -16,7 +16,7 @@ from ..config import Config
|
|||
from ..console import console
|
||||
from ..db import Database
|
||||
from ..exceptions import NonStreamableError
|
||||
from ..filepath_utils import clean_filename
|
||||
from ..filepath_utils import clean_filepath
|
||||
from ..metadata import (
|
||||
AlbumMetadata,
|
||||
Covers,
|
||||
|
@ -147,11 +147,22 @@ class PendingPlaylist(Pending):
|
|||
db: Database
|
||||
|
||||
async def resolve(self) -> Playlist | None:
|
||||
resp = await self.client.get_metadata(self.id, "playlist")
|
||||
meta = PlaylistMetadata.from_resp(resp, self.client.source)
|
||||
try:
|
||||
resp = await self.client.get_metadata(self.id, "playlist")
|
||||
except NonStreamableError as e:
|
||||
logger.error(
|
||||
f"Playlist {self.id} not available to stream on {self.client.source} ({e})",
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
meta = PlaylistMetadata.from_resp(resp, self.client.source)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating playlist: {e}")
|
||||
return None
|
||||
name = meta.name
|
||||
parent = self.config.session.downloads.folder
|
||||
folder = os.path.join(parent, clean_filename(name))
|
||||
folder = os.path.join(parent, clean_filepath(name))
|
||||
tracks = [
|
||||
PendingPlaylistTrack(
|
||||
id,
|
||||
|
@ -223,7 +234,7 @@ class PendingLastfmPlaylist(Pending):
|
|||
results: list[tuple[str | None, bool]] = await asyncio.gather(*requests)
|
||||
|
||||
parent = self.config.session.downloads.folder
|
||||
folder = os.path.join(parent, clean_filename(playlist_title))
|
||||
folder = os.path.join(parent, clean_filepath(playlist_title))
|
||||
|
||||
pending_tracks = []
|
||||
for pos, (id, from_fallback) in enumerate(results, start=1):
|
||||
|
@ -254,12 +265,20 @@ class PendingLastfmPlaylist(Pending):
|
|||
async def _make_query(
|
||||
self,
|
||||
query: str,
|
||||
s: Status,
|
||||
search_status: Status,
|
||||
callback,
|
||||
) -> tuple[str | None, bool]:
|
||||
"""Try searching for `query` with main source. If that fails, try with next source.
|
||||
"""Search for a track with the main source, and use fallback source
|
||||
if that fails.
|
||||
|
||||
If both fail, return None.
|
||||
Args:
|
||||
----
|
||||
query (str): Query to search
|
||||
s (Status):
|
||||
callback: function to call after each query completes
|
||||
|
||||
Returns: A 2-tuple, where the first element contains the ID if it was found,
|
||||
and the second element is True if the fallback source was used.
|
||||
"""
|
||||
with ExitStack() as stack:
|
||||
# ensure `callback` is always called
|
||||
|
@ -267,7 +286,7 @@ class PendingLastfmPlaylist(Pending):
|
|||
pages = await self.client.search("track", query, limit=1)
|
||||
if len(pages) > 0:
|
||||
logger.debug(f"Found result for {query} on {self.client.source}")
|
||||
s.found += 1
|
||||
search_status.found += 1
|
||||
return (
|
||||
SearchResults.from_pages(self.client.source, "track", pages)
|
||||
.results[0]
|
||||
|
@ -276,13 +295,13 @@ class PendingLastfmPlaylist(Pending):
|
|||
|
||||
if self.fallback_client is None:
|
||||
logger.debug(f"No result found for {query} on {self.client.source}")
|
||||
s.failed += 1
|
||||
search_status.failed += 1
|
||||
return None, False
|
||||
|
||||
pages = await self.fallback_client.search("track", query, limit=1)
|
||||
if len(pages) > 0:
|
||||
logger.debug(f"Found result for {query} on {self.client.source}")
|
||||
s.found += 1
|
||||
search_status.found += 1
|
||||
return (
|
||||
SearchResults.from_pages(
|
||||
self.fallback_client.source,
|
||||
|
@ -294,7 +313,7 @@ class PendingLastfmPlaylist(Pending):
|
|||
), True
|
||||
|
||||
logger.debug(f"No result found for {query} on {self.client.source}")
|
||||
s.failed += 1
|
||||
search_status.failed += 1
|
||||
return None, True
|
||||
|
||||
async def _parse_lastfm_playlist(
|
||||
|
|
|
@ -45,7 +45,32 @@ class Track(Media):
|
|||
await self.downloadable.size(),
|
||||
f"Track {self.meta.tracknumber}",
|
||||
) as callback:
|
||||
await self.downloadable.download(self.download_path, callback)
|
||||
try:
|
||||
await self.downloadable.download(self.download_path, callback)
|
||||
retry = False
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error downloading track '{self.meta.title}', retrying: {e}"
|
||||
)
|
||||
retry = True
|
||||
|
||||
if not retry:
|
||||
return
|
||||
|
||||
with get_progress_callback(
|
||||
self.config.session.cli.progress_bars,
|
||||
await self.downloadable.size(),
|
||||
f"Track {self.meta.tracknumber} (retry)",
|
||||
) as callback:
|
||||
try:
|
||||
await self.downloadable.download(self.download_path, callback)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Persistent error downloading track '{self.meta.title}', skipping: {e}"
|
||||
)
|
||||
self.db.set_failed(
|
||||
self.downloadable.source, "track", self.meta.info.id
|
||||
)
|
||||
|
||||
async def postprocess(self):
|
||||
if self.is_single:
|
||||
|
@ -110,7 +135,12 @@ class PendingTrack(Pending):
|
|||
logger.error(f"Track {self.id} not available for stream on {source}: {e}")
|
||||
return None
|
||||
|
||||
meta = TrackMetadata.from_resp(self.album, source, resp)
|
||||
try:
|
||||
meta = TrackMetadata.from_resp(self.album, source, resp)
|
||||
except Exception as e:
|
||||
logger.error(f"Error building track metadata for {id=}: {e}")
|
||||
return None
|
||||
|
||||
if meta is None:
|
||||
logger.error(f"Track {self.id} not available for stream on {source}")
|
||||
self.db.set_failed(source, "track", self.id)
|
||||
|
@ -118,11 +148,18 @@ class PendingTrack(Pending):
|
|||
|
||||
quality = self.config.session.get_source(source).quality
|
||||
downloadable = await self.client.get_downloadable(self.id, quality)
|
||||
|
||||
downloads_config = self.config.session.downloads
|
||||
if downloads_config.disc_subdirectories and self.album.disctotal > 1:
|
||||
folder = os.path.join(self.folder, f"Disc {meta.discnumber}")
|
||||
else:
|
||||
folder = self.folder
|
||||
|
||||
return Track(
|
||||
meta,
|
||||
downloadable,
|
||||
self.config,
|
||||
self.folder,
|
||||
folder,
|
||||
self.cover_path,
|
||||
self.db,
|
||||
)
|
||||
|
@ -154,7 +191,12 @@ class PendingSingle(Pending):
|
|||
logger.error(f"Error fetching track {self.id}: {e}")
|
||||
return None
|
||||
# Patch for soundcloud
|
||||
album = AlbumMetadata.from_track_resp(resp, self.client.source)
|
||||
try:
|
||||
album = AlbumMetadata.from_track_resp(resp, self.client.source)
|
||||
except Exception as e:
|
||||
logger.error(f"Error building album metadata for track {id=}: {e}")
|
||||
return None
|
||||
|
||||
if album is None:
|
||||
self.db.set_failed(self.client.source, "track", self.id)
|
||||
logger.error(
|
||||
|
@ -162,7 +204,11 @@ class PendingSingle(Pending):
|
|||
)
|
||||
return None
|
||||
|
||||
meta = TrackMetadata.from_resp(album, self.client.source, resp)
|
||||
try:
|
||||
meta = TrackMetadata.from_resp(album, self.client.source, resp)
|
||||
except Exception as e:
|
||||
logger.error(f"Error building track metadata for track {id=}: {e}")
|
||||
return None
|
||||
|
||||
if meta is None:
|
||||
self.db.set_failed(self.client.source, "track", self.id)
|
||||
|
@ -171,12 +217,15 @@ class PendingSingle(Pending):
|
|||
)
|
||||
return None
|
||||
|
||||
quality = getattr(self.config.session, self.client.source).quality
|
||||
config = self.config.session
|
||||
quality = getattr(config, self.client.source).quality
|
||||
assert isinstance(quality, int)
|
||||
folder = os.path.join(
|
||||
self.config.session.downloads.folder,
|
||||
self._format_folder(album),
|
||||
)
|
||||
parent = config.downloads.folder
|
||||
if config.filepaths.add_singles_to_folder:
|
||||
folder = os.path.join(parent, self._format_folder(album))
|
||||
else:
|
||||
folder = parent
|
||||
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
|
||||
embedded_cover_path, downloadable = await asyncio.gather(
|
||||
|
@ -197,6 +246,9 @@ class PendingSingle(Pending):
|
|||
c = self.config.session
|
||||
parent = c.downloads.folder
|
||||
formatter = c.filepaths.folder_format
|
||||
if c.downloads.source_subdirectories:
|
||||
parent = os.path.join(parent, self.client.source.capitalize())
|
||||
|
||||
return os.path.join(parent, meta.format_folder_path(formatter))
|
||||
|
||||
async def _download_cover(self, covers: Covers, folder: str) -> str | None:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Manages the information that will be embeded in the audio file."""
|
||||
|
||||
from . import util
|
||||
from .album import AlbumInfo, AlbumMetadata
|
||||
from .artist import ArtistMetadata
|
||||
|
|
|
@ -5,6 +5,7 @@ import re
|
|||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from ..filepath_utils import clean_filename
|
||||
from .covers import Covers
|
||||
from .util import get_quality_id, safe_get, typed
|
||||
|
||||
|
@ -64,17 +65,19 @@ class AlbumMetadata:
|
|||
def format_folder_path(self, formatter: str) -> str:
|
||||
# Available keys: "albumartist", "title", "year", "bit_depth", "sampling_rate",
|
||||
# "id", and "albumcomposer",
|
||||
|
||||
none_str = "Unknown"
|
||||
info: dict[str, str | int | float] = {
|
||||
"albumartist": self.albumartist,
|
||||
"albumcomposer": self.albumcomposer or none_str,
|
||||
"albumartist": clean_filename(self.albumartist),
|
||||
"albumcomposer": clean_filename(self.albumcomposer or "") or none_str,
|
||||
"bit_depth": self.info.bit_depth or none_str,
|
||||
"id": self.info.id,
|
||||
"sampling_rate": self.info.sampling_rate or none_str,
|
||||
"title": self.album,
|
||||
"title": clean_filename(self.album),
|
||||
"year": self.year,
|
||||
"container": self.info.container,
|
||||
}
|
||||
|
||||
return formatter.format(**info)
|
||||
|
||||
@classmethod
|
||||
|
@ -93,12 +96,12 @@ class AlbumMetadata:
|
|||
else:
|
||||
albumartist = typed(safe_get(resp, "artist", "name"), str)
|
||||
|
||||
albumcomposer = typed(safe_get(resp, "composer", "name"), str | None)
|
||||
albumcomposer = typed(safe_get(resp, "composer", "name", default=""), str)
|
||||
_label = resp.get("label")
|
||||
if isinstance(_label, dict):
|
||||
_label = _label["name"]
|
||||
label = typed(_label, str | None)
|
||||
description = typed(resp.get("description") or None, str | None)
|
||||
label = typed(_label or "", str)
|
||||
description = typed(resp.get("description", ""), str)
|
||||
disctotal = typed(
|
||||
max(
|
||||
track.get("media_number", 1)
|
||||
|
@ -112,8 +115,8 @@ class AlbumMetadata:
|
|||
# Non-embedded information
|
||||
cover_urls = Covers.from_qobuz(resp)
|
||||
|
||||
bit_depth = typed(resp.get("maximum_bit_depth"), int | None)
|
||||
sampling_rate = typed(resp.get("maximum_sampling_rate"), int | float | None)
|
||||
bit_depth = typed(resp.get("maximum_bit_depth", -1), int)
|
||||
sampling_rate = typed(resp.get("maximum_sampling_rate", -1.0), int | float)
|
||||
quality = get_quality_id(bit_depth, sampling_rate)
|
||||
# Make sure it is non-empty list
|
||||
booklets = typed(resp.get("goodies", None) or None, list | None)
|
||||
|
@ -224,14 +227,14 @@ class AlbumMetadata:
|
|||
safe_get(track, "publisher_metadata", "explicit", default=False),
|
||||
bool,
|
||||
)
|
||||
genre = typed(track["genre"], str | None)
|
||||
genre = typed(track.get("genre"), str | None)
|
||||
genres = [genre] if genre is not None else []
|
||||
artist = typed(safe_get(track, "publisher_metadata", "artist"), str | None)
|
||||
artist = artist or typed(track["user"]["username"], str)
|
||||
albumartist = artist
|
||||
date = typed(track["created_at"], str)
|
||||
date = typed(track.get("created_at"), str)
|
||||
year = date[:4]
|
||||
label = typed(track["label_name"], str | None)
|
||||
label = typed(track.get("label_name"), str | None)
|
||||
description = typed(track.get("description"), str | None)
|
||||
album_title = typed(
|
||||
safe_get(track, "publisher_metadata", "album_title"),
|
||||
|
@ -281,6 +284,7 @@ class AlbumMetadata:
|
|||
"""
|
||||
|
||||
Args:
|
||||
----
|
||||
resp: API response containing album metadata.
|
||||
|
||||
Returns: AlbumMetadata instance if the album is streamable, otherwise None.
|
||||
|
@ -297,12 +301,12 @@ class AlbumMetadata:
|
|||
# genre not returned by API
|
||||
date = typed(resp.get("releaseDate"), str)
|
||||
year = date[:4]
|
||||
_copyright = typed(resp.get("copyright"), str)
|
||||
_copyright = typed(resp.get("copyright", ""), str)
|
||||
|
||||
artists = typed(resp.get("artists", []), list)
|
||||
albumartist = ", ".join(a["name"] for a in artists)
|
||||
if not albumartist:
|
||||
albumartist = typed(safe_get(resp, "artist", "name"), str)
|
||||
albumartist = typed(safe_get(resp, "artist", "name", default=""), str)
|
||||
|
||||
disctotal = typed(resp.get("numberOfVolumes", 1), int)
|
||||
# label not returned by API
|
||||
|
@ -364,7 +368,7 @@ class AlbumMetadata:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def from_tidal_playlist_track_resp(cls, resp) -> AlbumMetadata | None:
|
||||
def from_tidal_playlist_track_resp(cls, resp: dict) -> AlbumMetadata | None:
|
||||
album_resp = resp["album"]
|
||||
streamable = resp.get("allowStreaming", False)
|
||||
if not streamable:
|
||||
|
@ -380,11 +384,13 @@ class AlbumMetadata:
|
|||
else:
|
||||
year = "Unknown Year"
|
||||
|
||||
_copyright = typed(resp.get("copyright"), str)
|
||||
_copyright = typed(resp.get("copyright", ""), str)
|
||||
artists = typed(resp.get("artists", []), list)
|
||||
albumartist = ", ".join(a["name"] for a in artists)
|
||||
if not albumartist:
|
||||
albumartist = typed(safe_get(resp, "artist", "name"), str)
|
||||
albumartist = typed(
|
||||
safe_get(resp, "artist", "name", default="Unknown Albumbartist"), str
|
||||
)
|
||||
|
||||
disctotal = typed(resp.get("volumeNumber", 1), int)
|
||||
# label not returned by API
|
||||
|
|
|
@ -37,7 +37,7 @@ def get_soundcloud_id(resp: dict) -> str:
|
|||
def parse_soundcloud_id(item_id: str) -> tuple[str, str]:
|
||||
info = item_id.split("|")
|
||||
assert len(info) == 2
|
||||
return tuple(info)
|
||||
return (info[0], info[1])
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
|
|
@ -8,14 +8,16 @@ from functools import wraps
|
|||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import click
|
||||
from click_help_colors import HelpColorsGroup # type: ignore
|
||||
from rich.logging import RichHandler
|
||||
from rich.markdown import Markdown
|
||||
from rich.prompt import Confirm
|
||||
from rich.traceback import install
|
||||
|
||||
from .. import db
|
||||
from ..config import DEFAULT_CONFIG_PATH, Config, set_user_defaults
|
||||
from .. import __version__, db
|
||||
from ..config import DEFAULT_CONFIG_PATH, Config, OutdatedConfigError, set_user_defaults
|
||||
from ..console import console
|
||||
from .main import Main
|
||||
|
||||
|
@ -33,7 +35,7 @@ def coro(f):
|
|||
help_headers_color="yellow",
|
||||
help_options_color="green",
|
||||
)
|
||||
@click.version_option(version="2.0")
|
||||
@click.version_option(version=__version__)
|
||||
@click.option(
|
||||
"--config-path",
|
||||
default=DEFAULT_CONFIG_PATH,
|
||||
|
@ -114,6 +116,11 @@ def rip(ctx, config_path, folder, no_db, quality, codec, no_progress, verbose):
|
|||
|
||||
try:
|
||||
c = Config(config_path)
|
||||
except OutdatedConfigError as e:
|
||||
console.print(e)
|
||||
console.print("Auto-updating config file...")
|
||||
Config.update_file(config_path)
|
||||
c = Config(config_path)
|
||||
except Exception as e:
|
||||
console.print(
|
||||
f"Error loading config from [bold cyan]{config_path}[/bold cyan]: {e}\n"
|
||||
|
@ -151,12 +158,33 @@ def rip(ctx, config_path, folder, no_db, quality, codec, no_progress, verbose):
|
|||
@coro
|
||||
async def url(ctx, urls):
|
||||
"""Download content from URLs."""
|
||||
if ctx.obj["config"] is None:
|
||||
return
|
||||
with ctx.obj["config"] as cfg:
|
||||
cfg: Config
|
||||
updates = cfg.session.misc.check_for_updates
|
||||
if updates:
|
||||
# Run in background
|
||||
version_coro = asyncio.create_task(latest_streamrip_version())
|
||||
else:
|
||||
version_coro = None
|
||||
|
||||
async with Main(cfg) as main:
|
||||
await main.add_all(urls)
|
||||
await main.resolve()
|
||||
await main.rip()
|
||||
|
||||
if version_coro is not None:
|
||||
latest_version, notes = await version_coro
|
||||
if latest_version != __version__:
|
||||
console.print(
|
||||
f"\n[green]A new version of streamrip [cyan]v{latest_version}[/cyan]"
|
||||
" is available! Run [white][bold]pip3 install streamrip --upgrade[/bold][/white]"
|
||||
" to update.[/green]\n"
|
||||
)
|
||||
|
||||
console.print(Markdown(notes))
|
||||
|
||||
|
||||
@rip.command()
|
||||
@click.argument(
|
||||
|
@ -176,11 +204,12 @@ async def file(ctx, path):
|
|||
with ctx.obj["config"] as cfg:
|
||||
async with Main(cfg) as main:
|
||||
async with aiofiles.open(path, "r") as f:
|
||||
content = await f.read()
|
||||
try:
|
||||
items: Any = json.loads(await f.read())
|
||||
items: Any = json.loads(content)
|
||||
loaded = True
|
||||
except json.JSONDecodeError:
|
||||
items: Any = [line async for line in f]
|
||||
items = content.split()
|
||||
loaded = False
|
||||
if loaded:
|
||||
console.print(
|
||||
|
@ -190,6 +219,12 @@ async def file(ctx, path):
|
|||
[(i["source"], i["media_type"], i["id"]) for i in items]
|
||||
)
|
||||
else:
|
||||
s = set(items)
|
||||
if len(s) < len(items):
|
||||
console.print(
|
||||
f"Found [orange]{len(items)-len(s)}[/orange] repeated URLs!"
|
||||
)
|
||||
items = list(s)
|
||||
console.print(
|
||||
f"Detected list of urls. Loading [yellow]{len(items)}[/yellow] items"
|
||||
)
|
||||
|
@ -209,14 +244,17 @@ def config():
|
|||
@click.pass_context
|
||||
def config_open(ctx, vim):
|
||||
"""Open the config file in a text editor."""
|
||||
config_path = ctx.obj["config"].path
|
||||
config_path = ctx.obj["config_path"]
|
||||
|
||||
console.print(f"Opening file at [bold cyan]{config_path}")
|
||||
if vim:
|
||||
if shutil.which("nvim") is not None:
|
||||
subprocess.run(["nvim", config_path])
|
||||
else:
|
||||
elif shutil.which("vim") is not None:
|
||||
subprocess.run(["vim", config_path])
|
||||
else:
|
||||
logger.error("Could not find nvim or vim. Using default launcher.")
|
||||
click.launch(config_path)
|
||||
else:
|
||||
click.launch(config_path)
|
||||
|
||||
|
@ -380,5 +418,22 @@ async def id(ctx, source, media_type, id):
|
|||
await main.rip()
|
||||
|
||||
|
||||
async def latest_streamrip_version() -> tuple[str, str | None]:
|
||||
async with aiohttp.ClientSession() as s:
|
||||
async with s.get("https://pypi.org/pypi/streamrip/json") as resp:
|
||||
data = await resp.json()
|
||||
version = data["info"]["version"]
|
||||
|
||||
if version == __version__:
|
||||
return version, None
|
||||
|
||||
async with s.get(
|
||||
"https://api.github.com/repos/nathom/streamrip/releases/latest"
|
||||
) as resp:
|
||||
json = await resp.json()
|
||||
notes = json["body"]
|
||||
return version, notes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rip()
|
||||
|
|
|
@ -4,8 +4,8 @@ import os
|
|||
import pytest
|
||||
from util import arun
|
||||
|
||||
from streamrip.config import Config
|
||||
from streamrip.client.qobuz import QobuzClient
|
||||
from streamrip.config import Config
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
|
|
@ -9,9 +9,9 @@ def arun(coro):
|
|||
|
||||
def afor(async_gen):
|
||||
async def _afor(async_gen):
|
||||
l = []
|
||||
item = []
|
||||
async for item in async_gen:
|
||||
l.append(item)
|
||||
return l
|
||||
item.append(item)
|
||||
return item
|
||||
|
||||
return arun(_afor(async_gen))
|
||||
|
|
Binary file not shown.
|
@ -1,10 +1,34 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
import tomlkit
|
||||
|
||||
from streamrip.config import *
|
||||
from streamrip.config import (
|
||||
ArtworkConfig,
|
||||
CliConfig,
|
||||
Config,
|
||||
ConfigData,
|
||||
ConversionConfig,
|
||||
DatabaseConfig,
|
||||
DeezerConfig,
|
||||
DownloadsConfig,
|
||||
FilepathsConfig,
|
||||
LastFmConfig,
|
||||
MetadataConfig,
|
||||
MiscConfig,
|
||||
QobuzConfig,
|
||||
QobuzDiscographyFilterConfig,
|
||||
SoundcloudConfig,
|
||||
TidalConfig,
|
||||
YoutubeConfig,
|
||||
_get_dict_keys_r,
|
||||
_nested_set,
|
||||
update_config,
|
||||
)
|
||||
|
||||
SAMPLE_CONFIG = "tests/test_config.toml"
|
||||
OLD_CONFIG = "tests/test_config_old.toml"
|
||||
|
||||
|
||||
# Define a fixture to create a sample ConfigData instance for testing
|
||||
|
@ -26,6 +50,98 @@ def sample_config() -> Config:
|
|||
return config
|
||||
|
||||
|
||||
def test_get_keys_r():
|
||||
d = {
|
||||
"key1": {
|
||||
"key2": {
|
||||
"key3": 1,
|
||||
"key4": 1,
|
||||
},
|
||||
"key6": [1, 2],
|
||||
5: 1,
|
||||
}
|
||||
}
|
||||
res = _get_dict_keys_r(d)
|
||||
print(res)
|
||||
assert res == {
|
||||
("key1", "key2", "key3"),
|
||||
("key1", "key2", "key4"),
|
||||
("key1", "key6"),
|
||||
("key1", 5),
|
||||
}
|
||||
|
||||
|
||||
def test_safe_set():
|
||||
d = {
|
||||
"key1": {
|
||||
"key2": {
|
||||
"key3": 1,
|
||||
"key4": 1,
|
||||
},
|
||||
"key6": [1, 2],
|
||||
5: 1,
|
||||
}
|
||||
}
|
||||
_nested_set(d, "key1", "key2", "key3", val=5)
|
||||
assert d == {
|
||||
"key1": {
|
||||
"key2": {
|
||||
"key3": 5,
|
||||
"key4": 1,
|
||||
},
|
||||
"key6": [1, 2],
|
||||
5: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_config_update():
|
||||
old = {
|
||||
"downloads": {"folder": "some_path", "use_service": True},
|
||||
"qobuz": {"email": "asdf@gmail.com", "password": "test"},
|
||||
"legacy_conf": {"something": 1, "other": 2},
|
||||
}
|
||||
new = {
|
||||
"downloads": {"folder": "", "use_service": False, "keep_artwork": True},
|
||||
"qobuz": {"email": "", "password": ""},
|
||||
"tidal": {"email": "", "password": ""},
|
||||
}
|
||||
update_config(old, new)
|
||||
assert new == {
|
||||
"downloads": {"folder": "some_path", "use_service": True, "keep_artwork": True},
|
||||
"qobuz": {"email": "asdf@gmail.com", "password": "test"},
|
||||
"tidal": {"email": "", "password": ""},
|
||||
}
|
||||
|
||||
|
||||
def test_config_throws_outdated():
|
||||
with pytest.raises(Exception, match="update"):
|
||||
_ = Config(OLD_CONFIG)
|
||||
|
||||
|
||||
def test_config_file_update():
|
||||
tmp_conf = "tests/test_config_old2.toml"
|
||||
shutil.copy("tests/test_config_old.toml", tmp_conf)
|
||||
Config._update_file(tmp_conf, SAMPLE_CONFIG)
|
||||
|
||||
with open(tmp_conf) as f:
|
||||
s = f.read()
|
||||
toml = tomlkit.parse(s) # type: ignore
|
||||
|
||||
assert toml["downloads"]["folder"] == "old_value" # type: ignore
|
||||
assert toml["downloads"]["source_subdirectories"] is True # type: ignore
|
||||
assert toml["downloads"]["concurrency"] is True # type: ignore
|
||||
assert toml["downloads"]["max_connections"] == 6 # type: ignore
|
||||
assert toml["downloads"]["requests_per_minute"] == 60 # type: ignore
|
||||
assert toml["cli"]["text_output"] is True # type: ignore
|
||||
assert toml["cli"]["progress_bars"] is True # type: ignore
|
||||
assert toml["cli"]["max_search_results"] == 100 # type: ignore
|
||||
assert toml["misc"]["version"] == "2.0.6" # type: ignore
|
||||
assert "YouTubeVideos" in str(toml["youtube"]["video_downloads_folder"])
|
||||
# type: ignore
|
||||
os.remove("tests/test_config_old2.toml")
|
||||
|
||||
|
||||
def test_sample_config_data_properties(sample_config_data):
|
||||
# Test the properties of ConfigData
|
||||
assert sample_config_data.modified is False # Ensure initial state is not modified
|
||||
|
@ -43,6 +159,7 @@ def test_sample_config_data_fields(sample_config_data):
|
|||
downloads=DownloadsConfig(
|
||||
folder="test_folder",
|
||||
source_subdirectories=False,
|
||||
disc_subdirectories=True,
|
||||
concurrency=True,
|
||||
max_connections=6,
|
||||
requests_per_minute=60,
|
||||
|
@ -127,7 +244,7 @@ def test_sample_config_data_fields(sample_config_data):
|
|||
bit_depth=24,
|
||||
lossy_bitrate=320,
|
||||
),
|
||||
misc=MiscConfig(version="2.0"),
|
||||
misc=MiscConfig(version="2.0", check_for_updates=True),
|
||||
_modified=False,
|
||||
)
|
||||
assert sample_config_data.downloads == test_config.downloads
|
||||
|
@ -145,15 +262,6 @@ def test_sample_config_data_fields(sample_config_data):
|
|||
assert sample_config_data.conversion == test_config.conversion
|
||||
|
||||
|
||||
# def test_config_save_file_called_on_del(sample_config, mocker):
|
||||
# sample_config.file.set_modified()
|
||||
# mockf = mocker.Mock()
|
||||
#
|
||||
# sample_config.save_file = mockf
|
||||
# sample_config.__del__()
|
||||
# mockf.assert_called_once()
|
||||
|
||||
|
||||
def test_config_update_on_save():
|
||||
tmp_config_path = "tests/config2.toml"
|
||||
shutil.copy(SAMPLE_CONFIG, tmp_config_path)
|
||||
|
@ -167,19 +275,6 @@ def test_config_update_on_save():
|
|||
assert conf2.session.downloads.folder == "new_folder"
|
||||
|
||||
|
||||
# def test_config_update_on_del():
|
||||
# tmp_config_path = "tests/config2.toml"
|
||||
# shutil.copy(SAMPLE_CONFIG, tmp_config_path)
|
||||
# conf = Config(tmp_config_path)
|
||||
# conf.file.downloads.folder = "new_folder"
|
||||
# conf.file.set_modified()
|
||||
# del conf
|
||||
# conf2 = Config(tmp_config_path)
|
||||
# os.remove(tmp_config_path)
|
||||
#
|
||||
# assert conf2.session.downloads.folder == "new_folder"
|
||||
|
||||
|
||||
def test_config_dont_update_without_set_modified():
|
||||
tmp_config_path = "tests/config2.toml"
|
||||
shutil.copy(SAMPLE_CONFIG, tmp_config_path)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
folder = "test_folder"
|
||||
# Put Qobuz albums in a 'Qobuz' folder, Tidal albums in 'Tidal' etc.
|
||||
source_subdirectories = false
|
||||
disc_subdirectories = true
|
||||
|
||||
# Download (and convert) tracks all at once, instead of sequentially.
|
||||
# If you are converting the tracks, or have fast internet, this will
|
||||
|
@ -185,4 +186,5 @@ max_search_results = 100
|
|||
|
||||
[misc]
|
||||
# Metadata to identify this config file. Do not change.
|
||||
version = "2.0"
|
||||
version = "2.0.6"
|
||||
check_for_updates = true
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
[downloads]
|
||||
# Folder where tracks are downloaded to
|
||||
folder = "old_value"
|
||||
# Put Qobuz albums in a 'Qobuz' folder, Tidal albums in 'Tidal' etc.
|
||||
source_subdirectories = true
|
||||
|
||||
[qobuz]
|
||||
# 1: 320kbps MP3, 2: 16/44.1, 3: 24/<=96, 4: 24/>=96
|
||||
quality = 3
|
||||
|
||||
# Authenticate to Qobuz using auth token? Value can be true/false only
|
||||
use_auth_token = false
|
||||
# Enter your userid if the above use_auth_token is set to true, else enter your email
|
||||
email_or_userid = "old_test@gmail.com"
|
||||
# Enter your auth token if the above use_auth_token is set to true, else enter the md5 hash of your plaintext password
|
||||
password_or_token = "old_test_pwd"
|
||||
# Do not change
|
||||
app_id = "old_12345"
|
||||
# Do not change
|
||||
secrets = ['old_secret1', 'old_secret2']
|
||||
|
||||
[tidal]
|
||||
# 0: 256kbps AAC, 1: 320kbps AAC, 2: 16/44.1 "HiFi" FLAC, 3: 24/44.1 "MQA" FLAC
|
||||
quality = 3
|
||||
# This will download videos included in Video Albums.
|
||||
download_videos = true
|
||||
|
||||
# Do not change any of the fields below
|
||||
user_id = "old_userid"
|
||||
country_code = "old_countrycode"
|
||||
access_token = "old_accesstoken"
|
||||
refresh_token = "old_refreshtoken"
|
||||
# Tokens last 1 week after refresh. This is the Unix timestamp of the expiration
|
||||
# time. If you haven't used streamrip in more than a week, you may have to log
|
||||
# in again using `rip config --tidal`
|
||||
token_expiry = "old_tokenexpiry"
|
||||
|
||||
[deezer]
|
||||
# 0, 1, or 2
|
||||
# This only applies to paid Deezer subscriptions. Those using deezloader
|
||||
# are automatically limited to quality = 1
|
||||
quality = 2
|
||||
# An authentication cookie that allows streamrip to use your Deezer account
|
||||
# See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie
|
||||
# for instructions on how to find this
|
||||
arl = "old_testarl"
|
||||
# This allows for free 320kbps MP3 downloads from Deezer
|
||||
# If an arl is provided, deezloader is never used
|
||||
use_deezloader = true
|
||||
# This warns you when the paid deezer account is not logged in and rip falls
|
||||
# back to deezloader, which is unreliable
|
||||
deezloader_warnings = true
|
||||
|
||||
[soundcloud]
|
||||
# Only 0 is available for now
|
||||
quality = 0
|
||||
# This changes periodically, so it needs to be updated
|
||||
client_id = "old_clientid"
|
||||
app_version = "old_appversion"
|
||||
|
||||
[youtube]
|
||||
# Only 0 is available for now
|
||||
quality = 0
|
||||
# Download the video along with the audio
|
||||
download_videos = false
|
||||
|
||||
[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
|
||||
# Path to the downloads database
|
||||
downloads_path = "old_downloadspath"
|
||||
# If a download fails, the item ID is stored here. Then, `rip repair` can be
|
||||
# called to retry the downloads
|
||||
failed_downloads_enabled = true
|
||||
failed_downloads_path = "old_faileddownloadspath"
|
||||
|
||||
# Convert tracks to a codec after downloading them.
|
||||
[conversion]
|
||||
enabled = false
|
||||
# FLAC, ALAC, OPUS, MP3, VORBIS, or AAC
|
||||
codec = "old_ALAC"
|
||||
# In Hz. Tracks are downsampled if their sampling rate is greater than this.
|
||||
# Value of 48000 is recommended to maximize quality and minimize space
|
||||
sampling_rate = 48000
|
||||
# Only 16 and 24 are available. It is only applied when the bit depth is higher
|
||||
# than this value.
|
||||
bit_depth = 24
|
||||
# Only applicable for lossy codecs
|
||||
lossy_bitrate = 320
|
||||
|
||||
# Filter a Qobuz artist's discography. Set to 'true' to turn on a filter.
|
||||
[qobuz_filters]
|
||||
# Remove Collectors Editions, live recordings, etc.
|
||||
extras = false
|
||||
# Picks the highest quality out of albums with identical titles.
|
||||
repeats = false
|
||||
# Remove EPs and Singles
|
||||
non_albums = false
|
||||
# Remove albums whose artist is not the one requested
|
||||
features = false
|
||||
# Skip non studio albums
|
||||
non_studio_albums = false
|
||||
# Only download remastered albums
|
||||
non_remaster = false
|
||||
|
||||
[artwork]
|
||||
# Write the image to the audio file
|
||||
embed = true
|
||||
# The size of the artwork to embed. Options: thumbnail, small, large, original.
|
||||
# "original" images can be up to 30MB, and may fail embedding.
|
||||
# Using "large" is recommended.
|
||||
embed_size = "old_large"
|
||||
|
||||
|
||||
[metadata]
|
||||
# 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.
|
||||
set_playlist_to_album = true
|
||||
# If part of a playlist, sets the `tracknumber` field in the metadata to the track's
|
||||
# position in the playlist instead of its position in its album
|
||||
renumber_playlist_tracks = true
|
||||
# The following metadata tags won't be applied
|
||||
# See https://github.com/nathom/streamrip/wiki/Metadata-Tag-Names for more info
|
||||
exclude = []
|
||||
|
||||
# Changes the folder and file names generated by streamrip.
|
||||
[filepaths]
|
||||
# Create folders for single tracks within the downloads directory using the folder_format
|
||||
# template
|
||||
add_singles_to_folder = false
|
||||
# Available keys: "albumartist", "title", "year", "bit_depth", "sampling_rate",
|
||||
# "id", and "albumcomposer"
|
||||
folder_format = "old_{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]"
|
||||
# Available keys: "tracknumber", "artist", "albumartist", "composer", "title",
|
||||
# and "albumcomposer", "explicit"
|
||||
|
||||
[misc]
|
||||
# Metadata to identify this config file. Do not change.
|
||||
version = "0.0.1"
|
||||
check_for_updates = true
|
|
@ -1,7 +1,8 @@
|
|||
import pytest
|
||||
import tomlkit
|
||||
from tomlkit.toml_document import TOMLDocument
|
||||
|
||||
from streamrip.config import *
|
||||
from streamrip.config import ConfigData
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
|
||||
from streamrip.metadata import *
|
||||
from streamrip.metadata import AlbumMetadata, TrackMetadata
|
||||
|
||||
with open("tests/qobuz_album_resp.json") as f:
|
||||
qobuz_album_resp = json.load(f)
|
||||
|
@ -16,10 +16,10 @@ def test_album_metadata_qobuz():
|
|||
assert info.quality == 3
|
||||
assert info.container == "FLAC"
|
||||
assert info.label == "Rhino - Warner Records"
|
||||
assert info.explicit == False
|
||||
assert info.explicit is False
|
||||
assert info.sampling_rate == 96
|
||||
assert info.bit_depth == 24
|
||||
assert info.booklets == None
|
||||
assert info.booklets is None
|
||||
|
||||
assert m.album == "Rumours"
|
||||
assert m.albumartist == "Fleetwood Mac"
|
||||
|
@ -29,19 +29,19 @@ def test_album_metadata_qobuz():
|
|||
assert not m.covers.empty()
|
||||
|
||||
assert m.albumcomposer == "Various Composers"
|
||||
assert m.comment == None
|
||||
assert m.compilation == None
|
||||
assert m.comment is None
|
||||
assert m.compilation is None
|
||||
assert (
|
||||
m.copyright
|
||||
== "© 1977 Warner Records Inc. ℗ 1977 Warner Records Inc. Marketed by Rhino Entertainment Company, A Warner Music Group Company."
|
||||
)
|
||||
assert m.date == "1977-02-04"
|
||||
assert m.description == None
|
||||
assert m.description == ""
|
||||
assert m.disctotal == 1
|
||||
assert m.encoder == None
|
||||
assert m.grouping == None
|
||||
assert m.lyrics == None
|
||||
assert m.purchase_date == None
|
||||
assert m.encoder is None
|
||||
assert m.grouping is None
|
||||
assert m.lyrics is None
|
||||
assert m.purchase_date is None
|
||||
assert m.tracktotal == 11
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
@ -12,9 +13,22 @@ from streamrip.exceptions import MissingCredentialsError
|
|||
logger = logging.getLogger("streamrip")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(qobuz_client):
|
||||
return qobuz_client
|
||||
@pytest.fixture(scope="session")
|
||||
def qobuz_client():
|
||||
config = Config.defaults()
|
||||
config.session.qobuz.email_or_userid = os.environ["QOBUZ_EMAIL"]
|
||||
config.session.qobuz.password_or_token = hashlib.md5(
|
||||
os.environ["QOBUZ_PASSWORD"].encode("utf-8"),
|
||||
).hexdigest()
|
||||
if "QOBUZ_APP_ID" in os.environ and "QOBUZ_SECRETS" in os.environ:
|
||||
config.session.qobuz.app_id = os.environ["QOBUZ_APP_ID"]
|
||||
config.session.qobuz.secrets = os.environ["QOBUZ_SECRETS"].split(",")
|
||||
client = QobuzClient(config)
|
||||
arun(client.login())
|
||||
|
||||
yield client
|
||||
|
||||
arun(client.session.close())
|
||||
|
||||
|
||||
def test_client_raises_missing_credentials():
|
||||
|
@ -26,8 +40,8 @@ def test_client_raises_missing_credentials():
|
|||
@pytest.mark.skipif(
|
||||
"QOBUZ_EMAIL" not in os.environ, reason="Qobuz credentials not found in env."
|
||||
)
|
||||
def test_client_get_metadata(client):
|
||||
meta = arun(client.get_metadata("s9nzkwg2rh1nc", "album"))
|
||||
def test_client_get_metadata(qobuz_client):
|
||||
meta = arun(qobuz_client.get_metadata("s9nzkwg2rh1nc", "album"))
|
||||
assert meta["title"] == "I Killed Your Dog"
|
||||
assert len(meta["tracks"]["items"]) == 16
|
||||
assert meta["maximum_bit_depth"] == 24
|
||||
|
@ -36,8 +50,8 @@ def test_client_get_metadata(client):
|
|||
@pytest.mark.skipif(
|
||||
"QOBUZ_EMAIL" not in os.environ, reason="Qobuz credentials not found in env."
|
||||
)
|
||||
def test_client_get_downloadable(client):
|
||||
d = arun(client.get_downloadable("19512574", 3))
|
||||
def test_client_get_downloadable(qobuz_client):
|
||||
d = arun(qobuz_client.get_downloadable("19512574", 3))
|
||||
assert isinstance(d, BasicDownloadable)
|
||||
assert d.extension == "flac"
|
||||
assert isinstance(d.url, str)
|
||||
|
@ -47,8 +61,8 @@ def test_client_get_downloadable(client):
|
|||
@pytest.mark.skipif(
|
||||
"QOBUZ_EMAIL" not in os.environ, reason="Qobuz credentials not found in env."
|
||||
)
|
||||
def test_client_search_limit(client):
|
||||
res = client.search("album", "rumours", limit=5)
|
||||
def test_client_search_limit(qobuz_client):
|
||||
res = qobuz_client.search("album", "rumours", limit=5)
|
||||
total = 0
|
||||
for r in arun(res):
|
||||
total += len(r["albums"]["items"])
|
||||
|
@ -58,9 +72,9 @@ def test_client_search_limit(client):
|
|||
@pytest.mark.skipif(
|
||||
"QOBUZ_EMAIL" not in os.environ, reason="Qobuz credentials not found in env."
|
||||
)
|
||||
def test_client_search_no_limit(client):
|
||||
def test_client_search_no_limit(qobuz_client):
|
||||
# Setting no limit has become impossible because `limit: int` now
|
||||
res = client.search("album", "rumours", limit=10000)
|
||||
res = qobuz_client.search("album", "rumours", limit=10000)
|
||||
correct_total = 0
|
||||
total = 0
|
||||
for r in arun(res):
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from mutagen.flac import FLAC
|
||||
from util import arun
|
||||
|
||||
from streamrip.metadata import *
|
||||
from streamrip.metadata import (
|
||||
AlbumInfo,
|
||||
AlbumMetadata,
|
||||
Covers,
|
||||
TrackInfo,
|
||||
TrackMetadata,
|
||||
tag_file,
|
||||
)
|
||||
|
||||
test_flac = "tests/silence.flac"
|
||||
TEST_FLAC_ORIGINAL = "tests/silence.flac"
|
||||
TEST_FLAC_COPY = "tests/silence_copy.flac"
|
||||
test_cover = "tests/1x1_pixel.jpg"
|
||||
|
||||
|
||||
def wipe_test_flac():
|
||||
audio = FLAC(test_flac)
|
||||
audio = FLAC(TEST_FLAC_COPY)
|
||||
# Remove all tags
|
||||
audio.delete()
|
||||
audio.save()
|
||||
|
@ -55,9 +66,10 @@ def sample_metadata() -> TrackMetadata:
|
|||
|
||||
|
||||
def test_tag_flac_no_cover(sample_metadata):
|
||||
shutil.copy(TEST_FLAC_ORIGINAL, TEST_FLAC_COPY)
|
||||
wipe_test_flac()
|
||||
arun(tag_file(test_flac, sample_metadata, None))
|
||||
file = FLAC(test_flac)
|
||||
arun(tag_file(TEST_FLAC_COPY, sample_metadata, None))
|
||||
file = FLAC(TEST_FLAC_COPY)
|
||||
assert file["title"][0] == "testtitle"
|
||||
assert file["album"][0] == "testalbum"
|
||||
assert file["composer"][0] == "testcomposer"
|
||||
|
@ -72,12 +84,14 @@ def test_tag_flac_no_cover(sample_metadata):
|
|||
assert file["tracktotal"][0] == "14"
|
||||
assert file["date"][0] == "1998-02-13"
|
||||
assert "purchase_date" not in file, file["purchase_date"]
|
||||
os.remove(TEST_FLAC_COPY)
|
||||
|
||||
|
||||
def test_tag_flac_cover(sample_metadata):
|
||||
shutil.copy(TEST_FLAC_ORIGINAL, TEST_FLAC_COPY)
|
||||
wipe_test_flac()
|
||||
arun(tag_file(test_flac, sample_metadata, test_cover))
|
||||
file = FLAC(test_flac)
|
||||
arun(tag_file(TEST_FLAC_COPY, sample_metadata, test_cover))
|
||||
file = FLAC(TEST_FLAC_COPY)
|
||||
assert file["title"][0] == "testtitle"
|
||||
assert file["album"][0] == "testalbum"
|
||||
assert file["composer"][0] == "testcomposer"
|
||||
|
@ -94,3 +108,4 @@ def test_tag_flac_cover(sample_metadata):
|
|||
with open(test_cover, "rb") as img:
|
||||
assert file.pictures[0].data == img.read()
|
||||
assert "purchase_date" not in file, file["purchase_date"]
|
||||
os.remove(TEST_FLAC_COPY)
|
||||
|
|
|
@ -25,19 +25,8 @@ def test_pending_resolve(qobuz_client: QobuzClient):
|
|||
dir = "tests/tests/Fleetwood Mac - Rumours (1977) [FLAC] [24B-96kHz]"
|
||||
assert os.path.isdir(dir)
|
||||
assert os.path.isfile(os.path.join(dir, "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.downloadable, Downloadable)
|
||||
assert t.cover_path is not None
|
||||
shutil.rmtree(dir)
|
||||
|
||||
|
||||
# def test_pending_resolve_mp3(qobuz_client: QobuzClient):
|
||||
# qobuz_client.config.session.qobuz.quality = 1
|
||||
# p = PendingSingle("19512574", qobuz_client, qobuz_client.config)
|
||||
# t = arun(p.resolve())
|
||||
# assert isinstance(t, Track)
|
||||
# assert False
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from streamrip import __version__ as init_version
|
||||
from streamrip.config import CURRENT_CONFIG_VERSION
|
||||
|
||||
toml_version_re = re.compile(r'version\s*\=\s*"([\d\.]+)"')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pyproject_version() -> str:
|
||||
with open("pyproject.toml") as f:
|
||||
m = toml_version_re.search(f.read())
|
||||
assert m is not None
|
||||
return m.group(1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_version() -> str | None:
|
||||
with open("streamrip/config.toml") as f:
|
||||
m = toml_version_re.search(f.read())
|
||||
assert m is not None
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def test_config_versions_match(config_version):
|
||||
assert config_version == CURRENT_CONFIG_VERSION
|
||||
|
||||
|
||||
def test_streamrip_versions_match(pyproject_version):
|
||||
assert pyproject_version == init_version
|
|
@ -9,9 +9,9 @@ def arun(coro):
|
|||
|
||||
def afor(async_gen):
|
||||
async def _afor(async_gen):
|
||||
l = []
|
||||
items = []
|
||||
async for item in async_gen:
|
||||
l.append(item)
|
||||
return l
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
return arun(_afor(async_gen))
|
||||
|
|
Loading…
Reference in New Issue