From 0191794285422eb4173421c04065f704f14a2b34 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 10 Nov 2019 21:59:58 +0100 Subject: [PATCH 01/15] subsonicplaylist plugin --- beetsplug/subsonicplaylist.py | 126 ++++++++++++++++++++++++++++++ docs/changelog.rst | 1 + docs/plugins/index.rst | 1 + docs/plugins/subsonicplaylist.rst | 24 ++++++ 4 files changed, 152 insertions(+) create mode 100644 beetsplug/subsonicplaylist.py create mode 100644 docs/plugins/subsonicplaylist.rst diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py new file mode 100644 index 000000000..5b7ed95bc --- /dev/null +++ b/beetsplug/subsonicplaylist.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2019, Joris Jensen +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +from __future__ import absolute_import, division, print_function + +import os +from hashlib import md5 +import xml.etree.ElementTree as ET +import random +import string +import requests +from beets.util import normpath, bytestring_path, mkdirall, syspath, \ + path_as_posix, sanitize_path + +from beets.ui import Subcommand + +from beets.plugins import BeetsPlugin + +__author__ = 'https://github.com/MrNuggelz' + + +class SubsonicPlaylistPlugin(BeetsPlugin): + + def __init__(self): + super(SubsonicPlaylistPlugin, self).__init__() + self.config.add( + { + 'relative_to': None, + 'playlist_dir': '.', + 'forward_slash': False, + 'playlist_ids': [], + 'playlist_names': [], + 'username': '', + 'password': '' + } + ) + self.config['password'].redact = True + + def create_playlist(self, xml, lib): + relative_to = self.config['relative_to'].get() + if relative_to: + relative_to = normpath(relative_to) + + playlist = ET.fromstring(xml)[0] + if playlist.attrib.get('code', '200') != '200': + alt_error = 'error getting playlist, but no error message found' + self._log.warn(playlist.attrib.get('message', alt_error)) + return + name = '{}.m3u'.format() + tracks = [(t.attrib['artist'], t.attrib['album'], t.attrib['title']) + for t in playlist] + track_paths = [] + for t in tracks: + query = 'artist:"{}" album:"{}" title:"{}"'.format(*t) + items = lib.items(query) + if len(items) > 0: + item_path = items[0].path + if relative_to: + item_path = os.path.relpath(items[0].path, relative_to) + track_paths.append(item_path) + else: + self._log.warn(u"{} | track not found ({})", name, query) + # write playlist + playlist_dir = self.config['playlist_dir'].as_filename() + playlist_dir = bytestring_path(playlist_dir) + m3u_path = normpath(os.path.join(playlist_dir, bytestring_path(name))) + mkdirall(m3u_path) + with open(syspath(m3u_path), 'wb') as f: + for path in track_paths: + if self.config['forward_slash'].get(): + path = path_as_posix(path) + f.write(path + b'\n') + + def get_playlist_by_id(self, playlist_id, lib): + xml = self.send('getPlaylist', '&id={}'.format(playlist_id)).text + self.create_playlist(xml, lib) + + def commands(self): + def build_playlist(lib, opts, args): + + if len(self.config['playlist_ids'].as_str_seq()) > 0: + for playlist_id in self.config['playlist_ids'].as_str_seq(): + self.get_playlist_by_id(playlist_id, lib) + if len(self.config['playlist_names'].as_str_seq()) > 0: + playlists = ET.fromstring(self.send('getPlaylists').text)[0] + if playlists.attrib.get('code', '200') != '200': + alt_error = 'error getting playlists,' \ + ' but no erro message found' + self._log.warn(playlists.attrib.get('message', alt_error)) + return + for name in self.config['playlist_names'].as_str_seq(): + for playlist in playlists: + if name == playlist.attrib['name']: + self.get_playlist_by_id(playlist.attrib['id'], lib) + + subsonicplaylist_cmds = Subcommand( + 'subsonicplaylist', help=u'import a subsonic playlist' + ) + subsonicplaylist_cmds.func = build_playlist + return [subsonicplaylist_cmds] + + def generate_token(self): + salt = ''.join(random.choices(string.ascii_lowercase + string.digits)) + return md5( + (self.config['password'].get() + salt).encode()).hexdigest(), salt + + def send(self, endpoint, params=''): + url = '{}/rest/{}?u={}&t={}&s={}&v=1.12.0&c=beets'.format( + self.config['base_url'].get(), + endpoint, + self.config['username'], + *self.generate_token()) + resp = requests.get(url + params) + return resp diff --git a/docs/changelog.rst b/docs/changelog.rst index 169da71ba..9a8d1ae5b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog New features: +* :doc:`/plugins/subsonicplaylist`: import playlist from a subsonic server. * :doc:`plugins/discogs` now adds two extra fields: `discogs_labelid` and `discogs_artistid` :bug: `3413` diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 066d6ec22..9cf2cc5a9 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -115,6 +115,7 @@ following to your configuration:: smartplaylist sonosupdate spotify + subsonicplaylist subsonicupdate the thumbnails diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst new file mode 100644 index 000000000..d6d64f60b --- /dev/null +++ b/docs/plugins/subsonicplaylist.rst @@ -0,0 +1,24 @@ +Subsonic Playlist Plugin +======================== + +The ``subsonicplaylist`` plugin allows to import playlist from a subsonic server + +Command Line Usage +------------------ + +To use the ``subsonicplaylist`` plugin, enable it in your configuration (see +:ref:`using-plugins`). Then use it by invoking the ``subsonicplaylist`` command. +The command will search the playlist on the subsonic server and create a playlist +using the beets library. Options to be defined in your config with their default value:: + + subsonicplaylist: + base_url: "https://your.subsonic.server" + 'relative_to': None, + 'playlist_dir': '.', + 'forward_slash': False, + 'playlist_ids': [], + 'playlist_names': [], + 'username': '', + 'password': '' + +Parameters `base_url`, `username` and `password` must be defined! \ No newline at end of file From ec696f5e58aa83240b578642fad9714455a3828e Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 11 Nov 2019 10:48:29 +0100 Subject: [PATCH 02/15] Removed unused sanitize_path import --- beetsplug/subsonicplaylist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 5b7ed95bc..9d3ee7cf6 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -22,7 +22,7 @@ import random import string import requests from beets.util import normpath, bytestring_path, mkdirall, syspath, \ - path_as_posix, sanitize_path + path_as_posix from beets.ui import Subcommand From bb305b17e14ee36bc46594418913d43c6128e02b Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 16 Nov 2019 14:36:48 +0100 Subject: [PATCH 03/15] set tags instead of creating m3u --- beetsplug/subsonicplaylist.py | 74 +++++++++++++++-------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 9d3ee7cf6..1dd73f939 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -15,14 +15,11 @@ from __future__ import absolute_import, division, print_function -import os from hashlib import md5 import xml.etree.ElementTree as ET import random import string import requests -from beets.util import normpath, bytestring_path, mkdirall, syspath, \ - path_as_posix from beets.ui import Subcommand @@ -37,9 +34,6 @@ class SubsonicPlaylistPlugin(BeetsPlugin): super(SubsonicPlaylistPlugin, self).__init__() self.config.add( { - 'relative_to': None, - 'playlist_dir': '.', - 'forward_slash': False, 'playlist_ids': [], 'playlist_names': [], 'username': '', @@ -48,62 +42,56 @@ class SubsonicPlaylistPlugin(BeetsPlugin): ) self.config['password'].redact = True - def create_playlist(self, xml, lib): - relative_to = self.config['relative_to'].get() - if relative_to: - relative_to = normpath(relative_to) + def update_tags(self, playlist_dict, lib): + for query, playlist_tag in playlist_dict.items(): + query = 'artist:"{}" album:"{}" title:"{}"'.format(*query) + items = lib.items(query) + if len(items) <= 0: + self._log.warn(u"{} | track not found ({})", playlist_tag, + query) + continue + for item in items: + item.update({'subsonic_playlist': playlist_tag}) + with lib.transaction(): + item.try_sync(write=True, move=False) + def get_playlist(self, playlist_id): + xml = self.send('getPlaylist', '&id={}'.format(playlist_id)).text playlist = ET.fromstring(xml)[0] if playlist.attrib.get('code', '200') != '200': alt_error = 'error getting playlist, but no error message found' self._log.warn(playlist.attrib.get('message', alt_error)) return - name = '{}.m3u'.format() + + name = playlist.attrib.get('name', 'undefined') tracks = [(t.attrib['artist'], t.attrib['album'], t.attrib['title']) for t in playlist] - track_paths = [] - for t in tracks: - query = 'artist:"{}" album:"{}" title:"{}"'.format(*t) - items = lib.items(query) - if len(items) > 0: - item_path = items[0].path - if relative_to: - item_path = os.path.relpath(items[0].path, relative_to) - track_paths.append(item_path) - else: - self._log.warn(u"{} | track not found ({})", name, query) - # write playlist - playlist_dir = self.config['playlist_dir'].as_filename() - playlist_dir = bytestring_path(playlist_dir) - m3u_path = normpath(os.path.join(playlist_dir, bytestring_path(name))) - mkdirall(m3u_path) - with open(syspath(m3u_path), 'wb') as f: - for path in track_paths: - if self.config['forward_slash'].get(): - path = path_as_posix(path) - f.write(path + b'\n') - - def get_playlist_by_id(self, playlist_id, lib): - xml = self.send('getPlaylist', '&id={}'.format(playlist_id)).text - self.create_playlist(xml, lib) + return name, tracks def commands(self): def build_playlist(lib, opts, args): - - if len(self.config['playlist_ids'].as_str_seq()) > 0: - for playlist_id in self.config['playlist_ids'].as_str_seq(): - self.get_playlist_by_id(playlist_id, lib) + ids = self.config['playlist_ids'].as_str_seq() if len(self.config['playlist_names'].as_str_seq()) > 0: - playlists = ET.fromstring(self.send('getPlaylists').text)[0] + playlists = ET.fromstring(self.send('getPlaylists').text)[ + 0] if playlists.attrib.get('code', '200') != '200': alt_error = 'error getting playlists,' \ ' but no erro message found' - self._log.warn(playlists.attrib.get('message', alt_error)) + self._log.warn( + playlists.attrib.get('message', alt_error)) return for name in self.config['playlist_names'].as_str_seq(): for playlist in playlists: if name == playlist.attrib['name']: - self.get_playlist_by_id(playlist.attrib['id'], lib) + ids.append(playlist.attrib['id']) + playlist_dict = dict() + for playlist_id in ids: + name, tracks = self.get_playlist(playlist_id) + for track in tracks: + if track not in playlist_dict: + playlist_dict[track] = ';' + playlist_dict[track] += name + ';' + self.update_tags(playlist_dict, lib) subsonicplaylist_cmds = Subcommand( 'subsonicplaylist', help=u'import a subsonic playlist' From e9dee5dca83a02c9985d3fff61f18d9a83aea8c6 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 16 Nov 2019 20:30:53 +0100 Subject: [PATCH 04/15] added flag to delete old tags --- beetsplug/subsonicplaylist.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 1dd73f939..ef100c6da 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -34,6 +34,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): super(SubsonicPlaylistPlugin, self).__init__() self.config.add( { + 'delete': False, 'playlist_ids': [], 'playlist_names': [], 'username': '', @@ -70,6 +71,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def commands(self): def build_playlist(lib, opts, args): + self._parse_opts(opts) ids = self.config['playlist_ids'].as_str_seq() if len(self.config['playlist_names'].as_str_seq()) > 0: playlists = ET.fromstring(self.send('getPlaylists').text)[ @@ -91,14 +93,35 @@ class SubsonicPlaylistPlugin(BeetsPlugin): if track not in playlist_dict: playlist_dict[track] = ';' playlist_dict[track] += name + ';' + # delete old tags + if self.config['delete']: + for item in lib.items('subsonic_playlist:";"'): + item.update({'subsonic_playlist': ''}) + with lib.transaction(): + item.try_sync(write=True, move=False) + self.update_tags(playlist_dict, lib) subsonicplaylist_cmds = Subcommand( 'subsonicplaylist', help=u'import a subsonic playlist' ) + subsonicplaylist_cmds.parser.add_option( + u'-d', + u'--delete', + action='store_true', + help=u'delete tag from items not in any playlist anymore', + ) subsonicplaylist_cmds.func = build_playlist return [subsonicplaylist_cmds] + def _parse_opts(self, opts): + + if opts.delete: + self.config['delete'].set(True) + + self.opts = opts + return True + def generate_token(self): salt = ''.join(random.choices(string.ascii_lowercase + string.digits)) return md5( From 00330da623be15eb7fd8f4ad19be65d7222dd945 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 16 Nov 2019 21:10:41 +0100 Subject: [PATCH 05/15] updated documentation --- docs/plugins/subsonicplaylist.rst | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index d6d64f60b..22fe16816 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -1,21 +1,30 @@ Subsonic Playlist Plugin ======================== -The ``subsonicplaylist`` plugin allows to import playlist from a subsonic server +The ``subsonicplaylist`` plugin allows to import playlists from a subsonic server. +This is done by retrieving the track infos from the subsonic server, searching +them in the beets library and adding the playlist names to the +`subsonic_playlist` tag of the found items. The content of the tag has the format: + + subsonic_playlist: ";first playlist;second playlist" + +To get all items in a playlist use the query `;playlist name;`. Command Line Usage ------------------ To use the ``subsonicplaylist`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then use it by invoking the ``subsonicplaylist`` command. -The command will search the playlist on the subsonic server and create a playlist -using the beets library. Options to be defined in your config with their default value:: +By default only the tags of the items found for playlists will be updated. +This means that, if one imported a playlist, then delete one song from it and +imported the playlist again, the deleted song will still have the playlist set +in its `subsonic_playlist` tag. To solve this problem one can use the `-d/--delete` +flag. This resets all `subsonic_playlist` tag before importing playlists. +Options to be defined in your config with their default value:: subsonicplaylist: - base_url: "https://your.subsonic.server" - 'relative_to': None, - 'playlist_dir': '.', - 'forward_slash': False, + 'base_url': "https://your.subsonic.server" + 'delete': False, 'playlist_ids': [], 'playlist_names': [], 'username': '', From 6d69d01016adaa780d660ee3c16199ad2afb96f5 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Mon, 13 Jan 2020 15:42:55 +0100 Subject: [PATCH 06/15] added database changed event to subsonicplaylist --- beetsplug/subsonicplaylist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index ef100c6da..fbbcd8ac3 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -20,6 +20,7 @@ import xml.etree.ElementTree as ET import random import string import requests +from beets import plugins from beets.ui import Subcommand @@ -101,6 +102,8 @@ class SubsonicPlaylistPlugin(BeetsPlugin): item.try_sync(write=True, move=False) self.update_tags(playlist_dict, lib) + ## notify plugins + plugins.send('database_change', lib=lib, model=self) subsonicplaylist_cmds = Subcommand( 'subsonicplaylist', help=u'import a subsonic playlist' From 499fcb8315e40e1554cfef5ccb4e8437fbfeb5b8 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 3 May 2020 12:56:54 +0200 Subject: [PATCH 07/15] Update docs/plugins/subsonicplaylist.rst Co-authored-by: Adrian Sampson --- docs/plugins/subsonicplaylist.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index 22fe16816..c712a4823 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -23,11 +23,11 @@ flag. This resets all `subsonic_playlist` tag before importing playlists. Options to be defined in your config with their default value:: subsonicplaylist: - 'base_url': "https://your.subsonic.server" - 'delete': False, - 'playlist_ids': [], - 'playlist_names': [], - 'username': '', - 'password': '' + base_url: "https://your.subsonic.server" + delete: no + playlist_ids: [] + playlist_names: [] + username: '' + password: '' -Parameters `base_url`, `username` and `password` must be defined! \ No newline at end of file +Parameters `base_url`, `username` and `password` must be defined! From 82f0c59f46d6a0bd164313bc83540d2c4dcc442b Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 13:10:24 +0200 Subject: [PATCH 08/15] review: updated subsonicplaylist.rst --- docs/plugins/subsonicplaylist.rst | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index c712a4823..b9a54c3de 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -2,11 +2,11 @@ Subsonic Playlist Plugin ======================== The ``subsonicplaylist`` plugin allows to import playlists from a subsonic server. -This is done by retrieving the track infos from the subsonic server, searching -them in the beets library and adding the playlist names to the +This is done by retrieving the track info from the subsonic server, searching +for them in the beets library, and adding the playlist names to the `subsonic_playlist` tag of the found items. The content of the tag has the format: - subsonic_playlist: ";first playlist;second playlist" + subsonic_playlist: ";first playlist;second playlist;" To get all items in a playlist use the query `;playlist name;`. @@ -15,12 +15,22 @@ Command Line Usage To use the ``subsonicplaylist`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then use it by invoking the ``subsonicplaylist`` command. +Next, configure the plugin to connect to your Subsonic server, like this:: + + subsonicplaylist: + base_url: http://subsonic.example.com + username: someUser + password: somePassword + +After this you can import your playlists by invoking the `subsonicplaylist` command. + By default only the tags of the items found for playlists will be updated. This means that, if one imported a playlist, then delete one song from it and imported the playlist again, the deleted song will still have the playlist set in its `subsonic_playlist` tag. To solve this problem one can use the `-d/--delete` flag. This resets all `subsonic_playlist` tag before importing playlists. -Options to be defined in your config with their default value:: + +Here's an example configuration with all the available options and their default values:: subsonicplaylist: base_url: "https://your.subsonic.server" From cb7dfe3f6f9fa37045cf6741e6909003784c62b0 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 14:05:22 +0200 Subject: [PATCH 09/15] review: updated subsonicplaylist.py --- beetsplug/subsonicplaylist.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index fbbcd8ac3..561b6032d 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -19,7 +19,13 @@ from hashlib import md5 import xml.etree.ElementTree as ET import random import string +from urllib.parse import urlencode + import requests +from beets.dbcore.query import SubstringQuery + +from beets.dbcore import AndQuery, Query, MatchQuery + from beets import plugins from beets.ui import Subcommand @@ -46,19 +52,21 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def update_tags(self, playlist_dict, lib): for query, playlist_tag in playlist_dict.items(): - query = 'artist:"{}" album:"{}" title:"{}"'.format(*query) + query = AndQuery([SubstringQuery("artist", query[0]), + SubstringQuery("album", query[1]), + SubstringQuery("title", query[2])]) items = lib.items(query) - if len(items) <= 0: + if not items: self._log.warn(u"{} | track not found ({})", playlist_tag, query) continue for item in items: - item.update({'subsonic_playlist': playlist_tag}) + item.subsonic_playlist = playlist_tag with lib.transaction(): item.try_sync(write=True, move=False) def get_playlist(self, playlist_id): - xml = self.send('getPlaylist', '&id={}'.format(playlist_id)).text + xml = self.send('getPlaylist', {'id': playlist_id}).text playlist = ET.fromstring(xml)[0] if playlist.attrib.get('code', '200') != '200': alt_error = 'error getting playlist, but no error message found' @@ -72,7 +80,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def commands(self): def build_playlist(lib, opts, args): - self._parse_opts(opts) + self.config.set_args(opts) ids = self.config['playlist_ids'].as_str_seq() if len(self.config['playlist_names'].as_str_seq()) > 0: playlists = ET.fromstring(self.send('getPlaylists').text)[ @@ -102,8 +110,6 @@ class SubsonicPlaylistPlugin(BeetsPlugin): item.try_sync(write=True, move=False) self.update_tags(playlist_dict, lib) - ## notify plugins - plugins.send('database_change', lib=lib, model=self) subsonicplaylist_cmds = Subcommand( 'subsonicplaylist', help=u'import a subsonic playlist' @@ -117,24 +123,18 @@ class SubsonicPlaylistPlugin(BeetsPlugin): subsonicplaylist_cmds.func = build_playlist return [subsonicplaylist_cmds] - def _parse_opts(self, opts): - - if opts.delete: - self.config['delete'].set(True) - - self.opts = opts - return True - def generate_token(self): salt = ''.join(random.choices(string.ascii_lowercase + string.digits)) return md5( (self.config['password'].get() + salt).encode()).hexdigest(), salt - def send(self, endpoint, params=''): + def send(self, endpoint, params=None): + if params is None: + params = dict() url = '{}/rest/{}?u={}&t={}&s={}&v=1.12.0&c=beets'.format( self.config['base_url'].get(), endpoint, self.config['username'], *self.generate_token()) - resp = requests.get(url + params) + resp = requests.get(url + urlencode(params)) return resp From 13b4a1413d1c69e79dcb497fb32675c7d96fb302 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 14:58:34 +0200 Subject: [PATCH 10/15] update all songs in one transaction --- beetsplug/subsonicplaylist.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 561b6032d..9c9c74974 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -51,18 +51,18 @@ class SubsonicPlaylistPlugin(BeetsPlugin): self.config['password'].redact = True def update_tags(self, playlist_dict, lib): - for query, playlist_tag in playlist_dict.items(): - query = AndQuery([SubstringQuery("artist", query[0]), - SubstringQuery("album", query[1]), - SubstringQuery("title", query[2])]) - items = lib.items(query) - if not items: - self._log.warn(u"{} | track not found ({})", playlist_tag, - query) - continue - for item in items: - item.subsonic_playlist = playlist_tag - with lib.transaction(): + with lib.transaction(): + for query, playlist_tag in playlist_dict.items(): + query = AndQuery([SubstringQuery("artist", query[0]), + SubstringQuery("album", query[1]), + SubstringQuery("title", query[2])]) + items = lib.items(query) + if not items: + self._log.warn(u"{} | track not found ({})", playlist_tag, + query) + continue + for item in items: + item.subsonic_playlist = playlist_tag item.try_sync(write=True, move=False) def get_playlist(self, playlist_id): From cb7ad191a35d6deda8872f633a7a44c8b26673d4 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 15:02:51 +0200 Subject: [PATCH 11/15] removed duplicate line from merge --- docs/changelog.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index cbd9f3ce2..88d289e35 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,7 +7,6 @@ Changelog New features: * :doc:`/plugins/subsonicplaylist`: import playlist from a subsonic server. -* :doc:`plugins/discogs` now adds two extra fields: `discogs_labelid` and * A new :ref:`extra_tags` configuration option allows more tagged metadata to be included in MusicBrainz queries. * A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets From 5c3debe2369cc161c2c9a875e3f2c20d8a2f5903 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sun, 3 May 2020 16:07:27 +0200 Subject: [PATCH 12/15] removed unused imports --- beetsplug/subsonicplaylist.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 9c9c74974..14cd855b1 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -24,9 +24,7 @@ from urllib.parse import urlencode import requests from beets.dbcore.query import SubstringQuery -from beets.dbcore import AndQuery, Query, MatchQuery - -from beets import plugins +from beets.dbcore import AndQuery from beets.ui import Subcommand From 4102aae100d07344a67432ee00dfe8ef30f3743b Mon Sep 17 00:00:00 2001 From: Joris Date: Tue, 5 May 2020 10:45:43 +0200 Subject: [PATCH 13/15] Update docs/plugins/subsonicplaylist.rst Co-authored-by: Adrian Sampson --- docs/plugins/subsonicplaylist.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/subsonicplaylist.rst b/docs/plugins/subsonicplaylist.rst index b9a54c3de..98c83ebe1 100644 --- a/docs/plugins/subsonicplaylist.rst +++ b/docs/plugins/subsonicplaylist.rst @@ -40,4 +40,4 @@ Here's an example configuration with all the available options and their default username: '' password: '' -Parameters `base_url`, `username` and `password` must be defined! +The `base_url`, `username`, and `password` options are required. From 4933671c10b955a6ce7a732b520d0b6c4a9994a0 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Fri, 8 May 2020 21:36:56 +0200 Subject: [PATCH 14/15] review: updated subsonicplaylist.py --- beetsplug/subsonicplaylist.py | 77 ++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 14cd855b1..d02a8919d 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -15,24 +15,44 @@ from __future__ import absolute_import, division, print_function -from hashlib import md5 -import xml.etree.ElementTree as ET import random import string +import xml.etree.ElementTree as ET +from hashlib import md5 from urllib.parse import urlencode import requests -from beets.dbcore.query import SubstringQuery from beets.dbcore import AndQuery - +from beets.dbcore.query import SubstringQuery +from beets.plugins import BeetsPlugin from beets.ui import Subcommand -from beets.plugins import BeetsPlugin - __author__ = 'https://github.com/MrNuggelz' +def filter_to_be_removed(items, keys): + if len(items) > len(keys): + dont_remove = [] + for artist, album, title in keys: + for item in items: + if artist == item['artist'] and \ + album == item['album'] and \ + title == item['title']: + dont_remove.append(item) + return [item for item in items if item not in dont_remove] + else: + def to_be_removed(item): + for artist, album, title in keys: + if artist == item['artist'] and\ + album == item['album'] and\ + title == item['title']: + return False + return True + + return [item for item in items if to_be_removed(item)] + + class SubsonicPlaylistPlugin(BeetsPlugin): def __init__(self): @@ -80,12 +100,12 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def build_playlist(lib, opts, args): self.config.set_args(opts) ids = self.config['playlist_ids'].as_str_seq() - if len(self.config['playlist_names'].as_str_seq()) > 0: + if self.config['playlist_names'].as_str_seq(): playlists = ET.fromstring(self.send('getPlaylists').text)[ 0] if playlists.attrib.get('code', '200') != '200': alt_error = 'error getting playlists,' \ - ' but no erro message found' + ' but no error message found' self._log.warn( playlists.attrib.get('message', alt_error)) return @@ -93,17 +113,17 @@ class SubsonicPlaylistPlugin(BeetsPlugin): for playlist in playlists: if name == playlist.attrib['name']: ids.append(playlist.attrib['id']) - playlist_dict = dict() - for playlist_id in ids: - name, tracks = self.get_playlist(playlist_id) - for track in tracks: - if track not in playlist_dict: - playlist_dict[track] = ';' - playlist_dict[track] += name + ';' + + playlist_dict = self.get_playlists(ids) + # delete old tags if self.config['delete']: - for item in lib.items('subsonic_playlist:";"'): - item.update({'subsonic_playlist': ''}) + existing = list(lib.items('subsonic_playlist:";"')) + to_be_removed = filter_to_be_removed( + existing, + playlist_dict.keys()) + for item in to_be_removed: + item['subsonic_playlist'] = '' with lib.transaction(): item.try_sync(write=True, move=False) @@ -129,10 +149,21 @@ class SubsonicPlaylistPlugin(BeetsPlugin): def send(self, endpoint, params=None): if params is None: params = dict() - url = '{}/rest/{}?u={}&t={}&s={}&v=1.12.0&c=beets'.format( - self.config['base_url'].get(), - endpoint, - self.config['username'], - *self.generate_token()) - resp = requests.get(url + urlencode(params)) + a, b = self.generate_token() + params['u'] = self.config['username'] + params['t'] = a + params['s'] = b + params['v'] = '1.12.0' + params['c'] = 'beets' + resp = requests.get('{}/rest/{}?{}'.format(self.config['base_url'].get(),endpoint,urlencode(params))) return resp + + def get_playlists(self, ids): + output = dict() + for playlist_id in ids: + name, tracks = self.get_playlist(playlist_id) + for track in tracks: + if track not in output: + output[track] = ';' + output[track] += name + ';' + return output \ No newline at end of file From 5d90296a20b336349261e1d39a95503a605112f9 Mon Sep 17 00:00:00 2001 From: MrNuggelz Date: Sat, 9 May 2020 13:16:56 +0200 Subject: [PATCH 15/15] fixed flake8 issues --- beetsplug/subsonicplaylist.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index d02a8919d..95bdaa886 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -155,7 +155,11 @@ class SubsonicPlaylistPlugin(BeetsPlugin): params['s'] = b params['v'] = '1.12.0' params['c'] = 'beets' - resp = requests.get('{}/rest/{}?{}'.format(self.config['base_url'].get(),endpoint,urlencode(params))) + resp = requests.get('{}/rest/{}?{}'.format( + self.config['base_url'].get(), + endpoint, + urlencode(params)) + ) return resp def get_playlists(self, ids): @@ -166,4 +170,4 @@ class SubsonicPlaylistPlugin(BeetsPlugin): if track not in output: output[track] = ';' output[track] += name + ';' - return output \ No newline at end of file + return output