The public repo must not document this instance's object-storage access-control posture; findings like that belong in private ops notes. The engineering guidance (never rely on URL obscurity, always request file tokens) stays.
97 lines
No EOL
5.6 KiB
Markdown
97 lines
No EOL
5.6 KiB
Markdown
# 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 with Android SDK at `~/Android/Sdk` (platforms 34/35/36, build-tools).
|
|
- 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). |