75 lines
2.3 KiB
Python
75 lines
2.3 KiB
Python
"""
|
|
Author: Uyanide pywang0608@foxmail.com
|
|
Date: 2026-03-25 04:09:36
|
|
Description: Data models.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
from typing import Optional, TYPE_CHECKING
|
|
from dataclasses import dataclass
|
|
|
|
from lrx_cli.config import SCORE_W_SYNCED
|
|
|
|
if TYPE_CHECKING:
|
|
from .lrc import LRCData
|
|
|
|
|
|
class CacheStatus(str, Enum):
|
|
"""Status of a cached lyric entry."""
|
|
|
|
SUCCESS_SYNCED = "SUCCESS_SYNCED"
|
|
SUCCESS_UNSYNCED = "SUCCESS_UNSYNCED"
|
|
NOT_FOUND = "NOT_FOUND"
|
|
NETWORK_ERROR = "NETWORK_ERROR"
|
|
|
|
|
|
@dataclass
|
|
class TrackMeta:
|
|
"""Metadata describing a track obtained from MPRIS or manual input."""
|
|
|
|
trackid: Optional[str] = None # Spotify track ID (without "spotify:track:" prefix)
|
|
length: Optional[int] = None # Duration in milliseconds
|
|
album: Optional[str] = None
|
|
artist: Optional[str] = None
|
|
title: Optional[str] = None
|
|
url: Optional[str] = None # Playback URL (file:// for local files)
|
|
|
|
@property
|
|
def is_local(self) -> bool:
|
|
"""True when the track is a local file (file:// URL)."""
|
|
return bool(self.url and self.url.startswith("file://"))
|
|
|
|
@property
|
|
def is_complete(self) -> bool:
|
|
"""True when all fields required by LRCLIB are present."""
|
|
return all([self.length, self.album, self.title, self.artist])
|
|
|
|
def display_name(self) -> str:
|
|
"""Human-readable representation for logging."""
|
|
parts = []
|
|
if self.artist:
|
|
parts.append(self.artist)
|
|
if self.title:
|
|
parts.append(self.title)
|
|
return " - ".join(parts) if parts else self.trackid or self.url or "(unknown)"
|
|
|
|
|
|
@dataclass
|
|
class LyricResult:
|
|
"""Result of a lyric fetch attempt, also used as cache record."""
|
|
|
|
status: CacheStatus
|
|
lyrics: Optional[LRCData] = None
|
|
source: Optional[str] = None # Which fetcher produced this result
|
|
ttl: Optional[int] = None # Hint for cache TTL (seconds)
|
|
confidence: float = 100.0 # 0-100 selection confidence (100 = trusted/exact)
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.status in (CacheStatus.NOT_FOUND, CacheStatus.NETWORK_ERROR):
|
|
self.confidence = 0.0
|
|
if self.status is CacheStatus.SUCCESS_UNSYNCED and self.confidence == 100.0:
|
|
# Fix: remove inflated confidence for unsynced results
|
|
self.confidence = 100 - SCORE_W_SYNCED
|