"flask",
     "gunicorn",
     "pillow",
+    "pywebpush",
     "yt-dlp",
 ]
 requires-python = ">=3.11"
 
 from pathlib import Path
 
 import click
+import pywebpush
 from flask import Flask, render_template, request, redirect, g, session, abort, \
         send_from_directory, flash, get_flashed_messages
 from werkzeug.middleware.proxy_fix import ProxyFix
 
 from . import activity, auth, colors, comments, datadir, db, jams, playlists, \
-        profiles, songs, users
+        profiles, push_notifications, songs, users
 from .logutils import flash_and_log
 
 # Logging
 app.register_blueprint(jams.bp)
 app.register_blueprint(playlists.bp)
 app.register_blueprint(profiles.bp)
+app.register_blueprint(push_notifications.bp)
 app.register_blueprint(songs.bp)
 db.init_app(app)
 
 def about():
     return render_template("about.html")
 
+@app.get("/service.js")
+def service_worker():
+    return send_from_directory("static", "service.js")
+
 def get_gif_data():
     # Convert all .gifs to base64 strings and embed them as dataset entries
     # in <div>s.  This is used by nav.js:customImage() - it replaces specific
 
 
 from flask import abort, Blueprint, redirect, render_template, request, session
 
-from . import db, songs
+from . import auth, db, push_notifications, songs
 from .sanitize import sanitize_user_text
 
 bp = Blueprint("comments", __name__)
     return song_comments
 
 @bp.route("/comment", methods=["GET", "POST"])
+@auth.requires_login
 def comment():
-    if not "userid" in session:
-        return redirect("/login")
-
     if not "threadid" in request.args:
         abort(400) # Must have threadid
 
             if userid in notification_targets:
                 notification_targets.remove(userid)
 
-            # Create notifications
+            # Create notifications in database
             for target in notification_targets:
                 db.query(
                         """
                         """,
                         [commentid, ObjectType.COMMENT, target, timestamp])
 
+            # Send push notifications
+            push_notifications.notify(
+                    notification_targets, f"Comment from {g.username}", content)
+
         db.commit()
 
         return redirect_to_previous_page()
 
 
 from . import datadir
 
-DB_VERSION = 5
+DB_VERSION = 6
 
 def get():
     db = getattr(g, '_database', None)
 
--- /dev/null
+import json
+import threading
+
+import pywebpush
+from flask import Blueprint, current_app, g, request
+
+from . import auth, db
+
+bp = Blueprint("push-notifications", __name__, url_prefix="/push-notifications")
+
+@bp.post("/subscribe")
+@auth.requires_login
+def subscribe():
+    if not request.json:
+        # Request must contain valid subscription JSON
+        abort(400)
+
+    db.query(
+            """
+            INSERT INTO users_push_subscriptions (userid, subscription)
+            VALUES (?, ?)
+            """,
+            [g.userid, json.dumps(request.json)])
+    db.commit()
+
+    current_app.logger.info(f"{g.username} registered push subscription")
+
+    return {"status": "success"}
+
+def get_user_subscriptions(userid):
+    rows = db.query(
+            """
+            SELECT * FROM users_push_subscriptions
+            WHERE userid = ?
+            """,
+            [userid])
+    str_subs = (r["subscription"] for r in rows)
+    subs = []
+    for s in str_subs:
+        try:
+            subs.append(json.loads(s))
+        except json.decoder.JSONDecodeError:
+            current_app.logger.error(f"Invalid subscription: {s}")
+    return subs
+
+def notify_all(title, body, _except=None):
+    # Notify all users (who have notifications enabled)
+    rows = db.query("SELECT * FROM users")
+    userids = [r["userid"] for r in rows]
+    if _except in userids:
+        userids.remove(_except)
+    notify(userids, title, body)
+
+def notify(userids, title, body):
+    # Send push notifications in background thread (could take a while)
+    thread = threading.Thread(
+            target=_do_push,
+            args=(current_app._get_current_object(), userids, title, body))
+    thread.start()
+
+def _do_push(app, userids, title, body):
+    data = {"title": title, "body": body}
+    data_str = json.dumps(data)
+    for userid in userids:
+        with app.app_context():
+            subs = get_user_subscriptions(userid)
+            for sub in subs:
+                try:
+                    # TODO: Use VAPID keys
+                    pywebpush.webpush(sub, data_str)
+                except pywebpush.WebPushException as ex:
+                    current_app.logger.error(f"Failed to send push: {ex}")
+
 
 from dataclasses import dataclass
 from typing import Optional
 
-from flask import Blueprint, current_app, render_template, request, redirect, \
+from flask import Blueprint, current_app, g, render_template, request, redirect, \
         session, abort, send_from_directory
 from yt_dlp import YoutubeDL
 from yt_dlp.utils import DownloadError
 
-from . import comments, colors, datadir, db, users
+from . import auth, comments, colors, datadir, db, push_notifications, users
 from .sanitize import sanitize_user_text
 from .logutils import flash_and_log
 
     return songs
 
 @bp.get("/edit-song")
+@auth.requires_login
 def edit_song():
-    if not "userid" in session:
-        return redirect("/login")  # Must be logged in to edit
-
     song = None
 
     song_colors = users.get_user_colors(session["userid"])
     return render_template("edit-song.html", song=song, **song_colors, eventid=eventid)
 
 @bp.post("/upload-song")
+@auth.requires_login
 def upload_song():
-    if not "userid" in session:
-        return redirect("/login")  # Must be logged in to edit
-
     userid = session["userid"]
 
     error = validate_song_form()
             db.commit()
 
             flash_and_log(f"Successfully uploaded '{title}'", "success")
-            return False
+
+            # Send push notifications to all other users
+            push_notifications.notify_all(
+                    f"New song from {g.username}", title, _except=g.userid)
+
+            return False  # No error
 
 def convert_song(tmp_file, request_file, yt_url):
     if request_file:
         ydl.download([yt_url])
 
 @bp.get("/delete-song/<int:songid>")
+@auth.requires_login
 def delete_song(songid):
     song_data = db.query(
         "select * from songs where songid = ?", [songid], one=True)
 
     title TEXT NOT NULL,
     description TEXT,
     threadid INTEGER,
-    FOREIGN KEY(userid) REFERENCES users(userid)
+    eventid INTEGER,
+    FOREIGN KEY(userid) REFERENCES users(userid),
+    FOREIGN KEY(eventid) REFERENCES jam_events(eventid)
 );
 CREATE INDEX idx_songs_by_user ON songs(userid);
+CREATE INDEX idx_songs_by_eventid ON songs(eventid);
 
 DROP TABLE IF EXISTS song_collaborators;
 CREATE TABLE song_collaborators (
     PRIMARY KEY(songid, tag)
 );
 CREATE INDEX idx_song_tags_tag ON song_tags(tag);
- 
--- Old comment system (superceded by comments/comment_threads/comment_notifications
---
--- DROP TABLE IF EXISTS song_comments;
--- CREATE TABLE song_comments (
---     commentid INTEGER PRIMARY KEY,
---     songid INTEGER NOT NULL,
---     userid INTEGER NOT NULL,
---     replytoid INTEGER,
---     created TEXT NOT NULL,
---     content TEXT NOT NULL,
---     FOREIGN KEY(songid) REFERENCES songs(songid) ON DELETE CASCADE,
---     FOREIGN KEY(userid) REFERENCES users(userid) ON DELETE CASCADE
--- );
--- CREATE INDEX idx_comments_by_song ON song_comments(songid);
--- CREATE INDEX idx_comments_by_user ON song_comments(userid);
--- CREATE INDEX idx_comments_by_replyto ON song_comments(replytoid);
--- CREATE INDEX idx_comments_by_time ON song_comments(created);
--- 
--- DROP TABLE IF EXISTS song_comment_notifications;
--- CREATE TABLE song_comment_notifications (
---     notificationid INTEGER PRIMARY KEY,
---     commentid INTEGER NOT NULL,
---     targetuserid INTEGER NOT NULL,
---     FOREIGN KEY(commentid) REFERENCES song_comments(commentid) ON DELETE CASCADE,
---     FOREIGN KEY(targetuserid) REFERENCES users(userid) ON DELETE CASCADE
--- );
--- CREATE INDEX idx_song_comment_notifications_by_target ON song_comment_notifications(targetuserid);
 
 DROP TABLE IF EXISTS playlists;
 CREATE TABLE playlists (
     DELETE FROM notifications WHERE objectid = OLD.commentid AND objecttype = 0;
 END;
 
-PRAGMA user_version = 4;
+DROP TABLE IF EXISTS jams;
+CREATE TABLE jams (
+    jamid INTEGER PRIMARY KEY,
+    ownerid INTEGER NOT NULL,
+    created TEXT NOT NULL,
+    title TEXT NOT NULL,
+    description TEXT,
+    FOREIGN KEY(ownerid) REFERENCES users(userid)
+);
+
+DROP TABLE IF EXISTS jam_events;
+CREATE TABLE jam_events(
+    eventid INTEGER PRIMARY KEY,
+    jamid INTEGER NOT NULL,
+    threadid INTEGER NOT NULL,
+    created TEXT NOT NULL,
+    title TEXT NOT NULL, -- Hidden until startdate
+    startdate TEXT,
+    enddate TEXT,
+    description TEXT, -- Hidden until startdate
+    FOREIGN KEY(jamid) REFERENCES jams(jamid),
+    FOREIGN KEY(threadid) REFERENCES comment_threads(threadid)
+);
+
+PRAGMA user_version = 5;
 
 
-DROP INDEX idx_songs_by_eventid;
-ALTER TABLE songs DROP COLUMN eventid;
-DROP TABLE IF EXISTS jams;
-DROP TABLE IF EXISTS jam_events;
+DROP TABLE users_push_subscriptions;
 
-PRAGMA user_version = 4;
+PRAGMA user_version = 5;
 
 
---DROP TABLE IF EXISTS jams;
-CREATE TABLE jams (
-    jamid INTEGER PRIMARY KEY,
-    ownerid INTEGER NOT NULL,
-    created TEXT NOT NULL,
-    title TEXT NOT NULL,
-    description TEXT,
-    FOREIGN KEY(ownerid) REFERENCES users(userid)
+-- DROP TABLE IF EXISTS users_push_subscriptions
+CREATE TABLE users_push_subscriptions (
+    userid INTEGER NOT NULL,
+    subscription TEXT NOT NULL,
+    FOREIGN KEY(userid) REFERENCES users(userid) ON DELETE CASCADE
 );
 
---DROP TABLE IF EXISTS jam_events;
-CREATE TABLE jam_events(
-    eventid INTEGER PRIMARY KEY,
-    jamid INTEGER NOT NULL,
-    threadid INTEGER NOT NULL,
-    created TEXT NOT NULL,
-    title TEXT NOT NULL, -- Hidden until startdate
-    startdate TEXT,
-    enddate TEXT,
-    description TEXT, -- Hidden until startdate
-    FOREIGN KEY(jamid) REFERENCES jams(jamid),
-    FOREIGN KEY(threadid) REFERENCES comment_threads(threadid)
-);
-
-ALTER TABLE songs ADD COLUMN eventid INTEGER REFERENCES jam_events(eventid);
-CREATE INDEX idx_songs_by_eventid ON songs(eventid);
-
-PRAGMA user_version = 5;
-
+PRAGMA user_version = 6;
 
       "src": "/static/lsp_notes.png",
       "sizes": "96x96",
       "type": "image/png"
-    },
+    }
   ],
   "start_url": ".",
   "display": "standalone",
 
-document.addEventListener("DOMContentLoaded", (e) => {
+document.addEventListener("DOMContentLoaded", async (e) => {
 
     // Handle link clicks with AJAX
     document.querySelectorAll("a").forEach((anchor) => {
     });
 });
 
+async function requestNotificationPermission() {
+    const permission = await window.Notification.requestPermission();
+    if (permission === "granted") {
+        // Register service worker
+        navigator.serviceWorker.register("service.js");
+    }
+    else {
+        console.log("Did not get permission to send notifications:", permission);
+    }
+}
+
 function onLinkClick(event) {
     if (event.defaultPrevented) {
         return;
 
--- /dev/null
+self.addEventListener("activate", async () => {
+    console.log("hello?");
+    try {
+        // TODO: Use VAPID key
+        const options = {};
+        const subscription = await self.registration.pushManager.subscribe(options);
+        console.log(JSON.stringify(subscription));
+        const response = await fetch(
+            "/push-notifications/subscribe", {
+                method: "post",
+                headers: {"Content-Type": "application/json"},
+                body: JSON.stringify(subscription)
+            }
+        );
+        console.log(response);
+    }
+    catch (err) {
+        console.log("Error", err);
+    }
+
+});
+
+self.addEventListener("push", (event) => {
+    if (event.data) {
+        const data = event.data.json();
+        self.registration.showNotification(data.title, {body: data.body});
+    }
+});
+
+// TODO: handle notificationclick event
 
 
 {% block body %}
 
+<button onclick="requestNotificationPermission()" class="button">Enable Notifications</button>
+
 <h2>hello!</h2>
 <div style="display: flex; flex-direction: row; justify-content: center; gap: 10px; align-items: center;">
 <div>🎶</div>