Experiment to rebuild Diffuse using web applets.
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 4.5 kB View raw
1import { SubsonicAPI, type Child } from "subsonic-api"; 2import * as URI from "uri-js"; 3 4import type { Consult, ConsultGrouping, GroupConsult, Track } from "@applets/core/types.d.ts"; 5import { SCHEME } from "./constants.ts"; 6import { 7 autoTypeToTrackKind, 8 buildURI, 9 consultServer, 10 createClient, 11 groupTracksByServer, 12 loadServers, 13 parseURI, 14 serverId, 15 serversFromTracks, 16} from "./common.ts"; 17import { provide, transfer } from "@scripts/common.ts"; 18 19//////////////////////////////////////////// 20// TASKS 21//////////////////////////////////////////// 22const actions = { 23 consult, 24 contextualize, 25 groupConsult, 26 list, 27 resolve, 28}; 29 30const { tasks } = provide({ actions, tasks: actions }); 31 32export type Actions = typeof actions; 33export type Tasks = typeof tasks; 34 35// Tasks 36 37async function consult(fileUriOrScheme: string): Promise<Consult> { 38 if (!fileUriOrScheme.includes(":")) return { supported: true, consult: "undetermined" }; 39 40 const parsed = parseURI(fileUriOrScheme); 41 if (!parsed) return { supported: true, consult: "undetermined" }; 42 43 const consult = await consultServer(parsed.server); 44 return { supported: true, consult }; 45} 46 47async function contextualize(tracks: Track[]) { 48 return serversFromTracks(tracks); 49} 50 51async function groupConsult(tracks: Track[]): Promise<GroupConsult> { 52 const groups = groupTracksByServer(tracks); 53 54 const promises = Object.entries(groups).map(async ([serverId, { server, tracks }]) => { 55 const available = await consultServer(server); 56 const grouping: ConsultGrouping = available 57 ? { available, tracks } 58 : { available, reason: "Server ping failed", tracks }; 59 60 return { 61 key: `${SCHEME}:${serverId}`, 62 grouping, 63 }; 64 }); 65 66 const entries = (await Promise.all(promises)).map((entry) => [entry.key, entry.grouping]); 67 const obj = Object.fromEntries(entries); 68 69 return transfer(obj); 70} 71 72async function list(cachedTracks: Track[] = []) { 73 const cache: Record<string, Record<string, Track>> = {}; 74 75 cachedTracks.forEach((t: Track) => { 76 const parsed = parseURI(t.uri); 77 if (!parsed || !parsed.path) return; 78 79 const sid = serverId(parsed?.server); 80 81 cache[sid] ??= {}; 82 cache[sid][URI.unescapeComponent(parsed.path)] = t; 83 }); 84 85 async function search(client: SubsonicAPI, offset = 0): Promise<Child[]> { 86 const result = await client.search3({ 87 query: "", 88 artistCount: 0, 89 albumCount: 0, 90 songCount: 1000, 91 songOffset: offset, 92 }); 93 94 const songs = result.searchResult3.song || []; 95 96 if (songs.length === 1000) { 97 const moreSongs = await search(client, offset + 1000); 98 return [...songs, ...moreSongs]; 99 } 100 101 return songs; 102 } 103 104 const servers = await loadServers(); 105 const promises = Object.values(servers).map(async (server) => { 106 const client = createClient(server); 107 const sid = serverId(server); 108 const list = await search(client, 0); 109 110 return list 111 .filter((song) => !song.isVideo) 112 .map((song) => { 113 const path = song.path 114 ? song.path.startsWith("/") 115 ? song.path 116 : `/${song.path}` 117 : undefined; 118 const fromCache = path ? cache[sid]?.[path] : undefined; 119 if (fromCache) return fromCache; 120 121 const track: Track = { 122 id: crypto.randomUUID(), 123 kind: autoTypeToTrackKind(song.type), 124 uri: buildURI(server, { songId: song.id, path }), 125 126 stats: { 127 bitrate: song.bitRate, 128 duration: song.duration, 129 }, 130 tags: { 131 album: song.album, 132 artist: song.artist, 133 disc: { no: song.discNumber || 1 }, 134 genre: song.genre, 135 title: song.title, 136 track: { no: song.track || 1 }, 137 year: song.year, 138 }, 139 }; 140 141 return track; 142 }); 143 }); 144 145 const tracks = (await Promise.all(promises)).flat(1); 146 return transfer(tracks); 147} 148 149async function resolve({ uri }: { method: string; uri: string }) { 150 const parsed = parseURI(uri); 151 if (!parsed) return undefined; 152 153 const client = createClient(parsed.server); 154 const songId = parsed.songId; 155 if (!songId) return undefined; 156 157 // TODO: 158 // const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days 159 // const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds; 160 161 const url = await client 162 .download({ 163 id: songId, 164 format: "raw", 165 }) 166 .then((a) => a.url); 167 168 return { expiresAt: Infinity, url }; 169}