Files
swingmusic-extended/src/swingmusic/services/library_integration.py
T
Tomas Dvorak 38f1981283 Move backend files to root level for cleaner GitHub display
- 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
2026-03-17 22:37:49 +01:00

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()