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: put more info in track tags + webamp theme improvements

+258 -77
+68 -1
src/components/core/types.d.ts
··· 71 71 } 72 72 73 73 export interface TrackStats { 74 + /** Album gain in dB */ 75 + albumGain?: number; 76 + 77 + /** Bits per second */ 74 78 bitrate?: number; 79 + 80 + /** Bit depth */ 81 + bitsPerSample?: number; 82 + 83 + /** Compression algorithm used */ 84 + codec?: string; 85 + 86 + /** Encoding format used */ 87 + container?: string; 88 + 89 + /** Duration in seconds */ 75 90 duration?: number; 91 + 92 + /** Is track lossless? */ 93 + lossless?: boolean; 94 + 95 + /** Number of audio channels */ 96 + numberOfChannels?: number; 97 + 98 + /** Samples per second */ 99 + sampleRate?: number; 100 + 101 + /** Track gain in dB */ 102 + trackGain?: number; 76 103 } 77 104 78 105 export interface TrackTags { 79 106 album?: string; 107 + albumartist?: string; 108 + albumartists?: string[]; 109 + albumartistsort?: string; 110 + albumsort?: string; 111 + arranger?: string[]; 80 112 artist?: string; 113 + artists?: string[]; 114 + artistsort?: string; 115 + asin?: string; 116 + averageLevel?: number; 117 + barcode?: string; 118 + bpm?: number; 119 + catalognumbers?: string[]; 120 + compilation?: boolean; 121 + composers?: string[]; 122 + composersort?: string; 123 + conductors?: string[]; 124 + date?: string; 81 125 disc: { no: number; of?: number }; 82 - genre?: string; 126 + djmixers?: string[]; 127 + engineers?: string[]; 128 + gapless?: boolean; 129 + genres?: string[]; 130 + isrc?: string[]; 131 + labels?: string[]; 132 + lyricists?: string[]; 133 + media?: string; 134 + mixers?: string[]; 135 + moods?: string[]; 136 + originaldate?: string; 137 + originalyear?: number; 138 + peakLevel?: number; 139 + producers?: string[]; 140 + publishers?: string[]; 141 + releasecountry?: string; 142 + releasedate?: string; 143 + releasestatus?: string; 144 + releasetypes?: string[]; 145 + remixers?: string[]; 146 + technicians?: string[]; 83 147 title: string; 148 + titlesort?: string; 84 149 track: { no: number; of?: number }; 150 + work?: string; 151 + writers?: string[]; 85 152 year?: number; 86 153 }
+8 -1
src/components/engine/queue/element.js
··· 1 + import QS from "query-string"; 2 + 1 3 import { DiffuseElement } from "@common/element.js"; 2 4 import { signal } from "@common/signal.js"; 3 5 import { listen, use } from "@common/worker.js"; ··· 17 19 constructor() { 18 20 super(); 19 21 22 + // Query 23 + const query = QS.stringify({ 24 + "fill-size": this.getAttribute("fill-size"), 25 + }); 26 + 20 27 // Setup worker 21 28 const name = `diffuse/engine/queue/${this.group}`; 22 - const url = "/components/engine/queue/worker.js"; 29 + const url = `/components/engine/queue/worker.js?${query}`; 23 30 24 31 let port; 25 32
+22 -3
src/components/engine/queue/worker.js
··· 1 + import QS from "query-string"; 2 + 1 3 import { announce, define, ostiary } from "@common/worker.js"; 2 4 import { effect, signal } from "@common/signal.js"; 3 5 import { arrayShuffle } from "@common/index.js"; ··· 7 9 * @import {Track} from "@components/core/types.d.ts" 8 10 */ 9 11 10 - const QUEUE_SIZE = 25; 12 + const QUERY = QS.parse(location.search); 13 + const qFillSize = QUERY?.["fill-size"]; 14 + 15 + /** @type {number} */ 16 + const FILL_SIZE = qFillSize && qFillSize !== null 17 + ? Array.isArray(qFillSize) && qFillSize[0] !== null 18 + ? parseInt(qFillSize[0], 10) 19 + : parseInt(/** @type {string} */ (qFillSize), 10) 20 + : 25; 11 21 12 22 //////////////////////////////////////////// 13 23 // STATE ··· 26 36 * @type {Actions['add']} 27 37 */ 28 38 export function add({ inFront, items }) { 39 + // TODO: An entry is always manual and should be added in the correct place 29 40 $future.value = inFront 30 41 ? [...items, ...$future.value] 31 42 : [...$future.value, ...items]; ··· 100 111 * @returns {Item[]} 101 112 */ 102 113 function fill(future) { 103 - if (future.length >= QUEUE_SIZE) return future; 114 + let fillFutureCount = 0; 115 + let manualFutureCount = 0; 116 + 117 + future.forEach((item) => { 118 + if (item.manualEntry) manualFutureCount++; 119 + else fillFutureCount++; 120 + }); 121 + 122 + if (fillFutureCount >= FILL_SIZE) return future; 104 123 105 124 /** @type {Item[]} */ 106 125 const pool = []; ··· 125 144 126 145 const poolSelection = arrayShuffle(reducedPool).slice( 127 146 0, 128 - QUEUE_SIZE - future.length, 147 + FILL_SIZE - fillFutureCount, 129 148 ); 130 149 131 150 return [...future, ...poolSelection];
+61 -6
src/components/input/opensubsonic/worker.js
··· 153 153 uri: buildURI(server, { songId: song.id, path }), 154 154 155 155 stats: { 156 - bitrate: song.bitRate, 156 + albumGain: undefined, 157 + bitrate: song.bitRate ? song.bitRate * 1000 : undefined, 158 + bitsPerSample: undefined, 159 + codec: undefined, 160 + container: undefined, 157 161 duration: song.duration, 162 + lossless: undefined, 163 + numberOfChannels: undefined, 164 + sampleRate: undefined, 165 + trackGain: undefined, 158 166 }, 159 167 tags: { 160 168 album: song.album, 161 - artist: song.artist, 162 - disc: { no: song.discNumber || 1 }, 163 - genre: song.genre, 164 - title: song.title, 165 - track: { no: song.track || 1 }, 169 + albumartist: song.albumArtists?.[0]?.name, 170 + albumartists: song.albumArtists?.map((a) => a.name), 171 + albumartistsort: song.albumArtists?.[0]?.sortName, 172 + albumsort: undefined, 173 + arranger: undefined, 174 + artist: song.artist ?? song.displayArtist, 175 + artists: undefined, 176 + artistsort: undefined, 177 + asin: undefined, 178 + averageLevel: undefined, 179 + barcode: undefined, 180 + bpm: song.bpm, 181 + catalognumbers: undefined, 182 + compilation: undefined, 183 + composers: song.displayComposer 184 + ? [song.displayComposer] 185 + : undefined, 186 + composersort: undefined, 187 + conductors: undefined, 188 + date: undefined, 189 + disc: { 190 + no: song.discNumber || 1, 191 + }, 192 + djmixers: undefined, 193 + engineers: undefined, 194 + gapless: undefined, 195 + genres: song.genres, 196 + isrc: undefined, 197 + labels: undefined, 198 + lyricists: undefined, 199 + media: undefined, 200 + mixers: undefined, 201 + moods: song.moods, 202 + originaldate: undefined, 203 + originalyear: undefined, 204 + peakLevel: undefined, 205 + producers: undefined, 206 + publishers: undefined, 207 + releasecountry: undefined, 208 + releasedate: undefined, 209 + releasestatus: undefined, 210 + releasetypes: undefined, 211 + remixers: undefined, 212 + technicians: undefined, 213 + title: song.title ?? "Unknown", 214 + titlesort: undefined, 215 + track: { 216 + no: song.track ?? 1, 217 + of: song.size, 218 + }, 219 + work: undefined, 220 + writers: undefined, 166 221 year: song.year, 167 222 }, 168 223 };
+4 -1
src/components/orchestrator/queue-tracks/element.js
··· 41 41 42 42 // Watch tracks collection 43 43 this.effect(() => { 44 - const tracks = this.output.tracks.collection(); 44 + const tracks = this.output.tracks.collection().filter((t) => 45 + t.kind !== "placeholder" 46 + ); 47 + 45 48 untracked(() => this.poolAvailable(tracks)); 46 49 }); 47 50 }
+4 -1
src/components/orchestrator/search-tracks/element.js
··· 40 40 41 41 // Watch tracks collection 42 42 this.effect(() => { 43 - const tracks = this.output.tracks.collection(); 43 + const tracks = this.output.tracks.collection().filter((t) => 44 + t.kind !== "placeholder" 45 + ); 46 + 44 47 this.supplyAvailable(tracks); 45 48 }); 46 49 }
+54 -3
src/components/processor/metadata/common.js
··· 55 55 56 56 /** @type {TrackStats} */ 57 57 const stats = { 58 + albumGain: meta.format.albumGain, 59 + bitrate: meta.format.bitrate, 60 + bitsPerSample: meta.format.bitsPerSample, 61 + codec: meta.format.codec, 62 + container: meta.format.container, 58 63 duration: meta.format.duration, 64 + lossless: meta.format.lossless, 65 + numberOfChannels: meta.format.numberOfChannels, 66 + sampleRate: meta.format.sampleRate, 67 + trackGain: meta.format.trackGain, 59 68 }; 60 69 61 70 /** @type {TrackTags} */ 62 71 const tags = { 63 72 album: meta.common.album, 73 + albumartist: meta.common.albumartist, 74 + albumartists: meta.common.albumartists ?? 75 + (meta.common.albumartist ? [meta.common.albumartist] : []), 76 + albumartistsort: meta.common.albumartistsort, 77 + albumsort: meta.common.albumsort, 78 + arranger: meta.common.arranger, 64 79 artist: meta.common.artist, 80 + artists: meta.common.artists ?? 81 + (meta.common.artist ? [meta.common.artist] : []), 82 + artistsort: meta.common.artistsort, 83 + asin: meta.common.asin, 84 + averageLevel: meta.common.averageLevel, 85 + barcode: meta.common.barcode, 86 + bpm: meta.common.bpm, 87 + catalognumbers: meta.common.catalognumber, 88 + compilation: meta.common.compilation, 89 + composers: meta.common.composer, 90 + composersort: meta.common.composersort, 91 + conductors: meta.common.conductor, 92 + date: meta.common.date, 65 93 disc: { 66 94 no: meta.common.disk.no || 1, 67 95 of: meta.common.disk.of ?? undefined, 68 96 }, 69 - genre: Array.isArray(meta.common.genre) 70 - ? meta.common.genre[0] 71 - : meta.common.genre, 97 + djmixers: meta.common.djmixer, 98 + engineers: meta.common.engineer, 99 + gapless: meta.common.gapless, 100 + genres: Array.isArray(meta.common.genre) 101 + ? meta.common.genre 102 + : [meta.common.genre], 103 + isrc: meta.common.isrc, 104 + labels: meta.common.label, 105 + lyricists: meta.common.lyricist, 106 + media: meta.common.media, 107 + mixers: meta.common.mixer, 108 + moods: meta.common.mood, 109 + originaldate: meta.common.originaldate, 110 + originalyear: meta.common.originalyear, 111 + peakLevel: meta.common.peakLevel, 112 + producers: meta.common.producer, 113 + publishers: meta.common.publisher, 114 + releasecountry: meta.common.releasecountry, 115 + releasedate: meta.common.releasedate, 116 + releasestatus: meta.common.releasestatus, 117 + releasetypes: meta.common.releasetype, 118 + remixers: meta.common.remixer, 119 + technicians: meta.common.technician, 72 120 title: meta.common.title || filename || urls?.head || "Unknown", 121 + titlesort: meta.common.titlesort, 73 122 track: { 74 123 no: meta.common.track.no || 1, 75 124 of: meta.common.track.of ?? undefined, 76 125 }, 126 + work: meta.common.work, 127 + writers: meta.common.writer, 77 128 year: meta.common.year, 78 129 }; 79 130
+6 -13
src/themes/webamp/browser/element.js
··· 20 20 21 21 /** @type {import("@components/engine/queue/element.js").CLASS} */ 22 22 this.queue = query(this, "queue-selector"); 23 - 24 - /** @type {import("../webamp.js").CLASS} */ 25 - this.amp = query(this, "webamp-selector"); 26 23 } 27 24 28 25 // LIFECYCLE ··· 67 64 * @param {Track} track 68 65 */ 69 66 playTrack(track) { 70 - console.log("Play track", track); 71 - // this.queue.add({ 72 - // inFront: true, 73 - // items: [ 74 - // { ...track, manualEntry: true }, 75 - // ], 76 - // }); 77 - 78 - this.amp.addTrack(track); 79 - this.amp.amp.setCurrentTrack(this.amp.amp.getPlaylistTracks().length - 1); 67 + this.queue.add({ 68 + inFront: true, 69 + items: [ 70 + { ...track, manualEntry: true }, 71 + ], 72 + }); 80 73 } 81 74 82 75 // RENDER
+27 -40
src/themes/webamp/index.js
··· 1 - import deepDiff from "@fry69/deep-diff"; 2 - 3 1 // import "@components/orchestrator/process-tracks/element.js"; 4 2 import "@components/orchestrator/queue-tracks/element.js"; 5 3 import "@components/output/indexed-db/element.js"; ··· 14 12 import "./browser/element.js"; 15 13 import "./window/element.js"; 16 14 import "./window-manager/element.js"; 17 - import WebampElement from "./webamp.js"; 18 - import { xxh32 } from "xxh32"; 19 - 20 - /** 21 - * @import {URLTrack} from "webamp" 22 - * 23 - * @import {Item} from "@components/engine/queue/types.d.ts" 24 - */ 15 + import WebampElement from "./webamp/element.js"; 25 16 26 17 const input = component(Input); 27 18 const queue = component(Queue); ··· 32 23 // 📡 33 24 //////////////////////////////////////////// 34 25 35 - let currBase = 0; 26 + const currBase = 0; 36 27 37 28 const $currTrack = signal(/** @type {null | number} */ (null)); 38 - const $playlist = signal(/** @type {Item[]} */ ([])); 29 + const $playlist = signal(/** @type {Set<string>} */ (new Set()), { 30 + eager: true, 31 + }); 39 32 40 33 //////////////////////////////////////////// 41 34 // ⚡️ ··· 91 84 ...future, 92 85 ]; 93 86 94 - const hashNew = xxh32(JSON.stringify(playlist.map((i) => i.id))); 95 - const hashOld = xxh32( 96 - JSON.stringify(untracked($playlist.get).map((i) => i.id)), 97 - ); 87 + const oldSet = untracked($playlist.get); 88 + const newSet = new Set(playlist.map((i) => i.id)); 89 + 90 + const addedItems = newSet.difference(oldSet); 91 + 92 + // TODO: Can't do removals yet without resetting the webamp instance. 93 + // const removedItems = oldSet.difference(newSet); 94 + 95 + if (addedItems.size === 0) return; 98 96 99 - if (hashNew === hashOld) return; 97 + playlist.forEach((item, idx) => { 98 + if (addedItems.has(item.id) === false) return; 100 99 101 - const webampTracks = playlist.map((item, idx) => { 102 - /** @type {URLTrack} */ 103 - const urlTrack = { 104 - url: item.uri, 105 - metaData: { 106 - title: item.tags?.title || "", 107 - artist: item.tags?.artist || "", 108 - album: item.tags?.album, 109 - }, 110 - duration: item.stats?.duration, 111 - }; 100 + // TODO 101 + if (item.stats?.duration == undefined) return; 112 102 113 - if (item.stats?.duration == undefined) { 114 - throw new Error("TODO: Fetch duration"); 115 - } 116 - return urlTrack; 103 + // TODO: Inserting at a specific index doesn't work 104 + ampElement.addTrack(item); 117 105 }); 118 106 119 - // currBase = currBase + amp.getPlaylistTracks().length; 120 - // amp.setCurrentTrack(currBase + (untracked($currTrack.get) ?? 0)); 107 + if (untracked($currTrack.get) === null) { 108 + amp.setCurrentTrack(past.length); 109 + } 121 110 122 - $playlist.value = playlist; 111 + $playlist.value = newSet; 123 112 }); 124 113 125 114 /** ··· 127 116 * reflect the change in our queue too. 128 117 */ 129 118 effect(() => { 130 - console.log("CURR", $currTrack.value); 131 - 132 - // if (($currTrack.value ?? 0) > untracked(queue.past).length) { 133 - // queue.shift(); 134 - // } 119 + if (($currTrack.value ?? 0) > untracked(queue.past).length) { 120 + queue.shift(); 121 + } 135 122 }); 136 123 137 124 ////////////////////////////////////////////
+1 -2
src/themes/webamp/index.vto
··· 33 33 input-selector="di-opensubsonic" 34 34 output-selector="do-indexed-db" 35 35 queue-selector="de-queue" 36 - webamp-selector="dtw-webamp" 37 36 ></dtw-browser> 38 37 </dtw-window> 39 38 </dtw-window-manager> ··· 71 70 COMPONENTS 72 71 73 72 --> 74 - <de-queue></de-queue> 73 + <de-queue fill-size="5"></de-queue> 75 74 76 75 <!-- Inputs, Outputs & Processors --> 77 76 <di-opensubsonic></di-opensubsonic>
+3 -6
src/themes/webamp/webamp.js src/themes/webamp/webamp/element.js
··· 75 75 artist: track.tags?.artist, 76 76 title: track.tags?.title, 77 77 album: track.tags?.album, 78 - // For now, we lie about these next three things. 79 - // TODO: Ideally we would leave these as null and force a media data 80 - // fetch when the user starts playing. 81 - sampleRate: 44000, 82 - bitrate: 192000, 83 - numberOfChannels: 2, 78 + sampleRate: track.stats?.sampleRate ?? 44000, 79 + bitrate: track.stats?.bitrate ?? 192000, 80 + numberOfChannels: 2, // TODO 84 81 id: idx, 85 82 }); 86 83 },