feat: upload metadata, unify auth
This commit is contained in:
140
api.py
140
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 <access_token>`` (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/<playlist_id>")
|
||||
@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 <access_token>`` (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__":
|
||||
|
||||
Reference in New Issue
Block a user