diff --git a/.gitignore b/.gitignore index a306afb..e4c9a2a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ build/ .idea/ *.swp *~ +tmp diff --git a/happy_ripper.py b/happy_ripper.py index b27cb53..b248136 100644 --- a/happy_ripper.py +++ b/happy_ripper.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from flask import Flask, render_template, request, jsonify -from threading import RLock, Thread +from threading import Thread import time from yt_dlp import YoutubeDL from yt_dlp.utils import DownloadError, ExtractorError @@ -16,13 +16,19 @@ class ReleaseInfo: artist: str title: str art_url: str + is_album: bool + is_topic: bool + + +@dataclass +class ReleaseStatus: + info: ReleaseInfo | None status: str -class YtdlpErrorCode(Exception): - def __init__(self, code): - super().__init__(f"Some videos failed with code {code}") - self.code = code +class RipError(Exception): + def __init__(self, msg): + super().__init__(msg) def create_or_wipe_folder(path: str) -> None: @@ -41,21 +47,76 @@ def get_content_json(url: str): def download_content(path: str, url: str) -> None: - with YoutubeDL( - { - "format": "m4a/bestaudio/best", - "postprocessors": [ - { - "key": "FFmpegExtractAudio", - "preferredcodec": "m4a", - } - ], - "outtmpl": f"{path}/%(title)s.%(ext)s", - } - ) as ydl: - error_code = ydl.download([url]) - if error_code: - raise YtdlpErrorCode(error_code) + try: + with YoutubeDL( + { + "format": "m4a/bestaudio/best", + "postprocessors": [ + { + "key": "FFmpegExtractAudio", + "preferredcodec": "m4a", + } + ], + "outtmpl": f"{path}/%(title)s.%(ext)s", + } + ) as ydl: + error_code = ydl.download([url]) + if error_code: + raise RipError(f"Content download failed ({error_code})") + + except DownloadError: + raise RipError("Content Download Failed") + + except ExtractorError: + raise RipError("Content Extraction Failed") + + +def find_release_info(url: str) -> ReleaseInfo: + try: + info = get_content_json(url) + + except DownloadError: + raise RipError("Info Download Failed") + + except ExtractorError: + raise RipError("Info Extraction Failed") + + entries = info.get("entries") + if entries is None: + raise RipError("Entries is none") + + first_entry = next(iter(entries), None) + if not first_entry: + raise RipError("Entries is empty") + + artist = first_entry.get("uploader") + if artist is None: + raise RipError("Uploader tag is none") + + is_topic = False + if " - Topic" in artist: + is_topic = True + artist = artist.replace(" - Topic", "") + + title = info.get("title") + if title is None: + raise RipError("Title tag is none") + + is_album = False + if "Album - " in title: + is_album = True + title = title.replace("Album - ", "") + + thumbnails = info.get("thumbnails") + if thumbnails is None: + raise RipError("Thumbnails is none") + + art_url = thumbnails[1]["url"] + + if not is_album: + raise RipError("Failed, not an album") + + return ReleaseInfo(artist, title, art_url, is_album, is_topic) def find_tmp_path() -> str: @@ -63,9 +124,7 @@ def find_tmp_path() -> str: return f"{project_root_path}/tmp" -def rip_worker( - rip_queue: list[str], status_callback: Callable[[ReleaseInfo],], finish_callback: Callable[[ReleaseInfo],] -): +def rip_worker(rip_queue: list[str], status_callback: Callable[[ReleaseInfo | str, bool],]): tmp_path = find_tmp_path() while True: @@ -73,144 +132,55 @@ def rip_worker( time.sleep(1) continue + status_callback("Gathering Info", False) + + url = rip_queue.pop(0) create_or_wipe_folder(tmp_path) - current_url = rip_queue.pop(0) - release_info = ReleaseInfo( - "", - current_url, - "https://imgs.search.brave.com/LeS4HHKZ1oz1T15VY5MwiUjWDjLiYKj0vgRABB3D2BY/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly90My5m/dGNkbi5uZXQvanBn/LzA2Lzg2LzE5LzM0/LzM2MF9GXzY4NjE5/MzQwN19ESFp3amV5/ZEJPUjF0RURrTEF6/d00zdzVrWXN0Unp6/Qi5qcGc", - "Starting", - ) - status_callback(release_info) - - is_album = False - is_topic = False - - # Info json -------------------------------------------------- - - release_info.status = "Gathering Info" - status_callback(release_info) - try: - info = get_content_json(current_url) + info = find_release_info(url) + status_callback(info, False) - except DownloadError: - release_info.status = "Info Download Failed" - finish_callback(release_info) - continue + status_callback("Downloading Content", False) - except ExtractorError: - release_info.status = "Info Extraction Failed" - finish_callback(release_info) - continue + download_content(tmp_path, url) - entries = info.get("entries") - if entries is None: - release_info.status = "Entries is none" - finish_callback(release_info) - continue + except RipError as e: + status_callback(repr(e), True) - first_entry = next(iter(entries), None) - if not first_entry: - release_info.status = "Entries is empty" - finish_callback(release_info) - continue - - artist = first_entry.get("uploader") - if artist is None: - release_info.status = "Uploader tag is none" - finish_callback(release_info) - continue - - if " - Topic" in artist: - is_topic = True - artist = artist.replace(" - Topic", "") - - release_info.artist = artist - - title = info.get("title") - if title is None: - release_info.status = "Title tag is none" - finish_callback(release_info) - continue - - if "Album - " in title: - is_album = True - title = title.replace("Album - ", "") - - release_info.title = title - - thumbnails = info.get("thumbnails") - if thumbnails is None: - release_info.status = "Thumbnails is none" - finish_callback(release_info) - continue - - art_url = thumbnails[1]["url"] - release_info.art_url = art_url - - if not is_album: - release_info.status = "Failed, not an album" - finish_callback(release_info) - continue - - release_info.status = "Finished gathering info" - status_callback(release_info) - - # Info json -------------------------------------------------- - - release_info.status = "Downloading" - status_callback(release_info) - - try: - download_content(tmp_path, current_url) - - except DownloadError: - release_info.status = "Content Download Failed" - finish_callback(release_info) - continue - - except YtdlpErrorCode: - release_info.status = "Content Download Failed with code" - finish_callback(release_info) - continue - - except ExtractorError: - release_info.status = "Content Extraction Failed" - finish_callback(release_info) - continue - - release_info.status = "Writting Metadata" - status_callback(release_info) - - release_info.status = "Finished Successfully" - finish_callback(release_info) + else: + status_callback("Finished Sucessfully", True) if __name__ == "__main__": app = Flask(__name__) - rip_queue = [] - finished_queue = [] + rip_queue: list[str] = [] + finished_queue: list[ReleaseStatus] = [] - current_release_info = ReleaseInfo("", "", "", "Waiting") + current_release_info: ReleaseInfo | None = None + current_release_status_message: str = "" - def get_current_release_info(release_info: ReleaseInfo): - global current_release_info - current_release_info = release_info - - def get_current_release_info_on_finish(release_info: ReleaseInfo): + def get_current_release_status(x: ReleaseInfo | str, finished: bool): global current_release_info + global current_release_status_message global finished_queue - current_release_info = ReleaseInfo("", "", "", "Waiting") - finished_queue.append(release_info) - if len(finished_queue) > 30: - finished_queue.pop(0) - ripping_thread = Thread( - target=rip_worker, args=(rip_queue, get_current_release_info, get_current_release_info_on_finish), daemon=True - ).start() + if type(x) is ReleaseInfo: + current_release_info = x + + elif type(x) is str: + current_release_status_message = x + + if finished: + finished_queue.append(ReleaseStatus(current_release_info, current_release_status_message)) + current_release_info = None + current_release_status_message = "Waiting" + + if len(finished_queue) > 30: + finished_queue.pop(0) + + ripping_thread = Thread(target=rip_worker, args=(rip_queue, get_current_release_status), daemon=True).start() @app.route("/") def index(): @@ -219,26 +189,31 @@ if __name__ == "__main__": @app.route("/rip", methods=["POST"]) def rip(): url = request.form.get("url") + if url is None: + return jsonify({"success": False, "message": "Url is None"}) + rip_queue.append(url) return jsonify({"success": True, "message": f"Added, {url}!"}) @app.route("/status", methods=["GET"]) def status(): - return jsonify( - { - "success": True, - "queue": rip_queue, - "finishedQueue": [ - {"artist": q.artist, "title": q.title, "status": q.status, "art_url": q.art_url} - for q in finished_queue - ], - "currentReleaseInfo": { - "artist": current_release_info.artist, - "title": current_release_info.title, - "status": current_release_info.status, - "art_url": current_release_info.art_url, - }, + data = {"success": True, "queue": rip_queue, "finishedQueue": [], "status": current_release_status_message} + + for q in finished_queue: + if q.info is None: + continue + + data["finishedQueue"].append( + {"artist": q.info.artist, "title": q.info.title, "status": q.status, "art_url": q.info.art_url} + ) + + if current_release_info is not None: + data["currentReleaseInfo"] = { + "artist": current_release_info.artist, + "title": current_release_info.title, + "art_url": current_release_info.art_url, } - ) + + return jsonify(data) app.run() diff --git a/templates/index.html b/templates/index.html index efc5b5a..92bf346 100644 --- a/templates/index.html +++ b/templates/index.html @@ -106,8 +106,13 @@ const response = await fetch("/status", { method: "GET" }); const statusJson = await response.json(); - const currentInfo = statusJson.currentReleaseInfo; - statusMessage.innerHTML = createCard(currentInfo.artist, currentInfo.title, currentInfo.status, currentInfo.art_url); + if ("currentReleaseInfo" in statusJson) { + const currentInfo = statusJson.currentReleaseInfo; + statusMessage.innerHTML = createCard(currentInfo.artist, currentInfo.title, statusJson.status, currentInfo.art_url); + } + else { + statusMessage.innerHTML = `
${statusJson.status}
`; + } queueTableBody.innerHTML = ""; diff --git a/tmp/Can't Stand Losing You.m4a b/tmp/Can't Stand Losing You.m4a deleted file mode 100644 index b793fda..0000000 Binary files a/tmp/Can't Stand Losing You.m4a and /dev/null differ diff --git a/tmp/De Do Do Do, De Da Da Da.m4a b/tmp/De Do Do Do, De Da Da Da.m4a deleted file mode 100644 index 71011a0..0000000 Binary files a/tmp/De Do Do Do, De Da Da Da.m4a and /dev/null differ diff --git a/tmp/Don't Stand So Close To Me.m4a b/tmp/Don't Stand So Close To Me.m4a deleted file mode 100644 index 7f0d8c5..0000000 Binary files a/tmp/Don't Stand So Close To Me.m4a and /dev/null differ diff --git a/tmp/Every Breath You Take.m4a b/tmp/Every Breath You Take.m4a deleted file mode 100644 index aa8359f..0000000 Binary files a/tmp/Every Breath You Take.m4a and /dev/null differ diff --git a/tmp/Every Little Thing She Does Is Magic.m4a b/tmp/Every Little Thing She Does Is Magic.m4a deleted file mode 100644 index c24f0fc..0000000 Binary files a/tmp/Every Little Thing She Does Is Magic.m4a and /dev/null differ diff --git a/tmp/Invisible Sun.m4a b/tmp/Invisible Sun.m4a deleted file mode 100644 index 0954ea8..0000000 Binary files a/tmp/Invisible Sun.m4a and /dev/null differ diff --git a/tmp/King Of Pain.m4a b/tmp/King Of Pain.m4a deleted file mode 100644 index c4cd2b6..0000000 Binary files a/tmp/King Of Pain.m4a and /dev/null differ diff --git a/tmp/Message In A Bottle.m4a b/tmp/Message In A Bottle.m4a deleted file mode 100644 index f279f75..0000000 Binary files a/tmp/Message In A Bottle.m4a and /dev/null differ diff --git a/tmp/Roxanne.m4a b/tmp/Roxanne.m4a deleted file mode 100644 index b45e8b1..0000000 Binary files a/tmp/Roxanne.m4a and /dev/null differ diff --git a/tmp/So Lonely.m4a b/tmp/So Lonely.m4a deleted file mode 100644 index 2069798..0000000 Binary files a/tmp/So Lonely.m4a and /dev/null differ diff --git a/tmp/Spirits In The Material World.m4a b/tmp/Spirits In The Material World.m4a deleted file mode 100644 index d8a20c8..0000000 Binary files a/tmp/Spirits In The Material World.m4a and /dev/null differ diff --git a/tmp/Synchronicity II.m4a b/tmp/Synchronicity II.m4a deleted file mode 100644 index 8f47946..0000000 Binary files a/tmp/Synchronicity II.m4a and /dev/null differ diff --git a/tmp/Tea In The Sahara.m4a b/tmp/Tea In The Sahara.m4a deleted file mode 100644 index d68cd58..0000000 Binary files a/tmp/Tea In The Sahara.m4a and /dev/null differ diff --git a/tmp/The Bed's Too Big Without You.m4a b/tmp/The Bed's Too Big Without You.m4a deleted file mode 100644 index ca30259..0000000 Binary files a/tmp/The Bed's Too Big Without You.m4a and /dev/null differ diff --git a/tmp/Walking On The Moon.m4a b/tmp/Walking On The Moon.m4a deleted file mode 100644 index 1d31663..0000000 Binary files a/tmp/Walking On The Moon.m4a and /dev/null differ diff --git a/tmp/Wrapped Around Your Finger.m4a b/tmp/Wrapped Around Your Finger.m4a deleted file mode 100644 index d81d834..0000000 Binary files a/tmp/Wrapped Around Your Finger.m4a and /dev/null differ