Compare commits

...

3 Commits

Author SHA1 Message Date
Dustin 0b84b8dbcf wip: make metadata optional 2024-01-13 10:18:49 -06:00
Dustin 2b43b2e3af Update poetry.lock 2021-12-12 22:15:45 -06:00
Dustin 7b3427fc2c 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`.
2021-12-12 22:15:38 -06:00
6 changed files with 113 additions and 14 deletions

2
.gitignore vendored
View File

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

54
poetry.lock generated
View File

@ -140,6 +140,14 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pillow"
version = "8.4.0"
description = "Python Imaging Library (Fork)"
category = "main"
optional = true
python-versions = ">=3.6"
[[package]]
name = "platformdirs"
version = "2.4.0"
@ -325,12 +333,13 @@ docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[extras]
art = ["Pillow"]
udev = ["pyudev"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "f152c05aca9b5b39776bacfafee5ee833dfc3deb1ef9c32455287659a55fd83c"
content-hash = "1ca0ba049692c125c3127417498e901fecbd9f63cb7269491dcbaa35fed08d00"
[metadata.files]
astroid = [
@ -417,6 +426,49 @@ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
pillow = [
{file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"},
{file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"},
{file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"},
{file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"},
{file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"},
{file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"},
{file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"},
{file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"},
{file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"},
{file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"},
{file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"},
{file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"},
{file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"},
{file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"},
{file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"},
{file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"},
{file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"},
{file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"},
{file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"},
{file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"},
{file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"},
{file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"},
{file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"},
{file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"},
{file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"},
{file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"},
{file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"},
{file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"},
{file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"},
{file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"},
{file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"},
{file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"},
{file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"},
{file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"},
{file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"},
{file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"},
{file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"},
]
platformdirs = [
{file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
{file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},

View File

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

View File

@ -64,7 +64,10 @@ class Status:
return Text('Done!')
def format_release(release: Release) -> str:
def format_release(release: Optional[Release]) -> str:
if release is None:
return '[bright_yellow]Unknown Artist - Unknown Album[/bright_yellow]'
more_info = [
'[bright_blue]{} disc(s)[/bright_blue]'.format(
len(release.medium_list)
@ -106,13 +109,14 @@ def prompt_menu(console: Console, choices: Iterable[Any]) -> int:
console.print(f'[red]Invalid selection: {i}[/red]')
continue
return i - 1
assert False, 'unreachable'
def prompt_release(console: Console, drive: DiscDrive) -> Release:
def prompt_release(console: Console, drive: DiscDrive) -> Optional[Release]:
releases = get_releases_from_disc(drive.get_disc()).release_list
if not releases:
console.print('[red]Could not find a matching MusicBrainz release')
raise SystemExit(1)
return None
if len(releases) == 1:
return releases[0]
console.print(
@ -181,12 +185,12 @@ def run(
release = get_release_by_id(mbid)
else:
release = prompt_release(console, dev)
if not sys.stdout.isatty():
if release and not sys.stdout.isatty():
print(f'{release.artist_credit_phrase} - {release.title}')
console.print(f'Ripping {format_release(release)}')
if release and len(release.medium_list) > 1:
num_discs = len(release.medium_list)
if len(release.medium_list) > 1:
discno = prompt_select_disc(console, num_discs)
else:
discno = 0

View File

@ -10,6 +10,10 @@ from .disc import Disc
musicbrainzngs.set_useragent('DCPlayer', '0.0.1', 'https://dcplayer.audio/')
class AlbumArtNotFound(Exception):
...
class Artist(pydantic.BaseModel):
id: str
name: str
@ -58,6 +62,15 @@ class ReleaseResponse(pydantic.BaseModel):
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:
res = musicbrainzngs.get_releases_by_discid(
disc.disc_id,

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import enum
import glob
import io
import logging
import os
import queue
@ -25,7 +26,12 @@ import mutagen
from . import inotify
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__)
@ -173,7 +179,7 @@ class ProcessThread(threading.Thread):
class EncodeThread(threading.Thread):
def __init__(self, release: Release, discno: int, q: ProcessQueue) -> None:
def __init__(self, release: Optional[Release], discno: int, q: ProcessQueue) -> None:
super().__init__(name='EncodeThread')
self.release = release
self.discno = discno
@ -207,11 +213,15 @@ class EncodeThread(threading.Thread):
log.info('Adding tags to %s', filename)
trackno = int(filename[5:7])
if not self.release:
log.warning('Cannot tag track %d: no release metadata', trackno)
return
artist = self.release.artist_credit[0].artist
album = self.release.title
medium = self.release.medium_list[self.discno]
track = medium.track_list[trackno - 1]
tags = mutagen.File(filename, easy=True)
assert tags
tags['tracknumber'] = str(trackno)
tags['artist'] = tags['albumartist'] = artist.name
tags['album'] = album
@ -258,7 +268,7 @@ class Ripper:
def __init__(
self,
device: DiscDrive,
release: Release,
release: Optional[Release],
discno: int,
tracks: Optional[TrackList] = None,
use_libcdio: bool = False,
@ -293,14 +303,17 @@ class Ripper:
def rip(self) -> Iterable[StatusMessage]:
start = time.monotonic()
if self.release:
dirname = safe_name(
f'{self.release.artist_credit_phrase} - {self.release.title}'
)
else:
dirname = 'Unknown Artist - Unknown Album'
if not os.path.isdir(dirname):
log.info('Creating directory: %s', dirname)
os.mkdir(dirname)
os.chdir(dirname)
if len(self.release.medium_list) > 1:
if self.release and len(self.release.medium_list) > 1:
subdirname = f'Disc {self.discno + 1}'
if not os.path.isdir(subdirname):
log.info('Creating directory: %s', subdirname)
@ -310,6 +323,8 @@ class Ripper:
for filename in glob.glob('track*.cdda.wav'):
os.unlink(filename)
if self.release and Image is not None:
threading.Thread(target=self.fetch_album_art).start()
self._process_thread.start()
self._encode_thread.start()
self._rip_thread.start()
@ -336,6 +351,17 @@ class Ripper:
def on_complete(self, message: str, is_err: bool) -> None:
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:
for k, v in FILENAME_SAFE_MAP.items():