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 124 lines 4.4 kB view raw
1/** 2 * @import {Track} from "~/definitions/types.d.ts" 3 */ 4 5/** 6 * Creates a time-cached version of an async consult function. 7 * Results are cached per key for the given TTL. 8 * 9 * @template T 10 * @param {(arg: T) => Promise<boolean>} fn 11 * @param {(arg: T) => string} keyFn 12 * @param {number} ttl - Cache TTL in milliseconds 13 * @returns {(arg: T) => Promise<boolean>} 14 * 15 * @example Caches results and avoids calling fn more than once per key 16 * ```js 17 * import { cachedConsult } from "~/components/input/common.js"; 18 * 19 * let callCount = 0; 20 * const cached = cachedConsult(async (uri) => { callCount++; return true; }, (uri) => uri); 21 * 22 * const r1 = await cached("https://example.com/stream"); 23 * const r2 = await cached("https://example.com/stream"); 24 * 25 * if (r1 !== true || r2 !== true) throw new Error("should return cached value"); 26 * if (callCount !== 1) throw new Error("fn should only be called once per key"); 27 * ``` 28 */ 29export function cachedConsult(fn, keyFn, ttl = 60_000 * 5) { 30 /** @type {Map<string, { value: boolean; expiry: number }>} */ 31 const cache = new Map(); 32 33 return async (arg) => { 34 const key = keyFn(arg); 35 const now = Date.now(); 36 const cached = cache.get(key); 37 38 if (cached && cached.expiry > now) { 39 return cached.value; 40 } 41 42 const value = await fn(arg); 43 cache.set(key, { value, expiry: now + ttl }); 44 return value; 45 }; 46} 47 48/** 49 * @param {{ fileUriOrScheme: string; handleFileUri: (args: { fileURI: string; tracks: Track[] }) => Track[]; inputScheme: string; tracks: Track[] }} _ 50 * 51 * @example Removes all tracks when given a matching scheme, returns all when scheme doesn't match 52 * ```js 53 * import { detach } from "~/components/input/common.js"; 54 * 55 * const tracks = JSON.parse('[{"$type":"sh.diffuse.output.track","id":"1","uri":"https://a.com/1.mp3"},{"$type":"sh.diffuse.output.track","id":"2","uri":"https://b.com/2.mp3"}]'); 56 * 57 * // @ts-ignore 58 * const removed = detach({ fileUriOrScheme: "https", inputScheme: "https", handleFileUri: () => [], tracks }); 59 * if (removed.length !== 0) throw new Error("matching scheme should remove all tracks"); 60 * 61 * // @ts-ignore 62 * const kept = detach({ fileUriOrScheme: "ftp", inputScheme: "https", handleFileUri: () => [], tracks }); 63 * if (kept.length !== 2) throw new Error("non-matching scheme should keep all tracks"); 64 * ``` 65 * 66 * @example Delegates to handleFileUri when a full URI is given 67 * ```js 68 * import { detach } from "~/components/input/common.js"; 69 * 70 * const tracks = JSON.parse('[{"$type":"sh.diffuse.output.track","id":"1","uri":"https://a.com/1.mp3"},{"$type":"sh.diffuse.output.track","id":"2","uri":"https://b.com/2.mp3"}]'); 71 * 72 * // @ts-ignore 73 * const result = detach({ fileUriOrScheme: "https://a.com/1.mp3", inputScheme: "https", handleFileUri: ({ tracks }) => tracks.filter((t) => t.id !== "1"), tracks }); 74 * if (result.length !== 1 || result[0].id !== "2") throw new Error("handleFileUri should filter by URI"); 75 * ``` 76 */ 77export function detach( 78 { fileUriOrScheme, handleFileUri, inputScheme, tracks }, 79) { 80 if (!fileUriOrScheme.includes("://")) { 81 // Delete everything if scheme matches 82 if (fileUriOrScheme === inputScheme) return []; 83 return tracks; 84 } 85 86 return handleFileUri({ fileURI: fileUriOrScheme, tracks }); 87} 88 89/** 90 * @param {string} scheme 91 * @param {string} groupId 92 * 93 * @example Returns scheme://groupId 94 * ```js 95 * import { groupKey } from "~/components/input/common.js"; 96 * 97 * if (groupKey("https", "example.com") !== "https://example.com") throw new Error(`expected "https://example.com"`); 98 * ``` 99 */ 100export function groupKey(scheme, groupId) { 101 return `${scheme}://${groupId}`; 102} 103 104/** 105 * @param {string} filename 106 * 107 * @example Returns truthy for audio extensions and falsy for non-audio ones 108 * ```js 109 * import { isAudioFile } from "~/components/input/common.js"; 110 * 111 * const audioExts = ["track.mp3", "track.flac", "track.ogg", "track.opus", "track.wav", "track.m4a", "track.webm"]; 112 * for (const f of audioExts) { 113 * if (!isAudioFile(f)) throw new Error(`${f} should be recognised as audio`); 114 * } 115 * 116 * const nonAudio = ["track.txt", "track.jpg", "track.pdf", "track"]; 117 * for (const f of nonAudio) { 118 * if (isAudioFile(f)) throw new Error(`${f} should not be recognised as audio`); 119 * } 120 * ``` 121 */ 122export function isAudioFile(filename) { 123 return filename.match(/\.(flac|m4a|mp3|mp4|ogg|opus|wav|webm)$/); 124}