update Beets

This commit is contained in:
AdeHub
2024-08-24 16:44:41 +12:00
parent a63098a919
commit 046d4d82b4
116 changed files with 17353 additions and 9964 deletions
+111 -107
View File
@@ -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}")