From 4368c05b80d5da20653a29906ed2e5f67b04d7a8 Mon Sep 17 00:00:00 2001 From: shano Date: Sun, 7 Sep 2025 17:24:18 +0800 Subject: [PATCH 1/8] feat: Flac metadata and optimize cover images --- package.json | 2 + src/main/modules/fileManager.ts | 79 ++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 11 deletions(-) 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/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index b7e74ed..c378154 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; @@ -386,7 +387,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 +577,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 +620,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 +630,7 @@ async function downloadMusic( NodeID3.removeTags(finalFilePath); const tags = { - title: filename, + title: songInfo?.name, artist: artistNames, TPE1: artistNames, TPE2: artistNames, @@ -634,10 +666,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 +740,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: '下载完成', From df236e491c6e87eb7ad498ddf07f4d666b77f26d Mon Sep 17 00:00:00 2001 From: shano Date: Tue, 9 Sep 2025 22:05:48 +0800 Subject: [PATCH 2/8] feat: add custom api --- custom-api-readme.md | 62 ++ src/i18n/lang/en-US/settings.ts | 13 + src/i18n/lang/ja-JP/settings.ts | 655 +++++++++--------- src/i18n/lang/ko-KR/settings.ts | 655 +++++++++--------- src/i18n/lang/zh-CN/settings.ts | 15 + src/i18n/lang/zh-Hant/settings.ts | 13 + src/main/modules/fileManager.ts | 33 + src/main/set.json | 3 + src/preload/index.d.ts | 1 + src/preload/index.ts | 3 +- src/renderer/api/music.ts | 66 +- src/renderer/api/parseFromCustomApi.ts | 106 +++ .../settings/MusicSourceSettings.vue | 151 ++-- src/renderer/store/modules/player.ts | 97 ++- src/renderer/store/modules/settings.ts | 13 + 15 files changed, 1142 insertions(+), 744 deletions(-) create mode 100644 custom-api-readme.md create mode 100644 src/renderer/api/parseFromCustomApi.ts 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/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index b5d3ebf..d7d0554 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -81,6 +81,19 @@ export default { 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', + + customApi: { + importConfig: 'Import JSON Config', + currentSource: 'Current Source', + notImported: 'No custom source imported yet.', + importSuccess: 'Successfully imported source: {name}', + importFailed: 'Import failed: {message}', + }, }, application: { closeAction: 'Close Action', diff --git a/src/i18n/lang/ja-JP/settings.ts b/src/i18n/lang/ja-JP/settings.ts index 182bf4d..12658ec 100644 --- a/src/i18n/lang/ja-JP/settings.ts +++ b/src/i18n/lang/ja-JP/settings.ts @@ -1,322 +1,335 @@ -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} 文字' - } - } +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のステータスバーに音楽コントロール機能を表示できます(再起動後に有効)' + showStatusBarContent: 'Macのステータスバーに音楽コントロール機能を表示できます(再起動後に有効)', + fallbackParser: '代替解析サービス (GD音楽台)', + fallbackParserDesc: '「GD音楽台」にチェックが入っていて、通常の音源で再生できない場合、このサービスが使用されます。', + parserGD: 'GD音楽台 (内蔵)', + parserCustom: 'カスタムAPI', + + customApi: { + 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} 文字' + } + } }; \ 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..2539d83 100644 --- a/src/i18n/lang/ko-KR/settings.ts +++ b/src/i18n/lang/ko-KR/settings.ts @@ -1,322 +1,335 @@ -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} 문자' - } - } +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 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)' + showStatusBarContent: 'Mac 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)', + fallbackParser: '대체 분석 서비스 (GD Music)', + fallbackParserDesc: '"GD Music"을 선택하고 일반 음원을 사용할 수 없을 때 이 서비스를 사용합니다.', + parserGD: 'GD Music (내장)', + parserCustom: '사용자 지정 API', + + customApi: { + 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: '안드로이드 스타일' + }, + 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 diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index df40155..5f25a5e 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -79,6 +79,21 @@ export default { autoPlayDesc: '重新打开应用时是否自动继续播放', showStatusBar: '是否显示状态栏控制功能', showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)' + showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)', + + fallbackParser: 'GD音乐台(music.gdstudio.xyz)设置', + fallbackParserDesc: 'GD音乐台将自动尝试多个音乐平台进行解析,无需额外配置。优先级高于其他解析方式,但是请求可能较慢。感谢(music.gdstudio.xyz)\n', + parserGD: 'GD 音乐台 (内置)', + parserCustom: '自定义 API', + + // 自定义API相关的提示 + customApi: { + importConfig: '导入 JSON 配置', + currentSource: '当前音源', + notImported: '尚未导入自定义音源。', + importSuccess: '成功导入音源: {name}', + importFailed: '导入失败: {message}', + }, }, application: { closeAction: '关闭行为', diff --git a/src/i18n/lang/zh-Hant/settings.ts b/src/i18n/lang/zh-Hant/settings.ts index 4468c19..17e5a43 100644 --- a/src/i18n/lang/zh-Hant/settings.ts +++ b/src/i18n/lang/zh-Hant/settings.ts @@ -79,6 +79,19 @@ export default { autoPlayDesc: '重新開啟應用程式時是否自動繼續播放', showStatusBar: '是否顯示狀態列控制功能', showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)' + showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)', + fallbackParser: '備用解析服務 (GD音樂台)', + fallbackParserDesc: '當勾選「GD音樂台」且常規音源無法播放時,將使用此服務嘗試解析。', + parserGD: 'GD 音樂台 (內建)', + parserCustom: '自訂 API', + + customApi: { + importConfig: '匯入 JSON 設定', + currentSource: '目前音源', + notImported: '尚未匯入自訂音源。', + importSuccess: '成功匯入音源:{name}', + importFailed: '匯入失敗:{message}', + }, }, application: { closeAction: '關閉行為', diff --git a/src/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index b7e74ed..20b434f 100644 --- a/src/main/modules/fileManager.ts +++ b/src/main/modules/fileManager.ts @@ -275,6 +275,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}`); + } + }); } /** diff --git a/src/main/set.json b/src/main/set.json index 59ae862..dfe9406 100644 --- a/src/main/set.json +++ b/src/main/set.json @@ -28,4 +28,7 @@ "contentZoomFactor": 1, "autoTheme": false, "manualTheme": "light" + "manualTheme": "light", + "customApiPlugin": "", + "customApiPluginName": "", } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 4af0cd2..11fb828 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -21,6 +21,7 @@ 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; } diff --git a/src/preload/index.ts b/src/preload/index.ts index c47ac7e..0d021e9 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,7 @@ const api = { return ipcRenderer.invoke(channel, ...args); } return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`)); - } + }, }; // 创建带类型的ipcRenderer对象,暴露给渲染进程 diff --git a/src/renderer/api/music.ts b/src/renderer/api/music.ts index ea574f7..0f52b73 100644 --- a/src/renderer/api/music.ts +++ b/src/renderer/api/music.ts @@ -10,6 +10,8 @@ import requestMusic from '@/utils/request_music'; import { searchAndGetBilibiliAudioUrl } from './bilibili'; import { parseFromGDMusic } from './gdmusic'; +import { parseFromCustomApi } from './parseFromCustomApi'; +import type { ParsedMusicResult } from './gdmusic'; const { addData, getData, deleteData } = musicDB; @@ -114,7 +116,7 @@ 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,19 +148,19 @@ const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => { * @returns 解析结果 */ export const getParsingMusicUrl = async (id: number, data: SongResult) => { - if(isElectron){ + if (isElectron) { const settingStore = useSettingsStore(); // 如果禁用了音乐解析功能,则直接返回空结果 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) { // 使用自定义音源 @@ -168,37 +170,55 @@ export const getParsingMusicUrl = async (id: number, data: SongResult) => { // 使用全局音源设置 musicSources = settingStore.setData.enabledMusicSources || []; console.log(`使用全局音源设置:`, musicSources); - if (musicSources.length > 0) { - return getUnblockMusicAudio(id, data, musicSources); - } } } catch (e) { - console.error('解析音源设置失败,使用全局设置', e); + console.error('解析音源设置失败,回退到默认全局设置', e); musicSources = settingStore.setData.enabledMusicSources || []; } - - // 2. 按优先级解析 - - // 2.1 Bilibili解析(优先级最高) + + const quality = settingStore.setData.musicQuality || 'higher'; + + // 优先级 1: 自定义 API + if (musicSources.includes('custom') && settingStore.setData.customApiPlugin) { + console.log('尝试使用 自定义API 解析...'); + const customResult = await parseFromCustomApi(id, data, quality); + if (customResult) { + return customResult; // 成功则直接返回 + } + console.log('自定义API解析失败,继续尝试其他音源...'); + } + + // 优先级 2: Bilibili if (musicSources.includes('bilibili')) { - return await getBilibiliAudio(data); + console.log('尝试使用 Bilibili 解析...'); + const bilibiliResult = await getBilibiliAudio(data); + if (bilibiliResult?.data?.data?.url) { // 检查返回的 URL 是否有效 + return bilibiliResult; + } + console.log('Bilibili解析失败,继续尝试其他音源...'); } - - // 2.2 GD音乐台解析 + + // 优先级 3: GD 音乐台 if (musicSources.includes('gdmusic')) { + console.log('尝试使用 GD音乐台 解析...'); const gdResult = await getGDMusicAudio(id, data); - if (gdResult) return gdResult; - // GD解析失败,继续下一步 - console.log('GD音乐台解析失败,尝试使用其他音源'); + if (gdResult) { + return gdResult; + } + console.log('GD音乐台解析失败,继续尝试其他音源...'); } - console.log('musicSources', musicSources); - // 2.3 使用unblockMusic解析其他音源 - if (musicSources.length > 0) { - return getUnblockMusicAudio(id, data, musicSources); + + // 优先级 4: UnblockMusic (migu, kugou, pyncmd) + const unblockSources = musicSources.filter( + source => !['custom', 'bilibili', 'gdmusic'].includes(source) + ); + if (unblockSources.length > 0) { + console.log('尝试使用 UnblockMusic 解析:', unblockSources); + return getUnblockMusicAudio(id, data, unblockSources); } } - // 3. 后备方案:使用API请求 + // 后备方案:使用API请求 console.log('无可用音源或不在Electron环境中,使用API请求'); return requestMusic.get('/music', { params: { id } }); }; diff --git a/src/renderer/api/parseFromCustomApi.ts b/src/renderer/api/parseFromCustomApi.ts new file mode 100644 index 0000000..e257739 --- /dev/null +++ b/src/renderer/api/parseFromCustomApi.ts @@ -0,0 +1,106 @@ +import axios from 'axios'; +import {get} from 'lodash'; +import {useSettingsStore} from '@/store'; + +// 从同级目录的 gdmusic.ts 导入类型,确保兼容性 +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; + } +}; \ No newline at end of file diff --git a/src/renderer/components/settings/MusicSourceSettings.vue b/src/renderer/components/settings/MusicSourceSettings.vue index a235343..c55c027 100644 --- a/src/renderer/components/settings/MusicSourceSettings.vue +++ b/src/renderer/components/settings/MusicSourceSettings.vue @@ -1,65 +1,87 @@ + \ No newline at end of file diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts index 6f99a0a..1b9bc46 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,48 @@ 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) { /* ignore parsing error */ } + } + // 如果全局或歌曲专属设置中启用了自定义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('自定义API解析失败,正在尝试使用内置音源...'); // 给用户一个提示 + } + } catch (error) { + console.error('调用自定义API时发生错误:', error); + message.error('自定义API请求出错,正在尝试使用内置音源...'); + } + } + // 如果自定义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 +167,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/settings.ts b/src/renderer/store/modules/settings.ts index 214b8e9..85f5c36 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) { // 如果是自动模式,切换到手动模式并设置相反的主题 @@ -209,5 +220,7 @@ export const useSettingsStore = defineStore('settings', () => { initializeSettings, initializeTheme, initializeSystemFonts + initializeSystemFonts, + setCustomApiPlugin, }; }); From 1171e0f9e79bf34ad4d60593fd8d70ac3eec43ca Mon Sep 17 00:00:00 2001 From: shano Date: Tue, 9 Sep 2025 22:09:44 +0800 Subject: [PATCH 3/8] feat: add custom api --- src/renderer/store/modules/settings.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/store/modules/settings.ts b/src/renderer/store/modules/settings.ts index 85f5c36..9461b3f 100644 --- a/src/renderer/store/modules/settings.ts +++ b/src/renderer/store/modules/settings.ts @@ -219,7 +219,6 @@ export const useSettingsStore = defineStore('settings', () => { setLanguage, initializeSettings, initializeTheme, - initializeSystemFonts initializeSystemFonts, setCustomApiPlugin, }; From 7ae6e041b5f3ba62fd6fadc978cb9f81e6c5848d Mon Sep 17 00:00:00 2001 From: shano Date: Tue, 9 Sep 2025 22:09:44 +0800 Subject: [PATCH 4/8] fix --- src/i18n/lang/en-US/settings.ts | 1 - src/i18n/lang/ja-JP/settings.ts | 3 +-- src/i18n/lang/ko-KR/settings.ts | 3 +-- src/i18n/lang/zh-CN/settings.ts | 1 - src/i18n/lang/zh-Hant/settings.ts | 1 - src/renderer/store/modules/settings.ts | 1 - 6 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index d7d0554..243d0eb 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -80,7 +80,6 @@ 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.', diff --git a/src/i18n/lang/ja-JP/settings.ts b/src/i18n/lang/ja-JP/settings.ts index 12658ec..0e7aecc 100644 --- a/src/i18n/lang/ja-JP/settings.ts +++ b/src/i18n/lang/ja-JP/settings.ts @@ -78,7 +78,6 @@ export default { autoPlay: '自動再生', autoPlayDesc: 'アプリを再起動した際に自動的に再生を継続するかどうか', showStatusBar: 'ステータスバーコントロール機能を表示するかどうか', - showStatusBarContent: 'Macのステータスバーに音楽コントロール機能を表示できます(再起動後に有効)' showStatusBarContent: 'Macのステータスバーに音楽コントロール機能を表示できます(再起動後に有効)', fallbackParser: '代替解析サービス (GD音楽台)', fallbackParserDesc: '「GD音楽台」にチェックが入っていて、通常の音源で再生できない場合、このサービスが使用されます。', @@ -332,4 +331,4 @@ export default { length: '現在の長さ:{length} 文字' } } -}; \ 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 2539d83..51754b3 100644 --- a/src/i18n/lang/ko-KR/settings.ts +++ b/src/i18n/lang/ko-KR/settings.ts @@ -78,7 +78,6 @@ export default { autoPlay: '자동 재생', autoPlayDesc: '앱을 다시 열 때 자동으로 재생을 계속할지 여부', showStatusBar: '상태바 제어 기능 표시 여부', - showStatusBarContent: 'Mac 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)' showStatusBarContent: 'Mac 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)', fallbackParser: '대체 분석 서비스 (GD Music)', fallbackParserDesc: '"GD Music"을 선택하고 일반 음원을 사용할 수 없을 때 이 서비스를 사용합니다.', @@ -332,4 +331,4 @@ export default { length: '현재 길이: {length} 문자' } } -}; \ No newline at end of file +}; diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index 5f25a5e..d798656 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -78,7 +78,6 @@ export default { autoPlay: '自动播放', autoPlayDesc: '重新打开应用时是否自动继续播放', showStatusBar: '是否显示状态栏控制功能', - showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)' showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)', fallbackParser: 'GD音乐台(music.gdstudio.xyz)设置', diff --git a/src/i18n/lang/zh-Hant/settings.ts b/src/i18n/lang/zh-Hant/settings.ts index 17e5a43..fc4c1ac 100644 --- a/src/i18n/lang/zh-Hant/settings.ts +++ b/src/i18n/lang/zh-Hant/settings.ts @@ -78,7 +78,6 @@ export default { autoPlay: '自動播放', autoPlayDesc: '重新開啟應用程式時是否自動繼續播放', showStatusBar: '是否顯示狀態列控制功能', - showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)' showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)', fallbackParser: '備用解析服務 (GD音樂台)', fallbackParserDesc: '當勾選「GD音樂台」且常規音源無法播放時,將使用此服務嘗試解析。', diff --git a/src/renderer/store/modules/settings.ts b/src/renderer/store/modules/settings.ts index 85f5c36..9461b3f 100644 --- a/src/renderer/store/modules/settings.ts +++ b/src/renderer/store/modules/settings.ts @@ -219,7 +219,6 @@ export const useSettingsStore = defineStore('settings', () => { setLanguage, initializeSettings, initializeTheme, - initializeSystemFonts initializeSystemFonts, setCustomApiPlugin, }; From 5ab3143fdd054a0dcbd2cc065a80255108cf5364 Mon Sep 17 00:00:00 2001 From: shano Date: Wed, 10 Sep 2025 00:42:17 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=E4=B8=8B=E8=BD=BD=E6=97=A0=E6=8D=9F?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E4=BC=9A=E8=BF=94=E5=9B=9Ehires=E9=9F=B3?= =?UTF-8?q?=E8=B4=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/api/music.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/api/music.ts b/src/renderer/api/music.ts index ea574f7..e6c908e 100644 --- a/src/renderer/api/music.ts +++ b/src/renderer/api/music.ts @@ -30,6 +30,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 +47,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' } }); }; From 76db7e3ad633175d58d7dd38f8f3731f86e01d96 Mon Sep 17 00:00:00 2001 From: alger Date: Sat, 13 Sep 2025 22:21:11 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A03D=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E7=BB=84=E4=BB=B6=E5=B9=B6=E4=BC=98=E5=8C=96=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E6=8C=89=E9=92=AEhover=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/components/cover/Cover3D.vue | 205 ++++++++++++++++++++ src/renderer/components/lyric/MusicFull.vue | 121 ++++++------ 2 files changed, 259 insertions(+), 67 deletions(-) create mode 100644 src/renderer/components/cover/Cover3D.vue 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/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; From e91667a2e624ff5e3ca26e16a367181158c843f7 Mon Sep 17 00:00:00 2001 From: alger Date: Sat, 13 Sep 2025 22:52:37 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96=E9=97=AE=E9=A2=98=20=E5=92=8C=20=E9=9F=B3=E6=BA=90?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=BA=A7=E4=BB=A5=E5=8F=8A=E9=9F=B3=E6=BA=90?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/lang/en-US/player.ts | 4 +- src/i18n/lang/ja-JP/player.ts | 246 +++++++++--------- src/i18n/lang/ko-KR/player.ts | 244 ++++++++--------- src/i18n/lang/zh-CN/player.ts | 4 +- src/i18n/lang/zh-Hant/player.ts | 4 +- src/main/set.json | 3 +- src/renderer/api/music.ts | 174 ++++++++----- src/renderer/api/parseFromCustomApi.ts | 169 ++++++------ .../settings/MusicSourceSettings.vue | 62 ++--- src/renderer/store/modules/player.ts | 8 +- 10 files changed, 494 insertions(+), 424 deletions(-) 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/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/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/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-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/main/set.json b/src/main/set.json index dfe9406..ac1785b 100644 --- a/src/main/set.json +++ b/src/main/set.json @@ -27,8 +27,7 @@ "showTopAction": false, "contentZoomFactor": 1, "autoTheme": false, - "manualTheme": "light" "manualTheme": "light", "customApiPlugin": "", - "customApiPluginName": "", + "customApiPluginName": "" } diff --git a/src/renderer/api/music.ts b/src/renderer/api/music.ts index 0f52b73..e92029e 100644 --- a/src/renderer/api/music.ts +++ b/src/renderer/api/music.ts @@ -9,9 +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'; -import type { ParsedMusicResult } from './gdmusic'; const { addData, getData, deleteData } = musicDB; @@ -32,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;` } }); @@ -47,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' } }); }; @@ -116,7 +119,8 @@ const getBilibiliAudio = async (data: SongResult) => { * @param data 歌曲数据 * @returns 解析结果,失败时返回null */ -const getGDMusicAudio = async (id: number, data: SongResult): Promise => { // <-- 在这里明确声明返回类型 +const getGDMusicAudio = async (id: number, data: SongResult): Promise => { + // <-- 在这里明确声明返回类型 try { const gdResult = await parseFromGDMusic(id, data, '999'); if (gdResult) { @@ -148,74 +152,120 @@ 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: '音乐解析功能已禁用' } }); - } + // 如果禁用了音乐解析功能,则直接返回空结果 + if (!enableMusicUnblock) { + return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } }); + } - // 1. 确定使用的音源列表(自定义或全局) - const songId = String(id); - const savedSourceStr = localStorage.getItem(`song_source_${songId}`); - let musicSources: any[] = []; + // 1. 确定使用的音源列表(自定义或全局) + const songId = String(id); + const savedSourceStr = (() => { + try { + return localStorage.getItem(`song_source_${songId}`); + } catch (e) { + console.warn('读取本地存储失败,忽略自定义音源', e); + return null; + } + })(); - try { - if (savedSourceStr) { - // 使用自定义音源 - musicSources = JSON.parse(savedSourceStr); - console.log(`使用歌曲 ${id} 自定义音源:`, musicSources); - } else { - // 使用全局音源设置 - musicSources = settingStore.setData.enabledMusicSources || []; - console.log(`使用全局音源设置:`, musicSources); + 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'; } - } catch (e) { - console.error('解析音源设置失败,回退到默认全局设置', e); - musicSources = settingStore.setData.enabledMusicSources || []; - } - const quality = settingStore.setData.musicQuality || 'higher'; - - // 优先级 1: 自定义 API - if (musicSources.includes('custom') && settingStore.setData.customApiPlugin) { - console.log('尝试使用 自定义API 解析...'); - const customResult = await parseFromCustomApi(id, data, quality); - if (customResult) { - return customResult; // 成功则直接返回 + // 优先级 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); } - console.log('自定义API解析失败,继续尝试其他音源...'); - } - // 优先级 2: Bilibili - if (musicSources.includes('bilibili')) { - console.log('尝试使用 Bilibili 解析...'); - const bilibiliResult = await getBilibiliAudio(data); - if (bilibiliResult?.data?.data?.url) { // 检查返回的 URL 是否有效 - return bilibiliResult; + // 优先级 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); } - console.log('Bilibili解析失败,继续尝试其他音源...'); - } - // 优先级 3: GD 音乐台 - if (musicSources.includes('gdmusic')) { - console.log('尝试使用 GD音乐台 解析...'); - const gdResult = await getGDMusicAudio(id, data); - if (gdResult) { - return gdResult; + // 优先级 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); } - console.log('GD音乐台解析失败,继续尝试其他音源...'); - } - // 优先级 4: UnblockMusic (migu, kugou, pyncmd) - const unblockSources = musicSources.filter( - source => !['custom', 'bilibili', 'gdmusic'].includes(source) - ); - if (unblockSources.length > 0) { - console.log('尝试使用 UnblockMusic 解析:', unblockSources); - return getUnblockMusicAudio(id, data, unblockSources); + // 优先级 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('getParsingMusicUrl 执行异常,将使用后备方案:', e); } // 后备方案:使用API请求 @@ -228,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 index e257739..c61d45c 100644 --- a/src/renderer/api/parseFromCustomApi.ts +++ b/src/renderer/api/parseFromCustomApi.ts @@ -1,106 +1,107 @@ import axios from 'axios'; -import {get} from 'lodash'; -import {useSettingsStore} from '@/store'; +import { get } from 'lodash'; -// 从同级目录的 gdmusic.ts 导入类型,确保兼容性 -import type {ParsedMusicResult} from './gdmusic'; +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; + 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 + id: number, + _songData: any, + quality: string = 'higher', + timeout: number = 10000 ): Promise => { - const settingsStore = useSettingsStore(); - const pluginString = settingsStore.setData.customApiPlugin; + const settingsStore = useSettingsStore(); + const pluginString = settingsStore.setData.customApiPlugin; - if (!pluginString) { - return null; + 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; + } } - 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; + // 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 }); } - console.log(`自定义API:正在使用插件 [${plugin.name}] 进行解析...`); + // 4. 使用 lodash.get 安全地从响应数据中提取URL + const musicUrl = get(response.data, plugin.responseUrlPath); - 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; - } + 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' } } - - // 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; + }; + } else { + console.error('自定义API:根据路径未能从响应中找到URL:', plugin.responseUrlPath); + return null; } -}; \ No newline at end of file + } catch (error) { + console.error(`自定义API [${plugin.name}] 执行失败:`, error); + return null; + } +}; diff --git a/src/renderer/components/settings/MusicSourceSettings.vue b/src/renderer/components/settings/MusicSourceSettings.vue index c55c027..ef23a9b 100644 --- a/src/renderer/components/settings/MusicSourceSettings.vue +++ b/src/renderer/components/settings/MusicSourceSettings.vue @@ -1,12 +1,12 @@ @@ -68,8 +67,8 @@ import { useMessage } from 'naive-ui'; import { ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useSettingsStore } from '@/store'; +import { useSettingsStore } from '@/store'; import { type Platform } from '@/types/music'; // 扩展 Platform 类型以包含 'custom' @@ -120,45 +119,48 @@ const importPlugin = async () => { }; // 监听自定义插件内容的变化。如果用户清除了插件,要确保 'custom' 选项被取消勾选 -watch(() => settingsStore.setData.customApiPlugin, (newPluginContent) => { - if (!newPluginContent) { - const index = selectedSources.value.indexOf('custom'); - if (index > -1) { - selectedSources.value.splice(index, 1); +watch( + () => settingsStore.setData.customApiPlugin, + (newPluginContent) => { + if (!newPluginContent) { + const index = selectedSources.value.indexOf('custom'); + if (index > -1) { + selectedSources.value.splice(index, 1); + } } } -}); +); // 同步外部show属性变化 watch( - () => props.show, - (newVal) => { - visible.value = newVal; - } + () => props.show, + (newVal) => { + visible.value = newVal; + } ); // 同步内部visible变化 watch( - () => visible.value, - (newVal) => { - emit('update:show', newVal); - } + () => visible.value, + (newVal) => { + emit('update:show', newVal); + } ); // 同步外部sources属性变化 watch( - () => props.sources, - (newVal) => { - selectedSources.value = [...newVal]; - }, - { deep: true } + () => props.sources, + (newVal) => { + selectedSources.value = [...newVal]; + }, + { deep: true } ); const handleConfirm = () => { // 确保至少选择一个音源 const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili']; const valuesToEmit = - selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms; + selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms; emit('update:sources', valuesToEmit); visible.value = false; }; @@ -167,4 +169,4 @@ const handleCancel = () => { selectedSources.value = [...props.sources]; visible.value = false; }; - \ No newline at end of file + diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts index 1b9bc46..e32165b 100644 --- a/src/renderer/store/modules/player.ts +++ b/src/renderer/store/modules/player.ts @@ -122,7 +122,9 @@ export const getSongUrl = async ( try { const songSources = JSON.parse(savedSourceStr); useCustomApiForSong = songSources.includes('custom'); - } catch (e) { /* ignore parsing error */ } + } catch (e) { + console.error('解析歌曲音源设置失败:', e); + } } // 如果全局或歌曲专属设置中启用了自定义API,则最优先尝试 @@ -140,11 +142,11 @@ export const getSongUrl = async ( } else { // 自定义API失败,给出提示,然后继续走默认流程 console.log('自定义API解析失败,将使用默认降级流程...'); - message.warning('自定义API解析失败,正在尝试使用内置音源...'); // 给用户一个提示 + message.warning(i18n.global.t('player.reparse.customApiFailed')); // 给用户一个提示 } } catch (error) { console.error('调用自定义API时发生错误:', error); - message.error('自定义API请求出错,正在尝试使用内置音源...'); + message.error(i18n.global.t('player.reparse.customApiError')); } } // 如果自定义API失败或未启用,则执行【原有】的解析流程 From 9003de8d4b2b6c740a597cf04227459272e2790f Mon Sep 17 00:00:00 2001 From: alger Date: Sat, 13 Sep 2025 23:16:07 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E9=9F=B3=E6=BA=90?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/lang/en-US/settings.ts | 12 +++++++ src/i18n/lang/ja-JP/settings.ts | 15 ++++++-- src/i18n/lang/ko-KR/settings.ts | 12 +++++++ src/i18n/lang/zh-CN/settings.ts | 12 +++++++ src/i18n/lang/zh-Hant/settings.ts | 12 +++++++ .../settings/MusicSourceSettings.vue | 35 +++++++++++-------- 6 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index 243d0eb..fcec2cd 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -86,12 +86,24 @@ export default { 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: { diff --git a/src/i18n/lang/ja-JP/settings.ts b/src/i18n/lang/ja-JP/settings.ts index 0e7aecc..c8f563d 100644 --- a/src/i18n/lang/ja-JP/settings.ts +++ b/src/i18n/lang/ja-JP/settings.ts @@ -81,10 +81,19 @@ export default { showStatusBarContent: 'Macのステータスバーに音楽コントロール機能を表示できます(再起動後に有効)', fallbackParser: '代替解析サービス (GD音楽台)', fallbackParserDesc: '「GD音楽台」にチェックが入っていて、通常の音源で再生できない場合、このサービスが使用されます。', - parserGD: 'GD音楽台 (内蔵)', - parserCustom: 'カスタムAPI', - + 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: 'カスタム音源はまだインポートされていません。', diff --git a/src/i18n/lang/ko-KR/settings.ts b/src/i18n/lang/ko-KR/settings.ts index 51754b3..4169ffc 100644 --- a/src/i18n/lang/ko-KR/settings.ts +++ b/src/i18n/lang/ko-KR/settings.ts @@ -84,12 +84,24 @@ export default { 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: { diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index d798656..7730e4e 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -85,13 +85,25 @@ export default { 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: { diff --git a/src/i18n/lang/zh-Hant/settings.ts b/src/i18n/lang/zh-Hant/settings.ts index fc4c1ac..433659b 100644 --- a/src/i18n/lang/zh-Hant/settings.ts +++ b/src/i18n/lang/zh-Hant/settings.ts @@ -84,12 +84,24 @@ export default { 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: { diff --git a/src/renderer/components/settings/MusicSourceSettings.vue b/src/renderer/components/settings/MusicSourceSettings.vue index ef23a9b..40e2a57 100644 --- a/src/renderer/components/settings/MusicSourceSettings.vue +++ b/src/renderer/components/settings/MusicSourceSettings.vue @@ -16,7 +16,7 @@ - {{ source.label }} + {{ t('settings.playback.sourceLabels.' + source.value) }}