Sfoglia il codice sorgente

Add library switch function and fix folder loader bug

Toby Chui 22 ore fa
parent
commit
9a7d68a722

+ 31 - 27
src/web/Musicify/backend/listArtists.js

@@ -1,6 +1,9 @@
 /*
     Musicify - List Artists
-    Groups all music files by top-level folder under each root's Music/ directory.
+    Recursively walks each root's Music/ directory and groups music files by their
+    immediate parent folder — so nested structures like Music/Genre/Artist/track.mp3
+    correctly surface "Artist" as the artist, not "Genre".
+    Files sitting directly in Music/ are grouped under "Unknown Artist".
     Returns: [ { name, path, songCount, songs: [{...}] } ]
 */
 includes("common.js");
@@ -8,41 +11,42 @@ requirelib("filelib");
 
 function main() {
     var musicRoots = getMusicRoots();
+    // Key: full parent-folder path (guarantees uniqueness across roots/nesting levels)
     var artistMap = {};
 
     for (var r = 0; r < musicRoots.length; r++) {
-        var musicRoot = musicRoots[r];
-        var topEntries = filelib.aglob(musicRoot + "*", "default");
+        var musicRoot = musicRoots[r]; // e.g. "user:/Music/"
 
-        // Songs directly in Music/ root belong to "Unknown Artist"
-        for (var i = 0; i < topEntries.length; i++) {
-            var entry = topEntries[i];
-            if (isHiddenFile(entry)) continue;
-            if (!filelib.isDir(entry) && isMusicFile(entry)) {
-                if (!artistMap["__unknown__"]) {
-                    artistMap["__unknown__"] = { name: "Unknown Artist", path: musicRoot, songs: [] };
-                }
-                artistMap["__unknown__"].songs.push(buildSongEntry(entry));
-            }
-        }
+        // Walk ALL files under the music root recursively
+        var allFiles = filelib.walk(musicRoot, "file");
+
+        for (var i = 0; i < allFiles.length; i++) {
+            var f = allFiles[i];
+            if (!isMusicFile(f) || isHiddenFile(f)) continue;
+
+            // Derive the immediate parent folder path
+            var parts = f.split("/");
+            parts.pop(); // remove filename
+            var parentPath = parts.join("/") + "/";
 
-        // Each subdirectory is an artist
-        for (var i = 0; i < topEntries.length; i++) {
-            var artistDir = topEntries[i];
-            if (!filelib.isDir(artistDir) || isHiddenFile(artistDir)) continue;
+            var artistKey, artistName, artistPath;
 
-            var artistName = artistDir.split("/").pop();
-            if (!artistMap[artistName]) {
-                artistMap[artistName] = { name: artistName, path: artistDir, songs: [] };
+            if (parentPath === musicRoot) {
+                // File lives directly inside Music/ → Unknown Artist
+                artistKey  = musicRoot + "__unknown__";
+                artistName = "Unknown Artist";
+                artistPath = musicRoot;
+            } else {
+                // Use the full parent path as the unique key
+                artistKey  = parentPath;
+                artistName = parts[parts.length - 1]; // last path component
+                artistPath = parentPath;
             }
 
-            var songFiles = filelib.walk(artistDir, "file");
-            for (var j = 0; j < songFiles.length; j++) {
-                var f = songFiles[j];
-                if (isMusicFile(f) && !isHiddenFile(f)) {
-                    artistMap[artistName].songs.push(buildSongEntry(f));
-                }
+            if (!artistMap[artistKey]) {
+                artistMap[artistKey] = { name: artistName, path: artistPath, songs: [] };
             }
+            artistMap[artistKey].songs.push(buildSongEntry(f));
         }
     }
 

+ 24 - 0
src/web/Musicify/backend/listRoots.js

@@ -0,0 +1,24 @@
+/*
+    Musicify - List Music Library Roots
+    Enumerates all mounted storage roots that have a Music/ folder.
+    Returns: [ { label, root } ]
+      label: disk identifier, e.g. "user:" or "disk:"
+      root:  vpath to the Music folder without trailing slash, e.g. "user:/Music"
+*/
+includes("common.js");
+requirelib("filelib");
+
+function main() {
+    var musicRoots = getMusicRoots();
+    var result = [];
+    for (var i = 0; i < musicRoots.length; i++) {
+        // getMusicRoots() returns paths like "user:/Music/" — strip trailing slash
+        var cleanRoot = musicRoots[i].replace(/\/$/, "");
+        // Extract the disk identifier (part before the first colon)
+        var diskId = cleanRoot.split(":")[0] + ":";
+        result.push({ label: diskId, root: cleanRoot });
+    }
+    sendJSONResp(JSON.stringify(result));
+}
+
+main();

+ 17 - 1
src/web/Musicify/index.html

@@ -846,7 +846,23 @@
             <!-- ── FOLDERS ───────────────────────────────────────────────── -->
             <div x-show="view === 'folders' && !loading">
                 <div class="content-header">
-                    <h2>Folders</h2>
+                    <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;min-height:28px;">
+                        <h2 style="margin:0;">Folders</h2>
+                        <!-- Library source picker (only shown when multiple storage roots exist) -->
+                        <template x-if="musicLibraries.length > 1">
+                            <div style="display:flex;align-items:center;gap:6px;">
+                                <span style="font-size:12px;color:var(--text2);">Library:</span>
+                                <select style="background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:5px 10px;font-size:12px;outline:none;cursor:pointer;transition:border-color .15s;"
+                                        x-on:focus="$event.target.style.borderColor='var(--accent)'"
+                                        x-on:blur="$event.target.style.borderColor='var(--border)'"
+                                        x-on:change="switchLibrary($event.target.value)">
+                                    <template x-for="lib in musicLibraries" :key="lib.root">
+                                        <option :value="lib.root" :selected="folderRoot === lib.root" x-text="lib.label"></option>
+                                    </template>
+                                </select>
+                            </div>
+                        </template>
+                    </div>
                     <!-- Breadcrumb -->
                     <div class="breadcrumb" style="margin-top:8px;">
                         <template x-for="(crumb, ci) in getFolderBreadcrumbs()" :key="crumb.path">

+ 49 - 8
src/web/Musicify/musicify.js

@@ -22,6 +22,7 @@ function musicifyApp() {
         folderPath: 'user:/Music',
         folderStack: [],        // stack of previous paths for back navigation
         folderContents: { folders: [], songs: [] },
+        musicLibraries: [],     // [ { label, root } ] from listRoots.js
 
         // ── Artists ─────────────────────────────────────────────────────────
         artists: [],
@@ -127,6 +128,9 @@ function musicifyApp() {
             // Load playlists for sidebar
             this._loadPlaylists();
 
+            // Pre-load available music library roots for the folder-view switcher
+            this._loadMusicLibraries();
+
             // Register service worker
             if ('serviceWorker' in navigator) {
                 navigator.serviceWorker.register('sw.js').catch(function(){});
@@ -161,8 +165,11 @@ function musicifyApp() {
             this.searchQuery = '';
             if (window.innerWidth <= 768) this.sidebarOpen = false;
 
-            if (v === 'folders' && this.folderContents.songs.length === 0 && this.folderContents.folders.length === 0) {
-                this.loadFolder(this.folderRoot);
+            if (v === 'folders') {
+                if (this.musicLibraries.length === 0) this._loadMusicLibraries();
+                if (this.folderContents.songs.length === 0 && this.folderContents.folders.length === 0) {
+                    this.loadFolder(this.folderRoot);
+                }
             } else if (v === 'artists' && this.artists.length === 0) {
                 this._loadArtists();
             } else if (v === 'recent' && this.recentSongs.length === 0) {
@@ -177,23 +184,57 @@ function musicifyApp() {
             this._loadPlaylistSongs(name);
         },
 
+        // ════════════════════════════════════════════════════════════════════
+        //  LIBRARY ROOTS
+        // ════════════════════════════════════════════════════════════════════
+        _loadMusicLibraries() {
+            const self = this;
+            fetch(ao_root + 'system/ajgi/interface?script=Musicify/backend/listRoots.js', {
+                method: 'POST', cache: 'no-cache',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({})
+            }).then(r => r.json()).then(data => {
+                // Remove tmp:/ and trash:/ from the array 
+                data = Array.isArray(data) ? data.map(d => {
+                    if (d.root.startsWith('tmp:/') || d.root.startsWith('trash:/')) {
+                        return null;
+                    }
+                    return d;
+                }) : [];
+                self.musicLibraries = Array.isArray(data) ? data : [];
+            }).catch(() => {});
+        },
+
+        switchLibrary(root) {
+            this.folderRoot = root;
+            this.folderStack = [];
+            this.folderContents = { folders: [], songs: [] };
+            this.loadFolder(root, false);
+        },
+
         // ════════════════════════════════════════════════════════════════════
         //  FOLDER BROWSER
         // ════════════════════════════════════════════════════════════════════
-        loadFolder(path) {
-            this.loading = true;
-            this.loadingMsg = 'Loading folder…';
+        loadFolder(path, showLoading = true) {
+            if (showLoading) {
+                this.loadingMsg = 'Loading folder…';
+                this.loading = true;
+            }
             const self = this;
             fetch(ao_root + 'system/ajgi/interface?script=Musicify/backend/listFolder.js', {
                 method: 'POST', cache: 'no-cache',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({ folder: path })
             }).then(r => r.json()).then(data => {
-                if (data.error) { self._showToast(data.error, 'error'); self.loading = false; return; }
+                if (data.error) { self._showToast(data.error, 'error'); if (showLoading) self.loading = false; return; }
                 self.folderContents = data;
                 self.folderPath = path;
-                self.loading = false;
-            }).catch(() => { self.loading = false; });
+                if (showLoading) {
+                    setTimeout(() => { self.loading = false; }, 100); // slight delay for smoother UX
+                };
+            }).catch(() => { if (showLoading){
+                setTimeout(() => { self.loading = false; }, 100); // slight delay for smoother UX
+            } });
         },
 
         folderNavigate(path) {