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.

refactor: group consult uris only

+150 -46
+17
src/common/utils.js
··· 63 63 } 64 64 65 65 /** 66 + * @param {string[]} uris 67 + * @returns {Record<string, string[]>} 68 + */ 69 + export function groupUrisPerScheme(uris) { 70 + /** @type {Record<string, string[]>} */ 71 + const acc = {}; 72 + 73 + uris.forEach((uri) => { 74 + const scheme = uri.substring(0, uri.indexOf(":")); 75 + acc[scheme] ??= []; 76 + acc[scheme].push(uri); 77 + }); 78 + 79 + return acc; 80 + } 81 + 82 + /** 66 83 * @param {unknown} test 67 84 */ 68 85 export function isPrimitive(test) {
+23 -13
src/components/configurator/input/worker.js
··· 1 1 import * as URI from "uri-js"; 2 2 3 - import { groupTracksPerScheme } from "@common/utils.js"; 3 + import { groupTracksPerScheme, groupUrisPerScheme } from "@common/utils.js"; 4 4 import { ostiary, rpc, workerProxy } from "@common/worker.js"; 5 5 6 6 /** ··· 65 65 * @type {ActionsWithTunnel<InputActions>['groupConsult']} 66 66 */ 67 67 export async function groupConsult({ data, ports }) { 68 - const tracks = data; 69 - const groups = groupTracksPerScheme(tracks); 68 + const uris = data; 69 + const groups = groupUrisPerScheme(uris); 70 70 71 71 /** @type {GroupConsult[]} */ 72 72 const consultations = await Promise.all( ··· 79 79 available: false, 80 80 reason: "Unsupported scheme", 81 81 scheme, 82 - tracks: groups[scheme] ?? [], 82 + uris: groups[scheme] ?? [], 83 83 }, 84 84 }; 85 85 } 86 86 87 - return await input.groupConsult(groups[scheme] ?? {}); 87 + return await input.groupConsult(groups[scheme] ?? []); 88 88 }), 89 89 ); 90 90 ··· 97 97 * @type {ActionsWithTunnel<InputActions>['list']} 98 98 */ 99 99 export async function list({ data, ports }) { 100 - const groups = await groupConsult({ data, ports }); 100 + const tracks = data; 101 + const uris = tracks.map((/** @type {Track} */ t) => t.uri); 102 + 103 + /** @type {Map<string, Track>} */ 104 + const tracksByUri = new Map( 105 + tracks.map((/** @type {Track} */ t) => [t.uri, t]), 106 + ); 107 + 108 + const groups = await groupConsult({ data: uris, ports }); 101 109 102 110 const promises = Object.values(groups).map( 103 - async ({ available, scheme, tracks }) => { 104 - if (!available) return tracks; 111 + async ({ available, scheme, uris }) => { 112 + const groupTracks = uris 113 + .map((uri) => tracksByUri.get(uri)) 114 + .filter((/** @type {Track | undefined} */ t) => t !== undefined); 115 + 116 + if (!available) return groupTracks; 105 117 106 118 const input = grabInput(scheme, ports); 107 - if (!input) return tracks; 108 - return await input.list(tracks); 119 + if (!input) return groupTracks; 120 + return await input.list(groupTracks); 109 121 }, 110 122 ); 111 123 112 124 const nested = await Promise.all(promises); 113 - const tracks = nested.flat(1); 114 - 115 - return tracks; 125 + return nested.flat(1); 116 126 } 117 127 118 128 /**
+26
src/components/input/https/common.js
··· 84 84 } 85 85 86 86 /** 87 + * Group URIs by host. 88 + * 89 + * @param {string[]} uris 90 + * @returns {Record<string, { host: string; uris: string[] }>} 91 + */ 92 + export function groupUrisByHost(uris) { 93 + /** @type {Record<string, { host: string; uris: string[] }>} */ 94 + const acc = {}; 95 + 96 + uris.forEach((uri) => { 97 + const parsed = parseURI(uri); 98 + if (!parsed) return; 99 + 100 + const host = parsed.host; 101 + 102 + if (acc[host]) { 103 + acc[host].uris.push(uri); 104 + } else { 105 + acc[host] = { host, uris: [uri] }; 106 + } 107 + }); 108 + 109 + return acc; 110 + } 111 + 112 + /** 87 113 * Extract unique hosts from tracks. 88 114 * 89 115 * @param {Track[]} tracks
+13 -13
src/components/input/https/worker.js
··· 4 4 groupKeyHash, 5 5 } from "@components/input/common.js"; 6 6 7 - import { groupTracksByHost, parseURI } from "./common.js"; 7 + import { groupTracksByHost, groupUrisByHost, parseURI } from "./common.js"; 8 8 import { SCHEME } from "./constants.js"; 9 9 10 10 /** ··· 70 70 /** 71 71 * @type {Actions['groupConsult']} 72 72 */ 73 - export async function groupConsult(tracks) { 74 - const groups = groupTracksByHost(tracks); 73 + export async function groupConsult(uris) { 74 + const groups = groupUrisByHost(uris); 75 75 76 76 const promises = Object.entries(groups).map( 77 - async ([_domainId, { host, tracks }]) => { 78 - // Pick one track to test reachability 79 - const testTrack = tracks[0]; 77 + async ([_domainId, { host, uris }]) => { 78 + // Pick one URI to test reachability 79 + const testUri = uris[0]; 80 80 let available = false; 81 81 82 - if (testTrack) { 82 + if (testUri) { 83 83 try { 84 84 const controller = new AbortController(); 85 85 const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout 86 86 87 - const response = await fetch(testTrack.uri, { 87 + const response = await fetch(testUri, { 88 88 method: "HEAD", 89 89 signal: controller.signal, 90 90 }); ··· 98 98 99 99 /** @type {ConsultGrouping} */ 100 100 const grouping = available 101 - ? { available, scheme: SCHEME, tracks } 102 - : { available, reason: "Host unreachable", scheme: SCHEME, tracks }; 101 + ? { available, scheme: SCHEME, uris } 102 + : { available, reason: "Host unreachable", scheme: SCHEME, uris }; 103 103 104 104 return { 105 105 key: await groupKeyHash(SCHEME, host), ··· 120 120 */ 121 121 export async function list(cachedTracks = []) { 122 122 return cachedTracks.map((track) => { 123 - const t = { ...track } 123 + const t = { ...track }; 124 124 125 125 if (t.kind === "placeholder") { 126 - t.kind = undefined 126 + t.kind = undefined; 127 127 } 128 128 129 - return t 129 + return t; 130 130 }); 131 131 } 132 132
+23
src/components/input/opensubsonic/common.js
··· 97 97 } 98 98 99 99 /** 100 + * @param {string[]} uris 101 + */ 102 + export function groupUrisByServer(uris) { 103 + /** @type {Record<string, { server: Server; uris: string[] }>} */ 104 + const acc = {}; 105 + 106 + uris.forEach((uri) => { 107 + const parsed = parseURI(uri); 108 + if (!parsed) return; 109 + 110 + const id = serverId(parsed.server); 111 + 112 + if (acc[id]) { 113 + acc[id].uris.push(uri); 114 + } else { 115 + acc[id] = { server: parsed.server, uris: [uri] }; 116 + } 117 + }); 118 + 119 + return acc; 120 + } 121 + 122 + /** 100 123 * Parse an opensubsonic URI. 101 124 * 102 125 * ```
+6 -5
src/components/input/opensubsonic/worker.js
··· 10 10 consultServer, 11 11 createClient, 12 12 groupTracksByServer, 13 + groupUrisByServer, 13 14 parseURI, 14 15 serverId, 15 16 } from "./common.js"; ··· 65 66 /** 66 67 * @type {Actions['groupConsult']} 67 68 */ 68 - export async function groupConsult(tracks) { 69 - const groups = groupTracksByServer(tracks); 69 + export async function groupConsult(uris) { 70 + const groups = groupUrisByServer(uris); 70 71 71 72 const promises = Object.entries(groups).map( 72 - async ([serverId, { server, tracks }]) => { 73 + async ([serverId, { server, uris }]) => { 73 74 const available = await consultServer(server); 74 75 75 76 /** @type {ConsultGrouping} */ 76 77 const grouping = available 77 - ? { available, scheme: SCHEME, tracks } 78 - : { available, reason: "Server ping failed", scheme: SCHEME, tracks }; 78 + ? { available, scheme: SCHEME, uris } 79 + : { available, reason: "Server ping failed", scheme: SCHEME, uris }; 79 80 80 81 return { 81 82 key: await groupKeyHash(SCHEME, serverId),
+23
src/components/input/s3/common.js
··· 117 117 } 118 118 119 119 /** 120 + * @param {string[]} uris 121 + */ 122 + export function groupUrisByBucket(uris) { 123 + /** @type {Record<string, { bucket: Bucket; uris: string[] }>} */ 124 + const acc = {}; 125 + 126 + uris.forEach((uri) => { 127 + const parsed = parseURI(uri); 128 + if (!parsed) return acc; 129 + 130 + const id = bucketId(parsed.bucket); 131 + 132 + if (acc[id]) { 133 + acc[id].uris.push(uri); 134 + } else { 135 + acc[id] = { bucket: parsed.bucket, uris: [uri] }; 136 + } 137 + }); 138 + 139 + return acc; 140 + } 141 + 142 + /** 120 143 * @returns {Promise<Record<string, Bucket>>} 121 144 */ 122 145 export async function loadBuckets() {
+6 -5
src/components/input/s3/worker.js
··· 10 10 consultBucket, 11 11 createClient, 12 12 groupTracksByBucket, 13 + groupUrisByBucket, 13 14 parseURI, 14 15 } from "./common.js"; 15 16 import { SCHEME } from "./constants.js"; ··· 64 65 /** 65 66 * @type {Actions['groupConsult']} 66 67 */ 67 - export async function groupConsult(tracks) { 68 - const groups = groupTracksByBucket(tracks); 68 + export async function groupConsult(uris) { 69 + const groups = groupUrisByBucket(uris); 69 70 70 71 const promises = Object.entries(groups).map( 71 - async ([bucketId, { bucket, tracks }]) => { 72 + async ([bucketId, { bucket, uris }]) => { 72 73 const available = await consultBucket(bucket); 73 74 74 75 /** @type {ConsultGrouping} */ 75 76 const grouping = available 76 - ? { available, scheme: SCHEME, tracks } 77 - : { available, reason: "Bucket unavailable", scheme: SCHEME, tracks }; 77 + ? { available, scheme: SCHEME, uris } 78 + : { available, reason: "Bucket unavailable", scheme: SCHEME, uris }; 78 79 79 80 return { 80 81 key: await groupKeyHash(SCHEME, bucketId),
+3 -3
src/components/input/types.d.ts
··· 14 14 | { supported: true; consult: "undetermined" | boolean }; 15 15 16 16 export type ConsultGrouping = 17 - | { available: false; reason: string; scheme: string; tracks: Track[] } 18 - | { available: true; scheme: string; tracks: Track[] }; 17 + | { available: false; reason: string; scheme: string; uris: string[] } 18 + | { available: true; scheme: string; uris: string[] }; 19 19 20 20 export type GroupConsult = Record<string, ConsultGrouping>; 21 21 22 22 export type InputActions = { 23 23 consult(fileUriOrScheme: string): Promise<Consult>; 24 24 detach(args: { fileUriOrScheme: string; tracks: Track[] }): Promise<Track[]>; 25 - groupConsult(tracks: Track[]): Promise<GroupConsult>; 25 + groupConsult(uris: string[]): Promise<GroupConsult>; 26 26 list(tracks: Track[]): Promise<Track[]>; 27 27 resolve(args: { method?: string; uri: string }): Promise<ResolvedUri>; 28 28 };
+10 -7
src/components/orchestrator/scoped-tracks/worker.js
··· 1 - import { filterByPlaylist as filterByPlaylistFn } from "@common/playlist.js"; 2 1 import { ostiary, rpc, workerProxy } from "@common/worker.js"; 3 2 4 3 /** ··· 29 28 ports.search.start(); 30 29 31 30 // Consult input 32 - const groups = await input.groupConsult(cachedTracks); 31 + const groups = await input.groupConsult( 32 + cachedTracks.map((t) => t.uri), 33 + ); 33 34 34 - /** @type {Track[]} */ 35 - const availableTracks = []; 35 + /** @type {Set<string>} */ 36 + const availableUris = new Set(); 36 37 37 38 Object.values(groups).forEach((value) => { 38 39 if (value.available === false) return; 39 - for (const track of value.tracks) { 40 - availableTracks.push(track); 40 + for (const uri of value.uris) { 41 + availableUris.add(uri); 41 42 } 42 - }, []); 43 + }); 44 + 45 + const availableTracks = cachedTracks.filter((t) => availableUris.has(t.uri)); 43 46 44 47 // Set pool 45 48 search.supply({ tracks: availableTracks });