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