# AGENTS.md — guidance for AI agents working on JTube TV Read this before making changes. The project is an Android TV app (Kotlin, Compose for TV) that talks to a PeerTube instance over its REST API v1. ## Environment facts - Host is Arch Linux. The Android SDK the build actually uses is `/opt/android-sdk` (`ANDROID_SDK_ROOT` — note `local.properties`' `sdk.dir` points to the outdated `~/Android/Sdk`, which doesn't exist; platforms 34/35/36, build-tools). - Emulator testing (host emulator + agent container over adb): see `docs/agent-container.md` — covers start script, ports, and what to reinstall after a container rebuild. - The Gradle wrapper is checked in (`gradlew` + `gradle/wrapper/`), so builds need only a JDK 17+ (`java`) on PATH. If `java` is missing, tell the user — do not install things on the host without asking. - Useful commands: - `./gradlew :app:assembleDebug` — build debug APK - `./gradlew :app:lintDebug` — lint - `adb connect :` + `adb install -r app/build/outputs/apk/debug/app-debug.apk` - This repo is local-only for now. If it gets a remote on git.jeena.net later, set the repo-local deploy key pin right away: `git config core.sshCommand "ssh -i ~/.ssh/agent-deploy-key -o IdentitiesOnly=yes"` ## Project layout ``` app/src/main/java/net/jeena/jtubetv/ JTubeApp.kt Application + AppGraph service locator (no DI framework) MainActivity.kt single activity, session state → Setup or Home data/api/PeerTubeApi.kt Retrofit endpoints data/api/Dtos.kt kotlinx-serialization models (ignoreUnknownKeys everywhere) data/auth/TokenStore.kt EncryptedSharedPreferences (baseUrl, username, tokens, client creds) data/network/Network.kt Retrofit/OkHttp: BaseUrlInterceptor, AuthInterceptor, TokenAuthenticator (single-flight refresh) data/PeerTubeRepository.kt all suspend functions used by UI ui/theme/Theme.kt Compose for TV theme ui/screens/… SetupScreen, HomeScreen, VideoListScreen (modes), ChannelsScreen, PlayerScreen ui/list/… VideoListViewModel + PagedList state helper ui/player/… ``` State flow: `MainActivity` observes `AppViewModel.sessionState`; `NeedsSetup` renders the setup/login screen, `Ready` renders `HomeScreen`, which switches content on a `Destination` enum. The player pushes a video onto a state and uses a `BackHandler`. ## PeerTube API notes (verified 2026-10-10 against tube.jeena.net, v8.3.1) - OAuth: `GET /api/v1/oauth-clients/local` → client_id/client_secret; `POST /api/v1/users/token` (form: grant_type=password) → access_token + refresh_token. Access tokens expire (~24h); refresh with grant_type=refresh_token. We never persist the user's password. - Pagination shape: `GET …?start=&count=&sort=` → `{ total, data: [...] }`. Sort `-publishedAt`; page size 25; infinite scroll by issuing the next page when the list is 5 items from the end. - Public/local videos listing, search, channels: unauthenticated endpoints work — - `GET /api/v1/videos?isLocal=true&search=` - `GET /api/v1/video-channels?search=…&searchTarget=local` (local channels only) - Authenticated (`Bearer` header): - `GET /api/v1/users/me/videos` — own videos, ALL privacy levels - `GET /api/v1/users/me/subscriptions/videos` and `users/me/history/videos` return the standard `{ total, data: [...] }` wrapper (verified live); history items are plain video objects — **`lastViewedAt` is NOT returned** (don't rely on it) - `GET /api/v1/accounts/{accountName}/video-channels` - `POST /api/v1/videos/{shortUuid}/watching` JSON body `{ "currentTime": }` → HTTP 204 (verified). This is what populates server-side history AND the stored playback position. Report every ~20 s of wall-clock playback once currentTime ≥ 5 s. - Resume-from-server possible via `GET /api/v1/videos/{shortUuid}?userHistory=true` → `userHistory: { currentTime, currentPercent }` (verified) - `GET /api/v1/videos/{shortUuid}` for playback URLs: prefer `streamingPlaylists[].playlistUrl` (HLS) else `files[].fileUrl`. - Private/internal media: attach `Authorization: Bearer` to playlist, segment and file requests (configured on the Media3 `HttpDataSource.Factory`) AND — for instances whose media lives in a private object-storage bucket — pass the short-lived `POST /api/v1/videos/{shortUuid}/token` token as `?videoFileToken=` on those URLs (response shape: `{ files: { token, expires } }`; verified live). - Some instances stream non-public media from object-storage hosts with only URL-based access control — never rely on that; always request and use the file token for non-public videos. (Instance-specific findings stay out of the public repo and live in the private ops notes/agent-local instructions.) ## Conventions - Kotlin + coroutines; suspend functions in the repository, `runCatching` → `Result` at repository boundaries; view models expose `StateFlow`. - No Dagger/Hilt/Room — keep the dependency surface small; revise only if it earns its keep. - kotlinx-serialization for JSON; DTOs must use `ignoreUnknownKeys` so new PeerTube fields never crash parsing. - Compose for TV (`androidx.tv:tv-material`) for sidebar/cards; make list items focusable + clickable so D-pad navigation works. - Package prefix `net.jeena.jtubetv`. Keep files focused; don't grow `MainActivity`. - minSdk 26 / target+compile 35; versions are pinned in `gradle/libs.versions.toml`. ## Scope discipline v0.1 features live in `README.md`; everything else in `BACKLOG.md`. Do not sneak in uploading, editing, subscriptions management, deletion or content creation — the user explicitly excluded them. If the user asks for a scope change, update `BACKLOG.md` first, then implement. When unsure how a PeerTube endpoint behaves, verify against the live instance instead of guessing (401 ⇒ exists & needs auth, 404 ⇒ route missing, 405 ⇒ wrong method on real route).