diff --git a/.gitignore b/.gitignore index e4c9a2a..3fe7769 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ build/ *.swp *~ tmp +Music +cookies.txt diff --git a/happy_ripper.py b/happy_ripper.py index 6d2e10e..7067b78 100644 --- a/happy_ripper.py +++ b/happy_ripper.py @@ -1,6 +1,7 @@ from dataclasses import dataclass import pathlib import os +import re import shutil import urllib.request from threading import Thread @@ -16,6 +17,8 @@ from mutagen.mp3 import MP3 from mutagen.id3 import ID3, APIC from flask import Flask, render_template, request, jsonify +MUSIC_FOLDER = "Music" + @dataclass class ReleaseInfo: @@ -39,10 +42,11 @@ class RipError(Exception): class MetadataPostProcessor(PostProcessor): - def __init__(self, info: ReleaseInfo, art_path: str): + def __init__(self, info: ReleaseInfo, art_path: str, on_finish: Callable): super().__init__() self.info = info self.art_path = art_path + self.on_finish = on_finish def run(self, information): file = pathlib.Path(information["filepath"]) @@ -77,6 +81,7 @@ class MetadataPostProcessor(PostProcessor): ) id3audio.save() + self.on_finish() return [], information @@ -101,6 +106,7 @@ def download_content(path: str, url: str, post_processor: PostProcessor) -> None { "format": "bestaudio/best", "ignoreerrors": True, + "restrictfilenames": True, "postprocessors": [ { "key": "FFmpegExtractAudio", @@ -172,6 +178,50 @@ def find_tmp_path() -> str: return f"{project_root_path}/tmp" +def sort_into_music(path: str, info: ReleaseInfo) -> None: + try: + for file_name in os.listdir(path): + if file_name == "cover.jpg": + continue + + audio = EasyID3(os.path.join(path, file_name)) + artist_list = audio.get("artist") + if artist_list is None: + raise RipError("Artist tag was None") + + artist = artist_list[0] + re.sub(r"[^\w_. -]", "_", artist) + title = info.title + re.sub(r"[^\w_. -]", "_", title) + + artist_path = os.path.join(MUSIC_FOLDER, artist) + release_path = os.path.join(MUSIC_FOLDER, artist, title) + + if not os.path.exists(artist_path): + os.makedirs(artist_path) + + if not os.path.exists(release_path): + os.mkdir(release_path) + + dst_file_path = os.path.join(release_path, file_name) + dst_file_path_cover = os.path.join(release_path, "cover.jpg") + + if os.path.exists(dst_file_path): + os.remove(dst_file_path) + + if os.path.exists(dst_file_path_cover): + os.remove(dst_file_path_cover) + + source_file_path = os.path.join(path, file_name) + shutil.move(source_file_path, release_path) + + source_file_path_cover = os.path.join(path, "cover.jpg") + shutil.copy(source_file_path_cover, release_path) + + except Exception: + raise RipError("Filesystem error") + + def rip_worker(rip_queue: list[str], status_callback: Callable[[ReleaseInfo | str, bool],]): tmp_path = find_tmp_path() @@ -195,12 +245,28 @@ def rip_worker(rip_queue: list[str], status_callback: Callable[[ReleaseInfo | st if not os.path.exists(art_path): raise RipError("Art download failed") - status_callback("Downloading content", False) - download_content(tmp_path, url, MetadataPostProcessor(info, art_path)) + status_callback("Downloading content (0)", False) + + on_index = 0 + + def on_download_finish() -> None: + nonlocal on_index + on_index += 1 + status_callback(f"Downloading content ({on_index})", False) + + download_content(tmp_path, url, MetadataPostProcessor(info, art_path, on_download_finish)) + if len(os.listdir(tmp_path)) == 0: + raise RipError("No content was downloaded") + + status_callback("Sorting content", False) + sort_into_music(tmp_path, info) except RipError as e: status_callback(e.msg, True) + except Exception: + status_callback("Unknown Error", True) + else: status_callback("Finished sucessfully", True)