""" Database models for Spotify downloader functionality """ from datetime import datetime from typing import Optional from sqlalchemy import ( JSON, Boolean, ForeignKey, Integer, String, Text, Float, and_, delete, func, insert, select, update, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from swingmusic.db.engine import DbEngine from swingmusic.db import Base class SpotifyDownloadTable(Base): __tablename__ = "spotify_downloads" id: Mapped[int] = mapped_column(primary_key=True) spotify_url: Mapped[str] = mapped_column(String(500), unique=True, nullable=False, index=True) spotify_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True) item_type: Mapped[str] = mapped_column(String(20), nullable=False) # track, album, playlist title: Mapped[str] = mapped_column(String(500), nullable=False) artist: Mapped[str] = mapped_column(String(500), nullable=False) album: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) image_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) release_date: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) # Download settings quality: Mapped[str] = mapped_column(String(20), nullable=False, default='flac') source: Mapped[str] = mapped_column(String(20), nullable=False, default='tidal') output_dir: Mapped[str] = mapped_column(String(1000), nullable=False) # Download status status: Mapped[str] = mapped_column(String(20), nullable=False, default='pending') progress: Mapped[int] = mapped_column(Integer, default=0) file_path: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True) file_size: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # Error handling error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) retry_count: Mapped[int] = mapped_column(Integer, default=0) max_retries: Mapped[int] = mapped_column(Integer, default=3) # Metadata metadata: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # Timestamps created_at: Mapped[float] = mapped_column(Float, nullable=False) started_at: Mapped[Optional[float]] = mapped_column(Float, nullable=True) completed_at: Mapped[Optional[float]] = mapped_column(Float, nullable=True) updated_at: Mapped[float] = mapped_column(Float, nullable=False) # User association user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("user.id"), nullable=True) @classmethod def create(cls, data: dict): """Create a new Spotify download record""" if 'created_at' not in data: data['created_at'] = datetime.now().timestamp() if 'updated_at' not in data: data['updated_at'] = datetime.now().timestamp() return cls.insert_one(data) @classmethod def get_by_id(cls, download_id: int): """Get download by ID""" result = cls.execute(select(cls).where(cls.id == download_id)) res = next(result).scalar() return res @classmethod def get_by_spotify_id(cls, spotify_id: str): """Get download by Spotify ID""" result = cls.execute(select(cls).where(cls.spotify_id == spotify_id)) res = next(result).scalar() return res @classmethod def get_by_url(cls, spotify_url: str): """Get download by Spotify URL""" result = cls.execute(select(cls).where(cls.spotify_url == spotify_url)) res = next(result).scalar() return res @classmethod def get_pending_downloads(cls, limit: int = 50): """Get pending downloads""" result = cls.execute( select(cls) .where(cls.status == 'pending') .order_by(cls.created_at) .limit(limit) ) return [item for item in next(result).scalars()] @classmethod def get_active_downloads(cls): """Get currently active downloads""" result = cls.execute( select(cls) .where(cls.status.in_(['downloading', 'processing'])) .order_by(cls.started_at) ) return [item for item in next(result).scalars()] @classmethod def get_download_history(cls, user_id: Optional[int] = None, limit: int = 100, offset: int = 0): """Get download history with pagination""" query = select(cls).where(cls.status.in_(['completed', 'failed', 'cancelled'])) if user_id: query = query.where(cls.user_id == user_id) query = query.order_by(cls.created_at.desc()).offset(offset).limit(limit) result = cls.execute(query) return [item for item in next(result).scalars()] @classmethod def update_status(cls, download_id: int, status: str, **kwargs): """Update download status and related fields""" update_data = { 'status': status, 'updated_at': datetime.now().timestamp() } update_data.update(kwargs) return cls.execute( update(cls) .where(cls.id == download_id) .values(update_data), commit=True ) @classmethod def update_progress(cls, download_id: int, progress: int): """Update download progress""" return cls.execute( update(cls) .where(cls.id == download_id) .values({ 'progress': progress, 'updated_at': datetime.now().timestamp() }), commit=True ) @classmethod def increment_retry(cls, download_id: int): """Increment retry count""" return cls.execute( update(cls) .where(cls.id == download_id) .values({ 'retry_count': cls.retry_count + 1, 'updated_at': datetime.now().timestamp() }), commit=True ) @classmethod def delete_completed(cls, older_than_days: int = 30): """Delete completed downloads older than specified days""" cutoff_time = datetime.now().timestamp() - (older_than_days * 24 * 60 * 60) return cls.execute( delete(cls) .where( and_( cls.status.in_(['completed', 'failed', 'cancelled']), cls.completed_at < cutoff_time ) ), commit=True ) @classmethod def get_statistics(cls): """Get download statistics""" result = cls.execute( select( cls.status, func.count(cls.id).label('count'), func.avg(cls.duration_ms).label('avg_duration') ) .group_by(cls.status) ) stats = {} for row in next(result): stats[row.status] = { 'count': row.count, 'avg_duration_ms': row.avg_duration } return stats class SpotifyDownloadSourceTable(Base): __tablename__ = "spotify_download_sources" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) display_name: Mapped[str] = mapped_column(String(100), nullable=False) priority: Mapped[int] = mapped_column(Integer, nullable=False, default=1) is_active: Mapped[bool] = mapped_column(Boolean, default=True) config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) created_at: Mapped[float] = mapped_column(Float, nullable=False) updated_at: Mapped[float] = mapped_column(Float, nullable=False) @classmethod def get_active_sources(cls): """Get all active download sources ordered by priority""" result = cls.execute( select(cls) .where(cls.is_active == True) .order_by(cls.priority) ) return [item for item in next(result).scalars()] @classmethod def get_by_name(cls, name: str): """Get source by name""" result = cls.execute(select(cls).where(cls.name == name)) res = next(result).scalar() return res @classmethod def update_source(cls, name: str, **kwargs): """Update source configuration""" kwargs['updated_at'] = datetime.now().timestamp() return cls.execute( update(cls) .where(cls.name == name) .values(kwargs), commit=True ) class SpotifyDownloadQueueTable(Base): __tablename__ = "spotify_download_queue" id: Mapped[int] = mapped_column(primary_key=True) download_id: Mapped[int] = mapped_column(ForeignKey("spotify_downloads.id"), nullable=False) priority: Mapped[int] = mapped_column(Integer, default=0) position: Mapped[int] = mapped_column(Integer, nullable=False) added_at: Mapped[float] = mapped_column(Float, nullable=False) started_at: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # Relationship to download download = relationship("SpotifyDownloadTable", backref="queue_items") @classmethod def add_to_queue(cls, download_id: int, priority: int = 0): """Add download to queue""" # Get current max position result = cls.execute(select(func.max(cls.position))) max_position = next(result).scalar() or 0 data = { 'download_id': download_id, 'priority': priority, 'position': max_position + 1, 'added_at': datetime.now().timestamp() } return cls.insert_one(data) @classmethod def get_next_item(cls): """Get next item from queue""" result = cls.execute( select(cls) .join(SpotifyDownloadTable) .where( and_( SpotifyDownloadTable.status == 'pending', cls.started_at.is_(None) ) ) .order_by(cls.priority.desc(), cls.position) .limit(1) ) res = next(result).scalar() return res @classmethod def remove_from_queue(cls, download_id: int): """Remove item from queue""" return cls.execute( delete(cls).where(cls.download_id == download_id), commit=True ) @classmethod def get_queue_length(cls): """Get current queue length""" result = cls.execute( select(func.count(cls.id)) .join(SpotifyDownloadTable) .where(SpotifyDownloadTable.status == 'pending') ) return next(result).scalar() or 0 # Create default download sources def create_default_sources(): """Create default download sources if they don't exist""" default_sources = [ { 'name': 'tidal', 'display_name': 'Tidal', 'priority': 1, 'is_active': True, 'config': { 'quality_preference': ['lossless', 'high', 'normal'], 'formats': ['flac', 'mp3'] } }, { 'name': 'qobuz', 'display_name': 'Qobuz', 'priority': 2, 'is_active': True, 'config': { 'quality_preference': ['lossless', 'high', 'normal'], 'formats': ['flac', 'mp3'] } }, { 'name': 'amazon', 'display_name': 'Amazon Music', 'priority': 3, 'is_active': False, # Disabled by default 'config': { 'quality_preference': ['high', 'normal'], 'formats': ['mp3', 'aac'] } } ] current_time = datetime.now().timestamp() for source_data in default_sources: source_data['created_at'] = current_time source_data['updated_at'] = current_time existing = SpotifyDownloadSourceTable.get_by_name(source_data['name']) if not existing: SpotifyDownloadSourceTable.insert_one(source_data) # Add execute method (assuming it exists in the base class) # This would need to be implemented based on the existing database pattern for table_class in [SpotifyDownloadTable, SpotifyDownloadSourceTable, SpotifyDownloadQueueTable]: if not hasattr(table_class, 'execute'): @classmethod def execute_method(cls, query, commit=False): engine = DbEngine() with engine.session() as session: result = session.execute(query) if commit: session.commit() return result table_class.execute = execute_method table_class.insert_one = lambda data: table_class.execute(insert(table_class).values(data), commit=True) class GlobalCatalogCacheTable(Base): __tablename__ = "global_catalog_cache" id: Mapped[int] = mapped_column(primary_key=True) spotify_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True) item_type: Mapped[str] = mapped_column(String(50), nullable=False) # track, album, artist, playlist, search, artist_top_tracks, etc. title: Mapped[str] = mapped_column(String(500), nullable=False) artist: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) album: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) popularity: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) preview_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) image_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) release_date: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) explicit: Mapped[bool] = mapped_column(Boolean, default=False) data: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # Full metadata JSON cached_at: Mapped[float] = mapped_column(Float, nullable=False) expires_at: Mapped[float] = mapped_column(Float, nullable=False) @classmethod def create(cls, data: dict): """Create a new catalog cache entry""" if 'cached_at' not in data: data['cached_at'] = datetime.now().timestamp() return cls.insert_one(data) @classmethod def get_by_spotify_id(cls, spotify_id: str, item_type: str = None): """Get cached item by Spotify ID and optionally type""" query = select(cls).where(cls.spotify_id == spotify_id) if item_type: query = query.where(cls.item_type == item_type) query = query.where(cls.expires_at > datetime.now().timestamp()) query = query.order_by(cls.cached_at.desc()) result = cls.execute(query) res = next(result).scalar() return res @classmethod def get_expired_entries(cls): """Get all expired cache entries""" result = cls.execute( select(cls).where(cls.expires_at <= datetime.now().timestamp()) ) return [item for item in next(result).scalars()] @classmethod def delete_expired(cls): """Delete all expired cache entries""" return cls.execute( delete(cls).where(cls.expires_at <= datetime.now().timestamp()), commit=True ) @classmethod def search_cached(cls, query: str, item_types: list = None, limit: int = 20): """Search cached items by title or artist""" query_filter = select(cls).where( and_( cls.expires_at > datetime.now().timestamp(), or_( cls.title.contains(query), cls.artist.contains(query) ) ) ) if item_types: query_filter = query_filter.where(cls.item_type.in_(item_types)) query_filter = query_filter.order_by(cls.popularity.desc()).limit(limit) result = cls.execute(query_filter) return [item for item in next(result).scalars()] @classmethod def get_cache_stats(cls): """Get cache statistics""" result = cls.execute( select( cls.item_type, func.count(cls.id).label('count'), func.avg(cls.popularity).label('avg_popularity') ) .where(cls.expires_at > datetime.now().timestamp()) .group_by(cls.item_type) ) stats = {} for row in next(result): stats[row.item_type] = { 'count': row.count, 'avg_popularity': row.avg_popularity } return stats class UserCatalogPreferencesTable(Base): __tablename__ = "user_catalog_preferences" id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), nullable=False, unique=True) show_explicit: Mapped[bool] = mapped_column(Boolean, default=True) default_quality: Mapped[str] = mapped_column(String(20), default='flac') auto_download: Mapped[bool] = mapped_column(Boolean, default=False) show_suggestions: Mapped[bool] = mapped_column(Boolean, default=True) preferred_genres: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) excluded_genres: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) max_search_results: Mapped[int] = mapped_column(Integer, default=20) max_top_tracks: Mapped[int] = mapped_column(Integer, default=15) max_albums_per_artist: Mapped[int] = mapped_column(Integer, default=20) max_trending_results: Mapped[int] = mapped_column(Integer, default=20) max_recommendations: Mapped[int] = mapped_column(Integer, default=20) preferred_markets: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) cache_ttl_preference: Mapped[int] = mapped_column(Integer, default=3600) # 1 hour created_at: Mapped[float] = mapped_column(Float, nullable=False) updated_at: Mapped[float] = mapped_column(Float, nullable=False) @classmethod def get_or_create(cls, user_id: int): """Get user preferences or create with defaults""" result = cls.execute(select(cls).where(cls.user_id == user_id)) existing = next(result).scalar() if existing: return existing # Create with defaults current_time = datetime.now().timestamp() default_prefs = { 'user_id': user_id, 'show_explicit': True, 'default_quality': 'flac', 'auto_download': False, 'show_suggestions': True, 'max_search_results': 20, 'max_top_tracks': 15, 'max_albums_per_artist': 20, 'max_trending_results': 20, 'max_recommendations': 20, 'preferred_markets': ['US'], 'cache_ttl_preference': 3600, 'created_at': current_time, 'updated_at': current_time } return cls.insert_one(default_prefs) @classmethod def update_preferences(cls, user_id: int, preferences: dict): """Update user catalog preferences""" preferences['updated_at'] = datetime.now().timestamp() return cls.execute( update(cls) .where(cls.user_id == user_id) .values(preferences), commit=True ) def save(self): """Save current preferences state""" self.updated_at = datetime.now().timestamp() return self.execute( update(self.__class__) .where(self.__class__.id == self.id) .values({ 'show_explicit': self.show_explicit, 'default_quality': self.default_quality, 'auto_download': self.auto_download, 'show_suggestions': self.show_suggestions, 'preferred_genres': self.preferred_genres, 'excluded_genres': self.excluded_genres, 'max_search_results': self.max_search_results, 'max_top_tracks': self.max_top_tracks, 'max_albums_per_artist': self.max_albums_per_artist, 'max_trending_results': self.max_trending_results, 'max_recommendations': self.max_recommendations, 'preferred_markets': self.preferred_markets, 'cache_ttl_preference': self.cache_ttl_preference, 'updated_at': self.updated_at }), commit=True ) class UniversalDownloadTable(Base): __tablename__ = "universal_downloads" id: Mapped[int] = mapped_column(primary_key=True) url: Mapped[str] = mapped_column(String(1000), nullable=False, index=True) service: Mapped[str] = mapped_column(String(50), nullable=False, index=True) # spotify, tidal, apple_music, etc. service_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True) item_type: Mapped[str] = mapped_column(String(20), nullable=False) # track, album, playlist, artist title: Mapped[str] = mapped_column(String(500), nullable=False) artist: Mapped[str] = mapped_column(String(500), nullable=False) album: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) image_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) release_date: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) # Download settings quality: Mapped[str] = mapped_column(String(20), nullable=False, default='high') output_dir: Mapped[str] = mapped_column(String(1000), nullable=False) # Download status status: Mapped[str] = mapped_column(String(20), nullable=False, default='pending') progress: Mapped[int] = mapped_column(Integer, default=0) file_path: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True) file_size: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # Error handling error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) retry_count: Mapped[int] = mapped_column(Integer, default=0) max_retries: Mapped[int] = mapped_column(Integer, default=3) # Metadata metadata: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # Timestamps created_at: Mapped[float] = mapped_column(Float, nullable=False) started_at: Mapped[Optional[float]] = mapped_column(Float, nullable=True) completed_at: Mapped[Optional[float]] = mapped_column(Float, nullable=True) updated_at: Mapped[float] = mapped_column(Float, nullable=False) # User association user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("user.id"), nullable=True) @classmethod def create(cls, data: dict): """Create a new universal download record""" if 'created_at' not in data: data['created_at'] = datetime.now().timestamp() if 'updated_at' not in data: data['updated_at'] = datetime.now().timestamp() return cls.insert_one(data) @classmethod def get_by_id(cls, download_id: int): """Get download by ID""" result = cls.execute(select(cls).where(cls.id == download_id)) res = next(result).scalar() return res @classmethod def get_by_service_id(cls, service: str, service_id: str): """Get download by service and service ID""" result = cls.execute( select(cls) .where(and_(cls.service == service, cls.service_id == service_id)) ) res = next(result).scalar() return res @classmethod def get_by_url(cls, url: str): """Get download by URL""" result = cls.execute(select(cls).where(cls.url == url)) res = next(result).scalar() return res @classmethod def get_pending_downloads(cls, limit: int = 50): """Get pending downloads""" result = cls.execute( select(cls) .where(cls.status == 'pending') .order_by(cls.created_at) .limit(limit) ) return [item for item in next(result).scalars()] @classmethod def get_active_downloads(cls): """Get currently active downloads""" result = cls.execute( select(cls) .where(cls.status.in_(['downloading', 'processing'])) .order_by(cls.started_at) ) return [item for item in next(result).scalars()] @classmethod def get_download_history(cls, user_id: Optional[int] = None, limit: int = 100, offset: int = 0): """Get download history with pagination""" query = select(cls).where(cls.status.in_(['completed', 'failed', 'cancelled'])) if user_id: query = query.where(cls.user_id == user_id) query = query.order_by(cls.created_at.desc()).offset(offset).limit(limit) result = cls.execute(query) return [item for item in next(result).scalars()] @classmethod def get_downloads_by_service(cls, service: str, limit: int = 50): """Get downloads by service""" result = cls.execute( select(cls) .where(cls.service == service) .order_by(cls.created_at.desc()) .limit(limit) ) return [item for item in next(result).scalars()] @classmethod def update_status(cls, download_id: int, status: str, **kwargs): """Update download status and related fields""" update_data = { 'status': status, 'updated_at': datetime.now().timestamp() } update_data.update(kwargs) return cls.execute( update(cls) .where(cls.id == download_id) .values(update_data), commit=True ) @classmethod def update_progress(cls, download_id: int, progress: int): """Update download progress""" return cls.execute( update(cls) .where(cls.id == download_id) .values({ 'progress': progress, 'updated_at': datetime.now().timestamp() }), commit=True ) @classmethod def increment_retry(cls, download_id: int): """Increment retry count""" return cls.execute( update(cls) .where(cls.id == download_id) .values({ 'retry_count': cls.retry_count + 1, 'updated_at': datetime.now().timestamp() }), commit=True ) @classmethod def delete_completed(cls, older_than_days: int = 30): """Delete completed downloads older than specified days""" cutoff_time = datetime.now().timestamp() - (older_than_days * 24 * 60 * 60) return cls.execute( delete(cls) .where( and_( cls.status.in_(['completed', 'failed', 'cancelled']), cls.completed_at < cutoff_time ) ), commit=True ) @classmethod def get_statistics(cls): """Get download statistics""" result = cls.execute( select( cls.service, cls.status, func.count(cls.id).label('count'), func.avg(cls.duration_ms).label('avg_duration') ) .group_by(cls.service, cls.status) ) stats = {} for row in next(result): service = row.service if service not in stats: stats[service] = {} stats[service][row.status] = { 'count': row.count, 'avg_duration_ms': row.avg_duration } return stats class UniversalDownloadSourceTable(Base): __tablename__ = "universal_download_sources" id: Mapped[int] = mapped_column(primary_key=True) service: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) # spotify, tidal, apple_music, etc. display_name: Mapped[str] = mapped_column(String(100), nullable=False) enabled: Mapped[bool] = mapped_column(Boolean, default=True) priority: Mapped[int] = mapped_column(Integer, nullable=False, default=1) supported_types: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) # track, album, playlist, artist features: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) # metadata, download, playlist config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) created_at: Mapped[float] = mapped_column(Float, nullable=False) updated_at: Mapped[float] = mapped_column(Float, nullable=False) @classmethod def get_enabled_sources(cls): """Get all enabled download sources ordered by priority""" result = cls.execute( select(cls) .where(cls.enabled == True) .order_by(cls.priority) ) return [item for item in next(result).scalars()] @classmethod def get_by_service(cls, service: str): """Get source by service name""" result = cls.execute(select(cls).where(cls.service == service)) res = next(result).scalar() return res @classmethod def update_source(cls, service: str, **kwargs): """Update source configuration""" kwargs['updated_at'] = datetime.now().timestamp() return cls.execute( update(cls) .where(cls.service == service) .values(kwargs), commit=True ) class UniversalDownloadQueueTable(Base): __tablename__ = "universal_download_queue" id: Mapped[int] = mapped_column(primary_key=True) download_id: Mapped[int] = mapped_column(ForeignKey("universal_downloads.id"), nullable=False) priority: Mapped[int] = mapped_column(Integer, default=0) position: Mapped[int] = mapped_column(Integer, nullable=False) added_at: Mapped[float] = mapped_column(Float, nullable=False) started_at: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # Relationship to download download = relationship("UniversalDownloadTable", backref="queue_items") @classmethod def add_to_queue(cls, download_id: int, priority: int = 0): """Add download to queue""" # Get current max position result = cls.execute(select(func.max(cls.position))) max_position = next(result).scalar() or 0 data = { 'download_id': download_id, 'priority': priority, 'position': max_position + 1, 'added_at': datetime.now().timestamp() } return cls.insert_one(data) @classmethod def get_next_item(cls): """Get next item from queue""" result = cls.execute( select(cls) .join(UniversalDownloadTable) .where( and_( UniversalDownloadTable.status == 'pending', cls.started_at.is_(None) ) ) .order_by(cls.priority.desc(), cls.position) .limit(1) ) res = next(result).scalar() return res @classmethod def remove_from_queue(cls, download_id: int): """Remove item from queue""" return cls.execute( delete(cls).where(cls.download_id == download_id), commit=True ) @classmethod def get_queue_length(cls): """Get current queue length""" result = cls.execute( select(func.count(cls.id)) .join(UniversalDownloadTable) .where(UniversalDownloadTable.status == 'pending') ) return next(result).scalar() or 0 # Create default universal download sources def create_default_universal_sources(): """Create default universal download sources if they don't exist""" default_sources = [ { 'service': 'spotify', 'display_name': 'Spotify', 'enabled': True, 'priority': 1, 'supported_types': ['track', 'album', 'playlist', 'artist'], 'features': ['metadata', 'download', 'playlist'], 'config': { 'quality_preference': ['lossless', 'high', 'medium', 'low'], 'formats': ['flac', 'mp3', 'aac'] } }, { 'service': 'tidal', 'display_name': 'Tidal', 'enabled': True, 'priority': 2, 'supported_types': ['track', 'album', 'playlist', 'artist'], 'features': ['metadata', 'download', 'playlist'], 'config': { 'quality_preference': ['lossless', 'high', 'medium', 'low'], 'formats': ['flac', 'mp3', 'aac'] } }, { 'service': 'apple_music', 'display_name': 'Apple Music', 'enabled': True, 'priority': 3, 'supported_types': ['track', 'album', 'playlist', 'artist'], 'features': ['metadata', 'download', 'playlist'], 'config': { 'quality_preference': ['lossless', 'high', 'medium', 'low'], 'formats': ['flac', 'mp3', 'aac'] } }, { 'service': 'youtube_music', 'display_name': 'YouTube Music', 'enabled': True, 'priority': 4, 'supported_types': ['video', 'playlist', 'channel'], 'features': ['metadata', 'download'], 'config': { 'quality_preference': ['high', 'medium', 'low'], 'formats': ['mp3', 'webm'] } }, { 'service': 'youtube', 'display_name': 'YouTube', 'enabled': True, 'priority': 5, 'supported_types': ['video', 'playlist', 'channel'], 'features': ['metadata', 'download'], 'config': { 'quality_preference': ['high', 'medium', 'low'], 'formats': ['mp4', 'webm', 'mp3'] } }, { 'service': 'soundcloud', 'display_name': 'SoundCloud', 'enabled': True, 'priority': 6, 'supported_types': ['track', 'playlist', 'artist'], 'features': ['metadata', 'download'], 'config': { 'quality_preference': ['high', 'medium', 'low'], 'formats': ['mp3'] } }, { 'service': 'deezer', 'display_name': 'Deezer', 'enabled': False, # Disabled by default 'priority': 7, 'supported_types': ['track', 'album', 'playlist', 'artist'], 'features': ['metadata', 'download', 'playlist'], 'config': { 'quality_preference': ['lossless', 'high', 'medium', 'low'], 'formats': ['flac', 'mp3'] } }, { 'service': 'bandcamp', 'display_name': 'Bandcamp', 'enabled': False, # Disabled by default 'priority': 8, 'supported_types': ['track', 'album'], 'features': ['metadata', 'download'], 'config': { 'quality_preference': ['lossless', 'high', 'medium', 'low'], 'formats': ['flac', 'mp3', 'aac'] } } ] current_time = datetime.now().timestamp() for source_data in default_sources: source_data['created_at'] = current_time source_data['updated_at'] = current_time existing = UniversalDownloadSourceTable.get_by_service(source_data['service']) if not existing: UniversalDownloadSourceTable.insert_one(source_data) # Add execute method for new universal tables for table_class in [UniversalDownloadTable, UniversalDownloadSourceTable, UniversalDownloadQueueTable]: if not hasattr(table_class, 'execute'): @classmethod def execute_method(cls, query, commit=False): engine = DbEngine() with engine.session() as session: result = session.execute(query) if commit: session.commit() return result table_class.execute = execute_method table_class.insert_one = lambda data: table_class.execute(insert(table_class).values(data), commit=True)