Skip to content
abilkhassimovPublic

About

An iPod Classic in your browser, pointed at a folder of music you already own. Click wheel, Cover Flow, games, themes. Google Drive folder as the library: no account, no API key, no OAuth.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

iPodify

An iPod Classic in your browser, pointed at a folder of music you already own.

The click wheel, Cover Flow, the games, the themes. Paste a link to a shared Google Drive folder and it becomes the library: artists, albums, songs, search, covers pulled out of the files themselves. No account, no API key, no OAuth, no subscription.

Open the iPod →

Live License Next.js React TypeScript Tests

Main menu Now Playing Cover Flow

Contents

What this is

A fork of tvillarete/ipod-classic-js, which recreated the iPod Classic interface on top of Apple Music and Spotify. Both services are gone here. The music source is a Google Drive folder shared with "anyone with the link", and playback is a plain <audio> element fed through this app's own origin.

The point is the thing streaming services took away: a library that is yours, in an interface that does not try to recommend anything.

Screens

Artists Albums
Artists, read off the folder tree Albums, with track and disc order
Blocks Settings
Blocks, written for the wheel Settings, six device themes

What it does

Music. Artists, albums, songs, search, Cover Flow. Track and disc numbers are read off file names (03 Title, 3-08 Title), so CD 1 / CD 2 under an album is read as discs of that album rather than as two albums.

Covers. Album art is lifted out of the audio itself: an APIC frame at the front of an MP3, a covr box inside an MP4. A file with no cover falls back to a plain sleeve instead of a broken image.

Photos and videos. Two more folder links, two more menus. Photo rows use the small copy Google already keeps rather than pulling megabytes to draw a stamp.

Games. Brick, Solitaire, and Blocks, a falling-block game written for the click wheel: the wheel moves, the centre rotates, holding the centre drops. All three are original code, because none of the click wheel originals were ever released as source.

Themes. Six device finishes, including the U2 edition and the 5th generation black and white.

Offline. The library is cached in localStorage and the app is installable as a PWA, so browsing works with no network. Only playback needs one.

Requirements

A Google Drive folder shared as "anyone with the link", holding music in an Artist / Album / Track tree. Deeper trees work.

Nothing else. No Google account, no API key, no OAuth consent screen, no Premium tier on anything.

Quick start

pnpm install
pnpm dev

Open http://127.0.0.1:3000/ipod, then Settings → Connect folder and paste the share link.

The app lives under the /ipod path (a leftover from the upstream author's personal site, kept so the fork stays mergeable). The bare root redirects there.

Controls

Wheel Keyboard Does
Scroll the wheel ↑ ↓ ← → Move the selection
Centre Enter Select
Menu Esc Back
Play/Pause Space Play or pause

Touch, mouse and keyboard all drive the same wheel. On a phone the device rotates with the handset and a pull on the screen rescans every connected folder.

How it works

Listing

GET /ipod/api/library?folder=<link or id> walks the folder breadth first and returns a flat file manifest. It reads drive.google.com/embeddedfolderview, the page Drive serves for embedding a shared folder, which answers for anyone the folder is shared with and needs no credentials. That page sends no CORS headers, so the walk happens on the server.

The walk is bounded: 2000 folders, 5000 files, depth 8, 12 requests in flight, a 10 second per-request timeout, an 8 MB per-page ceiling and a 45 second total budget. The budget is one deadline shared by every request, so the last request is cut by whatever is left rather than by its own timeout. Hitting a ceiling returns what was found with truncated: true rather than an error.

Folder and file ids are deduplicated, because Drive lists shortcuts and repeats: on the test folder that is 246 requests down to 242, and 181 files down to 179.

Every row in the listing has the same shape, so one row that will not parse is not a strange file, it is the markup having moved: the parser compares the rows it claimed against the rows it read and fails the page as unparsable. A folder with nothing in it is told apart from unreadable markup by the page containers, and reports empty.

The route is open to anyone with the URL and one call is up to 45 seconds of fan-out on our address, so it holds 8 scans per minute per address, 4 concurrent crawls per instance, and shares one in-flight crawl per folder between callers. Complete scans are cached for 5 minutes with a day of stale-while-revalidate, partial ones for 60 seconds, failures not at all.

Library

The manifest becomes artists, albums and songs from the folder tree. The result is cached in localStorage, so the menus open offline and a relaunch does not rescan.

Playback

GET /ipod/api/stream?id=<file id> passes the range in and the range out and keeps nothing.

The obvious design is no server at all: the file already sits behind a Drive URL that answers ranged requests with 206 and sends access-control-allow-origin: *. But Google refuses any request carrying sec-fetch-site: cross-site, which a browser writes on every cross-site request and which no page can remove. An <audio> element pointed straight at Drive gets a 403 dressed up as a decode error. Something without that header has to ask first.

Nothing is stored on the way through, so a folder shared with one person stays as private as its link. The service worker never caches the stream either: a cached 206 replayed as a whole file breaks the scrubber.

Playable containers are the ones a browser decodes natively: MP3, AAC/ALAC in MP4, FLAC, WAV, Vorbis, Opus. Apple Music downloads (.m4p, .movpkg) are listed on the folder screen as skipped rather than silently dropped.

Routes

Route Runtime Does
/api/library Node, 60s Walks a shared folder, returns a file manifest
/api/stream Node, 60s Ranged audio passthrough
/api/artwork Node, 30s Cover art read out of ID3 / MP4 tags
/api/thumb Node, 30s Drive's own thumbnail for photo rows

All four refuse foreign origins and rate limit per address.

Configuration

One variable, and only in production:

SITE_URL=your_production_domain

It is used for absolute metadata URLs (the share card). It may be a bare domain or a full origin. Anything carrying a path, query or credentials is rejected, and outside local development only https is accepted.

Project layout

app/
  api/            library, stream, artwork, thumb
  components/
    ClickWheel/   the wheel, its gestures and its haptics
    views/        every screen, registered in views/registry.ts
    ViewManager/  split screen, full screen, popups, keyboard
  hooks/
    audio/        the player, the queue, the failure handling
    cloud/        folder connection, scanning, caching
    navigation/   the view stack
  lib/
    drive/        the folder crawler and its parser
    tags/         ID3 and MP4 cover extraction
  utils/          library building, themes, storage, strings
public/           device art, glyphs, icons, service worker
tools/            glyph builder, OG card renderer, install checks
docs/screenshots/ the images in this file
PROJECT.md        working journal: every decision and why

PROJECT.md is the long version. It records what was built, what was rejected and the reasoning behind both, including the parts that turned out wrong.

Scripts

pnpm dev            # next dev
pnpm build          # next build + serwist service worker
pnpm start          # serve the build
pnpm test           # vitest
pnpm lint           # eslint
pnpm type-check     # tsc --noEmit
pnpm glyphs         # rebuild the glyph sheet from tools/glyphs.manifest.mjs
pnpm bundle-report  # build with the bundle analyzer

Testing

pnpm test

552 tests across 31 files. They cover the parts a screenshot cannot check: the folder crawler and its parser, tag extraction, library building from a tree, queue and repeat behaviour, the Blocks rules, theme and settings storage, install and update flows.

Anything visual is checked against the deployed build instead, because a green test on a local render proves nothing about what is actually served.

Deployment

Deployed on Vercel.

pnpm build
vercel --prod

/api/library runs on the Node runtime with a 60 second ceiling, because a tree walk is dozens of sequential round trips to Google.

Differences from upstream

Apple Music and Spotify are gone, and with them the service picker and the sign-in screen. There is one source and it is a link.

Playlists are gone too: a folder has no playlists, and a menu entry that can only ever be empty is worse than no menu entry.

Photos, Videos and the Blocks game are new. So are the six device themes, the PWA install flow, and the pull gesture that rescans every folder at once.

The long-press "Play Next" / "Play Later" menu was removed upstream of this work. It called window.MusicKit.getInstance() directly and only ever worked under Apple Music.

Contributing

Issues and pull requests are welcome. Two things before you open one:

  1. pnpm test && pnpm lint && pnpm type-check should pass.
  2. Comments explain why, not what. The codebase is written that way on purpose and PROJECT.md carries the longer form of the same habit.

Credits and license

MIT. Original work by Tanner Villarete, whose ipod-classic-js is the reason the wheel feels right.

Fork and everything above by Shokan Abilkhassimov.

iPod and iPod Classic are trademarks of Apple Inc. This is an unaffiliated tribute, not a product.

About

An iPod Classic in your browser, pointed at a folder of music you already own. Click wheel, Cover Flow, games, themes. Google Drive folder as the library: no account, no API key, no OAuth.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages