PHP

Riff is built the way a feature plugin is: one track shape everywhere, read through WordPress's own APIs. Everything here is public and keeps working within a major version; anything that has to go is deprecated for a release first.

Hooks

Every filter and action has a page of its own, generated from the docblock above the line it fires on. All hooks, PHP and JavaScript, are searchable in one list.

Filters

FilterDescription
riff_abilitiesFilters the abilities Riff registers, so an extension adds its own the way Riff adds its: each the REST route it runs, with what it takes and what it answers. Each is registered in Riff's category, run as a REST request to its route and allowed what the route's own permission callback allows, and offered to an MCP client only where the site turns that on.
riff_about_sectionsFilters the sections of the About panel, Riff's own included: track, the cover, the title and the artist and album, and credits, what the file's tags say about who made it.
riff_add_music_urlFilters where the Listen app sends someone who may upload to add music: the account menu's Add music, and Add your first album in an empty library.
riff_app_at_rootFilters whether the site's own address opens the Listen page's app. On, a request for the home page with no other query is the Listen page in standalone mode, every link to the app and the manifest's start address are the home page, and the app/ address redirects there. The Listen page's own address and the theme's front page setting are left as they are.
riff_app_manifestFilters the Listen app's web app manifest, for a host that names, colours or gives icons to each site's app its own way.
riff_app_precacheFilters what the installed Listen app's service worker keeps on the device, so it is there offline and on every launch after the first: a host's fonts that the first paint doesn't show, say. The fonts riff_app_preload_fonts returns are already in it.
riff_app_preload_fontsFilters the fonts a Listen page preloads, the faces its first paint shows, so a host that draws the app in fonts of its own has them fetched with the page rather than after the stylesheet asks. The installed app's service worker keeps them on the device too.
riff_app_viewsFilters the Listen app's views: Riff's own, by slug (home, search, albums, artists, tracks, posts and library), and any a plugin adds.
riff_can_playFilters whether the current visitor may play a track.
riff_client_navigationFilters whether a link on the site is followed without leaving the page, so what plays carries on. Only a block theme can: its template is the region that is swapped.
riff_cover_accentFilters whether the player takes its accent from the cover of the track playing: the cover's hue, muted and lightened until it reads on the player's dark surfaces, as --riff-cover-accent, on the seek bar, the playing bars, the current Up next item and a wash behind Now Playing, and in the Listen app's own document on its links, the track playing, Liked songs and the Browse tiles. A cover that is grey, near black or near white gives none, and the accent is the site's --riff-accent, as it is everywhere with this off.
riff_enabledFilters whether Riff's player loads on the current front-end request.
riff_extensionsFilters the extensions Riff loads, by name: a host can leave one out, or name another plugin it has placed in Riff's extensions/ folder.
riff_mcpFilters whether AI assistants may use Riff's abilities over MCP. On, and with the MCP Adapter plugin active, /wp-json/mcp/riff serves each of them as a tool to a signed-in user, and the adapter's default server lists them. Each still does only what that user may do. Off, there is no such server, and the default server does not list them.
riff_post_blocksFilters whether a post's blocks are served as a tree at riff/v1/posts/{id}/blocks to someone who cannot edit the post.
riff_profile_urlFilters where the Listen app's account menu sends a listener to edit their profile, for a host or a membership plugin with a profile page of its own.
riff_protect_storageFilters whether the storage a file is kept in elsewhere was made private, when it is protected, or public again, when it isn't. A plugin that keeps uploads in object storage or on a CDN answers for its own files: true once the copy there is private (or public again), or a WP_Error to say why it couldn't be, which the Media Library shows. Without an answer, a file only on this server is moved into the protected folder, and one kept elsewhere is not protected, as moving it would leave it public there. Riff can't send a file it doesn't have, so a plugin that protects its files signs their addresses through riff_protected_url too.
riff_protected_pathFilters the folder protected files are kept in, so the web server never has them to send: an absolute path outside the web root, which Riff makes when it is first needed, keeping each site's files in a folder of its own inside it (the site's ID on a network). Empty, the default, keeps them in a folder under uploads, which Apache is told to refuse and Nginx needs a rule for.
riff_protected_sendFilters how a protected file is handed over: 'php' sends it from PHP; 'x-sendfile' asks Apache's mod_xsendfile or LiteSpeed to send it; 'x-accel-redirect' asks Nginx, which then needs an internal location for the protected folder (see the readme).
riff_protected_urlFilters the address a protected file is sent from instead of WordPress: a signed address on a CDN or in object storage, such as a CloudFront or S3 presigned URL or a Bunny or Cloudflare token, that stops working at $expires. Riff has checked that the visitor may have the file before it asks, and sends them on with a 302 nothing caches. Empty, the default, sends the file as riff_protected_send says.
riff_sourcesFilters the sources tracks are read from, keyed by id.
riff_together_shareFilters whether a listener who hasn't chosen shares what they play with the site's other listeners. A site where listeners shouldn't see each other unless they ask, a class of children say, returns false.
riff_trackFilters a track before the player reads it.
riff_turntableFilters whether Now Playing shows the record and the turntable. Off, it shows the cover on its own, and a touch screen on its side shows the cover beside the controls.
riff_unlock_urlFilters where a locked track sends the visitor to unlock it: a sign-up, membership or checkout page.

Actions

ActionDescription
riff_before_playerFires before the player's markup.
riff_below_turntableFires in the place under the turntable, for what a plugin shows beside the record. Print one element for each thing: the place takes room only while one that isn't hidden is in it, and only where the turntable is the view. The record is out when #riff has data-riff-deck="out". Script it from there; window.riff.analyser() hears the music.
riff_private_refusedFires before a private site refuses a visitor, for a page of the site's own in place of WordPress's message.
riff_registerFires once a request, before the player's controls and views are first read: register them here with riff_register_control() and riff_register_view(), or take one of Riff's own away. A plugin's keyboard shortcuts are registered here too, with riff_register_shortcut().

Functions

The bar's controls and Now Playing's views are registered on the server, as WordPress registers blocks, so the page has every button and every view before any script runs. Register them on the riff_register action; Riff registers its own there too, and any of them can be taken away. In the browser, window.riff.view() and window.riff.control() give each what it does, by its id. How the player works has the whole of it.

add_action( 'riff_register', function () {
	riff_register_view( 'credits', array(
		'label' => __( 'Credits', 'my-plugin' ),
		'icon'  => '<svg viewBox="0 0 24 24">…</svg>',
		'order' => 32,
	) );
} );

riff_register_control( string $id, array $args = array() ): bool

Registers a control in the player's bar. Call it on the riff_register action.

Parameters

$idstring
Lowercase letters, digits and dashes.
$argsarray
The control.
  • labelstring
    What it is called, for a screen reader and a pointer's tooltip.
  • iconstring
    An icon of Riff's own by name, or SVG markup.
  • orderint
    Where it sits among the others, lowest first. Default 50.
  • widebool
    Only on a wide screen; a phone's bar keeps to the track, Play and what it needs. Default false.
  • rendercallable
    Markup of its own in place of a button, for an element that is not one; it is given the id and returns one element.
  • scriptstring
    The script module that brings it to life, which Riff enqueues wherever the player is. Register the module yourself.

Return

bool Whether it was registered.

riff_unregister_control( string $id ): bool

Takes a control away, Riff's own included.

Parameters

$idstring
The control's id.

Return

bool Whether there was one.

riff_register_view( string $id, array $args = array() ): bool

Registers a view the listener opens from its button in the bar: a screen of Now Playing, or a panel beside the page. Call it on the riff_register action.

Parameters

$idstring
Lowercase letters, digits and dashes.
$argsarray
The view.
  • labelstring
    Its name: its button's, its chip's and its region's.
  • iconstring
    An icon of Riff's own by name, or SVG markup.
  • orderint
    Where its button sits among the bar's controls, lowest first. Default 50.
  • placestring
    screen to take the whole screen above the bar, as Now Playing does; side for a panel beside the page. Default screen.
  • stagebool
    For a screen that is Now Playing's own stage, the track's art or its record, rather than a region of its own, with no button in the bar, which opens it itself. Default false.
  • widebool
    Its button only on a wide screen; on a phone its chip in Now Playing opens it. Default true.
  • rendercallable
    Markup for its region as the page is drawn; the script fills it as it opens. Given the id.
  • scriptstring
    The script module that brings it to life, which Riff enqueues wherever the player is. Register the module yourself.

Return

bool Whether it was registered.

riff_unregister_view( string $id ): bool

Takes a view away, Riff's own included.

Parameters

$idstring
The view's id.

Return

bool Whether there was one.

riff_register_shortcut( string $id, array $args = array() ): bool

Registers a key the player acts on for a plugin. It follows Riff's own single-key shortcuts: it does nothing while the listener types, it is listed with them on the listener's profile, and it is off wherever they are off. Call it on the riff_register action, and give it what it does with window.riff.shortcut().

Parameters

$idstring
Lowercase letters, digits and dashes.
$argsarray
The shortcut: its key and its label.
  • keystring
    One character, as the key types it: a letter, which acts without Shift whatever its case, a digit, or a mark such as [ or +. Not a space, and not one of Riff's own letters, K, J, L, N and P, nor a key another shortcut has.
  • labelstring
    What it does, as the profile lists it beside the key.

Return

bool Whether it was registered.

riff_listen_page(): int

The site's Listen page, where the player opens as an app: the oldest published page holding the Riff block or using the Listen template.

Return

int Page ID, or 0 when there is none.

Sources

A source answers three questions: which posts it can read, what tracks a post holds, and how those tracks render as blocks the player takes over. The library asks every source; the first that claims a post reads it.

A plugin adds its own post type or catalogue by implementing Riff\Source and registering it on init; the library, the REST routes and the player then read it like any other.

add_action( 'init', function () {
	Riff\Sources::register( new My_Catalogue_Source() );
} );

The Riff\Source interface

public function id(): string

A short id, such as "blocks".

Return

string

public function supports( WP_Post $post ): bool

Whether this source can read the post.

Parameters

$postWP_Post
Post.

Return

bool

public function tracks( WP_Post $post ): array

The tracks in the post, in order, in the shape Tracks::schema() describes.

Parameters

$postWP_Post
Post.

Return

array[]

public function html( WP_Post $post ): string

The tracks rendered as figures carrying data-riff-track, which riff.js turns into rows.

Parameters

$postWP_Post
Post.

Return

string

public function where(): string

A WHERE clause fragment (without a leading AND) matching posts this source can read, or an empty string when the source cannot say from SQL alone.

Return

string

Riff\Sources::register( Source $source ): void

Registers a source. The core blocks source registers itself at boot; a plugin registers its own on init.

Parameters

$sourceSource
Source.

Riff\Sources::unregister( string $id ): void

Removes a source.

Parameters

$idstring
Source id.

Block Bindings

riff/track (“Riff track”) is a Block Bindings source: a Heading or Paragraph bound to it shows a track's details, and an Image bound to it shows the cover, with no Riff block on the page. The binding's key picks the detail and id names the attachment; with no id, it is the post's first track. Every value is the track as the track schema has it, after riff_track, and a named track shows only where the media route would show it. In the editor the block shows that it is bound; the value is filled on the front end.

<!-- wp:heading {"metadata":{"bindings":{"content":{"source":"riff/track","args":{"key":"title","id":123}}}}} -->
<h2 class="wp-block-heading"></h2>
<!-- /wp:heading -->

<!-- wp:image {"metadata":{"bindings":{"url":{"source":"riff/track","args":{"key":"cover","id":123}}}}} -->
<figure class="wp-block-image"><img src="" alt=""/></figure>
<!-- /wp:image -->
KeyShows
titleThe track’s title.
artistThe artist.
albumThe album.
yearThe year, or nothing when the file has none.
durationThe length as the player shows it, such as 4:32, or nothing when unknown.
qualityThe format and rate as the player’s badge shows them, such as MP3 · 128 kbps or FLAC · 24-bit 96 kHz.
coverThe address of the largest artwork: the file’s own, else its post’s, else the cover Riff draws. For an Image block’s url.

Abilities

Through the Abilities API, AI tools and other plugins list the library and keep a listener's likes and playlists. Each ability runs its riff/v1 route as a REST request, so it answers what the route answers and is allowed what the route allows; the ones about likes, history and playlists need a signed-in user. Nothing plays through an ability: playback happens in a browser.

$albums = wp_get_ability( 'riff/albums' )->execute( array( 'search' => 'Compositions' ) );
AbilityWhat it doesInput
riff/tracksLists the tracks the requester can see, searchable by title, artist or album, or those on one album or by one artist, or named by ID, a page at a time.search: Words to search for.
album: Only the tracks on this album, by term ID, in album order.
artist: Only the tracks by this artist, by term ID.
include: Only these, by ID, such as one to look at on its own.
orderby: By title, or the most recently added first.
page: Page of results, from 1.
per_page: Tracks per page, up to 100.
Read only
riff/albumsLists the albums with a track the requester can see, by name, each with its artist, artwork and track count, searchable by name. An album's tracks are riff/tracks with its ID as album.search: Words to search for.
artist: Only the albums with a track by this artist, by term ID.
include: Only these, by ID, such as one to look at on its own.
page: Page of results, from 1.
Read only
riff/artistsLists the artists with a track the requester can see, by name, each with a track count, searchable by name. An artist's tracks are riff/tracks with their ID as artist.search: Words to search for.
include: Only these, by ID, such as one to look at on its own.
page: Page of results, from 1.
Read only
riff/libraryLists the posts with music the requester can see, newest first, each with its title, artwork, track count and length, a page at a time.page: Page of results, from 1.Read only
riff/library-postGets one published post with music, such as an album, with its tracks in order as the player reads them.id (required): The post's ID, as riff/library lists it.Read only
riff/library-summaryCounts the tracks, albums, artists and posts with music, and says how much space the audio and every upload take, and the site's limit where it has one. For someone who can upload files.–Read only
riff/likesLists what the signed-in user has liked: tracks, albums and artists, newest first.–Read only
riff/historyLists the tracks the signed-in user has played, most recent first.–Read only
riff/playlistsLists the signed-in user's playlists, each with its tracks.–Read only
riff/likeAdds a track, album or artist to the signed-in user's likes.kind (required): What to like: a track, an album or an artist.
id (required): Its ID: an audio attachment, an album term or an artist term.
Writes
riff/unlikeTakes a track, album or artist out of the signed-in user's likes.kind (required): What to like: a track, an album or an artist.
id (required): Its ID: an audio attachment, an album term or an artist term.
Writes
riff/create-playlistMakes a playlist for the signed-in user, empty or with tracks.title (required): The playlist's name.
tracks: Audio attachment IDs, in order.
Writes
riff/add-to-playlistAdds tracks, by audio attachment ID, to the end of one of the signed-in user's playlists.playlist (required): The playlist's ID.
tracks (required): Audio attachment IDs, in order.
Writes
riff/remove-from-playlistTakes the track at a place in one of the signed-in user's playlists out, counting from 0.playlist (required): The playlist's ID.
index (required): The track's place in the playlist, from 0.
Writes
riff/create-albumMakes an album of audio files already in the Media Library: a post with one audio block for each, in the order given, a draft unless asked otherwise. For someone who can write posts and edit the files.title (required): The album's title.
tracks (required): Audio attachment IDs, in order.
status: Draft, for its author to look over first; published; or private. Draft unless given.
artwork: An image attachment ID for the cover; the first track's artwork when not given.
Writes
riff/update-trackSets a track's title, artist, album or artwork, through the Media Library's own REST route. For someone who can edit the file.id (required): The audio attachment's ID.
title: The track's title.
artist: The artist, kept with the file's tags; empty to clear it.
album: The album, kept with the file's tags; empty to clear it.
artwork: An image attachment ID for the track's artwork; 0 to take it away.
Writes
riff/import-zipImports a zip of an album, or of a folder of albums, already uploaded to the Media Library and attached to no post, as uploading it there does: in the background, into private album posts with each track Protected, for its uploader to publish. For someone who can write posts and edit the zip.attachment (required): The zip's attachment ID.Writes

The Listen block

riff/app: Your music library and the player, on this page. Every post with audio or video is listed, and the music keeps playing while you browse. One per page. It supports align, color, typography, spacing, __experimentalBorder, shadow.

<!-- wp:riff/app {"align":"full"} /-->

Anywhere blocks are not an option, the [riff] shortcode does the same.

[riff]

[riff], the same as the block.

Return

string