Working queue and info json

This commit is contained in:
2026-09-06 16:31:38 -06:00
parent f7477a4656
commit 0f6a62335f
2 changed files with 253 additions and 39 deletions
+166 -22
View File
@@ -1,32 +1,45 @@
from dataclasses import dataclass
from flask import Flask, render_template, request, jsonify from flask import Flask, render_template, request, jsonify
from threading import Thread from threading import RLock, Thread
import time import time
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from yt_dlp.utils import DownloadError from yt_dlp.utils import DownloadError
from collections.abc import Callable
import pathlib import pathlib
import os import os
import shutil import shutil
def rip_worker(rip_queue: list[str]): @dataclass
project_root_path = pathlib.Path(__file__).parent.resolve() class ReleaseInfo:
tmp_path = f"{project_root_path}/tmp" artist: str
title: str
status: str
while True:
queue_is_empty = len(rip_queue) == 0
if queue_is_empty:
continue
# Creates tmp directory, if it already exists, it clears it, and makes a new one. class YtdlpErrorCode(Exception):
if not os.path.isdir(tmp_path): def __init__(self, code):
os.mkdir(tmp_path) super().__init__(f"Some videos failed with code {code}")
self.code = code
def create_or_wipe_folder(path: str) -> None:
"""Creates tmp directory, if it already exists, it clears it, and makes a new one."""
if not os.path.isdir(path):
os.mkdir(path)
else: else:
shutil.rmtree(tmp_path) shutil.rmtree(path)
os.mkdir(tmp_path) os.mkdir(path)
current_url = rip_queue[0]
try: def get_content_json(url: str):
with YoutubeDL({"playlist_items": "1"}) as ydl:
return ydl.extract_info(url, download=False)
def download_content(path: str, url: str) -> None:
with YoutubeDL( with YoutubeDL(
{ {
"format": "m4a/bestaudio/best", "format": "m4a/bestaudio/best",
@@ -36,30 +49,161 @@ def rip_worker(rip_queue: list[str]):
"preferredcodec": "m4a", "preferredcodec": "m4a",
} }
], ],
"outtmpl": f"{tmp_path}/%(title)s.%(ext)s", "outtmpl": f"{path}/%(title)s.%(ext)s",
} }
) as ydl: ) as ydl:
error_code = ydl.download([current_url]) error_code = ydl.download([url])
if error_code:
raise YtdlpErrorCode(error_code)
def find_tmp_path() -> str:
project_root_path = pathlib.Path(__file__).parent.resolve()
return f"{project_root_path}/tmp"
def rip_worker(
rip_queue: list[str], status_callback: Callable[[ReleaseInfo],], finish_callback: Callable[[ReleaseInfo],]
):
tmp_path = find_tmp_path()
while True:
if len(rip_queue) == 0:
time.sleep(1)
continue
create_or_wipe_folder(tmp_path)
current_url = rip_queue.pop(0)
release_info = ReleaseInfo("", current_url, "Starting")
status_callback(release_info)
is_album = False
is_topic = False
# Info json --------------------------------------------------
release_info.status = "Gathering Info"
status_callback(release_info)
info = get_content_json(current_url)
entries = info.get("entries")
if entries is None:
release_info.status = "Entries is none"
finish_callback(release_info)
continue
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
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: except DownloadError:
print("Download Error") release_info.status = "Content Download Failed"
finish_callback(release_info)
continue
rip_queue.pop(0) except YtdlpErrorCode:
time.sleep(1) release_info.status = "Content Download Failed with code"
finish_callback(release_info)
continue
release_info.status = "Writting Metadata"
status_callback(release_info)
release_info.status = "Finished Successfully"
finish_callback(release_info)
if __name__ == "__main__":
app = Flask(__name__) app = Flask(__name__)
rip_queue = [] rip_queue = []
ripping_thread = Thread(target=rip_worker, args=(rip_queue,), daemon=True).start() finished_queue = []
current_release_info = ReleaseInfo("", "", "Waiting")
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):
global current_release_info
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()
@app.route("/") @app.route("/")
def index(): def index():
return render_template("index.html") return render_template("index.html")
@app.route("/rip", methods=["POST"]) @app.route("/rip", methods=["POST"])
def rip(): def rip():
url = request.form.get("url") url = request.form.get("url")
rip_queue.append(url) rip_queue.append(url)
return jsonify({"success": True, "message": f"Added, {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} for q in finished_queue],
"currentReleaseInfo": {
"artist": current_release_info.artist,
"title": current_release_info.title,
"status": current_release_info.status,
},
}
)
app.run()
+70
View File
@@ -30,7 +30,47 @@
</form> </form>
</div> </div>
</br>
<div class="container">
<h2>Current</h2>
<p id="status-message">Waiting</p>
</div>
</br>
<div class="container">
<h2>Queue</h2>
<table class="table" id="queue-table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">URL</th>
</tr>
</thead>
<tbody id="queue-table-body"></tbody>
</table>
</div>
</br>
<div class="container">
<h2>Finished</h2>
<table class="table" id="finish-table">
<thead>
<tr>
<th scope="col">Artist</th>
<th scope="col">Title</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="finish-table-body"></tbody>
</table>
</div>
<script> <script>
const ripFormInput = document.getElementById("urlInput");
document document
.getElementById("rip-form") .getElementById("rip-form")
.addEventListener("submit", async function (e) { .addEventListener("submit", async function (e) {
@@ -44,8 +84,38 @@
}); });
const data = await response.json(); const data = await response.json();
ripFormInput.value = "";
}); });
const queueTableBody = document.getElementById("queue-table-body");
const finishTableBody = document.getElementById("finish-table-body");
const statusMessage = document.getElementById("status-message");
async function status() {
const response = await fetch("/status", { method: "GET" });
const statusJson = await response.json();
const currentInfo = statusJson.currentReleaseInfo;
statusMessage.innerText = `${currentInfo.title} by ${currentInfo.artist} | ${currentInfo.status}`;
queueTableBody.innerHTML = "";
var i = 1;
for (const url of statusJson.queue) {
queueTableBody.innerHTML += `<tr><td>${i}</td><td>${url}</td></tr>`;
i ++;
}
finishTableBody.innerHTML = "";
for (const q of statusJson.finishedQueue) {
finishTableBody.innerHTML += `<tr><td>${q.artist}</td><td>${q.title}</td><td>${q.status}</td></tr>`;
}
}
setInterval(status, 100);
</script> </script>
<script <script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"