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 189 lines 4.5 kB view raw
1import * as TID from "@atcute/tid"; 2import { ostiary, rpc } from "~/common/worker.js"; 3import { 4 detach as detachUtil, 5 groupKey, 6 isAudioFile, 7} from "~/components/input/common.js"; 8import { safeDecodeURIComponent } from "~/common/utils.js"; 9 10import { 11 buildTrackUrl, 12 buildURI, 13 checkAccessCached, 14 groupTracksByServer, 15 groupUrisByServer, 16 listFiles, 17 parseURI, 18 serverId, 19} from "./common.js"; 20import { SCHEME } from "./constants.js"; 21 22/** 23 * @import { InputActions as Actions, ConsultGrouping } from "~/components/input/types.d.ts"; 24 * @import { Track } from "~/definitions/types.d.ts"; 25 */ 26 27//////////////////////////////////////////// 28// ACTIONS 29//////////////////////////////////////////// 30 31/** 32 * @type {Actions['artwork']} 33 */ 34export async function artwork(_uri) { 35 return null; 36} 37 38/** 39 * @type {Actions['consult']} 40 */ 41export async function consult(fileUriOrScheme) { 42 if (!fileUriOrScheme.includes(":")) { 43 return { supported: true, consult: "undetermined" }; 44 } 45 46 const parsed = parseURI(fileUriOrScheme); 47 if (!parsed) return { supported: true, consult: "undetermined" }; 48 49 const accessible = await checkAccessCached(parsed.server); 50 return { supported: true, consult: accessible }; 51} 52 53/** 54 * @type {Actions['detach']} 55 */ 56export async function detach(args) { 57 return detachUtil({ 58 ...args, 59 60 inputScheme: SCHEME, 61 handleFileUri: ({ fileURI, tracks }) => { 62 const result = parseURI(fileURI); 63 if (!result) return tracks; 64 65 const id = serverId(result.server); 66 const groups = groupTracksByServer(tracks); 67 68 delete groups[id]; 69 70 return Object.values(groups).map((g) => g.tracks).flat(1); 71 }, 72 }); 73} 74 75/** 76 * @type {Actions['groupConsult']} 77 */ 78export async function groupConsult(uris) { 79 const groups = groupUrisByServer(uris); 80 81 const promises = Object.entries(groups).map( 82 async ([id, { server, uris }]) => { 83 const available = await checkAccessCached(server); 84 85 /** @type {ConsultGrouping} */ 86 const grouping = available 87 ? { available, scheme: SCHEME, uris } 88 : { available, reason: "WebDAV server unreachable", scheme: SCHEME, uris }; 89 90 return { key: groupKey(SCHEME, id), grouping }; 91 }, 92 ); 93 94 const entries = (await Promise.all(promises)).map((e) => [e.key, e.grouping]); 95 return Object.fromEntries(entries); 96} 97 98/** 99 * @type {Actions['list']} 100 */ 101export async function list(cachedTracks = []) { 102 /** @type {Record<string, Record<string, Track>>} */ 103 const cache = {}; 104 105 const groups = groupTracksByServer(cachedTracks); 106 107 Object.entries(groups).forEach(([id, { tracks }]) => { 108 tracks.forEach((track) => { 109 const parsed = parseURI(track.uri); 110 if (!parsed) return; 111 112 if (!cache[id]) cache[id] = {}; 113 cache[id][safeDecodeURIComponent(parsed.path)] = track; 114 }); 115 }); 116 117 const promises = Object.entries(groups).map(async ([id, { server }]) => { 118 const files = await listFiles(server); 119 120 let tracks = files 121 .filter((path) => isAudioFile(path)) 122 .map((path) => { 123 const cachedTrack = cache[id]?.[safeDecodeURIComponent(path)]; 124 125 const trackId = cachedTrack?.id || TID.now(); 126 const stats = cachedTrack?.stats; 127 const tags = cachedTrack?.tags; 128 const now = new Date().toISOString(); 129 130 /** @type {Track} */ 131 const track = { 132 $type: "sh.diffuse.output.track", 133 id: trackId, 134 createdAt: cachedTrack?.createdAt ?? now, 135 updatedAt: cachedTrack?.updatedAt ?? now, 136 stats, 137 tags, 138 uri: buildURI(server, path), 139 }; 140 141 return track; 142 }); 143 144 if (!tracks.length) { 145 const now = new Date().toISOString(); 146 147 tracks = [{ 148 $type: "sh.diffuse.output.track", 149 id: TID.now(), 150 createdAt: now, 151 updatedAt: now, 152 kind: "placeholder", 153 uri: buildURI(server), 154 }]; 155 } 156 157 return tracks; 158 }); 159 160 return (await Promise.all(promises)).flat(1); 161} 162 163/** 164 * @type {Actions['resolve']} 165 */ 166export async function resolve({ uri }) { 167 const parsed = parseURI(uri); 168 if (!parsed || !parsed.path) return undefined; 169 170 const url = buildTrackUrl(parsed.server, parsed.path); 171 const expiresAt = Math.round(Date.now() / 1000) + 60 * 60 * 24 * 365; 172 173 return { url, expiresAt }; 174} 175 176//////////////////////////////////////////// 177// ⚡️ 178//////////////////////////////////////////// 179 180ostiary((context) => { 181 rpc(context, { 182 artwork, 183 consult, 184 detach, 185 groupConsult, 186 list, 187 resolve, 188 }); 189});