All files / src bbcdiscs.js

96.42% Statements 27/28
88% Branches 22/25
100% Functions 6/6
100% Lines 24/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 8413x     13x                 13x               13x 11x                         10x 10x                   9x 9x 9x             7x 5x 5x 4x 4x 4x   3x   4x 4x                       2x 2x 2x 2x 1x      
const mirrorBase = "https://bbc.xania.org/archive/bbcdiscs";
 
/** How a disc came to be an image, which is the difference between the archives it came from. */
export const Provenance = {
    /** Read off the disc itself, by flux capture. */
    Captured: "captured",
    /** Rebuilt from a sector dump, so the surface around the data is inferred. */
    Reconstructed: "reconstructed",
};
 
// Numeric so a "Disc 2" would sort before a "Disc 10" rather than after it,
// and case-insensitive so a lower-cased title stays with its neighbours.
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
 
/**
 * Order the list by what someone is looking for, which is the disc's name.
 * The catalogue arrives grouped by publisher, so it has to be sorted here; the
 * remaining keys only settle ties, keeping a title's variants together and in
 * a stable order rather than the one the catalogue happens to list them in.
 */
export const byTitle = (a, b) =>
    collator.compare(a.title || a.path, b.title || b.path) ||
    collator.compare(a.publisher || "", b.publisher || "") ||
    collator.compare(a.disc || "", b.disc || "") ||
    collator.compare(a.variant || "", b.variant || "");
 
/**
 * How a disc reads in the list. Several fingerprinted variants of one title
 * sit next to each other, so the title alone doesn't identify a disc.
 *
 * @param {object} file manifest entry
 * @returns {{title: string, publisher: string, detail: string}}
 */
export function describe(file) {
    const detail = [file.disc, file.tracks?.join(", "), file.variant && `v${file.variant}`].filter(Boolean);
    return {
        title: file.title || file.path,
        publisher: file.publisher ?? "",
        detail: detail.join(" ยท "),
    };
}
 
export class BbcDiscArchive {
    /** @param {string} [baseUrl] where the mirror lives, to point at a test prefix */
    constructor(baseUrl = mirrorBase) {
        this._baseUrl = `${baseUrl}/hfe/`;
        this._catalogue = [];
        this._loaded = false;
    }
 
    /** @returns {Promise<object[]>} every manifest entry, sorted by title, fetched the first time it is asked for */
    async catalogue() {
        // Tracked separately from the catalogue: an archive can legitimately be
        // empty, and an empty array would mean "fetch it again" every time.
        if (this._loaded) return this._catalogue;
        const response = await fetch(`${this._baseUrl}manifest.json`);
        if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
        const data = await response.json();
        Iif (!Array.isArray(data?.files)) throw new Error("Invalid manifest: missing files array");
        this._catalogue = data.files
            // The captured discs were published before provenance was recorded.
            .map((file) => ({ ...file, provenance: file.provenance ?? Provenance.Captured }))
            .sort(byTitle);
        this._loaded = true;
        return this._catalogue;
    }
 
    /**
     * Nothing to unzip, unlike sth.js: blobs are stored compressed and served
     * with `Content-Encoding: br`, which the browser has undone by the time
     * this resolves.
     *
     * @param {string} path a manifest entry's `path`
     * @returns {Promise<Uint8Array>} the HFE image
     */
    async fetch(path) {
        const url = this._baseUrl + encodeURIComponent(path);
        console.log("Loading HFE from " + url);
        const response = await fetch(url);
        if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
        return new Uint8Array(await response.arrayBuffer());
    }
}