Skip to content

Archives

BarryThePirate edited this page Aug 25, 2026 · 2 revisions

Archives

Wraps the fishtank.live archive API that powers the site's /archives page: room/day/video listings, signed playback URLs, and pure helpers for parsing archive filenames and resolving what's "on air" at any moment in a season's timeline.

Auth: requesting a watch URL requires being logged in to fishtank.live with a season pass — the site marks s01 as free (FREE_ARCHIVE_SEASONS), paid seasons need the pass. Requests send the site's cookies automatically. On any failure, functions return null / [] — nothing throws.

import { archives } from 'ftl-ext-sdk';

// Listings (memoized in-memory — immutable historical data)
const rooms  = await archives.getRooms('s03');                  // ['bar', 'bedroom-1', ...]
const days   = await archives.getDays('s03', 'bar');            // ['2024-10-27', ..., '2024-11-20']
const videos = await archives.getVideos('s03', 'bar', '2024-10-27');
// [{ fileName: 's03_bar_24-10-27_17-39-31.mp4', startsAt: '2024-10-27T17:39:31', hour: 17, size: 175113530 }, ...]

// Signed playback URL (NOT cached — signed per file, hours-scale expiry)
const url = await archives.getWatchUrl('s03', 'bar', '2024-10-27', videos[0].fileName);
// 'https://fishtank-archives.b-cdn.net/s03/bar/2024-10-27/...?token=...&expires=...'

// Filename / label helpers (pure)
archives.parseVideoId('s03_bar_24-10-27_17-39-31');
// { season: 's03', room: 'bar', day: '2024-10-27', fileName: 's03_bar_24-10-27_17-39-31.mp4', startsAt: '2024-10-27T17:39:31' }
archives.formatRoomLabel('den-ptz');                            // 'Den PTZ'

// Thumbnail URL for a moment within a chunk (public CDN, no auth)
archives.thumbnailUrl('s03_bar_24-10-27_17-39-31.mp4', 95);
// 'https://cdn.fishtank.live/archive-thumbnails/primary/bar/2024-10-27/s03_bar_24-10-27_17-39-31/19.jpg'

// Time helpers — stamps are UTC; "house" = real America/New_York local
const t = archives.parseShowTime('2024-11-15T20:42:00');        // stamp (UTC) → epoch ms
archives.formatShowClock(t);                                    // '20:42:00' (stamp frame)
archives.formatShowDate(t);                                     // '2024-11-15' (day-folder frame)
archives.formatHouseClock(t);                                   // '15:42:00' (real local, DST-aware)
archives.formatHouseClock(t, true);                             // '3:42:00 PM' (12-hour)
archives.formatHouseDate(t);                                    // '2024-11-15'
archives.parseHouseTime('2024-11-15T18:00:00');                 // 6pm at the house → epoch ms

// Schedule helpers (pure, operate on a day's listing)
const chunk = archives.findChunkAt(videos, t);
// { video, offsetSeconds, nextStartsAtMs } — or null if t is before the first chunk
const next = archives.nextChunkAfter(videos, t);                // for "No Signal" countdowns

archives.clearCache();                                          // drop memoized listings

The chunk model

Archive footage is stored as ~15-minute mp4 chunks per room per day (~175–200 MB each). Filenames encode the schedule: s03_bar_24-10-27_17-39-31.mp4 starts at 17:39:31 show time on 2024-10-27.

There is no duration field in the listings. A chunk is nominally bounded by the next chunk's startsAt, but its exact playable length only becomes known from the video element's metadata after loading. Gaps between chunks are genuine downtime (the camera was off) — the site renders these as "No Signal".

Because of this, findChunkAt() may return an offsetSeconds that exceeds the chunk's real duration when the requested moment falls in a gap. Players should validate the offset against video.duration once metadata loads, and treat overshoot as "No Signal" until nextStartsAtMs (or the value from nextChunkAfter()).

Timestamps: stamp frame vs house frame

Archive timestamps (filenames and startsAt) are UTC — verified empirically by matching sunrise/sunset visible in the footage against the show location's sun times (darkness falls at stamp 21:2x on 2024-11-15 = 16:2x EST, the real local sunset). The site's own re-run clock displays stamp time as if it were local, which is why it reads 4–5 hours ahead of the visible time of day.

The module therefore works in two frames:

  • Stamp frame (parseShowTime, formatShowClock, formatShowDate): the UTC timestamps as written. Use this for all scheduling math, and always use formatShowDate() to pick a day folder — the archive's days listings are stamp dates.
  • House frame (parseHouseTime, formatHouseClock, formatHouseDate): real America/New_York local time, DST-aware (season 3 spans the November 2024 clock change). Use this for anything a human reads — clocks, day pickers, day numbers.

Share codes

A share code pins a moment in a season to a compact, human-readable string that means the same real moment for everyone:

FTL1-s03-D11-1817-kitchen
└┬─┘ └┬┘ └┬┘ └┬─┘ └──┬──┘
ver season day HHMM  room (optional — omit to land on the camera grid)

Day and time are house time (24-hour), so a code reads naturally in chat and resolves to one absolute moment. Codes also travel as links via the URL fragment — https://fishtank.live/#FTL1-s03-D11-1817-kitchen — which never reaches the server; client-side tools pick it up.

archives.buildShareCode({ season: 's03', day: 11, time: '18:17', room: 'kitchen' });
// 'FTL1-s03-D11-1817-kitchen'

archives.parseShareCode('https://fishtank.live/#ftl1-S03-D11-1817-KITCHEN');
// { season: 's03', day: 11, time: '18:17', room: 'kitchen' } — case/link tolerant

archives.shareUrl('FTL1-s03-D11-1817-kitchen');
// 'https://fishtank.live/#FTL1-s03-D11-1817-kitchen'

parseShareCode validates shape only (day ≥ 1, valid clock time); whether the season/day/room actually exist is the caller's job via the listings.

Thumbnails

Pre-generated JPEG frames exist on the public CDN (no auth, no token) at https://cdn.fishtank.live/archive-thumbnails/primary/{room}/{day}/{videoId}/{N}.jpg — one frame per 5 seconds of footage, N counting from 0. The site uses them as grid previews and video posters. thumbnailUrl(fileNameOrId, offsetSeconds) builds the URL for a given moment.

Coverage is not guaranteed for every chunk (frames appear to be generated as the site's re-run replays footage), so always attach an onerror fallback. Note the CDN sends no CORS headers — load thumbnails with <img> / poster, not fetch().

Signed URLs

getWatchUrl() returns a Bunny CDN URL signed per-file (?token=...&expires=<epoch>) with an hours-scale TTL — comfortably longer than a chunk's 15 minutes. Request at play time, never store long-term, and re-request on playback error (the most likely cause is an expired token).

Seasons

Currently available: s01 (free) and s03. Other seasons return empty listings until the site adds them. Room lists differ per season — always build UI from getRooms() rather than hard-coding.

Clone this wiki locally