Developing¶
These are the working notes for changing Mixtape itself: what each file does, how a mix is packed into a link, how the player keeps the reels in step, the test walk and deploying. Everything a user needs is in the other pages.
How it runs¶
Plain ES modules, no build step and no libraries. Any static web server can serve app/, but it has to be served
(ES modules don't load from file://).
| File | What |
|---|---|
app/index.html, app/style.css |
The page and the link sheet. Light and dark (custom properties, data-theme when the toggle is used), phone and computer, print styles for the J-card. |
app/main.js |
The router, the start page, the arrival, the print view, the link sheet, import and the theme toggle. window.__mixtape is there for checks. |
app/editor.js |
The editor: label fields, shells and papers, the two sides, dragging with pointer events (mouse and touch), the up/down/side buttons, the arrow keys on the handle, Undo, the letter, saving. |
app/player.js |
The player screen: keys, counter, the song that's on, the video box, the J-card and the letter, the flip animation. |
app/deck.js |
The tape deck: plays a side, loads the YouTube IFrame API, works out where the tape is and turns the reels. |
app/cassette.js |
The cassette in SVG: the 12 shells and 3 papers, the label text (shrunk to fit), the tape packs, hubs and the tape path. |
app/jcard.js |
The J-card on screen, the letter, and the print sheet with its fitting. |
app/share.js |
Packing a mix into the link and back. |
app/store.js |
Local storage (wrapped, with an in-memory fallback), cleaning a mix, export and import. |
app/links.js |
What a link is: YouTube video ids and start times, service names, tidying. |
app/fake-player.js |
The stand-in for YouTube that the test walk uses (see below). |
app/fonts/ |
Caveat and Courier Prime (Latin and Latin Extended), fonts.css and their OFL files. |
Screens¶
The screen is in the part of the address after #:
| Address | Screen |
|---|---|
#/ |
The start page. |
#/new |
A new mix. It turns into #/edit/<id> straight away, and is only saved once something changes. |
#/edit/<id>, #/play/<id>, #/print/<id> |
One of your mixes in the editor, the player or the print view. |
#m=<data> |
A mix someone sent. Add &print for its print view. |
The link¶
share.js turns a mix into short JSON, compresses it with CompressionStream('deflate-raw') and writes it as
base64url after #m=. A browser without compression gets #u= and the same JSON uncompressed, and both are read.
The JSON keys are short on purpose:
{ "v": 1, "t": title, "f": for, "b": from, "d": "YYYY-MM-DD", "s": shell, "p": paper, "l": letter,
"A": [[title, artist, link, note], ...], "B": [...] }
Empty fields and trailing empty values are left out, and links go through tidy() in links.js first (YouTube
becomes https://youtu.be/<id>, tracking parameters like si are dropped). A link from someone else is never
trusted: unpackMix runs it through cleanMix in store.js, which cuts every string to the limits in LIMITS,
allows only the known shells and papers and at most 30 songs a side, and everything is put on the page as text,
never as HTML. Links only open when they're http or https. Links over 8,000 characters (LONG_LINK) get a warning.
Storage¶
Four keys in local storage, all JSON: mixtape.mixes (your mixes), mixtape.got (the mixes you got, kept as their
link data), mixtape.lengths (YouTube video id → seconds, learned while playing) and mixtape.theme ("light",
"dark" or nothing). Every read and write is wrapped in try/catch, and when storage throws or is missing, the same
data lives in memory for as long as the page is open, and the page says so.
The deck and YouTube¶
deck.js loads https://www.youtube.com/iframe_api the first time someone presses play on a YouTube song, never
before, and makes one YT.Player with host: https://www.youtube-nocookie.com. Later songs go through
loadVideoById on the same player. Player states come back through onStateChange. Errors show a message (101 and
150 mean the owner doesn't allow embedding), and while a side is playing the deck moves on after 4 seconds.
Where the tape is (target()) is the length of the songs before this one plus the current time, over the length of
the whole side. Songs whose length isn't known yet count as 210 seconds, and real lengths are kept in
mixtape.lengths once a song has played. The cassette draws the two tape packs from that position with their
areas adding up (packRadii in cassette.js), and turns each hub by the tape speed over its pack's radius, so the
small reel spins faster, like a real one. When the position jumps (a skip, a pick from the J-card, a flip), the tape
winds there at 0.7 of a side a second with the reels spinning fast. With reduced motion the reels don't turn and the
tape jumps straight to its place.
Flipping keeps the tape where it was, so turn() mirrors the position (1 - shown) and the other side winds back
to its start.
The stand-in player¶
Headless Chrome can't play YouTube, so with ?fake in the address (before the #, like /mixtape/?fake#/play/<id>)
deck.js imports fake-player.js instead. It has the same few methods as the YouTube adapter, keeps time with the
clock, makes each song 2 to 5 minutes long from its video id, and fails like a video that can't be embedded when the
id starts with x. window.__mixtapeFakeSpeed speeds the clock up, to get to the end of a side quickly.
Checks¶
tools/walk.py # computer layout, light
tools/walk.py --dark --shots /tmp/mixtape # dark, with a picture of every screen
tools/walk.py --mobile # phone size, with real touch events for dragging
tools/walk.py --youtube # also checks the real YouTube player (needs the internet)
tools/walk.py https://your.copy/mixtape/ # another copy
walk.py makes a mix from scratch and edits it every way the editor allows (typing, shells, papers, adding songs,
up and down, across sides, dragging with a mouse or a finger, the arrow keys, Remove and Undo, the letter), makes a
link and decodes it, checks the long-link warning, duplicates and deletes with Undo, plays the demo mix with the
stand-in (Play, Pause, the reels and the tape, Next, an Open link, Back, Flip, picking from the J-card, the end of a
side, a video that won't embed), checks the print view is real size and that 20 songs a side fit, prints a PDF,
exports and imports, opens the link as the person who got it (the arrival, Mixes you got, Make your own), a broken
link and the theme button. Then it runs again with local storage switched off and once with reduced motion. On every
screen it checks that nothing is wider than the window and that buttons are at least 40 px, and it fails if the
page loads anything from anywhere but its own server. With --youtube it blocks YouTube once to check the "couldn't
reach" message and that pressing play asks for the IFrame API and nothing else, and then lets it through to check
that the player is made on youtube-nocookie.com.
It prints a line per check and any page errors, and exits 1 if anything failed. It starts its own headless Chrome
(Chrome for Testing by default, or whatever CHROME points to) and needs
the Python websocket-client package. The demo mix it uses is tools/demo-mix.json, with made-up songs.
Deploying¶
tools/deploy.sh
It copies app/ with rsync to the folder my web server serves Mixtape from. For your own server, change the
destination in the script. Mixtape needs no special headers, but the page should send a Referrer-Policy that
still sends the page's origin to other sites (the default strict-origin-when-cross-origin does), because YouTube's
player can refuse to play when it doesn't know which site it's on.