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

Configure Feed

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

feat: improve search

+117 -42
+1 -6
src/components/orchestrator/queue-tracks/element.js
··· 76 76 77 77 this.isLeader().then((isLeader) => { 78 78 if (!isLeader) return; 79 - 80 - untracked(() => 81 - this.#proxy.poolAvailable( 82 - tracks.filter((t) => t.kind !== "placeholder"), 83 - ) 84 - ); 79 + untracked(() => this.#proxy.poolAvailable(tracks)); 85 80 }); 86 81 }); 87 82
+1 -1
src/components/orchestrator/queue-tracks/worker.js
··· 16 16 * @type {ActionsWithTunnel<Actions>["poolAvailable"]} 17 17 */ 18 18 export async function poolAvailable({ data, ports }) { 19 - const cachedTracks = data; 19 + const cachedTracks = data.filter((t) => t.kind !== "placeholder"); 20 20 21 21 /** @type {ProxiedActions<InputActions>} */ 22 22 const input = workerProxy(() => ports.input);
+1 -4
src/components/orchestrator/search-tracks/element.js
··· 70 70 // Watch tracks collection 71 71 this.effect(async () => { 72 72 const collection = output.tracks.collection(); 73 - 74 73 if ((await this.isLeader()) === false) return; 75 - 76 - const tracks = collection.filter((t) => t.kind !== "placeholder"); 77 - this.#proxy.supplyAvailable(tracks); 74 + this.#proxy.supplyAvailable(collection); 78 75 }); 79 76 } 80 77
+2 -2
src/components/orchestrator/search-tracks/worker.js
··· 16 16 * @type {ActionsWithTunnel<Actions>["supplyAvailable"]} 17 17 */ 18 18 export async function supplyAvailable({ data, ports }) { 19 - const cachedTracks = data; 19 + const cachedTracks = data.filter((t) => t.kind !== "placeholder"); 20 20 21 21 /** @type {ProxiedActions<InputActions>} */ 22 22 const input = workerProxy(() => ports.input); ··· 39 39 }, []); 40 40 41 41 // Set pool 42 - await search.supply(availableTracks); 42 + await search.supply({ tracks: availableTracks }); 43 43 } 44 44 45 45 ////////////////////////////////////////////
+11
src/components/processor/search/constants.js
··· 1 + /** 2 + * Maps directly on the `Track` definition 3 + * (ie. `definitions/output/tracks.json`) 4 + */ 1 5 export const SCHEMA = { 2 6 id: /** @type {const} */ ("string"), 3 7 kind: /** @type {const} */ ("string"), ··· 7 11 genre: /** @type {const} */ ("string"), 8 12 title: /** @type {const} */ ("string"), 9 13 year: /** @type {const} */ ("number"), 14 + 15 + disc: { 16 + no: /** @type {const} */ ("number"), 17 + }, 18 + track: { 19 + no: /** @type {const} */ ("number"), 20 + }, 10 21 }, 11 22 12 23 // TODO:
+10 -2
src/components/processor/search/types.d.ts
··· 1 + import type { Orama, SearchParams } from "@orama/orama"; 2 + 1 3 import type { SignalReader } from "@common/signal.d.ts"; 2 4 import type { Track } from "@definitions/types.d.ts"; 5 + import type { SCHEMA } from "./constants.js"; 3 6 4 7 export type Actions = { 5 - search(term: string): Promise<Track[]>; 6 - supply(tracks: Track[]): Promise<void>; 8 + /** 9 + * https://docs.orama.com/docs/orama-js/search 10 + */ 11 + search(params: SearchParams<Schema>): Promise<Track[]>; 12 + supply(args: { tracks: Track[] }): Promise<void>; 7 13 }; 14 + 15 + export type Schema = Orama<typeof SCHEMA>; 8 16 9 17 export type State = { 10 18 cacheId: SignalReader<string>;
+47 -10
src/components/processor/search/worker.js
··· 7 7 import { effect, signal } from "@common/signal.js"; 8 8 9 9 /** 10 + * @import {SearchParams} from "@orama/orama"; 11 + * 10 12 * @import {Track} from "@definitions/types.d.ts" 11 - * @import {Actions} from "./types.d.ts" 13 + * @import {Actions, Schema} from "./types.d.ts" 12 14 */ 13 15 14 16 //////////////////////////////////////////// ··· 50 52 /** 51 53 * @type {Actions['search']} 52 54 */ 53 - export async function search(term) { 54 - term = term.trim(); 55 - return await _search(term, []); 55 + export async function search(params) { 56 + return await _search( 57 + "term" in params && typeof params.term === "string" 58 + ? { ...params, term: params.term.trim() } 59 + : params, 60 + [], 61 + ); 56 62 } 57 63 58 64 /** 59 65 * @type {Actions['supply']} 60 66 */ 61 - export async function supply(tracks) { 67 + export async function supply({ tracks }) { 62 68 // TODO: Generate a hash based on the track itself, 63 69 // so we can detect changes to tags or other data. 64 70 ··· 84 90 await Orama.insertMultiple(db, newTracks); 85 91 86 92 $inserted.value = newSet; 87 - $cacheId.value = xxh32(ids.sort().join("")).toString(); 93 + $cacheId.value = ids.length === 0 94 + ? "" 95 + : xxh32(ids.sort().join("")).toString(); 88 96 } 89 97 90 98 //////////////////////////////////////////// ··· 111 119 //////////////////////////////////////////// 112 120 113 121 /** 114 - * @param {string} term 122 + * @param {SearchParams<Schema>} params 115 123 * @param {Track[]} tracks 116 124 */ 117 - async function _search(term, tracks) { 125 + async function _search(params, tracks) { 118 126 const results = await Orama.search(db, { 127 + // @ts-ignore: No clue what the correct type is for this one 128 + sortBy, 129 + ...params, 119 130 // mode: "hybrid", 120 - term, 121 131 limit: 10000, 122 132 offset: tracks.length, 123 133 }); ··· 129 139 ); 130 140 131 141 if (allTracks.length < results.count) { 132 - return await _search(term, allTracks); 142 + return await _search(params, allTracks); 133 143 } else { 134 144 return allTracks; 135 145 } 136 146 } 147 + 148 + /** 149 + * @type {Orama.CustomSorterFunction<Orama.TypedDocument<Schema>>} 150 + */ 151 + function sortBy(a, b) { 152 + const artist = (a[2].tags?.artist ?? "").localeCompare( 153 + b[2].tags?.artist ?? "", 154 + ); 155 + if (artist != 0) return artist; 156 + 157 + const album = (a[2].tags?.album ?? "").localeCompare( 158 + b[2].tags?.album ?? "", 159 + ); 160 + if (album != 0) return album; 161 + 162 + const discNo = (a[2].tags?.disc?.no ?? 0) - (b[2].tags?.disc?.no ?? 0); 163 + if (discNo != 0) return discNo; 164 + 165 + const trackNo = (a[2].tags?.track?.no ?? 0) - (b[2].tags?.track?.no ?? 0); 166 + if (trackNo != 0) return trackNo; 167 + 168 + const title = (a[2].tags?.title ?? "").localeCompare( 169 + b[2].tags?.title ?? "", 170 + ); 171 + if (title != 0) return title; 172 + return 0; 173 + }
+4
src/index.vto
··· 30 30 title: "Blur / Artwork controller" 31 31 desc: > 32 32 Audio playback controller with artwork display. 33 + - url: "themes/webamp/browser/" 34 + title: "Webamp / Browser" 35 + desc: > 36 + Collection browser + search in a retro, win98, look. 33 37 34 38 # ELEMENTS 35 39
+40 -17
src/themes/webamp/browser/element.js
··· 20 20 21 21 #searchResults = signal(/** @type {Track[]} */ ([])); 22 22 23 + $input = signal(/** @type {InputElement | undefined} */ (undefined)); 24 + $output = signal( 25 + /** @type {OutputElement<Track[]> | undefined} */ (undefined), 26 + ); 27 + $queue = signal( 28 + /** @type {import("@components/engine/queue/element.js").CLASS | undefined} */ (undefined), 29 + ); 30 + $search = signal( 31 + /** @type {import("@components/processor/search/element.js").CLASS | undefined} */ (undefined), 32 + ); 33 + 23 34 // LIFECYCLE 24 35 25 36 /** ··· 40 51 /** @type {import("@components/processor/search/element.js").CLASS} */ 41 52 const search = query(this, "search-processor-selector"); 42 53 43 - this.input = input; 44 - this.output = output; 45 - this.queue = queue; 46 - this.search = search; 54 + this.$input.value = input; 55 + this.$output.value = output; 56 + this.$queue.value = queue; 57 + this.$search.value = search; 47 58 48 59 // Wait for the above dependencies to be defined, then render again. 49 60 whenElementsDefined({ input, output, search }).then(() => { ··· 88 99 * @param {Track} track 89 100 */ 90 101 playTrack(track) { 91 - this.queue?.add({ 102 + this.$queue.value?.add({ 92 103 inFront: true, 93 104 tracks: [track], 94 105 }); 95 106 96 - this.queue?.shift(); 107 + this.$queue.value?.shift(); 97 108 } 98 109 99 110 async performSearch() { ··· 101 112 const input = this.root().querySelector("#search-input"); 102 113 const term = input?.value?.trim(); 103 114 104 - this.#searchResults.value = await this.search?.search(term ?? "") ?? []; 115 + this.#searchResults.value = await this.$search.value?.search({ 116 + term: term, 117 + }) ?? []; 105 118 } 106 119 107 120 // RENDER ··· 110 123 * @param {RenderArg} _ 111 124 */ 112 125 render({ html }) { 126 + const isLoading = this.$output.value?.tracks.state() !== "loaded" || 127 + this.$search.value?.cacheId() === ""; 113 128 const tracks = this.#searchResults.value; 114 129 115 130 return html` ··· 190 205 </tr> 191 206 </thead> 192 207 <tbody> 193 - ${tracks.map((track) => { 194 - return html` 195 - <tr @click="${this.highlightTableEntry}" @dblclick="${() => 196 - this.playTrack(track)}"> 197 - <td>${track.tags?.title}</td> 198 - <td>${track.tags?.artist}</td> 199 - <td>${track.tags?.album}</td> 208 + ${isLoading 209 + ? html` 210 + <tr> 211 + <td>Loading ...</td> 212 + <td></td> 213 + <td></td> 200 214 </tr> 201 - `; 202 - })} 215 + ` 216 + : tracks.map((track) => { 217 + return html` 218 + <tr @click="${this.highlightTableEntry}" @dblclick="${() => 219 + this.playTrack(track)}"> 220 + <td>${track.tags?.title}</td> 221 + <td>${track.tags?.artist}</td> 222 + <td>${track.tags?.album}</td> 223 + </tr> 224 + `; 225 + })} 203 226 </tbody> 204 227 </table> 205 228 </div> ··· 216 239 export const CLASS = Browser; 217 240 export const NAME = "dtw-browser"; 218 241 219 - customElements.define(NAME, Browser); 242 + customElements.define(NAME, CLASS);