diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index c892a6040d..0867479582 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -45,6 +45,7 @@ def __init__(self): "playlist_dir": ".", "auto": True, "playlists": [], + "uri_template": None, "forward_slash": False, "prefix": "", "urlencode": False, @@ -72,6 +73,13 @@ def commands(self): action="store_true", help="display query results but don't write playlist files.", ) + spl_update.parser.add_option( + "--uri-template", + dest="uri_template", + metavar="TPL", + type="string", + help="playlist item URI template with `$id` placeholder, e.g. http://beets:8337/item/$id/file.", + ) spl_update.parser.add_option( "--extm3u", action="store_true", @@ -210,6 +218,8 @@ def update_playlists(self, lib, extm3u=None, pretend=False): playlist_dir = self.config["playlist_dir"].as_filename() playlist_dir = bytestring_path(playlist_dir) + tpl = self.config["uri_template"].get() + prefix = bytestring_path(self.config["prefix"].as_str()) relative_to = self.config["relative_to"].get() if relative_to: relative_to = normpath(relative_to) @@ -238,18 +248,26 @@ def update_playlists(self, lib, extm3u=None, pretend=False): m3u_name = sanitize_path(m3u_name, lib.replacements) if m3u_name not in m3us: m3us[m3u_name] = [] - item_path = item.path - if relative_to: - item_path = os.path.relpath(item.path, relative_to) - if item_path not in m3us[m3u_name]: - m3us[m3u_name].append({"item": item, "path": item_path}) + item_uri = item.path + if tpl: + item_uri = tpl.replace("$id", str(item.id)).encode("utf-8") + else: + if relative_to: + item_uri = os.path.relpath(item_uri, relative_to) + if self.config["forward_slash"].get(): + item_uri = path_as_posix(item_uri) + if self.config["urlencode"]: + item_uri = bytestring_path(pathname2url(item_uri)) + item_uri = prefix + item_uri + + if item_uri not in m3us[m3u_name]: + m3us[m3u_name].append({"item": item, "uri": item_uri}) if pretend and self.config["pretend_paths"]: - print(displayable_path(item_path)) + print(displayable_path(item_uri)) elif pretend: print(item) if not pretend: - prefix = bytestring_path(self.config["prefix"].as_str()) # Write all of the accumulated track lists to files. for m3u in m3us: m3u_path = normpath( @@ -261,18 +279,13 @@ def update_playlists(self, lib, extm3u=None, pretend=False): if extm3u: f.write(b"#EXTM3U\n") for entry in m3us[m3u]: - path = entry["path"] item = entry["item"] - if self.config["forward_slash"].get(): - path = path_as_posix(path) - if self.config["urlencode"]: - path = bytestring_path(pathname2url(path)) comment = "" if extm3u: comment = "#EXTINF:{},{} - {}\n".format( int(item.length), item.artist, item.title ) - f.write(comment.encode("utf-8") + prefix + path + b"\n") + f.write(comment.encode("utf-8") + entry["uri"] + b"\n") # Send an event when playlists were updated. send_event("smartplaylist_update") diff --git a/docs/changelog.rst b/docs/changelog.rst index 9259b5933c..7b5e88c3ae 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -148,7 +148,8 @@ New features: `synced` option to prefer synced lyrics over plain lyrics. * :ref:`import-cmd`: Expose import.quiet_fallback as CLI option. * :ref:`import-cmd`: Expose `import.incremental_skip_later` as CLI option. -* :doc:`/plugins/smartplaylist`: Add new config option `smartplaylist.extm3u`. +* :doc:`/plugins/smartplaylist`: Add new option `smartplaylist.extm3u`. +* :doc:`/plugins/smartplaylist`: Add new option `smartplaylist.uri_template`. Bug fixes: diff --git a/docs/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst index 6a78124e1b..b8bf547f97 100644 --- a/docs/plugins/smartplaylist.rst +++ b/docs/plugins/smartplaylist.rst @@ -119,3 +119,7 @@ other configuration options are: - **pretend_paths**: When running with ``--pretend``, show the actual file paths that will be written to the m3u file. Default: ``false``. - **extm3u**: Generate extm3u/m3u8 playlists. Default ``ǹo``. +- **uri_template**: Template with an ``$id`` placeholder used generate a + playlist item URI, e.g. ``http://beets:8337/item/$id/file``. + When this option is specified, the local path-related options ``prefix``, + ``relative_to``, ``forward_slash`` and ``urlencode`` are ignored. diff --git a/test/plugins/test_smartplaylist.py b/test/plugins/test_smartplaylist.py index f36601267d..45b92c97a2 100644 --- a/test/plugins/test_smartplaylist.py +++ b/test/plugins/test_smartplaylist.py @@ -241,6 +241,51 @@ def test_playlist_update_extm3u(self): + b"http://beets:8337/files/tagada.mp3\n", ) + def test_playlist_update_uri_template(self): + spl = SmartPlaylistPlugin() + + i = MagicMock() + type(i).id = PropertyMock(return_value=3) + type(i).path = PropertyMock(return_value=b"/tagada.mp3") + i.evaluate_template.side_effect = lambda pl, _: pl.replace( + b"$title", b"ta:ga:da" + ).decode() + + lib = Mock() + lib.replacements = CHAR_REPLACE + lib.items.return_value = [i] + lib.albums.return_value = [] + + q = Mock() + a_q = Mock() + pl = b"$title-my.m3u", (q, None), (a_q, None) + spl._matched_playlists = [pl] + + dir = bytestring_path(mkdtemp()) + tpl = "http://beets:8337/item/$id/file" + config["smartplaylist"]["uri_template"] = tpl + config["smartplaylist"]["playlist_dir"] = py3_path(dir) + # The following options should be ignored when uri_template is set + config["smartplaylist"]["relative_to"] = "/data" + config["smartplaylist"]["prefix"] = "/prefix" + config["smartplaylist"]["urlencode"] = True + try: + spl.update_playlists(lib) + except Exception: + rmtree(syspath(dir)) + raise + + lib.items.assert_called_once_with(q, None) + lib.albums.assert_called_once_with(a_q, None) + + m3u_filepath = path.join(dir, b"ta_ga_da-my_playlist_.m3u") + self.assertExists(m3u_filepath) + with open(syspath(m3u_filepath), "rb") as f: + content = f.read() + rmtree(syspath(dir)) + + self.assertEqual(content, b"http://beets:8337/item/3/file\n") + class SmartPlaylistCLITest(_common.TestCase, TestHelper): def setUp(self):