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.

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