diff --git a/api.py b/api.py index 686de82..3306f1a 100644 --- a/api.py +++ b/api.py @@ -17,13 +17,15 @@ # $ while sleep 1; do diff -q api.py /tmp/api.py; if [ $? -ne 0 ]; then scp api.py lockstep@api.lockstep.at:/var/sites/api.lockstep.at/; cp api.py /tmp/api.py; fi; done import os +import re import sqlite3 from base64 import b64encode from datetime import datetime, timedelta, timezone import json +from functools import wraps from urllib.parse import urlencode -from flask import Flask, request, session, jsonify, make_response, redirect, url_for +from flask import Flask, g, request, session, jsonify, make_response, redirect, url_for from werkzeug.middleware.proxy_fix import ProxyFix from authomatic import Authomatic from authomatic.adapters import WerkzeugAdapter @@ -114,6 +116,7 @@ ALLOWED_SPOTIFY_APP_POST_LOGIN_REDIRECT = os.environ.get( ) DB_PATH = os.environ.get("TOKEN_DB_PATH", "spotify_tokens.db") +METADATA_UPLOAD_DIR = os.environ.get("METADATA_UPLOAD_DIR", "uploaded_collections") app = Flask(__name__) app.secret_key = os.environ["FLASK_SECRET_KEY"] @@ -156,6 +159,17 @@ def init_db(): updated_at TEXT NOT NULL ) """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS uploaded_metadata ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + spotify_user_id TEXT NOT NULL, + track_id TEXT NOT NULL, + type TEXT NOT NULL, + version INTEGER NOT NULL, + file_name TEXT NOT NULL, + created_at TEXT NOT NULL + ) + """) conn.commit() conn.close() @@ -629,28 +643,59 @@ old example 1: } """ -@app.route("/me") -def me(): + +def spotify_access_token_from_authorization_header(): + auth = request.headers.get("Authorization", "") or "" + if not auth.startswith("Bearer "): + return None + token = auth[7:].strip() + return token or None + + +def get_request_spotify_access_token(): + """ + Prefer ``Authorization: Bearer `` (mobile / jukebox). + Fallback to Flask session + stored refresh flow (browser). + """ + bearer = spotify_access_token_from_authorization_header() + if bearer: + return bearer spotify_user_id = session.get("spotify_user_id") if not spotify_user_id: - return jsonify({"ok": False, "error": "Not logged in"}), 401 + return None + return get_valid_access_token(spotify_user_id) - access_token = get_valid_access_token(spotify_user_id) + +def require_auth(f): + @wraps(f) + def wrapped(*args, **kwargs): + token = get_request_spotify_access_token() + if not token: + return jsonify({"ok": False, "error": "Not logged in"}), 401 + g.spotify_access_token = token + return f(*args, **kwargs) + return wrapped + + +@app.route("/me") +@require_auth +def me(): + access_token = g.spotify_access_token profile = spotify_get_me(access_token) - row = get_token_record(spotify_user_id) + row = get_token_record(profile["id"]) return jsonify({ "ok": True, "profile": profile, - "stored_expires_at": row["expires_at"], + "stored_expires_at": row["expires_at"] if row else None, }) + @app.route("/playlists") +@require_auth def playlists(): - access_token = get_request_spotify_access_token() - if not access_token: - return jsonify({"ok": False, "error": "Not logged in"}), 401 + access_token = g.spotify_access_token """ user_id = "Sara" @@ -670,10 +715,9 @@ def playlists(): @app.route("/playlists/") +@require_auth def playlist(playlist_id): - access_token = get_request_spotify_access_token() - if not access_token: - return jsonify({"ok": False, "error": "Not logged in"}), 401 + access_token = g.spotify_access_token playlist_data = spotify_get( f"https://api.spotify.com/v1/playlists/{playlist_id}", @@ -709,26 +753,60 @@ def playlist(playlist_id): }) -def get_request_spotify_access_token(): - """ - Prefer ``Authorization: Bearer `` (mobile / jukebox). - Fallback to Flask session + stored refresh flow (browser). - """ - bearer = spotify_access_token_from_authorization_header() - if bearer: - return bearer - spotify_user_id = session.get("spotify_user_id") - if not spotify_user_id: - return None - return get_valid_access_token(spotify_user_id) +@app.route("/metadata", methods=["POST"]) +@require_auth +def upload_metadata(): + access_token = g.spotify_access_token + if not request.is_json: + return jsonify({"ok": False, "error": "Expected application/json"}), 400 -def spotify_access_token_from_authorization_header(): - auth = request.headers.get("Authorization", "") or "" - if not auth.startswith("Bearer "): - return None - token = auth[7:].strip() - return token or None + body = request.get_json(silent=True) + if not isinstance(body, dict): + return jsonify({"ok": False, "error": "Invalid JSON body"}), 400 + + track_id = body.get("trackId") + meta_type = body.get("type") + version = body.get("version") + collection = body.get("collection") + + if not track_id or not meta_type or version is None or collection is None: + return jsonify({ + "ok": False, + "error": "Missing trackId, type, version, or collection", + }), 400 + + try: + version = int(version) + except (TypeError, ValueError): + return jsonify({"ok": False, "error": "version must be an integer"}), 400 + + if not isinstance(collection, dict): + return jsonify({"ok": False, "error": "collection must be a JSON object"}), 400 + + profile = spotify_get_me(access_token) + spotify_user_id = profile["id"] + + os.makedirs(METADATA_UPLOAD_DIR, exist_ok=True) + ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") + safe_track = re.sub(r"[^\w\-]", "_", str(track_id))[:120] + file_name = f"{spotify_user_id}_{safe_track}_{meta_type}_{version}_{ts}.json" + file_path = os.path.join(METADATA_UPLOAD_DIR, file_name) + + with open(file_path, "w", encoding="utf-8") as f: + json.dump(collection, f, ensure_ascii=False) + + now = datetime.now(timezone.utc).isoformat() + conn = db() + conn.execute(""" + INSERT INTO uploaded_metadata ( + spotify_user_id, track_id, type, version, file_name, created_at + ) VALUES (?, ?, ?, ?, ?, ?) + """, (spotify_user_id, track_id, meta_type, version, file_name, now)) + conn.commit() + conn.close() + + return jsonify({"ok": True, "file_name": file_name}), 201 if __name__ == "__main__":