forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import * as TID from "@atcute/tid";
2import foundation from "~/common/foundation.js";
3
4foundation.setup({ title: "V3.x Import | Diffuse" });
5
6const main = /** @type {HTMLElement} */ (document.querySelector("main"));
7
8/**
9 * @import {PlaylistItem, Track} from "~/definitions/types.d.ts"
10 */
11
12// Setup
13const favourites = await foundation.orchestrator.favourites();
14const output = await foundation.orchestrator.output();
15
16// Elements
17const fileInput =
18 /** @type {HTMLInputElement} */ (document.querySelector("#file"));
19const importFavouritesBtn =
20 /** @type {HTMLButtonElement} */ (document.querySelector(
21 "#import-favourites",
22 ));
23const importPlaylistItemsBtn =
24 /** @type {HTMLButtonElement} */ (document.querySelector(
25 "#import-playlist-items",
26 ));
27const statusEl = /** @type {HTMLElement} */ (document.querySelector("#status"));
28
29/** @type {Record<string, any> | null} */
30let json = null;
31
32/**
33 * Show a status message.
34 * @param {string} message
35 * @param {"success" | "error"} type
36 */
37function showStatus(message, type) {
38 statusEl.textContent = message;
39 statusEl.className = `status status--${type}`;
40 statusEl.hidden = false;
41}
42
43// Parse file on selection
44fileInput.onchange = async () => {
45 const file = fileInput.files?.[0];
46
47 json = null;
48 statusEl.hidden = true;
49 importFavouritesBtn.disabled = true;
50 importPlaylistItemsBtn.disabled = true;
51
52 if (!file) return;
53
54 try {
55 json = JSON.parse(await file.text());
56 } catch (err) {
57 console.error("Failed to parse JSON:", err);
58 showStatus(
59 `Failed to parse JSON: ${/** @type {Error} */ (err).message}`,
60 "error",
61 );
62 return;
63 }
64
65 if (json?.favourites?.data?.length > 0) {
66 importFavouritesBtn.disabled = false;
67 }
68
69 if (json?.playlists?.data?.length > 0) {
70 importPlaylistItemsBtn.disabled = false;
71 }
72};
73
74// Import favourites on button click
75importFavouritesBtn.onclick = async () => {
76 /** @type {any[]} */
77 const items = json?.favourites?.data;
78 if (!items || items.length === 0) return;
79
80 try {
81 /** @type {Track[]} */
82 const tracks = items.map((item) => ({
83 $type: "sh.diffuse.output.track",
84 id: "",
85 uri: "",
86 tags: {
87 artist: item.artist ?? "",
88 title: item.title ?? "",
89 },
90 }));
91
92 await favourites.include(tracks);
93 showStatus(`Imported ${tracks.length} favourite(s).`, "success");
94 } catch (err) {
95 console.error("Import failed:", err);
96 showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error");
97 }
98};
99
100// Import playlist items on button click
101importPlaylistItemsBtn.onclick = async () => {
102 /** @type {any[]} */
103 const items = json?.playlists?.data;
104 if (!items || items.length === 0) return;
105
106 try {
107 const now = new Date().toISOString();
108
109 const existingCol = output.playlistItems.collection();
110 /** @type {any[]} */
111 const existing = existingCol.state === "loaded" ? existingCol.data : [];
112 const existingPlaylistNames = new Set(existing.map((p) => p.playlist));
113
114 const newPlaylistItems = items
115 .filter((item) => !existingPlaylistNames.has(item.name ?? "Untitled"))
116 .flatMap((item) => {
117 const playlistName = item.name ?? "Untitled";
118 const isUnordered = !!item.collection;
119
120 /** @type {PlaylistItem[]} */
121 const playlistItems = [];
122
123 /** @type {any[]} */ (item.tracks ?? []).forEach((track, index) => {
124 playlistItems.push({
125 $type: "sh.diffuse.output.playlistItem",
126 id: TID.now(),
127 playlist: playlistName,
128 positionedAfter: isUnordered
129 ? undefined
130 : index > 0
131 ? playlistItems[index - 1].id
132 : undefined,
133 criteria: [
134 {
135 field: "tags.album",
136 value: track.album ?? "",
137 transformations: ["toLowerCase"],
138 },
139 {
140 field: "tags.artist",
141 value: track.artist ?? "",
142 transformations: ["toLowerCase"],
143 },
144 {
145 field: "tags.title",
146 value: track.title ?? "",
147 transformations: ["toLowerCase"],
148 },
149 ],
150 createdAt: now,
151 updatedAt: now,
152 });
153 });
154
155 return playlistItems;
156 });
157
158 await output.playlistItems.save([...existing, ...newPlaylistItems]);
159 const playlistCount = new Set(newPlaylistItems.map((p) => p.playlist)).size;
160 showStatus(`Imported ${playlistCount} playlist(s).`, "success");
161 } catch (err) {
162 console.error("Import failed:", err);
163 showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error");
164 }
165};
166
167////////////////////////////////////////////
168// 🚀
169////////////////////////////////////////////
170
171foundation.ready();