Telegram/TDLib media source for the Capullo Audio Platform.
It treats a Telegram group/channel as a music library and implements the
capullo-audio-contracts SPI so the shared capullo-audio delivery engine can play and broadcast it.
This library does not depend on the engine, nor the engine on it - they are siblings meeting at the SPI. See Platform shape.
It is the platform's contract-stability anchor: unlike capullo-source-radiobrowser
(instant stream URL), here mediaRequestFor(id) suspends on a real TDLib download before
returning the local file URI, and onQueueAdvanced drives Telecloud's 2-track lookahead prefetch.
The src/test driver test exercises that feedback path against a fake TDLib client - the behavioural
validation radiobrowser only had at the type level.
| Module | What |
|---|---|
:capullo-source-telegram |
The library: TelegramSource (MediaSourceProvider + NowPlayingSource), TDLib client, chat sync, Room track index, on-demand download + 2-track prefetch. Namespace tech.capullo.source.telegram. |
:app |
Minimal harness proving the library links and assembles. |
TDLib (the org.drinkless.tdlib Java API + prebuilt libtdjni.so) comes from the
lib-tdlib-android jitpack AAR - a
normal dependency, no setup_tdlib.sh / git-lfs.
class TelegramSource(scope, downloadManager, metadataExtractor) :
MediaSourceProvider, NowPlayingSource {
suspend fun mediaRequestFor(id): MediaRequest // awaits the TDLib download → file:// URI (throws if deleted)
fun queue(): PlaybackQueue // finite playlist, isRotating = false
fun onQueueAdvanced(index) // fire-and-forget 2-track lookahead prefetch
val nowPlaying: StateFlow<NowPlaying> // DB tags immediately, ID3 tags + cover art post-download
}Validated contract note: for a fetch-based source the now-playing side effect lives in
mediaRequestFor (ID3 tags + art are only readable once the file is on disk), not in onQueueAdvanced
as it does for live radio. This implies the engine must call mediaRequestFor(idAt(i)) before
onQueueAdvanced(i). No contract signature change was required - the non-suspend, fire-and-forget
onQueueAdvanced is the correct shape for cancellable prefetch. See
src/test/.../TelegramSourceContractTest.
./gradlew :capullo-source-telegram:testDebugUnitTest # the contract-validation driver
./gradlew :app:assembleDebug # harness APKTDLib resolves from com.github.capullo-tech:lib-tdlib-android (jitpack) - nothing to populate.
The library is DI-free (no Hilt): the consuming app supplies Context, credentials
(TelegramCredentials), and a coroutine scope.
Security: the TDLib local database (login session + cached chats) is encrypted at rest by
default - TelegramCredentials requires a databaseEncryptionKey (32 random bytes; use
TelegramCredentials.newDatabaseEncryptionKey()), which the app generates once and keeps in the
Android Keystore / EncryptedSharedPreferences. It is not a user-facing password - encryption is
transparent. There is no cleartext path. Consuming apps should also set allowBackup=false (or
exclude the tdlib/ dir) so the session isn't captured in cloud backups. Dev builds resolve capullo-audio-contracts from a
sibling checkout (composite build); release/jitpack builds resolve it from
com.github.capullo-tech:capullo-audio-contracts.
Copyright 2026 capullo-tech. Licensed under GPLv3 - see LICENSE. TDLib is bundled (under the Boost Software License) via the lib-tdlib-android dependency.