#!/usr/bin/python3 import os import sys import yt_dlp from dataclasses import dataclass def _match_filter(info: dict, *, incomplete) -> str | None: _duration = info.get("duration") _duration_min = 60 if _duration and int(_duration) < _duration_min: return "Duration too short: < _duration_min" return None @dataclass(frozen=True) class Collection: """A music collection.""" title: str links: frozenset[str] def __eq__(self, other) -> bool: if isinstance(other, Collection): return self.title == other.title raise NotImplementedError def parse_raw_to_collections(raw_data: list[str]) -> frozenset[Collection]: collections: set[Collection] = set() _collection_data: list[str] = [] for index, line in enumerate(raw_data): if line.startswith("#"): continue elif line == "" or index + 1 == len(raw_data): if len(_collection_data) == 0: continue collections.add( Collection(_collection_data[0], frozenset(_collection_data[1:])) ) _collection_data.clear() else: _collection_data.append(line) return frozenset(collections) def get_ytdlp_options(output_dir: str) -> dict: return { "format": "bestaudio/best", "match_filter": _match_filter, "postprocessors": [ { "key": "FFmpegExtractAudio", #"preferredcodec": "m4a", }, { "key": "FFmpegMetadata", "add_metadata": True, }, { "key": "EmbedThumbnail", "already_have_thumbnail": False, }, ], "outtmpl": f"{output_dir}/%(title)s.%(ext)s", "restrictfilenames": True, "ignoreerrors": True, } def download_collection(collection: Collection, parent_dir: str) -> None: output_dir = os.path.join(parent_dir, collection.title) if os.path.isdir(output_dir): return os.makedirs(output_dir, exist_ok=True) with yt_dlp.YoutubeDL(get_ytdlp_options(output_dir)) as downloader: downloader.download(collection.links) def main() -> int: # input handling if len(sys.argv) != 2: return 1 with open(sys.argv[1], "r") as file: filedata = file.read().splitlines() for collection in parse_raw_to_collections(filedata): download_collection(collection, os.getcwd()) return 0 if __name__ == "__main__": exit(main())