Skip to content

Commit 3865c5a

Browse files
hoffiepabera
andauthored
Fix CoverartCacheManager (#2325)
* Fix CoverartCacheManager for songs with no art Previously, an ERROR was logged for each song without cover art when the Web UI was open. This commit avoids the error, caches the no-cover-art result and saves a roundtrip to mpd for all no-cover-art songs. * refactor: Reducing code and simplifying some logical statements * fix: flake8 error * refactor: reducing complexity for cache filename * refactor: introduce queuing for saving cache files * fix: remove slugify * feat: Use mutagen instead of MPD to retrieve cover art, include cache flush, and thread * fix: flake8 error * Update src/jukebox/components/playermpd/__init__.py Co-authored-by: Christian Hoffmann <[email protected]> --------- Co-authored-by: pabera <[email protected]>
1 parent 7c7024c commit 3865c5a

File tree

5 files changed

+97
-49
lines changed

5 files changed

+97
-49
lines changed

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ wheel
1010
# Jukebox Core
1111
# For USB inputs (reader, buttons) and bluetooth buttons
1212
evdev
13+
mutagen
1314
pyalsaaudio
1415
pulsectl
1516
python-mpd2
1617
ruamel.yaml
17-
python-slugify
1818
# For playlistgenerator
1919
requests
2020
# For the publisher event reactor loop:

src/jukebox/components/playermpd/__init__.py

+12-34
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
import logging
8888
import time
8989
import functools
90-
from slugify import slugify
90+
from pathlib import Path
9191
import components.player
9292
import jukebox.cfghandler
9393
import jukebox.utils as utils
@@ -521,47 +521,25 @@ def play_card(self, folder: str, recursive: bool = False):
521521

522522
@plugs.tag
523523
def get_single_coverart(self, song_url):
524-
"""
525-
Saves the album art image to a cache and returns the filename.
526-
"""
527-
base_filename = slugify(song_url)
528-
529-
try:
530-
metadata_list = self.mpd_client.listallinfo(song_url)
531-
metadata = {}
532-
if metadata_list:
533-
metadata = metadata_list[0]
534-
535-
if 'albumartist' in metadata and 'album' in metadata:
536-
base_filename = slugify(f"{metadata['albumartist']}-{metadata['album']}")
537-
538-
cache_filename = self.coverart_cache_manager.find_file_by_hash(base_filename)
539-
540-
if cache_filename:
541-
return cache_filename
542-
543-
# Cache file does not exist
544-
# Fetch cover art binary
545-
album_art_data = self.mpd_client.readpicture(song_url)
524+
mp3_file_path = Path(components.player.get_music_library_path(), song_url).expanduser()
525+
cache_filename = self.coverart_cache_manager.get_cache_filename(mp3_file_path)
546526

547-
# Save to cache
548-
cache_filename = self.coverart_cache_manager.save_to_cache(base_filename, album_art_data)
549-
550-
return cache_filename
551-
552-
except mpd.base.CommandError as e:
553-
logger.error(f"{e.__class__.__qualname__}: {e} at uri {song_url}")
554-
except Exception as e:
555-
logger.error(f"{e.__class__.__qualname__}: {e} at uri {song_url}")
556-
557-
return ""
527+
return cache_filename
558528

559529
@plugs.tag
560530
def get_album_coverart(self, albumartist: str, album: str):
561531
song_list = self.list_songs_by_artist_and_album(albumartist, album)
562532

563533
return self.get_single_coverart(song_list[0]['file'])
564534

535+
@plugs.tag
536+
def flush_coverart_cache(self):
537+
"""
538+
Deletes the Cover Art Cache
539+
"""
540+
541+
return self.coverart_cache_manager.flush_cache()
542+
565543
@plugs.tag
566544
def get_folder_content(self, folder: str):
567545
"""
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,90 @@
1-
import os
1+
from mutagen.mp3 import MP3
2+
from mutagen.id3 import ID3, APIC
3+
from pathlib import Path
4+
import hashlib
5+
import logging
6+
from queue import Queue
7+
from threading import Thread
28
import jukebox.cfghandler
39

10+
COVER_PREFIX = 'cover'
11+
NO_COVER_ART_EXTENSION = 'no-art'
12+
NO_CACHE = ''
13+
CACHE_PENDING = 'CACHE_PENDING'
14+
15+
logger = logging.getLogger('jb.CoverartCacheManager')
416
cfg = jukebox.cfghandler.get_handler('jukebox')
517

618

719
class CoverartCacheManager:
820
def __init__(self):
921
coverart_cache_path = cfg.setndefault('webapp', 'coverart_cache_path', value='../../src/webapp/build/cover-cache')
10-
self.cache_folder_path = os.path.expanduser(coverart_cache_path)
22+
self.cache_folder_path = Path(coverart_cache_path).expanduser()
23+
self.write_queue = Queue()
24+
self.worker_thread = Thread(target=self.process_write_requests)
25+
self.worker_thread.daemon = True # Ensure the thread closes with the program
26+
self.worker_thread.start()
27+
28+
def generate_cache_key(self, base_filename: str) -> str:
29+
return f"{COVER_PREFIX}-{hashlib.sha256(base_filename.encode()).hexdigest()}"
30+
31+
def get_cache_filename(self, mp3_file_path: str) -> str:
32+
base_filename = Path(mp3_file_path).stem
33+
cache_key = self.generate_cache_key(base_filename)
34+
35+
for path in self.cache_folder_path.iterdir():
36+
if path.stem == cache_key:
37+
if path.suffix == f".{NO_COVER_ART_EXTENSION}":
38+
return NO_CACHE
39+
return path.name
40+
41+
self.save_to_cache(mp3_file_path)
42+
return CACHE_PENDING
43+
44+
def save_to_cache(self, mp3_file_path: str):
45+
self.write_queue.put(mp3_file_path)
1146

12-
def find_file_by_hash(self, hash_value):
13-
for filename in os.listdir(self.cache_folder_path):
14-
if filename.startswith(hash_value):
15-
return filename
16-
return None
47+
def _save_to_cache(self, mp3_file_path: str):
48+
base_filename = Path(mp3_file_path).stem
49+
cache_key = self.generate_cache_key(base_filename)
50+
file_extension, data = self._extract_album_art(mp3_file_path)
1751

18-
def save_to_cache(self, base_filename, album_art_data):
19-
mime_type = album_art_data['type']
20-
file_extension = 'jpg' if mime_type == 'image/jpeg' else mime_type.split('/')[-1]
21-
cache_filename = f"{base_filename}.{file_extension}"
52+
cache_filename = f"{cache_key}.{file_extension}"
53+
full_path = self.cache_folder_path / cache_filename # Works due to Pathlib
2254

23-
with open(os.path.join(self.cache_folder_path, cache_filename), 'wb') as file:
24-
file.write(album_art_data['binary'])
55+
with full_path.open('wb') as file:
56+
file.write(data)
57+
logger.debug(f"Created file: {cache_filename}")
2558

2659
return cache_filename
60+
61+
def _extract_album_art(self, mp3_file_path: str) -> tuple:
62+
try:
63+
audio_file = MP3(mp3_file_path, ID3=ID3)
64+
except Exception as e:
65+
logger.error(f"Error reading MP3 file {mp3_file_path}: {e}")
66+
return (NO_COVER_ART_EXTENSION, b'')
67+
68+
for tag in audio_file.tags.values():
69+
if isinstance(tag, APIC):
70+
mime_type = tag.mime
71+
file_extension = 'jpg' if mime_type == 'image/jpeg' else mime_type.split('/')[-1]
72+
return (file_extension, tag.data)
73+
74+
return (NO_COVER_ART_EXTENSION, b'')
75+
76+
def process_write_requests(self):
77+
while True:
78+
mp3_file_path = self.write_queue.get()
79+
try:
80+
self._save_to_cache(mp3_file_path)
81+
except Exception as e:
82+
logger.error(f"Error processing write request: {e}")
83+
self.write_queue.task_done()
84+
85+
def flush_cache(self):
86+
for path in self.cache_folder_path.iterdir():
87+
if path.is_file():
88+
path.unlink()
89+
logger.debug(f"Deleted cached file: {path.name}")
90+
logger.info("Cache flushed successfully.")

src/jukebox/components/rpc_command_alias.py

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
'method': 'repeat',
7676
'note': 'Repeat',
7777
'ignore_card_removal_action': True},
78+
'flush_coverart_cache': {
79+
'package': 'player',
80+
'plugin': 'ctrl',
81+
'method': 'flush_coverart_cache'},
7882

7983
# VOLUME
8084
'set_volume': {

src/webapp/src/components/Library/lists/albums/album-list/album-list-item.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ const AlbumListItem = ({ albumartist, album, isButton = true }) => {
2929
album: album
3030
});
3131
if (result) {
32-
setCoverImage(`/cover-cache/${result}`);
32+
if(result !== 'CACHE_PENDING') {
33+
setCoverImage(`/cover-cache/${result}`);
34+
}
3335
};
3436
}
3537

0 commit comments

Comments
 (0)