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