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(".")
return redirect(request.referrer)
def validate_song_form():
- file = request.files["song"]
title = request.form["title"]
description = request.form["description"]
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(",")]
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
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
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()
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/<int:songid>")
def delete_song(songid):
flask
gunicorn
pillow
+yt-dlp
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;
/* 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;
}
</p>
{% if song %}
-<form action="/upload-song?songid={{ song.songid }}" method="post" enctype="multipart/form-data">
+<form action="/upload-song?songid={{ song.songid }}" method="post" enctype="multipart/form-data" onsubmit="onUpload()">
<h2>Edit Song</h2>
{% else %}
-<form action="/upload-song" method="post" enctype="multipart/form-data">
+<form action="/upload-song" method="post" enctype="multipart/form-data" onsubmit="onUpload()">
<h2>Upload a New Song</h2>
{% endif %}
<div class="upload-form">
- <label for="song">{% if song %}Replace {% endif %}Audio File</label><br>
- <input type="file" name="song" id="file-select" {% if not song %}required{% endif %}>
+ <input type="radio" id="file" name="upload-type" value="file" onchange="selectUploadMethod()" checked />
+ <label for="file">Upload a song from my device</label><br/>
+ </div>
+ <div class="upload-form">
+ <input type="radio" id="yt" name="upload-type" value="yt" onchange="selectUploadMethod()" />
+ <label for="yt">Import a song from YouTube</label>
+ </div>
+ <div class="upload-form" id="audio-file">
+ <label for="song-file">{% if song %}Replace {% endif %}Audio File</label><br>
+ <input type="file" name="song-file" id="song-file" {% if not song %}required{% endif %}>
+ </div>
+ <div class="upload-form" id="yt-url" hidden>
+ <label for="song-url">YouTube URL</label><br>
+ <input type="url" name="song-url" id="song-url">
</div>
<div class="upload-form">
<label for="title">Title</label><br>
</div>
<div class="upload-form">
{% if song %}
- <input type="submit" value="Update" onclick="onUpload(event)">
+ <input type="submit" value="Update" />
{% else %}
- <input type="submit" value="Upload" onclick="onUpload(event)">
+ <input type="submit" value="Upload" />
{% endif %}
<p id="uploading" hidden>uploading...</p>
</div>
</form>
-<!-- Automatically set song name from file name -->
<script>
+// Automatically set song name from file name
document.getElementById("file-select").addEventListener("change", function(e) {
var songTitle = document.getElementById("song-title");
if (e.target.files[0] && !songTitle.value) {
}
});
+// Show uploading text on submit
function onUpload(event) {
var uploading = document.getElementById("uploading")
uploading.hidden = false;
}
+
+// Toggle YouTube import/File upload
+function selectUploadMethod() {
+ if (document.getElementById("file").checked) {
+ // Show audio file upload button
+ document.getElementById("yt-url").hidden = true;
+ document.getElementById("song-url").required = false;
+
+ document.getElementById("audio-file").hidden = false;
+ document.getElementById("song-file").required = {% if song %}false{% else %}true{% endif %};
+ }
+ else {
+ // Show youtube import URL box
+ document.getElementById("yt-url").hidden = false;
+ document.getElementById("song-url").required = {% if song %}false{% else %}true{% endif %};
+
+ document.getElementById("audio-file").hidden = true;
+ document.getElementById("song-file").required = false;
+ }
+}
+
+document.addEventListener("DOMContentLoaded", selectUploadMethod());
+
</script>
{% endblock %}
<h2>Songs</h2>
-<!-- Upload New Song button -->
+<!-- Add Song button -->
{% if session["userid"] == userid %}
<div class="profile-action">
- <a class="button" href="/edit-song">Upload New Song</a>
+ <a class="button" href="/edit-song">Add Song</a>
</div>
{% endif %}
_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
################################################################################
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")