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.

feat: import v3 playlists tool

+47 -14
+3 -14
src/definitions/output/playlist.json
··· 7 7 "record": { 8 8 "type": "object", 9 9 "required": ["id", "items", "name", "unordered"], 10 - "nullable": ["autoGenerate"], 11 10 "properties": { 12 11 "id": { "type": "string" }, 13 - "autoGenerate": { 12 + "smart": { 14 13 "type": "ref", 15 - "ref": "#autoGenerate" 14 + "ref": "#smart" 16 15 }, 17 16 "createdAt": { "type": "string", "format": "datetime" }, 18 17 "items": { ··· 34 33 } 35 34 } 36 35 }, 37 - "autoGenerate": { 38 - "type": "object", 39 - "description": "Auto-generate the items of the playlist, overriding the included items. If this property is set, the playlist should always be treated as unordered.", 40 - "required": ["criteria"], 41 - "properties": { 42 - "criteria": { 43 - "type": "array", 44 - "items": { "type": "ref", "ref": "#criterion" } 45 - } 46 - } 47 - }, 48 36 "criterion": { 49 37 "type": "object", 50 38 "required": ["field", "value"], ··· 62 50 }, 63 51 "item": { 64 52 "type": "object", 53 + "description": "Does not necessarily match one track. You can make it a smart playlist by having the criteria match multiple tracks.", 65 54 "required": ["criteria"], 66 55 "properties": { 67 56 "criteria": {
+44
src/facets/tools/v3-import.html.txt
··· 17 17 <span class="with-icon"><i class="ph-fill ph-star"></i> Import favourites</span> 18 18 </button> 19 19 </div> 20 + <div class="row"> 21 + <button id="import-playlists" disabled> 22 + <span class="with-icon"><i class="ph-bold ph-playlist"></i> Import playlists</span> 23 + </button> 24 + </div> 20 25 <div id="status" class="status" hidden></div> 21 26 </div> 22 27 </main> ··· 85 90 // Elements 86 91 const fileInput = document.querySelector("#file"); 87 92 const importFavouritesBtn = document.querySelector("#import-favourites"); 93 + const importPlaylistsBtn = document.querySelector("#import-playlists"); 88 94 const statusEl = document.querySelector("#status"); 89 95 90 96 // Parsed data ··· 108 114 json = null; 109 115 statusEl.hidden = true; 110 116 importFavouritesBtn.disabled = true; 117 + importPlaylistsBtn.disabled = true; 111 118 112 119 if (!file) return; 113 120 ··· 121 128 122 129 if (json.favourites?.data?.length > 0) { 123 130 importFavouritesBtn.disabled = false; 131 + } 132 + 133 + if (json.playlists?.data?.length > 0) { 134 + importPlaylistsBtn.disabled = false; 124 135 } 125 136 }; 126 137 ··· 139 150 140 151 await favourites.include(tracks); 141 152 showStatus(`Imported ${tracks.length} favourite(s).`, "success"); 153 + } catch (err) { 154 + console.error("Import failed:", err); 155 + showStatus(`Import failed: ${err.message}`, "error"); 156 + } 157 + }; 158 + 159 + // Import playlists on button click 160 + importPlaylistsBtn.onclick = async () => { 161 + const items = json?.playlists?.data; 162 + if (!items || items.length === 0) return; 163 + 164 + try { 165 + const now = new Date().toISOString(); 166 + const existing = output.playlists.collection() ?? []; 167 + 168 + const newPlaylists = items.map((item) => ({ 169 + $type: "sh.diffuse.output.playlist", 170 + id: crypto.randomUUID(), 171 + name: item.name ?? "Untitled", 172 + unordered: !!item.collection, 173 + items: (item.tracks ?? []).map((track) => ({ 174 + criteria: [ 175 + { field: "tags.album", value: track.album ?? "", transformations: ["toLowerCase"] }, 176 + { field: "tags.artist", value: track.artist ?? "", transformations: ["toLowerCase"] }, 177 + { field: "tags.title", value: track.title ?? "", transformations: ["toLowerCase"] }, 178 + ], 179 + })), 180 + createdAt: now, 181 + updatedAt: now, 182 + })); 183 + 184 + await output.playlists.save([...existing, ...newPlaylists]); 185 + showStatus(`Imported ${newPlaylists.length} playlist(s).`, "success"); 142 186 } catch (err) { 143 187 console.error("Import failed:", err); 144 188 showStatus(`Import failed: ${err.message}`, "error");