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.

chore: remove processors all together, integrate search into scoped-tracks orchestrator

+31 -387
+1 -2
src/build.vto
··· 90 90 {{ echo -}}await foundation.orchestrator.scrobbleAudio(){{- /echo }} 91 91 {{ echo -}}await foundation.orchestrator.sources(){{- /echo }} 92 92 93 - {{ echo -}}await foundation.orchestrator.artwork(){{- /echo }} 94 - {{ echo -}}await foundation.processor.search(){{- /echo -}} 93 + {{ echo -}}await foundation.orchestrator.artwork(){{- /echo -}} 95 94 </code> 96 95 </div> 97 96 </section>
+6 -33
src/common/foundation.js
··· 79 79 ), 80 80 }, 81 81 82 - processor: { 83 - search: signal( 84 - /** @type {import("~/components/processor/search/element.js").CLASS | null} */ (null), 85 - ), 86 - }, 87 82 }; 88 83 89 84 /** ··· 121 116 sources, 122 117 }, 123 118 124 - processor: { 125 - search, 126 - }, 127 - 128 119 /** 129 120 * Element signals 130 121 */ ··· 157 148 sources: signals.orchestrator.sources.get, 158 149 }, 159 150 160 - processor: { 161 - search: signals.processor.search.get, 162 - }, 163 151 }, 164 152 165 153 // Utilities ··· 313 301 } 314 302 315 303 316 - async function search() { 317 - const { default: SearchProcessor } = await import( 318 - "~/components/processor/search/element.js" 319 - ); 320 - 321 - const s = new SearchProcessor(); 322 - s.setAttribute("group", GROUP); 323 - 324 - return findExistingOrAdd(s, signals.processor.search); 325 - } 326 - 327 304 // Orchestrators 328 305 async function autoQueue() { 329 306 const [{ default: AutoQueueOrchestrator }, q, r, t] = await Promise.all([ ··· 435 412 } 436 413 437 414 async function scopedTracks() { 438 - const [{ default: ScopedTracksOrchestrator }, i, o, e, s] = await Promise.all( 439 - [ 440 - import("~/components/orchestrator/scoped-tracks/element.js"), 441 - input(), 442 - output(), 443 - scope(), 444 - search(), 445 - ], 446 - ); 415 + const [{ default: ScopedTracksOrchestrator }, i, o, e] = await Promise.all([ 416 + import("~/components/orchestrator/scoped-tracks/element.js"), 417 + input(), 418 + output(), 419 + scope(), 420 + ]); 447 421 448 422 const sto = new ScopedTracksOrchestrator(); 449 423 sto.setAttribute("group", GROUP); 450 424 sto.setAttribute("input-selector", i.selector); 451 425 sto.setAttribute("output-selector", o.selector); 452 426 sto.setAttribute("scope-engine-selector", e.selector); 453 - sto.setAttribute("search-processor-selector", s.selector); 454 427 455 428 return findExistingOrAdd(sto, signals.orchestrator.scopedTracks); 456 429 }
+22 -10
src/components/orchestrator/scoped-tracks/element.js
··· 5 5 } from "~/common/element.js"; 6 6 import { batch, computed, signal } from "~/common/signal.js"; 7 7 import { filterByPlaylist } from "~/common/playlist.js"; 8 + import { listen } from "~/common/worker.js"; 8 9 9 10 /** 11 + * @import {ProxiedActions} from "~/common/worker.d.ts" 10 12 * @import {Track} from "~/definitions/types.d.ts" 11 13 * @import {InputElement} from "~/components/input/types.d.ts" 12 14 * @import {OutputElement} from "~/components/output/types.d.ts" 15 + * @import {Actions, State} from "./types.d.ts" 13 16 */ 14 17 15 18 //////////////////////////////////////////// ··· 18 21 19 22 class ScopedTracksOrchestrator extends BroadcastableDiffuseElement { 20 23 static NAME = "diffuse/orchestrator/scoped-tracks"; 24 + static WORKER_URL = "components/orchestrator/scoped-tracks/worker.js"; 25 + 26 + /** @type {ProxiedActions<Actions & State>} */ 27 + #proxy; 28 + 29 + constructor() { 30 + super(); 31 + this.#proxy = this.workerProxy(); 32 + } 21 33 22 34 // SIGNALS 23 35 ··· 28 40 /** @type {import("~/components/engine/scope/element.js").CLASS | null} */ (null), 29 41 ); 30 42 31 - #search = signal( 32 - /** @type {import("~/components/processor/search/element.js").CLASS | null} */ (null), 33 - ); 43 + #supplyFingerprint = signal(/** @type {string | undefined} */ (undefined)); 34 44 35 45 #selectedPlaylistItems = computed(() => { 36 46 const playlist = this.#scope.value?.playlist(); ··· 47 57 48 58 // STATE 49 59 60 + supplyFingerprint = this.#supplyFingerprint.get; 50 61 tracks = this.#tracksFinal.get; 51 62 52 63 // LIFECYCLE ··· 113 124 /** @type {OutputElement} */ 114 125 const output = query(this, "output-selector"); 115 126 116 - /** @type {import("~/components/processor/search/element.js").CLASS} */ 117 - const search = query(this, "search-processor-selector"); 118 - 119 127 /** @type {import("~/components/engine/scope/element.js").CLASS | null} */ 120 128 const scope = queryOptional(this, "scope-engine-selector"); 121 129 122 130 // Assign to self 123 131 this.#input.value = input; 124 132 this.#output.value = output; 125 - this.#search.value = search; 126 133 if (scope) this.#scope.value = scope; 134 + 135 + // Sync supply fingerprint with worker 136 + const link = this.workerLink(); 137 + listen("supplyFingerprint", this.#supplyFingerprint.set, link); 138 + this.#proxy.supplyFingerprint().then(this.#supplyFingerprint.set); 127 139 128 140 // When defined 129 141 await customElements.whenDefined(input.localName); ··· 161 173 }); 162 174 163 175 // Set pool 164 - search.supply({ tracks: availableTracks }); 176 + this.#proxy.supply({ tracks: availableTracks }); 165 177 166 178 this.#tracksAvailable.set(availableTracks); 167 179 }); 168 180 169 181 // Watch search supply 170 182 this.effect(async () => { 171 - const _trigger = search.supplyFingerprint(); 183 + const _trigger = this.#supplyFingerprint.value; 172 184 const availableTracks = this.#tracksAvailable.value; 173 185 const searchTerm = this.#scope.value?.searchTerm(); 174 186 175 187 if ((await this.isLeader()) === false) return; 176 188 177 189 if (searchTerm?.length) { 178 - const searchResults = await search.search({ 190 + const searchResults = await this.#proxy.search({ 179 191 term: searchTerm, 180 192 }); 181 193 this.#tracksSearch.set(searchResults);
src/components/processor/search/constants.js src/components/orchestrator/scoped-tracks/constants.js
-67
src/components/processor/search/element.js
··· 1 - import { DiffuseElement } from "~/common/element.js"; 2 - import { signal } from "~/common/signal.js"; 3 - import { listen } from "~/common/worker.js"; 4 - 5 - /** 6 - * @import {ProxiedActions} from "~/common/worker.d.ts"; 7 - * @import {Actions, State} from "./types.d.ts" 8 - */ 9 - 10 - //////////////////////////////////////////// 11 - // ELEMENT 12 - //////////////////////////////////////////// 13 - 14 - /** 15 - * @implements {ProxiedActions<Actions>} 16 - */ 17 - class SearchProcessor extends DiffuseElement { 18 - static NAME = "diffuse/processor/search"; 19 - static WORKER_URL = "components/processor/search/worker.js"; 20 - 21 - constructor() { 22 - super(); 23 - 24 - /** @type {ProxiedActions<Actions & State>} */ 25 - this.proxy = this.workerProxy(); 26 - 27 - this.search = this.proxy.search; 28 - this.supply = this.proxy.supply; 29 - } 30 - 31 - // SIGNALS 32 - 33 - #supplyFingerprint = signal(/** @type {string | undefined} */ (undefined)); 34 - 35 - // STATE 36 - 37 - supplyFingerprint = this.#supplyFingerprint.get; 38 - 39 - // LIFECYCLE 40 - 41 - /** 42 - * @override 43 - */ 44 - connectedCallback() { 45 - super.connectedCallback(); 46 - 47 - // Sync data with worker 48 - const link = this.workerLink(); 49 - 50 - // Listen for remote data changes 51 - listen("supplyFingerprint", this.#supplyFingerprint.set, link); 52 - 53 - // Fetch current data state 54 - this.proxy.supplyFingerprint().then(this.#supplyFingerprint.set); 55 - } 56 - } 57 - 58 - export default SearchProcessor; 59 - 60 - //////////////////////////////////////////// 61 - // REGISTER 62 - //////////////////////////////////////////// 63 - 64 - export const CLASS = SearchProcessor; 65 - export const NAME = "dp-search"; 66 - 67 - customElements.define(NAME, SearchProcessor);
src/components/processor/search/types.d.ts src/components/orchestrator/scoped-tracks/types.d.ts
src/components/processor/search/worker.js src/components/orchestrator/scoped-tracks/worker.js
+1 -15
src/elements.vto
··· 128 128 desc: "Fetches cover art for a given set of tracks, stored locally in indexedDB. Uses the artwork configurator to try each configured source in sequence." 129 129 - url: "components/orchestrator/scoped-tracks/element.js" 130 130 title: "Scoped Tracks" 131 - desc: "Supplies the tracks from the given output to the given search processor whenever the tracks collection changes. Additionally it can perform a search and other ways to reduce the scope of tracks based on the given scope engine. Provides a `tracks` signal similar to `output.tracks.collection`" 131 + desc: "Watches the given output's tracks collection and runs them through a built-in search index. Can perform a search and other ways to reduce the scope of tracks based on the given scope engine. Provides a `tracks` signal similar to `output.tracks.collection`" 132 132 133 133 output: 134 134 - url: "components/output/polymorphic/indexed-db/element.js" ··· 147 147 - url: "components/metadata/audio-file/element.js" 148 148 title: "Audio File" 149 149 desc: "Extracts tags and audio stats from audio files using the music-metadata library." 150 - 151 - processors: 152 - - url: "components/processor/search/element.js" 153 - title: "Search" 154 - desc: "Provides a way to search through a collection of tracks, powered by orama.js" 155 150 156 151 supplements: 157 152 - url: "components/supplement/last.fm/element.js" ··· 245 240 <li><a href="elements/#metadata">Metadata</a></li> 246 241 <li><a href="elements/#orchestrators">Orchestrators</a></li> 247 242 <li><a href="elements/#output">Output</a></li> 248 - <li><a href="elements/#processors">Processors</a></li> 249 243 <li><a href="elements/#supplements">Supplements</a></li> 250 244 <li><a href="elements/#transformers">Transformers</a></li> 251 245 <!----> ··· 311 305 items: output, 312 306 content: ` 313 307 Output is application-derived data such as playlists. These elements can receive such data and keep it around. These are categorised by the type of data they ingest, or many types in the case of polymorphic. Optionally use transformers to convert output into the expected format. 314 - ` 315 - }) }} 316 - 317 - {{ await comp.element({ 318 - title: "Processors", 319 - items: processors, 320 - content: ` 321 - These elements work with the output generated by the input elements to add more data to them, or process them in some other way. 322 308 ` 323 309 }) }} 324 310
+1 -2
src/themes/winamp/facet/index.inline.js
··· 17 17 const queue = await foundation.engine.queue(); 18 18 const repeatShuffle = await foundation.engine.repeatShuffle(); 19 19 const scopedTracks = await foundation.orchestrator.scopedTracks(); 20 - const search = await foundation.processor.search(); 21 20 22 21 await foundation.orchestrator.sources(); 23 22 await foundation.orchestrator.processTracks({ disableWhenReady: true }); ··· 65 64 const col = output.tracks.collection(); 66 65 if (col.state !== "loaded") return; 67 66 68 - const fingerprintSearch = search.supplyFingerprint(); 67 + const fingerprintSearch = scopedTracks.supplyFingerprint(); 69 68 if (fingerprintSearch === undefined) return; 70 69 71 70 const fingerprintQueue = queue.supplyFingerprint();
-258
tests/components/processor/search/test.ts
··· 1 - import { describe, it } from "@std/testing/bdd"; 2 - import { expect } from "@std/expect"; 3 - 4 - import { testWeb } from "@tests/common/index.ts"; 5 - import { trackA, trackB, tracks } from "~/testing/sample/tracks.js"; 6 - 7 - describe("components/processor/search", () => { 8 - it("finds tracks by album", async () => { 9 - const results = await testWeb(async () => { 10 - const SearchProcessor = await import( 11 - "~/components/processor/search/element.js" 12 - ); 13 - const processor = new SearchProcessor.CLASS(); 14 - 15 - document.body.append(processor); 16 - 17 - // Add sample tracks to the supply first 18 - const { tracks } = await import("~/testing/sample/tracks.js"); 19 - await processor.supply({ tracks }); 20 - 21 - // Search for a specific term 22 - return processor.search({ term: tracks[1]?.tags?.album }); 23 - }); 24 - 25 - expect(results[0]?.id).toBe(trackB.id); 26 - }); 27 - 28 - it("finds tracks by artist", async () => { 29 - const results = await testWeb(async () => { 30 - const SearchProcessor = await import( 31 - "~/components/processor/search/element.js" 32 - ); 33 - const processor = new SearchProcessor.CLASS(); 34 - 35 - document.body.append(processor); 36 - 37 - // Add sample tracks to the supply first 38 - const { tracks } = await import("~/testing/sample/tracks.js"); 39 - await processor.supply({ tracks }); 40 - 41 - // Search for a specific term 42 - return processor.search({ term: tracks[0]?.tags?.artist }); 43 - }); 44 - 45 - expect(results[0]?.id).toBe(trackA.id); 46 - }); 47 - 48 - it("finds tracks by title", async () => { 49 - const results = await testWeb(async () => { 50 - const SearchProcessor = await import( 51 - "~/components/processor/search/element.js" 52 - ); 53 - const processor = new SearchProcessor.CLASS(); 54 - 55 - document.body.append(processor); 56 - 57 - // Add sample tracks to the supply first 58 - const { tracks } = await import("~/testing/sample/tracks.js"); 59 - await processor.supply({ tracks }); 60 - 61 - // Search for a specific term 62 - return processor.search({ term: tracks[1]?.tags?.title }); 63 - }); 64 - 65 - expect(results[0]?.id).toBe(trackB.id); 66 - }); 67 - 68 - it("returns empty array when no tracks match the search term", async () => { 69 - const results = await testWeb(async () => { 70 - const SearchProcessor = await import( 71 - "~/components/processor/search/element.js" 72 - ); 73 - const processor = new SearchProcessor.CLASS(); 74 - 75 - document.body.append(processor); 76 - 77 - const { tracks } = await import("~/testing/sample/tracks.js"); 78 - await processor.supply({ tracks }); 79 - 80 - return processor.search({ term: "zzz-no-match-zzz" }); 81 - }); 82 - 83 - expect(results).toEqual([]); 84 - }); 85 - 86 - it("supplyFingerprint is undefined before first supply", async () => { 87 - const fp = await testWeb(async () => { 88 - const SearchProcessor = await import( 89 - "~/components/processor/search/element.js" 90 - ); 91 - const processor = new SearchProcessor.CLASS(); 92 - 93 - document.body.append(processor); 94 - 95 - return (await processor.supplyFingerprint()) ?? null; 96 - }); 97 - 98 - expect(fp).toBe(null); 99 - }); 100 - 101 - it("supplyFingerprint is set after supply", async () => { 102 - const fp = await testWeb(async () => { 103 - const SearchProcessor = await import( 104 - "~/components/processor/search/element.js" 105 - ); 106 - const processor = new SearchProcessor.CLASS(); 107 - 108 - document.body.append(processor); 109 - 110 - const { tracks } = await import("~/testing/sample/tracks.js"); 111 - await processor.supply({ tracks }); 112 - 113 - return processor.supplyFingerprint() ?? null; 114 - }); 115 - 116 - expect(fp).not.toBe(null); 117 - expect(typeof fp).toBe("string"); 118 - }); 119 - 120 - it("supply with same tracks does not change the fingerprint", async () => { 121 - const [fp1, fp2] = await testWeb(async () => { 122 - const SearchProcessor = await import( 123 - "~/components/processor/search/element.js" 124 - ); 125 - const processor = new SearchProcessor.CLASS(); 126 - 127 - document.body.append(processor); 128 - 129 - const { tracks } = await import("~/testing/sample/tracks.js"); 130 - await processor.supply({ tracks }); 131 - const fp1 = processor.supplyFingerprint(); 132 - 133 - await processor.supply({ tracks }); 134 - const fp2 = processor.supplyFingerprint(); 135 - 136 - return [fp1, fp2]; 137 - }); 138 - 139 - expect(fp1).toBe(fp2); 140 - }); 141 - 142 - it("supply removes tracks no longer in the list", async () => { 143 - const results = await testWeb(async () => { 144 - const SearchProcessor = await import( 145 - "~/components/processor/search/element.js" 146 - ); 147 - const processor = new SearchProcessor.CLASS(); 148 - 149 - document.body.append(processor); 150 - 151 - const { trackA, trackB } = await import("~/testing/sample/tracks.js"); 152 - 153 - // Supply both tracks then narrow to only trackB 154 - await processor.supply({ tracks: [trackA, trackB] }); 155 - await processor.supply({ tracks: [trackB] }); 156 - 157 - // trackA's artist "Artist" is absent from trackB — must return empty 158 - return processor.search({ term: "Artist" }); 159 - }); 160 - 161 - expect(results).toEqual([]); 162 - }); 163 - 164 - it("supply with empty list clears all tracks from the index", async () => { 165 - const results = await testWeb(async () => { 166 - const SearchProcessor = await import( 167 - "~/components/processor/search/element.js" 168 - ); 169 - const processor = new SearchProcessor.CLASS(); 170 - 171 - document.body.append(processor); 172 - 173 - const { tracks } = await import("~/testing/sample/tracks.js"); 174 - await processor.supply({ tracks }); 175 - await processor.supply({ tracks: [] }); 176 - 177 - return processor.search({ term: "Sample" }); 178 - }); 179 - 180 - expect(results).toEqual([]); 181 - }); 182 - 183 - it("sorts results by artist alphabetically", async () => { 184 - const ids = await testWeb(async () => { 185 - const SearchProcessor = await import( 186 - "~/components/processor/search/element.js" 187 - ); 188 - const processor = new SearchProcessor.CLASS(); 189 - 190 - document.body.append(processor); 191 - 192 - const testTracks = [ 193 - { 194 - $type: "sh.diffuse.output.track" as const, 195 - id: "sort-zebra", 196 - uri: "diffuse://sort-zebra", 197 - tags: { artist: "Zebra", title: "Sort Test" }, 198 - }, 199 - { 200 - $type: "sh.diffuse.output.track" as const, 201 - id: "sort-apple", 202 - uri: "diffuse://sort-apple", 203 - tags: { artist: "Apple", title: "Sort Test" }, 204 - }, 205 - { 206 - $type: "sh.diffuse.output.track" as const, 207 - id: "sort-mango", 208 - uri: "diffuse://sort-mango", 209 - tags: { artist: "Mango", title: "Sort Test" }, 210 - }, 211 - ]; 212 - 213 - await processor.supply({ tracks: testTracks }); 214 - const results = await processor.search({ term: "Sort Test" }); 215 - return results.map((t) => t.id); 216 - }); 217 - 218 - expect(ids).toEqual(["sort-apple", "sort-mango", "sort-zebra"]); 219 - }); 220 - 221 - it("sorts results by track number within the same album", async () => { 222 - const ids = await testWeb(async () => { 223 - const SearchProcessor = await import( 224 - "~/components/processor/search/element.js" 225 - ); 226 - const processor = new SearchProcessor.CLASS(); 227 - 228 - document.body.append(processor); 229 - 230 - const testTracks = [ 231 - { 232 - $type: "sh.diffuse.output.track" as const, 233 - id: "track-03", 234 - uri: "diffuse://track-03", 235 - tags: { artist: "Band", album: "Album", track: { no: 3 }, title: "C" }, 236 - }, 237 - { 238 - $type: "sh.diffuse.output.track" as const, 239 - id: "track-01", 240 - uri: "diffuse://track-01", 241 - tags: { artist: "Band", album: "Album", track: { no: 1 }, title: "A" }, 242 - }, 243 - { 244 - $type: "sh.diffuse.output.track" as const, 245 - id: "track-02", 246 - uri: "diffuse://track-02", 247 - tags: { artist: "Band", album: "Album", track: { no: 2 }, title: "B" }, 248 - }, 249 - ]; 250 - 251 - await processor.supply({ tracks: testTracks }); 252 - const results = await processor.search({ term: "Band" }); 253 - return results.map((t) => t.id); 254 - }); 255 - 256 - expect(ids).toEqual(["track-01", "track-02", "track-03"]); 257 - }); 258 - });