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 %}
-
-
{% 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")