How it works inside: indexer → playlist → stream
The project is built as a pipeline: Telegram channel → catalogue → broadcast.
1) The indexer runs through the channel, finds audio and GIF/MP4, reads tags and builds playlist.json.
2) /playlist?mix=&energy= returns JSON with tracks and visuals already in the right order for the current knobs.
3) /audio/{id} and /gif/{id} request media from Telegram and serve it to the browser.
The key principle: audio is not saved to disk. The server streams the data (chunked) as it arrives.
Visuals are cached to disk because they’re short and repeat, and the cache noticeably speeds up switching.
The most important part (the thing that turns a channel into a broadcast):
@app.get("/audio/{message_id}")
async def audio(message_id: int):
async def gen():
async for chunk in telethon_stream_bytes(message_id):
yield chunk
return StreamingResponse(
gen(),
media_type="audio/mpeg",
headers={"Cache-Control": "no-store", "Accept-Ranges": "none"},
)
— @app.get(“/audio/{message_id}”) The route: GET /audio/12345 lands here, {message_id} is the ID of the message with the track in Telegram.
— async def audio(message_id: int): An asynchronous handler, so the server doesn’t block during loading and serving. int fixes the parameter type.
— async def gen(): … yield chunk An asynchronous generator: it reads the track from Telegram in portions (async for) and immediately serves each piece (yield).
— StreamingResponse(gen(), …) Wraps the flow in a proper HTTP stream: playback can start before the file has fully loaded.
— media_type=“audio/mpeg” Explicitly sets Content-Type so the browser plays the audio correctly.
— headers no-store: don’t cache the audio. Accept-Ranges: none: no Range requests (seeking or loading “from the middle” isn’t provided for).