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
+35 -27
View File
@@ -16,20 +16,20 @@
"""
from abc import abstractmethod, ABCMeta
from abc import ABCMeta, abstractmethod
from importlib import import_module
from confuse import ConfigValueError
from beets import ui
from beets.plugins import BeetsPlugin
METASYNC_MODULE = 'beetsplug.metasync'
METASYNC_MODULE = "beetsplug.metasync"
# Dictionary to map the MODULE and the CLASS NAME of meta sources
SOURCES = {
'amarok': 'Amarok',
'itunes': 'Itunes',
"amarok": "Amarok",
"itunes": "Itunes",
}
@@ -45,13 +45,13 @@ class MetaSource(metaclass=ABCMeta):
def load_meta_sources():
""" Returns a dictionary of all the MetaSources
"""Returns a dictionary of all the MetaSources
E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true
"""
meta_sources = {}
for module_path, class_name in SOURCES.items():
module = import_module(METASYNC_MODULE + '.' + module_path)
module = import_module(METASYNC_MODULE + "." + module_path)
meta_sources[class_name.lower()] = getattr(module, class_name)
return meta_sources
@@ -61,8 +61,7 @@ META_SOURCES = load_meta_sources()
def load_item_types():
""" Returns a dictionary containing the item_types of all the MetaSources
"""
"""Returns a dictionary containing the item_types of all the MetaSources"""
item_types = {}
for meta_source in META_SOURCES.values():
item_types.update(meta_source.item_types)
@@ -70,42 +69,50 @@ def load_item_types():
class MetaSyncPlugin(BeetsPlugin):
item_types = load_item_types()
def __init__(self):
super().__init__()
def commands(self):
cmd = ui.Subcommand('metasync',
help='update metadata from music player libraries')
cmd.parser.add_option('-p', '--pretend', action='store_true',
help='show all changes but do nothing')
cmd.parser.add_option('-s', '--source', default=[],
action='append', dest='sources',
help='comma-separated list of sources to sync')
cmd = ui.Subcommand(
"metasync", help="update metadata from music player libraries"
)
cmd.parser.add_option(
"-p",
"--pretend",
action="store_true",
help="show all changes but do nothing",
)
cmd.parser.add_option(
"-s",
"--source",
default=[],
action="append",
dest="sources",
help="comma-separated list of sources to sync",
)
cmd.parser.add_format_option()
cmd.func = self.func
return [cmd]
def func(self, lib, opts, args):
"""Command handler for the metasync function.
"""
"""Command handler for the metasync function."""
pretend = opts.pretend
query = ui.decargs(args)
sources = []
for source in opts.sources:
sources.extend(source.split(','))
sources.extend(source.split(","))
sources = sources or self.config['source'].as_str_seq()
sources = sources or self.config["source"].as_str_seq()
meta_source_instances = {}
items = lib.items(query)
# Avoid needlessly instantiating meta sources (can be expensive)
if not items:
self._log.info('No items found matching query')
self._log.info("No items found matching query")
return
# Instantiate the meta sources
@@ -113,18 +120,19 @@ class MetaSyncPlugin(BeetsPlugin):
try:
cls = META_SOURCES[player]
except KeyError:
self._log.error('Unknown metadata source \'{}\''.format(
player))
self._log.error("Unknown metadata source '{}'".format(player))
try:
meta_source_instances[player] = cls(self.config, self._log)
except (ImportError, ConfigValueError) as e:
self._log.error('Failed to instantiate metadata source '
'\'{}\': {}'.format(player, e))
self._log.error(
"Failed to instantiate metadata source "
"'{}': {}".format(player, e)
)
# Avoid needlessly iterating over items
if not meta_source_instances:
self._log.error('No valid metadata sources found')
self._log.error("No valid metadata sources found")
return
# Sync the items with all of the meta sources
+35 -33
View File
@@ -16,35 +16,35 @@
"""
from os.path import basename
from datetime import datetime
from os.path import basename
from time import mktime
from xml.sax.saxutils import quoteattr
from beets.util import displayable_path
from beets.dbcore import types
from beets.library import DateType
from beets.util import displayable_path
from beetsplug.metasync import MetaSource
def import_dbus():
try:
return __import__('dbus')
return __import__("dbus")
except ImportError:
return None
dbus = import_dbus()
class Amarok(MetaSource):
item_types = {
'amarok_rating': types.INTEGER,
'amarok_score': types.FLOAT,
'amarok_uid': types.STRING,
'amarok_playcount': types.INTEGER,
'amarok_firstplayed': DateType(),
'amarok_lastplayed': DateType(),
"amarok_rating": types.INTEGER,
"amarok_score": types.FLOAT,
"amarok_uid": types.STRING,
"amarok_playcount": types.INTEGER,
"amarok_firstplayed": DateType(),
"amarok_lastplayed": DateType(),
}
query_xml = '<query version="1.0"> \
@@ -57,10 +57,11 @@ class Amarok(MetaSource):
super().__init__(config, log)
if not dbus:
raise ImportError('failed to import dbus')
raise ImportError("failed to import dbus")
self.collection = \
dbus.SessionBus().get_object('org.kde.amarok', '/Collection')
self.collection = dbus.SessionBus().get_object(
"org.kde.amarok", "/Collection"
)
def sync_from_source(self, item):
path = displayable_path(item.path)
@@ -73,35 +74,36 @@ class Amarok(MetaSource):
self.query_xml % quoteattr(basename(path))
)
for result in results:
if result['xesam:url'] != path:
if result["xesam:url"] != path:
continue
item.amarok_rating = result['xesam:userRating']
item.amarok_score = result['xesam:autoRating']
item.amarok_playcount = result['xesam:useCount']
item.amarok_uid = \
result['xesam:id'].replace('amarok-sqltrackuid://', '')
item.amarok_rating = result["xesam:userRating"]
item.amarok_score = result["xesam:autoRating"]
item.amarok_playcount = result["xesam:useCount"]
item.amarok_uid = result["xesam:id"].replace(
"amarok-sqltrackuid://", ""
)
if result['xesam:firstUsed'][0][0] != 0:
if result["xesam:firstUsed"][0][0] != 0:
# These dates are stored as timestamps in amarok's db, but
# exposed over dbus as fixed integers in the current timezone.
first_played = datetime(
result['xesam:firstUsed'][0][0],
result['xesam:firstUsed'][0][1],
result['xesam:firstUsed'][0][2],
result['xesam:firstUsed'][1][0],
result['xesam:firstUsed'][1][1],
result['xesam:firstUsed'][1][2]
result["xesam:firstUsed"][0][0],
result["xesam:firstUsed"][0][1],
result["xesam:firstUsed"][0][2],
result["xesam:firstUsed"][1][0],
result["xesam:firstUsed"][1][1],
result["xesam:firstUsed"][1][2],
)
if result['xesam:lastUsed'][0][0] != 0:
if result["xesam:lastUsed"][0][0] != 0:
last_played = datetime(
result['xesam:lastUsed'][0][0],
result['xesam:lastUsed'][0][1],
result['xesam:lastUsed'][0][2],
result['xesam:lastUsed'][1][0],
result['xesam:lastUsed'][1][1],
result['xesam:lastUsed'][1][2]
result["xesam:lastUsed"][0][0],
result["xesam:lastUsed"][0][1],
result["xesam:lastUsed"][0][2],
result["xesam:lastUsed"][1][0],
result["xesam:lastUsed"][1][1],
result["xesam:lastUsed"][1][2],
)
else:
last_played = first_played
+48 -45
View File
@@ -16,31 +16,32 @@
"""
from contextlib import contextmanager
import os
import plistlib
import shutil
import tempfile
import plistlib
from urllib.parse import urlparse, unquote
from contextlib import contextmanager
from time import mktime
from urllib.parse import unquote, urlparse
from confuse import ConfigValueError
from beets import util
from beets.dbcore import types
from beets.library import DateType
from confuse import ConfigValueError
from beets.util import bytestring_path, syspath
from beetsplug.metasync import MetaSource
@contextmanager
def create_temporary_copy(path):
temp_dir = tempfile.mkdtemp()
temp_path = os.path.join(temp_dir, 'temp_itunes_lib')
shutil.copyfile(path, temp_path)
temp_dir = bytestring_path(tempfile.mkdtemp())
temp_path = os.path.join(temp_dir, b"temp_itunes_lib")
shutil.copyfile(syspath(path), syspath(temp_path))
try:
yield temp_path
finally:
shutil.rmtree(temp_dir)
shutil.rmtree(syspath(temp_dir))
def _norm_itunes_path(path):
@@ -54,72 +55,74 @@ def _norm_itunes_path(path):
# which is unwanted in the case of Windows systems.
# E.g., '\\G:\\Music\\bar' needs to be stripped to 'G:\\Music\\bar'
return util.bytestring_path(os.path.normpath(
unquote(urlparse(path).path)).lstrip('\\')).lower()
return util.bytestring_path(
os.path.normpath(unquote(urlparse(path).path)).lstrip("\\")
).lower()
class Itunes(MetaSource):
item_types = {
'itunes_rating': types.INTEGER, # 0..100 scale
'itunes_playcount': types.INTEGER,
'itunes_skipcount': types.INTEGER,
'itunes_lastplayed': DateType(),
'itunes_lastskipped': DateType(),
'itunes_dateadded': DateType(),
"itunes_rating": types.INTEGER, # 0..100 scale
"itunes_playcount": types.INTEGER,
"itunes_skipcount": types.INTEGER,
"itunes_lastplayed": DateType(),
"itunes_lastskipped": DateType(),
"itunes_dateadded": DateType(),
}
def __init__(self, config, log):
super().__init__(config, log)
config.add({'itunes': {
'library': '~/Music/iTunes/iTunes Library.xml'
}})
config.add({"itunes": {"library": "~/Music/iTunes/iTunes Library.xml"}})
# Load the iTunes library, which has to be the .xml one (not the .itl)
library_path = config['itunes']['library'].as_filename()
library_path = config["itunes"]["library"].as_filename()
try:
self._log.debug(
f'loading iTunes library from {library_path}')
self._log.debug(f"loading iTunes library from {library_path}")
with create_temporary_copy(library_path) as library_copy:
with open(library_copy, 'rb') as library_copy_f:
with open(library_copy, "rb") as library_copy_f:
raw_library = plistlib.load(library_copy_f)
except OSError as e:
raise ConfigValueError('invalid iTunes library: ' + e.strerror)
raise ConfigValueError("invalid iTunes library: " + e.strerror)
except Exception:
# It's likely the user configured their '.itl' library (<> xml)
if os.path.splitext(library_path)[1].lower() != '.xml':
hint = ': please ensure that the configured path' \
' points to the .XML library'
if os.path.splitext(library_path)[1].lower() != ".xml":
hint = (
": please ensure that the configured path"
" points to the .XML library"
)
else:
hint = ''
raise ConfigValueError('invalid iTunes library' + hint)
hint = ""
raise ConfigValueError("invalid iTunes library" + hint)
# Make the iTunes library queryable using the path
self.collection = {_norm_itunes_path(track['Location']): track
for track in raw_library['Tracks'].values()
if 'Location' in track}
self.collection = {
_norm_itunes_path(track["Location"]): track
for track in raw_library["Tracks"].values()
if "Location" in track
}
def sync_from_source(self, item):
result = self.collection.get(util.bytestring_path(item.path).lower())
if not result:
self._log.warning(f'no iTunes match found for {item}')
self._log.warning(f"no iTunes match found for {item}")
return
item.itunes_rating = result.get('Rating')
item.itunes_playcount = result.get('Play Count')
item.itunes_skipcount = result.get('Skip Count')
item.itunes_rating = result.get("Rating")
item.itunes_playcount = result.get("Play Count")
item.itunes_skipcount = result.get("Skip Count")
if result.get('Play Date UTC'):
if result.get("Play Date UTC"):
item.itunes_lastplayed = mktime(
result.get('Play Date UTC').timetuple())
result.get("Play Date UTC").timetuple()
)
if result.get('Skip Date'):
if result.get("Skip Date"):
item.itunes_lastskipped = mktime(
result.get('Skip Date').timetuple())
result.get("Skip Date").timetuple()
)
if result.get('Date Added'):
item.itunes_dateadded = mktime(
result.get('Date Added').timetuple())
if result.get("Date Added"):
item.itunes_dateadded = mktime(result.get("Date Added").timetuple())