From 6ff26d2bbfc7ade30ee0c3e38682ef689b73bda6 Mon Sep 17 00:00:00 2001 From: Wolfgang Müller Date: Tue, 22 Feb 2022 11:35:39 +0100 Subject: Initial commit --- .gitignore | 3 ++ README.md | 18 +++++++++++ beetsplug/__init__.py | 2 ++ beetsplug/browse.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++ beetsplug/extras.py | 44 +++++++++++++++++++++++++++ setup.py | 21 +++++++++++++ 6 files changed, 170 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 beetsplug/__init__.py create mode 100644 beetsplug/browse.py create mode 100644 beetsplug/extras.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4176048 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.egg-info/ +/build/ +/dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1882fd --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# beets-plugins + +This is a collection of my beets plugins. + +## Plugins + +- `browse`: Browse items on MusicBrainz or the file system. See `beet help + browse` +- `extras`: Bring along extra files when moving or copying music. Expects a list + `paths` in the `extras` configuration with files or directories to bring along. + +## Installation + +To install these plugins locally, run: + + pip install --user . + +Then enable each plugin in the `beets` configuration. diff --git a/beetsplug/__init__.py b/beetsplug/__init__.py new file mode 100644 index 0000000..3ad9513 --- /dev/null +++ b/beetsplug/__init__.py @@ -0,0 +1,2 @@ +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) diff --git a/beetsplug/browse.py b/beetsplug/browse.py new file mode 100644 index 0000000..72999b9 --- /dev/null +++ b/beetsplug/browse.py @@ -0,0 +1,82 @@ +import subprocess +import uuid +import webbrowser + +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand, UserError + +MUSICBRAINZ_LOOKUP='https://musicbrainz.org/otherlookup/mbid?other-lookup.mbid=' +FIELD_NAMES = ['albumartist', 'album', 'artist', 'releasegroup', 'releasetrack', 'track', 'work'] + +class BrowsePlugin(BeetsPlugin): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def commands(self): + return [BrowseCommand(self.config)] + +class BrowseCommand(Subcommand): + + explorer = 'thunar' + + def __init__(self, config): + super().__init__('browse', parser=None, + help='browse items on MusicBrainz or the file system') + + self.parser.add_option('-f', '--field', type='string', + help='which field to look up on MusicBrainz, ' + 'e.g. album, artist, track, work, ...') + self.parser.add_option('-o', '--open', action='store_true', + help='open in the file browser instead of MusicBrainz') + self.parser.add_album_option() + + if 'explorer' in config: + self.explorer = config['explorer'].get() + + # pylint: disable=no-self-use + def browse_musicbrainz(self, item, field): + mbid = item.get(f'mb_{field}id') + if not mbid: + raise UserError(f'\'mb_{field}id\' not available for: {item}') + + try: + uuid.UUID(mbid) + except ValueError: + raise UserError(f'invalid UUID "{mbid}" for: {item}') from None + + webbrowser.open(MUSICBRAINZ_LOOKUP + mbid) + + def browse_filesystem(self, item, _): + try: + subprocess.Popen(self.explorer.split(' ') + [item.get('path')]) # pylint: disable=consider-using-with + except OSError as err: + raise UserError(err) from err + + def func(self, lib, opts, args): + queryfun = lib.albums if opts.album else lib.items + browsefun = self.browse_filesystem if opts.open else self.browse_musicbrainz + field = opts.field or ('album' if opts.album else 'track') + + if field not in FIELD_NAMES: + raise UserError(f'invalid field "{field}", try one of: {", ".join(FIELD_NAMES)}') + + if not args: + raise UserError('empty query, refusing') + + items = queryfun(args) + + if not items: + return + + if len(items) == 1: + browsefun(items[0], field) + return + + print('Query returned multiple matches, please disambiguate:') + for index, item in enumerate(items): + if index < 5: + print(item) + else: + print(f'[{len(items) - index} more]') + return diff --git a/beetsplug/extras.py b/beetsplug/extras.py new file mode 100644 index 0000000..32fe3f6 --- /dev/null +++ b/beetsplug/extras.py @@ -0,0 +1,44 @@ +import os +import shutil +import beets.plugins + +class ExtrasPlugin(beets.plugins.BeetsPlugin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.register_listener('item_moved', self.on_item_moved) + self.register_listener('item_copied', self.on_item_copied) + + def on_item_moved(self, _, source, destination): + for (sourcepath, destpath) in self.gather(source, destination): + os.rename(sourcepath, destpath) + + def on_item_copied(self, _, source, destination): + for (sourcepath, destpath) in self.gather(source, destination): + if os.path.isdir(sourcepath): + shutil.copytree(sourcepath, destpath) + else: + shutil.copy(sourcepath, destpath) + + def gather(self, source, destination): + candidates = [] + sourcedir = os.path.dirname(source) + destdir = os.path.dirname(destination) + + if sourcedir == destdir: + return [] + + paths = [beets.util.bytestring_path(p) for p in self.config['paths'].get()] + + for path in paths: + sourcepath = os.path.join(sourcedir, path) + if not os.path.exists(sourcepath): + continue + + destpath = os.path.join(destdir, path) + if os.path.exists(destpath): + continue + + candidates.append((sourcepath, destpath)) + + return candidates diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..378870b --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +import setuptools + +setuptools.setup( + name="beets-oriole", + version="0.1.0", + author="Wolfgang Müller", + author_email="wolf@oriole.systems", + description="A collection of beets plugins", + url="https://git.oriole.systems/beets-plugins", + classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + ], + packages=['beetsplug'], + python_requires=">=3.8", + + install_requires = [ + 'beets>=1.6.0', + 'python-mpd2>=3.0.5', + ] +) -- cgit v1.2.3-2-gb3c3