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 248 lines 6.0 kB view raw
1import * as TID from "@atcute/tid"; 2import { ostiary, rpc } from "~/common/worker.js"; 3import { groupKey } from "~/components/input/common.js"; 4import { 5 buildURI, 6 enumerateAudioFiles, 7 getHandleFile, 8 groupTracksByTid, 9 groupUrisByTid, 10 isSupported, 11 loadHandles, 12 parseURI, 13 saveHandles, 14} from "./common.js"; 15import { SCHEME } from "./constants.js"; 16 17/** 18 * @import { InputActions as Actions, ConsultGrouping } from "~/components/input/types.d.ts"; 19 * @import { Track } from "~/definitions/types.d.ts" 20 */ 21 22//////////////////////////////////////////// 23// ACTIONS 24//////////////////////////////////////////// 25 26/** 27 * @type {Actions['artwork']} 28 */ 29export async function artwork(_uri) { 30 return null; 31} 32 33/** 34 * @type {Actions['consult']} 35 */ 36export async function consult(fileUriOrScheme) { 37 if (!isSupported()) { 38 return { supported: false, reason: "No browser support" }; 39 } 40 41 if (!fileUriOrScheme.includes(":")) { 42 return { supported: true, consult: "undetermined" }; 43 } 44 45 const parsed = parseURI(fileUriOrScheme); 46 if (!parsed) return { supported: false, reason: "Unknown handle" }; 47 48 const handles = await loadHandles(); 49 const handle = handles[parsed.tid]; 50 51 if (!handle) return { supported: false, reason: "Unknown handle" }; 52 53 const permission = await /** @type {any} */ (handle).queryPermission({ 54 mode: "read", 55 }); 56 57 return { supported: true, consult: permission === "granted" }; 58} 59 60/** 61 * @type {Actions['detach']} 62 */ 63export async function detach({ fileUriOrScheme, tracks }) { 64 if (!fileUriOrScheme.includes("://")) { 65 if (fileUriOrScheme === SCHEME) return []; 66 return tracks; 67 } 68 69 const parsed = parseURI(fileUriOrScheme); 70 if (!parsed) return tracks; 71 72 const { tid } = parsed; 73 const groups = groupTracksByTid(tracks); 74 delete groups[tid]; 75 76 const handles = await loadHandles(); 77 delete handles[tid]; 78 await saveHandles(handles); 79 80 return Object.values(groups).map((g) => g.tracks).flat(1); 81} 82 83/** 84 * @type {Actions['groupConsult']} 85 */ 86export async function groupConsult(uris) { 87 const groups = groupUrisByTid(uris); 88 const handles = await loadHandles(); 89 90 const promises = Object.entries(groups).flatMap(async ([tid, { uris }]) => { 91 const handle = handles[tid]; 92 if (!handle) return []; 93 94 const available = 95 (await /** @type {any} */ (handle).queryPermission({ mode: "read" })) === 96 "granted"; 97 98 /** @type {ConsultGrouping} */ 99 const grouping = available ? { available, scheme: SCHEME, uris } : { 100 available: false, 101 reason: "Permission not granted", 102 scheme: SCHEME, 103 uris, 104 }; 105 106 return [{ key: groupKey(SCHEME, tid), grouping }]; 107 }); 108 109 const results = (await Promise.all(promises)).flat(1); 110 return Object.fromEntries(results.map((e) => [e.key, e.grouping])); 111} 112 113/** 114 * @type {Actions['list']} 115 */ 116export async function list(cachedTracks = []) { 117 const handles = await loadHandles(); 118 const now = new Date().toISOString(); 119 120 /** @type {Record<string, Track>} */ 121 const cacheByUri = {}; 122 123 cachedTracks.forEach((t) => { 124 cacheByUri[t.uri] = t; 125 }); 126 127 const trackGroups = groupTracksByTid(cachedTracks); 128 129 const allTids = new Set(Object.keys(trackGroups)); 130 131 const promises = [...allTids].map(async (tid) => { 132 const handle = handles[tid]; 133 if (!handle) return trackGroups[tid]?.tracks ?? /** @type {Track[]} */ ([]); 134 135 const perm = await /** @type {any} */ (handle).queryPermission({ 136 mode: "read", 137 }); 138 139 if (perm !== "granted") { 140 const cached = trackGroups[tid]?.tracks[0]; 141 142 /** @type {Track} */ 143 const placeholder = { 144 $type: "sh.diffuse.output.track", 145 id: cached?.id ?? TID.now(), 146 createdAt: cached?.createdAt ?? now, 147 updatedAt: now, 148 kind: "placeholder", 149 uri: buildURI(tid), 150 }; 151 152 return [placeholder]; 153 } 154 155 if (handle.kind === "file") { 156 const uri = buildURI(tid); 157 const cached = cacheByUri[uri]; 158 159 /** @type {Track} */ 160 const track = { 161 $type: "sh.diffuse.output.track", 162 id: cached?.id ?? TID.now(), 163 createdAt: cached?.createdAt ?? now, 164 updatedAt: cached?.updatedAt ?? now, 165 stats: cached?.stats, 166 tags: cached?.tags, 167 uri, 168 }; 169 170 return [track]; 171 } 172 173 const paths = await enumerateAudioFiles( 174 /** @type {FileSystemDirectoryHandle} */ (handle), 175 ); 176 177 if (!paths.length) { 178 /** @type {Track} */ 179 const placeholder = { 180 $type: "sh.diffuse.output.track", 181 id: TID.now(), 182 createdAt: now, 183 updatedAt: now, 184 kind: "placeholder", 185 uri: buildURI(tid), 186 }; 187 188 return [placeholder]; 189 } 190 191 return paths.map((path) => { 192 const uri = buildURI(tid, path); 193 const cached = cacheByUri[uri]; 194 195 /** @type {Track} */ 196 const track = { 197 $type: "sh.diffuse.output.track", 198 id: cached?.id ?? TID.now(), 199 createdAt: cached?.createdAt ?? now, 200 updatedAt: cached?.updatedAt ?? now, 201 stats: cached?.stats, 202 tags: cached?.tags, 203 uri, 204 }; 205 206 return track; 207 }); 208 }); 209 210 const tracks = (await Promise.all(promises)).flat(1); 211 return tracks; 212} 213 214/** 215 * @type {Actions['resolve']} 216 */ 217export async function resolve({ uri }) { 218 const parsed = parseURI(uri); 219 if (!parsed) return undefined; 220 221 const handles = await loadHandles(); 222 const handle = handles[parsed.tid]; 223 const path = parsed.path.replace(/^\//, ""); 224 225 if (!handle) return undefined; 226 if (handle.kind === "directory" && path === "") return undefined; 227 228 const fileHandle = await getHandleFile(handle, path); 229 const file = await fileHandle.getFile(); 230 231 const url = URL.createObjectURL(file); 232 return { url, expiresAt: Infinity }; 233} 234 235//////////////////////////////////////////// 236// ⚡️ 237//////////////////////////////////////////// 238 239ostiary((context) => { 240 rpc(context, { 241 artwork, 242 consult, 243 detach, 244 groupConsult, 245 list, 246 resolve, 247 }); 248});