Skip to content

Commit 3d7641a

Browse files
authored
Merge pull request #14 from mgoltzsche/aggregate-playlist-artists
list top artists/playlist, add mime types, fallback to default mime type
2 parents 20b0a9e + 369b28a commit 3d7641a

File tree

4 files changed

+73
-16
lines changed

4 files changed

+73
-16
lines changed

beetsplug/beetstream/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ def func(lib, opts, args):
8080
app.config['never_transcode'] = self.config['never_transcode']
8181
playlist_dir = self.config['playlist_dir']
8282
if not playlist_dir:
83-
playlist_dir = config['smartplaylist']['playlist_dir'].get()
83+
try:
84+
playlist_dir = config['smartplaylist']['playlist_dir'].get()
85+
except:
86+
pass
8487
app.config['playlist_dir'] = playlist_dir
8588

8689
# Enable CORS if required.

beetsplug/beetstream/playlistprovider.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,102 @@
22
import os
33
import pathlib
44
import re
5+
import sys
6+
from beetsplug.beetstream.utils import strip_accents
7+
from flask import current_app as app
58
from werkzeug.utils import safe_join
69

710
extinf_regex = re.compile(r'^#EXTINF:([0-9]+)( [^,]+)?,[\s]*(.*)')
11+
highint32 = 1<<31
812

913
class PlaylistProvider:
1014
def __init__(self, dir):
11-
self._dir = dir
15+
self.dir = dir
1216
self._playlists = {}
1317

1418
def _refresh(self):
15-
paths = glob.glob(os.path.join(self._dir, "**.m3u8"))
16-
paths += glob.glob(os.path.join(self._dir, "**.m3u"))
19+
self._playlists = {p.id: p for p in self._load_playlists()}
20+
app.logger.debug(f"Loaded {len(self._playlists)} playlists")
21+
22+
def _load_playlists(self):
23+
if not self.dir:
24+
return
25+
paths = glob.glob(os.path.join(self.dir, "**.m3u8"))
26+
paths += glob.glob(os.path.join(self.dir, "**.m3u"))
1727
paths.sort()
18-
self._playlists = {self._path2id(p): self._playlist(p) for p in paths}
28+
for path in paths:
29+
try:
30+
yield self._playlist(path)
31+
except Exception as e:
32+
app.logger.error(f"Failed to load playlist {filepath}: {e}")
1933

2034
def playlists(self):
2135
self._refresh()
22-
ids = [k for k in self._playlists]
36+
playlists = self._playlists
37+
ids = [k for k, v in playlists.items() if v]
2338
ids.sort()
24-
return [self._playlists[id] for id in ids]
39+
return [playlists[id] for id in ids]
2540

2641
def playlist(self, id):
27-
self._refresh()
28-
filepath = safe_join(self._dir, id)
29-
return self._playlist(filepath)
42+
filepath = safe_join(self.dir, id)
43+
playlist = self._playlist(filepath)
44+
if playlist.id not in self._playlists: # add to cache
45+
playlists = self._playlists.copy()
46+
playlists[playlist.id] = playlist
47+
self._playlists = playlists
48+
return playlist
3049

3150
def _playlist(self, filepath):
3251
id = self._path2id(filepath)
3352
name = pathlib.Path(os.path.basename(filepath)).stem
3453
playlist = self._playlists.get(id)
3554
mtime = pathlib.Path(filepath).stat().st_mtime
3655
if playlist and playlist.modified == mtime:
37-
return playlist # cached
56+
return playlist # cached metadata
57+
app.logger.debug(f"Loading playlist {filepath}")
3858
return Playlist(id, name, mtime, filepath)
3959

4060
def _path2id(self, filepath):
41-
return os.path.relpath(filepath, self._dir)
61+
return os.path.relpath(filepath, self.dir)
4262

43-
class Playlist():
63+
class Playlist:
4464
def __init__(self, id, name, modified, path):
4565
self.id = id
4666
self.name = name
4767
self.modified = modified
4868
self.path = path
4969
self.count = 0
5070
self.duration = 0
51-
for item in parse_m3u_playlist(self.path):
71+
artists = {}
72+
max_artists = 10
73+
for item in self.items():
5274
self.count += 1
5375
self.duration += item.duration
76+
artist = Artist(item.title.split(' - ')[0])
77+
found = artists.get(artist.key)
78+
if found:
79+
found.count += 1
80+
else:
81+
if len(artists) > max_artists:
82+
l = _sortedartists(artists)[:max_artists]
83+
artists = {a.key: a for a in l}
84+
artists[artist.key] = artist
85+
self.artists = ', '.join([a.name for a in _sortedartists(artists)])
5486

5587
def items(self):
5688
return parse_m3u_playlist(self.path)
5789

90+
def _sortedartists(artists):
91+
l = [a for _,a in artists.items()]
92+
l.sort(key=lambda a: (highint32-a.count, a.name))
93+
return l
94+
95+
class Artist:
96+
def __init__(self, name):
97+
self.key = strip_accents(name.lower())
98+
self.name = name
99+
self.count = 1
100+
58101
def parse_m3u_playlist(filepath):
59102
'''
60103
Parses an M3U playlist and yields its items, one at a time.

beetsplug/beetstream/playlists.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,8 @@ def _song(id):
4949
return map_song(g.lib.get_item(int(id)))
5050

5151
def playlist_provider():
52-
_playlist_provider._dir = app.config['playlist_dir']
52+
if 'playlist_dir' in app.config:
53+
_playlist_provider.dir = app.config['playlist_dir']
54+
if not _playlist_provider.dir:
55+
app.logger.warning('No playlist_dir configured')
5356
return _playlist_provider

beetsplug/beetstream/utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111
from math import ceil
1212
from xml.dom import minidom
1313

14+
DEFAULT_MIME_TYPE = 'application/octet-stream'
1415
EXTENSION_TO_MIME_TYPE_FALLBACK = {
1516
'.aac' : 'audio/aac',
1617
'.flac' : 'audio/flac',
1718
'.mp3' : 'audio/mpeg',
19+
'.mp4' : 'audio/mp4',
20+
'.m4a' : 'audio/mp4',
1821
'.ogg' : 'audio/ogg',
22+
'.opus' : 'audio/opus',
1923
}
2024

2125
def strip_accents(s):
@@ -214,6 +218,7 @@ def map_playlist(playlist):
214218
'name': playlist.name,
215219
'songCount': playlist.count,
216220
'duration': playlist.duration,
221+
'comment': playlist.artists,
217222
'created': timestamp_to_iso(playlist.modified),
218223
}
219224

@@ -222,6 +227,7 @@ def map_playlist_xml(xml, playlist):
222227
xml.set('name', playlist.name)
223228
xml.set('songCount', str(playlist.count))
224229
xml.set('duration', str(ceil(playlist.duration)))
230+
xml.set('comment', playlist.artists)
225231
xml.set('created', timestamp_to_iso(playlist.modified))
226232

227233
def artist_name_to_id(name):
@@ -257,7 +263,9 @@ def path_to_content_type(path):
257263
if result:
258264
return result
259265

260-
raise RuntimeError(f"Unable to determine content type for {path}")
266+
flask.current_app.logger.warning(f"No mime type mapped for {ext} extension: {path}")
267+
268+
return DEFAULT_MIME_TYPE
261269

262270
def handleSizeAndOffset(collection, size, offset):
263271
if size is not None:

0 commit comments

Comments
 (0)