A music player that connects to your cloud/distributed storage.
at v4 173 lines 3.9 kB view raw
1import * as IDB from "idb-keyval"; 2import * as URI from "fast-uri"; 3 4import { isAudioFile } from "~/components/input/common.js"; 5import { safeDecodeURIComponent } from "~/common/utils.js"; 6import { IDB_HANDLES, SCHEME } from "./constants.js"; 7 8/** 9 * @import { Track } from "~/definitions/types.d.ts" 10 */ 11 12//////////////////////////////////////////// 13// 🛠️ 14//////////////////////////////////////////// 15 16/** 17 * @param {string} tid 18 * @param {string} [path] 19 */ 20export function buildURI(tid, path = "/") { 21 return URI.serialize({ 22 scheme: SCHEME, 23 host: tid, 24 path, 25 }); 26} 27 28/** 29 * @param {FileSystemDirectoryHandle} dirHandle 30 * @param {string} [basePath] 31 * @returns {Promise<string[]>} 32 */ 33export async function enumerateAudioFiles(dirHandle, basePath = "/") { 34 const results = []; 35 36 for await (const [name, handle] of /** @type {any} */ (dirHandle).entries()) { 37 const entryPath = basePath + name; 38 39 if (handle.kind === "directory") { 40 const sub = await enumerateAudioFiles( 41 /** @type {FileSystemDirectoryHandle} */ (handle), 42 entryPath + "/", 43 ); 44 results.push(...sub); 45 } else if (isAudioFile(name)) { 46 results.push(entryPath); 47 } 48 } 49 50 return results; 51} 52 53/** 54 * @param {FileSystemHandle} handle 55 * @param {string} path 56 * @returns {Promise<FileSystemFileHandle>} 57 */ 58export async function getHandleFile(handle, path) { 59 if (handle.kind === "file") { 60 return /** @type {FileSystemFileHandle} */ (handle); 61 } 62 63 const parts = path.replace(/^\//, "").split("/").filter(Boolean); 64 let current = /** @type {FileSystemDirectoryHandle} */ (handle); 65 66 for (const part of parts.slice(0, -1)) { 67 current = await current.getDirectoryHandle(part); 68 } 69 70 return current.getFileHandle(/** @type {string} */ (parts.at(-1))); 71} 72 73/** 74 * @param {Track[]} tracks 75 * @returns {Record<string, { tid: string; tracks: Track[] }>} 76 */ 77export function groupTracksByTid(tracks) { 78 /** @type {Record<string, { tid: string; tracks: Track[] }>} */ 79 const acc = {}; 80 81 tracks.forEach((track) => { 82 const parsed = parseURI(track.uri); 83 if (!parsed) return; 84 85 const { tid } = parsed; 86 if (acc[tid]) { 87 acc[tid].tracks.push(track); 88 } else { 89 acc[tid] = { tid, tracks: [track] }; 90 } 91 }); 92 93 return acc; 94} 95 96/** 97 * @param {string[]} uris 98 * @returns {Record<string, { tid: string; uris: string[] }>} 99 */ 100export function groupUrisByTid(uris) { 101 /** @type {Record<string, { tid: string; uris: string[] }>} */ 102 const acc = {}; 103 104 uris.forEach((uri) => { 105 const parsed = parseURI(uri); 106 if (!parsed) return; 107 108 const { tid } = parsed; 109 if (acc[tid]) { 110 acc[tid].uris.push(uri); 111 } else { 112 acc[tid] = { tid, uris: [uri] }; 113 } 114 }); 115 116 return acc; 117} 118 119export function isSupported() { 120 return typeof (/** @type {any} */ (globalThis).showDirectoryPicker) !== 121 "undefined"; 122} 123 124/** 125 * @returns {Promise<Record<string, FileSystemHandle>>} 126 */ 127export async function loadHandles() { 128 const i = await IDB.get(IDB_HANDLES); 129 return i ?? {}; 130} 131 132/** 133 * @param {string} uriString 134 * @returns {{ tid: string; path: string } | undefined} 135 */ 136export function parseURI(uriString) { 137 try { 138 const url = new URL(uriString); 139 if (url.protocol !== `${SCHEME}:`) return undefined; 140 if (!url.host) return undefined; 141 142 return { 143 tid: url.host, 144 path: safeDecodeURIComponent(url.pathname), 145 }; 146 } catch { 147 return undefined; 148 } 149} 150 151/** 152 * @param {Record<string, FileSystemHandle>} handles 153 */ 154export async function saveHandles(handles) { 155 await IDB.set(IDB_HANDLES, handles); 156} 157 158/** 159 * @param {Track[]} tracks 160 * @returns {Record<string, string>} 161 */ 162export function tidsFromTracks(tracks) { 163 /** @type {Record<string, string>} */ 164 const acc = {}; 165 166 tracks.forEach((track) => { 167 const parsed = parseURI(track.uri); 168 if (!parsed) return; 169 acc[parsed.tid] = parsed.tid; 170 }); 171 172 return acc; 173}