diff --git a/custom-api-readme.md b/custom-api-readme.md new file mode 100644 index 0000000..b5bae12 --- /dev/null +++ b/custom-api-readme.md @@ -0,0 +1,62 @@ +## 🎵 自定义音源API配置 + +现在支持通过导入一个简单的 JSON 配置文件来对接第三方的音乐解析 API。这将提供极大的灵活性,可以接入任意第三方音源。 + +### 如何使用 + +1. 前往 **设置 -> 播放设置 -> 音源设置**。 +2. 在 **自定义 API 设置** 区域,点击 **“导入 JSON 配置”** 按钮。 +3. 选择你已经编写好的 `xxx.json` 配置文件。 +4. 导入成功后,程序将优先使用你的自定义 API 进行解析。 + +### JSON 配置文件格式说明 + +导入的配置文件必须是一个合法的 JSON 文件,并包含以下字段: + +| 字段名 | 类型 | 是否必须 | 描述 | +| ---------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | 是 | API 名称,将显示在应用的 UI 界面上。 | +| `apiUrl` | `string` | 是 | API 的基础请求地址。 | +| `method` | `string` | 否 | HTTP 请求方法。可以是 `"GET"` 或 `"POST"`。**如果省略,默认为 "GET"**。 | +| `params` | `object` | 是 | 请求时需要发送的参数。对于 `GET` 请求,它们会作为查询字符串;对于 `POST` 请求,它们会作为请求体。 | +| `qualityMapping` | `object` | 否 | **音质映射表**。用于将应用内部的音质值(如 `"lossless"`)翻译成你的 API 需要的特定值。如果省略,则直接使用应用内部值。 | +| `responseUrlPath`| `string` | 是 | **URL提取路径**。用于从 API 返回的 JSON 响应中找到最终可播放的音乐链接。支持点 `.` 和方括号 `[]` 语法来访问嵌套对象和数组。 | + +#### 占位符 + +在 `params` 对象的值中,你可以使用以下占位符,程序在请求时会自动替换它们: + +* `{songId}`: 将被替换为当前歌曲的 ID。 +* `{quality}`: 将被替换为当前用户设置的音质字符串 (例如, `"higher"`, `"lossless"`)。 + +#### 音质值列表 + +应用内部使用的音质值如下,你可以在 `qualityMapping` 中使用它们作为**键**: +`standard`, `higher`, `exhigh`, `lossless`, `hires`, `jyeffect`, `sky`, `dolby`, `jymaster` + +### 示例 + +假设有一个 API 如下: +`https://api.example.com/music?song_id=12345&bitrate=320000` +它返回的 JSON 是: +`{ "code": 200, "data": { "play_url": "http://..." } }` + +那么对应的 JSON 配置文件应该是: + +```json +{ + "name": "Example API", + "apiUrl": "https://api.example.com/music", + "method": "GET", + "params": { + "song_id": "{songId}", + "bitrate": "{quality}" + }, + "qualityMapping": { + "higher": "128000", + "exhigh": "320000", + "lossless": "999000" + }, + "responseUrlPath": "data.play_url" +} +``` \ No newline at end of file diff --git a/package.json b/package.json index f50d80b..a74e856 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,14 @@ "electron-window-state": "^5.0.3", "express": "^4.18.2", "file-type": "^21.0.0", + "flac-tagger": "^1.0.7", "font-list": "^1.5.1", "husky": "^9.1.7", "music-metadata": "^11.2.3", "netease-cloud-music-api-alger": "^4.26.1", "node-id3": "^0.2.9", "node-machine-id": "^1.1.12", + "sharp": "^0.34.3", "vue-i18n": "^11.1.3" }, "devDependencies": { diff --git a/src/i18n/lang/en-US/player.ts b/src/i18n/lang/en-US/player.ts index 279d367..3a74211 100644 --- a/src/i18n/lang/en-US/player.ts +++ b/src/i18n/lang/en-US/player.ts @@ -39,7 +39,9 @@ export default { warning: 'Please select a music source', bilibiliNotSupported: 'Bilibili videos do not support reparsing', processing: 'Processing...', - clear: 'Clear Custom Source' + clear: 'Clear Custom Source', + customApiFailed: 'Custom API parsing failed, trying built-in sources...', + customApiError: 'Custom API request error, trying built-in sources...' }, playBar: { expand: 'Expand Lyrics', diff --git a/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index b5d3ebf..fcec2cd 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -80,7 +80,31 @@ export default { autoPlayDesc: 'Auto resume playback when reopening the app', showStatusBar: 'Show Status Bar', showStatusBarContent: - 'You can display the music control function in your mac status bar (effective after a restart)' + 'You can display the music control function in your mac status bar (effective after a restart)', + fallbackParser: 'Fallback Parser (GD Music)', + fallbackParserDesc: 'When "GD Music" is checked and regular sources fail, this service will be used.', + parserGD: 'GD Music (Built-in)', + parserCustom: 'Custom API', + + // Source labels + sourceLabels: { + migu: 'Migu', + kugou: 'Kugou', + pyncmd: 'NetEase (Built-in)', + bilibili: 'Bilibili', + gdmusic: 'GD Music', + custom: 'Custom API' + }, + + customApi: { + sectionTitle: 'Custom API Settings', + importConfig: 'Import JSON Config', + currentSource: 'Current Source', + notImported: 'No custom source imported yet.', + importSuccess: 'Successfully imported source: {name}', + importFailed: 'Import failed: {message}', + enableHint: 'Import a JSON config file to enable', + }, }, application: { closeAction: 'Close Action', diff --git a/src/i18n/lang/ja-JP/player.ts b/src/i18n/lang/ja-JP/player.ts index 21e4777..a90efde 100644 --- a/src/i18n/lang/ja-JP/player.ts +++ b/src/i18n/lang/ja-JP/player.ts @@ -1,123 +1,125 @@ -export default { - nowPlaying: '再生中', - playlist: 'プレイリスト', - lyrics: '歌詞', - previous: '前へ', - play: '再生', - pause: '一時停止', - next: '次へ', - volumeUp: '音量を上げる', - volumeDown: '音量を下げる', - mute: 'ミュート', - unmute: 'ミュート解除', - songNum: '楽曲総数:{num}', - addCorrection: '{num}秒早める', - subtractCorrection: '{num}秒遅らせる', - playFailed: '現在の楽曲の再生に失敗しました。次の曲を再生します', - playMode: { - sequence: '順次再生', - loop: 'リピート再生', - random: 'ランダム再生' - }, - fullscreen: { - enter: 'フルスクリーン', - exit: 'フルスクリーン終了' - }, - close: '閉じる', - modeHint: { - single: 'リピート再生', - list: '自動で次の曲を再生' - }, - lrc: { - noLrc: '歌詞がありません。お楽しみください' - }, - reparse: { - title: '解析音源を選択', - desc: '音源をクリックして直接解析します。次回この楽曲を再生する際は選択した音源を使用します', - success: '再解析成功', - failed: '再解析失敗', - warning: '音源を選択してください', - bilibiliNotSupported: 'Bilibili動画は再解析をサポートしていません', - processing: '解析中...', - clear: 'カスタム音源をクリア' - }, - playBar: { - expand: '歌詞を展開', - collapse: '歌詞を折りたたみ', - like: 'いいね', - lyric: '歌詞', - noSongPlaying: '再生中の楽曲がありません', - eq: 'イコライザー', - playList: 'プレイリスト', - reparse: '再解析', - playMode: { - sequence: '順次再生', - loop: 'ループ再生', - random: 'ランダム再生' - }, - play: '再生開始', - pause: '再生一時停止', - prev: '前の曲', - next: '次の曲', - volume: '音量', - favorite: '{name}をお気に入りに追加しました', - unFavorite: '{name}をお気に入りから削除しました', - miniPlayBar: 'ミニ再生バー', - playbackSpeed: '再生速度', - advancedControls: 'その他の設定' - }, - eq: { - title: 'イコライザー', - reset: 'リセット', - on: 'オン', - off: 'オフ', - bass: '低音', - midrange: '中音', - treble: '高音', - presets: { - flat: 'フラット', - pop: 'ポップ', - rock: 'ロック', - classical: 'クラシック', - jazz: 'ジャズ', - electronic: 'エレクトロニック', - hiphop: 'ヒップホップ', - rb: 'R&B', - metal: 'メタル', - vocal: 'ボーカル', - dance: 'ダンス', - acoustic: 'アコースティック', - custom: 'カスタム' - } - }, - // タイマー機能関連 - sleepTimer: { - title: 'スリープタイマー', - cancel: 'タイマーをキャンセル', - timeMode: '時間で停止', - songsMode: '楽曲数で停止', - playlistEnd: 'プレイリスト終了後に停止', - afterPlaylist: 'プレイリスト終了後に停止', - activeUntilEnd: 'リスト終了まで再生', - minutes: '分', - hours: '時間', - songs: '曲', - set: '設定', - timerSetSuccess: '{minutes}分後に停止するよう設定しました', - songsSetSuccess: '{songs}曲再生後に停止するよう設定しました', - playlistEndSetSuccess: 'プレイリスト終了後に停止するよう設定しました', - timerCancelled: 'スリープタイマーをキャンセルしました', - timerEnded: 'スリープタイマーが作動しました', - playbackStopped: '音楽再生を停止しました', - minutesRemaining: '残り{minutes}分', - songsRemaining: '残り{count}曲' - }, - playList: { - clearAll: 'プレイリストをクリア', - alreadyEmpty: 'プレイリストは既に空です', - cleared: 'プレイリストをクリアしました', - empty: 'プレイリストが空です', - clearConfirmTitle: 'プレイリストをクリア', - clearConfirmContent: 'これによりプレイリスト内のすべての楽曲がクリアされ、現在の再生が停止されます。続行しますか?' - } +export default { + nowPlaying: '再生中', + playlist: 'プレイリスト', + lyrics: '歌詞', + previous: '前へ', + play: '再生', + pause: '一時停止', + next: '次へ', + volumeUp: '音量を上げる', + volumeDown: '音量を下げる', + mute: 'ミュート', + unmute: 'ミュート解除', + songNum: '楽曲総数:{num}', + addCorrection: '{num}秒早める', + subtractCorrection: '{num}秒遅らせる', + playFailed: '現在の楽曲の再生に失敗しました。次の曲を再生します', + playMode: { + sequence: '順次再生', + loop: 'リピート再生', + random: 'ランダム再生' + }, + fullscreen: { + enter: 'フルスクリーン', + exit: 'フルスクリーン終了' + }, + close: '閉じる', + modeHint: { + single: 'リピート再生', + list: '自動で次の曲を再生' + }, + lrc: { + noLrc: '歌詞がありません。お楽しみください' + }, + reparse: { + title: '解析音源を選択', + desc: '音源をクリックして直接解析します。次回この楽曲を再生する際は選択した音源を使用します', + success: '再解析成功', + failed: '再解析失敗', + warning: '音源を選択してください', + bilibiliNotSupported: 'Bilibili動画は再解析をサポートしていません', + processing: '解析中...', + clear: 'カスタム音源をクリア', + customApiFailed: 'カスタムAPIの解析に失敗しました。内蔵音源を試しています...', + customApiError: 'カスタムAPIのリクエストでエラーが発生しました。内蔵音源を試しています...' + }, + playBar: { + expand: '歌詞を展開', + collapse: '歌詞を折りたたみ', + like: 'いいね', + lyric: '歌詞', + noSongPlaying: '再生中の楽曲がありません', + eq: 'イコライザー', + playList: 'プレイリスト', + reparse: '再解析', + playMode: { + sequence: '順次再生', + loop: 'ループ再生', + random: 'ランダム再生' + }, + play: '再生開始', + pause: '再生一時停止', + prev: '前の曲', + next: '次の曲', + volume: '音量', + favorite: '{name}をお気に入りに追加しました', + unFavorite: '{name}をお気に入りから削除しました', + miniPlayBar: 'ミニ再生バー', + playbackSpeed: '再生速度', + advancedControls: 'その他の設定' + }, + eq: { + title: 'イコライザー', + reset: 'リセット', + on: 'オン', + off: 'オフ', + bass: '低音', + midrange: '中音', + treble: '高音', + presets: { + flat: 'フラット', + pop: 'ポップ', + rock: 'ロック', + classical: 'クラシック', + jazz: 'ジャズ', + electronic: 'エレクトロニック', + hiphop: 'ヒップホップ', + rb: 'R&B', + metal: 'メタル', + vocal: 'ボーカル', + dance: 'ダンス', + acoustic: 'アコースティック', + custom: 'カスタム' + } + }, + // タイマー機能関連 + sleepTimer: { + title: 'スリープタイマー', + cancel: 'タイマーをキャンセル', + timeMode: '時間で停止', + songsMode: '楽曲数で停止', + playlistEnd: 'プレイリスト終了後に停止', + afterPlaylist: 'プレイリスト終了後に停止', + activeUntilEnd: 'リスト終了まで再生', + minutes: '分', + hours: '時間', + songs: '曲', + set: '設定', + timerSetSuccess: '{minutes}分後に停止するよう設定しました', + songsSetSuccess: '{songs}曲再生後に停止するよう設定しました', + playlistEndSetSuccess: 'プレイリスト終了後に停止するよう設定しました', + timerCancelled: 'スリープタイマーをキャンセルしました', + timerEnded: 'スリープタイマーが作動しました', + playbackStopped: '音楽再生を停止しました', + minutesRemaining: '残り{minutes}分', + songsRemaining: '残り{count}曲' + }, + playList: { + clearAll: 'プレイリストをクリア', + alreadyEmpty: 'プレイリストは既に空です', + cleared: 'プレイリストをクリアしました', + empty: 'プレイリストが空です', + clearConfirmTitle: 'プレイリストをクリア', + clearConfirmContent: 'これによりプレイリスト内のすべての楽曲がクリアされ、現在の再生が停止されます。続行しますか?' + } }; \ No newline at end of file diff --git a/src/i18n/lang/ja-JP/settings.ts b/src/i18n/lang/ja-JP/settings.ts index 182bf4d..c8f563d 100644 --- a/src/i18n/lang/ja-JP/settings.ts +++ b/src/i18n/lang/ja-JP/settings.ts @@ -1,322 +1,343 @@ -export default { - theme: 'テーマ', - language: '言語', - regard: 'について', - logout: 'ログアウト', - sections: { - basic: '基本設定', - playback: '再生設定', - application: 'アプリケーション設定', - network: 'ネットワーク設定', - system: 'システム管理', - donation: '寄付サポート', - regard: 'について' - }, - basic: { - themeMode: 'テーマモード', - themeModeDesc: 'ライト/ダークテーマの切り替え', - autoTheme: 'システムに従う', - manualTheme: '手動切り替え', - language: '言語設定', - languageDesc: '表示言語を切り替え', - tokenManagement: 'Cookie管理', - tokenManagementDesc: 'NetEase Cloud MusicログインCookieを管理', - tokenStatus: '現在のCookieステータス', - tokenSet: '設定済み', - tokenNotSet: '未設定', - setToken: 'Cookieを設定', - modifyToken: 'Cookieを変更', - clearToken: 'Cookieをクリア', - font: 'フォント設定', - fontDesc: 'フォントを選択します。前に配置されたフォントが優先されます', - fontScope: { - global: 'グローバル', - lyric: '歌詞のみ' - }, - animation: 'アニメーション速度', - animationDesc: 'アニメーションを有効にするかどうか', - animationSpeed: { - slow: '非常に遅い', - normal: '通常', - fast: '非常に速い' - }, - fontPreview: { - title: 'フォントプレビュー', - chinese: '中国語', - english: 'English', - japanese: '日本語', - korean: '韓国語', - chineseText: '静夜思 床前明月光 疑是地上霜', - englishText: 'The quick brown fox jumps over the lazy dog', - japaneseText: 'あいうえお かきくけこ さしすせそ', - koreanText: '가나다라마 바사아자차 카타파하' - } - }, - playback: { - quality: '音質設定', - qualityDesc: '音楽再生の音質を選択(NetEase Cloud VIP)', - qualityOptions: { - standard: '標準', - higher: '高音質', - exhigh: '超高音質', - lossless: 'ロスレス', - hires: 'Hi-Res', - jyeffect: 'HD サラウンド', - sky: 'イマーシブサラウンド', - dolby: 'Dolby Atmos', - jymaster: '超高解像度マスター' - }, - musicSources: '音源設定', - musicSourcesDesc: '音楽解析に使用する音源プラットフォームを選択', - musicSourcesWarning: '少なくとも1つの音源プラットフォームを選択する必要があります', - musicUnblockEnable: '音楽解析を有効にする', - musicUnblockEnableDesc: '有効にすると、再生できない音楽の解析を試みます', - configureMusicSources: '音源を設定', - selectedMusicSources: '選択された音源:', - noMusicSources: '音源が選択されていません', - gdmusicInfo: 'GD音楽台は複数のプラットフォーム音源を自動解析し、最適な結果を自動選択できます', - autoPlay: '自動再生', - autoPlayDesc: 'アプリを再起動した際に自動的に再生を継続するかどうか', - showStatusBar: 'ステータスバーコントロール機能を表示するかどうか', - showStatusBarContent: 'Macのステータスバーに音楽コントロール機能を表示できます(再起動後に有効)' - }, - application: { - closeAction: '閉じる動作', - closeActionDesc: 'ウィンドウを閉じる際の動作を選択', - closeOptions: { - ask: '毎回確認', - minimize: 'トレイに最小化', - close: '直接終了' - }, - shortcut: 'ショートカット設定', - shortcutDesc: 'グローバルショートカットをカスタマイズ', - download: 'ダウンロード管理', - downloadDesc: 'ダウンロードリストボタンを常に表示するかどうか', - unlimitedDownload: '無制限ダウンロード', - unlimitedDownloadDesc: '有効にすると音楽を無制限でダウンロードします(ダウンロード失敗の可能性があります)。デフォルトは300曲制限', - downloadPath: 'ダウンロードディレクトリ', - downloadPathDesc: '音楽ファイルのダウンロード場所を選択', - remoteControl: 'リモートコントロール', - remoteControlDesc: 'リモートコントロール機能を設定' - }, - network: { - apiPort: '音楽APIポート', - apiPortDesc: '変更後はアプリの再起動が必要です', - proxy: 'プロキシ設定', - proxyDesc: '音楽にアクセスできない場合はプロキシを有効にできます', - proxyHost: 'プロキシアドレス', - proxyHostPlaceholder: 'プロキシアドレスを入力してください', - proxyPort: 'プロキシポート', - proxyPortPlaceholder: 'プロキシポートを入力してください', - realIP: 'realIP設定', - realIPDesc: '制限により、このプロジェクトは海外での使用が制限されます。realIPパラメータを使用して国内IPを渡すことで解決できます', - messages: { - proxySuccess: 'プロキシ設定を保存しました。アプリ再起動後に有効になります', - proxyError: '入力が正しいかどうか確認してください', - realIPSuccess: '実IPアドレス設定を保存しました', - realIPError: '有効なIPアドレスを入力してください' - } - }, - system: { - cache: 'キャッシュ管理', - cacheDesc: 'キャッシュをクリア', - cacheClearTitle: 'クリアするキャッシュタイプを選択してください:', - cacheTypes: { - history: { - label: '再生履歴', - description: '再生した楽曲の記録をクリア' - }, - favorite: { - label: 'お気に入り記録', - description: 'ローカルのお気に入り楽曲記録をクリア(クラウドのお気に入りには影響しません)' - }, - user: { - label: 'ユーザーデータ', - description: 'ログイン情報とユーザー関連データをクリア' - }, - settings: { - label: 'アプリ設定', - description: 'アプリのすべてのカスタム設定をクリア' - }, - downloads: { - label: 'ダウンロード記録', - description: 'ダウンロード履歴をクリア(ダウンロード済みファイルは削除されません)' - }, - resources: { - label: '音楽リソース', - description: '読み込み済みの音楽ファイル、歌詞などのリソースキャッシュをクリア' - }, - lyrics: { - label: '歌詞リソース', - description: '読み込み済みの歌詞リソースキャッシュをクリア' - } - }, - restart: '再起動', - restartDesc: 'アプリを再起動', - messages: { - clearSuccess: 'クリア成功。一部の設定は再起動後に有効になります' - } - }, - about: { - version: 'バージョン', - checkUpdate: '更新を確認', - checking: '確認中...', - latest: '現在最新バージョンです', - hasUpdate: '新しいバージョンが見つかりました', - gotoUpdate: '更新へ', - gotoGithub: 'Githubへ', - author: '作者', - authorDesc: 'algerkong スターを付けてください🌟', - messages: { - checkError: '更新確認に失敗しました。後でもう一度お試しください' - } - }, - validation: { - selectProxyProtocol: 'プロキシプロトコルを選択してください', - proxyHost: 'プロキシアドレスを入力してください', - portNumber: '有効なポート番号を入力してください(1-65535)' - }, - lyricSettings: { - title: '歌詞設定', - tabs: { - display: '表示', - interface: 'インターフェース', - typography: 'テキスト', - mobile: 'モバイル' - }, - pureMode: 'ピュアモード', - hideCover: 'カバーを非表示', - centerDisplay: '中央表示', - showTranslation: '翻訳を表示', - hideLyrics: '歌詞を非表示', - hidePlayBar: '再生バーを非表示', - hideMiniPlayBar: 'ミニ再生バーを非表示', - showMiniPlayBar: 'ミニ再生バーを表示', - backgroundTheme: '背景テーマ', - themeOptions: { - default: 'デフォルト', - light: 'ライト', - dark: 'ダーク' - }, - fontSize: 'フォントサイズ', - fontSizeMarks: { - small: '小', - medium: '中', - large: '大' - }, - letterSpacing: '文字間隔', - letterSpacingMarks: { - compact: 'コンパクト', - default: 'デフォルト', - loose: 'ゆったり' - }, - lineHeight: '行の高さ', - lineHeightMarks: { - compact: 'コンパクト', - default: 'デフォルト', - loose: 'ゆったり' - }, - mobileLayout: 'モバイルレイアウト', - layoutOptions: { - default: 'デフォルト', - ios: 'iOSスタイル', - android: 'Androidスタイル' - }, - mobileCoverStyle: 'カバースタイル', - coverOptions: { - record: 'レコード', - square: '正方形', - full: 'フルスクリーン' - }, - lyricLines: '歌詞行数', - mobileUnavailable: 'この設定はモバイルでのみ利用可能です' - }, - themeColor: { - title: '歌詞テーマカラー', - presetColors: 'プリセットカラー', - customColor: 'カスタムカラー', - preview: 'プレビュー効果', - previewText: '歌詞効果', - colorNames: { - 'spotify-green': 'Spotify グリーン', - 'apple-blue': 'Apple ブルー', - 'youtube-red': 'YouTube レッド', - orange: 'バイタルオレンジ', - purple: 'ミステリアスパープル', - pink: 'サクラピンク' - }, - tooltips: { - openColorPicker: 'カラーパレットを開く', - closeColorPicker: 'カラーパレットを閉じる' - }, - placeholder: '#1db954' - }, - shortcutSettings: { - title: 'ショートカット設定', - shortcut: 'ショートカット', - shortcutDesc: 'ショートカットをカスタマイズ', - shortcutConflict: 'ショートカットの競合', - inputPlaceholder: 'クリックしてショートカットを入力', - resetShortcuts: 'デフォルトに戻す', - disableAll: 'すべて無効', - enableAll: 'すべて有効', - togglePlay: '再生/一時停止', - prevPlay: '前の曲', - nextPlay: '次の曲', - volumeUp: '音量を上げる', - volumeDown: '音量を下げる', - toggleFavorite: 'お気に入り/お気に入り解除', - toggleWindow: 'ウィンドウ表示/非表示', - scopeGlobal: 'グローバル', - scopeApp: 'アプリ内', - enabled: '有効', - disabled: '無効', - messages: { - resetSuccess: 'デフォルトのショートカットに戻しました。保存を忘れずに', - conflict: '競合するショートカットがあります。再設定してください', - saveSuccess: 'ショートカット設定を保存しました', - saveError: 'ショートカットの保存に失敗しました。再試行してください', - cancelEdit: '変更をキャンセルしました', - disableAll: 'すべてのショートカットを無効にしました。保存を忘れずに', - enableAll: 'すべてのショートカットを有効にしました。保存を忘れずに' - } - }, - remoteControl: { - title: 'リモートコントロール', - enable: 'リモートコントロールを有効にする', - port: 'サービスポート', - allowedIps: '許可されたIPアドレス', - addIp: 'IPを追加', - emptyListHint: '空のリストはすべてのIPアクセスを許可することを意味します', - saveSuccess: 'リモートコントロール設定を保存しました', - accessInfo: 'リモートコントロールアクセスアドレス:' - }, - cookie: { - title: 'Cookie設定', - description: 'NetEase Cloud MusicのCookieを入力してください:', - placeholder: '完全なCookieを貼り付けてください...', - help: { - format: 'Cookieは通常「MUSIC_U=」で始まります', - source: 'ブラウザの開発者ツールのネットワークリクエストから取得できます', - storage: 'Cookie設定後、自動的にローカルストレージに保存されます' - }, - action: { - save: 'Cookieを保存', - paste: '貼り付け', - clear: 'クリア' - }, - validation: { - required: 'Cookieを入力してください', - format: 'Cookie形式が正しくない可能性があります。MUSIC_Uが含まれているか確認してください' - }, - message: { - saveSuccess: 'Cookieの保存に成功しました', - saveError: 'Cookieの保存に失敗しました', - pasteSuccess: '貼り付けに成功しました', - pasteError: '貼り付けに失敗しました。手動でコピーしてください' - }, - info: { - length: '現在の長さ:{length} 文字' - } - } -}; \ No newline at end of file +export default { + theme: 'テーマ', + language: '言語', + regard: 'について', + logout: 'ログアウト', + sections: { + basic: '基本設定', + playback: '再生設定', + application: 'アプリケーション設定', + network: 'ネットワーク設定', + system: 'システム管理', + donation: '寄付サポート', + regard: 'について' + }, + basic: { + themeMode: 'テーマモード', + themeModeDesc: 'ライト/ダークテーマの切り替え', + autoTheme: 'システムに従う', + manualTheme: '手動切り替え', + language: '言語設定', + languageDesc: '表示言語を切り替え', + tokenManagement: 'Cookie管理', + tokenManagementDesc: 'NetEase Cloud MusicログインCookieを管理', + tokenStatus: '現在のCookieステータス', + tokenSet: '設定済み', + tokenNotSet: '未設定', + setToken: 'Cookieを設定', + modifyToken: 'Cookieを変更', + clearToken: 'Cookieをクリア', + font: 'フォント設定', + fontDesc: 'フォントを選択します。前に配置されたフォントが優先されます', + fontScope: { + global: 'グローバル', + lyric: '歌詞のみ' + }, + animation: 'アニメーション速度', + animationDesc: 'アニメーションを有効にするかどうか', + animationSpeed: { + slow: '非常に遅い', + normal: '通常', + fast: '非常に速い' + }, + fontPreview: { + title: 'フォントプレビュー', + chinese: '中国語', + english: 'English', + japanese: '日本語', + korean: '韓国語', + chineseText: '静夜思 床前明月光 疑是地上霜', + englishText: 'The quick brown fox jumps over the lazy dog', + japaneseText: 'あいうえお かきくけこ さしすせそ', + koreanText: '가나다라마 바사아자차 카타파하' + } + }, + playback: { + quality: '音質設定', + qualityDesc: '音楽再生の音質を選択(NetEase Cloud VIP)', + qualityOptions: { + standard: '標準', + higher: '高音質', + exhigh: '超高音質', + lossless: 'ロスレス', + hires: 'Hi-Res', + jyeffect: 'HD サラウンド', + sky: 'イマーシブサラウンド', + dolby: 'Dolby Atmos', + jymaster: '超高解像度マスター' + }, + musicSources: '音源設定', + musicSourcesDesc: '音楽解析に使用する音源プラットフォームを選択', + musicSourcesWarning: '少なくとも1つの音源プラットフォームを選択する必要があります', + musicUnblockEnable: '音楽解析を有効にする', + musicUnblockEnableDesc: '有効にすると、再生できない音楽の解析を試みます', + configureMusicSources: '音源を設定', + selectedMusicSources: '選択された音源:', + noMusicSources: '音源が選択されていません', + gdmusicInfo: 'GD音楽台は複数のプラットフォーム音源を自動解析し、最適な結果を自動選択できます', + autoPlay: '自動再生', + autoPlayDesc: 'アプリを再起動した際に自動的に再生を継続するかどうか', + showStatusBar: 'ステータスバーコントロール機能を表示するかどうか', + showStatusBarContent: 'Macのステータスバーに音楽コントロール機能を表示できます(再起動後に有効)', + fallbackParser: '代替解析サービス (GD音楽台)', + fallbackParserDesc: '「GD音楽台」にチェックが入っていて、通常の音源で再生できない場合、このサービスが使用されます。', + parserGD: 'GD 音楽台 (内蔵)', + parserCustom: 'カスタム API', + sourceLabels: { + migu: 'Migu', + kugou: 'Kugou', + pyncmd: 'NetEase (内蔵)', + bilibili: 'Bilibili', + gdmusic: 'GD 音楽台', + custom: 'カスタム API' + }, + customApi: { + sectionTitle: 'カスタム API 設定', + enableHint: 'カスタム API を有効にするには、まずカスタム API をインポートする必要があります。', + importConfig: 'JSON設定をインポート', + currentSource: '現在の音源', + notImported: 'カスタム音源はまだインポートされていません。', + importSuccess: '音源のインポートに成功しました: {name}', + importFailed: 'インポートに失敗しました: {message}', + }, + }, + application: { + closeAction: '閉じる動作', + closeActionDesc: 'ウィンドウを閉じる際の動作を選択', + closeOptions: { + ask: '毎回確認', + minimize: 'トレイに最小化', + close: '直接終了' + }, + shortcut: 'ショートカット設定', + shortcutDesc: 'グローバルショートカットをカスタマイズ', + download: 'ダウンロード管理', + downloadDesc: 'ダウンロードリストボタンを常に表示するかどうか', + unlimitedDownload: '無制限ダウンロード', + unlimitedDownloadDesc: '有効にすると音楽を無制限でダウンロードします(ダウンロード失敗の可能性があります)。デフォルトは300曲制限', + downloadPath: 'ダウンロードディレクトリ', + downloadPathDesc: '音楽ファイルのダウンロード場所を選択', + remoteControl: 'リモートコントロール', + remoteControlDesc: 'リモートコントロール機能を設定' + }, + network: { + apiPort: '音楽APIポート', + apiPortDesc: '変更後はアプリの再起動が必要です', + proxy: 'プロキシ設定', + proxyDesc: '音楽にアクセスできない場合はプロキシを有効にできます', + proxyHost: 'プロキシアドレス', + proxyHostPlaceholder: 'プロキシアドレスを入力してください', + proxyPort: 'プロキシポート', + proxyPortPlaceholder: 'プロキシポートを入力してください', + realIP: 'realIP設定', + realIPDesc: '制限により、このプロジェクトは海外での使用が制限されます。realIPパラメータを使用して国内IPを渡すことで解決できます', + messages: { + proxySuccess: 'プロキシ設定を保存しました。アプリ再起動後に有効になります', + proxyError: '入力が正しいかどうか確認してください', + realIPSuccess: '実IPアドレス設定を保存しました', + realIPError: '有効なIPアドレスを入力してください' + } + }, + system: { + cache: 'キャッシュ管理', + cacheDesc: 'キャッシュをクリア', + cacheClearTitle: 'クリアするキャッシュタイプを選択してください:', + cacheTypes: { + history: { + label: '再生履歴', + description: '再生した楽曲の記録をクリア' + }, + favorite: { + label: 'お気に入り記録', + description: 'ローカルのお気に入り楽曲記録をクリア(クラウドのお気に入りには影響しません)' + }, + user: { + label: 'ユーザーデータ', + description: 'ログイン情報とユーザー関連データをクリア' + }, + settings: { + label: 'アプリ設定', + description: 'アプリのすべてのカスタム設定をクリア' + }, + downloads: { + label: 'ダウンロード記録', + description: 'ダウンロード履歴をクリア(ダウンロード済みファイルは削除されません)' + }, + resources: { + label: '音楽リソース', + description: '読み込み済みの音楽ファイル、歌詞などのリソースキャッシュをクリア' + }, + lyrics: { + label: '歌詞リソース', + description: '読み込み済みの歌詞リソースキャッシュをクリア' + } + }, + restart: '再起動', + restartDesc: 'アプリを再起動', + messages: { + clearSuccess: 'クリア成功。一部の設定は再起動後に有効になります' + } + }, + about: { + version: 'バージョン', + checkUpdate: '更新を確認', + checking: '確認中...', + latest: '現在最新バージョンです', + hasUpdate: '新しいバージョンが見つかりました', + gotoUpdate: '更新へ', + gotoGithub: 'Githubへ', + author: '作者', + authorDesc: 'algerkong スターを付けてください🌟', + messages: { + checkError: '更新確認に失敗しました。後でもう一度お試しください' + } + }, + validation: { + selectProxyProtocol: 'プロキシプロトコルを選択してください', + proxyHost: 'プロキシアドレスを入力してください', + portNumber: '有効なポート番号を入力してください(1-65535)' + }, + lyricSettings: { + title: '歌詞設定', + tabs: { + display: '表示', + interface: 'インターフェース', + typography: 'テキスト', + mobile: 'モバイル' + }, + pureMode: 'ピュアモード', + hideCover: 'カバーを非表示', + centerDisplay: '中央表示', + showTranslation: '翻訳を表示', + hideLyrics: '歌詞を非表示', + hidePlayBar: '再生バーを非表示', + hideMiniPlayBar: 'ミニ再生バーを非表示', + showMiniPlayBar: 'ミニ再生バーを表示', + backgroundTheme: '背景テーマ', + themeOptions: { + default: 'デフォルト', + light: 'ライト', + dark: 'ダーク' + }, + fontSize: 'フォントサイズ', + fontSizeMarks: { + small: '小', + medium: '中', + large: '大' + }, + letterSpacing: '文字間隔', + letterSpacingMarks: { + compact: 'コンパクト', + default: 'デフォルト', + loose: 'ゆったり' + }, + lineHeight: '行の高さ', + lineHeightMarks: { + compact: 'コンパクト', + default: 'デフォルト', + loose: 'ゆったり' + }, + mobileLayout: 'モバイルレイアウト', + layoutOptions: { + default: 'デフォルト', + ios: 'iOSスタイル', + android: 'Androidスタイル' + }, + mobileCoverStyle: 'カバースタイル', + coverOptions: { + record: 'レコード', + square: '正方形', + full: 'フルスクリーン' + }, + lyricLines: '歌詞行数', + mobileUnavailable: 'この設定はモバイルでのみ利用可能です' + }, + themeColor: { + title: '歌詞テーマカラー', + presetColors: 'プリセットカラー', + customColor: 'カスタムカラー', + preview: 'プレビュー効果', + previewText: '歌詞効果', + colorNames: { + 'spotify-green': 'Spotify グリーン', + 'apple-blue': 'Apple ブルー', + 'youtube-red': 'YouTube レッド', + orange: 'バイタルオレンジ', + purple: 'ミステリアスパープル', + pink: 'サクラピンク' + }, + tooltips: { + openColorPicker: 'カラーパレットを開く', + closeColorPicker: 'カラーパレットを閉じる' + }, + placeholder: '#1db954' + }, + shortcutSettings: { + title: 'ショートカット設定', + shortcut: 'ショートカット', + shortcutDesc: 'ショートカットをカスタマイズ', + shortcutConflict: 'ショートカットの競合', + inputPlaceholder: 'クリックしてショートカットを入力', + resetShortcuts: 'デフォルトに戻す', + disableAll: 'すべて無効', + enableAll: 'すべて有効', + togglePlay: '再生/一時停止', + prevPlay: '前の曲', + nextPlay: '次の曲', + volumeUp: '音量を上げる', + volumeDown: '音量を下げる', + toggleFavorite: 'お気に入り/お気に入り解除', + toggleWindow: 'ウィンドウ表示/非表示', + scopeGlobal: 'グローバル', + scopeApp: 'アプリ内', + enabled: '有効', + disabled: '無効', + messages: { + resetSuccess: 'デフォルトのショートカットに戻しました。保存を忘れずに', + conflict: '競合するショートカットがあります。再設定してください', + saveSuccess: 'ショートカット設定を保存しました', + saveError: 'ショートカットの保存に失敗しました。再試行してください', + cancelEdit: '変更をキャンセルしました', + disableAll: 'すべてのショートカットを無効にしました。保存を忘れずに', + enableAll: 'すべてのショートカットを有効にしました。保存を忘れずに' + } + }, + remoteControl: { + title: 'リモートコントロール', + enable: 'リモートコントロールを有効にする', + port: 'サービスポート', + allowedIps: '許可されたIPアドレス', + addIp: 'IPを追加', + emptyListHint: '空のリストはすべてのIPアクセスを許可することを意味します', + saveSuccess: 'リモートコントロール設定を保存しました', + accessInfo: 'リモートコントロールアクセスアドレス:' + }, + cookie: { + title: 'Cookie設定', + description: 'NetEase Cloud MusicのCookieを入力してください:', + placeholder: '完全なCookieを貼り付けてください...', + help: { + format: 'Cookieは通常「MUSIC_U=」で始まります', + source: 'ブラウザの開発者ツールのネットワークリクエストから取得できます', + storage: 'Cookie設定後、自動的にローカルストレージに保存されます' + }, + action: { + save: 'Cookieを保存', + paste: '貼り付け', + clear: 'クリア' + }, + validation: { + required: 'Cookieを入力してください', + format: 'Cookie形式が正しくない可能性があります。MUSIC_Uが含まれているか確認してください' + }, + message: { + saveSuccess: 'Cookieの保存に成功しました', + saveError: 'Cookieの保存に失敗しました', + pasteSuccess: '貼り付けに成功しました', + pasteError: '貼り付けに失敗しました。手動でコピーしてください' + }, + info: { + length: '現在の長さ:{length} 文字' + } + } +}; diff --git a/src/i18n/lang/ko-KR/player.ts b/src/i18n/lang/ko-KR/player.ts index 4d3c302..5a1c8b0 100644 --- a/src/i18n/lang/ko-KR/player.ts +++ b/src/i18n/lang/ko-KR/player.ts @@ -1,122 +1,124 @@ -export default { - nowPlaying: '현재 재생 중', - playlist: '재생 목록', - lyrics: '가사', - previous: '이전', - play: '재생', - pause: '일시정지', - next: '다음', - volumeUp: '볼륨 증가', - volumeDown: '볼륨 감소', - mute: '음소거', - unmute: '음소거 해제', - songNum: '총 곡 수: {num}', - addCorrection: '{num}초 앞당기기', - subtractCorrection: '{num}초 지연', - playFailed: '현재 곡 재생 실패, 다음 곡 재생', - playMode: { - sequence: '순차 재생', - loop: '한 곡 반복', - random: '랜덤 재생' - }, - fullscreen: { - enter: '전체화면', - exit: '전체화면 종료' - }, - close: '닫기', - modeHint: { - single: '한 곡 반복', - list: '자동으로 다음 곡 재생' - }, - lrc: { - noLrc: '가사가 없습니다. 음악을 감상해주세요' - }, - reparse: { - title: '음원 선택', - desc: '음원을 클릭하여 직접 분석하세요. 다음에 이 곡을 재생할 때 선택한 음원을 사용합니다', - success: '재분석 성공', - failed: '재분석 실패', - warning: '음원을 선택해주세요', - bilibiliNotSupported: 'B站 비디오는 재분석을 지원하지 않습니다', - processing: '분석 중...', - clear: '사용자 정의 음원 지우기' - }, - playBar: { - expand: '가사 펼치기', - collapse: '가사 접기', - like: '좋아요', - lyric: '가사', - noSongPlaying: '재생 중인 곡이 없습니다', - eq: '이퀄라이저', - playList: '재생 목록', - reparse: '재분석', - playMode: { - sequence: '순차 재생', - loop: '반복 재생', - random: '랜덤 재생' - }, - play: '재생 시작', - pause: '재생 일시정지', - prev: '이전 곡', - next: '다음 곡', - volume: '볼륨', - favorite: '{name} 즐겨찾기 추가됨', - unFavorite: '{name} 즐겨찾기 해제됨', - miniPlayBar: '미니 재생바', - playbackSpeed: '재생 속도', - advancedControls: '고급 설정' - }, - eq: { - title: '이퀄라이저', - reset: '재설정', - on: '켜기', - off: '끄기', - bass: '저음', - midrange: '중음', - treble: '고음', - presets: { - flat: '플랫', - pop: '팝', - rock: '록', - classical: '클래식', - jazz: '재즈', - electronic: '일렉트로닉', - hiphop: '힙합', - rb: 'R&B', - metal: '메탈', - vocal: '보컬', - dance: '댄스', - acoustic: '어쿠스틱', - custom: '사용자 정의' - } - }, - sleepTimer: { - title: '타이머 종료', - cancel: '타이머 취소', - timeMode: '시간으로 종료', - songsMode: '곡 수로 종료', - playlistEnd: '재생 목록 완료 후 종료', - afterPlaylist: '재생 목록 완료 후 종료', - activeUntilEnd: '목록 끝까지 재생', - minutes: '분', - hours: '시간', - songs: '곡', - set: '설정', - timerSetSuccess: '{minutes}분 후 종료로 설정됨', - songsSetSuccess: '{songs}곡 재생 후 종료로 설정됨', - playlistEndSetSuccess: '재생 목록 완료 후 종료로 설정됨', - timerCancelled: '타이머 종료 취소됨', - timerEnded: '타이머 종료 실행됨', - playbackStopped: '음악 재생이 중지됨', - minutesRemaining: '남은 시간 {minutes}분', - songsRemaining: '남은 곡 수 {count}곡' - }, - playList: { - clearAll: '재생 목록 비우기', - alreadyEmpty: '재생 목록이 이미 비어있습니다', - cleared: '재생 목록이 비워졌습니다', - empty: '재생 목록이 비어있습니다', - clearConfirmTitle: '재생 목록 비우기', - clearConfirmContent: '재생 목록의 모든 곡을 삭제하고 현재 재생을 중지합니다. 계속하시겠습니까?' - } +export default { + nowPlaying: '현재 재생 중', + playlist: '재생 목록', + lyrics: '가사', + previous: '이전', + play: '재생', + pause: '일시정지', + next: '다음', + volumeUp: '볼륨 증가', + volumeDown: '볼륨 감소', + mute: '음소거', + unmute: '음소거 해제', + songNum: '총 곡 수: {num}', + addCorrection: '{num}초 앞당기기', + subtractCorrection: '{num}초 지연', + playFailed: '현재 곡 재생 실패, 다음 곡 재생', + playMode: { + sequence: '순차 재생', + loop: '한 곡 반복', + random: '랜덤 재생' + }, + fullscreen: { + enter: '전체화면', + exit: '전체화면 종료' + }, + close: '닫기', + modeHint: { + single: '한 곡 반복', + list: '자동으로 다음 곡 재생' + }, + lrc: { + noLrc: '가사가 없습니다. 음악을 감상해주세요' + }, + reparse: { + title: '음원 선택', + desc: '음원을 클릭하여 직접 분석하세요. 다음에 이 곡을 재생할 때 선택한 음원을 사용합니다', + success: '재분석 성공', + failed: '재분석 실패', + warning: '음원을 선택해주세요', + bilibiliNotSupported: 'B站 비디오는 재분석을 지원하지 않습니다', + processing: '분석 중...', + clear: '사용자 정의 음원 지우기', + customApiFailed: '사용자 정의 API 분석 실패, 기본 음원을 시도합니다...', + customApiError: '사용자 정의 API 요청 오류, 기본 음원을 시도합니다...' + }, + playBar: { + expand: '가사 펼치기', + collapse: '가사 접기', + like: '좋아요', + lyric: '가사', + noSongPlaying: '재생 중인 곡이 없습니다', + eq: '이퀄라이저', + playList: '재생 목록', + reparse: '재분석', + playMode: { + sequence: '순차 재생', + loop: '반복 재생', + random: '랜덤 재생' + }, + play: '재생 시작', + pause: '재생 일시정지', + prev: '이전 곡', + next: '다음 곡', + volume: '볼륨', + favorite: '{name} 즐겨찾기 추가됨', + unFavorite: '{name} 즐겨찾기 해제됨', + miniPlayBar: '미니 재생바', + playbackSpeed: '재생 속도', + advancedControls: '고급 설정' + }, + eq: { + title: '이퀄라이저', + reset: '재설정', + on: '켜기', + off: '끄기', + bass: '저음', + midrange: '중음', + treble: '고음', + presets: { + flat: '플랫', + pop: '팝', + rock: '록', + classical: '클래식', + jazz: '재즈', + electronic: '일렉트로닉', + hiphop: '힙합', + rb: 'R&B', + metal: '메탈', + vocal: '보컬', + dance: '댄스', + acoustic: '어쿠스틱', + custom: '사용자 정의' + } + }, + sleepTimer: { + title: '타이머 종료', + cancel: '타이머 취소', + timeMode: '시간으로 종료', + songsMode: '곡 수로 종료', + playlistEnd: '재생 목록 완료 후 종료', + afterPlaylist: '재생 목록 완료 후 종료', + activeUntilEnd: '목록 끝까지 재생', + minutes: '분', + hours: '시간', + songs: '곡', + set: '설정', + timerSetSuccess: '{minutes}분 후 종료로 설정됨', + songsSetSuccess: '{songs}곡 재생 후 종료로 설정됨', + playlistEndSetSuccess: '재생 목록 완료 후 종료로 설정됨', + timerCancelled: '타이머 종료 취소됨', + timerEnded: '타이머 종료 실행됨', + playbackStopped: '음악 재생이 중지됨', + minutesRemaining: '남은 시간 {minutes}분', + songsRemaining: '남은 곡 수 {count}곡' + }, + playList: { + clearAll: '재생 목록 비우기', + alreadyEmpty: '재생 목록이 이미 비어있습니다', + cleared: '재생 목록이 비워졌습니다', + empty: '재생 목록이 비어있습니다', + clearConfirmTitle: '재생 목록 비우기', + clearConfirmContent: '재생 목록의 모든 곡을 삭제하고 현재 재생을 중지합니다. 계속하시겠습니까?' + } }; \ No newline at end of file diff --git a/src/i18n/lang/ko-KR/settings.ts b/src/i18n/lang/ko-KR/settings.ts index f5621c2..4169ffc 100644 --- a/src/i18n/lang/ko-KR/settings.ts +++ b/src/i18n/lang/ko-KR/settings.ts @@ -1,322 +1,346 @@ -export default { - theme: '테마', - language: '언어', - regard: '정보', - logout: '로그아웃', - sections: { - basic: '기본 설정', - playback: '재생 설정', - application: '애플리케이션 설정', - network: '네트워크 설정', - system: '시스템 관리', - donation: '후원 지원', - regard: '정보' - }, - basic: { - themeMode: '테마 모드', - themeModeDesc: '낮/밤 테마 전환', - autoTheme: '시스템 따라가기', - manualTheme: '수동 전환', - language: '언어 설정', - languageDesc: '표시 언어 전환', - tokenManagement: 'Cookie 관리', - tokenManagementDesc: '넷이즈 클라우드 뮤직 로그인 Cookie 관리', - tokenStatus: '현재 Cookie 상태', - tokenSet: '설정됨', - tokenNotSet: '설정되지 않음', - setToken: 'Cookie 설정', - modifyToken: 'Cookie 수정', - clearToken: 'Cookie 지우기', - font: '폰트 설정', - fontDesc: '폰트 선택, 앞에 있는 폰트를 우선 사용', - fontScope: { - global: '전역', - lyric: '가사만' - }, - animation: '애니메이션 속도', - animationDesc: '애니메이션 활성화 여부', - animationSpeed: { - slow: '매우 느림', - normal: '보통', - fast: '매우 빠름' - }, - fontPreview: { - title: '폰트 미리보기', - chinese: '中文', - english: 'English', - japanese: '日本語', - korean: '한국어', - chineseText: '静夜思 床前明月光 疑是地上霜', - englishText: 'The quick brown fox jumps over the lazy dog', - japaneseText: 'あいうえお かきくけこ さしすせそ', - koreanText: '가나다라마 바사아자차 카타파하' - } - }, - playback: { - quality: '음질 설정', - qualityDesc: '음악 재생 음질 선택 (넷이즈 클라우드 VIP)', - qualityOptions: { - standard: '표준', - higher: '높음', - exhigh: '매우 높음', - lossless: '무손실', - hires: 'Hi-Res', - jyeffect: 'HD 서라운드', - sky: '몰입형 서라운드', - dolby: '돌비 애트모스', - jymaster: '초고화질 마스터' - }, - musicSources: '음원 설정', - musicSourcesDesc: '음악 해석에 사용할 음원 플랫폼 선택', - musicSourcesWarning: '최소 하나의 음원 플랫폼을 선택해야 합니다', - musicUnblockEnable: '음악 해석 활성화', - musicUnblockEnableDesc: '활성화하면 재생할 수 없는 음악을 해석하려고 시도합니다', - configureMusicSources: '음원 구성', - selectedMusicSources: '선택된 음원:', - noMusicSources: '음원이 선택되지 않음', - gdmusicInfo: 'GD 뮤직은 여러 플랫폼 음원을 자동으로 해석하고 최적의 결과를 자동 선택합니다', - autoPlay: '자동 재생', - autoPlayDesc: '앱을 다시 열 때 자동으로 재생을 계속할지 여부', - showStatusBar: '상태바 제어 기능 표시 여부', - showStatusBarContent: 'Mac 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)' - }, - application: { - closeAction: '닫기 동작', - closeActionDesc: '창을 닫을 때의 동작 선택', - closeOptions: { - ask: '매번 묻기', - minimize: '트레이로 최소화', - close: '직접 종료' - }, - shortcut: '단축키 설정', - shortcutDesc: '전역 단축키 사용자 정의', - download: '다운로드 관리', - downloadDesc: '다운로드 목록 버튼을 항상 표시할지 여부', - unlimitedDownload: '무제한 다운로드', - unlimitedDownloadDesc: '활성화하면 음악을 무제한으로 다운로드합니다 (다운로드 실패가 발생할 수 있음), 기본 제한 300곡', - downloadPath: '다운로드 디렉토리', - downloadPathDesc: '음악 파일의 다운로드 위치 선택', - remoteControl: '원격 제어', - remoteControlDesc: '원격 제어 기능 설정' - }, - network: { - apiPort: '음악 API 포트', - apiPortDesc: '수정 후 앱을 재시작해야 합니다', - proxy: '프록시 설정', - proxyDesc: '음악에 액세스할 수 없을 때 프록시를 활성화할 수 있습니다', - proxyHost: '프록시 주소', - proxyHostPlaceholder: '프록시 주소를 입력하세요', - proxyPort: '프록시 포트', - proxyPortPlaceholder: '프록시 포트를 입력하세요', - realIP: 'realIP 설정', - realIPDesc: '제한으로 인해 이 프로젝트는 해외에서 사용할 때 제한을 받을 수 있으며, realIP 매개변수를 사용하여 국내 IP를 전달하여 해결할 수 있습니다', - messages: { - proxySuccess: '프록시 설정이 저장되었습니다. 앱을 재시작한 후 적용됩니다', - proxyError: '입력이 올바른지 확인하세요', - realIPSuccess: '실제 IP 설정이 저장되었습니다', - realIPError: '유효한 IP 주소를 입력하세요' - } - }, - system: { - cache: '캐시 관리', - cacheDesc: '캐시 지우기', - cacheClearTitle: '지울 캐시 유형을 선택하세요:', - cacheTypes: { - history: { - label: '재생 기록', - description: '재생한 곡 기록 지우기' - }, - favorite: { - label: '즐겨찾기 기록', - description: '로컬 즐겨찾기 곡 기록 지우기 (클라우드 즐겨찾기에는 영향 없음)' - }, - user: { - label: '사용자 데이터', - description: '로그인 정보 및 사용자 관련 데이터 지우기' - }, - settings: { - label: '앱 설정', - description: '앱의 모든 사용자 정의 설정 지우기' - }, - downloads: { - label: '다운로드 기록', - description: '다운로드 기록 지우기 (다운로드된 파일은 삭제되지 않음)' - }, - resources: { - label: '음악 리소스', - description: '로드된 음악 파일, 가사 등 리소스 캐시 지우기' - }, - lyrics: { - label: '가사 리소스', - description: '로드된 가사 리소스 캐시 지우기' - } - }, - restart: '재시작', - restartDesc: '앱 재시작', - messages: { - clearSuccess: '지우기 성공, 일부 설정은 재시작 후 적용됩니다' - } - }, - about: { - version: '버전', - checkUpdate: '업데이트 확인', - checking: '확인 중...', - latest: '현재 최신 버전입니다', - hasUpdate: '새 버전 발견', - gotoUpdate: '업데이트하러 가기', - gotoGithub: 'Github로 이동', - author: '작성자', - authorDesc: 'algerkong 별점🌟 부탁드려요', - messages: { - checkError: '업데이트 확인 실패, 나중에 다시 시도하세요' - } - }, - validation: { - selectProxyProtocol: '프록시 프로토콜을 선택하세요', - proxyHost: '프록시 주소를 입력하세요', - portNumber: '유효한 포트 번호를 입력하세요 (1-65535)' - }, - lyricSettings: { - title: '가사 설정', - tabs: { - display: '표시', - interface: '인터페이스', - typography: '텍스트', - mobile: '모바일' - }, - pureMode: '순수 모드', - hideCover: '커버 숨기기', - centerDisplay: '중앙 표시', - showTranslation: '번역 표시', - hideLyrics: '가사 숨기기', - hidePlayBar: '재생바 숨기기', - hideMiniPlayBar: '미니 재생바 숨기기', - showMiniPlayBar: '미니 재생바 표시', - backgroundTheme: '배경 테마', - themeOptions: { - default: '기본', - light: '밝음', - dark: '어둠' - }, - fontSize: '폰트 크기', - fontSizeMarks: { - small: '작음', - medium: '중간', - large: '큼' - }, - letterSpacing: '글자 간격', - letterSpacingMarks: { - compact: '좁음', - default: '기본', - loose: '넓음' - }, - lineHeight: '줄 높이', - lineHeightMarks: { - compact: '좁음', - default: '기본', - loose: '넓음' - }, - mobileLayout: '모바일 레이아웃', - layoutOptions: { - default: '기본', - ios: 'iOS 스타일', - android: '안드로이드 스타일' - }, - mobileCoverStyle: '커버 스타일', - coverOptions: { - record: '레코드', - square: '정사각형', - full: '전체화면' - }, - lyricLines: '가사 줄 수', - mobileUnavailable: '이 설정은 모바일에서만 사용 가능합니다' - }, - themeColor: { - title: '가사 테마 색상', - presetColors: '미리 설정된 색상', - customColor: '사용자 정의 색상', - preview: '미리보기 효과', - previewText: '가사 효과', - colorNames: { - 'spotify-green': 'Spotify 그린', - 'apple-blue': '애플 블루', - 'youtube-red': 'YouTube 레드', - orange: '활력 오렌지', - purple: '신비 퍼플', - pink: '벚꽃 핑크' - }, - tooltips: { - openColorPicker: '색상 선택기 열기', - closeColorPicker: '색상 선택기 닫기' - }, - placeholder: '#1db954' - }, - shortcutSettings: { - title: '단축키 설정', - shortcut: '단축키', - shortcutDesc: '단축키 사용자 정의', - shortcutConflict: '단축키 충돌', - inputPlaceholder: '클릭하여 단축키 입력', - resetShortcuts: '기본값 복원', - disableAll: '모두 비활성화', - enableAll: '모두 활성화', - togglePlay: '재생/일시정지', - prevPlay: '이전 곡', - nextPlay: '다음 곡', - volumeUp: '볼륨 증가', - volumeDown: '볼륨 감소', - toggleFavorite: '즐겨찾기/즐겨찾기 취소', - toggleWindow: '창 표시/숨기기', - scopeGlobal: '전역', - scopeApp: '앱 내', - enabled: '활성화', - disabled: '비활성화', - messages: { - resetSuccess: '기본 단축키로 복원되었습니다. 저장을 잊지 마세요', - conflict: '충돌하는 단축키가 있습니다. 다시 설정하세요', - saveSuccess: '단축키 설정이 저장되었습니다', - saveError: '단축키 저장 실패, 다시 시도하세요', - cancelEdit: '수정이 취소되었습니다', - disableAll: '모든 단축키가 비활성화되었습니다. 저장을 잊지 마세요', - enableAll: '모든 단축키가 활성화되었습니다. 저장을 잊지 마세요' - } - }, - remoteControl: { - title: '원격 제어', - enable: '원격 제어 활성화', - port: '서비스 포트', - allowedIps: '허용된 IP 주소', - addIp: 'IP 추가', - emptyListHint: '빈 목록은 모든 IP 액세스를 허용함을 의미합니다', - saveSuccess: '원격 제어 설정이 저장되었습니다', - accessInfo: '원격 제어 액세스 주소:' - }, - cookie: { - title: 'Cookie 설정', - description: '넷이즈 클라우드 뮤직의 Cookie를 입력하세요:', - placeholder: '완전한 Cookie를 붙여넣으세요...', - help: { - format: 'Cookie는 일반적으로 "MUSIC_U="로 시작합니다', - source: '브라우저 개발자 도구의 네트워크 요청에서 얻을 수 있습니다', - storage: 'Cookie 설정 후 자동으로 로컬 저장소에 저장됩니다' - }, - action: { - save: 'Cookie 저장', - paste: '붙여넣기', - clear: '지우기' - }, - validation: { - required: 'Cookie를 입력하세요', - format: 'Cookie 형식이 올바르지 않을 수 있습니다. MUSIC_U가 포함되어 있는지 확인하세요' - }, - message: { - saveSuccess: 'Cookie 저장 성공', - saveError: 'Cookie 저장 실패', - pasteSuccess: '붙여넣기 성공', - pasteError: '붙여넣기 실패, 수동으로 복사하세요' - }, - info: { - length: '현재 길이: {length} 문자' - } - } -}; \ No newline at end of file +export default { + theme: '테마', + language: '언어', + regard: '정보', + logout: '로그아웃', + sections: { + basic: '기본 설정', + playback: '재생 설정', + application: '애플리케이션 설정', + network: '네트워크 설정', + system: '시스템 관리', + donation: '후원 지원', + regard: '정보' + }, + basic: { + themeMode: '테마 모드', + themeModeDesc: '낮/밤 테마 전환', + autoTheme: '시스템 따라가기', + manualTheme: '수동 전환', + language: '언어 설정', + languageDesc: '표시 언어 전환', + tokenManagement: 'Cookie 관리', + tokenManagementDesc: '넷이즈 클라우드 뮤직 로그인 Cookie 관리', + tokenStatus: '현재 Cookie 상태', + tokenSet: '설정됨', + tokenNotSet: '설정되지 않음', + setToken: 'Cookie 설정', + modifyToken: 'Cookie 수정', + clearToken: 'Cookie 지우기', + font: '폰트 설정', + fontDesc: '폰트 선택, 앞에 있는 폰트를 우선 사용', + fontScope: { + global: '전역', + lyric: '가사만' + }, + animation: '애니메이션 속도', + animationDesc: '애니메이션 활성화 여부', + animationSpeed: { + slow: '매우 느림', + normal: '보통', + fast: '매우 빠름' + }, + fontPreview: { + title: '폰트 미리보기', + chinese: '中文', + english: 'English', + japanese: '日本語', + korean: '한국어', + chineseText: '静夜思 床前明月光 疑是地上霜', + englishText: 'The quick brown fox jumps over the lazy dog', + japaneseText: 'あいうえお かきくけこ さしすせそ', + koreanText: '가나다라마 바사아자차 카타파하' + } + }, + playback: { + quality: '음질 설정', + qualityDesc: '음악 재생 음질 선택 (넷이즈 클라우드 VIP)', + qualityOptions: { + standard: '표준', + higher: '높음', + exhigh: '매우 높음', + lossless: '무손실', + hires: 'Hi-Res', + jyeffect: 'HD 서라운드', + sky: '몰입형 서라운드', + dolby: '돌비 애트모스', + jymaster: '초고화질 마스터' + }, + musicSources: '음원 설정', + musicSourcesDesc: '음악 해석에 사용할 음원 플랫폼 선택', + musicSourcesWarning: '최소 하나의 음원 플랫폼을 선택해야 합니다', + musicUnblockEnable: '음악 해석 활성화', + musicUnblockEnableDesc: '활성화하면 재생할 수 없는 음악을 해석하려고 시도합니다', + configureMusicSources: '음원 구성', + selectedMusicSources: '선택된 음원:', + noMusicSources: '음원이 선택되지 않음', + gdmusicInfo: 'GD 뮤직은 여러 플랫폼 음원을 자동으로 해석하고 최적의 결과를 자동 선택합니다', + autoPlay: '자동 재생', + autoPlayDesc: '앱을 다시 열 때 자동으로 재생을 계속할지 여부', + showStatusBar: '상태바 제어 기능 표시 여부', + showStatusBarContent: 'Mac 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)', + fallbackParser: '대체 분석 서비스 (GD Music)', + fallbackParserDesc: '"GD Music"을 선택하고 일반 음원을 사용할 수 없을 때 이 서비스를 사용합니다.', + parserGD: 'GD Music (내장)', + parserCustom: '사용자 지정 API', + + // 음원 라벨 + sourceLabels: { + migu: 'Migu', + kugou: 'Kugou', + pyncmd: 'NetEase (내장)', + bilibili: 'Bilibili', + gdmusic: 'GD Music', + custom: '사용자 지정 API' + }, + + customApi: { + sectionTitle: '사용자 지정 API 설정', + importConfig: 'JSON 설정 가져오기', + currentSource: '현재 음원', + notImported: '아직 사용자 지정 음원을 가져오지 않았습니다.', + importSuccess: '음원 가져오기 성공: {name}', + importFailed: '가져오기 실패: {message}', + enableHint: '사용하려면 먼저 JSON 구성 파일을 가져오세요', + }, + }, + application: { + closeAction: '닫기 동작', + closeActionDesc: '창을 닫을 때의 동작 선택', + closeOptions: { + ask: '매번 묻기', + minimize: '트레이로 최소화', + close: '직접 종료' + }, + shortcut: '단축키 설정', + shortcutDesc: '전역 단축키 사용자 정의', + download: '다운로드 관리', + downloadDesc: '다운로드 목록 버튼을 항상 표시할지 여부', + unlimitedDownload: '무제한 다운로드', + unlimitedDownloadDesc: '활성화하면 음악을 무제한으로 다운로드합니다 (다운로드 실패가 발생할 수 있음), 기본 제한 300곡', + downloadPath: '다운로드 디렉토리', + downloadPathDesc: '음악 파일의 다운로드 위치 선택', + remoteControl: '원격 제어', + remoteControlDesc: '원격 제어 기능 설정' + }, + network: { + apiPort: '음악 API 포트', + apiPortDesc: '수정 후 앱을 재시작해야 합니다', + proxy: '프록시 설정', + proxyDesc: '음악에 액세스할 수 없을 때 프록시를 활성화할 수 있습니다', + proxyHost: '프록시 주소', + proxyHostPlaceholder: '프록시 주소를 입력하세요', + proxyPort: '프록시 포트', + proxyPortPlaceholder: '프록시 포트를 입력하세요', + realIP: 'realIP 설정', + realIPDesc: '제한으로 인해 이 프로젝트는 해외에서 사용할 때 제한을 받을 수 있으며, realIP 매개변수를 사용하여 국내 IP를 전달하여 해결할 수 있습니다', + messages: { + proxySuccess: '프록시 설정이 저장되었습니다. 앱을 재시작한 후 적용됩니다', + proxyError: '입력이 올바른지 확인하세요', + realIPSuccess: '실제 IP 설정이 저장되었습니다', + realIPError: '유효한 IP 주소를 입력하세요' + } + }, + system: { + cache: '캐시 관리', + cacheDesc: '캐시 지우기', + cacheClearTitle: '지울 캐시 유형을 선택하세요:', + cacheTypes: { + history: { + label: '재생 기록', + description: '재생한 곡 기록 지우기' + }, + favorite: { + label: '즐겨찾기 기록', + description: '로컬 즐겨찾기 곡 기록 지우기 (클라우드 즐겨찾기에는 영향 없음)' + }, + user: { + label: '사용자 데이터', + description: '로그인 정보 및 사용자 관련 데이터 지우기' + }, + settings: { + label: '앱 설정', + description: '앱의 모든 사용자 정의 설정 지우기' + }, + downloads: { + label: '다운로드 기록', + description: '다운로드 기록 지우기 (다운로드된 파일은 삭제되지 않음)' + }, + resources: { + label: '음악 리소스', + description: '로드된 음악 파일, 가사 등 리소스 캐시 지우기' + }, + lyrics: { + label: '가사 리소스', + description: '로드된 가사 리소스 캐시 지우기' + } + }, + restart: '재시작', + restartDesc: '앱 재시작', + messages: { + clearSuccess: '지우기 성공, 일부 설정은 재시작 후 적용됩니다' + } + }, + about: { + version: '버전', + checkUpdate: '업데이트 확인', + checking: '확인 중...', + latest: '현재 최신 버전입니다', + hasUpdate: '새 버전 발견', + gotoUpdate: '업데이트하러 가기', + gotoGithub: 'Github로 이동', + author: '작성자', + authorDesc: 'algerkong 별점🌟 부탁드려요', + messages: { + checkError: '업데이트 확인 실패, 나중에 다시 시도하세요' + } + }, + validation: { + selectProxyProtocol: '프록시 프로토콜을 선택하세요', + proxyHost: '프록시 주소를 입력하세요', + portNumber: '유효한 포트 번호를 입력하세요 (1-65535)' + }, + lyricSettings: { + title: '가사 설정', + tabs: { + display: '표시', + interface: '인터페이스', + typography: '텍스트', + mobile: '모바일' + }, + pureMode: '순수 모드', + hideCover: '커버 숨기기', + centerDisplay: '중앙 표시', + showTranslation: '번역 표시', + hideLyrics: '가사 숨기기', + hidePlayBar: '재생바 숨기기', + hideMiniPlayBar: '미니 재생바 숨기기', + showMiniPlayBar: '미니 재생바 표시', + backgroundTheme: '배경 테마', + themeOptions: { + default: '기본', + light: '밝음', + dark: '어둠' + }, + fontSize: '폰트 크기', + fontSizeMarks: { + small: '작음', + medium: '중간', + large: '큼' + }, + letterSpacing: '글자 간격', + letterSpacingMarks: { + compact: '좁음', + default: '기본', + loose: '넓음' + }, + lineHeight: '줄 높이', + lineHeightMarks: { + compact: '좁음', + default: '기본', + loose: '넓음' + }, + mobileLayout: '모바일 레이아웃', + layoutOptions: { + default: '기본', + ios: 'iOS 스타일', + android: '안드로이드 스타일' + }, + mobileCoverStyle: '커버 스타일', + coverOptions: { + record: '레코드', + square: '정사각형', + full: '전체화면' + }, + lyricLines: '가사 줄 수', + mobileUnavailable: '이 설정은 모바일에서만 사용 가능합니다' + }, + themeColor: { + title: '가사 테마 색상', + presetColors: '미리 설정된 색상', + customColor: '사용자 정의 색상', + preview: '미리보기 효과', + previewText: '가사 효과', + colorNames: { + 'spotify-green': 'Spotify 그린', + 'apple-blue': '애플 블루', + 'youtube-red': 'YouTube 레드', + orange: '활력 오렌지', + purple: '신비 퍼플', + pink: '벚꽃 핑크' + }, + tooltips: { + openColorPicker: '색상 선택기 열기', + closeColorPicker: '색상 선택기 닫기' + }, + placeholder: '#1db954' + }, + shortcutSettings: { + title: '단축키 설정', + shortcut: '단축키', + shortcutDesc: '단축키 사용자 정의', + shortcutConflict: '단축키 충돌', + inputPlaceholder: '클릭하여 단축키 입력', + resetShortcuts: '기본값 복원', + disableAll: '모두 비활성화', + enableAll: '모두 활성화', + togglePlay: '재생/일시정지', + prevPlay: '이전 곡', + nextPlay: '다음 곡', + volumeUp: '볼륨 증가', + volumeDown: '볼륨 감소', + toggleFavorite: '즐겨찾기/즐겨찾기 취소', + toggleWindow: '창 표시/숨기기', + scopeGlobal: '전역', + scopeApp: '앱 내', + enabled: '활성화', + disabled: '비활성화', + messages: { + resetSuccess: '기본 단축키로 복원되었습니다. 저장을 잊지 마세요', + conflict: '충돌하는 단축키가 있습니다. 다시 설정하세요', + saveSuccess: '단축키 설정이 저장되었습니다', + saveError: '단축키 저장 실패, 다시 시도하세요', + cancelEdit: '수정이 취소되었습니다', + disableAll: '모든 단축키가 비활성화되었습니다. 저장을 잊지 마세요', + enableAll: '모든 단축키가 활성화되었습니다. 저장을 잊지 마세요' + } + }, + remoteControl: { + title: '원격 제어', + enable: '원격 제어 활성화', + port: '서비스 포트', + allowedIps: '허용된 IP 주소', + addIp: 'IP 추가', + emptyListHint: '빈 목록은 모든 IP 액세스를 허용함을 의미합니다', + saveSuccess: '원격 제어 설정이 저장되었습니다', + accessInfo: '원격 제어 액세스 주소:' + }, + cookie: { + title: 'Cookie 설정', + description: '넷이즈 클라우드 뮤직의 Cookie를 입력하세요:', + placeholder: '완전한 Cookie를 붙여넣으세요...', + help: { + format: 'Cookie는 일반적으로 "MUSIC_U="로 시작합니다', + source: '브라우저 개발자 도구의 네트워크 요청에서 얻을 수 있습니다', + storage: 'Cookie 설정 후 자동으로 로컬 저장소에 저장됩니다' + }, + action: { + save: 'Cookie 저장', + paste: '붙여넣기', + clear: '지우기' + }, + validation: { + required: 'Cookie를 입력하세요', + format: 'Cookie 형식이 올바르지 않을 수 있습니다. MUSIC_U가 포함되어 있는지 확인하세요' + }, + message: { + saveSuccess: 'Cookie 저장 성공', + saveError: 'Cookie 저장 실패', + pasteSuccess: '붙여넣기 성공', + pasteError: '붙여넣기 실패, 수동으로 복사하세요' + }, + info: { + length: '현재 길이: {length} 문자' + } + } +}; diff --git a/src/i18n/lang/zh-CN/player.ts b/src/i18n/lang/zh-CN/player.ts index c7b4497..c9e567e 100644 --- a/src/i18n/lang/zh-CN/player.ts +++ b/src/i18n/lang/zh-CN/player.ts @@ -39,7 +39,9 @@ export default { warning: '请选择一个音源', bilibiliNotSupported: 'B站视频不支持重新解析', processing: '解析中...', - clear: '清除自定义音源' + clear: '清除自定义音源', + customApiFailed: '自定义API解析失败,正在尝试使用内置音源...', + customApiError: '自定义API请求出错,正在尝试使用内置音源...' }, playBar: { expand: '展开歌词', diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index df40155..7730e4e 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -78,7 +78,33 @@ export default { autoPlay: '自动播放', autoPlayDesc: '重新打开应用时是否自动继续播放', showStatusBar: '是否显示状态栏控制功能', - showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)' + showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)', + + fallbackParser: 'GD音乐台(music.gdstudio.xyz)设置', + fallbackParserDesc: 'GD音乐台将自动尝试多个音乐平台进行解析,无需额外配置。优先级高于其他解析方式,但是请求可能较慢。感谢(music.gdstudio.xyz)\n', + parserGD: 'GD 音乐台 (内置)', + parserCustom: '自定义 API', + + // 音源标签 + sourceLabels: { + migu: '咪咕音乐', + kugou: '酷狗音乐', + pyncmd: '网易云(内置)', + bilibili: 'Bilibili', + gdmusic: 'GD音乐台', + custom: '自定义 API' + }, + + // 自定义API相关的提示 + customApi: { + sectionTitle: '自定义 API 设置', + importConfig: '导入 JSON 配置', + currentSource: '当前音源', + notImported: '尚未导入自定义音源。', + importSuccess: '成功导入音源: {name}', + importFailed: '导入失败: {message}', + enableHint: '请先导入 JSON 配置文件才能启用', + }, }, application: { closeAction: '关闭行为', diff --git a/src/i18n/lang/zh-Hant/player.ts b/src/i18n/lang/zh-Hant/player.ts index 5e3ae78..8ca3b5e 100644 --- a/src/i18n/lang/zh-Hant/player.ts +++ b/src/i18n/lang/zh-Hant/player.ts @@ -39,7 +39,9 @@ export default { warning: '請選擇一個音源', bilibiliNotSupported: 'B站影片不支援重新解析', processing: '解析中...', - clear: '清除自訂音源' + clear: '清除自訂音源', + customApiFailed: '自定義API解析失敗,正在嘗試使用內置音源...', + customApiError: '自定義API請求出錯,正在嘗試使用內置音源...' }, playBar: { expand: '展開歌詞', diff --git a/src/i18n/lang/zh-Hant/settings.ts b/src/i18n/lang/zh-Hant/settings.ts index 4468c19..433659b 100644 --- a/src/i18n/lang/zh-Hant/settings.ts +++ b/src/i18n/lang/zh-Hant/settings.ts @@ -78,7 +78,31 @@ export default { autoPlay: '自動播放', autoPlayDesc: '重新開啟應用程式時是否自動繼續播放', showStatusBar: '是否顯示狀態列控制功能', - showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)' + showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)', + fallbackParser: '備用解析服務 (GD音樂台)', + fallbackParserDesc: '當勾選「GD音樂台」且常規音源無法播放時,將使用此服務嘗試解析。', + parserGD: 'GD 音樂台 (內建)', + parserCustom: '自訂 API', + + // 音源標籤 + sourceLabels: { + migu: '咪咕音樂', + kugou: '酷狗音樂', + pyncmd: '網易雲(內建)', + bilibili: 'Bilibili', + gdmusic: 'GD音樂台', + custom: '自訂 API' + }, + + customApi: { + sectionTitle: '自訂 API 設定', + importConfig: '匯入 JSON 設定', + currentSource: '目前音源', + notImported: '尚未匯入自訂音源。', + importSuccess: '成功匯入音源:{name}', + importFailed: '匯入失敗:{message}', + enableHint: '請先匯入 JSON 設定檔才能啟用', + }, }, application: { closeAction: '關閉行為', diff --git a/src/main/index.ts b/src/main/index.ts index efdfa6a..69f91d8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -16,6 +16,7 @@ import { setupUpdateHandlers } from './modules/update'; import { createMainWindow, initializeWindowManager, setAppQuitting } from './modules/window'; import { initWindowSizeManager } from './modules/window-size'; import { startMusicApi } from './server'; +import { initializeOtherApi } from './modules/otherApi'; // 导入所有图标 const iconPath = join(__dirname, '../../resources'); @@ -38,6 +39,8 @@ function initialize() { // 初始化文件管理 initializeFileManager(); + // 初始化其他 API (搜索建议等) + initializeOtherApi(); // 初始化窗口管理 initializeWindowManager(); // 初始化字体管理 diff --git a/src/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index b7e74ed..187161f 100644 --- a/src/main/modules/fileManager.ts +++ b/src/main/modules/fileManager.ts @@ -9,7 +9,8 @@ import * as mm from 'music-metadata'; import * as NodeID3 from 'node-id3'; import * as os from 'os'; import * as path from 'path'; - +import { FlacTagMap, writeFlacTags } from 'flac-tagger'; +import sharp from 'sharp'; import { getStore } from './config'; const MAX_CONCURRENT_DOWNLOADS = 3; @@ -275,6 +276,39 @@ export function initializeFileManager() { } } }); + + // 处理导入自定义API插件的请求 + ipcMain.handle('import-custom-api-plugin', async () => { + const result = await dialog.showOpenDialog({ + title: '选择自定义音源配置文件', + filters: [{ name: 'JSON Files', extensions: ['json'] }], + properties: ['openFile'] + }); + + if (result.canceled || result.filePaths.length === 0) { + return null; + } + + const filePath = result.filePaths[0]; + try { + const fileContent = fs.readFileSync(filePath, 'utf-8'); + + // 基础验证,确保它是个合法的JSON并且包含关键字段 + const pluginData = JSON.parse(fileContent); + if (!pluginData.name || !pluginData.apiUrl) { + throw new Error('无效的插件文件,缺少 name 或 apiUrl 字段。'); + } + + return { + name: pluginData.name, + content: fileContent // 返回完整的JSON字符串 + }; + } catch (error: any) { + console.error('读取或解析插件文件失败:', error); + // 向渲染进程抛出错误,以便UI可以显示提示 + throw new Error(`文件读取或解析失败: ${error.message}`); + } + }); } /** @@ -386,7 +420,7 @@ async function downloadMusic( let formattedFilename = filename; if (songInfo) { // 准备替换变量 - const artistName = songInfo.ar?.map((a: any) => a.name).join('/') || '未知艺术家'; + const artistName = songInfo.ar?.map((a: any) => a.name).join('、') || '未知艺术家'; const songName = songInfo.name || filename; const albumName = songInfo.al?.name || '未知专辑'; @@ -576,8 +610,39 @@ async function downloadMusic( timeout: 10000 }); - // 获取封面图片的buffer - coverImageBuffer = Buffer.from(coverResponse.data); + const originalCoverBuffer = Buffer.from(coverResponse.data); + const TWO_MB = 2 * 1024 * 1024; + // 检查图片大小是否超过2MB + if (originalCoverBuffer.length > TWO_MB) { + const originalSizeMB = (originalCoverBuffer.length / (1024 * 1024)).toFixed(2); + console.log(`封面图大于2MB (${originalSizeMB} MB),开始压缩...`); + try { + // 使用 sharp 进行压缩 + coverImageBuffer = await sharp(originalCoverBuffer) + .resize({ + width: 1600, + height: 1600, + fit: 'inside', + withoutEnlargement: true + }) + .jpeg({ + quality: 80, + mozjpeg: true + }) + .toBuffer(); + + const compressedSizeMB = (coverImageBuffer.length / (1024 * 1024)).toFixed(2); + console.log(`封面图压缩完成,新大小: ${compressedSizeMB} MB`); + + } catch (compressionError) { + console.error('封面图压缩失败,将使用原图:', compressionError); + coverImageBuffer = originalCoverBuffer; // 如果压缩失败,则回退使用原始图片 + } + } else { + // 如果图片不大于2MB,直接使用原图 + coverImageBuffer = originalCoverBuffer; + } + console.log('封面已准备好,将写入元数据'); } } @@ -588,7 +653,7 @@ async function downloadMusic( const fileFormat = fileExtension.toLowerCase(); const artistNames = - (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/ ') || '未知艺术家'; + (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('、') || '未知艺术家'; // 根据文件类型处理元数据 if (['.mp3'].includes(fileFormat)) { @@ -598,7 +663,7 @@ async function downloadMusic( NodeID3.removeTags(finalFilePath); const tags = { - title: filename, + title: songInfo?.name, artist: artistNames, TPE1: artistNames, TPE2: artistNames, @@ -634,10 +699,35 @@ async function downloadMusic( } catch (err) { console.error('Error writing ID3 tags:', err); } - } else { - // 对于非MP3文件,使用music-metadata来写入元数据可能需要专门的库 - // 或者根据不同文件类型使用专用工具,暂时只记录但不处理 - console.log(`文件类型 ${fileFormat} 不支持使用NodeID3写入标签,跳过元数据写入`); + } else if (['.flac'].includes(fileFormat)) { + try { + const tagMap: FlacTagMap = { + TITLE: songInfo?.name, + ARTIST: artistNames, + ALBUM: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename, + LYRICS: lyricsContent || '', + TRACKNUMBER: songInfo?.no ? String(songInfo.no) : undefined, + DATE: songInfo?.publishTime + ? new Date(songInfo.publishTime).getFullYear().toString() + : undefined + }; + + await writeFlacTags( + { + tagMap, + picture: coverImageBuffer + ? { + buffer: coverImageBuffer, + mime: 'image/jpeg' + } + : undefined + }, + finalFilePath + ); + console.log('FLAC tags written successfully'); + } catch (err) { + console.error('Error writing FLAC tags:', err); + } } // 保存下载信息 @@ -683,7 +773,7 @@ async function downloadMusic( // 发送桌面通知 try { const artistNames = - (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/') || + (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('、') || '未知艺术家'; const notification = new Notification({ title: '下载完成', diff --git a/src/main/modules/otherApi.ts b/src/main/modules/otherApi.ts new file mode 100644 index 0000000..efdf8ee --- /dev/null +++ b/src/main/modules/otherApi.ts @@ -0,0 +1,28 @@ +import axios from 'axios'; +import { ipcMain } from 'electron'; + +/** + * 初始化其他杂项 API(如搜索建议等) + */ +export function initializeOtherApi() { + // 搜索建议(从酷狗获取) + ipcMain.handle('get-search-suggestions', async (_, keyword: string) => { + if (!keyword || !keyword.trim()) { + return []; + } + try { + console.log(`[Main Process Proxy] Forwarding suggestion request for: ${keyword}`); + const response = await axios.get('http://msearchcdn.kugou.com/new/app/i/search.php', { + params: { + cmd: 302, + keyword: keyword + }, + timeout: 5000 + }); + return response.data; + } catch (error: any) { + console.error('[Main Process Proxy] Failed to fetch search suggestions:', error.message); + return []; + } + }); +} diff --git a/src/main/set.json b/src/main/set.json index e3f117b..ca146fd 100644 --- a/src/main/set.json +++ b/src/main/set.json @@ -28,5 +28,7 @@ "contentZoomFactor": 1, "autoTheme": false, "manualTheme": "light", - "isMenuExpanded": false + "isMenuExpanded": false, + "customApiPlugin": "", + "customApiPluginName": "" } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 4af0cd2..13aac75 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -21,7 +21,9 @@ interface API { onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void; onLanguageChanged: (callback: (locale: string) => void) => void; removeDownloadListeners: () => void; + importCustomApiPlugin: () => Promise<{ name: string; content: string } | null>; invoke: (channel: string, ...args: any[]) => Promise; + getSearchSuggestions: (keyword: string) => Promise; } // 自定义IPC渲染进程通信接口 diff --git a/src/preload/index.ts b/src/preload/index.ts index c47ac7e..5b311d2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -18,6 +18,7 @@ const api = { sendSong: (data) => ipcRenderer.send('update-current-song', data), unblockMusic: (id, data, enabledSources) => ipcRenderer.invoke('unblock-music', id, data, enabledSources), + importCustomApiPlugin: () => ipcRenderer.invoke('import-custom-api-plugin'), // 歌词窗口关闭事件 onLyricWindowClosed: (callback: () => void) => { ipcRenderer.on('lyric-window-closed', () => callback()); @@ -54,7 +55,9 @@ const api = { return ipcRenderer.invoke(channel, ...args); } return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`)); - } + }, + // 搜索建议 + getSearchSuggestions: (keyword: string) => ipcRenderer.invoke('get-search-suggestions', keyword), }; // 创建带类型的ipcRenderer对象,暴露给渲染进程 diff --git a/src/renderer/api/music.ts b/src/renderer/api/music.ts index ea574f7..e92029e 100644 --- a/src/renderer/api/music.ts +++ b/src/renderer/api/music.ts @@ -9,7 +9,9 @@ import request from '@/utils/request'; import requestMusic from '@/utils/request_music'; import { searchAndGetBilibiliAudioUrl } from './bilibili'; +import type { ParsedMusicResult } from './gdmusic'; import { parseFromGDMusic } from './gdmusic'; +import { parseFromCustomApi } from './parseFromCustomApi'; const { addData, getData, deleteData } = musicDB; @@ -30,6 +32,8 @@ export const getMusicUrl = async (id: number, isDownloaded: boolean = false) => params: { id, level: settingStore.setData.musicQuality || 'higher', + encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac', + // level为lossless时,encodeType=flac时网易云会返回hires音质,encodeType=aac时网易云会返回lossless音质 cookie: `${localStorage.getItem('token')} os=pc;` } }); @@ -45,7 +49,8 @@ export const getMusicUrl = async (id: number, isDownloaded: boolean = false) => return await request.get('/song/url/v1', { params: { id, - level: settingStore.setData.musicQuality || 'higher' + level: settingStore.setData.musicQuality || 'higher', + encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac' } }); }; @@ -114,7 +119,8 @@ const getBilibiliAudio = async (data: SongResult) => { * @param data 歌曲数据 * @returns 解析结果,失败时返回null */ -const getGDMusicAudio = async (id: number, data: SongResult) => { +const getGDMusicAudio = async (id: number, data: SongResult): Promise => { + // <-- 在这里明确声明返回类型 try { const gdResult = await parseFromGDMusic(id, data, '999'); if (gdResult) { @@ -146,59 +152,123 @@ const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => { * @returns 解析结果 */ export const getParsingMusicUrl = async (id: number, data: SongResult) => { - if(isElectron){ - const settingStore = useSettingsStore(); + try { + if (isElectron) { + let musicSources: any[] = []; + let quality: string = 'higher'; + try { + const settingStore = useSettingsStore(); + const enableMusicUnblock = settingStore?.setData?.enableMusicUnblock; - // 如果禁用了音乐解析功能,则直接返回空结果 - if (!settingStore.setData.enableMusicUnblock) { - return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } }); - } - - // 1. 确定使用的音源列表(自定义或全局) - const songId = String(id); - const savedSourceStr = localStorage.getItem(`song_source_${songId}`); - let musicSources: any[] = []; - - try { - if (savedSourceStr) { - // 使用自定义音源 - musicSources = JSON.parse(savedSourceStr); - console.log(`使用歌曲 ${id} 自定义音源:`, musicSources); - } else { - // 使用全局音源设置 - musicSources = settingStore.setData.enabledMusicSources || []; - console.log(`使用全局音源设置:`, musicSources); - if (musicSources.length > 0) { - return getUnblockMusicAudio(id, data, musicSources); + // 如果禁用了音乐解析功能,则直接返回空结果 + if (!enableMusicUnblock) { + return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } }); } + + // 1. 确定使用的音源列表(自定义或全局) + const songId = String(id); + const savedSourceStr = (() => { + try { + return localStorage.getItem(`song_source_${songId}`); + } catch (e) { + console.warn('读取本地存储失败,忽略自定义音源', e); + return null; + } + })(); + + if (savedSourceStr) { + try { + musicSources = JSON.parse(savedSourceStr); + console.log(`使用歌曲 ${id} 自定义音源:`, musicSources); + } catch (e) { + console.error('解析音源设置失败,回退到默认全局设置', e); + musicSources = settingStore?.setData?.enabledMusicSources || []; + } + } else { + // 使用全局音源设置 + musicSources = settingStore?.setData?.enabledMusicSources || []; + console.log(`使用全局音源设置:`, musicSources); + } + + quality = settingStore?.setData?.musicQuality || 'higher'; + } catch (e) { + console.error('读取设置失败,使用默认配置', e); + musicSources = []; + quality = 'higher'; + } + + // 优先级 1: 自定义 API + try { + const hasCustom = Array.isArray(musicSources) && musicSources.includes('custom'); + const customEnabled = (() => { + try { + const st = useSettingsStore(); + return Boolean(st?.setData?.customApiPlugin); + } catch { + return false; + } + })(); + if (hasCustom && customEnabled) { + console.log('尝试使用 自定义API 解析...'); + const customResult = await parseFromCustomApi(id, data, quality); + if (customResult) { + return customResult; // 成功则直接返回 + } + console.log('自定义API解析失败,继续尝试其他音源...'); + } + } catch (e) { + console.error('自定义API解析发生异常,继续尝试其他音源', e); + } + + // 优先级 2: Bilibili + try { + if (Array.isArray(musicSources) && musicSources.includes('bilibili')) { + console.log('尝试使用 Bilibili 解析...'); + const bilibiliResult = await getBilibiliAudio(data); + if (bilibiliResult?.data?.data?.url) { + return bilibiliResult; + } + console.log('Bilibili解析失败,继续尝试其他音源...'); + } + } catch (e) { + console.error('Bilibili解析发生异常,继续尝试其他音源', e); + } + + // 优先级 3: GD 音乐台 + try { + if (Array.isArray(musicSources) && musicSources.includes('gdmusic')) { + console.log('尝试使用 GD音乐台 解析...'); + const gdResult = await getGDMusicAudio(id, data); + if (gdResult) { + return gdResult; + } + console.log('GD音乐台解析失败,继续尝试其他音源...'); + } + } catch (e) { + console.error('GD音乐台解析发生异常,继续尝试其他音源', e); + } + + // 优先级 4: UnblockMusic (migu, kugou, pyncmd) + try { + const unblockSources = (Array.isArray(musicSources) ? musicSources : []).filter( + (source) => !['custom', 'bilibili', 'gdmusic'].includes(source) + ); + if (unblockSources.length > 0) { + console.log('尝试使用 UnblockMusic 解析:', unblockSources); + // 捕获内部可能的异常 + return await getUnblockMusicAudio(id, data, unblockSources); + } else { + console.warn('UnblockMusic API 不可用,跳过此解析方式'); + } + } catch (e) { + console.error('UnblockMusic 解析发生异常,继续后备方案', e); } - } catch (e) { - console.error('解析音源设置失败,使用全局设置', e); - musicSources = settingStore.setData.enabledMusicSources || []; - } - - // 2. 按优先级解析 - - // 2.1 Bilibili解析(优先级最高) - if (musicSources.includes('bilibili')) { - return await getBilibiliAudio(data); - } - - // 2.2 GD音乐台解析 - if (musicSources.includes('gdmusic')) { - const gdResult = await getGDMusicAudio(id, data); - if (gdResult) return gdResult; - // GD解析失败,继续下一步 - console.log('GD音乐台解析失败,尝试使用其他音源'); - } - console.log('musicSources', musicSources); - // 2.3 使用unblockMusic解析其他音源 - if (musicSources.length > 0) { - return getUnblockMusicAudio(id, data, musicSources); } + } catch (e) { + console.error('getParsingMusicUrl 执行异常,将使用后备方案:', e); } - // 3. 后备方案:使用API请求 + // 后备方案:使用API请求 console.log('无可用音源或不在Electron环境中,使用API请求'); return requestMusic.get('/music', { params: { id } }); }; @@ -208,6 +278,12 @@ export const likeSong = (id: number, like: boolean = true) => { return request.get('/like', { params: { id, like } }); }; +// 将每日推荐中的歌曲标记为不感兴趣,并获取一首新歌 +export const dislikeRecommendedSong = (id: number | string) => { + return request.get('/recommend/songs/dislike', { + params: { id } + }); +}; // 获取用户喜欢的音乐列表 export const getLikedList = (uid: number) => { return request.get('/likelist', { diff --git a/src/renderer/api/parseFromCustomApi.ts b/src/renderer/api/parseFromCustomApi.ts new file mode 100644 index 0000000..c61d45c --- /dev/null +++ b/src/renderer/api/parseFromCustomApi.ts @@ -0,0 +1,107 @@ +import axios from 'axios'; +import { get } from 'lodash'; + +import { useSettingsStore } from '@/store'; + +import type { ParsedMusicResult } from './gdmusic'; + +/** + * 定义自定义API JSON插件的结构 + */ +interface CustomApiPlugin { + name: string; + apiUrl: string; + method?: 'GET' | 'POST'; + params: Record; + qualityMapping?: Record; + responseUrlPath: string; +} + +/** + * 从用户导入的自定义API JSON配置中解析音乐URL + */ +export const parseFromCustomApi = async ( + id: number, + _songData: any, + quality: string = 'higher', + timeout: number = 10000 +): Promise => { + const settingsStore = useSettingsStore(); + const pluginString = settingsStore.setData.customApiPlugin; + + if (!pluginString) { + return null; + } + + let plugin: CustomApiPlugin; + try { + plugin = JSON.parse(pluginString); + if (!plugin.apiUrl || !plugin.params || !plugin.responseUrlPath) { + console.error('自定义API:JSON配置文件格式不正确。'); + return null; + } + } catch (error) { + console.error('自定义API:解析JSON配置文件失败。', error); + return null; + } + + console.log(`自定义API:正在使用插件 [${plugin.name}] 进行解析...`); + + try { + // 1. 准备请求参数,替换占位符 + const finalParams: Record = {}; + for (const [key, value] of Object.entries(plugin.params)) { + if (value === '{songId}') { + finalParams[key] = String(id); + } else if (value === '{quality}') { + // 使用 qualityMapping (如果存在) 进行音质翻译,否则直接使用原quality + finalParams[key] = plugin.qualityMapping?.[quality] ?? quality; + } else { + // 固定值参数 + finalParams[key] = value; + } + } + + // 2. 判断请求方法,默认为GET + const method = plugin.method?.toUpperCase() === 'POST' ? 'POST' : 'GET'; + let response; + + // 3. 根据方法发送不同的请求 + if (method === 'POST') { + console.log('自定义API:发送 POST 请求到:', plugin.apiUrl, '参数:', finalParams); + response = await axios.post(plugin.apiUrl, finalParams, { timeout }); + } else { + // 默认为 GET + const finalUrl = `${plugin.apiUrl}?${new URLSearchParams(finalParams).toString()}`; + console.log('自定义API:发送 GET 请求到:', finalUrl); + response = await axios.get(finalUrl, { timeout }); + } + + // 4. 使用 lodash.get 安全地从响应数据中提取URL + const musicUrl = get(response.data, plugin.responseUrlPath); + + if (musicUrl && typeof musicUrl === 'string') { + console.log('自定义API:成功获取URL!'); + // 5. 组装成应用所需的标准格式并返回 + return { + data: { + data: { + url: musicUrl, + br: parseInt(quality) * 1000, + size: 0, + md5: '', + platform: plugin.name.toLowerCase().replace(/\s/g, ''), + gain: 0 + }, + params: { id, type: 'song' } + } + }; + } else { + console.error('自定义API:根据路径未能从响应中找到URL:', plugin.responseUrlPath); + return null; + } + } catch (error) { + console.error(`自定义API [${plugin.name}] 执行失败:`, error); + return null; + } +}; diff --git a/src/renderer/api/search.ts b/src/renderer/api/search.ts index dbbc81b..4368bcf 100644 --- a/src/renderer/api/search.ts +++ b/src/renderer/api/search.ts @@ -1,3 +1,4 @@ +import { isElectron } from '@/utils'; import request from '@/utils/request'; interface IParams { @@ -12,3 +13,74 @@ export const getSearch = (params: IParams) => { params }); }; + +/** + * 搜索建议接口返回的数据结构 + */ +interface Suggestion { + keyword: string; +} + +interface KugouSuggestionResponse { + data: Suggestion[]; +} + +// 网易云搜索建议返回的数据结构(部分字段) +interface NeteaseSuggestResult { + result?: { + songs?: Array<{ name: string }>; + artists?: Array<{ name: string }>; + albums?: Array<{ name: string }>; + }; + code?: number; +} + +/** + * 从酷狗获取搜索建议 + * @param keyword 搜索关键词 + */ +export const getSearchSuggestions = async (keyword: string) => { + console.log('[API] getSearchSuggestions: 开始执行'); + + if (!keyword || !keyword.trim()) { + return Promise.resolve([]); + } + + console.log(`[API] getSearchSuggestions: 准备请求,关键词: "${keyword}"`); + + try { + let responseData: KugouSuggestionResponse; + if (isElectron) { + console.log('[API] Running in Electron, using IPC proxy.'); + responseData = await window.api.getSearchSuggestions(keyword); + } else { + // 非 Electron 环境下,使用网易云接口 + const res = await request.get('/search/suggest', { + params: { keywords: keyword } + }); + + const result = res?.data?.result || {}; + const names: string[] = []; + if (Array.isArray(result.songs)) names.push(...result.songs.map((s) => s.name)); + if (Array.isArray(result.artists)) names.push(...result.artists.map((a) => a.name)); + if (Array.isArray(result.albums)) names.push(...result.albums.map((al) => al.name)); + + // 去重并截取前10个 + const unique = Array.from(new Set(names)).slice(0, 10); + console.log('[API] getSearchSuggestions: 网易云建议解析成功:', unique); + return unique; + } + + if (responseData && Array.isArray(responseData.data)) { + const suggestions = responseData.data.map((item) => item.keyword).slice(0, 10); + console.log('[API] getSearchSuggestions: 成功解析建议:', suggestions); + return suggestions; + } + + console.warn('[API] getSearchSuggestions: 响应数据格式不正确,返回空数组。'); + return []; + } catch (error) { + console.error('[API] getSearchSuggestions: 请求失败,错误信息:', error); + return []; + } +}; diff --git a/src/renderer/components/common/MusicListNavigator.ts b/src/renderer/components/common/MusicListNavigator.ts index f5fef27..bb4a64b 100644 --- a/src/renderer/components/common/MusicListNavigator.ts +++ b/src/renderer/components/common/MusicListNavigator.ts @@ -18,22 +18,28 @@ export function navigateToMusicList( canRemove?: boolean; } ) { - const musicStore = useMusicStore(); - const { id, type, name, songList, listInfo, canRemove = false } = options; + const musicStore = useMusicStore(); + const { id, type, name, songList, listInfo, canRemove = false } = options; - // 保存数据到状态管理 - musicStore.setCurrentMusicList(songList, name, listInfo, canRemove); + // 如果是每日推荐,不需要设置 musicStore,直接从 recommendStore 获取 + if (type !== 'dailyRecommend') { + musicStore.setCurrentMusicList(songList, name, listInfo, canRemove); + } else { + // 确保 musicStore 的数据被清空,避免显示旧的列表 + musicStore.clearCurrentMusicList(); + } - // 路由跳转 - if (id) { + // 路由跳转 + if (id) { router.push({ name: 'musicList', params: { id }, query: { type } }); - } else { + } else { router.push({ - name: 'musicList' + name: 'musicList', + query: { type: 'dailyRecommend' } }); - } + } } diff --git a/src/renderer/components/cover/Cover3D.vue b/src/renderer/components/cover/Cover3D.vue new file mode 100644 index 0000000..aa1ba89 --- /dev/null +++ b/src/renderer/components/cover/Cover3D.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/src/renderer/components/home/TopBanner.vue b/src/renderer/components/home/TopBanner.vue index 92325e7..89dce4e 100644 --- a/src/renderer/components/home/TopBanner.vue +++ b/src/renderer/components/home/TopBanner.vue @@ -130,14 +130,13 @@ import { computed, onMounted, ref, watchEffect } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; -import { getDayRecommend, getHotSinger } from '@/api/home'; +import { getHotSinger } from '@/api/home'; import { getListDetail } from '@/api/list'; import { getMusicDetail } from '@/api/music'; import { getUserPlaylist } from '@/api/user'; import { navigateToMusicList } from '@/components/common/MusicListNavigator'; import { useArtist } from '@/hooks/useArtist'; -import { usePlayerStore, useUserStore } from '@/store'; -import { IDayRecommend } from '@/types/day_recommend'; +import { usePlayerStore, useRecommendStore, useUserStore } from '@/store'; import { Playlist } from '@/types/list'; import type { IListDetail } from '@/types/listDetail'; import { SongResult } from '@/types/music'; @@ -152,13 +151,21 @@ import { const userStore = useUserStore(); const playerStore = usePlayerStore(); +const recommendStore = useRecommendStore(); const router = useRouter(); const { t } = useI18n(); // 歌手信息 const hotSingerData = ref(); -const dayRecommendData = ref(); +const dayRecommendData = computed(() => { + if (recommendStore.dailyRecommendSongs.length > 0) { + return { + dailySongs: recommendStore.dailyRecommendSongs + }; + } + return null; +}); const userPlaylist = ref([]); // 为歌单弹窗添加的状态 @@ -230,22 +237,8 @@ onMounted(async () => { loadNonUserData(); }); -// 提取每日推荐加载逻辑到单独的函数 const loadDayRecommendData = async () => { - try { - const { - data: { data: dayRecommend } - } = await getDayRecommend(); - const dayRecommendSource = dayRecommend as unknown as IDayRecommend; - dayRecommendData.value = { - ...dayRecommendSource, - dailySongs: dayRecommendSource.dailySongs.filter( - (song: any) => !playerStore.dislikeList.includes(song.id) - ) - }; - } catch (error) { - console.error('获取每日推荐失败:', error); - } + await recommendStore.fetchDailyRecommendSongs(); }; // 加载不需要登录的数据 diff --git a/src/renderer/components/lyric/MusicFull.vue b/src/renderer/components/lyric/MusicFull.vue index 43001ea..b2d2512 100644 --- a/src/renderer/components/lyric/MusicFull.vue +++ b/src/renderer/components/lyric/MusicFull.vue @@ -9,24 +9,22 @@ >
- -
+
+ +
- - - - + + + + +
-
- + -
- -
{{ playMusic.name }}
@@ -151,6 +147,7 @@ import { useDebounceFn } from '@vueuse/core'; import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; +import Cover3D from '@/components/cover/Cover3D.vue'; import LyricCorrectionControl from '@/components/lyric/LyricCorrectionControl.vue'; import LyricSettings from '@/components/lyric/LyricSettings.vue'; import SimplePlayBar from '@/components/player/SimplePlayBar.vue'; @@ -183,10 +180,8 @@ const isDark = ref(false); const showStickyHeader = ref(false); const lyricSettingsRef = ref>(); -// 移除 computed 配置 const config = ref({ ...DEFAULT_LYRIC_CONFIG }); -// 监听设置组件的配置变化 watch( () => lyricSettingsRef.value?.config, (newConfig) => { @@ -525,10 +520,12 @@ defineExpose({ 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } + .drawer-back { @apply absolute bg-cover bg-center; z-index: -1; @@ -561,10 +558,6 @@ defineExpose({ @apply w-[50vh] h-[50vh] mb-8; } - .img { - @apply w-full h-full; - } - .music-info { @apply text-center w-[600px]; @@ -584,10 +577,6 @@ defineExpose({ @apply relative w-full h-full; } - .img { - @apply rounded-xl w-full h-full shadow-2xl transition-all duration-300; - } - .music-info { @apply w-full mt-4; @@ -610,9 +599,11 @@ defineExpose({ &.center { @apply w-auto; + .music-lrc { @apply w-full max-w-3xl mx-auto; } + .music-lrc-text { @apply text-center; } @@ -697,24 +688,30 @@ defineExpose({ .mobile { #drawer-target { @apply flex-col p-4 pt-8 justify-start; + .music-img { display: none; } + .music-lrc { height: calc(100vh - 260px) !important; width: 100vw; + span { padding-right: 0px !important; } + .hover-text { &:hover { background-color: transparent; } } + .music-lrc-text { @apply text-xl text-center; } } + .music-content { @apply h-[calc(100vh-120px)]; width: 100vw !important; @@ -751,8 +748,30 @@ defineExpose({ } } +.control-buttons-container { + @apply flex justify-between items-start z-[9999]; + + &.pure-mode { + @apply pointer-events-auto; /* 容器需要能接收hover事件 */ + + .control-btn { + @apply opacity-0 transition-all duration-300; + pointer-events: none; /* 按钮隐藏时不接收事件 */ + } + + &:hover .control-btn { + @apply opacity-100; + pointer-events: auto; /* hover时按钮可以点击 */ + } + } + + &:not(.pure-mode) .control-btn { + pointer-events: auto; + } +} + .control-btn { - @apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300 z-[9999]; + @apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300; background: rgba(142, 142, 142, 0.192); backdrop-filter: blur(12px); @@ -761,48 +780,16 @@ defineExpose({ color: var(--text-color-active); } - &.pure-mode { - background: transparent; - backdrop-filter: none; - - &:not(:hover) { - i { - opacity: 0; - } - } - } - &:hover { background: rgba(126, 121, 121, 0.2); + i { opacity: 1; } } } -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.loading-overlay { - @apply absolute inset-0 flex items-center justify-center rounded-xl; - background-color: rgba(0, 0, 0, 0.5); - z-index: 2; -} - -.loading-icon { - font-size: 48px; - color: white; - animation: spin 1s linear infinite; -} - .lyric-correction { - /* 仅在 hover 歌词区域时显示 */ .music-lrc:hover & { opacity: 1 !important; pointer-events: auto !important; diff --git a/src/renderer/components/settings/MusicSourceSettings.vue b/src/renderer/components/settings/MusicSourceSettings.vue index a235343..40e2a57 100644 --- a/src/renderer/components/settings/MusicSourceSettings.vue +++ b/src/renderer/components/settings/MusicSourceSettings.vue @@ -10,56 +10,84 @@ >

{{ t('settings.playback.musicSourcesDesc') }}

+ - + + - {{ source.label }} - + {{ t('settings.playback.sourceLabels.' + source.value) }} + + + {{ t('settings.playback.gdmusicInfo') }} + + + + + + + + {{ t('settings.playback.sourceLabels.custom') }} + + + {{ t('settings.playback.customApi.enableHint') }} + -
- {{ t('settings.playback.musicSourcesWarning') }} -
- -
-

GD音乐台(music.gdstudio.xyz)设置

-

- GD音乐台将自动尝试多个音乐平台进行解析,无需额外配置。优先级高于其他解析方式,但是请求可能较慢。感谢(music.gdstudio.xyz) -

+ +
+ + +
+

+ {{ t('settings.playback.customApi.sectionTitle') }} +

+
+ {{ + t('settings.playback.customApi.importConfig') + }} +

+ {{ t('settings.playback.customApi.currentSource') }}: + {{ settingsStore.setData.customApiPluginName }} +

+

+ {{ t('settings.playback.customApi.notImported') }} +

+
diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index e0c57fc..fe4d61f 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -15,6 +15,7 @@ export * from './modules/lyric'; export * from './modules/menu'; export * from './modules/music'; export * from './modules/player'; +export * from './modules/recommend'; export * from './modules/search'; export * from './modules/settings'; export * from './modules/user'; diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts index 6f99a0a..e32165b 100644 --- a/src/renderer/store/modules/player.ts +++ b/src/renderer/store/modules/player.ts @@ -78,10 +78,14 @@ export const isBilibiliIdMatch = (id1: string | number, id2: string | number): b // 提取公共函数:获取B站视频URL export const getSongUrl = async ( - id: string | number, - songData: SongResult, - isDownloaded: boolean = false + id: string | number, + songData: SongResult, + isDownloaded: boolean = false ) => { + const numericId = typeof id === 'string' ? parseInt(id, 10) : id; + const settingsStore = useSettingsStore(); + const { message } = createDiscreteApi(['message']); // 引入 message API 用于提示 + try { if (songData.playMusicUrl) { return songData.playMusicUrl; @@ -92,8 +96,8 @@ export const getSongUrl = async ( if (!songData.playMusicUrl && songData.bilibiliData.bvid && songData.bilibiliData.cid) { try { songData.playMusicUrl = await getBilibiliAudioUrl( - songData.bilibiliData.bvid, - songData.bilibiliData.cid + songData.bilibiliData.bvid, + songData.bilibiliData.cid ); return songData.playMusicUrl; } catch (error) { @@ -104,14 +108,50 @@ export const getSongUrl = async ( return songData.playMusicUrl || ''; } - const numericId = typeof id === 'string' ? parseInt(id, 10) : id; - // 检查是否有自定义音源设置 + // ==================== 自定义API最优先 ==================== + // 检查用户是否在全局设置中启用了 'custom' 音源 + const globalSources = settingsStore.setData.enabledMusicSources || []; + const useCustomApiGlobally = globalSources.includes('custom'); + + // 检查歌曲是否有专属的 'custom' 音源设置 const songId = String(id); - const savedSource = localStorage.getItem(`song_source_${songId}`); + const savedSourceStr = localStorage.getItem(`song_source_${songId}`); + let useCustomApiForSong = false; + if (savedSourceStr) { + try { + const songSources = JSON.parse(savedSourceStr); + useCustomApiForSong = songSources.includes('custom'); + } catch (e) { + console.error('解析歌曲音源设置失败:', e); + } + } + // 如果全局或歌曲专属设置中启用了自定义API,则最优先尝试 + if ( (useCustomApiGlobally || useCustomApiForSong) && settingsStore.setData.customApiPlugin) { + console.log(`优先级 1: 尝试使用自定义API解析歌曲 ${id}...`); + try { + // 直接从 api 目录导入 parseFromCustomApi 函数 + const { parseFromCustomApi } = await import('@/api/parseFromCustomApi'); + const customResult = await parseFromCustomApi(numericId, cloneDeep(songData), settingsStore.setData.musicQuality || 'higher'); + + if (customResult && customResult.data && customResult.data.data && customResult.data.data.url) { + console.log('自定义API解析成功!'); + if (isDownloaded) return customResult.data.data as any; + return customResult.data.data.url; + } else { + // 自定义API失败,给出提示,然后继续走默认流程 + console.log('自定义API解析失败,将使用默认降级流程...'); + message.warning(i18n.global.t('player.reparse.customApiFailed')); // 给用户一个提示 + } + } catch (error) { + console.error('调用自定义API时发生错误:', error); + message.error(i18n.global.t('player.reparse.customApiError')); + } + } + // 如果自定义API失败或未启用,则执行【原有】的解析流程 // 如果有自定义音源设置,直接使用getParsingMusicUrl获取URL - if (savedSource && songData.source !== 'bilibili') { + if (savedSourceStr && songData.source !== 'bilibili') { try { console.log(`使用自定义音源解析歌曲 ID: ${songId}`); const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); @@ -129,28 +169,33 @@ export const getSongUrl = async ( // 正常获取URL流程 const { data } = await getMusicUrl(numericId, isDownloaded); - let url = ''; - let songDetail = null; - try { - if (data.data[0].freeTrialInfo || !data.data[0].url) { + if (data && data.data && data.data[0]) { + const songDetail = data.data[0]; + const hasNoUrl = !songDetail.url; + const isTrial = !!songDetail.freeTrialInfo; + + if (hasNoUrl || isTrial) { + console.log(`官方URL无效 (无URL: ${hasNoUrl}, 试听: ${isTrial}),进入内置备用解析...`); const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); - url = res.data.data.url; - songDetail = res.data.data; - } else { - songDetail = data.data[0] as any; + if (isDownloaded) return res?.data?.data as any; + return res?.data?.data?.url || null; } - } catch (error) { - console.error('error', error); - url = data.data[0].url || ''; + + console.log('官方API解析成功!'); + if (isDownloaded) return songDetail as any; + return songDetail.url; } - if (isDownloaded) { - return songDetail; - } - url = url || data.data[0].url; - return url; + + console.log('官方API返回数据结构异常,进入内置备用解析...'); + const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); + if (isDownloaded) return res?.data?.data as any; + return res?.data?.data?.url || null; + } catch (error) { - console.error('error', error); - return null; + console.error('官方API请求失败,进入内置备用解析流程:', error); + const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); + if (isDownloaded) return res?.data?.data as any; + return res?.data?.data?.url || null; } }; diff --git a/src/renderer/store/modules/recommend.ts b/src/renderer/store/modules/recommend.ts new file mode 100644 index 0000000..616ba65 --- /dev/null +++ b/src/renderer/store/modules/recommend.ts @@ -0,0 +1,43 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +import { getDayRecommend } from '@/api/home'; +import type { IDayRecommend } from '@/types/day_recommend'; +import type { SongResult } from '@/types/music'; + +export const useRecommendStore = defineStore('recommend', () => { + const dailyRecommendSongs = ref([]); + + const fetchDailyRecommendSongs = async () => { + try { + const { data } = await getDayRecommend(); + const recommendData = data.data as unknown as IDayRecommend; + + if (recommendData && Array.isArray(recommendData.dailySongs)) { + dailyRecommendSongs.value = recommendData.dailySongs as any; + console.log(`[Recommend Store] 已加载 ${recommendData.dailySongs.length} 首每日推荐歌曲。`); + } else { + dailyRecommendSongs.value = []; + } + } catch (error) { + console.error('[Recommend Store] 获取每日推荐失败:', error); + dailyRecommendSongs.value = []; + } + }; + + const replaceSongInDailyRecommend = (oldSongId: number | string, newSong: SongResult) => { + const index = dailyRecommendSongs.value.findIndex((song) => song.id === oldSongId); + if (index !== -1) { + dailyRecommendSongs.value.splice(index, 1, newSong as any); + console.log(`[Recommend Store] 已将歌曲 ${oldSongId} 替换为 ${newSong.name}`); + } else { + console.warn(`[Recommend Store] 未在日推列表中找到要替换的歌曲ID: ${oldSongId}`); + } + }; + + return { + dailyRecommendSongs, + fetchDailyRecommendSongs, + replaceSongInDailyRecommend + }; +}); diff --git a/src/renderer/store/modules/settings.ts b/src/renderer/store/modules/settings.ts index 214b8e9..9461b3f 100644 --- a/src/renderer/store/modules/settings.ts +++ b/src/renderer/store/modules/settings.ts @@ -64,6 +64,17 @@ export const useSettingsStore = defineStore('settings', () => { // 初始化 setData setData.value = getInitialSettings(); + /** + * 保存导入的自定义API插件 + * @param plugin 包含name和content的对象 + */ + const setCustomApiPlugin = (plugin: { name: string; content: string }) => { + setSetData({ + customApiPlugin: plugin.content, + customApiPluginName: plugin.name + }); + }; + const toggleTheme = () => { if (setData.value.autoTheme) { // 如果是自动模式,切换到手动模式并设置相反的主题 @@ -208,6 +219,7 @@ export const useSettingsStore = defineStore('settings', () => { setLanguage, initializeSettings, initializeTheme, - initializeSystemFonts + initializeSystemFonts, + setCustomApiPlugin, }; }); diff --git a/src/renderer/views/music/MusicListPage.vue b/src/renderer/views/music/MusicListPage.vue index e930ce4..5c33efb 100644 --- a/src/renderer/views/music/MusicListPage.vue +++ b/src/renderer/views/music/MusicListPage.vue @@ -227,11 +227,11 @@ import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { subscribePlaylist, updatePlaylistTracks } from '@/api/music'; -import { getMusicDetail, getMusicListByType } from '@/api/music'; +import { getMusicDetail } from '@/api/music'; import PlayBottom from '@/components/common/PlayBottom.vue'; import SongItem from '@/components/common/SongItem.vue'; import { useDownload } from '@/hooks/useDownload'; -import { useMusicStore, usePlayerStore } from '@/store'; +import { useMusicStore, usePlayerStore, useRecommendStore } from '@/store'; import { SongResult } from '@/types/music'; import { getImgUrl, isElectron, isMobile, setAnimationClass } from '@/utils'; @@ -239,14 +239,39 @@ const { t } = useI18n(); const route = useRoute(); const playerStore = usePlayerStore(); const musicStore = useMusicStore(); +const recommendStore = useRecommendStore(); const message = useMessage(); // 从路由参数或状态管理获取数据 -const name = ref(''); const loading = ref(false); -const songList = ref([]); -const listInfo = ref(null); -const canRemove = ref(false); +const isDailyRecommend = computed(() => route.query.type === 'dailyRecommend'); +const name = computed(() => { + if (isDailyRecommend.value) { + return t('comp.recommendSinger.songlist'); // 日推的标题 + } + return musicStore.currentMusicListName || ''; // 其他列表的标题 +}); +const songList = computed(() => { + if (isDailyRecommend.value) { + // 如果是日推页面,直接使用 recommendStore 中响应式的数据 + return recommendStore.dailyRecommendSongs; + } + // 否则,使用 musicStore 中的静态数据 + return musicStore.currentMusicList || []; +}); +const listInfo = computed(() => { + if (isDailyRecommend.value) { + return null; + } + return musicStore.currentListInfo || null; +}); +const canRemove = computed(() => { + if (isDailyRecommend.value) { + return false; + } + return musicStore.canRemoveSong || false; +}); + const canCollect = ref(false); const isCollected = ref(false); @@ -303,78 +328,9 @@ const total = computed(() => { // 初始化数据 onMounted(() => { - initData(); checkCollectionStatus(); }); -// 从 pinia 或路由参数获取数据 - -// 从路由参数获取 -const routeId = route.params.id as string; -const routeType = route.query.type as string; - -const initData = () => { - // 优先从 pinia 获取数据 - if (musicStore.currentMusicList) { - name.value = musicStore.currentMusicListName || ''; - songList.value = musicStore.currentMusicList || []; - listInfo.value = musicStore.currentListInfo || null; - canRemove.value = musicStore.canRemoveSong || false; - - // 初始化歌曲列表 - initSongList(songList.value); - return; - } - - if (routeId) { - // 这里根据 type 和 id 加载数据 - // 例如: 获取歌单、专辑等 - loading.value = true; - loadDataByType(routeType, routeId).finally(() => { - loading.value = false; - }); - } -}; - -// 根据类型加载数据 -const loadDataByType = async (type: string, id: string) => { - try { - const result = await getMusicListByType(type, id); - - if (type === 'album') { - const { songs, album } = result.data; - name.value = album.name; - songList.value = songs.map((song: any) => { - song.al.picUrl = song.al.picUrl || album.picUrl; - song.picUrl = song.al.picUrl || album.picUrl || song.picUrl; - return song; - }); - listInfo.value = { - ...album, - creator: { - avatarUrl: album.artist.img1v1Url, - nickname: `${album.artist.name} - ${album.company}` - }, - description: album.description - }; - } else if (type === 'playlist') { - const { playlist } = result.data; - name.value = playlist.name; - listInfo.value = playlist; - - // 初始化歌曲列表 - if (playlist.tracks) { - songList.value = playlist.tracks; - } - } - - // 初始化歌曲列表 - initSongList(songList.value); - } catch (error) { - console.error('加载数据失败:', error); - } -}; - const getCoverImgUrl = computed(() => { const coverImgUrl = listInfo.value?.coverImgUrl; if (coverImgUrl) { @@ -394,22 +350,27 @@ const getCoverImgUrl = computed(() => { return ''; }); -const getDisplaySongs = computed(() => { - if (routeType === 'dailyRecommend') { - return displayedSongs.value.filter((song) => !playerStore.dislikeList.includes(song.id)); - } else { - return displayedSongs.value; - } -}); - // 过滤歌曲列表 const filteredSongs = computed(() => { + // 1. 确定数据源是来自store的完整列表(日推)还是来自本地分页的列表(其他) + const sourceList = isDailyRecommend.value + ? songList.value // 如果是日推,直接使用来自 recommendStore 的完整、响应式列表 + : displayedSongs.value; // 否则,使用用于分页加载的 displayedSongs + + // 2. 过滤不喜欢的歌曲 + const dislikeFilteredList = sourceList.filter( + (song) => !playerStore.dislikeList.includes(song.id) + ); + // ================================================================= + + // 3. 如果没有搜索词,直接返回处理后的列表 if (!searchKeyword.value) { - return getDisplaySongs.value; + return dislikeFilteredList; } + // 4. 如果有搜索词,在处理后的列表上进行搜索 const keyword = searchKeyword.value.toLowerCase().trim(); - return getDisplaySongs.value.filter((song) => { + return dislikeFilteredList.filter((song) => { const songName = song.name?.toLowerCase() || ''; const albumName = song.al?.name?.toLowerCase() || ''; const artists = song.ar || song.artists || []; @@ -439,6 +400,17 @@ const filteredSongs = computed(() => { }); }); +const resetListState = () => { + page.value = 0; + loadedIds.value.clear(); + displayedSongs.value = []; + completePlaylist.value = []; + hasMore.value = true; + loadingList.value = false; + searchKeyword.value = ''; + isFullPlaylistLoaded.value = false; +}; + // 格式化歌曲数据 const formatSong = (item: any) => { if (!item) { @@ -803,6 +775,20 @@ watch(searchKeyword, () => { } }); +watch( + songList, + (newSongs) => { + resetListState(); + initSongList(newSongs); + if (hasMore.value && listInfo.value?.trackIds) { + setTimeout(() => { + loadMoreSongs(); + }, 300); + } + }, + { immediate: true } +); + // 组件卸载时清理状态 onUnmounted(() => { isPlaylistLoading.value = false;