jtube-tv/AGENTS.md
Jeena 39d3b12182 channel: redesign the header, reachable Subscribe button (#27)
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.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 12:25:12 +00:00

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 — 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.)

Issue workflow (contract, 2026-10-14)

Forgejo issues at git.jeena.net/jeena/jtube-tv are Jeena's acceptance queue — the workflow is strict:

  1. Only Jeena closes issues. The agent never closes anything. Every issue is a change Jeena tests on real TV hardware before giving the OK.
  2. 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.
  3. 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.
  4. Agent testing on the emulator/TV via adb is a precondition, not a substitute for Jeena's check.
  5. 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 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

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).