From: Chris Fulljames Date: Sat, 8 Feb 2025 14:02:33 +0000 (-0500) Subject: Add YouTube importer X-Git-Url: https://littlesong.place/gitweb/?a=commitdiff_plain;h=ed335053fd5d3099bbdc54971e38e6e560b0a809;p=littlesongplace.git Add YouTube importer --- diff --git a/main.py b/main.py index 856e062..415b194 100644 --- a/main.py +++ b/main.py @@ -23,6 +23,8 @@ from flask import Flask, render_template, request, redirect, g, session, abort, from PIL import Image, UnidentifiedImageError from werkzeug.utils import secure_filename from werkzeug.middleware.proxy_fix import ProxyFix +from yt_dlp import YoutubeDL +from yt_dlp.utils import DownloadError DB_VERSION = 2 DATA_DIR = Path(os.environ["DATA_DIR"]) if "DATA_DIR" in os.environ else Path(".") @@ -271,7 +273,6 @@ def upload_song(): return redirect(request.referrer) def validate_song_form(): - file = request.files["song"] title = request.form["title"] description = request.form["description"] @@ -327,7 +328,8 @@ def update_song(): except ValueError: abort(400) - file = request.files["song"] + file = request.files["song"] if "song" in request.files else None + yt_url = request.form["song-url"] if "song-url" in request.form else None title = request.form["title"] description = request.form["description"] tags = [t.strip() for t in request.form["tags"].split(",")] @@ -341,9 +343,9 @@ def update_song(): abort(401) error = False - if file: + if file or yt_url: with tempfile.NamedTemporaryFile(delete=False) as tmp_file: - passed = convert_song(tmp_file, file) + passed = convert_song(tmp_file, file, yt_url) if passed: # Move file to permanent location @@ -374,14 +376,15 @@ def update_song(): return error def create_song(): - file = request.files["song"] + file = request.files["song"] if "song" in request.files else None + yt_url = request.form["song-url"] if "song-url" in request.form else None title = request.form["title"] description = request.form["description"] tags = [t.strip() for t in request.form["tags"].split(",")] collaborators = [c.strip() for c in request.form["collabs"].split(",")] with tempfile.NamedTemporaryFile(delete=False) as tmp_file: - passed = convert_song(tmp_file, file) + passed = convert_song(tmp_file, file, yt_url) if not passed: return True @@ -410,9 +413,20 @@ def create_song(): flash_and_log(f"Successfully uploaded '{title}'", "success") return False -def convert_song(tmp_file, request_file): - request_file.save(tmp_file) - tmp_file.close() +def convert_song(tmp_file, request_file, yt_url): + if request_file: + # Get uploaded file + request_file.save(tmp_file) + tmp_file.close() + else: + # Import from YouTube + tmp_file.close() + os.unlink(tmp_file.name) # Delete file so yt-dlp doesn't complain + try: + yt_import(tmp_file, yt_url) + except DownloadError: + flash_and_log(f"Failed to import from YouTube URL: {yt_url}") + return False result = subprocess.run(["mpck", tmp_file.name], stdout=subprocess.PIPE) res_stdout = result.stdout.decode() @@ -439,6 +453,14 @@ def convert_song(tmp_file, request_file): flash_and_log("Invalid audio file", "error") return False +def yt_import(tmp_file, yt_url): + ydl_opts = { + 'format': 'm4a/bestaudio/best', + 'outtmpl': tmp_file.name, + } + with YoutubeDL(ydl_opts) as ydl: + ydl.download([yt_url]) + @app.get("/delete-song/") def delete_song(songid): diff --git a/requirements.txt b/requirements.txt index 3d48ce7..4a8e1a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ bleach[css] flask gunicorn pillow +yt-dlp diff --git a/static/styles.css b/static/styles.css index 28d5ed1..130bc63 100644 --- a/static/styles.css +++ b/static/styles.css @@ -42,7 +42,7 @@ textarea { margin: 10px; } -input[type=text], input[type=password] { +input[type=text], input[type=password], input[type=url] { background: var(--yellow); margin: 10px; font-family: sans-serif; @@ -151,7 +151,11 @@ div.navbar { /* Upload/Edit Form */ -div.upload-form input[type=text] { +div.upload-form { + margin-bottom: 10px; +} + +div.upload-form input[type=text], div.upload-form input[type=url] { width: calc(100% - 20px); box-sizing: border-box; } diff --git a/templates/edit-song.html b/templates/edit-song.html index 7b47df0..3299a72 100644 --- a/templates/edit-song.html +++ b/templates/edit-song.html @@ -11,15 +11,27 @@ Most standard audio/video formats are supported - .wav, .mp3, .ogg, .mp4, etc.

{% if song %} -
+

Edit Song

{% else %} - +

Upload a New Song

{% endif %}
-
- + +
+
+
+ + +
+
+
+ +
+

@@ -39,16 +51,16 @@ Most standard audio/video formats are supported - .wav, .mp3, .ogg, .mp4, etc.
{% if song %} - + {% else %} - + {% endif %}
- {% endblock %} diff --git a/templates/profile.html b/templates/profile.html index b27278e..eb41271 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -112,10 +112,10 @@

Songs

- + {% if session["userid"] == userid %} {% endif %} diff --git a/test/test_offline.py b/test/test_offline.py index 2ff9a38..9d25fdf 100644 --- a/test/test_offline.py +++ b/test/test_offline.py @@ -303,6 +303,22 @@ def test_upload_song_from_mp4(client): _create_user(client, "user", "password", login=True) _test_upload_song(client, b"Successfully uploaded 'song title'", filename="sample-4s.mp4") +def test_upload_song_from_youtube(client): + _create_user(client, "user", "password", login=True) + data = { + "song-url": "https://youtu.be/5e5Z6gZWiEs", + "title": "song title", + "description": "song description", + "tags": "tag", + "collabs": "collab", + } + response = client.post("/upload-song", data=data) + assert response.status_code == 302 + + response = client.get(f"/users/user") + assert response.status_code == 200 + assert b"Successfully uploaded 'song title'" in response.data + ################################################################################ # Edit Song ################################################################################ @@ -337,6 +353,22 @@ def test_update_song_success(client): with open("sample-6s.mp3", "rb") as expected_file: assert response.data == expected_file.read() +def test_update_song_from_youtube(client): + _create_user_and_song(client) + data = { + "song-url": "https://youtu.be/5e5Z6gZWiEs", + "title": "song title", + "description": "song description", + "tags": "tag", + "collabs": "collab", + } + response = client.post("/upload-song?songid=1", data=data) + assert response.status_code == 302 + + response = client.get(f"/users/user") + assert response.status_code == 200 + assert b"Successfully updated 'song title'" in response.data + def test_update_song_bad_title(client): _create_user_and_song(client) _test_upload_song(client, b"not a valid song title", error=True, songid=1, title="\r\n")