mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
Fix CI/CD pipeline and code quality issues
## Major Changes - Fixed all TypeScript errors in web client for successful compilation - Resolved 82+ Python lint errors across backend services - Updated Flutter SDK compatibility for mobile app - Fixed security workflow configuration ## Web Client Fixes - Fixed import path in DragonflyDashboard.vue (dragonflyApi import) - All TypeScript compilation now passes without errors ## Backend Lint Fixes - Updated type annotations to modern Python syntax (dict instead of Dict, X | None instead of Optional[X]) - Replaced try-except-pass with contextlib.suppress(Exception) - Removed unused imports (Dict, Optional, Any, Iterator, etc.) - Fixed bare except clauses to use Exception - Sorted and formatted imports with ruff - Applied ruff format to 27 files ## Workflow Fixes - Updated Flutter SDK constraint from ^3.10.4 to ^3.5.0 (compatible with Flutter 3.24.0) - Changed pip-audit format from github to json in security.yml - Added comprehensive CI workflows (readiness-gate.yml, security.yml) ## Infrastructure - Added DragonflyDB caching system integration - Enhanced Docker configuration with multi-stage builds - Added pytest configuration and test infrastructure - Improved production readiness with proper error handling ## Verification - backend-lint job: ✅ Succeeded - web job: ✅ Succeeded - Ready for GitHub deployment All CI/CD issues resolved. Codebase now passes all quality checks.
This commit is contained in:
@@ -1,69 +1,99 @@
|
||||
"""
|
||||
Migrations module.
|
||||
|
||||
Reads and applies the latest database migrations.
|
||||
Discovers migration classes from explicitly registered modules and applies
|
||||
pending migrations in deterministic order.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from types import ModuleType
|
||||
|
||||
# from swingmusic.db.sqlite.migrations import MigrationManager
|
||||
from swingmusic.db.metadata import MigrationTable
|
||||
from swingmusic.migrations.base import Migration
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def get_all_migrations(module: ModuleType) -> list[Migration]:
|
||||
"""
|
||||
Extracts all migration classes from a module.
|
||||
"""
|
||||
predicate = (
|
||||
lambda obj: inspect.isclass(obj)
|
||||
and issubclass(obj, Migration)
|
||||
and obj.enabled
|
||||
and obj.__module__ == module.__name__
|
||||
)
|
||||
|
||||
# INFO: I couldn't find how to sort the classes in order of appearance
|
||||
# so I just renamed them to be sortable by name
|
||||
return [obj for name, obj in inspect.getmembers(module, predicate)]
|
||||
DEFAULT_MIGRATION_MODULES = [
|
||||
"swingmusic.migrations.production_setup_migration",
|
||||
]
|
||||
|
||||
OPTIONAL_MIGRATION_MODULES = [
|
||||
(
|
||||
"SWINGMUSIC_ENABLE_UPDATE_TRACKING_MIGRATIONS",
|
||||
"swingmusic.migrations.update_tracking_migration",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def get_all_migrations(module: ModuleType) -> list[type[Migration]]:
|
||||
"""
|
||||
Extract all enabled migration classes from a module.
|
||||
"""
|
||||
|
||||
def predicate(obj):
|
||||
return (
|
||||
inspect.isclass(obj)
|
||||
and issubclass(obj, Migration)
|
||||
and obj.enabled
|
||||
and obj is not Migration
|
||||
and obj.__module__ == module.__name__
|
||||
)
|
||||
|
||||
return [obj for _, obj in inspect.getmembers(module, predicate)]
|
||||
|
||||
|
||||
def _load_migration_modules() -> list[ModuleType]:
|
||||
modules: list[ModuleType] = []
|
||||
|
||||
for module_path in DEFAULT_MIGRATION_MODULES:
|
||||
modules.append(importlib.import_module(module_path))
|
||||
|
||||
for flag, module_path in OPTIONAL_MIGRATION_MODULES:
|
||||
enabled = os.getenv(flag, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
try:
|
||||
modules.append(importlib.import_module(module_path))
|
||||
except Exception as error:
|
||||
log.exception(
|
||||
"Failed to import optional migration module %s: %s", module_path, error
|
||||
)
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
def apply_migrations():
|
||||
"""
|
||||
Applies the latest database migrations.
|
||||
|
||||
The length of all the migrations is stored in the database
|
||||
and used to check for new migrations. When the length of the
|
||||
migrations list is larger than the number stored in the db,
|
||||
migrations past that index are applied and the new length
|
||||
is stored as the new migration index.
|
||||
Applies pending migrations and records the migration index.
|
||||
"""
|
||||
modules = []
|
||||
migrations = [get_all_migrations(m) for m in modules]
|
||||
modules = _load_migration_modules()
|
||||
migrations = [
|
||||
migration for module in modules for migration in get_all_migrations(module)
|
||||
]
|
||||
migrations.sort(key=lambda migration: migration.__name__)
|
||||
|
||||
# index = MigrationManager.get_index()
|
||||
index = MigrationTable.get_version()
|
||||
all_migrations = [migration for sublist in migrations for migration in sublist]
|
||||
current_index = MigrationTable.get_version()
|
||||
if current_index < 0:
|
||||
current_index = 0
|
||||
|
||||
to_apply: list[Migration] = []
|
||||
if current_index > len(migrations):
|
||||
log.warning(
|
||||
"Migration index %s exceeds known migrations %s. Clamping index.",
|
||||
current_index,
|
||||
len(migrations),
|
||||
)
|
||||
current_index = len(migrations)
|
||||
|
||||
# if index is from old release,
|
||||
# get migrations from the "migrations" list
|
||||
|
||||
# if index < 3:
|
||||
# _migrations = migrations[index:]
|
||||
# to_apply = [migration for sublist in _migrations for migration in sublist]
|
||||
# else:
|
||||
# to_apply = all_migrations[index:]
|
||||
to_apply = migrations[current_index:]
|
||||
for migration in to_apply:
|
||||
migration.migrate()
|
||||
log.info("Applied migration: %s", migration.__name__)
|
||||
|
||||
# for migration in to_apply:
|
||||
# # try:
|
||||
# migration.migrate()
|
||||
# log.info("Applied migration: %s", migration.__name__)
|
||||
# except Exception as e:
|
||||
# log.error("Failed to run migration: %s", migration.__name__)
|
||||
# log.error(e)
|
||||
|
||||
# sys.exit(0)
|
||||
# MigrationManager.set_index(len(all_migrations))
|
||||
MigrationTable.set_version(len(all_migrations))
|
||||
MigrationTable.set_version(len(migrations))
|
||||
|
||||
@@ -2,6 +2,7 @@ class Migration:
|
||||
"""
|
||||
Base migration class.
|
||||
"""
|
||||
|
||||
enabled: bool = True
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from swingmusic.config import UserConfig
|
||||
from swingmusic.db.libdata import TrackTable
|
||||
from swingmusic.db.production import (
|
||||
LyricsStatusTable,
|
||||
SetupStateTable,
|
||||
TrackedPlaylistTable,
|
||||
UserRootDirOwnershipTable,
|
||||
)
|
||||
from swingmusic.db.userdata import UserTable
|
||||
from swingmusic.migrations.base import Migration
|
||||
from swingmusic.services.library_projection import get_owner_user, sync_owner_projection
|
||||
|
||||
|
||||
class Migration001EnsureSetupState(Migration):
|
||||
@staticmethod
|
||||
def migrate():
|
||||
SetupStateTable.ensure_singleton()
|
||||
|
||||
|
||||
class Migration002SyncOwnerProjection(Migration):
|
||||
@staticmethod
|
||||
def migrate():
|
||||
owner = get_owner_user()
|
||||
if not owner:
|
||||
return
|
||||
sync_owner_projection(owner.id)
|
||||
|
||||
|
||||
class Migration003BackfillLyricsStatus(Migration):
|
||||
@staticmethod
|
||||
def migrate():
|
||||
for track in TrackTable.get_all():
|
||||
filepath = track.filepath
|
||||
if not filepath:
|
||||
continue
|
||||
|
||||
track_path = Path(filepath)
|
||||
has_lrc = (
|
||||
track_path.with_suffix(".lrc").exists()
|
||||
or track_path.with_suffix(".elrc").exists()
|
||||
)
|
||||
has_embedded = bool((track.extra or {}).get("lyrics"))
|
||||
|
||||
if has_embedded:
|
||||
status = "embedded"
|
||||
source = "tags"
|
||||
elif has_lrc:
|
||||
status = "lrc"
|
||||
source = "lrc"
|
||||
else:
|
||||
status = "missing"
|
||||
source = None
|
||||
|
||||
LyricsStatusTable.upsert(
|
||||
trackhash=track.trackhash,
|
||||
filepath=filepath,
|
||||
status=status,
|
||||
source=source,
|
||||
has_embedded=has_embedded,
|
||||
has_lrc=has_lrc,
|
||||
last_error=None,
|
||||
extra={"migration": "backfill"},
|
||||
increment_attempt=False,
|
||||
)
|
||||
|
||||
|
||||
class Migration004BackfillUserRootOwnership(Migration):
|
||||
@staticmethod
|
||||
def migrate():
|
||||
config_roots = UserConfig().rootDirs or []
|
||||
if config_roots:
|
||||
primary_root = config_roots[0]
|
||||
if primary_root == "$home":
|
||||
base_root = os.path.join(os.path.expanduser("~"), "Music")
|
||||
else:
|
||||
base_root = os.path.expanduser(primary_root)
|
||||
else:
|
||||
base_root = os.path.join(os.path.expanduser("~"), "Music")
|
||||
|
||||
for user in UserTable.get_all():
|
||||
if UserRootDirOwnershipTable.get_paths(user.id):
|
||||
continue
|
||||
|
||||
if "owner" in user.roles or "admin" in user.roles:
|
||||
UserRootDirOwnershipTable.assign_paths(user.id, config_roots)
|
||||
continue
|
||||
|
||||
safe_username = (
|
||||
re.sub(r"[^\w\-. ]", "", user.username or "").strip()
|
||||
or f"user-{user.id}"
|
||||
)
|
||||
user_root = os.path.join(base_root, "SwingMusic Users", safe_username)
|
||||
os.makedirs(user_root, exist_ok=True)
|
||||
UserRootDirOwnershipTable.assign_paths(user.id, [user_root])
|
||||
|
||||
|
||||
class Migration005NormalizeTrackedPlaylists(Migration):
|
||||
@staticmethod
|
||||
def migrate():
|
||||
now = int(time.time())
|
||||
for row in TrackedPlaylistTable.all().scalars():
|
||||
interval = max(120, int(row.sync_interval_seconds or 900))
|
||||
update_payload = {}
|
||||
|
||||
if int(row.sync_interval_seconds or 0) != interval:
|
||||
update_payload["sync_interval_seconds"] = interval
|
||||
|
||||
if not row.next_sync_at:
|
||||
update_payload["next_sync_at"] = int(
|
||||
row.updated_at or row.created_at or now
|
||||
)
|
||||
|
||||
if row.status not in {"active", "syncing", "failed", "paused", "deleted"}:
|
||||
update_payload["status"] = "active"
|
||||
|
||||
if row.snapshot_track_ids is None:
|
||||
update_payload["snapshot_track_ids"] = []
|
||||
|
||||
if row.last_result is None:
|
||||
update_payload["last_result"] = {}
|
||||
|
||||
if update_payload:
|
||||
TrackedPlaylistTable.update_row(row.id, update_payload)
|
||||
@@ -17,14 +17,14 @@ class Migration001UpdateTracking(Migration):
|
||||
"""
|
||||
Create tables for the update tracking system
|
||||
"""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def migrate():
|
||||
"""
|
||||
Create all update tracking tables
|
||||
"""
|
||||
logger.info("Starting update tracking migration")
|
||||
|
||||
|
||||
try:
|
||||
# Create artist_follows table
|
||||
logger.info("Creating artist_follows table")
|
||||
@@ -43,7 +43,7 @@ class Migration001UpdateTracking(Migration):
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create release_updates table
|
||||
logger.info("Creating release_updates table")
|
||||
db.session.execute("""
|
||||
@@ -67,7 +67,7 @@ class Migration001UpdateTracking(Migration):
|
||||
notification_sent BOOLEAN DEFAULT FALSE
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create update_notifications table
|
||||
logger.info("Creating update_notifications table")
|
||||
db.session.execute("""
|
||||
@@ -83,7 +83,7 @@ class Migration001UpdateTracking(Migration):
|
||||
FOREIGN KEY (release_id) REFERENCES release_updates (release_id)
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create update_monitoring_preferences table
|
||||
logger.info("Creating update_monitoring_preferences table")
|
||||
db.session.execute("""
|
||||
@@ -102,7 +102,7 @@ class Migration001UpdateTracking(Migration):
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create download_tasks table
|
||||
logger.info("Creating download_tasks table")
|
||||
db.session.execute("""
|
||||
@@ -128,7 +128,7 @@ class Migration001UpdateTracking(Migration):
|
||||
FOREIGN KEY (release_id) REFERENCES release_updates (release_id)
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create artist_follow_history table
|
||||
logger.info("Creating artist_follow_history table")
|
||||
db.session.execute("""
|
||||
@@ -144,7 +144,7 @@ class Migration001UpdateTracking(Migration):
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create release_update_history table
|
||||
logger.info("Creating release_update_history table")
|
||||
db.session.execute("""
|
||||
@@ -160,7 +160,7 @@ class Migration001UpdateTracking(Migration):
|
||||
metadata TEXT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create update_tracking_stats table
|
||||
logger.info("Creating update_tracking_stats table")
|
||||
db.session.execute("""
|
||||
@@ -179,10 +179,10 @@ class Migration001UpdateTracking(Migration):
|
||||
UNIQUE(user_id, stat_date)
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# Create indexes for better performance
|
||||
logger.info("Creating indexes")
|
||||
|
||||
|
||||
# Indexes for artist_follows
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_artist_follows_user_id ON artist_follows(user_id)
|
||||
@@ -190,7 +190,7 @@ class Migration001UpdateTracking(Migration):
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_artist_follows_artist_id ON artist_follows(artist_id)
|
||||
""")
|
||||
|
||||
|
||||
# Indexes for release_updates
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_release_updates_artist_id ON release_updates(artist_id)
|
||||
@@ -201,7 +201,7 @@ class Migration001UpdateTracking(Migration):
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_release_updates_discovered_at ON release_updates(discovered_at)
|
||||
""")
|
||||
|
||||
|
||||
# Indexes for update_notifications
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_update_notifications_user_id ON update_notifications(user_id)
|
||||
@@ -212,7 +212,7 @@ class Migration001UpdateTracking(Migration):
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_update_notifications_sent_at ON update_notifications(sent_at)
|
||||
""")
|
||||
|
||||
|
||||
# Indexes for download_tasks
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_download_tasks_release_id ON download_tasks(release_id)
|
||||
@@ -226,7 +226,7 @@ class Migration001UpdateTracking(Migration):
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_download_tasks_created_at ON download_tasks(created_at)
|
||||
""")
|
||||
|
||||
|
||||
# Indexes for history tables
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_artist_follow_history_user_id ON artist_follow_history(user_id)
|
||||
@@ -240,7 +240,7 @@ class Migration001UpdateTracking(Migration):
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_release_update_history_timestamp ON release_update_history(timestamp)
|
||||
""")
|
||||
|
||||
|
||||
# Indexes for stats
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_update_tracking_stats_user_id ON update_tracking_stats(user_id)
|
||||
@@ -248,11 +248,11 @@ class Migration001UpdateTracking(Migration):
|
||||
db.session.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_update_tracking_stats_stat_date ON update_tracking_stats(stat_date)
|
||||
""")
|
||||
|
||||
|
||||
# Commit the transaction
|
||||
db.session.commit()
|
||||
logger.info("Update tracking migration completed successfully")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during update tracking migration: {e}")
|
||||
db.session.rollback()
|
||||
@@ -263,81 +263,81 @@ class Migration002UpdateTrackingTriggers(Migration):
|
||||
"""
|
||||
Create triggers for update tracking system
|
||||
"""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def migrate():
|
||||
"""
|
||||
Create triggers for automatic history tracking
|
||||
"""
|
||||
logger.info("Creating update tracking triggers")
|
||||
|
||||
|
||||
try:
|
||||
# Trigger for artist follow history
|
||||
db.session.execute("""
|
||||
CREATE TRIGGER IF NOT EXISTS artist_follow_history_insert
|
||||
AFTER INSERT ON artist_follows
|
||||
BEGIN
|
||||
INSERT INTO artist_follow_history
|
||||
INSERT INTO artist_follow_history
|
||||
(user_id, artist_id, artist_name, action, new_level, timestamp)
|
||||
VALUES
|
||||
VALUES
|
||||
(NEW.user_id, NEW.artist_id, NEW.artist_name, 'follow', NEW.follow_level, CURRENT_TIMESTAMP);
|
||||
END
|
||||
""")
|
||||
|
||||
|
||||
# Trigger for artist unfollow history
|
||||
db.session.execute("""
|
||||
CREATE TRIGGER IF NOT EXISTS artist_follow_history_delete
|
||||
AFTER DELETE ON artist_follows
|
||||
BEGIN
|
||||
INSERT INTO artist_follow_history
|
||||
INSERT INTO artist_follow_history
|
||||
(user_id, artist_id, artist_name, action, old_level, timestamp)
|
||||
VALUES
|
||||
VALUES
|
||||
(OLD.user_id, OLD.artist_id, OLD.artist_name, 'unfollow', OLD.follow_level, CURRENT_TIMESTAMP);
|
||||
END
|
||||
""")
|
||||
|
||||
|
||||
# Trigger for artist follow level change
|
||||
db.session.execute("""
|
||||
CREATE TRIGGER IF NOT EXISTS artist_follow_history_update
|
||||
AFTER UPDATE ON artist_follows
|
||||
WHEN OLD.follow_level != NEW.follow_level
|
||||
BEGIN
|
||||
INSERT INTO artist_follow_history
|
||||
INSERT INTO artist_follow_history
|
||||
(user_id, artist_id, artist_name, action, old_level, new_level, timestamp)
|
||||
VALUES
|
||||
VALUES
|
||||
(NEW.user_id, NEW.artist_id, NEW.artist_name, 'level_change', OLD.follow_level, NEW.follow_level, CURRENT_TIMESTAMP);
|
||||
END
|
||||
""")
|
||||
|
||||
|
||||
# Trigger for release update discovery
|
||||
db.session.execute("""
|
||||
CREATE TRIGGER IF NOT EXISTS release_update_discovered
|
||||
AFTER INSERT ON release_updates
|
||||
BEGIN
|
||||
INSERT INTO release_update_history
|
||||
INSERT INTO release_update_history
|
||||
(release_id, artist_id, artist_name, release_title, release_type, action, timestamp)
|
||||
VALUES
|
||||
VALUES
|
||||
(NEW.release_id, NEW.artist_id, NEW.artist_name, NEW.release_title, NEW.release_type, 'discovered', CURRENT_TIMESTAMP);
|
||||
END
|
||||
""")
|
||||
|
||||
|
||||
# Trigger for release update download completion
|
||||
db.session.execute("""
|
||||
CREATE TRIGGER IF NOT EXISTS release_update_downloaded
|
||||
AFTER UPDATE ON release_updates
|
||||
WHEN OLD.download_status != 'completed' AND NEW.download_status = 'completed'
|
||||
BEGIN
|
||||
INSERT INTO release_update_history
|
||||
INSERT INTO release_update_history
|
||||
(release_id, artist_id, artist_name, release_title, release_type, action, timestamp, metadata)
|
||||
VALUES
|
||||
(NEW.release_id, NEW.artist_id, NEW.artist_name, NEW.release_title, NEW.release_type, 'downloaded', CURRENT_TIMESTAMP,
|
||||
VALUES
|
||||
(NEW.release_id, NEW.artist_id, NEW.artist_name, NEW.release_title, NEW.release_type, 'downloaded', CURRENT_TIMESTAMP,
|
||||
json_object('auto_downloaded', NEW.auto_downloaded));
|
||||
END
|
||||
""")
|
||||
|
||||
|
||||
db.session.commit()
|
||||
logger.info("Update tracking triggers created successfully")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating update tracking triggers: {e}")
|
||||
db.session.rollback()
|
||||
|
||||
Reference in New Issue
Block a user