Forráskód Böngészése

修改取百度网盘逻辑,修改排序

sequoia00 6 napja
szülő
commit
400799176c

+ 2 - 2
apk/app/build.gradle.kts

@@ -5,12 +5,12 @@ plugins {
 
 android {
     namespace = "com.musicweb.player"
-    compileSdk = 35
+    compileSdk = 34
 
     defaultConfig {
         applicationId = "com.musicweb.player"
         minSdk = 24
-        targetSdk = 35
+        targetSdk = 34
         versionCode = 1
         versionName = "1.0"
     }

+ 8 - 33
apk/app/src/main/java/com/musicweb/player/MainActivity.kt

@@ -13,11 +13,8 @@ import android.webkit.WebResourceRequest
 import android.webkit.WebSettings
 import android.webkit.WebView
 import android.webkit.WebViewClient
-import android.widget.EditText
-import android.widget.LinearLayout
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.ContextCompat
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.musicweb.player.player.PlayerService
 
 class MainActivity : AppCompatActivity() {
@@ -80,12 +77,7 @@ class MainActivity : AppCompatActivity() {
             webChromeClient = WebChromeClient()
         }
         setContentView(webView)
-
-        if (prefs.getString(KEY_BASE_URL, null).isNullOrBlank()) {
-            promptForUrl()
-        } else {
-            webView.loadUrl(requireBaseUrl())
-        }
+        webView.loadUrl(requireBaseUrl())
     }
 
     override fun onBackPressed() {
@@ -113,28 +105,6 @@ class MainActivity : AppCompatActivity() {
             .trimEnd('/')
     }
 
-    private fun promptForUrl() {
-        val input = EditText(this).apply {
-            setText(DEFAULT_BASE_URL)
-            setSelection(text.length)
-        }
-        val container = LinearLayout(this).apply {
-            setPadding(48, 32, 48, 0)
-            addView(input)
-        }
-        MaterialAlertDialogBuilder(this)
-            .setTitle(R.string.set_server_title)
-            .setMessage(R.string.set_server_message)
-            .setView(container)
-            .setCancelable(false)
-            .setPositiveButton(android.R.string.ok) { _, _ ->
-                val url = input.text?.toString()?.trim().orEmpty().ifBlank { DEFAULT_BASE_URL }
-                prefs.edit().putString(KEY_BASE_URL, url).apply()
-                webView.loadUrl(url.trimEnd('/'))
-            }
-            .show()
-    }
-
     private fun injectBridge() {
         val script = """
             (function() {
@@ -288,11 +258,16 @@ class MainActivity : AppCompatActivity() {
                 action = PlayerService.ACTION_PREVIOUS
             })
         }
+
+        @JavascriptInterface
+        fun saveBaseUrl(url: String) {
+            prefs.edit().putString(KEY_BASE_URL, url.trimEnd('/')).apply()
+        }
     }
 
     companion object {
-        private const val KEY_BASE_URL = "base_url"
-        private const val DEFAULT_BASE_URL = "http://110.42.102.94:8006/"
+        const val KEY_BASE_URL = "base_url"
+        const val DEFAULT_BASE_URL = "http://110.42.102.94:8006/"
     }
 }
 

+ 7 - 0
apk/app/src/main/res/values/strings.xml

@@ -2,4 +2,11 @@
     <string name="app_name">MusicWeb</string>
     <string name="set_server_title">设置云端地址</string>
     <string name="set_server_message">请输入可从安卓设备访问的音乐站点地址。</string>
+    <string name="settings_title">设置</string>
+    <string name="settings_server_label">服务器地址</string>
+    <string name="settings_url_hint">例如 http://192.168.1.100:8006</string>
+    <string name="settings_save">保存</string>
+    <string name="settings_error_empty">地址不能为空</string>
+    <string name="settings_error_protocol">地址必须以 http:// 或 https:// 开头</string>
+    <string name="settings_saved">已保存,重启应用生效</string>
 </resources>

+ 135 - 2
app/main.py

@@ -6,6 +6,7 @@ import shutil
 import threading
 import uuid
 import base64
+import re
 import urllib.error
 import urllib.parse
 import urllib.request
@@ -24,6 +25,18 @@ LOCAL_MUSIC_DIR = BASE_DIR / "mp3file"
 CLOUD_MUSIC_DIR = Path("/mnt/baiducloud/百度网盘/mp3file")
 CLOUD_WEBDAV_BASE = "http://110.42.102.94:5244/dav"
 CLOUD_ALIST_BASE = "http://110.42.102.94:5244"
+def _nat_sort_key(name: str) -> tuple:
+    """Sort key that treats numeric substrings as integers for natural ordering."""
+    parts = re.split(r'(\d+)', name.lower())
+    result = []
+    for part in parts:
+        if part.isdigit():
+            result.append((0, int(part), part))
+        else:
+            result.append((1, 0, part))
+    return result
+
+
 CLOUD_WEBDAV_USER = "sequoia00"
 CLOUD_WEBDAV_PASSWORD = "792199bb"
 MUSIC_ROOTS: list[tuple[str, Path, str]] = [
@@ -56,6 +69,7 @@ app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
 templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
 CLOUD_REFRESH_STATE = {"running": False}
 CLOUD_REFRESH_LOCK = threading.Lock()
+_ALIST_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
 
 
 class FolderCreateRequest(BaseModel):
@@ -166,6 +180,66 @@ def cloud_alist_api_path(relative_path: str) -> str:
     return f"/百度网盘/mp3file/{inner_path}" if inner_path else "/百度网盘/mp3file"
 
 
+def _alist_list_dir(alist_path: str, token: str) -> list[dict[str, Any]]:
+    """List files in an alist directory, handling pagination."""
+    items: list[dict[str, Any]] = []
+    page = 1
+    while True:
+        payload = json.dumps({"path": alist_path, "password": "", "page": page, "per_page": 200}).encode("utf-8")
+        request = urllib.request.Request(
+            f"{CLOUD_ALIST_BASE.rstrip('/')}/api/fs/list",
+            data=payload,
+            headers={
+                "Content-Type": "application/json",
+                "Authorization": token,
+                "User-Agent": "MusicWebPlayer/1.0",
+            },
+        )
+        try:
+            with urllib.request.urlopen(request, timeout=20) as response:
+                result = json.loads(response.read().decode("utf-8"))
+        except urllib.error.HTTPError:
+            break
+        except urllib.error.URLError:
+            break
+        data = result.get("data")
+        if not data or not isinstance(data, dict):
+            break
+        content_list = data.get("content", [])
+        if not content_list:
+            break
+        items.extend(content_list)
+        if not data.get("has_more"):
+            break
+        page += 1
+    return items
+
+
+def _get_alist_token() -> str | None:
+    """Get alist login token, cached briefly to avoid repeated logins."""
+    now = datetime.now(timezone.utc).timestamp()
+    cached = _ALIST_TOKEN_CACHE.get("token")
+    if cached and cached[1] > now:
+        return cached[0]
+    payload = json.dumps(
+        {"username": CLOUD_WEBDAV_USER, "password": CLOUD_WEBDAV_PASSWORD}
+    ).encode("utf-8")
+    request = urllib.request.Request(
+        f"{CLOUD_ALIST_BASE.rstrip('/')}/api/auth/login",
+        data=payload,
+        headers={"Content-Type": "application/json", "User-Agent": "MusicWebPlayer/1.0"},
+    )
+    try:
+        with urllib.request.urlopen(request, timeout=20) as response:
+            result = json.loads(response.read().decode("utf-8"))
+    except (urllib.error.HTTPError, urllib.error.URLError):
+        return None
+    token = result.get("data", {}).get("token")
+    if token:
+        _ALIST_TOKEN_CACHE["token"] = (token, now + 8000)
+    return token
+
+
 def alist_login_token() -> str:
     payload = json.dumps(
         {
@@ -272,7 +346,7 @@ def build_tree(root: Path, prefix: str = "", label: str = "音乐库") -> dict[s
         "folders": [],
         "tracks": [],
     }
-    for child in sorted(root.iterdir(), key=lambda item: (item.is_file(), item.name.lower())):
+    for child in sorted(root.iterdir(), key=lambda item: (item.is_file(), _nat_sort_key(item.name))):
         if child.is_dir():
             child_rel = child.relative_to(root).as_posix()
             child_prefix = f"{prefix}/{child_rel}" if prefix else child_rel
@@ -290,6 +364,36 @@ def collect_tracks(root: Path, prefix: str = "") -> list[dict[str, str]]:
     return tracks
 
 
+def _build_cloud_tree_from_alist(alist_path: str, prefix: str, label: str, token: str) -> dict[str, Any]:
+    """Build a library tree node by querying the alist API recursively."""
+    node = {
+        "name": label,
+        "path": prefix,
+        "cover": None,
+        "folders": [],
+        "tracks": [],
+    }
+    try:
+        items = _alist_list_dir(alist_path, token)
+    except Exception:
+        return node
+    folders: list[dict[str, Any]] = []
+    tracks: list[dict[str, str]] = []
+    for item in sorted(items, key=lambda i: (i.get("is_dir", 0), _nat_sort_key(i.get("name", "")))):
+        name = item.get("name", "")
+        is_dir = bool(item.get("is_dir"))
+        if is_dir:
+            child_prefix = f"{prefix}/{name}" if prefix else name
+            child_path = f"{alist_path.rstrip('/')}/{name}" if alist_path else name
+            folders.append(_build_cloud_tree_from_alist(child_path, child_prefix, name, token))
+        else:
+            track_path = f"{prefix}/{name}" if prefix else name
+            tracks.append(build_track(track_path))
+    node["folders"] = folders
+    node["tracks"] = tracks
+    return node
+
+
 def root_entry(source: str) -> tuple[str, Path, str]:
     for prefix, root, label in MUSIC_ROOTS:
         if prefix == source:
@@ -359,7 +463,36 @@ def write_cloud_library_cache(payload: dict[str, Any]) -> dict[str, Any]:
 
 
 def build_cloud_library_payload() -> dict[str, Any]:
-    return build_library_payload("cloud")
+    ensure_storage()
+    token = _get_alist_token()
+    if not token:
+        return {
+            "tree": {"name": "音乐库", "path": "", "cover": None, "folders": [], "tracks": []},
+            "all_tracks": [],
+            "playlists": load_playlists(),
+        }
+    try:
+        cloud_tree = _build_cloud_tree_from_alist("/百度网盘/mp3file", "cloud", "百度网盘", token)
+    except Exception:
+        cloud_tree = {"name": "百度网盘", "path": "cloud", "cover": None, "folders": [], "tracks": []}
+    all_tracks: list[dict[str, str]] = []
+    def _collect(node: dict[str, Any]) -> None:
+        all_tracks.extend(node.get("tracks", []))
+        for child in node.get("folders", []):
+            _collect(child)
+    _collect(cloud_tree)
+    sanitize_library_tree(cloud_tree)
+    return {
+        "tree": {
+            "name": "音乐库",
+            "path": "",
+            "cover": None,
+            "folders": [cloud_tree],
+            "tracks": [],
+        },
+        "all_tracks": all_tracks,
+        "playlists": load_playlists(),
+    }
 
 
 def cloud_library_payload_from_cache() -> dict[str, Any] | None:

+ 22 - 19
playlists.json

@@ -1,17 +1,4 @@
 [
-  {
-    "id": "d5e397954b4c48f9a4c6a4a345b261cb",
-    "name": "我的2",
-    "tracks": [
-      "经典老歌/212 伍佰 And China Blue-挪威的森林.mp3",
-      "经典老歌/211 吴奇隆-祝你一路顺风.mp3",
-      "陈小春/04 献世.mp3",
-      "陈小春/13 友情岁月.mp3",
-      "经典老歌/209 巫启贤-思念谁.mp3",
-      "经典老歌/059 黄安-新鸳鸯蝴蝶梦.mp3",
-      "经典老歌/086 李宗盛-凡人歌.mp3"
-    ]
-  },
   {
     "id": "69f403626eee44de9bb8c93d1b4cbd83",
     "name": "o8",
@@ -24,16 +11,32 @@
     ]
   },
   {
-    "id": "37ef81d77b7e4c2097c004e7b98d6384",
-    "name": "",
+    "id": "c688ccb2ced445b0948429107c069b8e",
+    "name": "",
     "tracks": [
       "李宗盛/CD 1/山丘.m4a",
       "李宗盛/CD 1/爱的代价.flac",
       "陈小春/13 友情岁月.mp3",
-      "周深/216.周深-生活总该迎着光亮.mp4",
-      "经典老歌/014 陈慧琳-记事本.mp3",
-      "经典老歌/019 陈琳-你的柔情我永远不懂.mp3",
-      "经典老歌/020 陈明真-梦醒时分.mp3"
+      "陈小春/04 献世.mp3",
+      "陈小春/16 成王败寇.mp3"
+    ]
+  },
+  {
+    "id": "135e45c7ed93465a93a8177930be9b5b",
+    "name": "齐",
+    "tracks": [
+      "cloud/齐秦-2011 齐秦精选[K2HD]/01.大约在冬季.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/02.冬雨.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/03.独行.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/04.外面的世界.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/05.哑口.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/06.狼Ⅱ(巡行).mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/07.狼.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/08.原来的我.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/09.花祭.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/10.九个太阳.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/11.感.mp3",
+      "cloud/齐秦-2011 齐秦精选[K2HD]/12.让我孤独的时候还能够想着你.mp3"
     ]
   }
 ]

+ 5 - 0
run.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+cd /home/myprojector/musicwebplay
+pkill -9 -f "uvicorn app.main" 2>/dev/null
+sleep 1
+exec python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8006 --no-access-log

+ 54 - 0
static/css/style.css

@@ -848,3 +848,57 @@ audio {
     bottom: max(10px, env(safe-area-inset-bottom, 0px));
   }
 }
+
+.settings-bar {
+  background: rgba(247, 251, 255, 0.97);
+  border-bottom: 1px solid var(--line);
+  padding: 10px 12px 12px;
+  position: sticky;
+  top: 0;
+  z-index: 200;
+  backdrop-filter: blur(16px);
+}
+.settings-bar-inner {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  max-width: 1180px;
+  margin: 0 auto;
+}
+.settings-bar-label {
+  font-size: 0.85rem;
+  color: var(--muted);
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+.settings-bar input[type="text"] {
+  flex: 1;
+  min-width: 0;
+  padding: 8px 12px;
+  font-size: 0.9rem;
+  border-radius: 10px;
+}
+.settings-save-btn {
+  padding: 8px 16px;
+  font-size: 0.88rem;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+.settings-open-btn {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 6px;
+  margin-left: 4px;
+  color: var(--muted);
+  flex-shrink: 0;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  line-height: 0;
+}
+.settings-open-btn:hover {
+  background: rgba(35, 136, 232, 0.08);
+  color: var(--accent);
+}

+ 28 - 0
static/js/app.js

@@ -1013,3 +1013,31 @@ document.querySelectorAll(".library-tab").forEach((button) => {
 
 updateToggleStates();
 fetchLibrary();
+(function initSettings() {
+  var saved = localStorage.getItem('musicweb_server_url');
+  if (saved) {
+    document.getElementById('settingsUrlInput').value = saved;
+  }
+  document.getElementById('settingsOpenBtn').onclick = function() {
+    document.getElementById('settingsBar').classList.remove('is-hidden');
+    document.getElementById('settingsUrlInput').focus();
+    document.getElementById('settingsUrlInput').select();
+    window.scrollTo({top: 0, behavior: 'smooth'});
+  };
+  document.getElementById('settingsCloseBtn').onclick = function() {
+    document.getElementById('settingsBar').classList.add('is-hidden');
+  };
+  document.getElementById('settingsSaveBtn').onclick = async function() {
+    var url = document.getElementById('settingsUrlInput').value.trim();
+    if (!url) { alert('地址不能为空'); return; }
+    if (!url.startsWith('http://') && !url.startsWith('https://')) { alert('地址必须以 http:// 或 https:// 开头'); return; }
+    localStorage.setItem('musicweb_server_url', url);
+    try { window.AndroidPlayer.saveBaseUrl(url.trimEnd('/')); } catch(e) {}
+    document.getElementById('settingsBar').classList.add('is-hidden');
+    showIconToast('已保存,页面即将刷新');
+    setTimeout(function() { location.reload(); }, 600);
+  };
+  document.getElementById('settingsUrlInput').addEventListener('keydown', function(e) {
+    if (e.key === 'Enter') document.getElementById('settingsSaveBtn').click();
+  });
+})();

+ 13 - 1
templates/index.html

@@ -7,6 +7,15 @@
     <link rel="stylesheet" href="/static/css/style.css" />
   </head>
   <body>
+      <div id="settingsBar" class="settings-bar is-hidden">
+        <div class="settings-bar-inner">
+          <div class="settings-bar-label">服务器地址</div>
+          <input id="settingsUrlInput" type="text" placeholder="如 http://192.168.1.100:8006" />
+          <button id="settingsSaveBtn" class="primary-btn settings-save-btn">保存</button>
+          <button id="settingsCloseBtn" class="text-btn">关闭</button>
+        </div>
+      </div>
+
     <div class="app-shell">
       <header class="app-header">
         <div class="header-copy">
@@ -15,6 +24,9 @@
           <p class="subtext">移动端专辑浏览与播放</p>
         </div>
         <div class="header-pill" title="当前目录:根目录">当前目录:<span id="folderIndicator">根目录</span></div>
+        <button id="settingsOpenBtn" class="settings-open-btn" aria-label="设置" title="设置">
+          <svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1 1 12 8.4a3.6 3.6 0 0 1 0 7.2z"/></svg>
+        </button>
       </header>
 
       <main class="page-content">
@@ -231,6 +243,6 @@
       </div>
     </template>
 
-    <script src="/static/js/app.js?v=20260511-1"></script>
+    <script src="/static/js/app.js?v=20260818"></script>
   </body>
 </html>