This is a teeny tiny API/Proxy to run on my server so you can see what I'm listening to on my website.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve cover art handling

Co-authored-by: Copilot <copilot@github.com>

+126 -74
+125 -72
src/main.py
··· 1 1 import asyncio 2 2 import base64 3 + import os 4 + import sys 5 + import traceback 3 6 4 7 from fastapi.websockets import WebSocketState 5 8 import uvicorn 6 - from fastapi import FastAPI, WebSocket 9 + from fastapi import FastAPI, Request, Response, WebSocket 7 10 from fastapi.middleware.cors import CORSMiddleware 8 11 9 12 from subsonic import get_cover_art, get_now_playing ··· 30 33 "link": None, 31 34 "coverArt": None, 32 35 } 33 - self.cover_art = { 34 - "coverArt64": None, 35 - "coverArt128": None, 36 - } 37 - self.metadata_revision = -1 38 - self.coverArt_revision = -1 36 + self.cover_art = {} 37 + self.revision = -1 38 + 39 + def set_not_playing(self): 40 + self.playing = False 39 41 40 - def update_metadata(self, artist, title, youtube, coverArt): 42 + def set_listening_to(self, artist, title, youtube, coverArt): 41 43 self.playing = True 42 44 metadata = { 43 45 "artist": artist, ··· 47 49 } 48 50 if metadata != self.metadata: 49 51 self.metadata = metadata 50 - self.metadata_revision += 1 52 + self.revision += 1 51 53 52 - def update_cover_art(self, coverArt32, coverArt64, coverArt128): 53 - if ( 54 - coverArt32 != self.cover_art.get("coverArt32") 55 - or coverArt64 != self.cover_art.get("coverArt64") 56 - or coverArt128 != self.cover_art.get("coverArt128") 57 - ): 58 - self.cover_art = { 59 - "coverArt32": coverArt32, 60 - "coverArt64": coverArt64, 61 - "coverArt128": coverArt128, 62 - } 63 - self.coverArt_revision += 1 54 + def purge_cover_art(self): 55 + self.cover_art = {} 56 + 57 + def add_cover_art(self, size, coverArt): 58 + if self.cover_art.get(size) != coverArt: 59 + self.cover_art[size] = coverArt 64 60 65 - def playing_dict(self): 61 + def get_is_playing(self): 66 62 return { 67 63 "op": 0, 68 64 "d": self.playing, 69 65 } 70 66 71 - def metadata_dict(self): 67 + def get_metadata(self): 72 68 return { 73 69 "op": 1, 74 70 "d": { ··· 78 74 }, 79 75 } 80 76 81 - def cover_art_dict(self, size=64): 77 + def get_cover_art_size(self, size): 78 + options = [opt_size for opt_size in self.cover_art.keys() if opt_size <= size] 79 + 80 + if len(options) == 0: 81 + return None 82 + 83 + return max(options) 84 + 85 + def get_cover_art(self, size=64): 86 + if size not in self.cover_art: 87 + return None 82 88 return { 83 89 "op": 2, 84 - "d": dataurl("image/png", self.cover_art.get(f"coverArt{size}")) 85 - if self.cover_art.get(f"coverArt{size}") 86 - else None, 90 + "d": dataurl("image/png", self.cover_art[size]), 87 91 } 88 92 89 93 90 94 now_playing = CurrentlyPlaying() 91 95 92 96 93 - async def update_metadata(): 94 - while True: 95 - np = await get_now_playing() 96 - if np: 97 - now_playing.update_metadata( 98 - artist=np["artist"], 99 - title=np["title"], 100 - youtube=np["youtube"], 101 - coverArt=np["coverArt"], 102 - ) 103 - else: 104 - now_playing.playing = False 105 - await asyncio.sleep(1) 97 + async def update_cover_art(): 98 + if not now_playing.metadata.get("coverArt"): 99 + now_playing.cover_art = {} 100 + return 101 + 102 + async def fetch_cover_art(size: int): 103 + await asyncio.sleep(0.01 * size) 104 + cover_art = await get_cover_art(now_playing.metadata["coverArt"], size=size) 105 + now_playing.add_cover_art(size, cover_art) 106 + 107 + await asyncio.gather(*map(fetch_cover_art, (32, 64, 128, 256, 512))) 106 108 107 109 108 - async def update_cover_art(): 110 + async def update_metadata(): 109 111 while True: 110 - if now_playing.metadata["coverArt"]: 111 - now_playing.update_cover_art( 112 - coverArt32=await get_cover_art( 113 - now_playing.metadata["coverArt"], size=32 114 - ), 115 - coverArt64=await get_cover_art( 116 - now_playing.metadata["coverArt"], size=64 117 - ), 118 - coverArt128=await get_cover_art( 119 - now_playing.metadata["coverArt"], size=128 120 - ), 121 - ) 122 - else: 123 - now_playing.cover_art["coverArt64"] = None 124 - now_playing.cover_art["coverArt128"] = None 125 - await asyncio.sleep(1) 112 + try: 113 + np = await get_now_playing() 114 + last_cover_art = now_playing.metadata.get("coverArt") 115 + if np: 116 + now_playing.set_listening_to( 117 + artist=np["artist"], 118 + title=np["title"], 119 + youtube=np["youtube"], 120 + coverArt=np["coverArt"], 121 + ) 122 + if now_playing.metadata.get("coverArt") != last_cover_art: 123 + now_playing.purge_cover_art() 124 + await update_cover_art() 125 + last_cover_art = now_playing.metadata.get("coverArt") 126 + else: 127 + now_playing.set_not_playing() 128 + except Exception: 129 + traceback.print_exc() 130 + await asyncio.sleep(1) 131 + await asyncio.sleep(0.2) 126 132 127 133 128 134 TASK_UPDATE_METADATA: asyncio.Task | None = None 129 - TASK_UPDATE_COVER_ART: asyncio.Task | None = None 130 135 131 136 132 137 async def ensure_updating(): 133 - print("Ensuring updating...") 134 - global TASK_UPDATE_METADATA, TASK_UPDATE_COVER_ART 138 + global TASK_UPDATE_METADATA 135 139 if not TASK_UPDATE_METADATA or TASK_UPDATE_METADATA.done(): 136 140 TASK_UPDATE_METADATA = asyncio.create_task(update_metadata()) 137 - if not TASK_UPDATE_COVER_ART or TASK_UPDATE_COVER_ART.done(): 138 - TASK_UPDATE_COVER_ART = asyncio.create_task(update_cover_art()) 141 + 142 + 143 + @app.get("/") 144 + async def root(request: Request, inline: int = 0): 145 + await ensure_updating() 146 + actual_size = now_playing.get_cover_art_size(inline) 147 + return { 148 + "playing": now_playing.playing, 149 + "metadata": { 150 + "artist": now_playing.metadata.get("artist"), 151 + "title": now_playing.metadata.get("title"), 152 + "link": now_playing.metadata.get("link"), 153 + }, 154 + "coverArt": { 155 + "max": str(request.url_for("cover_art")), 156 + **{ 157 + size: str(request.url_for("cover_art_res", res=size)) 158 + for size in now_playing.cover_art.keys() 159 + }, 160 + **( 161 + {actual_size: dataurl("image/png", now_playing.cover_art[actual_size])} 162 + if actual_size 163 + else {} 164 + ), 165 + }, 166 + } 167 + 168 + @app.get("/coverart") 169 + async def cover_art(request: Request): 170 + await ensure_updating() 171 + size = now_playing.get_cover_art_size(512) 172 + if not size: 173 + return Response(status_code=404) 174 + return Response(status_code=307, headers={ 175 + "Cache-Control": "no-cache", 176 + "Location": str(request.url_for("cover_art_res", res=size)), 177 + }) 178 + 179 + @app.get("/coverart/{res}") 180 + async def cover_art_res(res: int, lower: bool = False): 181 + await ensure_updating() 182 + if lower: 183 + res = now_playing.get_cover_art_size(res) 184 + if res not in now_playing.cover_art: 185 + return {"error": "No cover art of that size available."} 186 + return Response(content=now_playing.cover_art[res], media_type="image/png", headers={ 187 + "Cache-Control": "no-cache", 188 + }) 139 189 140 190 141 191 @app.websocket("/ws") 142 192 async def websocket_endpoint(websocket: WebSocket, res: int = 32): 143 193 await ensure_updating() 144 194 await websocket.accept() 145 - if res not in (32, 64, 128): 195 + if res not in (32, 64, 128, 256, 512): 146 196 await websocket.send_json({"error": "I don't like you."}) 147 197 await websocket.close() 148 198 return 149 199 150 - last_metadata_revision = -1 151 - last_cover_art_revision = -1 200 + last_revision = -1 201 + last_size = None 152 202 last_is_playing = None 153 203 154 204 while websocket.client_state == WebSocketState.CONNECTED: 155 205 if now_playing.playing != last_is_playing: 156 - await websocket.send_json(now_playing.playing_dict()) 206 + await websocket.send_json(now_playing.get_is_playing()) 157 207 last_is_playing = now_playing.playing 158 - if now_playing.metadata_revision != last_metadata_revision: 159 - await websocket.send_json(now_playing.metadata_dict()) 160 - last_metadata_revision = now_playing.metadata_revision 161 - if now_playing.coverArt_revision != last_cover_art_revision: 162 - await websocket.send_json(now_playing.cover_art_dict(size=res)) 163 - last_cover_art_revision = now_playing.coverArt_revision 208 + 209 + if now_playing.revision != last_revision: 210 + await websocket.send_json(now_playing.get_metadata()) 211 + last_size = None 212 + last_revision = now_playing.revision 213 + 214 + if (new_size := now_playing.get_cover_art_size(res)) != last_size: 215 + await websocket.send_json(now_playing.get_cover_art(size=new_size)) 216 + last_size = new_size 164 217 await asyncio.sleep(0.5) 165 218 166 219
+1 -2
src/subsonic.py
··· 20 20 resp = await session.get( 21 21 ORIGIN + "/rest/" + name + "?" + query + "&" + QUERY_ARGS, timeout=timeout 22 22 ) 23 - print(ORIGIN + "/rest/" + name + "?" + query + "&" + QUERY_ARGS, file=sys.stderr) 24 23 resp.raise_for_status() 25 24 return resp 26 25 ··· 28 27 async def execute(name, query=""): 29 28 async with aiohttp.ClientSession() as session: 30 29 resp = await get(session, name, query) 31 - bs = BeautifulSoup(await resp.text(), "xml") 30 + bs = BeautifulSoup(await resp.text(), "xml") 32 31 sresp = bs.find("subsonic-response") 33 32 if not sresp or sresp.get("status") != "ok": 34 33 raise Exception(