jtube-tv/AGENTS.md
Jeena 40a9d1e2c6 docs: Agent container emulator workflow
Document how the host emulator is driven over adb from the agent
container: the start script now boots headless by default and trusts
remote adb keys via ADB_VENDOR_KEYS at launch, since the TV system
image is a user build that blocks pushing keys to adb_keys after boot.
AGENTS.md points to the new doc and fixes the stale SDK path.
2026-10-12 09:42:08 +00:00

5.9 KiB

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 <ip>:<port> + 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": <seconds> } → 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<T> 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).