diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 120361d311..12a1c92181 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -45,6 +45,7 @@ def __init__(self): "playlist_dir": ".", "auto": True, "playlists": [], + "uri_format": None, "forward_slash": False, "prefix": "", "urlencode": False, @@ -109,6 +110,12 @@ def commands(self): action="store_true", help="URL-encode all paths.", ) + spl_update.parser.add_option( + "--uri-format", + dest="uri_format", + type="string", + help="playlist item URI template, e.g. http://beets:8337/item/$id/file.", + ) spl_update.parser.add_option( "--output", type="string", @@ -247,6 +254,8 @@ def update_playlists(self, lib, pretend=False): playlist_dir = self.config["playlist_dir"].as_filename() playlist_dir = bytestring_path(playlist_dir) + tpl = self.config["uri_format"].get() + prefix = bytestring_path(self.config["prefix"].as_str()) relative_to = self.config["relative_to"].get() if relative_to: relative_to = normpath(relative_to) @@ -275,18 +284,26 @@ def update_playlists(self, lib, 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( @@ -303,18 +320,13 @@ def update_playlists(self, lib, pretend=False): if m3u8: 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 m3u8: 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/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst index 365b5af321..a40d188823 100644 --- a/docs/plugins/smartplaylist.rst +++ b/docs/plugins/smartplaylist.rst @@ -118,9 +118,13 @@ other configuration options are: - **urlencode**: URL-encode all paths. Default: ``no``. - **pretend_paths**: When running with ``--pretend``, show the actual file paths that will be written to the m3u file. Default: ``false``. +- **uri_format**: 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. - **output**: Specify the playlist format: m3u|m3u8. Default ``m3u``. For many configuration options, there is a corresponding CLI option, e.g. ``--playlist-dir``, ``--relative-to``, ``--prefix``, ``--forward-slash``, -``--urlencode``, ``--output``, ``--pretend-paths``. +``--urlencode``, ``--uri-format``, ``--output``, ``--pretend-paths``. CLI options take precedence over those specified within the configuration file. diff --git a/test/plugins/test_smartplaylist.py b/test/plugins/test_smartplaylist.py index 96eac625fb..921ae815ee 100644 --- a/test/plugins/test_smartplaylist.py +++ b/test/plugins/test_smartplaylist.py @@ -241,6 +241,51 @@ def test_playlist_update_output_m3u8(self): + b"http://beets:8337/files/tagada.mp3\n", ) + def test_playlist_update_uri_format(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_format"] = tpl + config["smartplaylist"]["playlist_dir"] = py3_path(dir) + # The following options should be ignored when uri_format 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):