Package

A media source package is a zip archive with a .boppa or .zip extension. This page documents every file Boppa reads from it. A package that is missing a required file, or has a field of the wrong type, is rejected with an error naming the file and the field.

Layout

PathRequiredDescription
manifest.jsonYesThe source's identity and media types. See manifest.json below.
icon.svgNoRaw SVG shown as the source's icon.
container/*.jsNoScripts loaded into the source's container.
playback/index.htmlNoA player page served from the package.
playback/config.jsonNoA hosted player page and user agent. See playback/config.json.
playback/*.jsNoUser scripts injected into the player page.
workers/<name>/config.jsonPer workerA worker's page and title.
workers/<name>/*.jsNoUser scripts injected into that worker's page.
popup/<id>/config.jsonPer popupA popup's page and title.
popup/<id>/*.jsNoUser scripts injected into that popup's page.
boppa.jsonNoThe update header.

Files may sit at the root of the archive or inside a single top-level folder, so zipping the package's folder itself works. Hidden files (any path segment starting with .) are ignored.

Only .js files directly inside a folder are loaded. Files in a subfolder are not. Scripts in a folder load in byte order of their file names, so a numeric prefix such as 10-data.js and 20-playback.js is the simplest way to control order.

Every file Boppa reads must be UTF-8.

manifest.json

{
  "id": "example.com",
  "fqdn": "example.com",
  "version": "1.0.0",
  "name": "Example",
  "mediaKind": "music",
  "author": "you",
  "gitUrl": "https://github.com/you/example-source",
  "highlightColor": "#FF5500",
  "allowedUrls": ["example.com", "*.examplecdn.com"],
  "trackTypes": [{ "id": "song", "belongsTo": ["album"] }],
  "profileTypes": [{ "id": "artist" }],
  "tracklistTypes": [{ "id": "album" }, { "id": "playlist" }]
}
FieldTypeRequiredDescription
idstringYesUnique identifier for this package. See id.
fqdnstringYesThe site the source serves. See fqdn.
versionstringYesFree-form version string. Compared on update checks, see Publishing & Sharing.
namestringYesDisplay name shown throughout the app.
mediaKindstringNoWhat the source offers, in lowercase, for example "music" or "videos". Defaults to "media".
authorstringNoName or handle of the package's author, shown in the source's details.
gitUrlstringNoURL of the git repository the package is developed in. Shown in the source's details, labelled GitHub URL for github.com links, and opens in the browser when tapped.
highlightColorstringNoHex color, for example "#FF5500", used for accents associated with the source.
allowedUrlslist of stringsNoHosts the container is allowed to fetch(). Defaults to [fqdn]. See allowedUrls.
trackTypeslistYesThe kinds of tracks the source has. See Media Types.
profileTypeslistYesThe kinds of profiles the source has. See Media Types.
tracklistTypeslistYesThe kinds of tracklists the source has. See Media Types.

The three type lists are required but may be empty.

id

A short, stable string that identifies this package. It may only contain letters, digits, ., -, and _. Choose an id once and do not change it. Update checks only apply a package whose id matches the one already installed, and two packages with the same id can't be added side by side.

fqdn

The site this source serves, for example archive.org, using the same characters as id. It is also the default for allowedUrls and the base URL of a bundled player page. Every track, profile, and tracklist Boppa stores is keyed on the source's fqdn, not its id. Two packages that serve the same site with the same fqdn share the same library: saved tracks, playlists, and recently played items work with whichever of them is enabled.

Only one source per fqdn can be enabled at a time. Adding or enabling a source disables any other source with the same fqdn.

allowedUrls

A list of hosts (or full URLs, from which only the host is used) that the container is allowed to reach with fetch(). A request whose host matches an entry, or is a subdomain of one, is allowed. Anything else is rejected before the request is made.

"allowedUrls": ["example.com", "*.examplecdn.com"]

example.com and *.example.com both allow example.com and every subdomain of it. If allowedUrls is omitted, only the source's own fqdn and its subdomains are allowed. There is no way to allow every domain, every domain the container talks to must be listed.

allowedUrls limits the container only. Player pages, workers, and popups are ordinary web pages and load whatever they load.

playback/config.json

{
  "url": "https://example.com/player",
  "customUserAgent": "Mozilla/5.0 ..."
}
FieldTypeRequiredDescription
urlstringNoA hosted player page. Cannot be combined with playback/index.html.
customUserAgentstringNoA custom user agent for the player page.

Only include this file when playing through a WebView.

config.json for workers and popups

Each folder in workers/ and popup/ has a config.json:

{
  "title": "Log In",
  "url": "https://example.com/login",
  "customUserAgent": "Mozilla/5.0 ..."
}
FieldTypeRequiredDescription
titlestringYesA label for the page. A popup shows it as the sheet's title.
urlstringYesThe page to load.
customUserAgentstringNoA custom user agent for the page.

The folder name is the worker's name or the popup's id, the value scripts pass to boppa.worker(name) or boppa.popup(id).

User script metadata

Every .js file in playback/, workers/<name>/, and popup/<id>/ is a user script injected into a WebView, and must start with a metadata block:

// ==UserScript==
// @name        Detect Login
// @run-at      document-end
// ==/UserScript==

(function () {
  // ...
})()
KeyRequiredDescription
@nameYesA human-readable label for the script.
@run-atNodocument-start, document-end, or document-idle. Defaults to document-idle, which runs at the same point as document-end.
@description, @version, @author, @namespaceNoInformational only.

Any other key is rejected, as is a key declared twice. User scripts are injected into every frame of the page, including <iframe> elements. A script meant only for the top-level frame should check if (window !== window.top) return at its start, and vice versa.

Scripts in container/ are not user scripts and need no metadata block.

Update header

A package can start with a small header that lets Boppa check for updates without downloading the whole package. The header is a file named boppa.json stored as the first entry of the archive:

{"format":1,"id":"example.com","version":"1.0.0"}

The entry must be exactly 256 bytes, so pad the JSON with trailing spaces. It must be stored uncompressed, with no extra field. Its id and version must match manifest.json, otherwise the package is rejected. format is 1. A package with a higher format asks the user to update Boppa.

A package without the header still works. Update checks just download the whole package every launch instead of reading its first 296 bytes. See Publishing & Sharing for how to build a package with a header.