Skip to content

Commit a7c3aea

Browse files
committed
smartplaylist: add --uri-template option
Beets web API already allows remote players to access audio files but it doesn't provide a way to expose the playlists defined using the smartplaylist plugin. Now the smartplaylist plugin provides an option to generate ID-based item URIs/URLs instead of paths. Once playlists are generated this way, they can be served using a regular HTTP server such as nginx. To provide sufficient flexibility for various ways of integrating beets remotely (e.g. beets API, beets API with context path, AURA API, mopidy resource URI, etc), the new option has been defined as a template with an `$id` placeholder (assuming each remote integration requires a different path schema but they all rely on using the beets item `id` as identifier/path segment). To prevent local path-related plugin configuration from leaking into a HTTP URL-based playlist generation (invoked with CLI option in addition to the local playlists generated into another directory), setting the new option makes the plugin ignore the other path-related options `prefix`, `relative_to`, `forward_slash` and `urlencode`. Usage examples: * `beet splupdate --uri-template 'http://beets:8337/item/$id/file'` (for beets web API) * `beet splupdate --uri-template 'http://beets:8337/aura/tracks/$id/audio'` (for AURA API) (While it was already possible to generate playlists containing HTTP URLs previously using the `prefix` option, it did not allow to generate ID-based URLs pointing to the beets web API but required to expose the audio files using a web server directly and refer to them using their file system path.) Relates to #5037
1 parent 60ad1ba commit a7c3aea

File tree

4 files changed

+72
-13
lines changed

4 files changed

+72
-13
lines changed

beetsplug/smartplaylist.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(self):
4545
"playlist_dir": ".",
4646
"auto": True,
4747
"playlists": [],
48+
"uri_template": None,
4849
"forward_slash": False,
4950
"prefix": "",
5051
"urlencode": False,
@@ -72,6 +73,13 @@ def commands(self):
7273
action="store_true",
7374
help="display query results but don't write playlist files.",
7475
)
76+
spl_update.parser.add_option(
77+
"--uri-template",
78+
dest="uri_template",
79+
metavar="TPL",
80+
type="string",
81+
help="playlist item URI template with `$id` placeholder, e.g. http://beets:8337/item/$id/file.",
82+
)
7583
spl_update.parser.add_option(
7684
"--extm3u",
7785
action="store_true",
@@ -208,6 +216,7 @@ def update_playlists(self, lib, extm3u=None, pretend=False):
208216
"Updating {0} smart playlists...", len(self._matched_playlists)
209217
)
210218

219+
tpl = self.config["uri_template"].get()
211220
playlist_dir = self.config["playlist_dir"].as_filename()
212221
playlist_dir = bytestring_path(playlist_dir)
213222
relative_to = self.config["relative_to"].get()
@@ -238,13 +247,22 @@ def update_playlists(self, lib, extm3u=None, pretend=False):
238247
m3u_name = sanitize_path(m3u_name, lib.replacements)
239248
if m3u_name not in m3us:
240249
m3us[m3u_name] = []
241-
item_path = item.path
242-
if relative_to:
243-
item_path = os.path.relpath(item.path, relative_to)
244-
if item_path not in m3us[m3u_name]:
245-
m3us[m3u_name].append({"item": item, "path": item_path})
250+
item_uri = item.path
251+
if tpl:
252+
item_uri = tpl.replace("$id", str(item.id)).encode("utf-8")
253+
else:
254+
if relative_to:
255+
item_uri = os.path.relpath(item_uri, relative_to)
256+
if self.config["forward_slash"].get():
257+
item_uri = path_as_posix(item_uri)
258+
if self.config["urlencode"]:
259+
item_uri = bytestring_path(pathname2url(item_uri))
260+
item_uri = prefix + item_uri
261+
262+
if item_uri not in m3us[m3u_name]:
263+
m3us[m3u_name].append({"item": item, "uri": item_uri})
246264
if pretend and self.config["pretend_paths"]:
247-
print(displayable_path(item_path))
265+
print(displayable_path(item_uri))
248266
elif pretend:
249267
print(item)
250268

@@ -261,18 +279,13 @@ def update_playlists(self, lib, extm3u=None, pretend=False):
261279
if extm3u:
262280
f.write(b"#EXTM3U\n")
263281
for entry in m3us[m3u]:
264-
path = entry["path"]
265282
item = entry["item"]
266-
if self.config["forward_slash"].get():
267-
path = path_as_posix(path)
268-
if self.config["urlencode"]:
269-
path = bytestring_path(pathname2url(path))
270283
comment = ""
271284
if extm3u:
272285
comment = "#EXTINF:{},{} - {}\n".format(
273286
int(item.length), item.artist, item.title
274287
)
275-
f.write(comment.encode("utf-8") + prefix + path + b"\n")
288+
f.write(comment.encode("utf-8") + entry["uri"] + b"\n")
276289
# Send an event when playlists were updated.
277290
send_event("smartplaylist_update")
278291

docs/changelog.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ New features:
148148
`synced` option to prefer synced lyrics over plain lyrics.
149149
* :ref:`import-cmd`: Expose import.quiet_fallback as CLI option.
150150
* :ref:`import-cmd`: Expose `import.incremental_skip_later` as CLI option.
151-
* :doc:`/plugins/smartplaylist`: Add new config option `smartplaylist.extm3u`.
151+
* :doc:`/plugins/smartplaylist`: Add new option `smartplaylist.extm3u`.
152+
* :doc:`/plugins/smartplaylist`: Add new option `smartplaylist.uri_template`.
152153

153154
Bug fixes:
154155

docs/plugins/smartplaylist.rst

+4
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,7 @@ other configuration options are:
119119
- **pretend_paths**: When running with ``--pretend``, show the actual file
120120
paths that will be written to the m3u file. Default: ``false``.
121121
- **extm3u**: Generate extm3u/m3u8 playlists. Default ``ǹo``.
122+
- **uri_template**: Template with an ``$id`` placeholder used generate a
123+
playlist item URI, e.g. ``http://beets/item/$id/file``.
124+
When this option is specified, the local path-related options ``prefix``,
125+
``relative_to``, ``forward_slash`` and ``urlencode`` are ignored.

test/plugins/test_smartplaylist.py

+41
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,47 @@ def test_playlist_update_extm3u(self):
241241
+ b"http://beets:8337/files/tagada.mp3\n",
242242
)
243243

244+
def test_playlist_update_uri_template(self):
245+
spl = SmartPlaylistPlugin()
246+
247+
i = MagicMock()
248+
type(i).id = PropertyMock(return_value=3)
249+
type(i).path = PropertyMock(return_value=b"/tagada.mp3")
250+
i.evaluate_template.side_effect = lambda pl, _: pl.replace(
251+
b"$title", b"ta:ga:da"
252+
).decode()
253+
254+
lib = Mock()
255+
lib.replacements = CHAR_REPLACE
256+
lib.items.return_value = [i]
257+
lib.albums.return_value = []
258+
259+
q = Mock()
260+
a_q = Mock()
261+
pl = b"$title-my<playlist>.m3u", (q, None), (a_q, None)
262+
spl._matched_playlists = [pl]
263+
264+
dir = bytestring_path(mkdtemp())
265+
tpl = "http://beets:8337/item/$id/file"
266+
config["smartplaylist"]["uri_template"] = tpl
267+
config["smartplaylist"]["playlist_dir"] = py3_path(dir)
268+
try:
269+
spl.update_playlists(lib)
270+
except Exception:
271+
rmtree(syspath(dir))
272+
raise
273+
274+
lib.items.assert_called_once_with(q, None)
275+
lib.albums.assert_called_once_with(a_q, None)
276+
277+
m3u_filepath = path.join(dir, b"ta_ga_da-my_playlist_.m3u")
278+
self.assertExists(m3u_filepath)
279+
with open(syspath(m3u_filepath), "rb") as f:
280+
content = f.read()
281+
rmtree(syspath(dir))
282+
283+
self.assertEqual(content, b"http://beets:8337/item/3/file\n")
284+
244285

245286
class SmartPlaylistCLITest(_common.TestCase, TestHelper):
246287
def setUp(self):

0 commit comments

Comments
 (0)