diff --git a/auto-eslint.mjs b/auto-eslint.mjs index 5716b26d5..2dd6c614e 100644 --- a/auto-eslint.mjs +++ b/auto-eslint.mjs @@ -1,315 +1,315 @@ export default { - globals: { - Component: true, - ComponentPublicInstance: true, - ComputedRef: true, - DirectiveBinding: true, - EffectScope: true, - ExtractDefaultPropTypes: true, - ExtractPropTypes: true, - ExtractPublicPropTypes: true, - InjectionKey: true, - MaybeRef: true, - MaybeRefOrGetter: true, - PropType: true, - Ref: true, - ShallowRef: true, - Slot: true, - Slots: true, - VNode: true, - WritableComputedRef: true, - asyncComputed: true, - autoResetRef: true, - computed: true, - computedAsync: true, - computedEager: true, - computedInject: true, - computedWithControl: true, - controlledComputed: true, - controlledRef: true, - createApp: true, - createEventHook: true, - createGlobalState: true, - createInjectionState: true, - createReactiveFn: true, - createRef: true, - createReusableTemplate: true, - createSharedComposable: true, - createTemplatePromise: true, - createUnrefFn: true, - customRef: true, - debouncedRef: true, - debouncedWatch: true, - defineAsyncComponent: true, - defineComponent: true, - eagerComputed: true, - effectScope: true, - extendRef: true, - getCurrentInstance: true, - getCurrentScope: true, - getCurrentWatcher: true, - h: true, - ignorableWatch: true, - inject: true, - injectLocal: true, - isDefined: true, - isProxy: true, - isReactive: true, - isReadonly: true, - isRef: true, - isShallow: true, - makeDestructurable: true, - markRaw: true, - nextTick: true, - onActivated: true, - onBeforeMount: true, - onBeforeRouteLeave: true, - onBeforeRouteUpdate: true, - onBeforeUnmount: true, - onBeforeUpdate: true, - onClickOutside: true, - onDeactivated: true, - onElementRemoval: true, - onErrorCaptured: true, - onKeyStroke: true, - onLongPress: true, - onMounted: true, - onRenderTracked: true, - onRenderTriggered: true, - onScopeDispose: true, - onServerPrefetch: true, - onStartTyping: true, - onUnmounted: true, - onUpdated: true, - onWatcherCleanup: true, - pausableWatch: true, - provide: true, - provideLocal: true, - reactify: true, - reactifyObject: true, - reactive: true, - reactiveComputed: true, - reactiveOmit: true, - reactivePick: true, - readonly: true, - ref: true, - refAutoReset: true, - refDebounced: true, - refDefault: true, - refManualReset: true, - refThrottled: true, - refWithControl: true, - resolveComponent: true, - resolveRef: true, - shallowReactive: true, - shallowReadonly: true, - shallowRef: true, - syncRef: true, - syncRefs: true, - templateRef: true, - throttledRef: true, - throttledWatch: true, - toRaw: true, - toReactive: true, - toRef: true, - toRefs: true, - toValue: true, - triggerRef: true, - tryOnBeforeMount: true, - tryOnBeforeUnmount: true, - tryOnMounted: true, - tryOnScopeDispose: true, - tryOnUnmounted: true, - unref: true, - unrefElement: true, - until: true, - useActiveElement: true, - useAnimate: true, - useArrayDifference: true, - useArrayEvery: true, - useArrayFilter: true, - useArrayFind: true, - useArrayFindIndex: true, - useArrayFindLast: true, - useArrayIncludes: true, - useArrayJoin: true, - useArrayMap: true, - useArrayReduce: true, - useArraySome: true, - useArrayUnique: true, - useAsyncQueue: true, - useAsyncState: true, - useAttrs: true, - useBase64: true, - useBattery: true, - useBluetooth: true, - useBreakpoints: true, - useBroadcastChannel: true, - useBrowserLocation: true, - useCached: true, - useClipboard: true, - useClipboardItems: true, - useCloned: true, - useColorMode: true, - useConfirmDialog: true, - useCountdown: true, - useCounter: true, - useCssModule: true, - useCssVar: true, - useCssVars: true, - useCurrentElement: true, - useCycleList: true, - useDark: true, - useDateFormat: true, - useDebounce: true, - useDebounceFn: true, - useDebouncedRefHistory: true, - useDeviceMotion: true, - useDeviceOrientation: true, - useDevicePixelRatio: true, - useDevicesList: true, - useDialog: true, - useDisplayMedia: true, - useDocumentVisibility: true, - useDraggable: true, - useDropZone: true, - useElementBounding: true, - useElementByPoint: true, - useElementHover: true, - useElementSize: true, - useElementVisibility: true, - useEventBus: true, - useEventListener: true, - useEventSource: true, - useEyeDropper: true, - useFavicon: true, - useFetch: true, - useFileDialog: true, - useFileSystemAccess: true, - useFocus: true, - useFocusWithin: true, - useFps: true, - useFullscreen: true, - useGamepad: true, - useGeolocation: true, - useId: true, - useIdle: true, - useImage: true, - useInfiniteScroll: true, - useIntersectionObserver: true, - useInterval: true, - useIntervalFn: true, - useKeyModifier: true, - useLastChanged: true, - useLink: true, - useLoadingBar: true, - useLocalStorage: true, - useMagicKeys: true, - useManualRefHistory: true, - useMediaControls: true, - useMediaQuery: true, - useMemoize: true, - useMemory: true, - useMessage: true, - useModel: true, - useMounted: true, - useMouse: true, - useMouseInElement: true, - useMousePressed: true, - useMutationObserver: true, - useNavigatorLanguage: true, - useNetwork: true, - useNotification: true, - useNow: true, - useObjectUrl: true, - useOffsetPagination: true, - useOnline: true, - usePageLeave: true, - useParallax: true, - useParentElement: true, - usePerformanceObserver: true, - usePermission: true, - usePointer: true, - usePointerLock: true, - usePointerSwipe: true, - usePreferredColorScheme: true, - usePreferredContrast: true, - usePreferredDark: true, - usePreferredLanguages: true, - usePreferredReducedMotion: true, - usePreferredReducedTransparency: true, - usePrevious: true, - useRafFn: true, - useRefHistory: true, - useResizeObserver: true, - useRoute: true, - useRouter: true, - useSSRWidth: true, - useScreenOrientation: true, - useScreenSafeArea: true, - useScriptTag: true, - useScroll: true, - useScrollLock: true, - useSessionStorage: true, - useShare: true, - useSlots: true, - useSorted: true, - useSpeechRecognition: true, - useSpeechSynthesis: true, - useStepper: true, - useStorage: true, - useStorageAsync: true, - useStyleTag: true, - useSupported: true, - useSwipe: true, - useTemplateRef: true, - useTemplateRefsList: true, - useTextDirection: true, - useTextSelection: true, - useTextareaAutosize: true, - useThrottle: true, - useThrottleFn: true, - useThrottledRefHistory: true, - useTimeAgo: true, - useTimeAgoIntl: true, - useTimeout: true, - useTimeoutFn: true, - useTimeoutPoll: true, - useTimestamp: true, - useTitle: true, - useToNumber: true, - useToString: true, - useToggle: true, - useTransition: true, - useUrlSearchParams: true, - useUserMedia: true, - useVModel: true, - useVModels: true, - useVibrate: true, - useVirtualList: true, - useWakeLock: true, - useWebNotification: true, - useWebSocket: true, - useWebWorker: true, - useWebWorkerFn: true, - useWindowFocus: true, - useWindowScroll: true, - useWindowSize: true, - watch: true, - watchArray: true, - watchAtMost: true, - watchDebounced: true, - watchDeep: true, - watchEffect: true, - watchIgnorable: true, - watchImmediate: true, - watchOnce: true, - watchPausable: true, - watchPostEffect: true, - watchSyncEffect: true, - watchThrottled: true, - watchTriggerable: true, - watchWithFilter: true, - whenever: true, - }, -}; + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "DirectiveBinding": true, + "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "MaybeRef": true, + "MaybeRefOrGetter": true, + "PropType": true, + "Ref": true, + "ShallowRef": true, + "Slot": true, + "Slots": true, + "VNode": true, + "WritableComputedRef": true, + "asyncComputed": true, + "autoResetRef": true, + "computed": true, + "computedAsync": true, + "computedEager": true, + "computedInject": true, + "computedWithControl": true, + "controlledComputed": true, + "controlledRef": true, + "createApp": true, + "createEventHook": true, + "createGlobalState": true, + "createInjectionState": true, + "createReactiveFn": true, + "createRef": true, + "createReusableTemplate": true, + "createSharedComposable": true, + "createTemplatePromise": true, + "createUnrefFn": true, + "customRef": true, + "debouncedRef": true, + "debouncedWatch": true, + "defineAsyncComponent": true, + "defineComponent": true, + "eagerComputed": true, + "effectScope": true, + "extendRef": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "getCurrentWatcher": true, + "h": true, + "ignorableWatch": true, + "inject": true, + "injectLocal": true, + "isDefined": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "isShallow": true, + "makeDestructurable": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeRouteLeave": true, + "onBeforeRouteUpdate": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onClickOutside": true, + "onDeactivated": true, + "onElementRemoval": true, + "onErrorCaptured": true, + "onKeyStroke": true, + "onLongPress": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onStartTyping": true, + "onUnmounted": true, + "onUpdated": true, + "onWatcherCleanup": true, + "pausableWatch": true, + "provide": true, + "provideLocal": true, + "reactify": true, + "reactifyObject": true, + "reactive": true, + "reactiveComputed": true, + "reactiveOmit": true, + "reactivePick": true, + "readonly": true, + "ref": true, + "refAutoReset": true, + "refDebounced": true, + "refDefault": true, + "refManualReset": true, + "refThrottled": true, + "refWithControl": true, + "resolveComponent": true, + "resolveRef": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "syncRef": true, + "syncRefs": true, + "templateRef": true, + "throttledRef": true, + "throttledWatch": true, + "toRaw": true, + "toReactive": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "tryOnBeforeMount": true, + "tryOnBeforeUnmount": true, + "tryOnMounted": true, + "tryOnScopeDispose": true, + "tryOnUnmounted": true, + "unref": true, + "unrefElement": true, + "until": true, + "useActiveElement": true, + "useAnimate": true, + "useArrayDifference": true, + "useArrayEvery": true, + "useArrayFilter": true, + "useArrayFind": true, + "useArrayFindIndex": true, + "useArrayFindLast": true, + "useArrayIncludes": true, + "useArrayJoin": true, + "useArrayMap": true, + "useArrayReduce": true, + "useArraySome": true, + "useArrayUnique": true, + "useAsyncQueue": true, + "useAsyncState": true, + "useAttrs": true, + "useBase64": true, + "useBattery": true, + "useBluetooth": true, + "useBreakpoints": true, + "useBroadcastChannel": true, + "useBrowserLocation": true, + "useCached": true, + "useClipboard": true, + "useClipboardItems": true, + "useCloned": true, + "useColorMode": true, + "useConfirmDialog": true, + "useCountdown": true, + "useCounter": true, + "useCssModule": true, + "useCssVar": true, + "useCssVars": true, + "useCurrentElement": true, + "useCycleList": true, + "useDark": true, + "useDateFormat": true, + "useDebounce": true, + "useDebounceFn": true, + "useDebouncedRefHistory": true, + "useDeviceMotion": true, + "useDeviceOrientation": true, + "useDevicePixelRatio": true, + "useDevicesList": true, + "useDialog": true, + "useDisplayMedia": true, + "useDocumentVisibility": true, + "useDraggable": true, + "useDropZone": true, + "useElementBounding": true, + "useElementByPoint": true, + "useElementHover": true, + "useElementSize": true, + "useElementVisibility": true, + "useEventBus": true, + "useEventListener": true, + "useEventSource": true, + "useEyeDropper": true, + "useFavicon": true, + "useFetch": true, + "useFileDialog": true, + "useFileSystemAccess": true, + "useFocus": true, + "useFocusWithin": true, + "useFps": true, + "useFullscreen": true, + "useGamepad": true, + "useGeolocation": true, + "useId": true, + "useIdle": true, + "useImage": true, + "useInfiniteScroll": true, + "useIntersectionObserver": true, + "useInterval": true, + "useIntervalFn": true, + "useKeyModifier": true, + "useLastChanged": true, + "useLink": true, + "useLoadingBar": true, + "useLocalStorage": true, + "useMagicKeys": true, + "useManualRefHistory": true, + "useMediaControls": true, + "useMediaQuery": true, + "useMemoize": true, + "useMemory": true, + "useMessage": true, + "useModel": true, + "useMounted": true, + "useMouse": true, + "useMouseInElement": true, + "useMousePressed": true, + "useMutationObserver": true, + "useNavigatorLanguage": true, + "useNetwork": true, + "useNotification": true, + "useNow": true, + "useObjectUrl": true, + "useOffsetPagination": true, + "useOnline": true, + "usePageLeave": true, + "useParallax": true, + "useParentElement": true, + "usePerformanceObserver": true, + "usePermission": true, + "usePointer": true, + "usePointerLock": true, + "usePointerSwipe": true, + "usePreferredColorScheme": true, + "usePreferredContrast": true, + "usePreferredDark": true, + "usePreferredLanguages": true, + "usePreferredReducedMotion": true, + "usePreferredReducedTransparency": true, + "usePrevious": true, + "useRafFn": true, + "useRefHistory": true, + "useResizeObserver": true, + "useRoute": true, + "useRouter": true, + "useSSRWidth": true, + "useScreenOrientation": true, + "useScreenSafeArea": true, + "useScriptTag": true, + "useScroll": true, + "useScrollLock": true, + "useSessionStorage": true, + "useShare": true, + "useSlots": true, + "useSorted": true, + "useSpeechRecognition": true, + "useSpeechSynthesis": true, + "useStepper": true, + "useStorage": true, + "useStorageAsync": true, + "useStyleTag": true, + "useSupported": true, + "useSwipe": true, + "useTemplateRef": true, + "useTemplateRefsList": true, + "useTextDirection": true, + "useTextSelection": true, + "useTextareaAutosize": true, + "useThrottle": true, + "useThrottleFn": true, + "useThrottledRefHistory": true, + "useTimeAgo": true, + "useTimeAgoIntl": true, + "useTimeout": true, + "useTimeoutFn": true, + "useTimeoutPoll": true, + "useTimestamp": true, + "useTitle": true, + "useToNumber": true, + "useToString": true, + "useToggle": true, + "useTransition": true, + "useUrlSearchParams": true, + "useUserMedia": true, + "useVModel": true, + "useVModels": true, + "useVibrate": true, + "useVirtualList": true, + "useWakeLock": true, + "useWebNotification": true, + "useWebSocket": true, + "useWebWorker": true, + "useWebWorkerFn": true, + "useWindowFocus": true, + "useWindowScroll": true, + "useWindowSize": true, + "watch": true, + "watchArray": true, + "watchAtMost": true, + "watchDebounced": true, + "watchDeep": true, + "watchEffect": true, + "watchIgnorable": true, + "watchImmediate": true, + "watchOnce": true, + "watchPausable": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "watchThrottled": true, + "watchTriggerable": true, + "watchWithFilter": true, + "whenever": true + } +} diff --git a/components.d.ts b/components.d.ts index 1db15c675..eeb6b7f2d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -19,16 +19,11 @@ declare module 'vue' { AutoClose: typeof import('./src/components/Modal/AutoClose.vue')['default'] BackgroundRender: typeof import('./src/components/AMLL/BackgroundRender.vue')['default'] BatchList: typeof import('./src/components/Modal/BatchList.vue')['default'] - CacheDirectory: typeof import('./src/components/Setting/components/CacheDirectory.vue')['default'] - CacheLimitConfig: typeof import('./src/components/Setting/components/CacheLimitConfig.vue')['default'] CacheSizeLimit: typeof import('./src/components/Setting/components/CacheSizeLimit.vue')['default'] ChangeRate: typeof import('./src/components/Modal/ChangeRate.vue')['default'] CloudMatch: typeof import('./src/components/Modal/CloudMatch.vue')['default'] - CommentFilter: typeof import('./src/components/Modal/CommentFilter.vue')['default'] CommentList: typeof import('./src/components/List/CommentList.vue')['default'] - ConfigurableInputNumber: typeof import('./src/components/Setting/items/ConfigurableInputNumber.vue')['default'] ContextMenuManager: typeof import('./src/components/Modal/Setting/ContextMenuManager.vue')['default'] - copy: typeof import('./src/components/Global/Provider copy.vue')['default'] CopyLyrics: typeof import('./src/components/Modal/CopyLyrics.vue')['default'] CoverList: typeof import('./src/components/List/CoverList.vue')['default'] CoverManager: typeof import('./src/components/Modal/Setting/CoverManager.vue')['default'] @@ -36,10 +31,7 @@ declare module 'vue' { CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default'] CustomCode: typeof import('./src/components/Modal/Setting/CustomCode.vue')['default'] DefaultLyric: typeof import('./src/components/Player/PlayerLyric/DefaultLyric.vue')['default'] - DiscordRpcConfig: typeof import('./src/components/Setting/components/DiscordRpcConfig.vue')['default'] - DownloadDirectory: typeof import('./src/components/Setting/components/DownloadDirectory.vue')['default'] DownloadModal: typeof import('./src/components/Modal/DownloadModal.vue')['default'] - DownloadPathButtons: typeof import('./src/components/Setting/components/DownloadPathButtons.vue')['default'] Equalizer: typeof import('./src/components/Modal/Equalizer.vue')['default'] ExcludeComment: typeof import('./src/components/Modal/Setting/ExcludeComment.vue')['default'] ExcludeLyrics: typeof import('./src/components/Modal/Setting/ExcludeLyrics.vue')['default'] @@ -47,19 +39,12 @@ declare module 'vue' { FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default'] FullPlayerMobile: typeof import('./src/components/Player/FullPlayerMobile.vue')['default'] FullscreenPlayerManager: typeof import('./src/components/Modal/Setting/FullscreenPlayerManager.vue')['default'] - GeneralSetting: typeof import('./src/components/Setting/old/GeneralSetting.vue')['default'] HomePageSectionManager: typeof import('./src/components/Modal/Setting/HomePageSectionManager.vue')['default'] JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default'] - KeyboardSetting: typeof import('./src/components/Setting/old/KeyboardSetting.vue')['default'] - LastfmConfig: typeof import('./src/components/Setting/components/LastfmConfig.vue')['default'] ListComment: typeof import('./src/components/List/ListComment.vue')['default'] ListDetail: typeof import('./src/components/List/ListDetail.vue')['default'] LocalLyricDirectories: typeof import('./src/components/Setting/components/LocalLyricDirectories.vue')['default'] - LocalMusicDirectories: typeof import('./src/components/Setting/components/LocalMusicDirectories.vue')['default'] LocalMusicDirectory: typeof import('./src/components/Modal/Setting/LocalMusicDirectory.vue')['default'] - LocalMusicDirectoryModal: typeof import('./src/components/Modal/Setting/LocalMusicDirectoryModal.vue')['default'] - LocalPathConfig: typeof import('./src/components/Setting/components/LocalPathConfig.vue')['default'] - LocalSetting: typeof import('./src/components/Setting/old/LocalSetting.vue')['default'] Login: typeof import('./src/components/Modal/Login/Login.vue')['default'] LoginCookie: typeof import('./src/components/Modal/Login/LoginCookie.vue')['default'] LoginPhone: typeof import('./src/components/Modal/Login/LoginPhone.vue')['default'] @@ -68,7 +53,6 @@ declare module 'vue' { Logo: typeof import('./src/components/Layout/Logo.vue')['default'] LyricPlayer: typeof import('./src/components/AMLL/LyricPlayer.vue')['default'] LyricPreview: typeof import('./src/components/Setting/components/LyricPreview.vue')['default'] - LyricsSetting: typeof import('./src/components/Setting/old/LyricsSetting.vue')['default'] MainPlayer: typeof import('./src/components/Player/MainPlayer.vue')['default'] MainSetting: typeof import('./src/components/Setting/MainSetting.vue')['default'] Menu: typeof import('./src/components/Layout/Menu.vue')['default'] @@ -151,7 +135,6 @@ declare module 'vue' { NText: typeof import('naive-ui')['NText'] NThing: typeof import('naive-ui')['NThing'] NTree: typeof import('naive-ui')['NTree'] - OtherSetting: typeof import('./src/components/Setting/old/OtherSetting.vue')['default'] PersonalFM: typeof import('./src/components/Player/PlayerComponents/PersonalFM.vue')['default'] PlayerBackground: typeof import('./src/components/Player/PlayerMeta/PlayerBackground.vue')['default'] PlayerComment: typeof import('./src/components/Player/PlayerComponents/PlayerComment.vue')['default'] @@ -166,9 +149,7 @@ declare module 'vue' { PlayerSpectrum: typeof import('./src/components/Player/PlayerComponents/PlayerSpectrum.vue')['default'] PlaylistAdd: typeof import('./src/components/Modal/PlaylistAdd.vue')['default'] PlaylistPageManager: typeof import('./src/components/Modal/Setting/PlaylistPageManager.vue')['default'] - PlaySetting: typeof import('./src/components/Setting/old/PlaySetting.vue')['default'] Provider: typeof import('./src/components/Global/Provider.vue')['default'] - ProxyConfig: typeof import('./src/components/Setting/components/ProxyConfig.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] ScalingModal: typeof import('./src/components/Modal/ScalingModal.vue')['default'] @@ -193,17 +174,14 @@ declare module 'vue' { SongUnlockManager: typeof import('./src/components/Modal/Setting/SongUnlockManager.vue')['default'] StreamingServerConfig: typeof import('./src/components/Modal/Setting/StreamingServerConfig.vue')['default'] StreamingServerList: typeof import('./src/components/Setting/components/StreamingServerList.vue')['default'] - StreamingSetting: typeof import('./src/components/Setting/old/StreamingSetting.vue')['default'] SvgIcon: typeof import('./src/components/Global/SvgIcon.vue')['default'] TextContainer: typeof import('./src/components/Global/TextContainer.vue')['default'] ThemeConfig: typeof import('./src/components/Modal/ThemeConfig.vue')['default'] - ThirdSetting: typeof import('./src/components/Setting/old/ThirdSetting.vue')['default'] UniversalSetting: typeof import('./src/components/Setting/UniversalSetting.vue')['default'] UpdateApp: typeof import('./src/components/Modal/UpdateApp.vue')['default'] UpdatePlaylist: typeof import('./src/components/Modal/UpdatePlaylist.vue')['default'] User: typeof import('./src/components/Layout/User.vue')['default'] UserAgreement: typeof import('./src/components/Modal/UserAgreement.vue')['default'] VirtualScroll: typeof import('./src/components/UI/VirtualScroll.vue')['default'] - WebsocketConfig: typeof import('./src/components/Setting/components/WebsocketConfig.vue')['default'] } } diff --git a/electron/main/ipc/ipc-store.ts b/electron/main/ipc/ipc-store.ts index 04e35bfdc..57cd8464f 100644 --- a/electron/main/ipc/ipc-store.ts +++ b/electron/main/ipc/ipc-store.ts @@ -95,17 +95,17 @@ const initStoreIpc = (): void => { if (filePaths && filePaths.length > 0) { console.log("[IPC] Importing from:", filePaths[0]); const fileContent = await readFile(filePaths[0], "utf-8"); - + let settings; try { settings = JSON.parse(fileContent); - } catch (e) { + } catch { return { success: false, error: "invalid_json" }; } // 基础结构验证 if (!settings || typeof settings !== "object") { - return { success: false, error: "invalid_format" }; + return { success: false, error: "invalid_format" }; } // 恢复 Electron Store 配置 diff --git a/native/external-media-integration/index.d.ts b/native/external-media-integration/index.d.ts index 80658212a..be6065e15 100644 --- a/native/external-media-integration/index.d.ts +++ b/native/external-media-integration/index.d.ts @@ -1,7 +1,7 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ /** 关闭 Discord RPC */ -export declare function disableDiscordRpc(): void; +export declare function disableDiscordRpc(): void /** * 禁用媒体控件 @@ -10,7 +10,7 @@ export declare function disableDiscordRpc(): void; * * 会在调用 API 失败时抛出错误 */ -export declare function disableSystemMedia(): void; +export declare function disableSystemMedia(): void /** Discord 配置参数 */ export interface DiscordConfigPayload { @@ -19,9 +19,9 @@ export interface DiscordConfigPayload { * * 注意暂停时进度会固定为 0 */ - showWhenPaused: boolean; + showWhenPaused: boolean /** 显示模式,参考 [`DiscordDisplayMode`] */ - displayMode?: DiscordDisplayMode; + displayMode?: DiscordDisplayMode } /** @@ -29,13 +29,12 @@ export interface DiscordConfigPayload { * * 不打开详细信息面板时,在用户名下方显示的小字 */ -export type DiscordDisplayMode = - /** Listening to SPlayer */ - | "Name" - /** Listening to Rick Astley */ - | "State" - /** Listening to Never Gonna Give You Up */ - | "Details"; +export type DiscordDisplayMode = /** Listening to SPlayer */ +'Name'| +/** Listening to Rick Astley */ +'State'| +/** Listening to Never Gonna Give You Up */ +'Details'; /** * 启用 Discord RPC @@ -44,7 +43,7 @@ export type DiscordDisplayMode = * * 启用后会立刻尝试连接,如果 Discord 未启动,或因为其他未知原因连接失败,会每 5 秒尝试连接一次 */ -export declare function enableDiscordRpc(): void; +export declare function enableDiscordRpc(): void /** * 启用媒体控件 @@ -53,7 +52,7 @@ export declare function enableDiscordRpc(): void; * * 会在调用 API 失败时抛出错误 */ -export declare function enableSystemMedia(): void; +export declare function enableSystemMedia(): void /** * 初始化插件 @@ -70,20 +69,20 @@ export declare function enableSystemMedia(): void; * * 如果其他 API 调用失败,则只会打印日志并静默失败 */ -export declare function initialize(logDir: string): void; +export declare function initialize(logDir: string): void export interface MetadataParam { - songName: string; - authorName: string; - albumName: string; + songName: string + authorName: string + albumName: string /** 封面的原始字节数据,适用于除 Discord RPC 之外的其他平台 */ - coverData?: Buffer; + coverData?: Buffer /** * 封面的 HTTP URL,更新 Discord RPC 时必传,其他平台可不传 * * Linux 平台在没有提供 `cover_data` 时会使用它 */ - originalCoverUrl?: string; + originalCoverUrl?: string /** * 网易云音乐中对应的曲目 ID * @@ -92,25 +91,26 @@ export interface MetadataParam { * - 生成 Discord RPC 的按钮链接 * - MacOS 和 Linux 会使用此值来填充唯一的曲目 ID */ - ncmId?: number; + ncmId?: number /** * 当前歌曲时长,单位是毫秒 * * 用于 Linux、MacOS、Discord RPC 的元数据更新。Windows 使用 [`TimelinePayload`] 的 * `total_time` 字段。 */ - duration?: number; + duration?: number } -export type PlaybackStatus = "Playing" | "Paused"; +export type PlaybackStatus = 'Playing'| +'Paused'; export interface PlayModePayload { - isShuffling: boolean; - repeatMode: RepeatMode; + isShuffling: boolean + repeatMode: RepeatMode } export interface PlayStatePayload { - status: PlaybackStatus; + status: PlaybackStatus } /** @@ -124,34 +124,35 @@ export interface PlayStatePayload { * * 如果 N-API 创建线程安全函数失败,会抛出错误。通常不应该发生,除非 JS 环境已经销毁了 */ -export declare function registerEventHandler(callback: (arg: SystemMediaEvent) => void): void; +export declare function registerEventHandler(callback: (arg: SystemMediaEvent) => void): void -export type RepeatMode = "None" | "Track" | "List"; +export type RepeatMode = 'None'| +'Track'| +'List'; /** 关闭插件,清理资源 */ -export declare function shutdown(): void; +export declare function shutdown(): void export interface SystemMediaEvent { - type: SystemMediaEventType; - positionMs?: number; + type: SystemMediaEventType + positionMs?: number } -export type SystemMediaEventType = - | "Play" - | "Pause" - | "Stop" - | "NextSong" - | "PreviousSong" - | "ToggleShuffle" - | "ToggleRepeat" - /** 绝对位置,毫秒 */ - | "Seek"; +export type SystemMediaEventType = 'Play'| +'Pause'| +'Stop'| +'NextSong'| +'PreviousSong'| +'ToggleShuffle'| +'ToggleRepeat'| +/** 绝对位置,毫秒 */ +'Seek'; export interface TimelinePayload { /** 单位是毫秒 */ - currentTime: number; + currentTime: number /** 单位是毫秒 */ - totalTime: number; + totalTime: number } /** @@ -162,7 +163,7 @@ export interface TimelinePayload { * * `payload` - 配置信息,可以配置是否在暂停后也显示 Discord Activity 和 状态显示风格。详情请查看 * [`DiscordConfigPayload`] */ -export declare function updateDiscordConfig(payload: DiscordConfigPayload): void; +export declare function updateDiscordConfig(payload: DiscordConfigPayload): void /** * 更新歌曲元数据 @@ -173,7 +174,7 @@ export declare function updateDiscordConfig(payload: DiscordConfigPayload): void * * 更新 Discord RPC 的元数据时,必须提供 `original_cover_url` */ -export declare function updateMetadata(payload: MetadataParam): void; +export declare function updateMetadata(payload: MetadataParam): void /** * 更新播放模式 @@ -182,14 +183,14 @@ export declare function updateMetadata(payload: MetadataParam): void; * * 只会更新媒体控件的信息,不会更新 Discord RPC 上的信息 */ -export declare function updatePlayMode(payload: PlayModePayload): void; +export declare function updatePlayMode(payload: PlayModePayload): void /** * 更新播放状态 (播放/暂停) * * 同时也会更新 Discord 的播放状态 (如果启用了 Discord RPC) */ -export declare function updatePlayState(payload: PlayStatePayload): void; +export declare function updatePlayState(payload: PlayStatePayload): void /** * 更新进度信息 @@ -200,4 +201,4 @@ export declare function updatePlayState(payload: PlayStatePayload): void; * * Discord RPC 实现的进度更新有节流,调用此函数无需担心 Discord RPC 的速率限制 */ -export declare function updateTimeline(payload: TimelinePayload): void; +export declare function updateTimeline(payload: TimelinePayload): void diff --git a/src/components/Card/SongListCard.vue b/src/components/Card/SongListCard.vue index cedff1a32..e97870f71 100644 --- a/src/components/Card/SongListCard.vue +++ b/src/components/Card/SongListCard.vue @@ -35,7 +35,7 @@ -
+
{{ title }} @@ -127,6 +127,10 @@ const songList = computed(() => sampleSize(props.data, 3)); display: flex; flex-direction: column; justify-content: space-evenly; + &.center { + align-items: center; + text-align: center; + } .name { font-size: 18px; font-weight: bold; diff --git a/src/components/Modal/ThemeConfig.vue b/src/components/Modal/ThemeConfig.vue index 530d7c20b..75f618584 100644 --- a/src/components/Modal/ThemeConfig.vue +++ b/src/components/Modal/ThemeConfig.vue @@ -83,7 +83,15 @@ class="color-section" :class="{ disabled: settingStore.themeFollowCover }" > - 选择主题色 +
+ 选择主题色 + + + 全部随机 + +
{ settingStore.themeColorType = key; }; +// 随机主题 +const randomizeTheme = () => { + settingStore.themeFollowCover = false; + // 随机全局着色 (50% 概率) + settingStore.themeGlobalColor = Math.random() > 0.5; + // 随机变体 + const randomVariant = variantOptions[Math.floor(Math.random() * variantOptions.length)]; + settingStore.themeVariant = randomVariant.value as typeof settingStore.themeVariant; + // 随机颜色 (生成随机 Hex 并应用到自定义) + const randomHex = `#${Math.floor(Math.random() * 16777215) + .toString(16) + .padStart(6, "0")}`; + settingStore.themeCustomColor = randomHex; + settingStore.themeColorType = "custom"; + themeGlobalColorChange(true); +}; + // 全局着色更改 const themeGlobalColorChange = (val: boolean) => { if (val) getCoverColor(musicStore.songCover); @@ -420,6 +445,15 @@ const clearBackgroundImage = async () => { opacity: 0.4; pointer-events: none; } + .section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + .section-title { + margin-bottom: 0; + } + } .section-title { display: block; font-size: 13px; diff --git a/src/components/Player/PlayerMeta/PlayerData.vue b/src/components/Player/PlayerMeta/PlayerData.vue index 6545ff078..5ef24585b 100644 --- a/src/components/Player/PlayerMeta/PlayerData.vue +++ b/src/components/Player/PlayerMeta/PlayerData.vue @@ -67,7 +67,18 @@ {{ lyricMode }} - + + + {{ audioSourceText }} + + + {{ audioSourceText }} @@ -142,6 +153,7 @@ import { debounce, isObject } from "lodash-es"; import { removeBrackets } from "@/utils/format"; import { SongUnlockServer } from "@/core/player/SongManager"; import { useLyricManager } from "@/core/player/LyricManager"; +import { usePlayerController } from "@/core/player/PlayerController"; const props = defineProps<{ /** 数据居中 */ @@ -155,6 +167,7 @@ const musicStore = useMusicStore(); const statusStore = useStatusStore(); const settingStore = useSettingStore(); const lyricManager = useLyricManager(); +const player = usePlayerController(); // 当前歌词模式 const lyricMode = computed(() => { @@ -204,13 +217,24 @@ const sourceMap: Record = { [SongUnlockServer.GEQUBAO]: "Gequbao", }; +const audioSourceOptions = computed(() => { + return statusStore.availableAudioSources.map((source) => ({ + label: sourceMap[source] || source.toUpperCase(), + value: source, + })); +}); + +const handleAudioSourceChange = (val: string) => { + player.switchAudioSource(val); +}; + const audioSourceText = computed(() => { if (musicStore.playSong.path) return "LOCAL"; if (musicStore.playSong.type === "streaming") return "STREAMING"; if (statusStore.audioSource) { return sourceMap[statusStore.audioSource] || statusStore.audioSource.toUpperCase(); } - return "ONLINE"; + return "Netease"; }); const jumpPage = debounce( diff --git a/src/components/Setting/config/general.ts b/src/components/Setting/config/general.ts index fe0920abb..4e6050108 100644 --- a/src/components/Setting/config/general.ts +++ b/src/components/Setting/config/general.ts @@ -1,9 +1,7 @@ import { useDataStore, useMusicStore, useSettingStore } from "@/stores"; import { usePlayerController } from "@/core/player/PlayerController"; import { isElectron } from "@/utils/env"; -import { - openExcludeComment, -} from "@/utils/modal"; +import { openExcludeComment } from "@/utils/modal"; import { sendRegisterProtocol } from "@/utils/protocol"; import { SettingConfig } from "@/types/settings"; import { ref, computed, h } from "vue"; @@ -95,7 +93,7 @@ export const useGeneralSettings = (): SettingConfig => { window.$message.error(errorMsg); } } - } catch (error) { + } catch { window.$message.error("设置导出出错"); } }; @@ -108,7 +106,10 @@ export const useGeneralSettings = (): SettingConfig => { h( NAlert, { type: "warning", showIcon: true, style: { marginBottom: "12px" } }, - { default: () => "导入设置将覆盖当前所有配置(包括主题、快捷键、音效设置等)并重启软件。" }, + { + default: () => + "导入设置将覆盖当前所有配置(包括主题、快捷键、音效设置等)并重启软件。", + }, ), h("div", null, "是否继续?"), ]), @@ -127,26 +128,26 @@ export const useGeneralSettings = (): SettingConfig => { // "status-store", // "music-store", ]; - - storesToRestore.forEach(key => { + + storesToRestore.forEach((key) => { if (data.renderer[key]) { localStorage.setItem(key, data.renderer[key]); restoredCount++; } }); } - + if (restoredCount > 0 || data.electron) { - window.$message.success("设置导入成功,即将重启"); - setTimeout(() => { - window.location.reload(); - }, 1000); + window.$message.success("设置导入成功,即将重启"); + setTimeout(() => { + window.location.reload(); + }, 1000); } else { - window.$message.warning("未找到可恢复的设置数据"); + window.$message.warning("未找到可恢复的设置数据"); } } else { if (result?.error !== "cancelled") { - window.$message.error("设置导入失败: " + (result?.error || "未知错误")); + window.$message.error("设置导入失败: " + (result?.error || "未知错误")); } } } catch (error) { diff --git a/src/components/Setting/config/local.ts b/src/components/Setting/config/local.ts index 7c2aeefb2..a4c5efdc2 100644 --- a/src/components/Setting/config/local.ts +++ b/src/components/Setting/config/local.ts @@ -442,6 +442,17 @@ export const useLocalSettings = (): SettingConfig => { set: (v) => (settingStore.downloadMakeYrc = v), }), }, + { + key: "downloadSaveAsAss", + label: "下载时另存为 ASS 文件", + type: "switch", + description: "生成 ASS 字幕文件以支持第三方播放器识别(源文件仍内嵌 LRC)", + disabled: computed(() => !settingStore.downloadMeta || !settingStore.downloadLyric), + value: computed({ + get: () => settingStore.downloadSaveAsAss, + set: (v) => (settingStore.downloadSaveAsAss = v), + }), + }, { key: "downloadLyricToTraditional", label: "下载歌词转繁体", diff --git a/src/components/Setting/config/network.ts b/src/components/Setting/config/network.ts index 75908946b..d7dc60f33 100644 --- a/src/components/Setting/config/network.ts +++ b/src/components/Setting/config/network.ts @@ -170,7 +170,7 @@ export const useNetworkSettings = (): SettingConfig => { window.$message.success(`已成功连接到 Last.fm 账号: ${sessionResponse.session.name}`); lastfmAuthLoading.value = false; } - } catch (error) { + } catch { // 用户还未授权,继续等待 } }, 2000); diff --git a/src/core/player/LyricManager.ts b/src/core/player/LyricManager.ts index 3dd1d295e..f5e863ed4 100644 --- a/src/core/player/LyricManager.ts +++ b/src/core/player/LyricManager.ts @@ -70,9 +70,9 @@ class LyricManager { public async switchLyricSource(source: string) { const statusStore = useStatusStore(); const musicStore = useMusicStore(); - + if (statusStore.preferredLyricSource === source) return; - + statusStore.preferredLyricSource = source; // 保存并强制重新加载歌词 if (musicStore.playSong) { @@ -415,7 +415,11 @@ class LyricManager { if (neteaseData?.lrc?.lyric) { lrcLines = parseLrc(neteaseData.lrc.lyric) || []; if (neteaseData?.tlyric?.lyric) - lrcLines = this.alignLyrics(lrcLines, parseLrc(neteaseData.tlyric.lyric), "translatedLyric"); + lrcLines = this.alignLyrics( + lrcLines, + parseLrc(neteaseData.tlyric.lyric), + "translatedLyric", + ); if (neteaseData?.romalrc?.lyric) lrcLines = this.alignLyrics(lrcLines, parseLrc(neteaseData.romalrc.lyric), "romanLyric"); } @@ -424,7 +428,11 @@ class LyricManager { if (neteaseData?.yrc?.lyric) { yrcLines = parseYrc(neteaseData.yrc.lyric) || []; if (neteaseData?.ytlrc?.lyric) - yrcLines = this.alignLyrics(yrcLines, parseLrc(neteaseData.ytlrc.lyric), "translatedLyric"); + yrcLines = this.alignLyrics( + yrcLines, + parseLrc(neteaseData.ytlrc.lyric), + "translatedLyric", + ); if (neteaseData?.yromalrc?.lyric) yrcLines = this.alignLyrics(yrcLines, parseLrc(neteaseData.yromalrc.lyric), "romanLyric"); } @@ -444,7 +452,7 @@ class LyricManager { // 3. 选择源 let selected = statusStore.preferredLyricSource; - + // 如果没有偏好或偏好不可用,使用默认优先级 if (!selected || !candidates[selected]) { // 默认优先级: TTML > QM > YRC > LRC @@ -456,14 +464,14 @@ class LyricManager { // 4. 应用结果 const finalData = (selected && candidates[selected]) || { lrcData: [], yrcData: [] }; - + statusStore.usingTTMLLyric = selected === "TTML"; statusStore.usingQRCLyric = selected === "QM"; // 排除过滤 & 简繁转换 let processedData = this.handleLyricExclude(finalData); processedData = await this.applyChineseVariant(processedData); - + // 设置最终歌词 this.setFinalLyric(processedData, req); @@ -885,23 +893,21 @@ class LyricManager { if (!str) return str; // If the entire string is enclosed in brackets (e.g. "(Chorus)"), remove them if not in enclosure mode - if (!isEnclosure && /^\s*[\((][^()()]*[\))]\s*$/.test(str)) { + if (!isEnclosure && /^\s*[((][^()()]*[))]\s*$/.test(str)) { return str - .replace(/^\s*[\((]/, "") - .replace(/[\))]\s*$/, "") + .replace(/^\s*[((]/, "") + .replace(/[))]\s*$/, "") .trim(); } - let res = str.replace(/[\((]/g, startStr); + let res = str.replace(/[((]/g, startStr); if (isEnclosure) { - res = res.replace(/[\))]/g, endStr); + res = res.replace(/[))]/g, endStr); } else { // Separator mode: // 1. Remove ) if it's at the end of the string (effectively just a closing marker) // 2. Otherwise replace ) with endStr (usually space) - res = res - .replace(/[\))](?=\s*$)/g, "") - .replace(/[\))]/g, endStr); + res = res.replace(/[))](?=\s*$)/g, "").replace(/[))]/g, endStr); // Cleanup double dashes if the separator contains a dash if (startStr.includes("-")) { @@ -917,15 +923,15 @@ class LyricManager { // If the whole line is in brackets and we are NOT in enclosure mode (e.g. dash mode), // we likely want to strip the brackets entirely instead of replacing them with dashes. const fullText = line.words.map((w) => w.word).join(""); - const isFullBracket = /^\s*[\((][^()()]*[\))]\s*$/.test(fullText); + const isFullBracket = /^\s*[((][^()()]*[))]\s*$/.test(fullText); if (isFullBracket && !isEnclosure) { // Remove the first opening bracket found in the words let foundStart = false; for (const word of line.words) { if (foundStart) break; - if (/[\((]/.test(word.word)) { - word.word = word.word.replace(/[\((]/, ""); + if (/[((]/.test(word.word)) { + word.word = word.word.replace(/[((]/, ""); foundStart = true; } } @@ -934,12 +940,11 @@ class LyricManager { for (let i = line.words.length - 1; i >= 0; i--) { if (foundEnd) break; const word = line.words[i]; - if (/[\))]/.test(word.word)) { + if (/[))]/.test(word.word)) { // Find the last occurrence of ) or ) const lastIndex = Math.max(word.word.lastIndexOf(")"), word.word.lastIndexOf(")")); if (lastIndex !== -1) { - word.word = - word.word.substring(0, lastIndex) + word.word.substring(lastIndex + 1); + word.word = word.word.substring(0, lastIndex) + word.word.substring(lastIndex + 1); foundEnd = true; } } @@ -948,14 +953,14 @@ class LyricManager { // Normal replacement logic line.words.forEach((word, index) => { // Replace opening brackets - word.word = word.word.replace(/[\((]/g, startStr); + word.word = word.word.replace(/[((]/g, startStr); if (isEnclosure) { // Enclosure mode: simply replace closing brackets with endStr - word.word = word.word.replace(/[\))]/g, endStr); + word.word = word.word.replace(/[))]/g, endStr); } else { // Separator mode: logic to handle closing brackets nicely - word.word = word.word.replace(/[\))]/g, (_, offset, string) => { + word.word = word.word.replace(/[))]/g, (_, offset, string) => { const isAtEnd = offset === string.length - 1; // If ) is at the end of the word... if (isAtEnd) { @@ -1159,15 +1164,15 @@ class LyricManager { public async handleLyric(song: SongType) { const settingStore = useSettingStore(); const statusStore = useStatusStore(); - + // 标记当前歌词请求(避免旧请求覆盖新请求) const req = ++this.lyricReqSeq; this.activeLyricReq = req; - + // 加载歌词源偏好 const pref = await this.getLyricPreference(song.id); statusStore.preferredLyricSource = pref; - + const isStreaming = song?.type === "streaming"; try { let lyricData: SongLyric = { lrcData: [], yrcData: [] }; diff --git a/src/core/player/PlayerController.ts b/src/core/player/PlayerController.ts index 6bd7c6830..33599b36d 100644 --- a/src/core/player/PlayerController.ts +++ b/src/core/player/PlayerController.ts @@ -99,14 +99,17 @@ class PlayerController { // 简单的防削波保护 (如果有峰值信息) // 目标: gain * peak <= 1.0 - const peak = settingStore.replayGainMode === "album" ? (albumPeak ?? trackPeak) : (trackPeak ?? albumPeak); + const peak = + settingStore.replayGainMode === "album" ? (albumPeak ?? trackPeak) : (trackPeak ?? albumPeak); if (peak && peak > 0) { if (targetGain * peak > 1.0) { targetGain = 1.0 / peak; } } - console.log(`🔊 [ReplayGain] Applied: ${targetGain.toFixed(4)} (Mode: ${settingStore.replayGainMode})`); + console.log( + `🔊 [ReplayGain] Applied: ${targetGain.toFixed(4)} (Mode: ${settingStore.replayGainMode})`, + ); audioManager.setReplayGain(targetGain); } @@ -148,25 +151,25 @@ class PlayerController { } try { - // 停止当前播放 + statusStore.playLoading = true; + const audioSource = await songManager.getAudioSource(playSongData); + // 检查请求是否过期 + if (requestToken !== this.currentRequestToken) { + console.log(`🚫 [${playSongData.id}] 请求已过期,舍弃`); + return; + } + if (!audioSource.url) throw new Error("AUDIO_SOURCE_EMPTY"); audioManager.stop(); musicStore.playSong = playSongData; - statusStore.currentTime = options.seek ?? 0; - const duration = this.getDuration() || statusStore.duration; - if (duration > 0) { - statusStore.progress = calculateProgress(statusStore.currentTime, duration); - } else { - statusStore.progress = 0; - } + // 重置进度 + statusStore.progress = 0; statusStore.lyricIndex = -1; // 重置重试计数 const sid = playSongData.type === "radio" ? playSongData.dj?.id : playSongData.id; if (this.retryInfo.songId !== sid) { this.retryInfo = { songId: sid || 0, count: 0 }; } - // 设置加载状态 - statusStore.playLoading = true; statusStore.lyricLoading = true; // 重置 AB 循环 statusStore.abLoop.enable = false; @@ -178,15 +181,8 @@ class PlayerController { lyricLoading: true, }); } - // 获取歌词 + // 获取歌词 (在切换后获取,避免旧歌配新词) lyricManager.handleLyric(playSongData); - // 获取音频 - const audioSource = await songManager.getAudioSource(playSongData); - if (requestToken !== this.currentRequestToken) { - console.log(`🚫 [${playSongData.id}] 请求已过期,舍弃`); - return; - } - if (!audioSource.url) throw new Error("AUDIO_SOURCE_EMPTY"); console.log(`🎧 [${playSongData.id}] 最终播放信息:`, audioSource); // 更新音质和解锁状态 statusStore.songQuality = audioSource.quality; @@ -248,6 +244,54 @@ class PlayerController { } } + /** + * 切换音频源 + * @param source 音频源标识 + */ + public async switchAudioSource(source: string) { + const dataStore = useDataStore(); + const statusStore = useStatusStore(); + const songManager = useSongManager(); + const musicStore = useMusicStore(); + const audioManager = useAudioManager(); + const playSongData = musicStore.playSong; + if (!playSongData || playSongData.path) return; + try { + statusStore.playLoading = true; + // 保存偏好 + await dataStore.setAudioSourcePreference(playSongData.id, source); + statusStore.preferredAudioSource = source; + // 清除预取缓存 + songManager.clearPrefetch(); + // 获取新音频源 + const audioSource = await songManager.getAudioSourceFromSpecificServer(playSongData, source); + + if (!audioSource.url) { + window.$message.error("切换音频源失败:无法获取播放链接"); + statusStore.playLoading = false; + return; + } + + console.log(`🔄 [${playSongData.id}] 切换音频源:`, audioSource); + + // 更新状态 + statusStore.songQuality = audioSource.quality; + statusStore.playUblock = audioSource.isUnlocked ?? false; + statusStore.audioSource = audioSource.source; + + // 保持当前进度和播放状态 + const seek = statusStore.currentTime; + const shouldAutoPlay = statusStore.playStatus; + // 停止当前播放 + audioManager.stop(); + await this.loadAndPlay(audioSource.url, shouldAutoPlay, seek); + } catch (error) { + console.error("❌ 切换音频源失败:", error); + statusStore.playLoading = false; + window.$message.error("切换音频源失败"); + } + } + /** * 加载音频流并播放 */ diff --git a/src/core/player/SongManager.ts b/src/core/player/SongManager.ts index 4db4a707a..45d8524ca 100644 --- a/src/core/player/SongManager.ts +++ b/src/core/player/SongManager.ts @@ -179,28 +179,22 @@ class SongManager { }; /** - * 获取解锁播放链接 - * @param songData 歌曲数据 - * @returns + * 获取所有可用解锁源 */ - public getUnlockSongUrl = async (song: SongType): Promise => { + public getAvailableUnlockSources = async (song: SongType): Promise => { const settingStore = useSettingStore(); const songId = song.id; - // 优先检查本地缓存 - const cachedUrl = await this.checkLocalCache(songId); - if (cachedUrl) { - return { id: songId, url: cachedUrl, isUnlocked: true }; - } + const artist = Array.isArray(song.artists) ? song.artists[0].name : song.artists; const keyWord = song.name + "-" + artist; if (!songId || !keyWord) { - return { id: songId, url: undefined }; + return []; } // 获取音源列表 const servers = settingStore.songUnlockServer.filter((s) => s.enabled).map((s) => s.key); if (servers.length === 0) { - return { id: songId, url: undefined }; + return []; } // 并发执行 @@ -214,28 +208,41 @@ class SongManager { ), ); - // 按顺序找成功项 + const sources: AudioSource[] = []; for (const r of results) { if (r.status === "fulfilled" && r.value.success) { const unlockUrl = r.value?.result?.url; - // 解锁成功后,触发下载 - this.triggerCacheDownload(songId, unlockUrl); // 推断音质 let quality = QualityType.HQ; if (unlockUrl && (unlockUrl.includes(".flac") || unlockUrl.includes(".wav"))) { quality = QualityType.SQ; } - console.log(`最终音质判断:详细输出:`, { unlockUrl, quality }); - return { + sources.push({ id: songId, url: unlockUrl, isUnlocked: true, quality, source: r.value.server, - }; + }); } } - return { id: songId, url: undefined }; + return sources; + }; + + /** + * 获取解锁播放链接 + * @param songData 歌曲数据 + * @returns + */ + public getUnlockSongUrl = async (song: SongType): Promise => { + const sources = await this.getAvailableUnlockSources(song); + if (sources.length > 0) { + const s = sources[0]; + // 解锁成功后,触发下载 + if (s.url) this.triggerCacheDownload(s.id, s.url); + return s; + } + return { id: song.id, url: undefined }; }; /** @@ -321,6 +328,84 @@ class SongManager { console.log("🧹 已清除歌曲 URL 缓存"); } + /** + * 获取指定音频源的链接 + * @param song 歌曲 + * @param source 目标音频源标识 + */ + public getAudioSourceFromSpecificServer = async ( + song: SongType, + source: string, + ): Promise => { + const songId = song.type === "radio" ? song.dj?.id : song.id; + if (!songId) return { id: 0, url: undefined, quality: undefined, isUnlocked: false }; + + try { + // 1. 官方源 (netease) + if (source === "netease") { + const { url, isTrial, quality } = await this.getOnlineUrl(songId, !!song.pc); + return { + id: songId, + url, + isTrial, + quality, + source: "netease", + isUnlocked: false, + }; + } + + // 2. 解锁源 (其他) + const settingStore = useSettingStore(); + const canUnlock = isElectron && song.type !== "radio" && settingStore.useSongUnlock; + + if (canUnlock) { + // 构建关键词 + const artist = Array.isArray(song.artists) ? song.artists[0].name : song.artists; + const keyWord = song.name + "-" + artist; + + // 请求特定解锁源 + const result = await unlockSongUrl(songId, keyWord, source as SongUnlockServer); + + if (result.code === 200 && result.url) { + const unlockUrl = result.url; + // 推断音质 + let quality = QualityType.HQ; + if (unlockUrl && (unlockUrl.includes(".flac") || unlockUrl.includes(".wav"))) { + quality = QualityType.SQ; + } + + // 检查本地缓存 + const cachedUrl = await this.checkLocalCache(songId, quality); + if (cachedUrl) { + return { + id: songId, + url: cachedUrl, + isUnlocked: true, + quality, + source: source, + }; + } + + // 触发缓存下载 + this.triggerCacheDownload(songId, unlockUrl, quality); + + return { + id: songId, + url: unlockUrl, + isUnlocked: true, + quality, + source: source, + }; + } + } + + return { id: songId, url: undefined, quality: undefined, isUnlocked: false, source }; + } catch (e) { + console.error(`❌ 获取特定音频源失败 [${source}]:`, e); + return { id: songId, url: undefined, quality: undefined, isUnlocked: false, source }; + } + }; + /** * 获取音频源 * 始终从此方法获取对应歌曲播放信息 @@ -329,6 +414,7 @@ class SongManager { */ public getAudioSource = async (song: SongType): Promise => { const settingStore = useSettingStore(); + const statusStore = useStatusStore(); // 本地文件直接返回 if (song.path && song.type !== "streaming") { @@ -360,34 +446,88 @@ class SongManager { const songId = song.type === "radio" ? song.dj?.id : song.id; if (!songId) return { id: 0, url: undefined, quality: undefined, isUnlocked: false }; - // 检查缓存并返回 + // 获取偏好 + const dataStore = useDataStore(); + const pref = await dataStore.getAudioSourcePreference(songId); + statusStore.preferredAudioSource = pref; + + // 检查缓存并返回 (如果偏好匹配) if (this.nextPrefetch && this.nextPrefetch.id === songId && settingStore.useNextPrefetch) { - console.log(`🚀 [${songId}] 使用预加载缓存播放`); - const cachedSource = this.nextPrefetch; - this.nextPrefetch = undefined; - return cachedSource; + if (!pref || this.nextPrefetch.source === pref) { + console.log(`🚀 [${songId}] 使用预加载缓存播放`); + const cachedSource = this.nextPrefetch; + this.nextPrefetch = undefined; + return cachedSource; + } } // 在线获取 try { // 是否可解锁 const canUnlock = isElectron && song.type !== "radio" && settingStore.useSongUnlock; - // 尝试获取官方链接 - const { url: officialUrl, isTrial, quality } = await this.getOnlineUrl(songId, !!song.pc); - // 如果官方链接有效且非试听(或者用户接受试听) - if (officialUrl && (!isTrial || (isTrial && settingStore.playSongDemo))) { - if (isTrial) window.$message.warning("当前歌曲仅可试听"); - return { id: songId, url: officialUrl, quality, isUnlocked: false, source: "netease" }; + + // 并行获取官方和解锁源 + const officialPromise = this.getOnlineUrl(songId, !!song.pc); + const unlockPromise = canUnlock ? this.getAvailableUnlockSources(song) : Promise.resolve([]); + + const [officialRes, unlockSources] = await Promise.all([officialPromise, unlockPromise]); + + // 构建候选列表 + const candidates: AudioSource[] = []; + // 官方源 + if ( + officialRes.url && + (!officialRes.isTrial || (officialRes.isTrial && settingStore.playSongDemo)) + ) { + candidates.push({ ...officialRes, source: "netease" }); } - // 尝试解锁 - if (canUnlock) { - const unlockUrl = await this.getUnlockSongUrl(song); - if (unlockUrl.url) { - console.log(`🔓 [${songId}] 解锁成功`, unlockUrl); - return unlockUrl; + // 解锁源 + // candidates.push(...unlockSources); + // 解锁源去重添加 + for (const s of unlockSources) { + if (!candidates.some((c) => c.source === s.source)) { + candidates.push(s); } } - // 最后的兜底:检查本地是否有缓存(不区分音质) + + // 更新可用源列表 + statusStore.availableAudioSources = candidates.map((s) => s.source || "unknown"); + + // 选择源 + let selected: AudioSource | undefined; + + // 1. 尝试使用偏好源 + if (pref) { + selected = candidates.find((s) => s.source === pref); + } + + // 2. 如果没有偏好或偏好不可用,使用默认策略 + if (!selected) { + // 优先官方 + selected = candidates.find((s) => s.source === "netease"); + // 其次解锁源 + if (!selected && candidates.length > 0) { + selected = candidates[0]; + } + } + + if (selected) { + statusStore.audioSource = selected.source; + // 如果是解锁源,触发缓存下载 (getOnlineUrl 已内部处理官方源缓存) + if (selected.isUnlocked && selected.url) { + // 检查本地缓存是否已存在 + const cachedUrl = await this.checkLocalCache(songId, selected.quality); + if (cachedUrl) { + console.log(`🚀 [${songId}] 使用本地缓存 (Source: ${selected.source})`); + return { ...selected, url: cachedUrl }; + } + // 未找到缓存,触发下载并使用远程 URL + this.triggerCacheDownload(songId, selected.url, selected.quality); + } + return selected; + } + + // 3. 最后的兜底:检查本地是否有缓存(不区分音质) const fallbackUrl = await this.checkLocalCache(songId); if (fallbackUrl) { console.log(`🚀 [${songId}] 网络请求失败,使用本地缓存兜底`, fallbackUrl); diff --git a/src/core/resource/DownloadManager.ts b/src/core/resource/DownloadManager.ts index 0b1a5bccc..de591af60 100644 --- a/src/core/resource/DownloadManager.ts +++ b/src/core/resource/DownloadManager.ts @@ -9,7 +9,9 @@ import { qqMusicMatch } from "@/api/qqmusic"; import { songLevelData } from "@/utils/meta"; import { getPlayerInfoObj } from "@/utils/format"; import { getConverter, type ConverterMode } from "@/utils/opencc"; -import { lyricLinesToTTML, parseQRCLyric } from "@/utils/lyricParser"; +import { lyricLinesToTTML, parseQRCLyric, parseSmartLrc } from "@/utils/lyricParser"; +import { generateASS } from "@/utils/assGenerator"; +import { parseTTML, parseYrc, type LyricLine } from "@applemusic-like-lyrics/lyric"; interface DownloadTask { song: SongType; @@ -374,8 +376,14 @@ class DownloadManager { } if (isElectron) { - const { downloadMeta, downloadCover, downloadLyric, saveMetaFile, downloadMakeYrc } = - settingStore; + const { + downloadMeta, + downloadCover, + downloadLyric, + saveMetaFile, + downloadMakeYrc, + downloadSaveAsAss, + } = settingStore; let lyric = ""; let yrcLyric = ""; let ttmlLyric = ""; @@ -385,7 +393,7 @@ class DownloadManager { lyric = await this.processLyric(lyricResult); // 获取逐字歌词内容用于另存 - if (downloadMakeYrc) { + if (downloadMakeYrc || downloadSaveAsAss) { console.log(`[Download] Fetching verbatim lyrics for ${song.name} (${song.id})...`); try { const ttmlRes = await songLyricTTML(song.id); @@ -495,6 +503,60 @@ class DownloadManager { } } + if (result.status !== "cancelled" && result.status !== "error" && downloadSaveAsAss) { + try { + let lines: LyricLine[] = []; + // Try TTML + if (ttmlLyric) { + const parsed = parseTTML(ttmlLyric); + if (parsed?.lines) lines = parsed.lines; + } + // Try YRC (QRC) + else if (yrcLyric) { + // yrcLyric might be QRC XML + if (yrcLyric.trim().startsWith("<") || yrcLyric.includes("")) { + lines = parseQRCLyric(yrcLyric); + } else { + lines = parseYrc(yrcLyric) || []; + } + } + // Fallback to LRC (embedded lyric) + else if (lyric) { + const parsed = parseSmartLrc(lyric); + if (parsed?.lines) lines = parsed.lines; + } + + if (lines.length > 0) { + let assContent = generateASS(lines, { + title: song.name, + artist: rawArtist, + }); + + // 繁体转换 + assContent = await this._convertToTraditionalIfNeeded(assContent); + + const fileName = `${safeFileName}.ass`; + const encoding = settingStore.downloadLyricEncoding || "utf-8"; + + console.log(`[Download] Saving ASS file: ${fileName}`); + const saveRes = await window.electron.ipcRenderer.invoke("save-file-content", { + path: targetPath, + fileName, + content: assContent, + encoding, + }); + + if (saveRes.success) { + console.log(`[Download] Saved ASS file successfully: ${fileName}`); + } else { + console.error(`[Download] Failed to save ASS file: ${saveRes.message}`); + } + } + } catch (e) { + console.error("[Download] Failed to save ASS file exception", e); + } + } + if (result.status === "skipped") { return { success: true, skipped: true, message: result.message }; } diff --git a/src/stores/data.ts b/src/stores/data.ts index acae92b15..1fe0f55b5 100644 --- a/src/stores/data.ts +++ b/src/stores/data.ts @@ -51,6 +51,8 @@ interface ListState { /** 总大小 */ totalSize: string; }>; + /** 音频源偏好 memory cache */ + audioSourcePreference: Record; } type UserDataKeys = keyof ListState["userLikeData"]; @@ -76,6 +78,13 @@ const backgroundDB = localforage.createInstance({ storeName: "background", }); +// audioPrefDB +const audioPrefDB = localforage.createInstance({ + name: "music-data", + description: "Audio source preferences", + storeName: "audio_preferences", +}); + export const useDataStore = defineStore("data", { state: (): ListState => ({ // 播放列表 @@ -130,6 +139,8 @@ export const useDataStore = defineStore("data", { }, // 正在下载的歌曲列表 downloadingSongs: [], + // 音频源偏好 + audioSourcePreference: {}, }), getters: { // 是否为喜欢歌曲 @@ -530,6 +541,46 @@ export const useDataStore = defineStore("data", { throw error; } }, + /** + * 获取音频源偏好 + * @param id 歌曲ID + * @returns 音频源标识 + */ + async getAudioSourcePreference(id: number | string): Promise { + // 优先从内存读取 + const key = String(id); + if (this.audioSourcePreference[key]) { + return this.audioSourcePreference[key]; + } + // 从 DB 读取并缓存到内存 + try { + const val = await audioPrefDB.getItem(key); + if (val) { + this.audioSourcePreference[key] = val; + return val; + } + return null; + } catch (error) { + console.error(`Error getting audio preference for ${id}:`, error); + return null; + } + }, + /** + * 保存音频源偏好 + * @param id 歌曲ID + * @param source 音频源标识 + */ + async setAudioSourcePreference(id: number | string, source: string): Promise { + const key = String(id); + // 更新内存 + this.audioSourcePreference[key] = source; + // 更新 DB + try { + await audioPrefDB.setItem(key, source); + } catch (error) { + console.error(`Error setting audio preference for ${id}:`, error); + } + }, }, // 持久化 persist: { diff --git a/src/stores/migrations/settingMigrations.ts b/src/stores/migrations/settingMigrations.ts index 36bcf67cc..bfc0d1117 100644 --- a/src/stores/migrations/settingMigrations.ts +++ b/src/stores/migrations/settingMigrations.ts @@ -6,7 +6,7 @@ import type { SettingState } from "../setting"; /** * 当前设置 Schema 版本号 */ -export const CURRENT_SETTING_SCHEMA_VERSION = 9; +export const CURRENT_SETTING_SCHEMA_VERSION = 8; /** * 迁移函数类型 @@ -167,40 +167,4 @@ export const settingMigrations: Record = { excludeLyricsUserRegexes: oldState.excludeUserRegexes, }; }, - 9: () => { - return { - showSongAlbum: true, - showSongDuration: true, - showSongOperations: true, - showSongArtist: true, - fullscreenPlayerElements: { - like: true, - addToPlaylist: true, - download: true, - comments: true, - desktopLyric: true, - moreSettings: true, - copyLyric: true, - lyricOffset: true, - lyricSettings: true, - }, - contextMenuOptions: { - play: true, - playNext: true, - addToPlaylist: true, - mv: true, - dislike: true, - more: true, - cloudImport: true, - deleteFromPlaylist: true, - deleteFromCloud: true, - deleteFromLocal: true, - openFolder: true, - cloudMatch: true, - wiki: true, - search: true, - download: true, - }, - }; - }, }; diff --git a/src/stores/setting.ts b/src/stores/setting.ts index 4d9879d1f..a37606a79 100644 --- a/src/stores/setting.ts +++ b/src/stores/setting.ts @@ -109,6 +109,8 @@ export interface SettingState { useUnlockForDownload: boolean; /** 内嵌暂逐字歌词 (beta) */ downloadMakeYrc: boolean; + /** 下载后另存为 ASS 格式 */ + downloadSaveAsAss: boolean; /** 下载歌词转繁体 */ downloadLyricToTraditional: boolean; /** 下载歌词文件编码 */ @@ -571,6 +573,7 @@ export const useSettingStore = defineStore("setting", { usePlaybackForDownload: false, useUnlockForDownload: false, downloadMakeYrc: false, + downloadSaveAsAss: false, downloadLyricToTraditional: false, downloadLyricEncoding: "utf-8", saveMetaFile: false, diff --git a/src/stores/status.ts b/src/stores/status.ts index 6617aa106..108073153 100644 --- a/src/stores/status.ts +++ b/src/stores/status.ts @@ -62,6 +62,10 @@ interface StatusState { availableLyricSources: string[]; /** 用户偏好的歌词源(用于切换) */ preferredLyricSource: string | null; + /** 可用的音频源列表 */ + availableAudioSources: string[]; + /** 用户偏好的音频源(用于切换) */ + preferredAudioSource: string | null; /** 当前歌曲音质 */ songQuality: QualityType | undefined; /** 当前歌曲音源 */ @@ -172,6 +176,8 @@ export const useStatusStore = defineStore("status", { usingQRCLyric: false, availableLyricSources: [], preferredLyricSource: null, + availableAudioSources: [], + preferredAudioSource: null, songQuality: undefined, audioSource: undefined, playIndex: -1, diff --git a/src/utils/assGenerator.ts b/src/utils/assGenerator.ts new file mode 100644 index 000000000..2d866a603 --- /dev/null +++ b/src/utils/assGenerator.ts @@ -0,0 +1,68 @@ +import { type LyricLine } from "@applemusic-like-lyrics/lyric"; + +/** + * 将毫秒转换为 ASS 时间格式 (H:MM:SS.cc) + */ +const formatTime = (ms: number): string => { + const totalSeconds = Math.floor(ms / 1000); + const centiseconds = Math.floor((ms % 1000) / 10); // ASS uses centiseconds (0-99) + + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${centiseconds.toString().padStart(2, "0")}`; +}; + +/** + * 生成 ASS 字幕内容 + * @param lines 歌词行数组 + * @param metadata 元数据(标题、艺术家等) + */ +export const generateASS = ( + lines: LyricLine[], + metadata: { title?: string; artist?: string } = {} +): string => { + const { title = "Unknown Title", artist = "Unknown Artist" } = metadata; + + const header = `[Script Info] +Title: ${title} - ${artist} +ScriptType: v4.00+ +WrapStyle: 0 +ScaledBorderAndShadow: yes +YCbCr Matrix: TV.601 +PlayResX: 1920 +PlayResY: 1080 + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +`; + + const events = lines + .map((line) => { + // 忽略空行 + const text = line.words.map((w) => w.word).join("").trim(); + if (!text) return null; + + const startTime = formatTime(line.startTime); + const endTime = formatTime(line.endTime); + + // 处理翻译 + let dialogueText = text; + if (line.translatedLyric) { + dialogueText += `\\N${line.translatedLyric}`; + } + + // 如果有罗马音,也可以加上,但 ASS 通常不宜过分拥挤,暂只加翻译 + + return `Dialogue: 0,${startTime},${endTime},Default,,0,0,0,,${dialogueText}`; + }) + .filter(Boolean) + .join("\n"); + + return header + events; +}; diff --git a/src/views/Home/HomeOnline.vue b/src/views/Home/HomeOnline.vue index e2a740322..27c84dbca 100644 --- a/src/views/Home/HomeOnline.vue +++ b/src/views/Home/HomeOnline.vue @@ -93,6 +93,7 @@ const settingStore = useSettingStore(); // 日推标题 const dailySongsTitle = computed(() => { + if (settingStore.hiddenCovers.home) return "每日推荐"; const day = new Date().getDate(); return h("div", { class: "date" }, [ h("div", { class: "date-icon" }, [