forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import * as IDB from "idb-keyval";
2
3import { create as createCid } from "~/common/cid.js";
4import { ostiary, rpc, workerProxy } from "~/common/worker.js";
5
6/**
7 * @import {Track} from "~/definitions/types.d.ts"
8 * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts"
9 * @import {Actions} from "~/components/artwork/types.d.ts"
10 * @import {Artwork} from "./types.d.ts"
11 */
12
13// multicodec raw bytes
14const RAW = 0x55;
15
16const IDB_PREFIX = "~/components/orchestrator/artwork";
17const IDB_ARTWORK_PREFIX = `${IDB_PREFIX}/cache`;
18
19/** @type {Map<string, Promise<Uint8Array | null>>} */
20const inFlight = new Map();
21
22////////////////////////////////////////////
23// ACTIONS
24////////////////////////////////////////////
25
26/**
27 * @type {ActionsWithTunnel<Actions>['get']}
28 */
29export async function get({ data: track, ports }) {
30 const existing = inFlight.get(track.id);
31 if (existing) return existing;
32
33 const promise = processRequest(track, ports).finally(() => {
34 inFlight.delete(track.id);
35 });
36
37 inFlight.set(track.id, promise);
38 return promise;
39}
40
41////////////////////////////////////////////
42// ⚡️
43////////////////////////////////////////////
44
45ostiary((context) => {
46 rpc(context, { get });
47});
48
49////////////////////////////////////////////
50// 🛠️
51////////////////////////////////////////////
52
53/**
54 * @param {Track} track
55 * @param {Record<string, MessagePort>} ports
56 * @returns {Promise<Uint8Array | null>}
57 */
58async function processRequest(track, ports) {
59 // Check if already processed
60
61 /** @type {string[] | undefined} */
62 const cachedCids = await IDB.get(
63 `${IDB_ARTWORK_PREFIX}/track/${track.id}`,
64 );
65
66 if (cachedCids?.length) {
67 /** @type {Artwork[]} */
68 const art = await Promise.all(
69 cachedCids.map((cid) => IDB.get(`${IDB_ARTWORK_PREFIX}/image/${cid}`)),
70 );
71
72 const found = art.filter(Boolean);
73 if (found.length) return found[0].bytes;
74 }
75
76 // 🚀
77
78 /** @type {ProxiedActions<Actions>} */
79 const configurator = workerProxy(() => {
80 ports.artwork.start();
81 return ports.artwork;
82 });
83
84 const bytes = await configurator.get(track);
85
86 if (bytes === null) {
87 await IDB.set(`${IDB_ARTWORK_PREFIX}/track/${track.id}`, []);
88 return null;
89 }
90
91 const mime = detectMime(bytes);
92
93 /** @type {Artwork} */
94 const art = { bytes, mime };
95
96 // Save artwork to IDB — store by content CID, map track to that CID
97 const cid = await createCid(RAW, bytes);
98 const key = `${IDB_ARTWORK_PREFIX}/image/${cid}`;
99 if (!await IDB.get(key)) await IDB.set(key, art);
100
101 await IDB.set(`${IDB_ARTWORK_PREFIX}/track/${track.id}`, [cid]);
102
103 return bytes;
104}
105
106/**
107 * @param {Uint8Array} bytes
108 * @returns {string}
109 */
110function detectMime(bytes) {
111 if (bytes[0] === 0xFF && bytes[1] === 0xD8) return "image/jpeg";
112 if (bytes[0] === 0x89 && bytes[1] === 0x50) return "image/png";
113 if (bytes[0] === 0x47 && bytes[1] === 0x49) return "image/gif";
114 if (bytes[0] === 0x52 && bytes[1] === 0x49) return "image/webp";
115 return "image/jpeg";
116}