Support fetching cover art images from the Cover Art Archive

Add option `--cover-art` to `whipper cd rip` command which accepts three values:
- `file`: save the downloaded cover image as standalone file in the rip folder (named `cover.jpg`)
- `embed`: embed the download cover image into all the ripped audio tracks (no standalone file will be kept)
- `complete`: save standalone cover image as standalone file and embed it into all the ripped audio tracks (`file` + `embed`)

Every cover art is fetched from the Cover Art Archive as JPEG thumbnail with a maximum dimension of 500px.
Other supported values for the thumbnails are 250, 500 and 1200 (currently only some images have a corresponding 1200px sized thumbnail).

This feature introduces an optional dependency on the `Pillow` module which is required for the decoding of the cover file (required by the `embed` and `complete` option values).

Problem:
- EmbedPicTureTask shouldn't be a task.

Signed-off-by: ABCbum <kimlong221002@gmail.com>
Co-authored-by: JoeLametta <JoeLametta@users.noreply.github.com>
Signed-off-by: JoeLametta <JoeLametta@users.noreply.github.com>
This commit is contained in:
ABCbum
2019-12-23 23:03:49 +07:00
committed by JoeLametta
parent 150f0d5e91
commit f61214a238
4 changed files with 135 additions and 6 deletions

View File

@@ -20,6 +20,7 @@
import argparse
import cdio
import importlib.util
import os
import glob
import logging
@@ -290,6 +291,14 @@ Log files will log the path to tracks relative to this directory.
help="whether to continue ripping if "
"the disc is a CD-R",
default=False)
self.parser.add_argument('-C', '--cover-art',
action="store", dest="fetch_cover_art",
help="Fetch cover art and save it as "
"standalone file, embed into FLAC files "
"or perform both actions: file, embed, "
"complete option values respectively",
choices=['file', 'embed', 'complete'],
default=None)
def handle_arguments(self):
self.options.output_directory = os.path.expanduser(
@@ -342,6 +351,19 @@ Log files will log the path to tracks relative to this directory.
logger.info("creating output directory %s", dirname)
os.makedirs(dirname)
self.coverArtPath = None
if (self.options.fetch_cover_art in {"embed", "complete"} and
importlib.util.find_spec("PIL") is None):
logger.warning("the cover art option '%s' won't be honored "
"because the 'pillow' module isn't available",
self.options.fetch_cover_art)
elif self.options.fetch_cover_art in {"file", "embed", "complete"}:
self.coverArtPath = self.program.getCoverArt(
dirname,
self.program.metadata.mbid)
if self.options.fetch_cover_art == "file":
self.coverArtPath = None # NOTE: avoid image embedding (hacky)
# FIXME: turn this into a method
def _ripIfNotRipped(number):
logger.debug('ripIfNotRipped for track %d', number)
@@ -412,7 +434,8 @@ Log files will log the path to tracks relative to this directory.
what='track %d of %d%s' % (
number,
len(self.itable.tracks),
extra))
extra),
coverArtPath=self.coverArtPath)
break
# FIXME: catching too general exception (Exception)
except Exception as e:
@@ -474,6 +497,11 @@ Log files will log the path to tracks relative to this directory.
continue
_ripIfNotRipped(i + 1)
if (self.options.fetch_cover_art == "embed" and
self.coverArtPath is not None):
logger.debug('deleting cover art file at: %r', self.coverArtPath)
os.remove(self.coverArtPath)
logger.debug('writing cue file for %r', discName)
self.program.writeCue(discName)

View File

@@ -19,7 +19,8 @@
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
from mutagen.flac import FLAC
from mutagen.flac import FLAC, Picture
from mutagen.id3 import PictureType
from whipper.extern.task import task
@@ -89,3 +90,71 @@ class TaggingTask(task.Task):
w.save()
self.stop()
class EmbedPictureTask(task.Task):
description = 'Embed picture to FLAC'
def __init__(self, track_path, cover_art_path):
self.track_path = track_path
self.cover_art_path = cover_art_path
def start(self, runner):
task.Task.start(self, runner)
self.schedule(0.0, self._embed_picture)
def _make_flac_picture(self, cover_art_filename):
"""
Given a path to a jpg/png file, return a FLAC picture for embedding.
The embedding will be performed using the mutagen module.
:param cover_art_filename: path to cover art image file
:type cover_art_filename: str
:returns: a valid FLAC picture for embedding
:rtype: mutagen.flac.Picture or None
"""
if not cover_art_filename:
return
from PIL import Image
im = Image.open(cover_art_filename)
# NOTE: the cover art thumbnails we're getting from the Cover Art
# Archive should be always in the JPEG format: this check is currently
# useless but will leave it here to better handle unexpected formats.
if im.format == 'JPEG':
mime = 'image/jpeg'
elif im.format == 'PNG':
mime = 'image/png'
else:
# we only support png and jpeg
logger.warning("no cover art will be added because the fetched "
"image format is unsupported")
return
pic = Picture()
with open(cover_art_filename, 'rb') as f:
pic.data = f.read()
pic.type = PictureType.COVER_FRONT
pic.mime = mime
pic.width, pic.height = im.size
if im.mode not in ('P', 'RGB', 'SRGB'):
logger.warning("no cover art will be added because the fetched "
"image mode is unsupported")
return
return pic
def _embed_picture(self):
"""
Get flac picture generated from mutagen.flac.Picture then embed
it to given track if the flac picture exists.
"""
flac_pic = self._make_flac_picture(self.cover_art_path)
if flac_pic:
w = FLAC(self.track_path)
w.add_picture(flac_pic)
self.stop()

View File

@@ -27,6 +27,7 @@ import re
import os
import time
from tempfile import NamedTemporaryFile
from whipper.common import accurip, cache, checksum, common, mbngs, path
from whipper.program import cdrdao, cdparanoia
from whipper.image import image
@@ -470,6 +471,35 @@ class Program:
stop = track.getIndex(1).absolute - 1
return start, stop
def getCoverArt(self, path, release_id):
"""
Get cover art image from Cover Art Archive.
:param path: where to store the fetched image
:type path: str
:param release_id: a release id (self.program.metadata.mbid)
:type release_id: str
:returns: path to the downloaded cover art, else `None`
:rtype: str or None
"""
cover_art_path = os.path.join(path, 'cover.jpg')
logger.debug('fetching cover art for release: %r', release_id)
try:
data = musicbrainzngs.get_image_front(release_id, 500)
except musicbrainzngs.ResponseError as e:
logger.error('error fetching cover art: %r', e)
return
if data:
with NamedTemporaryFile(suffix='.cover.jpg', delete=False) as f:
f.write(data)
os.chmod(f.name, 0o644)
os.replace(f.name, cover_art_path)
logger.debug('cover art fetched at: %r', cover_art_path)
return cover_art_path
return
@staticmethod
def verifyTrack(runner, trackResult):
is_wave = not trackResult.filename.endswith('.flac')
@@ -490,7 +520,7 @@ class Program:
return ret
def ripTrack(self, runner, trackResult, offset, device, taglist,
overread, what=None):
overread, what=None, coverArtPath=None):
"""
Ripping the track may change the track's filename as stored in
trackResult.
@@ -516,7 +546,8 @@ class Program:
offset=offset,
device=device,
taglist=taglist,
what=what)
what=what,
coverArtPath=coverArtPath)
runner.run(t)

View File

@@ -427,7 +427,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
_tmppath = None
def __init__(self, path, table, start, stop, overread, offset=0,
device=None, taglist=None, what="track"):
device=None, taglist=None, what="track", coverArtPath=None):
"""
:param path: where to store the ripped track
:type path: str
@@ -493,8 +493,9 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
self.tasks.append(checksum.CRC32Task(tmppath))
self.tasks.append(encode.SoxPeakTask(tmppath))
# TODO: Move tagging outside of cdparanoia
# TODO: Move tagging and embed picture outside of cdparanoia
self.tasks.append(encode.TaggingTask(tmpoutpath, taglist))
self.tasks.append(encode.EmbedPictureTask(tmpoutpath, coverArtPath))
self.checksum = None