mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-20 16:03:59 +01:00
update Beets
This commit is contained in:
+111
-107
@@ -19,51 +19,60 @@ Spec: standards.freedesktop.org/thumbnail-spec/latest/index.html
|
||||
"""
|
||||
|
||||
|
||||
from hashlib import md5
|
||||
import os
|
||||
import shutil
|
||||
from itertools import chain
|
||||
from pathlib import PurePosixPath
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import os
|
||||
import shutil
|
||||
from hashlib import md5
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
from xdg import BaseDirectory
|
||||
|
||||
from beets import util
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.ui import Subcommand, decargs
|
||||
from beets import util
|
||||
from beets.util.artresizer import ArtResizer, get_im_version, get_pil_version
|
||||
|
||||
from beets.util import bytestring_path, displayable_path, syspath
|
||||
from beets.util.artresizer import ArtResizer
|
||||
|
||||
BASE_DIR = os.path.join(BaseDirectory.xdg_cache_home, "thumbnails")
|
||||
NORMAL_DIR = util.bytestring_path(os.path.join(BASE_DIR, "normal"))
|
||||
LARGE_DIR = util.bytestring_path(os.path.join(BASE_DIR, "large"))
|
||||
NORMAL_DIR = bytestring_path(os.path.join(BASE_DIR, "normal"))
|
||||
LARGE_DIR = bytestring_path(os.path.join(BASE_DIR, "large"))
|
||||
|
||||
|
||||
class ThumbnailsPlugin(BeetsPlugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config.add({
|
||||
'auto': True,
|
||||
'force': False,
|
||||
'dolphin': False,
|
||||
})
|
||||
self.config.add(
|
||||
{
|
||||
"auto": True,
|
||||
"force": False,
|
||||
"dolphin": False,
|
||||
}
|
||||
)
|
||||
|
||||
self.write_metadata = None
|
||||
if self.config['auto'] and self._check_local_ok():
|
||||
self.register_listener('art_set', self.process_album)
|
||||
if self.config["auto"] and self._check_local_ok():
|
||||
self.register_listener("art_set", self.process_album)
|
||||
|
||||
def commands(self):
|
||||
thumbnails_command = Subcommand("thumbnails",
|
||||
help="Create album thumbnails")
|
||||
thumbnails_command = Subcommand(
|
||||
"thumbnails", help="Create album thumbnails"
|
||||
)
|
||||
thumbnails_command.parser.add_option(
|
||||
'-f', '--force',
|
||||
dest='force', action='store_true', default=False,
|
||||
help='force regeneration of thumbnails deemed fine (existing & '
|
||||
'recent enough)')
|
||||
"-f",
|
||||
"--force",
|
||||
dest="force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="force regeneration of thumbnails deemed fine (existing & "
|
||||
"recent enough)",
|
||||
)
|
||||
thumbnails_command.parser.add_option(
|
||||
'--dolphin', dest='dolphin', action='store_true', default=False,
|
||||
help="create Dolphin-compatible thumbnail information (for KDE)")
|
||||
"--dolphin",
|
||||
dest="dolphin",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="create Dolphin-compatible thumbnail information (for KDE)",
|
||||
)
|
||||
thumbnails_command.func = self.process_query
|
||||
|
||||
return [thumbnails_command]
|
||||
@@ -75,29 +84,29 @@ class ThumbnailsPlugin(BeetsPlugin):
|
||||
self.process_album(album)
|
||||
|
||||
def _check_local_ok(self):
|
||||
"""Check that's everythings ready:
|
||||
- local capability to resize images
|
||||
- thumbnail dirs exist (create them if needed)
|
||||
- detect whether we'll use PIL or IM
|
||||
- detect whether we'll use GIO or Python to get URIs
|
||||
"""Check that everything is ready:
|
||||
- local capability to resize images
|
||||
- thumbnail dirs exist (create them if needed)
|
||||
- detect whether we'll use PIL or IM
|
||||
- detect whether we'll use GIO or Python to get URIs
|
||||
"""
|
||||
if not ArtResizer.shared.local:
|
||||
self._log.warning("No local image resizing capabilities, "
|
||||
"cannot generate thumbnails")
|
||||
self._log.warning(
|
||||
"No local image resizing capabilities, "
|
||||
"cannot generate thumbnails"
|
||||
)
|
||||
return False
|
||||
|
||||
for dir in (NORMAL_DIR, LARGE_DIR):
|
||||
if not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
if not os.path.exists(syspath(dir)):
|
||||
os.makedirs(syspath(dir))
|
||||
|
||||
if get_im_version():
|
||||
self.write_metadata = write_metadata_im
|
||||
tool = "IM"
|
||||
else:
|
||||
assert get_pil_version() # since we're local
|
||||
self.write_metadata = write_metadata_pil
|
||||
tool = "PIL"
|
||||
self._log.debug("using {0} to write metadata", tool)
|
||||
if not ArtResizer.shared.can_write_metadata:
|
||||
raise RuntimeError(
|
||||
f"Thumbnails: ArtResizer backend {ArtResizer.shared.method}"
|
||||
f" unexpectedly cannot write image metadata."
|
||||
)
|
||||
self._log.debug(f"using {ArtResizer.shared.method} to write metadata")
|
||||
|
||||
uri_getter = GioURI()
|
||||
if not uri_getter.available:
|
||||
@@ -108,20 +117,20 @@ class ThumbnailsPlugin(BeetsPlugin):
|
||||
return True
|
||||
|
||||
def process_album(self, album):
|
||||
"""Produce thumbnails for the album folder.
|
||||
"""
|
||||
self._log.debug('generating thumbnail for {0}', album)
|
||||
"""Produce thumbnails for the album folder."""
|
||||
self._log.debug("generating thumbnail for {0}", album)
|
||||
if not album.artpath:
|
||||
self._log.info('album {0} has no art', album)
|
||||
self._log.info("album {0} has no art", album)
|
||||
return
|
||||
|
||||
if self.config['dolphin']:
|
||||
if self.config["dolphin"]:
|
||||
self.make_dolphin_cover_thumbnail(album)
|
||||
|
||||
size = ArtResizer.shared.get_size(album.artpath)
|
||||
if not size:
|
||||
self._log.warning('problem getting the picture size for {0}',
|
||||
album.artpath)
|
||||
self._log.warning(
|
||||
"problem getting the picture size for {0}", album.artpath
|
||||
)
|
||||
return
|
||||
|
||||
wrote = True
|
||||
@@ -130,9 +139,9 @@ class ThumbnailsPlugin(BeetsPlugin):
|
||||
wrote &= self.make_cover_thumbnail(album, 128, NORMAL_DIR)
|
||||
|
||||
if wrote:
|
||||
self._log.info('wrote thumbnail for {0}', album)
|
||||
self._log.info("wrote thumbnail for {0}", album)
|
||||
else:
|
||||
self._log.info('nothing to do for {0}', album)
|
||||
self._log.info("nothing to do for {0}", album)
|
||||
|
||||
def make_cover_thumbnail(self, album, size, target_dir):
|
||||
"""Make a thumbnail of given size for `album` and put it in
|
||||
@@ -140,19 +149,28 @@ class ThumbnailsPlugin(BeetsPlugin):
|
||||
"""
|
||||
target = os.path.join(target_dir, self.thumbnail_file_name(album.path))
|
||||
|
||||
if os.path.exists(target) and \
|
||||
os.stat(target).st_mtime > os.stat(album.artpath).st_mtime:
|
||||
if self.config['force']:
|
||||
self._log.debug("found a suitable {1}x{1} thumbnail for {0}, "
|
||||
"forcing regeneration", album, size)
|
||||
if (
|
||||
os.path.exists(syspath(target))
|
||||
and os.stat(syspath(target)).st_mtime
|
||||
> os.stat(syspath(album.artpath)).st_mtime
|
||||
):
|
||||
if self.config["force"]:
|
||||
self._log.debug(
|
||||
"found a suitable {1}x{1} thumbnail for {0}, "
|
||||
"forcing regeneration",
|
||||
album,
|
||||
size,
|
||||
)
|
||||
else:
|
||||
self._log.debug("{1}x{1} thumbnail for {0} exists and is "
|
||||
"recent enough", album, size)
|
||||
self._log.debug(
|
||||
"{1}x{1} thumbnail for {0} exists and is " "recent enough",
|
||||
album,
|
||||
size,
|
||||
)
|
||||
return False
|
||||
resized = ArtResizer.shared.resize(size, album.artpath,
|
||||
util.syspath(target))
|
||||
self.add_tags(album, util.syspath(resized))
|
||||
shutil.move(resized, target)
|
||||
resized = ArtResizer.shared.resize(size, album.artpath, target)
|
||||
self.add_tags(album, resized)
|
||||
shutil.move(syspath(resized), syspath(target))
|
||||
return True
|
||||
|
||||
def thumbnail_file_name(self, path):
|
||||
@@ -160,52 +178,35 @@ class ThumbnailsPlugin(BeetsPlugin):
|
||||
See https://standards.freedesktop.org/thumbnail-spec/latest/x227.html
|
||||
"""
|
||||
uri = self.get_uri(path)
|
||||
hash = md5(uri.encode('utf-8')).hexdigest()
|
||||
return util.bytestring_path(f"{hash}.png")
|
||||
hash = md5(uri.encode("utf-8")).hexdigest()
|
||||
return bytestring_path(f"{hash}.png")
|
||||
|
||||
def add_tags(self, album, image_path):
|
||||
"""Write required metadata to the thumbnail
|
||||
See https://standards.freedesktop.org/thumbnail-spec/latest/x142.html
|
||||
"""
|
||||
mtime = os.stat(album.artpath).st_mtime
|
||||
metadata = {"Thumb::URI": self.get_uri(album.artpath),
|
||||
"Thumb::MTime": str(mtime)}
|
||||
mtime = os.stat(syspath(album.artpath)).st_mtime
|
||||
metadata = {
|
||||
"Thumb::URI": self.get_uri(album.artpath),
|
||||
"Thumb::MTime": str(mtime),
|
||||
}
|
||||
try:
|
||||
self.write_metadata(image_path, metadata)
|
||||
ArtResizer.shared.write_metadata(image_path, metadata)
|
||||
except Exception:
|
||||
self._log.exception("could not write metadata to {0}",
|
||||
util.displayable_path(image_path))
|
||||
self._log.exception(
|
||||
"could not write metadata to {0}", displayable_path(image_path)
|
||||
)
|
||||
|
||||
def make_dolphin_cover_thumbnail(self, album):
|
||||
outfilename = os.path.join(album.path, b".directory")
|
||||
if os.path.exists(outfilename):
|
||||
if os.path.exists(syspath(outfilename)):
|
||||
return
|
||||
artfile = os.path.split(album.artpath)[1]
|
||||
with open(outfilename, 'w') as f:
|
||||
f.write('[Desktop Entry]\n')
|
||||
f.write('Icon=./{}'.format(artfile.decode('utf-8')))
|
||||
with open(syspath(outfilename), "w") as f:
|
||||
f.write("[Desktop Entry]\n")
|
||||
f.write("Icon=./{}".format(artfile.decode("utf-8")))
|
||||
f.close()
|
||||
self._log.debug("Wrote file {0}", util.displayable_path(outfilename))
|
||||
|
||||
|
||||
def write_metadata_im(file, metadata):
|
||||
"""Enrich the file metadata with `metadata` dict thanks to IM."""
|
||||
command = ['convert', file] + \
|
||||
list(chain.from_iterable(('-set', k, v)
|
||||
for k, v in metadata.items())) + [file]
|
||||
util.command_output(command)
|
||||
return True
|
||||
|
||||
|
||||
def write_metadata_pil(file, metadata):
|
||||
"""Enrich the file metadata with `metadata` dict thanks to PIL."""
|
||||
from PIL import Image, PngImagePlugin
|
||||
im = Image.open(file)
|
||||
meta = PngImagePlugin.PngInfo()
|
||||
for k, v in metadata.items():
|
||||
meta.add_text(k, v, 0)
|
||||
im.save(file, "PNG", pnginfo=meta)
|
||||
return True
|
||||
self._log.debug("Wrote file {0}", displayable_path(outfilename))
|
||||
|
||||
|
||||
class URIGetter:
|
||||
@@ -221,7 +222,7 @@ class PathlibURI(URIGetter):
|
||||
name = "Python Pathlib"
|
||||
|
||||
def uri(self, path):
|
||||
return PurePosixPath(util.py3_path(path)).as_uri()
|
||||
return PurePosixPath(os.fsdecode(path)).as_uri()
|
||||
|
||||
|
||||
def copy_c_string(c_string):
|
||||
@@ -232,12 +233,12 @@ def copy_c_string(c_string):
|
||||
# work. A more surefire way would be to allocate a ctypes buffer and copy
|
||||
# the data with `memcpy` or somesuch.
|
||||
s = ctypes.cast(c_string, ctypes.c_char_p).value
|
||||
return b'' + s
|
||||
return b"" + s
|
||||
|
||||
|
||||
class GioURI(URIGetter):
|
||||
"""Use gio URI function g_file_get_uri. Paths must be utf-8 encoded.
|
||||
"""
|
||||
"""Use gio URI function g_file_get_uri. Paths must be utf-8 encoded."""
|
||||
|
||||
name = "GIO"
|
||||
|
||||
def __init__(self):
|
||||
@@ -266,8 +267,11 @@ class GioURI(URIGetter):
|
||||
def uri(self, path):
|
||||
g_file_ptr = self.libgio.g_file_new_for_path(path)
|
||||
if not g_file_ptr:
|
||||
raise RuntimeError("No gfile pointer received for {}".format(
|
||||
util.displayable_path(path)))
|
||||
raise RuntimeError(
|
||||
"No gfile pointer received for {}".format(
|
||||
displayable_path(path)
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
uri_ptr = self.libgio.g_file_get_uri(g_file_ptr)
|
||||
@@ -275,8 +279,10 @@ class GioURI(URIGetter):
|
||||
self.libgio.g_object_unref(g_file_ptr)
|
||||
if not uri_ptr:
|
||||
self.libgio.g_free(uri_ptr)
|
||||
raise RuntimeError("No URI received from the gfile pointer for "
|
||||
"{}".format(util.displayable_path(path)))
|
||||
raise RuntimeError(
|
||||
"No URI received from the gfile pointer for "
|
||||
"{}".format(displayable_path(path))
|
||||
)
|
||||
|
||||
try:
|
||||
uri = copy_c_string(uri_ptr)
|
||||
@@ -286,6 +292,4 @@ class GioURI(URIGetter):
|
||||
try:
|
||||
return uri.decode(util._fsencoding())
|
||||
except UnicodeDecodeError:
|
||||
raise RuntimeError(
|
||||
f"Could not decode filename from GIO: {uri!r}"
|
||||
)
|
||||
raise RuntimeError(f"Could not decode filename from GIO: {uri!r}")
|
||||
|
||||
Reference in New Issue
Block a user