diff options
Diffstat (limited to 'overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist')
| -rw-r--r-- | overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py | 0 | ||||
| -rw-r--r-- | overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py | 107 |
2 files changed, 107 insertions, 0 deletions
diff --git a/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py | |||
diff --git a/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py new file mode 100644 index 00000000..fd739805 --- /dev/null +++ b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | import click | ||
| 2 | from pathlib import Path | ||
| 3 | import tomllib | ||
| 4 | import requests | ||
| 5 | from urllib.parse import urljoin | ||
| 6 | from operator import itemgetter | ||
| 7 | import re | ||
| 8 | from frozendict import frozendict | ||
| 9 | |||
| 10 | class BearerAuth(requests.auth.AuthBase): | ||
| 11 | def __init__(self, token): | ||
| 12 | self.token = token | ||
| 13 | def __call__(self, r): | ||
| 14 | r.headers["authorization"] = "Bearer " + self.token | ||
| 15 | return r | ||
| 16 | |||
| 17 | class ABSSession(requests.Session): | ||
| 18 | def __init__(self, config): | ||
| 19 | super().__init__() | ||
| 20 | self.base_url = config['instance'] | ||
| 21 | self.auth = BearerAuth(config['api_token']) | ||
| 22 | |||
| 23 | def request(self, method, url, *args, **kwargs): | ||
| 24 | joined_url = urljoin(self.base_url, url) | ||
| 25 | return super().request(method, joined_url, *args, **kwargs) | ||
| 26 | |||
| 27 | @click.command() | ||
| 28 | @click.argument('config_file', type=click.Path(dir_okay=False, path_type=Path)) | ||
| 29 | def main(config_file: Path): | ||
| 30 | with config_file.open('rb') as fh: | ||
| 31 | config = tomllib.load(fh) | ||
| 32 | |||
| 33 | with ABSSession(config) as s: | ||
| 34 | libraries = s.get('/api/libraries').json()['libraries'] | ||
| 35 | playlists = s.get('/api/playlists').json()['playlists'] | ||
| 36 | |||
| 37 | for library_config in config['libraries']: | ||
| 38 | [library] = filter(lambda l: l['name'] == library_config['name'], libraries) | ||
| 39 | filtered_playlists = list(filter(lambda p: p['name'] == library_config['playlist'] and p['libraryId'] == library['id'], playlists)) | ||
| 40 | def get_playlist(): | ||
| 41 | playlist = None | ||
| 42 | if filtered_playlists: | ||
| 43 | [playlist] = filtered_playlists | ||
| 44 | if not playlist: | ||
| 45 | playlist = s.post('/api/playlists', json={ | ||
| 46 | 'libraryId': library['id'], | ||
| 47 | 'name': library_config['playlist'], | ||
| 48 | }).json() | ||
| 49 | return playlist | ||
| 50 | |||
| 51 | podcasts = dict() | ||
| 52 | items = s.get('/api/libraries/{}/items'.format(library['id'])).json()['results'] | ||
| 53 | for item in items: | ||
| 54 | item = s.get('/api/items/{}'.format(item['id']), json={'expanded': True}).json() | ||
| 55 | episodes = list() | ||
| 56 | for episode in sorted(item['media']['episodes'], key = itemgetter('publishedAt')): | ||
| 57 | progress = s.get('/api/me/progress/{}/{}'.format(episode['libraryItemId'], episode['id'])) | ||
| 58 | if progress.ok and progress.json()["isFinished"]: | ||
| 59 | continue | ||
| 60 | episodes.append(episode) | ||
| 61 | podcasts[item['media']['metadata']['title']] = list(map(lambda x: frozendict({ 'libraryItemId': x['libraryItemId'], 'episodeId': x['id']}), episodes)) | ||
| 62 | def lookup_podcast(expr): | ||
| 63 | expr = re.compile(expr, flags=re.I) | ||
| 64 | matches = filter(lambda t: expr.search(t), podcasts.keys()) | ||
| 65 | match list(matches): | ||
| 66 | case [x]: | ||
| 67 | return (x,) | ||
| 68 | case _: | ||
| 69 | raise RuntimeError("No unique match for ‘{}’".format(expr)) | ||
| 70 | |||
| 71 | priorities = [ | ||
| 72 | [ | ||
| 73 | k | ||
| 74 | for item in (section if type(section) is list else [section]) | ||
| 75 | for k in lookup_podcast(item) | ||
| 76 | ] | ||
| 77 | for section in library_config['priorities'] | ||
| 78 | ] | ||
| 79 | |||
| 80 | playlist_items = list() | ||
| 81 | for section in priorities: | ||
| 82 | while any(map(lambda item: item in podcasts, section)): | ||
| 83 | for item in section: | ||
| 84 | if not item in podcasts: | ||
| 85 | continue | ||
| 86 | |||
| 87 | if not podcasts[item]: | ||
| 88 | del podcasts[item] | ||
| 89 | continue | ||
| 90 | |||
| 91 | playlist_items.append(podcasts[item].pop(0)) | ||
| 92 | |||
| 93 | playlist = get_playlist() | ||
| 94 | current_playlist_items = map(lambda item: frozendict({ k: v for k, v in item.items() if k in {'libraryItemId', 'episodeId'}}), playlist['items']) | ||
| 95 | |||
| 96 | if current_playlist_items == playlist_items: | ||
| 97 | continue | ||
| 98 | |||
| 99 | to_remove = set(current_playlist_items) - set(playlist_items) | ||
| 100 | if to_remove: | ||
| 101 | s.post('/api/playlists/{}/batch/remove'.format(playlist['id']), json={'items': list(to_remove)}).raise_for_status() | ||
| 102 | playlist = get_playlist() | ||
| 103 | to_add = set(playlist_items) - set(current_playlist_items) | ||
| 104 | if to_add: | ||
| 105 | s.post('/api/playlists/{}/batch/add'.format(playlist['id']), json={'items': list(to_add)}).raise_for_status() | ||
| 106 | |||
| 107 | r = s.patch('/api/playlists/{}'.format(playlist['id']), json={'items': playlist_items}).raise_for_status() | ||
