mirror of
https://github.com/beetbox/beets.git
synced 2026-01-17 13:44:01 +01:00
smartplaylist: add extm3u/extinf/m3u8 support
This is to be able to display meaningful metadata and search a playlist within a player without having to load the linked audio files of a playlist.
This commit is contained in:
parent
c1a232ec7b
commit
b07a2e42f4
4 changed files with 80 additions and 6 deletions
|
|
@ -49,6 +49,7 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
|||
"prefix": "",
|
||||
"urlencode": False,
|
||||
"pretend_paths": False,
|
||||
"extm3u": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -71,6 +72,17 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
|||
action="store_true",
|
||||
help="display query results but don't write playlist files.",
|
||||
)
|
||||
spl_update.parser.add_option(
|
||||
"--extm3u",
|
||||
action="store_true",
|
||||
help="add artist/title as m3u8 comments to playlists.",
|
||||
)
|
||||
spl_update.parser.add_option(
|
||||
"--no-extm3u",
|
||||
action="store_false",
|
||||
dest="extm3u",
|
||||
help="do not add artist/title as extm3u comments to playlists.",
|
||||
)
|
||||
spl_update.func = self.update_cmd
|
||||
return [spl_update]
|
||||
|
||||
|
|
@ -99,7 +111,7 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
|||
else:
|
||||
self._matched_playlists = self._unmatched_playlists
|
||||
|
||||
self.update_playlists(lib, opts.pretend)
|
||||
self.update_playlists(lib, opts.extm3u, opts.pretend)
|
||||
|
||||
def build_queries(self):
|
||||
"""
|
||||
|
|
@ -185,7 +197,7 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
|||
|
||||
self._unmatched_playlists -= self._matched_playlists
|
||||
|
||||
def update_playlists(self, lib, pretend=False):
|
||||
def update_playlists(self, lib, extm3u=None, pretend=False):
|
||||
if pretend:
|
||||
self._log.info(
|
||||
"Showing query results for {0} smart playlists...",
|
||||
|
|
@ -230,7 +242,7 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
|||
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_path)
|
||||
m3us[m3u_name].append({"item": item, "path": item_path})
|
||||
if pretend and self.config["pretend_paths"]:
|
||||
print(displayable_path(item_path))
|
||||
elif pretend:
|
||||
|
|
@ -244,13 +256,23 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
|||
os.path.join(playlist_dir, bytestring_path(m3u))
|
||||
)
|
||||
mkdirall(m3u_path)
|
||||
extm3u = extm3u is None and self.config["extm3u"] or extm3u
|
||||
with open(syspath(m3u_path), "wb") as f:
|
||||
for path in m3us[m3u]:
|
||||
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))
|
||||
f.write(prefix + path + b"\n")
|
||||
comment = ""
|
||||
if extm3u:
|
||||
comment = "#EXTINF:{},{} - {}\n".format(
|
||||
int(item.length), item.artist, item.title
|
||||
)
|
||||
f.write(comment.encode("utf-8") + prefix + path + b"\n")
|
||||
# Send an event when playlists were updated.
|
||||
send_event("smartplaylist_update")
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ 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`.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
|
|
|||
|
|
@ -118,3 +118,4 @@ other configuration options are:
|
|||
- **urlencoded**: 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``.
|
||||
- **extm3u**: Generate extm3u/m3u8 playlists. Default ``ǹo``.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from shutil import rmtree
|
|||
from tempfile import mkdtemp
|
||||
from test import _common
|
||||
from test.helper import TestHelper
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from unittest.mock import MagicMock, Mock, PropertyMock
|
||||
|
||||
from beets import config
|
||||
from beets.dbcore import OrQuery
|
||||
|
|
@ -191,6 +191,56 @@ class SmartPlaylistTest(_common.TestCase):
|
|||
|
||||
self.assertEqual(content, b"/tagada.mp3\n")
|
||||
|
||||
def test_playlist_update_extm3u(self):
|
||||
spl = SmartPlaylistPlugin()
|
||||
|
||||
i = MagicMock()
|
||||
type(i).artist = PropertyMock(return_value="fake artist")
|
||||
type(i).title = PropertyMock(return_value="fake title")
|
||||
type(i).length = PropertyMock(return_value=300.123)
|
||||
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<playlist>.m3u", (q, None), (a_q, None)
|
||||
spl._matched_playlists = [pl]
|
||||
|
||||
dir = bytestring_path(mkdtemp())
|
||||
config["smartplaylist"]["extm3u"] = True
|
||||
config["smartplaylist"]["prefix"] = "http://beets:8337/files"
|
||||
config["smartplaylist"]["relative_to"] = False
|
||||
config["smartplaylist"]["playlist_dir"] = py3_path(dir)
|
||||
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"#EXTM3U\n"
|
||||
+ b"#EXTINF:300,fake artist - fake title\n"
|
||||
+ b"http://beets:8337/files/tagada.mp3\n",
|
||||
)
|
||||
|
||||
|
||||
class SmartPlaylistCLITest(_common.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue