Repository navigation
Development
Notes for working on the Moonfin Roku codebase. For build commands, see Building from Source.
- Written in BrighterScript, which transpiles to BrightScript, with SceneGraph XML for the UI.
- Built with the
brighterscriptcompiler (bsc) throughnpm run build. - Linted with
@rokucommunity/bslintand formatted according tobsfmt.json. - Target resolution is FHD, 1920x1080.
- UI changes should be tested on real Roku hardware when possible. The emulator doesn't reproduce focus and animation behavior faithfully.
├── source/ # Entry point, API clients, utilities, constants, enums
│ ├── Main.bs # Channel entry point
│ ├── api/ # Jellyfin, Seerr, and plugin API calls
│ └── utils/ # Shared helpers
├── components/ # SceneGraph components, one .bs + .xml pair each
│ ├── home/ # Home screen, media bar, rows
│ ├── details/ # Movie, series, season, and playlist detail screens
│ ├── settings/ # Settings screens and the plugin sync surface
│ ├── seerr/ # Seerr discovery, browse, and request flows
│ ├── mediaPlayers/ # Video, audio, and photo playback
│ ├── liveTv/ # Guide, channels, and recordings
│ └── ... # Login, search, genres, favorites, keyboards, tasks
├── locale/ # Translations, one folder per locale
├── images/ # Bundled artwork and icons
├── settings/ # Settings definitions
└── manifest # Roku channel manifest
Four BrighterScript and SceneGraph behaviours that have each cost real debugging time in this codebase. Read them before your first change.
-
Namespace members shadow locals. Inside
namespace foo, a bare identifier resolves to a namespace member before a local variable or parameter, case-insensitively. A parameter callednamein a namespace that also hasfunction Name()transpiles to the function pointer, compiles clean, and fails on the device with "Type Mismatch ... can't be applied to Function and String". Don't reuse a namespace function's name for a local. -
Task threads don't see the main thread's globals.
GetGlobalAA()is per thread. A value set inMain.bsis empty inside a SceneGraph Task, so the auth header there falls back to a device id ofRoku. From a Task, never look up your own session by device id. Match on the playing item and the client name instead. -
Where a shared mixin file can live. A
.bsundersource/is validated in thesourcescope too, so it can't call component-only functions. A.bsundercomponents/joins only the scopes of the components that import it, which is what a shared un-namespaced mixin needs. Handlers named in XML (onChange=,observeField) resolve unqualified, andautoImportComponentScriptflattens the imports at transpile time. -
Verify what shipped, not what you saved. Editors and sync tools have been seen reverting a save between edit and build here. After changing a file, grep for the new line in the source, in
build/staging/, and inside the zip, in the same command as the build.
You don't have to steer the remote to test playback. With the app running, curl -d '' "http://<Roku IP>:8060/input?mediaType=movie&contentId=<item id>" plays that item straight away through the app's deep link handler. http://<Roku IP>:8060/query/media-player then shows the player state, and the server's /Sessions for the client "Moonfin Roku" shows whether it direct played or transcoded, and why.
Translations are managed in Weblate at translate.moonfin.io, which owns the contents of locale/. Edit strings there rather than by hand so contributions aren't overwritten on the next sync.
- Check the existing issues before starting, in case your idea or bug is already tracked.
- Open an issue first for anything significant, so the approach can be discussed before the work happens.
- Match the existing code style, which
bsfmt.jsonandbslint.jsonenforce. - Test on real Roku hardware.
To submit a change, fork the repo, create a feature branch, make your changes with clear commit messages, test on device, and open a pull request with a clear description.
Using Moonfin
Going deeper
For developers