alpha
Login
or
Join now
tokono.ma
/
diffuse-applets
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Experiment to rebuild Diffuse using web applets.
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
feat: artwork processor pt. 2
author
Steven Vandevelde
date
1 year ago
(Jun 7, 2025, 5:07 PM +0200)
commit
759bca9d
759bca9d0a8e3e958c7a4df61566d930fb81b605
parent
5f4e8023
5f4e8023b156c146eb3fb7744e93891805779ca7
+113
-27
4 changed files
Expand all
Collapse all
Unified
Split
src
pages
orchestrator
input-cache
_applet.astro
processor
artwork
_applet.astro
types.d.ts
metadata
_applet.astro
+6
-6
src/pages/orchestrator/input-cache/_applet.astro
Reviewed
···
23
23
};
24
24
25
25
// Start processing when tracks are loaded
26
26
-
configurator.output
27
27
-
.then((output) => {
28
28
-
return waitUntilAppletData(output, (d) => d?.tracks.state === "loaded");
29
29
-
})
30
30
-
.then(process);
26
26
+
context
27
27
+
.settled()
28
28
+
.then(() => configurator.output)
29
29
+
.then((output) => waitUntilAppletData(output, (d) => d?.tracks.state === "loaded"))
30
30
+
.then(() => process());
31
31
32
32
////////////////////////////////////////////
33
33
// ACTIONS
···
78
78
79
79
const metadataProcessor = await processor.metadata;
80
80
const { stats, tags } = await metadataProcessor.sendAction(
81
81
-
"extract",
81
81
+
"supply",
82
82
{ urls: { get: resGet.url, head: resHead?.url || resGet.url } },
83
83
{
84
84
timeoutDuration: 60000 * 15,
+97
-17
src/pages/processor/artwork/_applet.astro
Reviewed
···
1
1
<script>
2
2
+
import type { IPicture } from "music-metadata";
2
3
import * as IDB from "idb-keyval";
3
4
4
5
import { applet, register } from "@scripts/applets/common";
5
5
-
import type { ArtworkRequest, State } from "./types.d.ts";
6
6
+
import type { ArtworkRequest, State, Artwork } from "./types.d.ts";
7
7
+
import type { Extraction } from "../metadata/types.d.ts";
6
8
7
9
////////////////////////////////////////////
8
10
// SETUP
9
11
////////////////////////////////////////////
10
12
const IDB_PREFIX = "@applets/processor/artwork";
13
13
+
const IDB_ARTWORK_PREFIX = `${IDB_PREFIX}/artwork`;
11
14
12
15
const context = register<State>();
13
16
let queue: ArtworkRequest[] = [];
14
17
15
18
// Initial data
16
19
context.data = {
17
17
-
artwork: [],
20
20
+
artwork: {},
18
21
};
19
22
20
23
// Applet connections
···
23
26
};
24
27
25
28
// Load already-downloaded artwork
26
26
-
IDB.keys().then((keys) => {
27
27
-
console.log(keys);
28
28
-
const artworkKeys = keys.filter((k) => k.toString().startsWith(`${IDB_PREFIX}/artwork/`));
29
29
+
IDB.keys().then(async (keys) => {
30
30
+
const artworkKeys = keys.filter((k) => k.toString().startsWith(`${IDB_ARTWORK_PREFIX}/`));
31
31
+
32
32
+
artworkKeys.forEach(async (key) => {
33
33
+
if (typeof key !== "string") return;
34
34
+
35
35
+
const artwork = await IDB.get(key);
36
36
+
const cacheId = key.split("/").reverse()[0];
37
37
+
38
38
+
context.data.artwork[cacheId] = artwork;
39
39
+
});
29
40
});
30
41
31
42
////////////////////////////////////////////
···
34
45
function supply(items: ArtworkRequest[]) {
35
46
const exe = !queue[0];
36
47
queue = [...queue, ...items];
37
37
-
console.log("supply", queue);
38
48
if (exe) shiftQueue();
39
49
}
40
50
···
43
53
////////////////////////////////////////////
44
54
// 🛠️
45
55
////////////////////////////////////////////
56
56
+
async function lastFm(req: ArtworkRequest): Promise<Artwork[]> {
57
57
+
if (!navigator.onLine) return [];
58
58
+
59
59
+
const query = req.tags?.artist;
60
60
+
61
61
+
return await fetch(
62
62
+
`https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`,
63
63
+
)
64
64
+
.then((r) => r.json())
65
65
+
.then((r) => lastFmCover(r.results.albummatches.album));
66
66
+
}
67
67
+
68
68
+
function lastFmCover(remainingMatches: any[]): Promise<Artwork[]> {
69
69
+
const album = remainingMatches[0];
70
70
+
const url = album ? album.image[album.image.length - 1]["#text"] : null;
71
71
+
72
72
+
return url && url !== ""
73
73
+
? fetch(url)
74
74
+
.then((r) => r.blob())
75
75
+
.then(async (b) => [{ bytes: await b.bytes(), mime: b.type }])
76
76
+
.catch((_) => lastFmCover(remainingMatches.slice(1)))
77
77
+
: album && lastFmCover(remainingMatches.slice(1));
78
78
+
}
79
79
+
80
80
+
async function musicBrainz(req: ArtworkRequest): Promise<Artwork[]> {
81
81
+
const artist = req.tags?.artist;
82
82
+
const album = req.tags?.album;
83
83
+
84
84
+
if (!navigator.onLine) return [];
85
85
+
if (!album && !artist) return [];
86
86
+
87
87
+
// TODO
88
88
+
const variousArtists = false;
89
89
+
90
90
+
const query = `release:"${album}"` + (variousArtists ? `` : ` AND artist:"${artist}"`);
91
91
+
const encodedQuery = encodeURIComponent(query);
92
92
+
93
93
+
return await fetch(`https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`)
94
94
+
.then((r) => r.json())
95
95
+
.then((r) => musicBrainzCover(r.releases));
96
96
+
}
97
97
+
98
98
+
async function musicBrainzCover(remainingReleases: any[]): Promise<Artwork[]> {
99
99
+
const release = remainingReleases[0];
100
100
+
if (!release) return [];
101
101
+
102
102
+
return await fetch(`https://coverartarchive.org/release/${release.id}/front-500`)
103
103
+
.then((r) => r.blob())
104
104
+
.then(async (b) => {
105
105
+
if (b && b.type.startsWith("image/")) {
106
106
+
return [{ bytes: await b.bytes(), mime: b.type }];
107
107
+
} else {
108
108
+
return musicBrainzCover(remainingReleases.slice(1));
109
109
+
}
110
110
+
})
111
111
+
.catch(() => musicBrainzCover(remainingReleases.slice(1)));
112
112
+
}
113
113
+
46
114
async function shiftQueue() {
47
115
const next = queue.shift();
48
116
if (!next) return;
49
117
50
118
// Check if already processed
51
119
const cache = await IDB.get(`${IDB_PREFIX}/${next.cacheId}`);
52
52
-
if (cache) return;
120
120
+
if (cache && cache.length > 0) return;
53
121
54
122
// 🚀
55
55
-
let art: Uint8Array | undefined;
123
123
+
let art: Artwork[] = [];
56
124
57
125
// Get metadata + possible artwork from file metadata
58
126
const proc = await processor.metadata;
59
59
-
const meta = await proc.sendAction("supply", { ...next, includeArtwork: true });
127
127
+
const meta = await proc.sendAction<Extraction>("supply", { ...next, includeArtwork: true });
128
128
+
if (!next.tags) next.tags = meta.tags;
60
129
61
61
-
//
62
62
-
art = meta.artwork?.sort((a: any, b: any) => {
63
63
-
if (a.data.length > b.data.length) return -1;
64
64
-
if (a.data.length < b.data.length) return 1;
65
65
-
return 0;
66
66
-
})?.[0]?.data;
130
130
+
// Add artwork from metadata
131
131
+
const fromMeta =
132
132
+
meta.artwork?.map((a: IPicture) => {
133
133
+
return { bytes: a.data, mime: a.format };
134
134
+
}) || [];
67
135
68
68
-
console.log(art);
136
136
+
art.push(...fromMeta);
137
137
+
138
138
+
// If no artwork, try finding it on other sources
139
139
+
if (art.length === 0) {
140
140
+
const fromMusicBrainz = await musicBrainz(next);
141
141
+
art.push(...fromMusicBrainz);
142
142
+
}
143
143
+
144
144
+
if (art.length === 0) {
145
145
+
const fromLastFm = await lastFm(next);
146
146
+
art.push(...fromLastFm);
147
147
+
}
69
148
70
149
// Save artwork to IDB
71
71
-
await IDB.set(`${IDB_PREFIX}/${next.cacheId}`, art || "TRIED");
150
150
+
await IDB.set(`${IDB_ARTWORK_PREFIX}/${next.cacheId}`, art);
151
151
+
context.data.artwork[next.cacheId] = art;
72
152
73
153
// 🏹
74
154
shiftQueue();
+9
-3
src/pages/processor/artwork/types.d.ts
Reviewed
···
1
1
-
export type Artwork = {};
1
1
+
import { TrackTags } from "@applets/core/types";
2
2
3
3
-
export type ArtworkRequest = {
3
3
+
export type Artwork = {
4
4
+
bytes: Uint8Array;
5
5
+
mime: string;
6
6
+
};
7
7
+
8
8
+
export type ArtworkRequest<Tags = TrackTags> = {
4
9
cacheId: string;
5
10
mimeType?: string;
6
11
stream?: ReadableStream;
12
12
+
tags?: Tags;
7
13
urls?: Urls;
8
14
};
9
15
10
16
export type State = {
11
11
-
artwork: Artwork[];
17
17
+
artwork: Record<string, Artwork[]>;
12
18
};
13
19
14
20
export type Urls = { get: string; head: string };
+1
-1
src/pages/processor/metadata/_applet.astro
Reviewed
···
24
24
mimeType?: string;
25
25
stream?: ReadableStream;
26
26
urls?: Urls;
27
27
-
}) {
27
27
+
}): Promise<Extraction> {
28
28
// Construct records
29
29
// TODO: Use other metadata lib as fallback: https://github.com/buzz/mediainfo.js
30
30
const response = await musicMetadataTags(args).catch((err): Extraction => {