|
2 | 2 | import os
|
3 | 3 | import pathlib
|
4 | 4 | import re
|
| 5 | +import sys |
| 6 | +from beetsplug.beetstream.utils import strip_accents |
| 7 | +from flask import current_app as app |
5 | 8 | from werkzeug.utils import safe_join
|
6 | 9 |
|
7 | 10 | extinf_regex = re.compile(r'^#EXTINF:([0-9]+)( [^,]+)?,[\s]*(.*)')
|
| 11 | +highint32 = 1<<31 |
8 | 12 |
|
9 | 13 | class PlaylistProvider:
|
10 | 14 | def __init__(self, dir):
|
11 |
| - self._dir = dir |
| 15 | + self.dir = dir |
12 | 16 | self._playlists = {}
|
13 | 17 |
|
14 | 18 | 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")) |
17 | 27 | 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}") |
19 | 33 |
|
20 | 34 | def playlists(self):
|
21 | 35 | 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] |
23 | 38 | ids.sort()
|
24 |
| - return [self._playlists[id] for id in ids] |
| 39 | + return [playlists[id] for id in ids] |
25 | 40 |
|
26 | 41 | 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 |
30 | 49 |
|
31 | 50 | def _playlist(self, filepath):
|
32 | 51 | id = self._path2id(filepath)
|
33 | 52 | name = pathlib.Path(os.path.basename(filepath)).stem
|
34 | 53 | playlist = self._playlists.get(id)
|
35 | 54 | mtime = pathlib.Path(filepath).stat().st_mtime
|
36 | 55 | if playlist and playlist.modified == mtime:
|
37 |
| - return playlist # cached |
| 56 | + return playlist # cached metadata |
| 57 | + app.logger.debug(f"Loading playlist {filepath}") |
38 | 58 | return Playlist(id, name, mtime, filepath)
|
39 | 59 |
|
40 | 60 | def _path2id(self, filepath):
|
41 |
| - return os.path.relpath(filepath, self._dir) |
| 61 | + return os.path.relpath(filepath, self.dir) |
42 | 62 |
|
43 |
| -class Playlist(): |
| 63 | +class Playlist: |
44 | 64 | def __init__(self, id, name, modified, path):
|
45 | 65 | self.id = id
|
46 | 66 | self.name = name
|
47 | 67 | self.modified = modified
|
48 | 68 | self.path = path
|
49 | 69 | self.count = 0
|
50 | 70 | self.duration = 0
|
51 |
| - for item in parse_m3u_playlist(self.path): |
| 71 | + artists = {} |
| 72 | + max_artists = 10 |
| 73 | + for item in self.items(): |
52 | 74 | self.count += 1
|
53 | 75 | 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)]) |
54 | 86 |
|
55 | 87 | def items(self):
|
56 | 88 | return parse_m3u_playlist(self.path)
|
57 | 89 |
|
| 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 | + |
58 | 101 | def parse_m3u_playlist(filepath):
|
59 | 102 | '''
|
60 | 103 | Parses an M3U playlist and yields its items, one at a time.
|
|
0 commit comments