Without a mouse the channel banner was invisible: opening a channel focused the first video card, which scrolled the header (a grid item) off-screen. The header is now pinned above the grid and never scrolls away. The banner is its backdrop, darkened to black at the bottom; the avatar sits beside the title with a "by <owner> on <platform>" byline, the subscriber/video/created stats, and the description flowing into the black beneath (a long one grows the header down instead of being clipped). A Subscribe button sits on the banner, reachable with UP from the first card, wired to the same follow/unfollow as the player. AGENTS.md: subscribing is in scope (already on the player, now the channel page too), so drop it from the excluded list.
7.2 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— notelocal.properties'sdk.dirpoints 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. Ifjavais missing, tell the user — do not install things on the host without asking. - Useful commands:
./gradlew :app:assembleDebug— build debug APK./gradlew :app:lintDebug— lintadb 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 (
Bearerheader):GET /api/v1/users/me/videos— own videos, ALL privacy levelsGET /api/v1/users/me/subscriptions/videosandusers/me/history/videosreturn the standard{ total, data: [...] }wrapper (verified live); history items are plain video objects —lastViewedAtis NOT returned (don't rely on it)GET /api/v1/accounts/{accountName}/video-channelsPOST /api/v1/videos/{shortUuid}/watchingJSON 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: preferstreamingPlaylists[].playlistUrl(HLS) elsefiles[].fileUrl.- Private/internal media: attach
Authorization: Bearerto playlist, segment and file requests (configured on the Media3HttpDataSource.Factory) AND — for instances whose media lives in a private object-storage bucket — pass the short-livedPOST /api/v1/videos/{shortUuid}/tokentoken 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.)
Issue workflow (contract, 2026-10-14)
Forgejo issues at git.jeena.net/jeena/jtube-tv are Jeena's acceptance queue — the workflow is strict:
- Only Jeena closes issues. The agent never closes anything. Every issue is a change Jeena tests on real TV hardware before giving the OK.
- Commit messages reference issues without closing keywords. "Closes", "fixes", "resolves" and their variants auto-close issues when pushed — never use them. Reference by plain number or "Addresses #N", and mention the why in the body so the review stands on its own.
- An implemented issue is not done until Jeena's OK on the TV. State in the commit or final message which build to install to test it.
- Agent testing on the emulator/TV via adb is a precondition, not a substitute for Jeena's check.
- If a fix turned out to land in a different commit than the one that mentions the issue, name the fixing commit explicitly in the message body so review can trace it. (A bad example that must not repeat: bundling "Closes #21" into an unrelated pushed commit — the commit-to-issue link looked wrong.)
Conventions
- Kotlin + coroutines; suspend functions in the repository,
runCatching→Result<T>at repository boundaries; view models exposeStateFlow. - No Dagger/Hilt/Room — keep the dependency surface small; revise only if it earns its keep.
- kotlinx-serialization for JSON; DTOs must use
ignoreUnknownKeysso 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 growMainActivity. - minSdk 26 / target+compile 35; versions are pinned in
gradle/libs.versions.toml.
Scope discipline
Shipped features live in README.md; anything else is tracked as Forgejo issues at
git.jeena.net/jeena/jtube-tv. Do not sneak in uploading, editing, deletion or content
creation — the user explicitly excluded them. (Subscribing/unsubscribing to channels is
in scope and already implemented — on the player and the channel page.) If the user asks
for a scope change, confirm it with them before implementing.
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).