ripper: Add support for fetching album art

If the `PIL` module is available, which can be installed by activating
the *art* extra for Rupert, the ripper will now download the album art
for the CD and store it in a file named `folder.jpg`.
pull/3/head
Dustin 2021-12-12 22:10:29 -06:00
parent 943cf4dbd2
commit 7b3427fc2c
4 changed files with 37 additions and 1 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
/.venv /.venv
/dist /dist
__pycache__/
*.py[co]

View File

@ -13,6 +13,7 @@ pydantic = "^1.7.3"
python-libdiscid = "^2.0.1" python-libdiscid = "^2.0.1"
musicbrainzngs = "^0.7.1" musicbrainzngs = "^0.7.1"
mutagen = "^1.45.1" mutagen = "^1.45.1"
Pillow = {version = "^8.4.0", optional = true}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pylint = "^2.6.0" pylint = "^2.6.0"
@ -24,6 +25,7 @@ rope = "^0.18.0"
ripper = "rupert.main:main" ripper = "rupert.main:main"
[tool.poetry.extras] [tool.poetry.extras]
art = ["pillow"]
udev = ["pyudev"] udev = ["pyudev"]
[tool.black] [tool.black]

View File

@ -10,6 +10,10 @@ from .disc import Disc
musicbrainzngs.set_useragent('DCPlayer', '0.0.1', 'https://dcplayer.audio/') musicbrainzngs.set_useragent('DCPlayer', '0.0.1', 'https://dcplayer.audio/')
class AlbumArtNotFound(Exception):
...
class Artist(pydantic.BaseModel): class Artist(pydantic.BaseModel):
id: str id: str
name: str name: str
@ -58,6 +62,15 @@ class ReleaseResponse(pydantic.BaseModel):
release_count: int = pydantic.Field(alias='release-count') release_count: int = pydantic.Field(alias='release-count')
def fetch_album_art(release: Release) -> bytes:
try:
return musicbrainzngs.get_image_front(release.id)
except OSError as e:
raise AlbumArtNotFound(
'No album art found for release %s', release.id
) from e
def get_releases_from_disc(disc: Disc) -> ReleaseResponse: def get_releases_from_disc(disc: Disc) -> ReleaseResponse:
res = musicbrainzngs.get_releases_by_discid( res = musicbrainzngs.get_releases_by_discid(
disc.disc_id, disc.disc_id,

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import enum import enum
import glob import glob
import io
import logging import logging
import os import os
import queue import queue
@ -25,7 +26,12 @@ import mutagen
from . import inotify from . import inotify
from .disc import DiscDrive from .disc import DiscDrive
from .musicbrainz import Release from .musicbrainz import AlbumArtNotFound, Release, fetch_album_art
try:
from PIL import Image
except ImportError:
Image = None # type: ignore
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -310,6 +316,8 @@ class Ripper:
for filename in glob.glob('track*.cdda.wav'): for filename in glob.glob('track*.cdda.wav'):
os.unlink(filename) os.unlink(filename)
if Image is not None:
threading.Thread(target=self.fetch_album_art).start()
self._process_thread.start() self._process_thread.start()
self._encode_thread.start() self._encode_thread.start()
self._rip_thread.start() self._rip_thread.start()
@ -336,6 +344,17 @@ class Ripper:
def on_complete(self, message: str, is_err: bool) -> None: def on_complete(self, message: str, is_err: bool) -> None:
self._status_queue.put((None, (message, is_err))) self._status_queue.put((None, (message, is_err)))
def fetch_album_art(self) -> None:
try:
data = fetch_album_art(self.release)
except AlbumArtNotFound:
log.error('No album art found for %s', self.release.title)
buf = io.BytesIO(data)
img = Image.open(buf)
img.thumbnail((1000, 1000))
with open('folder.jpg', 'wb') as f:
img.save(f)
def safe_name(name: str) -> str: def safe_name(name: str) -> str:
for k, v in FILENAME_SAFE_MAP.items(): for k, v in FILENAME_SAFE_MAP.items():