A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v4 180 lines 4.2 kB view raw
1import * as Orama from "@orama/orama"; 2import { xxh32 } from "xxh32"; 3 4import { SCHEMA } from "./constants.js"; 5import { announce, ostiary, rpc } from "~/common/worker.js"; 6import { effect, signal } from "~/common/signal.js"; 7 8/** 9 * @import {SearchParams} from "@orama/orama"; 10 * 11 * @import {Track} from "~/definitions/types.d.ts" 12 * @import {Actions, Schema} from "./types.d.ts" 13 */ 14 15//////////////////////////////////////////// 16// STATE 17//////////////////////////////////////////// 18 19/** @type {Set<string>} */ 20export let inserted = new Set(); 21export let insertedFingerprint = ""; 22 23// Communicated state 24export const $supplyFingerprint = signal( 25 /** @type {string | undefined} */ (undefined), 26); 27 28//////////////////////////////////////////// 29// DATABASE 30//////////////////////////////////////////// 31 32/** 33 * @type {Orama.OramaPlugin[]} 34 */ 35const PLUGINS = []; 36 37const db = Orama.create({ 38 schema: SCHEMA, 39 plugins: PLUGINS, 40}); 41 42//////////////////////////////////////////// 43// ACTIONS 44//////////////////////////////////////////// 45 46/** 47 * @type {Actions['search']} 48 */ 49export async function search(params) { 50 return await _search( 51 "term" in params && typeof params.term === "string" 52 ? { ...params, term: params.term.trim() } 53 : params, 54 [], 55 ); 56} 57 58/** 59 * @type {Actions['supply']} 60 */ 61export async function supply({ tracks }) { 62 // TODO: Generate a hash based on the track itself, 63 // so we can detect changes to tags or other data. 64 65 /** @type {Map<string, Track>} */ 66 const tracksMap = new Map(); 67 68 for (const track of tracks) { 69 if (!track.tags) continue; 70 tracksMap.set(track.id, track); 71 } 72 73 const ids = Array.from(tracksMap.keys()); 74 const idsString = ids.sort().join(""); 75 const fingerprint = xxh32(idsString).toString(); 76 77 if (fingerprint === insertedFingerprint) return; 78 79 const currentSet = inserted; 80 const newSet = new Set(ids); 81 82 inserted = newSet; 83 insertedFingerprint = fingerprint; 84 85 const removedIds = currentSet.difference(newSet); 86 const newIds = newSet.difference(currentSet); 87 88 /** @type {Track[]} */ 89 const newTracks = []; 90 91 for (const id of newIds) { 92 const track = tracksMap.get(id); 93 if (track) newTracks.push(track); 94 } 95 96 await Orama.removeMultiple(db, Array.from(removedIds)); 97 await Orama.insertMultiple(db, newTracks); 98 99 $supplyFingerprint.value = fingerprint; 100} 101 102//////////////////////////////////////////// 103// ⚡️ 104//////////////////////////////////////////// 105 106ostiary((context) => { 107 rpc(context, { 108 search, 109 supply, 110 111 // State 112 supplyFingerprint: $supplyFingerprint.get, 113 }); 114 115 // Effects 116 // ------- 117 118 // Communicate state 119 effect(() => 120 announce("supplyFingerprint", $supplyFingerprint.value, context) 121 ); 122}); 123 124//////////////////////////////////////////// 125// ⛔️ 126//////////////////////////////////////////// 127 128/** 129 * @param {SearchParams<Schema>} params 130 * @param {Track[]} tracks 131 */ 132async function _search(params, tracks) { 133 const results = await Orama.search(db, { 134 // @ts-ignore: No clue what the correct type is for this one 135 sortBy, 136 ...params, 137 threshold: 0, // AND operator: all search terms must match in at least one property 138 limit: 10000, 139 offset: tracks.length, 140 }); 141 142 const allTracks = tracks.concat( 143 results.hits.map(( 144 hit, 145 ) => /** @type {Track} */ (/** @type {unknown} */ (hit.document))), 146 ); 147 148 if (allTracks.length < results.count) { 149 return await _search(params, allTracks); 150 } else { 151 return allTracks; 152 } 153} 154 155/** 156 * @type {Orama.CustomSorterFunction<Orama.TypedDocument<Schema>>} 157 */ 158function sortBy(a, b) { 159 const artist = (a[2].tags?.artist ?? "").localeCompare( 160 b[2].tags?.artist ?? "", 161 ); 162 if (artist != 0) return artist; 163 164 const album = (a[2].tags?.album ?? "").localeCompare( 165 b[2].tags?.album ?? "", 166 ); 167 if (album != 0) return album; 168 169 const discNo = (a[2].tags?.disc?.no ?? 0) - (b[2].tags?.disc?.no ?? 0); 170 if (discNo != 0) return discNo; 171 172 const trackNo = (a[2].tags?.track?.no ?? 0) - (b[2].tags?.track?.no ?? 0); 173 if (trackNo != 0) return trackNo; 174 175 const title = (a[2].tags?.title ?? "").localeCompare( 176 b[2].tags?.title ?? "", 177 ); 178 if (title != 0) return title; 179 return 0; 180}