]> littlesong.place Git - littlesongplace.git/commitdiff
More work on tags, collabs, uploads
authorChris Fulljames <christianfulljames@gmail.com>
Sun, 5 Jan 2025 14:37:31 +0000 (09:37 -0500)
committerChris Fulljames <christianfulljames@gmail.com>
Sun, 5 Jan 2025 14:37:31 +0000 (09:37 -0500)
main.py
schema.sql
templates/base.html
templates/login.html
templates/profile.html
templates/signup.html

diff --git a/main.py b/main.py
index edbce88a32c2cd9cb245c3cf8e123a98866cdf58..6b67547c11e97f8e0f6f6d0fd9000f46777d1389 100644 (file)
--- a/main.py
+++ b/main.py
@@ -1,13 +1,16 @@
 import os
+import shutil
 import sqlite3
+import subprocess
 import sys
+import tempfile
 import uuid
 from pathlib import Path, PosixPath
 
 import bcrypt
 import click
 from flask import Flask, render_template, request, redirect, g, session, abort, \
-        send_from_directory
+        send_from_directory, flash
 from werkzeug.utils import secure_filename
 
 ################################################################################
@@ -35,22 +38,27 @@ def signup_post():
     password = request.form["password"]
     password_confirm = request.form["password_confirm"]
 
-    error = None
+    error = False
     if not username.isidentifier():
-        error = "Username cannot contain special characters"
+        flash("Username cannot contain special characters", "error")
+        error = True
     elif len(username) < 3:
-        error ="Username must be at least 3 characters"
+        flash("Username must be at least 3 characters", "error")
+        error = True
 
     elif password != password_confirm:
-        error = "Passwords do not match"
+        flash("Passwords do not match", "error")
+        error = True
     elif len(password) < 8:
-        error = "Password must be at least 8 characters"
+        flash("Password must be at least 8 characters", "error")
+        error = True
 
     if query_db("select * from users where username = ?", [username], one=True):
-        error = f"Username '{username}' is already taken"
+        flash(f"Username '{username}' is already taken", "error")
+        error = True
 
     if error:
-        return render_template("signup.html", error=error)
+        return redirect(request.referrer)
 
     password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
     query_db("insert into users (username, password) values (?, ?)", [username, password])
@@ -76,7 +84,8 @@ def login_post():
         session.permanent = True
         return redirect("/")
 
-    return render_template("login.html", error="Invalid username/password")
+    flash("Invalid username/password", "error")
+    return render_template("login.html")
 
 @app.get("/logout")
 def logout():
@@ -104,8 +113,20 @@ def users_profile(profile_username):
     # Get songs for current profile
     profile_userid = profile_data["userid"]
     profile_songs_data = query_db("select * from songs where userid = ?", [profile_userid])
-
-    return render_template("profile.html", name=profile_username, username=username, songs=profile_songs_data)
+    profile_songs_tags = {}
+    profile_songs_collabs = {}
+    for song in profile_songs_data:
+        songid = song["songid"]
+        profile_songs_tags[songid] = query_db("select (tag) from song_tags where songid = ?", [songid])
+        profile_songs_collabs[songid] = query_db("select (name) from song_collaborators where songid = ?", [songid])
+
+    return render_template(
+            "profile.html",
+            name=profile_username,
+            username=username,
+            songs=profile_songs_data,
+            songs_tags=profile_songs_tags,
+            songs_collaborators=profile_songs_collabs)
 
 @app.post("/uploadsong")
 def upload_song():
@@ -122,62 +143,71 @@ def upload_song():
     title = request.form["title"]
     description = request.form["description"]
 
-    error = None
+    error = False
+
+    # Check if title is valid
+    if not title.isprintable():
+        flash(f"'{title}' is not a valid song title", "error")
+        error = True
+
+    # Check if description is valid
+    if not description.isprintable():
+        flash(f"Description contains invalid characters", "error")
+        error = True
 
     # Check if tags are valid
     tags = request.form["tags"]
     tags = [t.strip() for t in tags.split(",")]
     for tag in tags:
-        if not tag.isidentifier():
-            error = f"'{tag}' is not a valid tag name"
-            break
+        if not tag.isprintable():
+            flash(f"'{tag}' is not a valid tag name", "error")
+            error = True
 
     # Check if collaborators are valid
     collaborators = request.form["collabs"]
     collaborators = [c.strip() for c in collaborators.split(",")]
-    collab_ids = {}
     for collab in collaborators:
-        # Check if @user exists
-        if collab.startswith("@"):
-            collab_user_data = query_db("select * from users where username = ?", [collab[1:]], one=True)
-            if collab_user_data is None:
-                error = f"Invalid collaborator username: {collab}"
-                break
+        if not collab.isprintable():
+            flash(f"'{collab}' is not a valid collaborator name", "error")
+            error = True
+
+    # Validate and save mp3 file
+    if not error:
+        with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
+            file.save(tmp_file)
+            tmp_file.close()
+
+            result = subprocess.run(["mpck", tmp_file.name], stdout=subprocess.PIPE)
+            lines = result.stdout.decode().split("\r\n")
+            lines = [l.strip().lower() for l in lines]
+            passed = any(l.startswith("result") and l.endswith("ok") for l in lines)
+
+            if not passed:
+                flash("Invalid mp3 file", "error")
             else:
-                collab_ids[collab] = collab_user_data["userid"]
+                # Create song
+                song_data = query_db(
+                        "insert into songs (userid, title, description) values (?, ?, ?) returning (songid)",
+                        [userid, title, description], one=True)
+                songid = song_data["songid"]
+                filepath = userpath / (str(song_data["songid"]) + ".mp3")
 
-        # Check if valid name
-        elif not collab.isprintable():
-            error = f"Invalid collaborator name: {collab}"
-            break
+                # Move file to permanent location
+                shutil.move(tmp_file.name, filepath)
 
-    # TODO: Handle errors above
-    # TODO: Validate song file
+                # Assign tags
+                for tag in tags:
+                    query_db("insert into song_tags (tag, songid) values (?, ?)", [tag, songid])
 
-    # Create song
-    song_data = query_db(
-            "insert into songs (userid, title, description) values (?, ?, ?) returning (songid)",
-            [userid, title, description], one=True)
-    songid = song_data["songid"]
+                # Assign collaborators
+                for collab in collaborators:
+                    query_db("insert into song_collaborators (songid, name) values (?, ?)", [songid, collab])
 
-    # Assign tags
-    for tag in tags:
-        query_db("insert into song_tags (tag, songid) values (?, ?)", [tag, songid])
-
-    # List collaborators
-    for collab in collaborators:
-        if collab.startswith("@"):
-            collab_id = collab_ids[collab]
-            query_db("insert into song_collaborators (songid, userid) values (?, ?)", [songid, collab_id])
-        else:
-            query_db("insert into song_collaborators (songid, name) values (?, ?)", [songid, collab])
-
-    get_db().commit()
+                get_db().commit()
 
-    filepath = userpath / (str(song_data["songid"]) + ".mp3")
-    file.save(filepath)
+                flash(f"Successfully uploaded '{title}'", "success")
 
-    return redirect(f"/users/{username}")
+    return redirect(request.referrer)
 
 @app.get("/song/<userid>/<songid>")
 def song(userid, songid):
index e7b8f22d7c345c6478873c3d4f831bfc87422ee5..2dcc41d2ef9fd697d28c79cd2c033501922103f9 100644 (file)
@@ -16,19 +16,18 @@ CREATE TABLE songs (
 
 DROP TABLE IF EXISTS song_collaborators;
 CREATE TABLE song_collaborators (
-    collabid INTEGER NOT NULL,
     songid INTEGER NOT NULL,
-    userid INTEGER,
-    name TEXT,
-    FOREIGN KEY(userid) REFERENCES users(userid),
-    PRIMARY KEY(collabid, songid),
-    CONSTRAINT userid_or_name CHECK ((userid IS NULL and name IS NOT NULL) OR (userid IS NOT NULL and name IS NULL))
+    name TEXT NOT NULL,
+    FOREIGN KEY(songid) REFERENCES songs(songid),
+    PRIMARY KEY(songid, name)
 );
 
 DROP TABLE IF EXISTS song_tags;
 CREATE TABLE song_tags (
-    tag TEXT NOT NULL,
     songid INTEGER NOT NULL,
+    tag TEXT NOT NULL,
     FOREIGN KEY(songid) REFERENCES songs(songid),
-    PRIMARY KEY(tag, songid)
+    PRIMARY KEY(songid, tag)
 );
+CREATE INDEX idx_song_tags_tag ON song_tags(tag);
+
index 1ab2582654d06e956ddbd06d294a412b34c2af51..70ac676b49c9f16bd4f9b2b5121f0934fb5e4c63 100644 (file)
             <a href="/">Home</a>
         </div>
 
+        {% with messages = get_flashed_messages(with_categories=True) %}
+        {% if messages %}
+        <ul class="flashes">
+            {% for category, message in messages %}
+            <li class="flash-msg {{ category }}">{{ message }}</li>
+            {% endfor %}
+        </ul>
+        {% endif %}
+        {% endwith %}
+
         {% block body %}
         {% endblock %}
     </body>
index 6b490c19f203783797a56eab1afa088dde5e24ea..8ed110bf22e63770aa87830eb5f0c465eb4a26c7 100644 (file)
@@ -24,9 +24,5 @@
     </div>
 </form>
 
-{% if error %}
-<div class="login-error">{{ error }}</div>
-{% endif %}
-
 {% endblock %}
 
index 92fc9155a72f5c6110f7b94c09d8df7472ebd9d4..fb41cca48941e823c0db9e095ff12c94bbcc37ac 100644 (file)
     <div class="song-title">{{ song["title"] }}</div>
     <audio src="/song/{{ song["userid"] }}/{{ song["songid"] }}" controls></audio>
     <div class="song-description">{{ song["description"] }}</div>
+    <div class="song-tags">
+        {% for tag in songs_tags[song["songid"]] %}
+        <a href="/songs-by-tag/{{ tag["tag"] }}">{{ tag["tag"] }}</a>
+        {% endfor %}
+    </div>
+    <div class="song-collabs">
+        {% for collab in songs_collaborators[song["songid"]] %}
+            {% if collab["name"].startswith("@") %}
+            <a href="/users/{{ collab["name"][1:] }}" class="collab-link">{{ collab["name"] }}</a>
+            {% else %}
+            <span class="collab-name">{{ collab["name"] }}</span>
+            {% endif %}
+        {% endfor %}
+    </div>
 </div>
 {% endfor %}
 
index 5f7a07f470dad05d18e15fb5f7436f45718a97e7..ed7fc0eacf95973b398d163b6077ab2b7108471e 100644 (file)
     </div>
 </form>
 
-{% if error %}
-<div class="signup-error">Error: {{ error }}</div>
-{% endif %}
-
 {% endblock %}