yt-dlp migration (scdl v3.0.0) (#537)

### Added

- `--yt-dlp-args <argstring>` to allow passing of yt-dlp args as a string (using yt-dlp CLI format)

### Fixed

- Make `--sync` method work as intended

### Changed

- Use [yt-dlp](https://github.com/yt-dlp/yt-dlp/) to download and process tracks
- Store description in "comment" tag instead of "description" tag for Ogg/FLAC files
- Change default track format string to include track artist and ID
- Drop `termcolor`, `requests`, `tqdm`, `pathvalidate`, `filelock` dependencies
- Switch to `pyproject.toml` instead of `setup.py`
- **Breaking:** Change `--sync` file format to include downloaded filenames

### Removed

- **Breaking:** Remove `--remove`
- **Breaking:** Remove `-n`
- **Breaking:** Drop support for Python 3.7 and 3.8
- **Breaking:** When writing to stdout, files no longer contain metadata
This commit is contained in:
gavin
2025-08-09 13:21:33 -04:00
committed by GitHub
parent eb555d8993
commit e4dec65065
25 changed files with 1545 additions and 1829 deletions

View File

@@ -10,7 +10,7 @@ jobs:
test:
strategy:
matrix:
version: ['3.8', '3.12']
version: ['3.9', '3.13']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -18,18 +18,22 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.version }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.version }}
version: '0.8.8'
- name: Install dependencies
run: |
pip install -e .[dev]
run: uv sync
- name: Lint
if: '!cancelled()'
run: ruff check --output-format=github
run: uv run ruff check --output-format=github
- name: Format check
if: '!cancelled()'
run: ruff format --check
run: uv run ruff format --check
- name: Type check
if: '!cancelled()'
run: mypy
run: uv run mypy
- name: Install ffmpeg
run: |
sudo apt update
@@ -38,8 +42,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }}
run: |
pytest -vv --exitfirst
run: uv run pytest -vv --exitfirst
publish:
needs: test
if: startsWith(github.ref, 'refs/tags/v')
@@ -49,14 +52,16 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.9
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: 3.9
version: '0.8.8'
- name: Install dependencies
run: |
pip install -e .
run: uv sync
- name: Build package
run: |
pip install --upgrade build
python -m build
run: uv build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:

5
.gitignore vendored
View File

@@ -4,10 +4,13 @@ dist/
*.egg-info/
__pycache__/
*.mp3
*.opus
*.m4a
*.flac
.vscode
.venv
.env
.coverage*
.idea
.python-version
.DS_store
.DS_Store

View File

@@ -1,10 +1,16 @@
# Soundcloud Music Downloader
## Status of the project
As of version 3, this script is a wrapper around `yt-dlp` with some defaults/patches for backwards compatibility.
Development is not active and new features will likely not be merged, especially if they can be covered with the
use of `--yt-dlp-args`. Bug reports/fixes are welcome.
## Description
This script is able to download music from SoundCloud and set id3tag to the downloaded music.
Compatible with Windows, OS X, Linux.
## System requirements
* python3
@@ -91,6 +97,7 @@ scdl me -f
--add-description Adds the description to a seperate txt file (can be read by some players)
--no-playlist Skip downloading playlists
--opus Prefer downloading opus streams over mp3 streams
--yt-dlp-args String with custom args to forward to yt-dlp
```

View File

@@ -1,3 +0,0 @@
[mypy]
packages = scdl, tests
check_untyped_defs = true

74
pyproject.toml Normal file
View File

@@ -0,0 +1,74 @@
[project]
name = "scdl"
version = "3.0.0"
authors=[
{name = "7x11x13"}, {name = "FlyinGrub"}
]
description = "Download Music from Souncloud"
readme = "README.md"
requires-python = ">=3.9.0"
dependencies = [
"docopt-ng>=0.9.0",
"mutagen>=1.47.0",
"soundcloud-v2>=1.6.0",
"yt-dlp>=2025.2.19",
]
classifiers = [
"Programming Language :: Python",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Internet",
"Topic :: Multimedia :: Sound/Audio",
]
[dependency-groups]
dev = [
"music-tag>=0.4.3",
"mypy>=1.13.0",
"pytest>=8.3.4",
"pytest-dotenv>=0.5.2",
"ruff>=0.8.3",
"types-requests>=2.32.0.20241016",
"typing_extensions>=4.12.2; python_version < '3.11'",
]
[project.urls]
Issues = "https://github.com/scdl-org/scdl/issues"
Repository = "https://github.com/scdl-org/scdl"
Changelog = "https://github.com/scdl-org/scdl/blob/master/CHANGELOG.md"
[project.scripts]
scdl = "scdl.scdl:_main"
[tool.uv]
package = true
[tool.ruff]
target-version = "py39"
line-length = 120
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"ANN",
"C90", "D",
"S", "BLE", "FBT", "A", "EM", "FA", "G", "SLF", "PTH",
"PLR", "TRY",
"PLW2901", "ANN204",
"COM812", "ISC001",
"EXE"
]
[tool.mypy]
packages = ["scdl", "tests"]
check_untyped_defs = true
disable_error_code = ["import-untyped"]
[[tool.mypy.overrides]]
module = "scdl.patches.*"
ignore_errors = true

View File

@@ -1,13 +0,0 @@
target-version = "py37"
line-length = 100
[lint]
select = ["ALL"]
ignore = [
"C90", "D",
"S", "BLE", "FBT", "A", "EM", "FA", "G", "SLF", "PTH",
"PLR", "TRY",
"PLW2901", "ANN204",
"COM812", "ISC001",
"EXE"
]

View File

@@ -1,3 +1,6 @@
"""Python Soundcloud Music Downloader."""
__version__ = "v2.12.4"
from . import patches # noqa: F401, I001
from scdl.scdl import download_url
__all__ = ["download_url"]

View File

@@ -1,171 +0,0 @@
from base64 import b64encode
from dataclasses import dataclass
from functools import singledispatch
from typing import Optional, Union
from mutagen import (
FileType,
aiff,
flac,
id3,
mp3,
mp4,
oggopus,
oggspeex,
oggtheora,
wave,
)
JPEG_MIME_TYPE: str = "image/jpeg"
@dataclass(frozen=True)
class MetadataInfo:
artist: str
title: str
description: Optional[str]
genre: Optional[str]
artwork_jpeg: Optional[bytes]
link: Optional[str]
date: Optional[str]
album_title: Optional[str]
album_author: Optional[str]
album_track_num: Optional[int]
album_total_track_num: Optional[int]
@singledispatch
def assemble_metadata(file: FileType, meta: MetadataInfo) -> None:
raise NotImplementedError
def _get_flac_pic(jpeg_data: bytes) -> flac.Picture:
pic = flac.Picture()
pic.data = jpeg_data
pic.mime = JPEG_MIME_TYPE
pic.type = id3.PictureType.COVER_FRONT
return pic
def _get_apic(jpeg_data: bytes) -> id3.APIC:
return id3.APIC(
encoding=3,
mime=JPEG_MIME_TYPE,
type=3,
desc="Cover",
data=jpeg_data,
)
def _assemble_vorbis_tags(file: FileType, meta: MetadataInfo) -> None:
file["artist"] = meta.artist
file["title"] = meta.title
if meta.genre:
file["genre"] = meta.genre
if meta.link:
# https://getmusicbee.com/forum/index.php?topic=39759.0
file["WWWAUDIOFILE"] = meta.link
if meta.date:
file["date"] = meta.date
if meta.album_title:
file["album"] = meta.album_title
if meta.album_author:
file["albumartist"] = meta.album_author
if meta.album_track_num is not None:
file["tracknumber"] = str(meta.album_track_num)
if meta.description:
# https://xiph.org/vorbis/doc/v-comment.html
# prefer 'description' over 'comment'
file["description"] = meta.description
@assemble_metadata.register(flac.FLAC)
def _(file: flac.FLAC, meta: MetadataInfo) -> None:
_assemble_vorbis_tags(file, meta)
if meta.artwork_jpeg:
file.add_picture(_get_flac_pic(meta.artwork_jpeg))
@assemble_metadata.register(oggtheora.OggTheora)
@assemble_metadata.register(oggspeex.OggSpeex)
@assemble_metadata.register(oggopus.OggOpus)
def _(file: oggopus.OggOpus, meta: MetadataInfo) -> None:
_assemble_vorbis_tags(file, meta)
if meta.artwork_jpeg:
pic = _get_flac_pic(meta.artwork_jpeg).write()
file["metadata_block_picture"] = b64encode(pic).decode()
@assemble_metadata.register(aiff.AIFF)
@assemble_metadata.register(mp3.MP3)
@assemble_metadata.register(wave.WAVE)
def _(file: Union[wave.WAVE, mp3.MP3], meta: MetadataInfo) -> None:
file["TIT2"] = id3.TIT2(encoding=3, text=meta.title)
file["TPE1"] = id3.TPE1(encoding=3, text=meta.artist)
if meta.description:
file["COMM"] = id3.COMM(encoding=3, lang="ENG", text=meta.description)
if meta.genre:
file["TCON"] = id3.TCON(encoding=3, text=meta.genre)
if meta.link:
file["WOAF"] = id3.WOAF(url=meta.link)
if meta.date:
file["TDAT"] = id3.TDAT(encoding=3, text=meta.date)
if meta.album_title:
file["TALB"] = id3.TALB(encoding=3, text=meta.album_title)
if meta.album_author:
file["TPE2"] = id3.TPE2(encoding=3, text=meta.album_author)
if meta.album_track_num is not None:
file["TRCK"] = id3.TRCK(encoding=3, text=str(meta.album_track_num))
if meta.artwork_jpeg:
file["APIC"] = _get_apic(meta.artwork_jpeg)
@assemble_metadata.register(mp4.MP4)
def _(file: mp4.MP4, meta: MetadataInfo) -> None:
file["\251ART"] = meta.artist
file["\251nam"] = meta.title
if meta.genre:
file["\251gen"] = meta.genre
if meta.link:
# https://getmusicbee.com/forum/index.php?topic=39759.0
file["----:com.apple.iTunes:WWWAUDIOFILE"] = meta.link.encode()
if meta.date:
file["\251day"] = meta.date
if meta.album_title:
file["\251alb"] = meta.album_title
if meta.album_author:
file["aART"] = meta.album_author
if meta.album_track_num is not None:
file["trkn"] = [(meta.album_track_num, meta.album_total_track_num)]
if meta.description:
file["\251cmt"] = meta.description
if meta.artwork_jpeg:
file["covr"] = [mp4.MP4Cover(meta.artwork_jpeg)]

13
scdl/patches/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
from . import (
old_archive_ids,
sync_download_archive,
thumbnail_selection,
trim_filenames,
)
__all__ = [
"old_archive_ids",
"sync_download_archive",
"thumbnail_selection",
"trim_filenames",
]

View File

@@ -0,0 +1,283 @@
# https://github.com/yt-dlp/yt-dlp/pull/11817
import base64
import collections
import functools
import os
import re
from typing import ClassVar
import mutagen
from mutagen import (
FileType,
aiff,
dsdiff,
dsf,
flac,
id3,
mp3,
mp4,
oggopus,
oggspeex,
oggtheora,
oggvorbis,
trueaudio,
wave,
)
from yt_dlp.compat import imghdr
from yt_dlp.postprocessor.common import PostProcessor
from yt_dlp.utils import PostProcessingError, date_from_str, variadic
class MutagenPostProcessorError(PostProcessingError):
pass
class MutagenPP(PostProcessor):
_MUTAGEN_SUPPORTED_EXTS = ("alac", "aiff", "flac", "mp3", "m4a", "ogg", "opus", "vorbis", "wav")
_VORBIS_METADATA: ClassVar[dict[str, str]] = {
"title": "title",
"artist": "artist",
"genre": "genre",
"album": "album",
"albumartist": "album_artist",
"comment": "description",
"composer": "composer",
"tracknumber": "track",
"WWWAUDIOFILE": "purl", # https://getmusicbee.com/forum/index.php?topic=39759.0
}
_ID3_METADATA: ClassVar[dict[str, str]] = {
"TIT2": "title",
"TPE1": "artist",
"COMM": "description",
"TCON": "genre",
"WOAF": "purl",
"TALB": "album",
"TPE2": "album_artist",
"TRCK": "track",
"TCOM": "composer",
"TPOS": "disc",
}
_MP4_METADATA: ClassVar[dict[str, str]] = {
"\251ART": "artist",
"\251nam": "title",
"\251gen": "genre",
"\251alb": "album",
"aART": "album_artist",
"\251cmt": "description",
"\251wrt": "composer",
"disk": "disc",
"tvsh": "show",
"tvsn": "season_number",
"egid": "episode_id",
"tven": "episode_sort",
}
def __init__(self, post_overwrites: bool, downloader=None):
super().__init__(downloader)
self._post_overwrites = post_overwrites
def _get_flac_pic(self, thumbnail: dict) -> flac.Picture:
pic = flac.Picture()
pic.data = thumbnail["data"]
pic.mime = f"image/{thumbnail['type']}"
pic.type = id3.PictureType.COVER_FRONT
return pic
def _get_metadata_dict(self, info):
meta_prefix = "meta"
metadata = collections.defaultdict(dict)
def add(meta_list, info_list=None):
value = next(
(
info[key]
for key in [f"{meta_prefix}_", *variadic(info_list or meta_list)]
if info.get(key) is not None
),
None,
)
if value not in ("", None):
value = ", ".join(map(str, variadic(value)))
value = value.replace("\0", "") # nul character cannot be passed in command line
metadata["common"].update({meta_f: value for meta_f in variadic(meta_list)})
# Info on media metadata/metadata supported by ffmpeg:
# https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
# https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/
# https://kodi.wiki/view/Video_file_tagging
add("title", ("track", "title"))
add("date", "upload_date")
add(("description", "synopsis"), "description")
add(("purl", "comment"), "webpage_url")
add("track", "track_number")
add("artist", ("artist", "artists", "creator", "creators", "uploader", "uploader_id"))
add("composer", ("composer", "composers"))
add("genre", ("genre", "genres"))
add("album")
add("album_artist", ("album_artist", "album_artists"))
add("disc", "disc_number")
add("show", "series")
add("season_number")
add("episode_id", ("episode", "episode_id"))
add("episode_sort", "episode_number")
if "embed-metadata" in self.get_param("compat_opts", []):
add("comment", "description")
metadata["common"].pop("synopsis", None)
meta_regex = rf"{re.escape(meta_prefix)}(?P<i>\d+)?_(?P<key>.+)"
for key, value in info.items():
mobj = re.fullmatch(meta_regex, key)
if value is not None and mobj:
metadata[mobj.group("i") or "common"][mobj.group("key")] = value.replace("\0", "")
return metadata
@functools.singledispatchmethod
def _assemble_metadata(self, file: FileType, meta: dict) -> None: # noqa: ARG002
raise MutagenPostProcessorError(f"Filetype {file.__class__.__name__} is not currently supported")
@_assemble_metadata.register(flac.FLAC)
def _(self, file: flac.FLAC, meta: dict) -> None:
for file_key, meta_key in self._VORBIS_METADATA.items():
if meta.get(meta_key):
file[file_key] = meta[meta_key]
if meta.get("date"):
# Vorbis uses ISO 8601 format YYYY-MM-DD
date = date_from_str(meta["date"])
file["date"] = date.strftime("%Y-%m-%d")
if meta.get("thumbnail"):
pic = self._get_flac_pic(meta["thumbnail"])
file.add_picture(pic)
@_assemble_metadata.register(oggvorbis.OggVorbis)
@_assemble_metadata.register(oggtheora.OggTheora)
@_assemble_metadata.register(oggspeex.OggSpeex)
@_assemble_metadata.register(oggopus.OggOpus)
def _(self, file: oggopus.OggOpus, meta: dict) -> None:
for file_key, meta_key in self._VORBIS_METADATA.items():
if meta.get(meta_key):
file[file_key] = meta[meta_key]
if meta.get("date"):
# Vorbis uses ISO 8601 format YYYY-MM-DD
date = date_from_str(meta["date"])
file["date"] = date.strftime("%Y-%m-%d")
if meta.get("thumbnail"):
pic = self._get_flac_pic(meta["thumbnail"])
file["METADATA_BLOCK_PICTURE"] = base64.b64encode(pic.write()).decode("ascii")
@_assemble_metadata.register(trueaudio.TrueAudio)
@_assemble_metadata.register(dsf.DSF)
@_assemble_metadata.register(dsdiff.DSDIFF)
@_assemble_metadata.register(aiff.AIFF)
@_assemble_metadata.register(mp3.MP3)
@_assemble_metadata.register(wave.WAVE)
def _(self, file: wave.WAVE, meta: dict) -> None:
for file_key, meta_key in self._ID3_METADATA.items():
if meta.get(meta_key):
id3_class = getattr(id3, file_key)
if issubclass(id3_class, id3.UrlFrame):
file[file_key] = id3_class(url=meta[meta_key])
else:
file[file_key] = id3_class(encoding=id3.Encoding.UTF8, text=meta[meta_key])
if meta.get("date"):
# ID3 uses ISO 8601 format YYYY-MM-DD
date = date_from_str(meta["date"])
file["TDRC"] = id3.TDRC(encoding=id3.Encoding.UTF8, text=date.strftime("%Y-%m-%d"))
if meta.get("thumbnail"):
file["APIC"] = id3.APIC(
encoding=3,
mime=f'image/{meta["thumbnail"]["type"]}',
type=3,
desc="Cover (front)",
data=meta["thumbnail"]["data"],
)
@_assemble_metadata.register(mp4.MP4)
def _(self, file: mp4.MP4, meta: dict) -> None:
for file_key, meta_key in self._MP4_METADATA.items():
if meta.get(meta_key):
file[file_key] = meta[meta_key]
if meta.get("date"):
# no standard but iTunes uses YYYY-MM-DD format
date = date_from_str(meta["date"])
file["\251day"] = date.strftime("%Y-%m-%d")
if meta.get("purl"):
# https://getmusicbee.com/forum/index.php?topic=39759.0
file["----:com.apple.iTunes:WWWAUDIOFILE"] = meta["purl"].encode()
file["purl"] = meta["purl"]
if meta.get("track"):
file["trkn"] = [(meta["track"], 0)]
if meta.get("covr"):
f = {"jpeg": mp4.MP4Cover.FORMAT_JPEG, "png": mp4.MP4Cover.FORMAT_PNG}
file["covr"] = [mp4.MP4Cover(meta["covr"]["data"], f[meta["covr"]["type"]])]
def _get_thumbnail(self, info: dict):
if not info.get("thumbnails"):
self.to_screen("There aren't any thumbnails to embed")
return None
idx = next((-i for i, t in enumerate(info["thumbnails"][::-1], 1) if t.get("filepath")), None)
if idx is None:
self.to_screen("There are no thumbnails on disk")
return None
thumbnail_filename = info["thumbnails"][idx]["filepath"]
if not os.path.exists(thumbnail_filename):
self.report_warning("Skipping embedding the thumbnail because the file is missing.")
return None
with open(thumbnail_filename, "rb") as thumbfile:
thumb_data = thumbfile.read()
self._delete_downloaded_files(
thumbnail_filename,
info=info,
)
type_ = imghdr.what(h=thumb_data)
if not type_:
self.report_warning("Could not determine thumbnail image type")
return None
if type_ not in {"jpeg", "png"}:
self.report_warning(f"Incompatible thumbnail image type: {type_}")
return None
return {"data": thumb_data, "type": type_}
def run(self, info: dict):
thumbnail = self._get_thumbnail(info)
if not info["__real_download"] and not self._post_overwrites:
return [], info
filename = info["filepath"]
metadata = self._get_metadata_dict(info)["common"]
if thumbnail:
metadata["thumbnail"] = thumbnail
if not metadata:
self.to_screen("There isn't any metadata to add")
return [], info
if info["ext"] not in self._MUTAGEN_SUPPORTED_EXTS:
raise MutagenPostProcessorError(f'Unsupported file extension: {info["ext"]}')
self.to_screen(f'Adding metadata to "{filename}"')
try:
f = mutagen.File(filename)
self._assemble_metadata(f, metadata)
f.save()
except Exception as err:
raise MutagenPostProcessorError("Unable to embed metadata") from err
return [], info

View File

@@ -0,0 +1,13 @@
from yt_dlp import YoutubeDL
def in_download_archive(self, info_dict):
if not self.archive:
return False
vid_ids = [self._make_archive_id(info_dict)]
vid_ids.extend(info_dict.get("_old_archive_ids") or [])
return any(id_ in self.archive for id_ in vid_ids) or any(id_.split()[1] in self.archive for id_ in vid_ids if id_)
YoutubeDL.in_download_archive = in_download_archive

View File

@@ -0,0 +1,32 @@
import email.message
import urllib.parse
from pathlib import Path
from yt_dlp.networking.common import Request, Response
from yt_dlp.postprocessor.common import PostProcessor
def _parse_header(content_disposition):
if not content_disposition:
return {}
message = email.message.Message()
message["content-type"] = content_disposition
return dict(message.get_params({}))
class OriginalFilenamePP(PostProcessor):
def run(self, info):
for format in info.get("formats", ()):
if format.get("format_id") == "download":
res: Response = self._downloader.urlopen(Request(format["url"], headers=format["http_headers"]))
params = _parse_header(res.get_header("content-disposition"))
if "filename" not in params:
break
filename = urllib.parse.unquote(params["filename"][-1], encoding="utf-8")
old_outtmpl = self._downloader.params["outtmpl"]["default"]
self._downloader.params["outtmpl"]["default"] = (
Path(old_outtmpl).with_name(filename).with_suffix(".%(ext)s").as_posix()
)
break
return [], info

View File

@@ -0,0 +1,16 @@
# https://github.com/yt-dlp/yt-dlp/issues/11583 workaround
from yt_dlp.postprocessor.common import PostProcessor
class OuttmplPP(PostProcessor):
def __init__(self, video_outtmpl: str, playlist_outtmpl: str, downloader=None):
super().__init__(downloader)
self._outtmpls = {False: video_outtmpl, True: playlist_outtmpl}
def run(self, info):
in_playlist = info.get("playlist_uploader") is not None
self._downloader.params["outtmpl"]["default"] = self._outtmpls[in_playlist]
if not in_playlist:
for meta in ("track", "album_artist", "album"):
info[f"meta_{meta}"] = None
return [], info

View File

@@ -0,0 +1,69 @@
import errno
from functools import partial
from pathlib import Path
from yt_dlp import YoutubeDL
from yt_dlp.utils import locked_file
class SyncDownloadHelper:
def __init__(self, scdl_args, ydl: YoutubeDL):
self._ydl = ydl
self._enabled = bool(scdl_args.get("sync"))
self._sync_file = scdl_args.get("sync")
self._all_files: dict[str, Path] = {}
self._downloaded: set[str] = set()
self._init()
def _init(self):
if not self._enabled:
return
# track downloaded ids/filenames
def track_downloaded(d):
if d["status"] != "finished":
return
info = d["info_dict"]
id_ = f"soundcloud {info['id']}"
self._downloaded.add(id_)
self._all_files[id_] = d["filename"]
self._ydl.add_progress_hook(track_downloaded)
# add already downloaded files to the archive
try:
with locked_file(self._sync_file, "r", encoding="utf-8") as archive_file:
for line in archive_file:
line = line.strip()
if not line:
continue
ie, id_, filename = line.split(maxsplit=2)
self._ydl.archive.add(f"{ie} {id_}")
self._all_files[f"{ie} {id_}"] = Path(filename)
except OSError as ioe:
if ioe.errno != errno.ENOENT:
raise
# track ids checked against the archive
old_match_entry = self._ydl._match_entry
def _match_entry(ydl, info_dict, incomplete=False, silent=False):
self._downloaded.add(ydl._make_archive_id(info_dict))
return old_match_entry(info_dict, incomplete, silent)
self._ydl._match_entry = partial(_match_entry, self._ydl)
def post_download(self):
if not self._enabled:
return
# remove extra files
to_remove = {self._all_files[key] for key in (set(self._all_files.keys()) - self._downloaded)}
self._ydl._delete_downloaded_files(*to_remove)
with locked_file(self._sync_file, "w", encoding="utf-8") as archive_file:
for k, v in self._all_files.items():
if k in self._downloaded:
archive_file.write(f"{k} {v}\n")

View File

@@ -0,0 +1,41 @@
# https://github.com/yt-dlp/yt-dlp/pull/11809
import yt_dlp
import yt_dlp.options as options
from yt_dlp.YoutubeDL import YoutubeDL
def _sort_thumbnails_patched(self, thumbnails):
thumbnails.sort(
key=lambda t: (
t.get("id") == self.params.get("thumbnail_id") if t.get("id") is not None else False,
t.get("preference") if t.get("preference") is not None else -1,
t.get("width") if t.get("width") is not None else -1,
t.get("height") if t.get("height") is not None else -1,
t.get("id") if t.get("id") is not None else "",
t.get("url"),
)
)
old_parse_options = yt_dlp.parse_options
def parse_options_patched(argv=None):
parsed = old_parse_options(argv)
parsed[3]["thumbnail_id"] = parsed[1].thumbnail_id
return parsed
old_create_parser = options.create_parser
def create_parser_patched():
parser = old_create_parser()
thumbnail = parser.get_option_group("--write-thumbnail")
thumbnail.add_option("--thumbnail-id", metavar="ID", dest="thumbnail_id", help="ID of thumbnail to write to disk")
return parser
YoutubeDL._sort_thumbnails = _sort_thumbnails_patched
yt_dlp.parse_options = parse_options_patched
options.create_parser = create_parser_patched

View File

@@ -0,0 +1,113 @@
# https://github.com/yt-dlp/yt-dlp/pull/12023
import os
import platform
import re
import sys
from pathlib import Path
import yt_dlp.__init__
from yt_dlp import YoutubeDL, options
from yt_dlp.__init__ import validate_options as old_validate_options
from yt_dlp.utils import OUTTMPL_TYPES, preferredencoding, replace_extension
from yt_dlp.YoutubeDL import _catch_unsafe_extension_error
def evaluate_outtmpl(self, outtmpl, info_dict, *args, trim_filename=False, **kwargs):
outtmpl, info_dict = self.prepare_outtmpl(outtmpl, info_dict, *args, **kwargs)
if not trim_filename:
return self.escape_outtmpl(outtmpl) % info_dict
ext_suffix = ".%(ext\0s)s"
suffix = ""
if outtmpl.endswith(ext_suffix):
outtmpl = outtmpl[: -len(ext_suffix)]
suffix = ext_suffix % info_dict
outtmpl = self.escape_outtmpl(outtmpl)
filename = outtmpl % info_dict
def parse_trim_file_name(trim_file_name):
if trim_file_name is None or trim_file_name == "none":
return 0, None
mobj = re.match(r"(?:(?P<length>\d+)(?P<mode>b|c)?|none)", trim_file_name)
return int(mobj.group("length")), mobj.group("mode") or "c"
max_file_name, mode = parse_trim_file_name(self.params.get("trim_file_name"))
if max_file_name == 0:
# no maximum
return filename + suffix
encoding = sys.getfilesystemencoding() if platform.system() != "Windows" else "utf-16-le"
def trim_filename(name: str):
if mode == "b":
name = name.encode(encoding)
name = name[:max_file_name]
return name.decode(encoding, "ignore")
return name[:max_file_name]
filename = os.path.join(*map(trim_filename, Path(filename).parts or "."))
return filename + suffix
@_catch_unsafe_extension_error
def _prepare_filename(self, info_dict, *, outtmpl=None, tmpl_type=None):
assert None in (outtmpl, tmpl_type), "outtmpl and tmpl_type are mutually exclusive"
if outtmpl is None:
outtmpl = self.params["outtmpl"].get(tmpl_type or "default", self.params["outtmpl"]["default"])
try:
outtmpl = self._outtmpl_expandpath(outtmpl)
filename = self.evaluate_outtmpl(outtmpl, info_dict, True, trim_filename=True)
if not filename:
return None
if tmpl_type in ("", "temp"):
final_ext, ext = self.params.get("final_ext"), info_dict.get("ext")
if final_ext and ext and final_ext != ext and filename.endswith(f".{final_ext}"):
filename = replace_extension(filename, ext, final_ext)
elif tmpl_type:
force_ext = OUTTMPL_TYPES[tmpl_type]
if force_ext:
filename = replace_extension(filename, force_ext, info_dict.get("ext"))
return filename
except ValueError as err:
self.report_error("Error in output template: " + str(err) + " (encoding: " + repr(preferredencoding()) + ")")
return None
def new_validate_options(opts):
def validate(cndn, name, value=None, msg=None):
if cndn:
return True
raise ValueError((msg or 'invalid {name} "{value}" given').format(name=name, value=value))
def validate_regex(name, value, regex):
return validate(value is None or re.match(regex, value), name, value)
ret = old_validate_options(opts)
validate_regex("trim filenames", opts.trim_file_name, r"(?:\d+[bc]?|none)")
return ret
old_create_parser = options.create_parser
def create_parser_patched():
parser = old_create_parser()
filesystem = parser.get_option_group("--trim-filenames")
filesystem.remove_option("--trim-filenames")
filesystem.add_option(
"--trim-filenames",
"--trim-file-names",
metavar="LENGTH",
dest="trim_file_name",
default="none",
help="Limit the filename length (excluding extension) to the specified number of characters or bytes",
)
return parser
YoutubeDL.evaluate_outtmpl = evaluate_outtmpl
YoutubeDL._prepare_filename = _prepare_filename
yt_dlp.__init__.validate_options = new_validate_options
options.create_parser = create_parser_patched

View File

@@ -2,15 +2,7 @@
client_id =
auth_token =
path = .
name_format = {title}
playlist_name_format = {playlist[title]}_{title}
name_format = [%(id)s] %(uploader)s - %(title)s.%(ext)s
playlist_name_format = %(playlist_index)s. %(uploader)s - %(title)s.%(ext)s
# example name_format values:
# {timestamp}_{user[username]}_{title}
# {id}_{user[username]}_{title}
# {id}_{user[id]}_{title}
# list of all BasicTrack attributes can be found at: https://github.com/7x11x13/soundcloud.py/blob/main/soundcloud/resource/track.py#L35
# playlist_name_format playlist attributes:
# playlist[author] - username of playlist author
# playlist[title] - name of playlist
# For name formats see https://github.com/yt-dlp/yt-dlp/?tab=readme-ov-file#output-template

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +1,49 @@
from logging import Logger
import yt_dlp
import yt_dlp.options
"""Copied from
https://github.com/davidfischer-ch/pytoolbox/blob/master/pytoolbox/logging.py
https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/devscripts/cli_to_api.py
"""
import email.message
import logging
import re
from types import MappingProxyType
from typing import Dict, Optional
from termcolor import colored
__all__ = ("ColorizeFilter",)
_create_parser = yt_dlp.options.create_parser
class ColorizeFilter(logging.Filter):
COLOR_BY_LEVEL = MappingProxyType(
def _parse_patched_options(opts):
patched_parser = _create_parser()
patched_parser.defaults.update(
{
logging.DEBUG: "blue",
logging.WARNING: "yellow",
logging.ERROR: "red",
logging.INFO: "white",
},
"ignoreerrors": False,
"retries": 0,
"fragment_retries": 0,
"extract_flat": False,
"concat_playlist": "never",
}
)
def filter(self, record: logging.LogRecord) -> bool:
record.raw_msg = record.msg
color = self.COLOR_BY_LEVEL.get(record.levelno)
if color:
record.msg = colored(record.msg, color) # type: ignore[arg-type]
return True
yt_dlp.options.create_parser = lambda: patched_parser
try:
return yt_dlp.parse_options(opts)
finally:
yt_dlp.options.create_parser = _create_parser
def size_in_bytes(insize: str) -> int:
"""Return the size in bytes from strings such as '5 mb' into 5242880.
>>> size_in_bytes('1m')
1048576
>>> size_in_bytes('1.5m')
1572864
>>> size_in_bytes('2g')
2147483648
>>> size_in_bytes(None)
Traceback (most recent call last):
raise ValueError('no string specified')
ValueError: no string specified
>>> size_in_bytes('')
Traceback (most recent call last):
raise ValueError('no string specified')
ValueError: no string specified
"""
if insize is None or insize.strip() == "":
raise ValueError("no string specified")
units = {
"k": 1024,
"m": 1024**2,
"g": 1024**3,
"t": 1024**4,
"p": 1024**5,
}
match = re.search(r"^\s*([0-9\.]+)\s*([kmgtp])?", insize, re.IGNORECASE)
if match is None:
raise ValueError("match not found")
size, unit = match.groups()
if size:
size = float(size)
if unit:
size = size * units[unit.lower().strip()]
return int(size)
_default_opts = _parse_patched_options([]).ydl_opts
def parse_header(content_disposition: Optional[str]) -> Dict[str, str]:
if not content_disposition:
return {}
message = email.message.Message()
message["content-type"] = content_disposition
return dict(message.get_params({}))
def cli_to_api(opts):
opts = yt_dlp.parse_options(opts).ydl_opts
diff = {k: v for k, v in opts.items() if _default_opts[k] != v}
if "postprocessors" in diff:
diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in _default_opts["postprocessors"]]
return diff
class YTLogger(Logger):
def debug(self, msg: object, *args, **kwargs):
# For compatibility with youtube-dl, both debug and info are passed into debug
# You can distinguish them by the prefix '[debug] '
if isinstance(msg, str) and msg.startswith("[debug] "):
super().debug(msg, *args, **kwargs)
else:
self.info(msg, *args, **kwargs)

View File

@@ -1,65 +0,0 @@
from os import path
from setuptools import find_packages, setup
import scdl
this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
long_description = f.read()
setup(
name="scdl",
version=scdl.__version__,
packages=find_packages(),
include_package_data=True,
author="FlyinGrub",
author_email="flyinggrub@gmail.com",
description="Download Music from Souncloud",
long_description=long_description,
long_description_content_type="text/markdown",
install_requires=[
"docopt-ng",
"mutagen>=1.45.0",
"termcolor",
"requests",
"tqdm",
"pathvalidate",
"soundcloud-v2>=1.5.2",
"filelock>=3.0.0",
"typing_extensions; python_version < '3.11'",
],
extras_require={
"dev": [
"pytest",
"pytest-cov",
"pytest-dotenv",
"music-tag",
"ruff",
"mypy",
"types-requests",
"types-tqdm",
],
},
url="https://github.com/flyingrub/scdl",
classifiers=[
"Programming Language :: Python",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Internet",
"Topic :: Multimedia :: Sound/Audio",
],
python_requires=">=3.7",
entry_points={
"console_scripts": [
"scdl = scdl.scdl:main",
],
},
)

View File

@@ -0,0 +1,3 @@
import pytest
pytest.register_assert_rewrite("tests.utils")

View File

@@ -51,22 +51,6 @@ def test_playlist(tmp_path: Path) -> None:
assert_track_playlist_2(tmp_path)
def test_n(tmp_path: Path) -> None:
os.chdir(tmp_path)
r = call_scdl_with_auth(
"-l",
"https://soundcloud.com/one-thousand-and-one/sets/test-playlist/s-ZSLfNrbPoXR",
"--playlist-name-format",
"{playlist[tracknumber]}_{title}",
"--onlymp3",
"-n",
"1",
)
assert r.returncode == 0
assert_track(tmp_path / "test playlist", "1_test track 2.mp3", check_metadata=False)
assert_not_track(tmp_path / "test playlist", "2_testing - test track.mp3")
def test_offset(tmp_path: Path) -> None:
os.chdir(tmp_path)
r = call_scdl_with_auth(
@@ -111,7 +95,7 @@ def test_no_strict_playlist(tmp_path: Path) -> None:
"--playlist-name-format",
"{playlist[tracknumber]}_{title}",
"--onlymp3",
"--max-size=10kb",
"--max-size=10k",
)
assert r.returncode == 0
assert_not_track(tmp_path / "test playlist", "1_testing - test track.mp3")
@@ -142,18 +126,18 @@ def test_sync(tmp_path: Path) -> None:
"https://soundcloud.com/7x11x13/wan-bushi-eurodance-vibes-part-123",
"--onlymp3",
"--name-format",
"{title}",
"remove_this",
"--path",
"test playlist",
)
assert r.returncode == 0
assert_track(
tmp_path / "test playlist",
"Wan Bushi - Eurodance Vibes (part 1+2+3).mp3",
"remove_this.mp3",
check_metadata=False,
)
with open("archive.txt", "w", encoding="utf-8") as f:
f.writelines(["1032303631"])
f.writelines(["soundcloud 1032303631 ./test playlist/remove_this.mp3"])
r = call_scdl_with_auth(
"-l",
"https://soundcloud.com/one-thousand-and-one/sets/test-playlist/s-ZSLfNrbPoXR",
@@ -163,6 +147,9 @@ def test_sync(tmp_path: Path) -> None:
"archive.txt",
)
assert r.returncode == 0
assert_not_track(tmp_path / "test playlist", "Wan Bushi - Eurodance Vibes (part 1+2+3).mp3")
assert_not_track(tmp_path / "test playlist", "remove_this.mp3")
with open("archive.txt") as f:
assert f.read().split() == ["1855267053", "1855318536"]
assert [line.split()[1] for line in f.read().splitlines()] == [
"1855267053",
"1855318536",
]

View File

@@ -1,10 +1,9 @@
import math
import os
from pathlib import Path
import pytest
from tests.utils import assert_not_track, assert_track, call_scdl_with_auth
from tests.utils import assert_track, call_scdl_with_auth
@pytest.mark.skipif(not os.getenv("AUTH_TOKEN"), reason="No auth token specified")
@@ -33,7 +32,8 @@ def test_original_to_stdout(tmp_path: Path) -> None:
with open("track.wav", "wb") as f:
assert isinstance(r.stdout, bytes)
f.write(r.stdout)
assert_track(tmp_path, "track.wav", "copy", "saves", None)
# https://github.com/yt-dlp/yt-dlp/issues/8815
assert_track(tmp_path, "track.wav", "copy", "saves", None, check_metadata=False)
def test_mp3_to_stdout(tmp_path: Path) -> None:
@@ -52,7 +52,8 @@ def test_mp3_to_stdout(tmp_path: Path) -> None:
assert isinstance(r.stdout, bytes)
f.write(r.stdout)
assert_track(tmp_path, "track.mp3")
# https://github.com/yt-dlp/yt-dlp/issues/8815
assert_track(tmp_path, "track.mp3", check_metadata=False)
@pytest.mark.skipif(not os.getenv("AUTH_TOKEN"), reason="No auth token specified")
@@ -72,7 +73,8 @@ def test_flac_to_stdout(tmp_path: Path) -> None:
f.write(r.stdout)
assert r.returncode == 0
assert_track(tmp_path, "track.flac", "copy", "saves", None)
# https://github.com/yt-dlp/yt-dlp/issues/8815
assert_track(tmp_path, "track.flac", "copy", "saves", None, check_metadata=False)
@pytest.mark.skipif(not os.getenv("AUTH_TOKEN"), reason="No auth token specified")
@@ -107,7 +109,7 @@ def test_m4a(tmp_path: Path) -> None:
tmp_path,
"track.m4a",
"Wan Bushi - Eurodance Vibes (part 1+2+3)",
"7x11x13",
"Wan Bushi",
"Electronic",
None,
)
@@ -208,6 +210,14 @@ def test_force_metadata(tmp_path: Path) -> None:
assert r.returncode == 0
assert_track(tmp_path, "track.wav", "og title", "og artist", "og genre", 0)
r = call_scdl_with_auth(
"-l",
"https://soundcloud.com/violinbutterflynet/original",
"--name-format",
"track",
)
assert_track(tmp_path, "track.wav", "og title", "og artist", "og genre", 0)
r = call_scdl_with_auth(
"-l",
"https://soundcloud.com/violinbutterflynet/original",
@@ -263,10 +273,10 @@ def test_maxsize(tmp_path: Path) -> None:
"-l",
"https://soundcloud.com/one-thousand-and-one/test-track",
"--onlymp3",
"--max-size=10kb",
"--max-size=10k",
)
assert r.returncode == 1
assert "not within --min-size=0 and --max-size=10240" in r.stderr
assert r.returncode == 0
assert "format is not available" in r.stderr
def test_minsize(tmp_path: Path) -> None:
@@ -275,10 +285,10 @@ def test_minsize(tmp_path: Path) -> None:
"-l",
"https://soundcloud.com/one-thousand-and-one/test-track",
"--onlymp3",
"--min-size=1mb",
"--min-size=1m",
)
assert r.returncode == 1
assert f"not within --min-size={1024**2} and --max-size={math.inf}" in r.stderr
assert r.returncode == 0
assert "format is not available" in r.stderr
def test_only_original(tmp_path: Path) -> None:
@@ -288,8 +298,8 @@ def test_only_original(tmp_path: Path) -> None:
"https://soundcloud.com/one-thousand-and-one/test-track-2/s-fgLQFAzNIMP",
"--only-original",
)
assert r.returncode == 1
assert "does not have original file available" in r.stderr
assert r.returncode == 0
assert "Requested format is not available" in r.stderr
def test_overwrite(tmp_path: Path) -> None:
@@ -310,8 +320,8 @@ def test_overwrite(tmp_path: Path) -> None:
"track",
"--onlymp3",
)
assert r.returncode == 1
assert "already exists" in r.stderr
assert r.returncode == 0
assert "has already been downloaded" in r.stderr
r = call_scdl_with_auth(
"-l",
@@ -322,6 +332,7 @@ def test_overwrite(tmp_path: Path) -> None:
"--overwrite",
)
assert r.returncode == 0
assert "Deleting existing file" in r.stderr
def test_path(tmp_path: Path) -> None:
@@ -338,30 +349,6 @@ def test_path(tmp_path: Path) -> None:
assert_track(tmp_path, "track.mp3", check_metadata=False)
def test_remove(tmp_path: Path) -> None:
os.chdir(tmp_path)
r = call_scdl_with_auth(
"-l",
"https://soundcloud.com/one-thousand-and-one/test-track",
"--name-format",
"track",
"--onlymp3",
)
assert r.returncode == 0
assert_track(tmp_path, "track.mp3", check_metadata=False)
r = call_scdl_with_auth(
"-l",
"https://soundcloud.com/one-thousand-and-one/test-track-2/s-fgLQFAzNIMP",
"--name-format",
"track2",
"--remove",
"--onlymp3",
)
assert r.returncode == 0
assert_track(tmp_path, "track2.mp3", check_metadata=False)
assert_not_track(tmp_path, "track.mp3")
def test_download_archive(tmp_path: Path) -> None:
os.chdir(tmp_path)
r = call_scdl_with_auth(
@@ -383,8 +370,8 @@ def test_download_archive(tmp_path: Path) -> None:
"--onlymp3",
"--download-archive=archive.txt",
)
assert r.returncode == 1
assert "already exists" in r.stderr
assert r.returncode == 0
assert "already been recorded in the archive" in r.stderr
def test_description_file(tmp_path: Path) -> None:
@@ -402,3 +389,16 @@ def test_description_file(tmp_path: Path) -> None:
assert desc_file.exists()
with open(desc_file, encoding="utf-8") as f:
assert f.read().splitlines() == ["test description:", "9439290883"]
def test_trim_filenames(tmp_path: Path) -> None:
os.chdir(tmp_path)
r = call_scdl_with_auth(
"-l",
"https://soundcloud.com/one-thousand-and-one/test-track",
"--name-format",
"a" * 500,
"--onlymp3",
)
assert r.returncode == 0
assert_track(tmp_path, "a" * 240 + ".mp3")

View File

@@ -14,12 +14,10 @@ def test_all(tmp_path: Path) -> None:
"-l",
"https://soundcloud.com/one-thousand-and-one",
"-a",
"-o",
"3",
"--onlymp3",
)
assert r.returncode == 0
assert count_files(tmp_path) == 3
assert count_files(tmp_path) == 5
def test_tracks(tmp_path: Path) -> None:

468
uv.lock generated Normal file
View File

@@ -0,0 +1,468 @@
version = 1
requires-python = ">=3.9.0"
[[package]]
name = "certifi"
version = "2024.8.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 },
{ url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 },
{ url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 },
{ url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 },
{ url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 },
{ url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 },
{ url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 },
{ url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 },
{ url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 },
{ url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 },
{ url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 },
{ url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 },
{ url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 },
{ url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 },
{ url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 },
{ url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 },
{ url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 },
{ url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 },
{ url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 },
{ url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 },
{ url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 },
{ url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 },
{ url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 },
{ url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 },
{ url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 },
{ url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 },
{ url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 },
{ url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 },
{ url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 },
{ url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 },
{ url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 },
{ url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 },
{ url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 },
{ url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 },
{ url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 },
{ url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 },
{ url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 },
{ url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 },
{ url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 },
{ url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 },
{ url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 },
{ url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 },
{ url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 },
{ url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 },
{ url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 },
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
{ url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 },
{ url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 },
{ url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 },
{ url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 },
{ url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 },
{ url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 },
{ url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 },
{ url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 },
{ url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 },
{ url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 },
{ url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 },
{ url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 },
{ url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 },
{ url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 },
{ url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 },
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "dacite"
version = "1.8.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/0f/cf0943f4f55f0fbc7c6bd60caf1343061dff818b02af5a0d444e473bb78d/dacite-1.8.1-py3-none-any.whl", hash = "sha256:cc31ad6fdea1f49962ea42db9421772afe01ac5442380d9a99fcf3d188c61afe", size = 14309 },
]
[[package]]
name = "docopt-ng"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/50/8d6806cf13138127692ae6ff79ddeb4e25eb3b0bcc3c1bd033e7e04531a9/docopt_ng-0.9.0.tar.gz", hash = "sha256:91c6da10b5bb6f2e9e25345829fb8278c78af019f6fc40887ad49b060483b1d7", size = 32264 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/4a/c3b77fc1a24510b08918b43a473410c0168f6e657118807015f1f1edceea/docopt_ng-0.9.0-py3-none-any.whl", hash = "sha256:bfe4c8b03f9fca424c24ee0b4ffa84bf7391cb18c29ce0f6a8227a3b01b81ff9", size = 16689 },
]
[[package]]
name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "music-tag"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mutagen" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/f4/ebcdd2fc9bfaf569b795250090e4f4088dc65a5a3e32c53baa9bfc3fc296/music-tag-0.4.3.tar.gz", hash = "sha256:0aab6e6eeda8df0f5316ec2d2190bd74561b7e03562ab091ce8d5687cdbcfff6", size = 23153 }
[[package]]
name = "mutagen"
version = "1.47.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391 },
]
[[package]]
name = "mypy"
version = "1.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 },
{ url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 },
{ url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 },
{ url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 },
{ url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 },
{ url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 },
{ url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 },
{ url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 },
{ url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 },
{ url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
{ url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 },
{ url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 },
{ url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 },
{ url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 },
{ url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 },
{ url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
{ url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
{ url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
{ url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
{ url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
{ url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 },
{ url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 },
{ url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 },
{ url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 },
{ url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 },
{ url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
[[package]]
name = "pytest"
version = "8.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
]
[[package]]
name = "pytest-dotenv"
version = "0.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/b0/cafee9c627c1bae228eb07c9977f679b3a7cb111b488307ab9594ba9e4da/pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732", size = 3782 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/da/9da67c67b3d0963160e3d2cbc7c38b6fae342670cc8e6d5936644b2cf944/pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f", size = 3993 },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "ruff"
version = "0.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/5e/683c7ef7a696923223e7d95ca06755d6e2acbc5fd8382b2912a28008137c/ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3", size = 3378522 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/c4/bfdbb8b9c419ff3b52479af8581026eeaac3764946fdb463dec043441b7d/ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6", size = 10535860 },
{ url = "https://files.pythonhosted.org/packages/ef/c5/0aabdc9314b4b6f051168ac45227e2aa8e1c6d82718a547455e40c9c9faa/ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939", size = 10346327 },
{ url = "https://files.pythonhosted.org/packages/1a/78/4843a59e7e7b398d6019cf91ab06502fd95397b99b2b858798fbab9151f5/ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d", size = 9942585 },
{ url = "https://files.pythonhosted.org/packages/91/5a/642ed8f1ba23ffc2dd347697e01eef3c42fad6ac76603be4a8c3a9d6311e/ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13", size = 10797597 },
{ url = "https://files.pythonhosted.org/packages/30/25/2e654bc7226da09a49730a1a2ea6e89f843b362db80b4b2a7a4f948ac986/ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18", size = 10307244 },
{ url = "https://files.pythonhosted.org/packages/c0/2d/a224d56bcd4383583db53c2b8f410ebf1200866984aa6eb9b5a70f04e71f/ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502", size = 11362439 },
{ url = "https://files.pythonhosted.org/packages/82/01/03e2857f9c371b8767d3e909f06a33bbdac880df17f17f93d6f6951c3381/ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d", size = 12078538 },
{ url = "https://files.pythonhosted.org/packages/af/ae/ff7f97b355da16d748ceec50e1604a8215d3659b36b38025a922e0612e9b/ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82", size = 11616172 },
{ url = "https://files.pythonhosted.org/packages/6a/d0/6156d4d1e53ebd17747049afe801c5d7e3014d9b2f398b9236fe36ba4320/ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452", size = 12919886 },
{ url = "https://files.pythonhosted.org/packages/4e/84/affcb30bacb94f6036a128ad5de0e29f543d3f67ee42b490b17d68e44b8a/ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd", size = 11212599 },
{ url = "https://files.pythonhosted.org/packages/60/b9/5694716bdefd8f73df7c0104334156c38fb0f77673d2966a5a1345bab94d/ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20", size = 10784637 },
{ url = "https://files.pythonhosted.org/packages/24/7e/0e8f835103ac7da81c3663eedf79dec8359e9ae9a3b0d704bae50be59176/ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc", size = 10390591 },
{ url = "https://files.pythonhosted.org/packages/27/da/180ec771fc01c004045962ce017ca419a0281f4bfaf867ed0020f555b56e/ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060", size = 10894298 },
{ url = "https://files.pythonhosted.org/packages/6d/f8/29f241742ed3954eb2222314b02db29f531a15cab3238d1295e8657c5f18/ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea", size = 11275965 },
{ url = "https://files.pythonhosted.org/packages/79/e9/5b81dc9afc8a80884405b230b9429efeef76d04caead904bd213f453b973/ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964", size = 8807651 },
{ url = "https://files.pythonhosted.org/packages/ea/67/7291461066007617b59a707887b90e319b6a043c79b4d19979f86b7a20e7/ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9", size = 9625289 },
{ url = "https://files.pythonhosted.org/packages/03/8f/e4fa95288b81233356d9a9dcaed057e5b0adc6399aa8fd0f6d784041c9c3/ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936", size = 9078754 },
]
[[package]]
name = "scdl"
version = "3.0.0"
source = { editable = "." }
dependencies = [
{ name = "docopt-ng" },
{ name = "mutagen" },
{ name = "soundcloud-v2" },
{ name = "yt-dlp" },
]
[package.dev-dependencies]
dev = [
{ name = "music-tag" },
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-dotenv" },
{ name = "ruff" },
{ name = "types-requests" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
[package.metadata]
requires-dist = [
{ name = "docopt-ng", specifier = ">=0.9.0" },
{ name = "mutagen", specifier = ">=1.47.0" },
{ name = "soundcloud-v2", specifier = ">=1.6.0" },
{ name = "yt-dlp", specifier = ">=2025.2.19" },
]
[package.metadata.requires-dev]
dev = [
{ name = "music-tag", specifier = ">=0.4.3" },
{ name = "mypy", specifier = ">=1.13.0" },
{ name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-dotenv", specifier = ">=0.5.2" },
{ name = "ruff", specifier = ">=0.8.3" },
{ name = "types-requests", specifier = ">=2.32.0.20241016" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.12.2" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "soundcloud-v2"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dacite" },
{ name = "python-dateutil" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3b/bb/ba779b3cb9597ddf88bb7b31bd6d2a984f972ee3a8f198d32935540058a7/soundcloud-v2-1.6.0.tar.gz", hash = "sha256:462513146c0ffc9ec729c1c616f4f72b0dcd33f81478c64207f265f072e78243", size = 16361 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/3b/0945ba33081a8c165d7400d22c61968cf4d2472cdc121062842b38952ef9/soundcloud_v2-1.6.0-py3-none-any.whl", hash = "sha256:5e3a6cfcec80f59a5ca6fa631d9bde75140b5472892249197178e5229b402cdb", size = 20684 },
]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
]
[[package]]
name = "types-requests"
version = "2.32.0.20241016"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fa/3c/4f2a430c01a22abd49a583b6b944173e39e7d01b688190a5618bd59a2e22/types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", size = 18065 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/01/485b3026ff90e5190b5e24f1711522e06c79f4a56c8f4b95848ac072e20f/types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747", size = 15836 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "urllib3"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
]
[[package]]
name = "yt-dlp"
version = "2025.2.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/36/ef300ba4a228b74612d4013b43ed303a0d6d2de17a71fc37e0b821577e0a/yt_dlp-2025.2.19.tar.gz", hash = "sha256:f33ca76df2e4db31880f2fe408d44f5058d9f135015b13e50610dfbe78245bea", size = 2929199 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/45/6d1b759e68f5363b919828fb0e0c167a1cd5003b5b7c74cc0f0c2096be4f/yt_dlp-2025.2.19-py3-none-any.whl", hash = "sha256:3ed218eaeece55e9d715afd41abc450dc406ee63bf79355169dfde312d38fdb8", size = 3186543 },
]