What WordPress has, and what Riff adds

The Media Library is where Riff's music lives. Riff uses what WordPress already has, and adds something only where WordPress has no equivalent. Each addition below says why it exists, and links the core ticket when there is one. An addition that lands in core is deprecated in Riff and then removed, following the stability rule in the plugin readme.

When you add storage, a field, a relation or a query to Riff, add a row here in the same pull request.

Used as WordPress has it

WhatWhere it lives in WordPress
A trackAn audio attachment.
IconsThe drawings of @wordpress/icons, the set behind core's icon library (wp_get_icon(), 7.1), with the few it lacks, such as a heart and a speaker, drawn to match. They're inlined rather than read from the library: the player's script modules need them as strings, and the library core ships holds only some of them.
Title, artist, album, year, genre, track number, disc number, lengthRead from the file's tags by getID3 on upload (wp_read_audio_metadata()), kept in _wp_attachment_metadata; the disc number as part_of_a_set, which the tracks Riff sends carry as disc.
Bit rate, sample rate, bit depth, codec, losslessThe same upload metadata.
A track's credits in the About panel: composer, lyricist, conductor, album artist, genre, label, copyrightThe same upload metadata, under the names getID3 gives the tags (composer, band, publisher, copyright_message and the rest), read back from the media REST route's media_details, as Riff Lyrics reads the lyrics. The same answer gives the IDs of the albums and artists it is filed under (riff_albums, riff_artists), named through riff/v1/albums and riff/v1/artists, since WordPress's own term routes for them are closed.
LyricsThe file's lyrics tag (ID3 USLT), which WordPress reads on upload into the same metadata as unsynchronised_lyric, and the media REST route returns in media_details. Riff Lyrics shows it, and its Media Library field edits that same key.
ArtworkThe attachment's featured image. WordPress saves artwork embedded in the file as an image and sets it on upload; Riff's Artwork field in the Media Library sets the same featured image.
A cover's alt textThe image's own alt text, _wp_attachment_image_alt, as the Image block reads it; a cover Riff draws names the album and artist it draws.
A video's captionsThe video block's text tracks, read from the block; the Media Library field above for a video with no block.
Where a track appearsThe audio, video and playlist blocks in posts.
A track's details in a Heading, Paragraph or ImageBlock Bindings: Riff registers the riff/track source, and core fills the block from it on the front end. Core decides which attributes a source can fill (a paragraph's, heading's and list item's content, an image's url, alt, title, id and caption, a button's text and link, and a few more); in the editor the block shows that it is bound, since Riff registers no editor-side source.
A shared playlistA post with the Playlist block (WordPress 7.1).
A listener's own playlistA private post of Riff's riff_playlist type, written by the listener, holding one Playlist block (WordPress 7.1) whose playlist-track blocks are its tracks in order.
Who can play whatThe status and visibility of the post a track appears in.
People and app sign-inWordPress users and application passwords. Riff Sign In signs listeners in with the same cookie, wp_set_auth_cookie(), and makes new accounts only when "Anyone can register" is on. With Riff Sign In's Listeners setting on Settings › General, people who cannot write posts are kept to their profile in wp-admin, by their own capabilities rather than a role of Riff's.
AI tools and other plugins reading the library or keeping likes and playlistsThe Abilities API: Riff registers one ability per riff/v1 route it answers for, each run as that route's REST request, except the Listen app's own riff/v1/played and riff/v1/together.
A track's artist, album, video version, captions and protection in the Media Library's views and formsThe Entity Fields API (WordPress 7.2; on 7.1 the same fields are on the attachment's edit screen): Riff registers each as a field of the attachment entity, reading and writing a REST field of the same name on wp/v2/media, so DataViews shows and edits them beside core's own without a screen of Riff's.
Per-site upload limits on a networkMultisite's blog_upload_space.
Unpacking a zip of an album, given to wp riff import or uploaded to the Media Libraryunzip_file() through WP_Filesystem(), as plugin and theme uploads are unpacked: it keeps every path inside the folder it unpacks to and leaves out __MACOSX. The folder is a new one in get_temp_dir(), removed once the import ends. A zip uploaded to the Media Library and attached to no post is imported in the background by WP-Cron, as the person who uploaded it, into private posts of their own with each track it adds Protected, and is then attached to its album's post; core keeps the zip itself at a public address, so the docs ask its uploader to delete it once the album is in. On a network, zip has to be among Network Settings' upload file types, as it is on a network set up on current WordPress.
Whether Now Playing shows the turntableA site option, riff_turntable, registered with register_setting() on Settings › Media, where WordPress keeps how media is shown, and in the REST settings route; the riff_turntable filter decides last. On a network each site keeps its own.
Whether the site's address opens the Listen appA site option, riff_app_at_root, registered with register_setting() on Settings › Riff and in the REST settings route; the riff_app_at_root filter decides last, so a network can turn it on for every site. On, the home page's request is routed to the Listen page with the app's query variable, through core's request filter, so the theme's front page setting is left alone.
Whether a link keeps the music playingA site option, riff_client_navigation, registered with register_setting() on Settings › Riff and in the REST settings route; the riff_client_navigation filter decides last, so a network can turn it on for every site. On, a block theme's links go through core's Interactivity API router and its @wordpress/interactivity-router/full-page link handler, and the template is made a router region through core's template output buffer (wp_template_enhancement_output_buffer, 6.9). Riff Lyrics', Riff Chords' and Riff Sing's scripts load with the player on every page, so they need no opting in with add_client_navigation_support_to_script_module().
Which instrument Riff Chords draws chords forThe listener's own browser storage, riff-chords:instrument, as the queue and the volume are kept: a choice about how this browser shows chords, nothing the site needs.
Which panel a listener left openThe listener's own browser storage, riff:panel, as the queue and the volume are kept: a choice about this browser's screen, nothing the site needs.
A listener's choices about how the site behaves for them: the player's single-key shortcuts, and whether they share what they listen toUser options under Personal Options on their profile (riff_shortcuts, riff_together_share), so on a network each site keeps its own.

Added by Riff, and why

GapWhat Riff addsWhyUpstreamStatus
No full-page client-side navigation in core, and none for a classic themeWhere the site turns client navigation on, Navigation::region() marks a block theme's .wp-site-blocks a router region (riff/page) and carries over what the router leaves as the first page had it: the body's classes, the canonical and short links and the admin bar's edit links. A classic theme has no template to mark, so its links still load the next page, and the player picks the queue up where it was.Core's router swaps only the regions a page marks, and core's own full-page module sends every link through it but marks none. The player sits outside the template, so marking the template keeps the one <audio> playing across a link. Once core navigates the whole page, Riff can stop marking the region itself.Gutenberg #60951: full-page client-side navigation, closed into the Interactivity API's release iterations (#83078 for 7.2) and still experimental; Gutenberg #80298: keeping an element across a navigationIn Riff
The media route renders an attachment's empty caption from its description, as a post's excerpt is made from its content, so an audio file without one has a caption of its own addressOn wp/v2/media, an audio file without a caption has an empty caption.rendered, through rest_prepare_attachmentThe About panel ends a track's credits with its caption, the credit line an archive's recording is given; a made-up caption would credit every other track with its file's address.None filedIn Riff
No link between a song and its video_riff_video post meta on the audio attachment, set from the Media Library's Video version fieldCore has no relation between two attachments.None filedIn Riff
No measure of a song's loudness over timeriff_levels in the song's _wp_attachment_metadata: digits 0 to 9, at most 240, measured by wp riff levels with ffmpeg where the server has it, as the demo albums' areA drawn cover puts the music in its groove. WordPress reads a file's tags and length on upload but nothing of its sound, and keeps what it read about a file in its metadata, so the levels sit beside it.None filedIn Riff
No smaller version of a song to stream_riff_stream post meta on the audio attachment, the ID of another audio attachment the player streams in its place, set from the Media Library's Streaming version field or made by wp riff transcode (an MP3 by ffmpeg, where the server has it, uploaded through media_handle_sideload() to the song's post and without its tags). A file named as another's version is left out of Tracks, search and the catalogue. A service that streams from elsewhere sets the track's src in riff_track instead.A lossless upload is several times the size of what a listener on a phone can tell from it, and the streaming services send a smaller copy and keep the original. Core keeps one file per audio upload, has no relation between two attachments, and no transcoder; image sizes are the nearest thing, and are for images only.None filedIn Riff
No captions for a video outside a post_riff_captions post meta on a video attachment, pointing at a WebVTT file in the Media Library, set from the Media Library's Captions fieldThe video block keeps its text tracks on the block, so a video version chosen in the Media Library, which no block holds, has nowhere to name one. A video block's own tracks are read as they are.None filedIn Riff
No chord chart for a song_riff_chords post meta on the audio attachment, registered with register_post_meta(): the chart in ChordPro, with LRC's times on its lines and chords, set from the Media Library's Chords field and read as riff_chords on wp/v2/media, by Riff Chords, a plugin beside Riff (#685). The song's parts are the chart's own sections ({start_of_verse} and the rest), not anything stored apart from it.A file's tags have no chord frame for WordPress to read, as they have the lyrics Riff Lyrics shows, so the chart is kept on the file it belongs to, as _riff_video is, and needs no table.None filedIn Riff Chords
The file's language tag is read and droppedlanguage in _wp_attachment_metadata, kept from getID3's tags on upload through the wp_read_audio_metadata filterA title in another script needs a lang attribute for a screen reader to pronounce it, and the file's tag says what it is. wp_add_id3_tag_data() keeps an MP3's TLAN as written, a three-letter code such as fra where lang wants fr, and reads only ID3, so a FLAC, Ogg or M4A file's tags, the language among them, are not kept at all.Not yetIn Riff
A FLAC, Ogg or M4A file's tags are read and droppedMedia::tags() on the wp_read_audio_metadata filter keeps them in _wp_attachment_metadata under the names ID3 gives the same tags (albumartist as band, date as year, tracknumber as track_number, discnumber as part_of_a_set), leaving any WordPress keptwp_add_id3_tag_data() reads only ID3, so these files keep no title, artist or album, and the catalog has nothing to file them by.Core #66417In Riff
No waveform for an uploadLevels from the demo seed only; uploads have none yetDrawing a waveform otherwise means fetching the whole file in the browser, as the Playlist block does.Gutenberg #84712: measure peaks once at uploadProposed upstream
Albums and artists can't be browsed or queried; they sit in serialized _wp_attachment_metadatariff_album and riff_artist taxonomies on attachments, filed from the file's album, artist and band tags whenever its metadata is saved; an album's artist is riff_album_artist term meta; _riff_catalog marks a file filed, and a background job files a library from before, walking it by ID from where its last batch stopped, which riff_catalog_backfill_from keeps (wp riff catalog does it at once)Taxonomies on attachments are native: the Media Library shows them as columns and filters, and REST and WP_Query can query them. A track's edit screen has no box for either: core's tag box splits names on commas, which artist and album names are full of, so the core Metadata box's Artist and Album are where a track is filed. Search and browsing need something queryable (#343, #366).Not yetIn Riff
No place for an image a plugin generatesA drawn cover, once drawn, is kept as an SVG file in uploads/riff-art/{version}/, and pages link to the file; riff_art_stamp names the drawing, and moves when a post, a track's tags or levels, or an album changes. The folder before stays for a page cached a moment ago; a post that stops being public takes every folder.Drawn at ?riff-art, every cover started WordPress for each request, and a signed-in listener passes every page cache, so one Home view queued about twenty requests on the host's few PHP workers. Core keeps an attachment's image sizes as files beside it for the same reason, but a drawn cover is no attachment.Not yetIn Riff
No way for a plugin to ship parts of itself that a site can turn off one by oneExtensions: plugins of the monorepo that plugins/riff/extensions.json names, copied into riff/extensions/<name>/ by a release and loaded by Riff as it loads, each unless it is active as its own plugin, off, or needs a newer Riff; riff_extensions keeps each site's on or off, and on a network riff_extensions_network keeps the network's, with a lock that makes it every site's. Turned on and off on Settings › Riff, and Network Admin › Settings › Riff (Extensions)Riff's own features are plugins that use Riff's public APIs, as anyone's can, and a site that does not want one should not load it. WordPress's plugins screen would show each as a plugin of its own, to install and update apart from the Riff it was made for.None filedIn Riff
No colour for an image_riff_tint attachment meta on a cover image: its most colourful shade, read from the thumbnail's pixels the first time an album page shows it (#396)Standalone mode tints an album's header from its cover, as the streaming apps do. Reading the pixels on every page view would be slow, and the colour belongs to the image.Not yetIn Riff
No artwork for an albumPlanned: term meta on riff_album pointing to an image attachmentThe cover stays an image in the Media Library.Not yetPlanned
No picture for an artistriff_artist_image term meta on a riff_artist term, pointing at an image in the Media Library, set from an Image field on the artist's edit screen (#397)The artist page leads with a picture, as the apps' do. The picture stays an image in the Media Library; without one, the artist's newest album cover is used.Not yetIn Riff
Playlists that belong to a listener, not to the siteThe riff_playlist post type: private, out of archives, feeds and search, listed under Media in the admin, opening in the block editor, with its tracks in core's Playlist block; made and changed through riff/v1/users/me/playlists (#401)A playlist is a post holding a Playlist block, so it needs no table, keeps its order and duplicates as the block does, and edits in the editor. Its own type keeps listeners' private lists out of the blog and its Posts grid, and lets a listener own one without the right to publish posts.Not yetIn Riff
No likes or listening history for a userThe riff_likes and riff_history user options (user meta under the site's table prefix, so each site on a network keeps its own), read and written through riff/v1/users/me/likes and riff/v1/users/me/history; a visitor's stay in their browser as riff-likes and riff-history, and join their account when they sign in (#400)Likes belong to a user and are a short list of IDs, which user meta holds without a table of its own. Visitors have no user, so their browser keeps theirs until they have one.Not yetIn Riff
No way to install a site's player as an appA web app manifest at a Listen page's app/ address, and that address rendering the player outside the theme (#379)Browsers install a page that names a manifest; WordPress has no manifest of its own.Not yetIn Riff
No private site: a post can be private, but the site and its files stay publicPrivate site under Settings › Reading: every front-end and REST address asks that the visitor can read the site, its audio and video go into Protect's folder, and core's own search-engine setting is turned on. Images keep their public addresses (Private_Site)An artist's library shouldn't be public until they say so.Not yetIn Riff
Every upload is a public file, whatever post it belongs toAn opt-in Protected box that moves an audio or video file into a folder the web server refuses, and serves it at ?riff-file=ID to whoever may play it, with byte ranges, or, with a CDN or storage plugin answering riff_protected_url, sends them on to a signed address there once they are checked; a file kept off this server, in object storage or after an offload plugin removed it, is protected only when the plugin that keeps it answers riff_protect_storage by making its copy there private; _riff_protected attachment meta keeps the file's original place, riff_protected_dir names the folder, and the riff_protected_open transient keeps for a day whether the web server was found sending it, so the Media Library can warn without asking again. A host can keep the folder outside the web root with RIFF_PROTECTED_PATH or riff_protected_path, as WordPress has no private place for uploads, and core's own _wp_attached_file then holds the file's absolute path, as it does for any file outside uploads; the riff_protected_folders option lists the folders files may still be in while the riff_protected_move cron event moves them, riff_protected_move_state keeps how far it got, riff_protected_move_lock keeps cron and WP-CLI from moving at once, and _riff_protected_moving attachment meta names where one file is going, so a move cut short is finished rather than lost (#406)Hiding a private post's tracks means little while the file plays for anyone with its address. Core serves uploads straight from disk and has no permission check on a file.Core #17255: statuses for media, which restricted access was closed as a duplicate ofIn Riff
No passkeys, and no sign-in without a passwordRiff Sign In, a plugin beside Riff: riff_passkeys user meta holding each passkey's public key, riff_passkey_id for the lookup and riff_passkey_handle for the authenticator's handle; a code sent to an email as a transient for ten minutes (#431)Core signs in with passwords, and application passwords are for apps. Listeners of a music site should not need a password to keep their likes.Core #55950: passwordless sign-in with passkeysIn Riff Sign In
No way to see who else on the site is listening, or to listen alongWith the Presence API feature plugin active, a row for each signed-in listener who is playing, riff-{user ID}, in its riff/listening room: their queue as track IDs, which of them plays, and a schedule: where the music is at a time on the server's clock and whether it plays from then, a change by a listener someone follows due a moment ahead so every follower makes it at once. Read and written through riff/v1/together, which turns the IDs back into the tracks each reader may see. Who follows a listener is a room of its own, riff/session:{user ID}, one row for each follower while they follow (#687)Presence is written every few seconds and gone in minutes. In options or user meta that would invalidate caches site-wide, which is why the Presence API keeps it in a table of its own with an expiry. Its own REST routes need edit_posts, so a listener can't call them, and Riff calls its PHP API behind a route of its own.The Presence API, proposed for coreIn Riff, with the Presence API
No bulk import from a folderwp riff import, a thin layer over media_handle_sideload() (#367), which takes a zip too; a zip uploaded to the Media Library and attached to no post is imported the same way, its albums found again by its attachment rather than its path (#821)Every file goes through core's own upload path, so its tags and artwork are read the way an upload's are. Most music arrives as a zip, and core keeps an uploaded zip as a file to download.Not yetIn Riff
No import from the archives that publish recordings for reusewp riff archive, which reads the Library of Congress's and Europeana's JSON APIs, reads each item's own rights statement and takes only the public domain mark, CC0, CC BY and CC BY-SA, and from the Library of Congress, which says it in words, what it knows of no U.S. copyright in, then downloads what it takes with wp_safe_remote_get() and imports it through Import::folder(). Each track's credit is its caption, and its source, licence and credit its description, the attachment's own fields; the album post opens with the credit, and the About panel ends its credits with the caption (#823)Rights are per item, and a site made from an archive has to credit it. WordPress's importers bring in another site's posts; none reads an archive's catalogue or its rights.None filedIn Riff
unzip_file() checks for disk space only during WP-Cron, sets no limit on what an archive unpacks to, reads each file whole into memory, and falls back to PclZip, which inflates the whole archive into memory, for a zip ZipArchive can't readBefore unpacking a zip, Riff opens it with ZipArchive and refuses one that can't be read whole, that declares more than ten times its own size or more than the disk has free, or that holds a file larger than PHP's memory has left, and has unzip_file() use ZipArchive; on a server without PHP's zip extension a zip is refusedMusic hardly compresses, so a real album unpacks to about its own size; a zip that unpacks to far more is built to fill the disk or the memory. A lossless track can be larger than PHP's memory, and reading it whole would end the request with a fatal error rather than say why.Not yetIn Riff
No tune for a song_riff_sing_melody post meta on the audio attachment: the melody in UltraStar's text format, the open one karaoke games read (each syllable with its start, length and pitch), set from Riff Sing's Melody field in the Media Library and read through riff_sing_melody on wp/v2/media; or _riff_sing_midi, the ID of a MIDI file in the Media Library whose vocal track is the melody, set from the Melody file field and read as the file's address through riff_sing_midiRiff Sing scores singing against the song's melody. Core keeps what it reads from a file in its metadata, and a file's tags have no melody; post meta is where an attachment keeps what someone wrote about it, as the Video version and Captions fields do. A melody from a score stays the MIDI file it was exported as, uploaded like any other file, as captions stay a WebVTT file.None filedIn Riff Sing
No notion of a site's Listen pageThe riff_listen_page transient: the ID of the oldest published page holding the Riff block or the Listen template, kept for a day and forgotten whenever a page changesAbout links an album and an artist to their pages in the Listen app from any page with the player, and finding the page takes two queries; core keeps a page for a purpose only for its own (the front page, the posts page, the privacy policy).None filedIn Riff
No way to tell a file was already uploaded_riff_import_hash attachment meta (the file's SHA-1), and _riff_import_folder post meta on an album postRunning an import again must not duplicate the library, and new tracks must join their album. Core has no content hash on attachments.Not yetIn Riff
A real AAC .m4a is refused: core lists m4a as audio/mpeg, then the file's sniffed type, audio/mp4, doesn't match; .opus isn't listed at allm4a is allowed as audio/mp4 and opus as audio/ogg on any upload (Import::mimes()).Both are common in music libraries, and the player adds music through the same route as any upload. On a network, its list of allowed files still has the last word.Not yetIn Riff

Private core APIs Riff still uses

Core can change these without notice. Each has no public equivalent yet; the API coverage report (npm run api-coverage) lists them under "Private APIs in use".

WhatWhereWhyUpstream
_wp_render_title_tag, _block_template_render_title_tag and _block_template_viewport_meta_tag, unhooked from wp_headStandalone::template()Standalone mode's document prints its own title and viewport, and a second viewport later in the head would drop viewport-fit=cover. Core has no public way to stop printing either tag; its filters change the title's text, not whether the tag is printed.Core #59510 proposes a filter for the viewport tag; #61900 asked for the title and was pointed at pre_get_document_title
__experimentalBorder, __experimentalSkipSerialization, __experimentalFontFamily, __experimentalLetterSpacing and __experimentalDefaultControls in the Listen block's block.jsonfeatures/listen/block/block.jsonThe block's border and font controls, and its colours and font family applied to the views inside it rather than to the wrapper. WordPress 7.1 has no stable key for any of them; switch when it does.Core #61728, closed after Gutenberg reverted the stable keys; Gutenberg #63001 and #64312 are open

Development tooling

GapWhat Riff addsUpstreamStatus
@wordpress/env 11.17 breaks with simple-git 4, which Riff pins for its security fixes.github/scripts/patch-wp-env.js, run on installGutenberg #84137Merged upstream; remove the patch once a released @wordpress/env includes it

Edit this page on GitHub