mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-09 05:06:44 +10:00
* Add function tofull-size current playing song through a button in the bottom control panel
* Add an API also a feature in the home page, to allow user type a song name, then create a backend job to auto search and download song in the background, when pick the song from the results, put the result into priority candicdate if the name either has “official song” or “官方”, if the same exact song has been downloaded before, then just put the job as pending to confirm, and let user to confirm, once user confirmed, then the system can download the duplicate song, we need to persist the those jobs, if a job fails, then we mark it failed, and user can retry it later through UI, also add an summary about how to trigger this function through API
* Add API keys section in settings, so we need to evaluate api keys for public APIs
* Now we only put the auto search and download song into the pubic API
* Add feature in search to search artist, which would return all the matched artists,
* Add sort feature to the artists and library page, like the one we have in playlist detail page
* By default sort the song in the library page by added at desc
108 lines
4.9 KiB
Python
108 lines
4.9 KiB
Python
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Table, Text, Boolean
|
|
from sqlalchemy.orm import relationship
|
|
from datetime import datetime, timedelta
|
|
from app.db.session import Base
|
|
import secrets
|
|
|
|
# Association table for playlist-music many-to-many relationship
|
|
playlist_music = Table(
|
|
'playlist_music',
|
|
Base.metadata,
|
|
Column('playlist_id', Integer, ForeignKey('playlists.id'), primary_key=True),
|
|
Column('music_id', Integer, ForeignKey('music.id'), primary_key=True),
|
|
Column('position', Integer, default=0),
|
|
Column('added_at', DateTime, default=datetime.utcnow),
|
|
)
|
|
|
|
|
|
class Music(Base):
|
|
__tablename__ = "music"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
title = Column(String, index=True)
|
|
artist = Column(String, index=True, nullable=True)
|
|
album = Column(String, nullable=True)
|
|
duration = Column(Float, nullable=True)
|
|
file_path = Column(String, unique=True)
|
|
file_size = Column(Integer, nullable=True)
|
|
file_format = Column(String, nullable=True) # mp3, flac, m4a, wav, ogg, etc.
|
|
file_location = Column(String, nullable=True) # Full absolute path for user reference
|
|
file_exists = Column(Boolean, default=True) # Track if file still exists on disk
|
|
source_url = Column(String, nullable=True)
|
|
source_type = Column(String, nullable=True) # local, youtube, bilibili, etc.
|
|
thumbnail = Column(String, nullable=True)
|
|
lyrics = Column(Text, nullable=True)
|
|
share_token = Column(String, unique=True, index=True, nullable=True) # Secure share token
|
|
share_token_expires_at = Column(DateTime, nullable=True) # Expiration timestamp
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
last_scanned_at = Column(DateTime, nullable=True) # Last time file was verified
|
|
|
|
playlists = relationship("Playlist", secondary=playlist_music, back_populates="music_items")
|
|
|
|
def generate_share_token(self, expiration_days: int = 14):
|
|
"""Generate a secure random share token with expiration"""
|
|
if not self.share_token:
|
|
self.share_token = secrets.token_urlsafe(16)
|
|
self.share_token_expires_at = datetime.utcnow() + timedelta(days=expiration_days)
|
|
|
|
def is_share_token_valid(self) -> bool:
|
|
"""Check if share token is still valid"""
|
|
if not self.share_token or not self.share_token_expires_at:
|
|
return False
|
|
return datetime.utcnow() < self.share_token_expires_at
|
|
|
|
|
|
class Playlist(Base):
|
|
__tablename__ = "playlists"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String, unique=True, index=True)
|
|
description = Column(Text, nullable=True)
|
|
thumbnail = Column(String, nullable=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
music_items = relationship("Music", secondary=playlist_music, back_populates="playlists")
|
|
|
|
|
|
class AppSettings(Base):
|
|
__tablename__ = "app_settings"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
key = Column(String, unique=True, index=True)
|
|
value = Column(Text)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
|
|
class DownloadJob(Base):
|
|
__tablename__ = "download_jobs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
song_name = Column(String, index=True)
|
|
status = Column(String, index=True) # pending, searching, downloading, completed, failed, waiting_confirmation
|
|
search_results = Column(Text, nullable=True) # JSON string of search results
|
|
selected_result = Column(Text, nullable=True) # JSON string of selected result
|
|
selected_result_index = Column(Integer, nullable=True) # Index of selected result
|
|
priority = Column(Boolean, default=False) # True if contains "official song" or "官方"
|
|
error_message = Column(Text, nullable=True)
|
|
music_id = Column(Integer, ForeignKey('music.id'), nullable=True) # Reference to downloaded music
|
|
confirmed = Column(Boolean, default=False) # Whether user confirmed duplicate download
|
|
is_duplicate = Column(Boolean, default=False) # Whether song already exists
|
|
duplicate_music_id = Column(Integer, nullable=True) # ID of existing duplicate
|
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
|
|
class APIKey(Base):
|
|
__tablename__ = "api_keys"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
key = Column(String, unique=True, index=True)
|
|
name = Column(String)
|
|
description = Column(Text, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
expires_at = Column(DateTime, nullable=True)
|
|
last_used_at = Column(DateTime, nullable=True)
|