Bubble search, player and CRM for a library-music publisher
The website and the back office of a Dutch library-music publisher: a search for people who do not know what to search for, a player that survives navigation, licences paid through Mollie, and a CRM that ran the royalty statements.
AllMusic Publishing was a library-music publisher in Landsmeer, just north of Amsterdam. Library music is the music under things: commercials, corporate films, the bit before the presenter starts. Its customers are producers who need forty seconds of something by Thursday. From 2016 to the end of 2022 I built and ran their website, allmusic.nl, and Marsyas, the back office the staff worked in. The site is no longer online; this is written from the code, and from a copy I got running again on my own machine.
Nobody knows what to search for
The customer of a library cannot name what they want. They know the film. They do not know whether it needs something orchestral or something with a ukulele, and a search box that expects a genre makes them feel stupid first. The site was designed around that. It is one page: a PHP shell renders the first request, after that every click goes through one ajax endpoint and swaps the section in the middle, so the player and the search drawer never reload. The drawer slides in from the left, and its first tab says: pick three bubbles describing your project.
The bubbles are keywords, 252 of them when I counted the cached list, the same keywords the staff attach to every track in Marsyas. On open the list is shuffled, sorted by length so the short words sit on the small bubbles, and cut to a multiple of twelve. Each ring of twelve gets radii from 8 to 19 and a colour from a palette of twenty, shuffled per visit, so the drawer never looks the same twice and the size of a bubble means nothing. The layout is a d3 force simulation: the bubbles sit on a circle, are pushed apart so they do not overlap, and the arrows on either side, or the arrow keys, rotate the next twelve into view. Click one and it grows, then drops into the first of three dashed slots at the bottom. A fourth pick replaces the third. Clicking a chosen bubble again unpicks it. When three are in, the drawer waits 550 milliseconds, long enough to see the third land, and searches.
Three picks become a URL, search/anthem-zen-haunting, which the server turns into key1, key2 and key3 for the search function on Lambda, thirty-five results at a time. The scoring ran in a MySQL search engine whose code is not in any repository I still have; the site only sends the keys in the order they were picked. Below the results the page loads the next thirty-five whenever the reader is within 2000 pixels of the bottom, after a deliberate two-second pause so the list does not run away from a fast scroll.
Getting the bubbles to move took three attempts: a collision demo, a Box2D port to see whether real physics would do, and four hand-typed arrangements that gave every visit the same picture. The d3 simulation that replaced them is the one in the recording.
The player sits outside the part of the page the ajax swaps, which is the entire reason the site is one page: a producer can start a track, open the pricing page, read the terms, come back, and the track is still playing. Each track carries a pre-rendered waveform image, and the playhead over it is one CSS transform transition set to the remaining duration, so it moves without a timer. The keyboard works the way an editing suite does: left and right scrub by ten per cent on the first press and one per cent per step when held, up and down step through the list, plus and minus put the track in the current project and take it out. Every listen longer than a second and a half is batched and reported to the API, which is what told the staff which tracks actually got played.
From a demo to a licence
What a producer downloads is never the master. When a track arrives it goes through a Lambda that runs sox: the master is normalised to 44.1 kHz, 16-bit, and the preview mp3 is the same audio mixed with a one-minute spoken demo overlay, repeated as many times as the track is long, soft enough to judge the music under. Tracks are collected into projects, and licensing happens at the moment of download, when the API decides whether this account may have the clean file for this project or has to wait for approval. The zip is built on the server in five-megabyte parts, up to a hundred of them, with a progress file on S3 that the page polls, because forty WAV files are not something a browser should assemble.
Pricing was kept short. A free account downloads demo files and pays per use. Corporate licences are monthly subscriptions. All Sync licences cover worldwide corporate video and Benelux radio and television, in five sizes from 25 minutes at €150 a month to unlimited at €675, the price per minute falling from €6.00 to €3.75 on the way. Payment went through Mollie, so iDEAL, PayPal, Bancontact and cards, with an on-invoice path for the companies that insisted, a VAT number check and direct debit by IBAN. The account section held invoices by year, the download history, usage reporting, user management for agencies with several editors, and a form to release a YouTube copyright claim on a video that had licensed the music properly.
InspireMe
InspireMe was the other answer to the same problem. Instead of asking the producer to search, the staff picked a handful of tracks under a mood, gave it a photograph and a name, and the app on the producer’s phone showed the new sets as they came, with a player that ran through them. The website carried a web version that played a set and moved on to the next. I built the app; its code is lost, and the three screens below are what remains of it.
Marsyas
Marsyas is what the staff used from December 2018, and where most of the actual publishing happened. A new album goes in through it: the files are uploaded and each track is annotated from the keyword list, with a type-ahead that only offers words not yet on the track, because tagging a sixty-track album is an afternoon and every saved keystroke is felt. InspireMe sets are made here, contracts are created and linked to songs, and there is a customer record with overlays for a customer’s invoices, statements, downloads and every audition the player reported, which is how a sales call could start with what the customer had actually been listening to. Press q anywhere and the customer search opens.
The heavy part is royalties. Every quarter the collecting societies send money and files: BUMA and Stemra as CRD, a fixed-width CISAC format where the record type sits in the first three bytes and everything after it is read by offset; SENA and SABAM in their own formats. Marsyas uploads them to S3, a Lambda parses them into staging tables in SQL Server and matches receipts to songs, and what does not match lands in a receipt manager and a missing-payments list for a person to resolve. Then a statement run in two passes: the pre run allocates money to composers whose shares the publisher was paid directly, the final run allocates to clients, and it refuses to start until the previous quarter’s pre-run file exists in the accounting export. Each composer gets a statement by source, with the effective percentage and the amount due per line, to approve or send back.
One failure that shaped it: invoices and statements needed a PDF, and the first version, from July 2019, sent an HTML invoice template of its own to Pdfcrowd, a hosted converter. That meant a second copy of every document in a template that did not look like the screen, and two commits that October are both called fixed pdf problem. The replacement, from April 2020, is blunt. Any screen with a print button scrapes its own stylesheets and the part of the page next to the button, posts them to a small PHP endpoint that drives a headless Chrome, and gets back a PDF of exactly what the staff member was looking at.
A second Marsyas, in React, was started in 2020 and never finished replacing the first; its to-do file still lists relogin as not quite working. Both ran side by side to the end.
The pieces and how they talk. The website’s PHP knows nothing but one api() function; every screen, on the site or in the CRM, is a request to a Lambda with a prefix that says who is asking.
Around the edges
Smaller repositories orbit the two big ones: audio-visualizer renders a video from a track with ffmpeg on a Lambda layer and youtube-uploader puts it on the publisher’s channel; mailer sent the system mail; VoiceGen on the website turned a script into a voice-over through Amazon Polly, so a producer could try a track under a rough narration. allgen.io and MusiqGen, the 2021 experiment in generated music and voices with a second developer, are a separate story.
The constraint that shaped all of it was that the front end had to stay thin. The PHP on the web server holds no data and no business rules; it maps a URL to a view, asks the API and renders. That is why the CRM could be rebuilt in React against the same endpoints, and why the copy on my machine today opens the drawer, shuffles 252 bubbles and lets me pick three, then reports a connection problem, because the Lambdas it would ask are gone. The last commit to the site is from 2 December 2022. Its configuration has a switch that sets the site to closed at midnight on 1 January 2023, and nothing in the code ever read it.