aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--README.md18
-rw-r--r--beetsplug/__init__.py2
-rw-r--r--beetsplug/browse.py82
-rw-r--r--beetsplug/extras.py44
-rw-r--r--setup.py21
6 files changed, 170 insertions, 0 deletions
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',
+ ]
+)