#!/usr/bin/env python3
# vim: set ts=4 sw=4 expandtab syntax=python :
import os
import os.path
import html
import json
import hashlib
import textwrap
import urllib.parse as up
import urllib.request as urq
from datetime import datetime, timezone
from pathlib import Path
CONFIG = None
SERVER_TYPE = None
def loadConfig(cfgfile = None):
if cfgfile is None or cfgfile == '':
cfgdir = Path(os.path.expanduser(os.getenv('XDG_CONFIG_DIR', '~/.config')))
cfgfile = cfgdir / 'iris-subsonic.json'
cfgfile = Path(cfgfile)
if not cfgfile.is_file():
raise RuntimeError(f"Configuration file {cfgfile!r} does not exist")
with open(cfgfile, 'r') as fh:
return json.loads(fh.read())
def hashPassword(password):
salt = hashlib.md5(os.urandom(24)).hexdigest()
hashed = hashlib.md5((password + salt).encode('utf-8')).hexdigest()
return (salt, hashed)
def apiGet(slug, params = {}):
if not 'u' in params:
params['u'] = CONFIG['user']
if not 'p' in params:
params['p'] = CONFIG['pass']
if 'p' in params:
s, t = hashPassword(params['p'])
params['s'] = s
params['t'] = t
del params['p']
params['f'] = 'json'
params['c'] = 'iris-subsonic/0.1'
params['v'] = '1.16.1'
url = CONFIG['host'] + slug + '?' + up.urlencode(params)
with urq.urlopen(url) as resp:
res = json.loads(resp.read())
if 'subsonic-response' in res and 'status' in res['subsonic-response']:
if res['subsonic-response']['status'] == 'ok':
return res['subsonic-response']
status = res['subsonic-response']['status']
raise RuntimeError(f"Subsonic API returned non-ok status for {slug!r}: {status!r}")
def apiPing(params = {}):
return apiGet('/rest/ping', params)
def apiAlbumList(params = {}):
if not 'type' in params:
params['type'] = 'alphabeticalByArtist'
if not 'size' in params:
params['size'] = 500
albums, coffset = ([], 0)
while True:
params['offset'] = coffset
res = apiGet('/rest/getAlbumList2', params)
if 'album' not in res['albumList2'] or len(res['albumList2']['album']) == 0:
break
albums += res['albumList2']['album']
coffset += len(res['albumList2']['album'])
return albums
def apiAlbumInfo(params = {}):
# XXX: this is not implemented in Navidrome
if SERVER_TYPE in ['navidrome']:
raise RuntimeError(f"/rest/getAlbumInfo2 not implemented on server type {SERVER_TYPE!r}")
res = apiGet('/rest/getAlbumInfo2', params)
if not 'albumInfo' in res:
return None
return res['albumInfo']
def renderAlbums():
albumelements = []
albums = apiAlbumList({'type': 'alphabeticalByArtist', 'size': 500})
for entity in albums:
album_info = {}
try:
album_info = apiAlbumInfo({'id': entity['id']})
except:
album_info = {}
bracketed = []
n_album = html.escape(entity['album'])
n_artist = html.escape(entity['artist'])
# if we have a MusicBrainz release ID, include a link to it
if 'musicBrainzId' in album_info and album_info['musicBrainzId'] is not None:
mbuuid = album_info['musicBrainzId']
mblink = html.escape(f"https://musicbrainz.org/release/{mbuuid}")
bracketed.append(f"<a href=\"{mblink}\" target=\"_blank\">musicbrainz</a>")
bracketed = ", ".join(bracketed)
if len(bracketed.strip()) > 0:
bracketed = f" ({bracketed})"
albumelements.append(f"<li><strong>{n_album}</strong> - {n_artist}{bracketed}</li>")
return albumelements
def main():
global CONFIG
CONFIG = loadConfig(os.getenv('IRIS_SUBSONIC_CONFIG', None))
global SERVER_TYPE
pingres = apiPing()
SERVER_TYPE = pingres['type']
# template variables!
host = html.escape(CONFIG['host'])
ts = html.escape(datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S %Z"))
albumelements = renderAlbums()
albumcount = len(albumelements)
albumelements = textwrap.indent("\n".join(albumelements), ' ' * 3).strip()
# template
output = textwrap.dedent(f"""\
<!DOCTYPE html>
<html prefix="og: https://ogp.me/ns#">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, width=device-width">
<meta property="og:title" content="Album list for {host}">
<meta property="og:description" content="As of {ts}, this server has {albumcount} albums.">
<title>Album list for {host}</title>
<style>
*, *::before, *::after {{ box-sizing: border-box; }}
html, body {{ padding: 0; margin: 0; font-family: sans-serif; }}
body {{ padding: 1rem; }}
</style>
</head>
<body>
<h1>
Album list for
<a href="{host}" target="_blank">{host}</a>
</h1>
<p>
There is a total of
<code>{albumcount}</code>
albums on this server.
</p>
<hr>
<ul class="album-list">
{albumelements}
</ul>
<hr>
<p>
List generated at {ts} by
<a href="https://irys.cc/misc/prettyalbums.py.html" target="_blank">prettyalbums.py</a> :)
</p>
</body>
</html>
""")
# and print to stdout :)
print(output.strip())
return 0
if __name__ == "__main__":
exit(main())