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 185 lines 5.1 kB view raw
1import { defineElement, DiffuseElement, query } from "~/common/element.js"; 2import { computed, signal } from "~/common/signal.js"; 3 4/** 5 * @import {SignalReader} from "~/common/signal.d.ts" 6 * @import {Track} from "~/definitions/types.d.ts" 7 */ 8 9//////////////////////////////////////////// 10// ELEMENT 11//////////////////////////////////////////// 12 13class CoverGroupsOrchestrator extends DiffuseElement { 14 static NAME = "diffuse/orchestrator/cover-groups"; 15 16 // SIGNALS 17 18 #provider = signal( 19 /** @type {DiffuseElement & { tracks: SignalReader<Track[]> } | null} */ (null), 20 ); 21 22 // STATE 23 24 artistGroups = computed(() => { 25 const provider = this.#provider.value; 26 const groups = /** @type {any} */ (provider)?.groups?.(); 27 28 /** @type {{ label: string; groups: ArtistGroup[] }[]} */ 29 const result = []; 30 31 if (groups?.length) { 32 const allTracks = provider?.tracks() ?? []; 33 34 // Total track counts per artist across all groups 35 /** @type {Map<string, number>} */ 36 const totalCounts = new Map(); 37 for (const track of allTracks) { 38 const key = String(track.tags?.artist ?? "").toLowerCase(); 39 totalCounts.set(key, (totalCounts.get(key) ?? 0) + 1); 40 } 41 42 for ( 43 const group 44 of /** @type {{ label: string; tracks: Track[] }[]} */ (groups) 45 ) { 46 const artists = deduplicateArtists(group.tracks).map((a) => ({ 47 ...a, 48 trackCount: totalCounts.get(a.artistKey) ?? a.trackCount, 49 })); 50 if (artists.length) result.push({ label: group.label, groups: artists }); 51 } 52 } else { 53 const allTracks = provider?.tracks() ?? []; 54 const artists = deduplicateArtists(allTracks); 55 if (artists.length) result.push({ label: "", groups: artists }); 56 } 57 58 return result; 59 }); 60 61 coverGroups = computed(() => { 62 const provider = this.#provider.value; 63 const groups = /** @type {any} */ (provider)?.groups?.(); 64 65 /** @type {{ label: string; groups: CoverGroup[] }[]} */ 66 const result = []; 67 68 if (groups?.length) { 69 for ( 70 const group 71 of /** @type {{ label: string; tracks: Track[] }[]} */ (groups) 72 ) { 73 const albums = deduplicateAlbums(group.tracks); 74 if (albums.length) result.push({ label: group.label, groups: albums }); 75 } 76 } else { 77 const tracks = provider?.tracks() ?? []; 78 const albums = deduplicateAlbums(tracks); 79 if (albums.length) result.push({ label: "", groups: albums }); 80 } 81 82 return result; 83 }); 84 85 // LIFECYCLE 86 87 /** 88 * @override 89 */ 90 async connectedCallback() { 91 super.connectedCallback(); 92 93 /** @type {DiffuseElement & { tracks: SignalReader<Track[]> }} */ 94 const provider = query(this, "tracks-selector"); 95 96 await customElements.whenDefined(provider.localName); 97 this.#provider.value = provider; 98 } 99} 100 101export default CoverGroupsOrchestrator; 102 103//////////////////////////////////////////// 104// HELPERS 105//////////////////////////////////////////// 106 107/** 108 * @typedef {{ albumKey: string; albumName: string; artist: string; track: Track }} CoverGroup 109 */ 110 111/** 112 * @typedef {{ artistKey: string; artistName: string; trackCount: number; track: Track }} ArtistGroup 113 */ 114 115/** 116 * @param {Track[]} tracks 117 * @returns {CoverGroup[]} 118 */ 119function deduplicateAlbums(tracks) { 120 /** @type {Map<string, { track: Track; artists: Set<string> }>} */ 121 const albumMap = new Map(); 122 123 for (const track of tracks) { 124 const albumKey = String(track.tags?.album ?? "").toLowerCase(); 125 const existing = albumMap.get(albumKey); 126 if (existing) { 127 existing.artists.add(track.tags?.artist ?? "Unknown artist"); 128 } else { 129 albumMap.set(albumKey, { 130 track, 131 artists: new Set([track.tags?.artist ?? "Unknown artist"]), 132 }); 133 } 134 } 135 136 return [...albumMap.entries()] 137 .sort(([a], [b]) => a.localeCompare(b)) 138 .map(([albumKey, { track, artists }]) => ({ 139 albumKey, 140 albumName: track.tags?.album ?? "Unknown album", 141 artist: artists.size > 1 ? "Various Artists" : /** @type {string} */ (artists.values().next().value), 142 track, 143 })); 144} 145 146/** 147 * @param {Track[]} tracks 148 * @returns {ArtistGroup[]} 149 */ 150function deduplicateArtists(tracks) { 151 /** @type {Map<string, { artistName: string; count: number; track: Track }>} */ 152 const map = new Map(); 153 154 for (const track of tracks) { 155 const artistKey = String(track.tags?.artist ?? "").toLowerCase(); 156 const existing = map.get(artistKey); 157 if (existing) { 158 existing.count++; 159 } else { 160 map.set(artistKey, { 161 artistName: track.tags?.artist ?? "Unknown artist", 162 count: 1, 163 track, 164 }); 165 } 166 } 167 168 return [...map.entries()] 169 .sort(([a], [b]) => a.localeCompare(b)) 170 .map(([artistKey, { artistName, count, track }]) => ({ 171 artistKey, 172 artistName, 173 trackCount: count, 174 track, 175 })); 176} 177 178//////////////////////////////////////////// 179// REGISTER 180//////////////////////////////////////////// 181 182export const CLASS = CoverGroupsOrchestrator; 183export const NAME = "do-cover-groups"; 184 185defineElement(NAME, CLASS);