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 192 lines 4.3 kB view raw
1import { S3Client } from "@bradenmacdonald/s3-lite-client"; 2import * as IDB from "idb-keyval"; 3import * as URI from "fast-uri"; 4import QS from "query-string"; 5 6import { cachedConsult } from "~/components/input/common.js"; 7import { safeDecodeURIComponent } from "~/common/utils.js"; 8import { ENCODINGS, IDB_BUCKETS, SCHEME } from "./constants.js"; 9 10/** 11 * @import { Track } from "~/definitions/types.d.ts"; 12 * @import { Bucket } from "./types.d.ts"; 13 */ 14 15//////////////////////////////////////////// 16// 🛠️ 17//////////////////////////////////////////// 18 19/** 20 * @param {Track[]} tracks 21 */ 22export function bucketsFromTracks(tracks) { 23 /** @type {Record<string, Bucket>} */ 24 const acc = {}; 25 26 tracks.forEach((track) => { 27 const parsed = parseURI(track.uri); 28 if (!parsed) return; 29 30 const id = bucketId(parsed.bucket); 31 if (acc[id]) return; 32 33 acc[id] = parsed.bucket; 34 }); 35 36 return acc; 37} 38 39/** 40 * @param {Bucket} bucket 41 */ 42export function bucketId(bucket) { 43 return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`; 44} 45 46/** 47 * @param {Bucket} bucket 48 * @param {string} [path] 49 */ 50export function buildURI(bucket, path) { 51 return URI.serialize({ 52 scheme: SCHEME, 53 userinfo: `${bucket.accessKey}:${bucket.secretKey}`, 54 host: bucket.host.replace(/^\w+:\/\//, ""), 55 path: path, 56 query: QS.stringify({ 57 bucketName: bucket.bucketName, 58 bucketPath: bucket.path, 59 region: bucket.region, 60 }), 61 }); 62} 63 64/** 65 * @param {Bucket} bucket 66 */ 67export async function consultBucket(bucket) { 68 const client = createClient(bucket); 69 return await client.bucketExists(bucket.bucketName); 70} 71 72export const consultBucketCached = cachedConsult(consultBucket, bucketId); 73 74/** 75 * @param {Bucket} bucket 76 */ 77export function createClient(bucket) { 78 return new S3Client({ 79 bucket: bucket.bucketName, 80 endPoint: `http${ 81 bucket.host.startsWith("localhost") ? "" : "s" 82 }://${bucket.host}`, 83 region: bucket.region, 84 pathStyle: false, 85 accessKey: bucket.accessKey, 86 secretKey: bucket.secretKey, 87 }); 88} 89 90/** 91 * @param {string} a 92 */ 93export function encodeAwsUriComponent(a) { 94 return encodeURIComponent(a).replace( 95 /(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim, 96 (match) => /** @type {any} */ (ENCODINGS)[match] ?? match, 97 ); 98} 99 100/** 101 * @param {Track[]} tracks 102 */ 103export function groupTracksByBucket(tracks) { 104 /** @type {Record<string, { bucket: Bucket; tracks: Track[] }>} */ 105 const acc = {}; 106 107 tracks.forEach((track) => { 108 const parsed = parseURI(track.uri); 109 if (!parsed) return acc; 110 111 const id = bucketId(parsed.bucket); 112 113 if (acc[id]) { 114 acc[id].tracks.push(track); 115 } else { 116 acc[id] = { bucket: parsed.bucket, tracks: [track] }; 117 } 118 }); 119 120 return acc; 121} 122 123/** 124 * @param {string[]} uris 125 */ 126export function groupUrisByBucket(uris) { 127 /** @type {Record<string, { bucket: Bucket; uris: string[] }>} */ 128 const acc = {}; 129 130 uris.forEach((uri) => { 131 const parsed = parseURI(uri); 132 if (!parsed) return acc; 133 134 const id = bucketId(parsed.bucket); 135 136 if (acc[id]) { 137 acc[id].uris.push(uri); 138 } else { 139 acc[id] = { bucket: parsed.bucket, uris: [uri] }; 140 } 141 }); 142 143 return acc; 144} 145 146/** 147 * @returns {Promise<Record<string, Bucket>>} 148 */ 149export async function loadBuckets() { 150 const i = await IDB.get(IDB_BUCKETS); 151 return i ? i : {}; 152} 153 154/** 155 * @param {string} uriString 156 * @returns {{ bucket: Bucket; path: string } | undefined} 157 */ 158export function parseURI(uriString) { 159 const uri = URI.parse(uriString); 160 if (uri.scheme !== SCHEME) return undefined; 161 if (!uri.host) return undefined; 162 163 const [accessKey, secretKey] = uri.userinfo?.split(":") ?? []; 164 if (!accessKey || !secretKey) return undefined; 165 166 const qs = QS.parse(uri.query || ""); 167 168 const bucket = { 169 accessKey, 170 bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "", 171 host: uri.host, 172 path: qs.bucketPath === "string" ? qs.bucketPath : "/", 173 region: typeof qs.region === "string" ? qs.region : "", 174 secretKey, 175 }; 176 177 const path = 178 (bucket.path.replace(/\/$/, "") + safeDecodeURIComponent(uri.path || "")) 179 .replace( 180 /^\//, 181 "", 182 ); 183 184 return { bucket, path }; 185} 186 187/** 188 * @param {Record<string, Bucket>} items 189 */ 190export async function saveBuckets(items) { 191 await IDB.set(IDB_BUCKETS, items); 192}