forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import * as Output from "~/common/output.js";
2import foundation from "~/common/foundation.js";
3import { effect } from "~/common/signal.js";
4
5// Set doc title
6foundation.setup({ title: "Export & Import | Diffuse" });
7
8// Setup
9const main = /** @type {HTMLElement} */ (document.querySelector("main"));
10const output = await foundation.orchestrator.output();
11
12// Elements
13const exportBtn =
14 /** @type {HTMLButtonElement} */ (document.querySelector("#export"));
15const fileInput =
16 /** @type {HTMLInputElement} */ (document.querySelector("#file"));
17const importTracksBtn =
18 /** @type {HTMLButtonElement} */ (document.querySelector("#import-tracks"));
19const importPlaylistItemsBtn =
20 /** @type {HTMLButtonElement} */ (document.querySelector(
21 "#import-playlist-items",
22 ));
23const importFacetsBtn =
24 /** @type {HTMLButtonElement} */ (document.querySelector("#import-facets"));
25const statusEl = /** @type {HTMLElement} */ (document.querySelector("#status"));
26
27/** @type {Record<string, any> | null} */
28let json = null;
29
30/**
31 * Show a status message.
32 * @param {string} message
33 * @param {"success" | "error"} type
34 */
35function showStatus(message, type) {
36 statusEl.textContent = message;
37 statusEl.className = `status status--${type}`;
38 statusEl.hidden = false;
39}
40
41// Enable export button once output is ready
42effect(() => {
43 exportBtn.disabled = !output.ready();
44});
45
46// Export all data as a JSON snapshot
47exportBtn.onclick = async () => {
48 const facets = await Output.data(output.facets);
49 const playlistItems = await Output.data(output.playlistItems);
50 const tracks = await Output.data(output.tracks);
51
52 const data = {
53 exportedAt: new Date().toISOString(),
54 facets,
55 playlistItems,
56 tracks,
57 };
58
59 const blob = new Blob([JSON.stringify(data, null, 2)], {
60 type: "application/json",
61 });
62
63 const url = URL.createObjectURL(blob);
64 const a = document.createElement("a");
65 a.href = url;
66 a.download = `diffuse-${new Date().toISOString().slice(0, 10)}.json`;
67 document.body.append(a);
68 a.click();
69 a.remove();
70
71 URL.revokeObjectURL(url);
72};
73
74// Parse file on selection
75fileInput.onchange = async () => {
76 const file = fileInput.files?.[0];
77
78 json = null;
79 statusEl.hidden = true;
80 importTracksBtn.disabled = true;
81 importPlaylistItemsBtn.disabled = true;
82 importFacetsBtn.disabled = true;
83
84 if (!file) return;
85
86 try {
87 json = JSON.parse(await file.text());
88 } catch (err) {
89 console.error("Failed to parse JSON:", err);
90 showStatus(
91 `Failed to parse JSON: ${/** @type {Error} */ (err).message}`,
92 "error",
93 );
94 return;
95 }
96
97 if (Array.isArray(json?.tracks) && json.tracks.length > 0) {
98 importTracksBtn.disabled = false;
99 }
100
101 if (Array.isArray(json?.playlistItems) && json.playlistItems.length > 0) {
102 importPlaylistItemsBtn.disabled = false;
103 }
104
105 if (Array.isArray(json?.facets) && json.facets.length > 0) {
106 importFacetsBtn.disabled = false;
107 }
108};
109
110/**
111 * @param {HTMLButtonElement} btn
112 * @param {string} label
113 */
114function setButtonLabel(btn, label) {
115 const span = /** @type {HTMLElement} */ (btn.querySelector("span"));
116 span.textContent = label;
117}
118
119// Import tracks
120importTracksBtn.onclick = async () => {
121 /** @type {any[]} */
122 const tracks = json?.tracks;
123 if (!Array.isArray(tracks) || tracks.length === 0) return;
124
125 importTracksBtn.disabled = true;
126 setButtonLabel(importTracksBtn, " Importing ...");
127 try {
128 await output.tracks.save(tracks);
129 showStatus(`Imported ${tracks.length} track(s).`, "success");
130 } catch (err) {
131 console.error("Import failed:", err);
132 showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error");
133 } finally {
134 setButtonLabel(importTracksBtn, " Import tracks");
135 importTracksBtn.disabled = false;
136 }
137};
138
139// Import playlist items
140importPlaylistItemsBtn.onclick = async () => {
141 /** @type {any[]} */
142 const playlistItems = json?.playlistItems;
143 if (!Array.isArray(playlistItems) || playlistItems.length === 0) return;
144
145 importPlaylistItemsBtn.disabled = true;
146 setButtonLabel(importPlaylistItemsBtn, " Importing ...");
147 try {
148 await output.playlistItems.save(playlistItems);
149 const playlistCount = new Set(playlistItems.map((p) => p.playlist)).size;
150 showStatus(`Imported ${playlistCount} playlist(s).`, "success");
151 } catch (err) {
152 console.error("Import failed:", err);
153 showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error");
154 } finally {
155 setButtonLabel(importPlaylistItemsBtn, " Import playlist items");
156 importPlaylistItemsBtn.disabled = false;
157 }
158};
159
160// Import facets
161importFacetsBtn.onclick = async () => {
162 /** @type {any[]} */
163 const facets = json?.facets;
164 if (!Array.isArray(facets) || facets.length === 0) return;
165
166 importFacetsBtn.disabled = true;
167 setButtonLabel(importFacetsBtn, " Importing ...");
168 try {
169 await output.facets.save(facets);
170 showStatus(`Imported ${facets.length} facet(s).`, "success");
171 } catch (err) {
172 console.error("Import failed:", err);
173 showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error");
174 } finally {
175 setButtonLabel(importFacetsBtn, " Import facets");
176 importFacetsBtn.disabled = false;
177 }
178};
179
180////////////////////////////////////////////
181// 🚀
182////////////////////////////////////////////
183
184foundation.ready();