Files
musicseerr/backend/services/cache_service.py
T
Harvey a69a26852e Cut down unnecessary logging (#48)
* Cut down unnecessary logging

* fix format etc

* fix checks

* fix tests
2026-04-14 00:02:38 +01:00

300 lines
12 KiB
Python

import asyncio
import logging
import shutil
import subprocess
import time
from pathlib import Path
from infrastructure.cache.memory_cache import CacheInterface
from infrastructure.cache.cache_keys import AUDIODB_PREFIX
from infrastructure.persistence import LibraryDB
from infrastructure.cache.disk_cache import DiskMetadataCache
from api.v1.schemas.cache import CacheStats, CacheClearResponse
logger = logging.getLogger(__name__)
def get_covers_cache_dir() -> Path:
from core.config import get_settings
return get_settings().cache_dir / "covers"
class CacheService:
def __init__(self, cache: CacheInterface, library_db: LibraryDB, disk_cache: DiskMetadataCache):
self._cache = cache
self._library_db = library_db
self._disk_cache = disk_cache
self._cached_stats: CacheStats | None = None
self._stats_cache_time: float = 0.0
self._stats_cache_ttl: float = 30.0
self._stats_lock = asyncio.Lock()
def _clear_genre_disk_cache(self) -> int:
try:
from core.dependencies import get_home_service
return get_home_service().clear_genre_disk_cache()
except Exception: # noqa: BLE001
logger.debug("Genre disk cache cleanup skipped (home service unavailable)")
return 0
async def get_stats(self) -> CacheStats:
covers_cache_dir = get_covers_cache_dir()
async with self._stats_lock:
now = time.time()
if self._cached_stats and (now - self._stats_cache_time) < self._stats_cache_ttl:
return self._cached_stats
memory_entries = self._cache.size()
memory_bytes = self._cache.estimate_memory_bytes()
memory_mb = memory_bytes / (1024 * 1024)
metadata_stats = self._disk_cache.get_stats()
metadata_count = metadata_stats['total_count']
metadata_albums = metadata_stats['album_count']
metadata_artists = metadata_stats['artist_count']
disk_count = 0
disk_bytes = 0
if covers_cache_dir.exists():
du_available = shutil.which('du') is not None
if du_available:
try:
result = await asyncio.to_thread(
subprocess.run,
['du', '-sb', str(covers_cache_dir)],
capture_output=True,
text=True,
timeout=5.0,
)
if result.returncode == 0:
disk_bytes = int(result.stdout.split()[0])
result = await asyncio.to_thread(
subprocess.run,
['find', str(covers_cache_dir), '-type', 'f'],
capture_output=True,
text=True,
timeout=5.0,
)
if result.returncode == 0:
lines = result.stdout.strip()
disk_count = len(lines.split('\n')) if lines else 0
else:
du_available = False
except (subprocess.TimeoutExpired, subprocess.SubprocessError, ValueError) as e:
logger.warning(f"Subprocess disk stats failed, falling back to Python: {e}")
du_available = False
if not du_available:
def _python_scan() -> tuple[int, int]:
count = 0
total = 0
for file_path in covers_cache_dir.rglob("*"):
if file_path.is_file():
count += 1
total += file_path.stat().st_size
return count, total
disk_count, disk_bytes = await asyncio.to_thread(_python_scan)
disk_mb = disk_bytes / (1024 * 1024)
lib_stats = await self._library_db.get_stats()
lib_bytes = lib_stats['db_size_bytes']
lib_mb = lib_bytes / (1024 * 1024)
total_bytes = memory_bytes + disk_bytes + lib_bytes
total_mb = total_bytes / (1024 * 1024)
stats = CacheStats(
memory_entries=memory_entries,
memory_size_bytes=memory_bytes,
memory_size_mb=round(memory_mb, 2),
disk_metadata_count=metadata_count,
disk_metadata_albums=metadata_albums,
disk_metadata_artists=metadata_artists,
disk_cover_count=disk_count,
disk_cover_size_bytes=disk_bytes,
disk_cover_size_mb=round(disk_mb, 2),
library_db_artist_count=lib_stats['artist_count'],
library_db_album_count=lib_stats['album_count'],
library_db_size_bytes=lib_bytes,
library_db_size_mb=round(lib_mb, 2),
library_db_last_sync=lib_stats.get('last_sync'),
total_size_bytes=total_bytes,
total_size_mb=round(total_mb, 2),
disk_audiodb_artist_count=metadata_stats.get('audiodb_artist_count', 0),
disk_audiodb_album_count=metadata_stats.get('audiodb_album_count', 0),
)
self._cached_stats = stats
self._stats_cache_time = now
return stats
async def clear_memory_cache(self) -> CacheClearResponse:
try:
entries_before = self._cache.size()
await self._cache.clear()
self._cached_stats = None
return CacheClearResponse(
success=True,
message=f"Successfully cleared {entries_before} memory cache entries",
cleared_memory_entries=entries_before,
cleared_disk_files=0
)
except Exception as e: # noqa: BLE001
logger.error(f"Failed to clear memory cache: {e}")
return CacheClearResponse(
success=False,
message=f"Failed to clear memory cache: {str(e)}",
cleared_memory_entries=0,
cleared_disk_files=0
)
async def clear_disk_cache(self) -> CacheClearResponse:
covers_cache_dir = get_covers_cache_dir()
try:
metadata_stats = self._disk_cache.get_stats()
metadata_count = metadata_stats['total_count']
await self._disk_cache.clear_all()
files_cleared = 0
if covers_cache_dir.exists():
for file_path in covers_cache_dir.rglob("*"):
if file_path.is_file():
file_path.unlink()
files_cleared += 1
files_cleared += self._clear_genre_disk_cache()
self._cached_stats = None
return CacheClearResponse(
success=True,
message=f"Successfully cleared {metadata_count} metadata files and {files_cleared} cover images from disk",
cleared_memory_entries=0,
cleared_disk_files=files_cleared + metadata_count
)
except Exception as e: # noqa: BLE001
logger.error(f"Failed to clear disk cache: {e}")
return CacheClearResponse(
success=False,
message=f"Failed to clear disk cache: {str(e)}",
cleared_memory_entries=0,
cleared_disk_files=0
)
async def clear_all_cache(self) -> CacheClearResponse:
covers_cache_dir = get_covers_cache_dir()
try:
memory_entries = self._cache.size()
await self._cache.clear()
metadata_stats = self._disk_cache.get_stats()
metadata_count = metadata_stats['total_count']
await self._disk_cache.clear_all()
disk_files = 0
if covers_cache_dir.exists():
for file_path in covers_cache_dir.rglob("*"):
if file_path.is_file():
file_path.unlink()
disk_files += 1
disk_files += self._clear_genre_disk_cache()
self._cached_stats = None
return CacheClearResponse(
success=True,
message=f"Successfully cleared {memory_entries} memory entries, {metadata_count} metadata files, and {disk_files} cover files (library database preserved)",
cleared_memory_entries=memory_entries,
cleared_disk_files=disk_files + metadata_count
)
except Exception as e: # noqa: BLE001
logger.error(f"Failed to clear all cache: {e}")
return CacheClearResponse(
success=False,
message=f"Couldn't clear the cache: {str(e)}",
cleared_memory_entries=0,
cleared_disk_files=0
)
async def clear_covers_cache(self) -> CacheClearResponse:
covers_cache_dir = get_covers_cache_dir()
try:
files_cleared = 0
if covers_cache_dir.exists():
for file_path in covers_cache_dir.rglob("*"):
if file_path.is_file():
file_path.unlink()
files_cleared += 1
self._cached_stats = None
return CacheClearResponse(
success=True,
message=f"Successfully cleared {files_cleared} cover images",
cleared_memory_entries=0,
cleared_disk_files=files_cleared
)
except Exception as e: # noqa: BLE001
logger.error(f"Failed to clear covers cache: {e}")
return CacheClearResponse(
success=False,
message=f"Failed to clear covers cache: {str(e)}",
cleared_memory_entries=0,
cleared_disk_files=0
)
async def clear_library_cache(self) -> CacheClearResponse:
try:
lib_stats = await self._library_db.get_stats()
artists_before = lib_stats['artist_count']
albums_before = lib_stats['album_count']
await self._library_db.clear()
self._cached_stats = None
return CacheClearResponse(
success=True,
message=f"Successfully cleared library database: {artists_before} artists, {albums_before} albums",
cleared_memory_entries=0,
cleared_disk_files=0,
cleared_library_artists=artists_before,
cleared_library_albums=albums_before
)
except Exception as e: # noqa: BLE001
logger.error(f"Failed to clear library cache: {e}")
return CacheClearResponse(
success=False,
message=f"Failed to clear library cache: {str(e)}",
cleared_memory_entries=0,
cleared_disk_files=0
)
async def clear_audiodb(self) -> CacheClearResponse:
try:
stats_before = self._disk_cache.get_stats()
count_before = stats_before.get('audiodb_artist_count', 0) + stats_before.get('audiodb_album_count', 0)
await self._disk_cache.clear_audiodb()
memory_cleared = await self._cache.clear_prefix(AUDIODB_PREFIX)
self._cached_stats = None
return CacheClearResponse(
success=True,
message=f"Successfully cleared {count_before} AudioDB cache entries and {memory_cleared} memory entries",
cleared_memory_entries=memory_cleared,
cleared_disk_files=count_before,
)
except Exception as e: # noqa: BLE001
logger.error(f"Failed to clear AudioDB cache: {e}")
return CacheClearResponse(
success=False,
message=f"Failed to clear AudioDB cache: {str(e)}",
cleared_memory_entries=0,
cleared_disk_files=0,
)