mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
38f1981283
- Move all backend files from swingmusic/ to root level - Backend files now display directly on GitHub repository page - Keep client applications as submodules (swingmusic-android, swingmusic-desktop, swingmusic-webclient) - Update README to reflect new structure (no cd swingmusic needed) - Cleaner, more professional GitHub repository layout Files moved to root: - src/ (main source code) - pyproject.toml, requirements.txt, run.py - swingmusic.spec, uv.lock, version.txt - services/ Result: GitHub shows backend files directly while maintaining organized structure
284 lines
10 KiB
Python
284 lines
10 KiB
Python
"""
|
|
Library integration service for Spotify downloads
|
|
Handles automatic addition of downloaded tracks to SwingMusic library
|
|
"""
|
|
|
|
import os
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, Any
|
|
from datetime import datetime
|
|
|
|
from swingmusic.db.libdata import TrackTable
|
|
from swingmusic.db.engine import DbEngine
|
|
from swingmusic.config import UserConfig
|
|
from swingmusic.utils import create_valid_filename
|
|
from swingmusic import logger
|
|
|
|
|
|
class LibraryIntegrator:
|
|
"""Handles integration of downloaded tracks into SwingMusic library"""
|
|
|
|
def __init__(self):
|
|
self.config = UserConfig()
|
|
self.music_dirs = self.config.rootDirs
|
|
|
|
def add_downloaded_track(self, download_item: Dict[str, Any]) -> bool:
|
|
"""
|
|
Add a downloaded track to the SwingMusic library
|
|
|
|
Args:
|
|
download_item: Dictionary containing download information
|
|
|
|
Returns:
|
|
bool: True if successfully added, False otherwise
|
|
"""
|
|
try:
|
|
if not download_item.get('file_path') or not os.path.exists(download_item['file_path']):
|
|
logger.error(f"Downloaded file not found: {download_item.get('file_path')}")
|
|
return False
|
|
|
|
# Check if track already exists in library
|
|
if self._track_exists(download_item['file_path']):
|
|
logger.info(f"Track already exists in library: {download_item['file_path']}")
|
|
return True
|
|
|
|
# Create track record
|
|
track_data = self._create_track_data(download_item)
|
|
|
|
# Insert into database
|
|
self._insert_track(track_data)
|
|
|
|
logger.info(f"Added track to library: {track_data['title']} by {track_data['artists']}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding track to library: {e}")
|
|
return False
|
|
|
|
def add_downloaded_album(self, download_item: Dict[str, Any], track_files: list[str]) -> int:
|
|
"""
|
|
Add all tracks from a downloaded album to the library
|
|
|
|
Args:
|
|
download_item: Album download information
|
|
track_files: List of downloaded track file paths
|
|
|
|
Returns:
|
|
int: Number of tracks successfully added
|
|
"""
|
|
added_count = 0
|
|
|
|
try:
|
|
for track_file in track_files:
|
|
if not os.path.exists(track_file):
|
|
logger.warning(f"Track file not found: {track_file}")
|
|
continue
|
|
|
|
# Check if track already exists
|
|
if self._track_exists(track_file):
|
|
logger.info(f"Track already exists in library: {track_file}")
|
|
added_count += 1
|
|
continue
|
|
|
|
# Create track data for album track
|
|
track_data = self._create_album_track_data(download_item, track_file)
|
|
|
|
# Insert into database
|
|
self._insert_track(track_data)
|
|
added_count += 1
|
|
|
|
logger.info(f"Added {added_count} tracks from album to library")
|
|
return added_count
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding album to library: {e}")
|
|
return added_count
|
|
|
|
def _track_exists(self, filepath: str) -> bool:
|
|
"""Check if track already exists in library"""
|
|
try:
|
|
with DbEngine.manager() as conn:
|
|
result = conn.execute(
|
|
TrackTable.select().where(TrackTable.filepath == filepath)
|
|
)
|
|
return result.scalar() is not None
|
|
except Exception as e:
|
|
logger.error(f"Error checking if track exists: {e}")
|
|
return False
|
|
|
|
def _create_track_data(self, download_item: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create track data dictionary from download item"""
|
|
filepath = download_item['file_path']
|
|
file_stat = os.stat(filepath)
|
|
|
|
# Extract metadata from download item
|
|
title = download_item.get('title', 'Unknown Title')
|
|
artist = download_item.get('artist', 'Unknown Artist')
|
|
album = download_item.get('album', 'Unknown Album')
|
|
|
|
# Generate hashes
|
|
trackhash = self._generate_track_hash(filepath, title, artist)
|
|
albumhash = self._generate_album_hash(album, artist)
|
|
|
|
# Extract file information
|
|
folder = os.path.basename(os.path.dirname(filepath))
|
|
|
|
return {
|
|
'title': title,
|
|
'artists': artist,
|
|
'albumartists': artist,
|
|
'album': album,
|
|
'albumhash': albumhash,
|
|
'trackhash': trackhash,
|
|
'filepath': filepath,
|
|
'folder': folder,
|
|
'duration': download_item.get('duration_ms', 0) // 1000, # Convert to seconds
|
|
'bitrate': self._get_bitrate_from_quality(download_item.get('quality', 'flac')),
|
|
'date': self._parse_date(download_item.get('release_date')),
|
|
'track': download_item.get('track_number', 1),
|
|
'disc': 1,
|
|
'last_mod': int(file_stat.st_mtime),
|
|
'extra': {
|
|
'spotify_id': download_item.get('spotify_id'),
|
|
'source': download_item.get('source', 'spotify'),
|
|
'download_date': datetime.now().isoformat()
|
|
}
|
|
}
|
|
|
|
def _create_album_track_data(self, download_item: Dict[str, Any], track_file: str) -> Dict[str, Any]:
|
|
"""Create track data for album track"""
|
|
file_stat = os.stat(track_file)
|
|
|
|
# Extract filename for title (if metadata not available)
|
|
filename = os.path.splitext(os.path.basename(track_file))[0]
|
|
|
|
# Use download item metadata as base
|
|
title = download_item.get('title', filename)
|
|
artist = download_item.get('artist', 'Unknown Artist')
|
|
album = download_item.get('album', 'Unknown Album')
|
|
|
|
# Generate hashes
|
|
trackhash = self._generate_track_hash(track_file, title, artist)
|
|
albumhash = self._generate_album_hash(album, artist)
|
|
|
|
# Extract file information
|
|
folder = os.path.basename(os.path.dirname(track_file))
|
|
|
|
return {
|
|
'title': title,
|
|
'artists': artist,
|
|
'albumartists': artist,
|
|
'album': album,
|
|
'albumhash': albumhash,
|
|
'trackhash': trackhash,
|
|
'filepath': track_file,
|
|
'folder': folder,
|
|
'duration': download_item.get('duration_ms', 0) // 1000,
|
|
'bitrate': self._get_bitrate_from_quality(download_item.get('quality', 'flac')),
|
|
'date': self._parse_date(download_item.get('release_date')),
|
|
'track': download_item.get('track_number', 1),
|
|
'disc': 1,
|
|
'last_mod': int(file_stat.st_mtime),
|
|
'extra': {
|
|
'spotify_id': download_item.get('spotify_id'),
|
|
'source': download_item.get('source', 'spotify'),
|
|
'download_date': datetime.now().isoformat(),
|
|
'album_download': True
|
|
}
|
|
}
|
|
|
|
def _insert_track(self, track_data: Dict[str, Any]):
|
|
"""Insert track into database"""
|
|
try:
|
|
with DbEngine.manager(commit=True) as conn:
|
|
conn.execute(TrackTable.insert().values(track_data))
|
|
except Exception as e:
|
|
logger.error(f"Error inserting track: {e}")
|
|
raise
|
|
|
|
def _generate_track_hash(self, filepath: str, title: str, artist: str) -> str:
|
|
"""Generate unique track hash"""
|
|
content = f"{filepath}:{title}:{artist}"
|
|
return hashlib.md5(content.encode()).hexdigest()
|
|
|
|
def _generate_album_hash(self, album: str, artist: str) -> str:
|
|
"""Generate album hash"""
|
|
content = f"{album}:{artist}"
|
|
return hashlib.md5(content.encode()).hexdigest()
|
|
|
|
def _get_bitrate_from_quality(self, quality: str) -> int:
|
|
"""Get approximate bitrate based on quality"""
|
|
quality_bitrates = {
|
|
'flac': 1411, # Approximate FLAC bitrate
|
|
'mp3_320': 320,
|
|
'mp3_128': 128
|
|
}
|
|
return quality_bitrates.get(quality, 320)
|
|
|
|
def _parse_date(self, date_str: Optional[str]) -> Optional[int]:
|
|
"""Parse date string to timestamp"""
|
|
if not date_str:
|
|
return None
|
|
|
|
try:
|
|
# Try various date formats
|
|
formats = ['%Y-%m-%d', '%Y', '%Y-%m']
|
|
for fmt in formats:
|
|
try:
|
|
dt = datetime.strptime(date_str, fmt)
|
|
return int(dt.timestamp())
|
|
except ValueError:
|
|
continue
|
|
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
def remove_downloaded_track(self, filepath: str) -> bool:
|
|
"""
|
|
Remove a downloaded track from the library
|
|
|
|
Args:
|
|
filepath: Path to the track file
|
|
|
|
Returns:
|
|
bool: True if successfully removed
|
|
"""
|
|
try:
|
|
with DbEngine.manager(commit=True) as conn:
|
|
result = conn.execute(
|
|
TrackTable.delete().where(TrackTable.filepath == filepath)
|
|
)
|
|
return result.rowcount > 0
|
|
except Exception as e:
|
|
logger.error(f"Error removing track from library: {e}")
|
|
return False
|
|
|
|
def update_track_metadata(self, filepath: str, metadata: Dict[str, Any]) -> bool:
|
|
"""
|
|
Update metadata for a track in the library
|
|
|
|
Args:
|
|
filepath: Path to the track file
|
|
metadata: New metadata to apply
|
|
|
|
Returns:
|
|
bool: True if successfully updated
|
|
"""
|
|
try:
|
|
with DbEngine.manager(commit=True) as conn:
|
|
result = conn.execute(
|
|
TrackTable.update()
|
|
.where(TrackTable.filepath == filepath)
|
|
.values(metadata)
|
|
)
|
|
return result.rowcount > 0
|
|
except Exception as e:
|
|
logger.error(f"Error updating track metadata: {e}")
|
|
return False
|
|
|
|
|
|
# Global instance
|
|
library_integrator = LibraryIntegrator()
|