feat(api): finish Group A stubbed endpoints
Implements previously-stubbed /api/v1 endpoints (hexagonal: ports -> repos
-> services -> routers wired in deps):
- playlists: GET /{id}/cover (serves stored cover, 404 when absent)
- settings: GET/PATCH /settings + GET/PUT /settings/scrobbling — lazy
per-user row, write-only Fernet-encrypted scrobble session key; adds
user_settings table + migration (chains off dc126696f5a6)
- storage: GET /duplicates, /broken, /missing-metadata + admin POST
/cleanup (arq cleanup_storage worker; reconciles local refs only,
guarded against a storage-outage mass delete)
- admin: GET /services, /sources, /settings + POST /reindex; PATCH
/settings and /sources/{source} return 501 (config is env-managed)
Adds NotSupportedError (-> HTTP 501). Integration tests for each surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""user_settings: per-user preferences + scrobbling config
|
||||
|
||||
Revision ID: 20260728_user_settings
|
||||
Revises: dc126696f5a6
|
||||
Create Date: 2026-07-28 10:00:00.000000
|
||||
|
||||
Adds the ``user_settings`` table (1:1 with ``users``, PK = user_id): general
|
||||
preferences (theme, stream quality) plus scrobbling config. The scrobbler
|
||||
session key is stored Fernet-encrypted, never in plaintext.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260728_user_settings"
|
||||
down_revision: str | None = "dc126696f5a6"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"user_settings",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("theme", sa.String(length=16), nullable=False),
|
||||
sa.Column("stream_quality", sa.String(length=16), nullable=False),
|
||||
sa.Column("scrobble_enabled", sa.Boolean(), nullable=False),
|
||||
sa.Column("scrobble_provider", sa.String(length=16), nullable=True),
|
||||
sa.Column("scrobble_username", sa.String(length=255), nullable=True),
|
||||
sa.Column("scrobble_session_key_enc", sa.String(length=512), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
name=op.f("fk_user_settings_user_id_users"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("user_id", name=op.f("pk_user_settings")),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("user_settings")
|
||||
Reference in New Issue
Block a user