├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── build ├── 777x777.png └── icon.ico ├── electron-builder.yml ├── electron.vite.config.mjs ├── package.json ├── pnpm-lock.yaml ├── resources └── icon.png ├── src ├── main │ ├── index.ts │ └── type.d.ts ├── preload │ ├── index.js │ └── lyric.js └── renderer │ ├── auto-imports.d.ts │ ├── components.d.ts │ ├── desktopLyric.html │ ├── index.html │ ├── public │ ├── icon.png │ └── manifest.json │ └── src │ ├── App.vue │ ├── assets │ └── font.less │ ├── components │ ├── commentItem.vue │ ├── headerTop.vue │ ├── itemCard.vue │ ├── itemCardList.vue │ ├── lyricLine.vue │ ├── marqueePlus.vue │ ├── musicController.vue │ ├── musicList.vue │ ├── navigation.vue │ ├── player.vue │ ├── playinglist.vue │ └── settingItem.vue │ ├── desktopLyric │ ├── lyricApp.vue │ └── window.d.ts │ ├── desktopLyricMain.js │ ├── main.ts │ ├── modules │ ├── api.ts │ ├── lyric.ts │ ├── messageApi.vue │ ├── modalApi.vue │ ├── module.d.ts │ ├── notificationApi.vue │ └── types │ │ ├── lyric.d.ts │ │ └── song.d.ts │ ├── pages │ ├── account.vue │ ├── comments.vue │ ├── container.vue │ ├── home.vue │ ├── login.vue │ ├── playlist.vue │ ├── search.vue │ └── setting.vue │ ├── router │ └── index.ts │ ├── stores │ ├── download.ts │ ├── index.ts │ ├── play.ts │ ├── setting.ts │ ├── theme.ts │ └── user.ts │ ├── utils │ └── mitt.ts │ └── window.d.ts ├── tsconfig.json └── uno.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:vue/vue3-recommended', 8 | '@electron-toolkit', 9 | '@vue/eslint-config-prettier' 10 | ], 11 | rules: { 12 | 'vue/require-default-prop': 'off', 13 | 'vue/multi-word-component-names': 'off' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy website 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - main 8 | 9 | # 任务 10 | jobs: 11 | build-and-deploy: 12 | # 服务器环境:最新版 Ubuntu 13 | runs-on: ubuntu-latest 14 | steps: 15 | # 拉取代码 16 | - name: pull 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | persist-credentials: false 21 | # 1、生成静态文件 22 | - name: Build 23 | run: npm install && npm run build 24 | 25 | # 2、部署到 GitHub Pages 26 | - name: Deploy to GitHub Pages 27 | uses: JamesIves/github-pages-deploy-action@releases/v3 28 | with: 29 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 30 | REPOSITORY_NAME: xwzkj/xwzkj.github.io 31 | BRANCH: main 32 | FOLDER: out/renderer 33 | 34 | # - name: Deploy to server 35 | # uses: wlixcc/SFTP-Deploy-Action@v1.2.4 36 | # with: 37 | # username: 'root' #ssh user name 38 | # server: '${{ secrets.SERVER_IP }}' #引用之前创建好的secret 39 | # ssh_private_key: '${{ secrets.SSH_PRIVATE_KEY }}' #引用之前创建好的secret 40 | # local_path: './out/renderer/*' # 对应我们项目build的文件夹路径 41 | # remote_path: '/opt/1panel/apps/openresty/openresty/www/sites/music.xwzkj.top/index/vue' 42 | # delete_remote_files: true 43 | 44 | - name: 打包文件为tar.gz 45 | run: cd ./out/renderer && tar -czvf ../../music-site.tar.gz * 46 | 47 | - name: 发布到服务器 48 | uses: marcodallasanta/ssh-scp-deploy@v1.2.0 49 | with: 50 | local: 'music-site.tar.gz' # Local file path - REQUIRED false - DEFAULT ./ 51 | remote: '/opt/xwzkj/temp/' # Remote file path - REQUIRED false - DEFAULT ~/ 52 | host: ${{secrets.SERVER_IP}} # Remote server address - REQUIRED true 53 | port: '22' # Remote server port - REQUIRED false - DEFAULT 22 54 | user: 'root' # Remote server user - REQUIRED true 55 | key: ${{secrets.SSH_PRIVATE_KEY}} # Remote server private key - REQUIRED at least one of "password" or "key" 56 | pre_upload: rm -f /opt/xwzkj/temp/music-site.tar.gz 57 | # 删掉之前的打包 58 | # Command to run via ssh before scp upload - REQUIRED false 59 | post_upload: cd /opt/1panel/apps/openresty/openresty/www/sites/music.xwzkj.top/index/vue && rm -rf * && tar -xzvf /opt/xwzkj/temp/music-site.tar.gz && rm -f /opt/xwzkj/temp/music-site.tar.gz 60 | # Command to run via ssh after scp upload - REQUIRED false 61 | ssh_options: -o StrictHostKeyChecking=no # A set of ssh_option separated by -o - REQUIRED false - DEFAULT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null 62 | scp_options: -v -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*' # 任何标签的推送都会触发 6 | 7 | jobs: 8 | release: 9 | # 服务器环境:最新版 windows 10 | runs-on: windows-latest 11 | steps: 12 | # 拉取代码 13 | - name: pull 14 | uses: actions/checkout@v2 15 | with: 16 | persist-credentials: false 17 | 18 | - name: 构建 19 | run: npm install && npm run build:win 20 | 21 | - name: 创建发行版 22 | uses: ncipollo/release-action@v1.14.0 23 | with: 24 | token: ${{ secrets.ACCESS_TOKEN }} 25 | artifacts: 'dist/*.exe' 26 | body: '由 GitHub Actions 自动发布' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | *.log* 6 | src/renderer/src/modules/build-info.js -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npmmirror.com/mirrors/electron/ 2 | electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ 3 | shamefully-hoist=true 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "vscode.typescript-language-features" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "vscode.typescript-language-features" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 奶酪音乐 2 | 3 | >项目基于neteaseCloudMusicApi 4 | 5 | 项目网页端预览:[github pages](https://xwzkj.github.io) 6 | 客户端请前往release下载 7 | 项目未完工 8 | -------------------------------------------------------------------------------- /build/777x777.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwzkj/CheeseMusic/23cd8c1cb9b912154d010a8e227bda0776a8c398/build/777x777.png -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwzkj/CheeseMusic/23cd8c1cb9b912154d010a8e227bda0776a8c398/build/icon.ico -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.xwzkj.music 2 | productName: 奶酪音乐 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | asarUnpack: 12 | - resources/** 13 | win: 14 | executableName: CheeseMusic 15 | nsis: 16 | artifactName: ${name}-${version}-setup.${ext} 17 | shortcutName: ${productName} 18 | uninstallDisplayName: ${productName} 19 | createDesktopShortcut: always 20 | oneClick: false 21 | allowToChangeInstallationDirectory: true 22 | mac: 23 | entitlementsInherit: build/entitlements.mac.plist 24 | extendInfo: 25 | - NSCameraUsageDescription: Application requests access to the device's camera. 26 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 27 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 28 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 29 | notarize: false 30 | dmg: 31 | artifactName: ${name}-${version}.${ext} 32 | linux: 33 | target: 34 | - AppImage 35 | # - snap 36 | - deb 37 | maintainer: electronjs.org 38 | category: Utility 39 | icon: build/777x777.png 40 | appImage: 41 | artifactName: ${name}-${version}.${ext} 42 | npmRebuild: false 43 | publish: 44 | provider: generic 45 | url: https://example.com/auto-updates 46 | electronDownload: 47 | mirror: https://npmmirror.com/mirrors/electron/ 48 | -------------------------------------------------------------------------------- /electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g 2 | const DRIVE_LETTER_REGEX = /^[a-z]:/i 3 | import { resolve } from 'path' 4 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 5 | import vue from '@vitejs/plugin-vue' 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | import Components from 'unplugin-vue-components/vite' 8 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' 9 | import Icons from 'unplugin-icons/vite' 10 | import IconsResolver from 'unplugin-icons/resolver' 11 | import UnoCSS from 'unocss/vite' 12 | import autoprefixer from 'autoprefixer' 13 | import { version } from './package.json'; 14 | import fs from 'fs'; 15 | import { execSync } from 'child_process'; 16 | import dayjs from 'dayjs' 17 | 18 | function getGitCommitHash() { 19 | return execSync('git rev-parse --short HEAD').toString().trim(); 20 | } 21 | 22 | export default defineConfig({ 23 | main: { 24 | plugins: [externalizeDepsPlugin()] 25 | }, 26 | preload: { 27 | plugins: [externalizeDepsPlugin()], 28 | build: { 29 | rollupOptions: { 30 | input: { 31 | // 可以为不同的预加载脚本指定不同的文件 32 | index: resolve(__dirname, 'src/preload/index.js'), 33 | lyric: resolve(__dirname, 'src/preload/lyric.js') 34 | }, 35 | output: { 36 | entryFileNames: '[name].mjs', 37 | chunkFileNames: '[name].mjs' 38 | } 39 | } 40 | } 41 | }, 42 | renderer: { 43 | resolve: { 44 | alias: { 45 | '@': resolve('src/renderer/src') 46 | }, 47 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] 48 | }, 49 | css: { 50 | postcss: { 51 | plugins: [autoprefixer()] 52 | } 53 | }, 54 | plugins: [ 55 | vue(), 56 | AutoImport({ 57 | imports: [ 58 | 'vue', 59 | { 60 | 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'] 61 | } 62 | ], 63 | resolvers: [] 64 | }), 65 | Components({ 66 | resolvers: [ 67 | IconsResolver({ 68 | prefix: 'i', 69 | enabledCollections: ['ep', 'hugeicons', 'ant-design', 'ic', 'solar', 'iconamoon'] 70 | }), 71 | NaiveUiResolver() 72 | ] 73 | }), 74 | Icons({ 75 | compiler: 'vue3', 76 | autoInstall: true 77 | }), 78 | UnoCSS(), 79 | { 80 | name: 'build-info-plugin', 81 | buildStart() { 82 | const date = dayjs().format('YYYY-MM-DD HH:mm:ss'); 83 | const buildNumber = getGitCommitHash(); // 获取 Git 提交哈希 84 | const buildInfo = `export default { version: '${version}', buildNumber: '${buildNumber}', buildTime: '${date}' }`; 85 | console.log('Build Info:', buildInfo); 86 | // 将构建信息写入文件 87 | fs.writeFileSync( 88 | resolve(__dirname, 'src/renderer/src/modules/build-info.js'), 89 | buildInfo 90 | ); 91 | }, 92 | }, 93 | ], 94 | server: { 95 | port: 80, 96 | host: '0.0.0.0' 97 | }, 98 | build: { 99 | target: 'es6', 100 | reportCompressedSize: false, 101 | sourcemap: false, 102 | commonjsOptions: { 103 | ignoreTryCatch: false 104 | }, 105 | assetsDir: 'assets', 106 | chunkSizeWarningLimit: 2000, // 解决包大小超过500kb的警告 107 | rollupOptions: { 108 | input: { 109 | index: resolve(__dirname, 'src/renderer/index.html'), 110 | lyric: resolve(__dirname, 'src/renderer/desktopLyric.html') 111 | }, 112 | output: { 113 | manualChunks: {}, 114 | chunkFileNames: 'assets/[name]-[hash].js', 115 | entryFileNames: 'assets/[name]-[hash].js', 116 | assetFileNames: 'assets/[name]-[hash].[ext]', 117 | // 解决文件名中的非法字符 118 | sanitizeFileName: (name) => { 119 | const match = DRIVE_LETTER_REGEX.exec(name) 120 | const driveLetter = match ? match[0] : '' 121 | return driveLetter + name.slice(driveLetter.length).replace(INVALID_CHAR_REGEX, '') 122 | } 123 | }, 124 | } 125 | } 126 | } 127 | }) 128 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cheesemusic", 3 | "version": "1.1.5", 4 | "description": "奶酪音乐", 5 | "main": "./out/main/index.js", 6 | "author": "xwzkj", 7 | "homepage": "https://github.com/xwzkj/music", 8 | "type": "module", 9 | "scripts": { 10 | "format": "prettier --write .", 11 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 12 | "start": "electron-vite preview", 13 | "dev": "chcp 65001 && electron-vite dev", 14 | "build": "electron-vite build", 15 | "postinstall": "electron-builder install-app-deps", 16 | "build:unpack": "npm run build && electron-builder --dir", 17 | "build:win": "npm run build && electron-builder --win", 18 | "build:mac": "npm run build && electron-builder --mac", 19 | "build:linux": "npm run build && electron-builder --linux" 20 | }, 21 | "dependencies": { 22 | "@electron-toolkit/preload": "^3.0.1", 23 | "@electron-toolkit/utils": "^3.0.0", 24 | "NeteaseCloudMusicApi": "^4.27.0", 25 | "ajv": "^8.17.1", 26 | "axios": "^1.7.2", 27 | "conf": "^13.0.1", 28 | "electron-store": "^10.0.0", 29 | "open": "^10.1.0", 30 | "path": "^0.12.7" 31 | }, 32 | "devDependencies": { 33 | "@ant-design/colors": "^7.1.0", 34 | "@electron-toolkit/eslint-config": "^1.0.2", 35 | "@iconify/json": "^2.2.231", 36 | "@rushstack/eslint-patch": "^1.10.4", 37 | "@types/animejs": "^3.1.12", 38 | "@types/wicg-file-system-access": "^2023.10.5", 39 | "@vitejs/plugin-vue": "^5.1.1", 40 | "@vue/eslint-config-prettier": "^9.0.0", 41 | "animejs": "^3.2.2", 42 | "autoprefixer": "^10.4.20", 43 | "colorthief": "^2.4.0", 44 | "crypto-js": "^4.2.0", 45 | "dayjs": "^1.11.13", 46 | "electron": "^33.2.1", 47 | "electron-builder": "^25.1.8", 48 | "electron-vite": "^3.1.0", 49 | "eslint": "^8.57.0", 50 | "eslint-plugin-vue": "^9.27.0", 51 | "less": "^4.2.0", 52 | "mitt": "^3.0.1", 53 | "naive-ui": "^2.41.0", 54 | "pinia": "^2.3.1", 55 | "prettier": "^3.3.3", 56 | "unocss": "^0.62.3", 57 | "unplugin-auto-import": "^0.17.8", 58 | "unplugin-icons": "^0.19.0", 59 | "unplugin-vue-components": "^0.27.3", 60 | "vite": "^5.3.5", 61 | "vue": "^3.5.13", 62 | "vue-router": "^4.4.0" 63 | }, 64 | "pnpm": { 65 | "onlyBuiltDependencies": [ 66 | "electron", 67 | "esbuild", 68 | "sharp", 69 | "vue-demi" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwzkj/CheeseMusic/23cd8c1cb9b912154d010a8e227bda0776a8c398/resources/icon.png -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, session, screen } from 'electron' 2 | import open from 'open' 3 | import { join, dirname } from 'path' 4 | import os from 'os' 5 | import { electronApp, optimizer, is } from '@electron-toolkit/utils' 6 | import ElectronStore from 'electron-store' 7 | // const fs = require('fs') 8 | import * as fs from 'fs' 9 | import netease from 'NeteaseCloudMusicApi' 10 | 11 | console.log('奶酪音乐 ©丸子'); 12 | 13 | 14 | let mainWindow: BrowserWindow; 15 | let lyricWindow: BrowserWindow; 16 | let store: ElectronStore; 17 | let primaryDisplay: Electron.Display; 18 | let lyricWindowLocked: boolean; 19 | function mkdirIfUnexist(dir: string) { 20 | if (!fs.existsSync(dir)) { 21 | fs.mkdirSync(dir, { recursive: true }); 22 | } 23 | } 24 | function pathToAbsolute(filePath: string) { 25 | return join(dirname(app.getPath('exe')), filePath); 26 | } 27 | function lockLyricWindow(isLock: boolean) { 28 | if (isLock) { 29 | lyricWindow.setIgnoreMouseEvents(true, { forward: true }) 30 | lyricWindowLocked = true 31 | } else { 32 | lyricWindow.setIgnoreMouseEvents(false) 33 | lyricWindowLocked = false 34 | } 35 | store.set('lyricWindow.locked', lyricWindowLocked) 36 | } 37 | 38 | if (is.dev) { 39 | //更改数据目录到程序文件夹内 40 | mkdirIfUnexist(pathToAbsolute('./data/userData')); 41 | app.setPath('userData', pathToAbsolute('./data/userData')); 42 | mkdirIfUnexist(pathToAbsolute('./data/sessionData')); 43 | app.setPath('sessionData', pathToAbsolute('./data/sessionData')); 44 | console.log('调试模式 运行目录', app.getPath('exe')) 45 | } 46 | //////////////////////////////////////////////////////////////////////////////////////////////ready 47 | app.on('ready', async () => { 48 | //防多开 主要是多开有问题 49 | let singleRunKey = { key: 'com.xwzkj.music' } 50 | if (is.dev) { 51 | singleRunKey.key += '.dev' 52 | } 53 | if (!app.requestSingleInstanceLock(singleRunKey)) { 54 | app.quit() 55 | } 56 | 57 | 58 | primaryDisplay = screen.getPrimaryDisplay(); 59 | store = new ElectronStore() 60 | // 往亦晕音乐api 本地处理 61 | ipcMain.handle('netease', async (_, path, data) => { 62 | try { 63 | let func = netease[path as keyof typeof netease] as Function 64 | let res = await func(data) 65 | return { data: res.body }; 66 | } catch (e) { 67 | console.log('wyyapi', e); 68 | return { data: e } 69 | } 70 | }) 71 | // 桌面歌词传递 72 | ipcMain.on('lyric', (_, data) => { 73 | // console.log('lyric: ', data); 74 | 75 | if (lyricWindow) { 76 | lyricWindow.webContents.send('lyric', data) 77 | } 78 | }) 79 | // 主题色传递 80 | ipcMain.on('themeColors', (_, data) => { 81 | // console.log('theme: ', data); 82 | if (lyricWindow) { 83 | lyricWindow.webContents.send('themeColors', data) 84 | } 85 | }) 86 | // 桌面歌词窗口打开关闭 87 | ipcMain.on('lyricWindowShow', (_, data) => { 88 | // console.log('lyricWindowShow: ', data); 89 | if (data) { 90 | lyricWindow.show() 91 | } else { 92 | lyricWindow.hide() 93 | } 94 | }) 95 | // 桌面歌词窗口锁定(鼠标穿透 96 | lyricWindowLocked = store.get('lyricWindow.locked') 97 | ipcMain.on('lyricWindowLock', (_, islock) => { 98 | // console.log('lyricWindowLock:', islock); 99 | lockLyricWindow(islock) 100 | }) 101 | ipcMain.handle('isLyricWindowLocked', () => { 102 | // console.log('isLyricWindowLocked:', lyricWindowLocked); 103 | return lyricWindowLocked 104 | }) 105 | 106 | ipcMain.handle('getAppVersion', () => { 107 | return app.getVersion(); 108 | }) 109 | // 浏览器打开url 110 | ipcMain.handle('openUrl', async (_, url) => { 111 | await open(url) 112 | return true; 113 | }) 114 | ipcMain.on('window-close', () => { 115 | mainWindow.close() 116 | }) 117 | //加载devTool插件 118 | if (os.platform() == 'win32' && is.dev) { 119 | if (fs.existsSync("F:/code/web/vue-devtools")) { 120 | console.log('devtools exists'); 121 | } else { 122 | console.log('devtools not exists'); 123 | return 124 | } 125 | await session.defaultSession.loadExtension("F:/code/web/vue-devtools") 126 | } 127 | // Set app user model id for windows 128 | electronApp.setAppUserModelId('com.xwzkj.music') 129 | createWindow() 130 | app.on('second-instance', (event, commandLine, workingDirectory, additionalData) => { 131 | console.log(additionalData, commandLine) 132 | //试图运行第二个实例,将第一个实例窗口聚焦 133 | if (mainWindow) { 134 | if (mainWindow.isMinimized()) mainWindow.restore() 135 | mainWindow.focus() 136 | } 137 | }) 138 | }) 139 | app.on('window-all-closed', () => { 140 | app.quit() 141 | }) 142 | 143 | function createWindow() { 144 | // Create the browser window. 145 | mainWindow = new BrowserWindow({ 146 | width: 1150, 147 | minWidth: 900, 148 | height: 800, 149 | minHeight: 600, 150 | autoHideMenuBar: true, 151 | frame: false, 152 | icon: join(__dirname, '../../resources/icon.png'), 153 | webPreferences: { 154 | preload: join(__dirname, '../preload/index.mjs'), 155 | sandbox: false, 156 | // webSecurity: false, 157 | } 158 | }) 159 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 160 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 161 | } else { 162 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')) 163 | } 164 | if (is.dev) { 165 | mainWindow.webContents.openDevTools() 166 | } 167 | // 使用 session API 设置 CORS 头 168 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => { 169 | delete details.responseHeaders!['access-control-allow-origin'] 170 | details.responseHeaders!['Access-Control-Allow-Origin'] = ['*'] 171 | // console.log(details.responseHeaders); 172 | callback({ 173 | responseHeaders: details.responseHeaders 174 | }) 175 | }) 176 | 177 | lyricWindow = new BrowserWindow({ 178 | width: 1000, 179 | height: 160, 180 | autoHideMenuBar: true, 181 | icon: join(__dirname, '../../resources/icon.png'), 182 | titleBarStyle: 'hidden', 183 | alwaysOnTop: true, 184 | transparent: true, 185 | webPreferences: { 186 | preload: join(__dirname, '../preload/lyric.mjs'), 187 | sandbox: false, 188 | } 189 | }) 190 | lyricWindow.setAlwaysOnTop(true, 'screen-saver') 191 | // 先隐藏 然后通过主窗口控制显示状态 192 | lyricWindow.hide(); 193 | //开始读取配置 194 | console.log(store.get('lyricWindow')); 195 | // 设置窗口大小 196 | let temp: any = store.get('lyricWindow.size', [1000, 160]) 197 | if (temp[0] >= primaryDisplay.workAreaSize.width || temp[1] > primaryDisplay.workAreaSize.height - 100) { 198 | temp = [1000, 160] 199 | } 200 | lyricWindow.setSize(temp[0], temp[1]) 201 | // 设置位置 202 | temp = store.get('lyricWindow.position', [0, primaryDisplay.workAreaSize.height - 160]) 203 | if (temp[0] < 0 || temp[0] > primaryDisplay.workAreaSize.width - 50 || temp[1] < 0 || temp[1] > primaryDisplay.workAreaSize.height - 50) { 204 | temp = [0, primaryDisplay.workAreaSize.height - 160] 205 | } 206 | lyricWindow.setPosition(temp[0], temp[1]) 207 | //设置锁定 208 | temp = store.get('lyricWindow.locked', false) 209 | lockLyricWindow(temp) 210 | 211 | //加载页面 212 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 213 | lyricWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/desktopLyric.html') 214 | } else { 215 | lyricWindow.loadFile(join(__dirname, '../renderer/desktopLyric.html')) 216 | } 217 | 218 | lyricWindow.setSkipTaskbar(true) 219 | 220 | 221 | // 窗口事件 222 | // 歌词窗口 保存位置 223 | lyricWindow.on('moved', () => { 224 | store.set('lyricWindow.position', lyricWindow.getPosition()) 225 | }) 226 | // 歌词窗口 保存大小 227 | lyricWindow.on('resized', () => { 228 | store.set('lyricWindow.size', lyricWindow.getSize()) 229 | }) 230 | mainWindow.on('close', (e) => { 231 | if (lyricWindow) { 232 | lyricWindow.close() 233 | } 234 | }) 235 | } -------------------------------------------------------------------------------- /src/main/type.d.ts: -------------------------------------------------------------------------------- 1 | type conf = { 2 | lyricWindow: { 3 | position: number[], 4 | size: number[], 5 | locked: boolean 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/preload/index.js: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | 3 | contextBridge.exposeInMainWorld('isElectron', true) 4 | contextBridge.exposeInMainWorld('api', { 5 | 'netease': (url, data) => { 6 | if (url.startsWith('/')) { 7 | url = url.slice(1); 8 | } 9 | // 将斜杠替换为下划线 10 | let path = url.replace(/\//g, '_'); 11 | // console.log('[debug][api]调用网易云API', path, data); 12 | return ipcRenderer.invoke('netease', path, data) 13 | }, 14 | 'sendLyric': (lyricNowObj) => { 15 | return ipcRenderer.send('lyric', lyricNowObj); 16 | }, 17 | 'sendThemeColors': (data) => { 18 | return ipcRenderer.send('themeColors', data); 19 | }, 20 | 'setLyricWindowShow': (show) => { 21 | return ipcRenderer.send('lyricWindowShow', show); 22 | }, 23 | 'openUrl': (url) => { 24 | return ipcRenderer.invoke('openUrl', url); 25 | }, 26 | 'appVersion': await ipcRenderer.invoke('getAppVersion'), 27 | // 'appVersion': '1.0.0' 28 | 29 | 'windowClose': () => { 30 | return ipcRenderer.send('window-close') 31 | }, 32 | }) -------------------------------------------------------------------------------- /src/preload/lyric.js: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | 3 | contextBridge.exposeInMainWorld('getLyric', (callback) => { 4 | return ipcRenderer.on('lyric', callback) 5 | }) 6 | contextBridge.exposeInMainWorld('getThemeColors', (callback) => { 7 | return ipcRenderer.on('themeColors', callback) 8 | }) 9 | contextBridge.exposeInMainWorld('lyricWindowLock', (isLock) => { 10 | return ipcRenderer.send('lyricWindowLock', isLock) 11 | }) 12 | contextBridge.exposeInMainWorld('isLyricWindowLocked', () => { 13 | return ipcRenderer.invoke('isLyricWindowLocked') 14 | }) 15 | -------------------------------------------------------------------------------- /src/renderer/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const customRef: typeof import('vue')['customRef'] 12 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 13 | const defineComponent: typeof import('vue')['defineComponent'] 14 | const effectScope: typeof import('vue')['effectScope'] 15 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 16 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 17 | const h: typeof import('vue')['h'] 18 | const inject: typeof import('vue')['inject'] 19 | const isProxy: typeof import('vue')['isProxy'] 20 | const isReactive: typeof import('vue')['isReactive'] 21 | const isReadonly: typeof import('vue')['isReadonly'] 22 | const isRef: typeof import('vue')['isRef'] 23 | const markRaw: typeof import('vue')['markRaw'] 24 | const nextTick: typeof import('vue')['nextTick'] 25 | const onActivated: typeof import('vue')['onActivated'] 26 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 27 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 28 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 29 | const onDeactivated: typeof import('vue')['onDeactivated'] 30 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 31 | const onMounted: typeof import('vue')['onMounted'] 32 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 33 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 34 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 35 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 36 | const onUnmounted: typeof import('vue')['onUnmounted'] 37 | const onUpdated: typeof import('vue')['onUpdated'] 38 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 39 | const provide: typeof import('vue')['provide'] 40 | const reactive: typeof import('vue')['reactive'] 41 | const readonly: typeof import('vue')['readonly'] 42 | const ref: typeof import('vue')['ref'] 43 | const resolveComponent: typeof import('vue')['resolveComponent'] 44 | const shallowReactive: typeof import('vue')['shallowReactive'] 45 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 46 | const shallowRef: typeof import('vue')['shallowRef'] 47 | const toRaw: typeof import('vue')['toRaw'] 48 | const toRef: typeof import('vue')['toRef'] 49 | const toRefs: typeof import('vue')['toRefs'] 50 | const toValue: typeof import('vue')['toValue'] 51 | const triggerRef: typeof import('vue')['triggerRef'] 52 | const unref: typeof import('vue')['unref'] 53 | const useAttrs: typeof import('vue')['useAttrs'] 54 | const useCssModule: typeof import('vue')['useCssModule'] 55 | const useCssVars: typeof import('vue')['useCssVars'] 56 | const useDialog: typeof import('naive-ui')['useDialog'] 57 | const useId: typeof import('vue')['useId'] 58 | const useLoadingBar: typeof import('naive-ui')['useLoadingBar'] 59 | const useMessage: typeof import('naive-ui')['useMessage'] 60 | const useModel: typeof import('vue')['useModel'] 61 | const useNotification: typeof import('naive-ui')['useNotification'] 62 | const useSlots: typeof import('vue')['useSlots'] 63 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 64 | const watch: typeof import('vue')['watch'] 65 | const watchEffect: typeof import('vue')['watchEffect'] 66 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 67 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 68 | } 69 | // for type re-export 70 | declare global { 71 | // @ts-ignore 72 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 73 | import('vue') 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CommentItem: typeof import('./src/components/commentItem.vue')['default'] 11 | HeaderTop: typeof import('./src/components/headerTop.vue')['default'] 12 | IAntDesignHeartFilled: typeof import('~icons/ant-design/heart-filled')['default'] 13 | IAntDesignHeartOutlined: typeof import('~icons/ant-design/heart-outlined')['default'] 14 | IHugeiconsArrowDown01: typeof import('~icons/hugeicons/arrow-down01')['default'] 15 | IHugeiconsArrowLeft01: typeof import('~icons/hugeicons/arrow-left01')['default'] 16 | IHugeiconsArrowRight01: typeof import('~icons/hugeicons/arrow-right01')['default'] 17 | IHugeiconsArrowUp01: typeof import('~icons/hugeicons/arrow-up01')['default'] 18 | IHugeiconsCheckmarkSquare01: typeof import('~icons/hugeicons/checkmark-square01')['default'] 19 | IHugeiconsDragDrop: typeof import('~icons/hugeicons/drag-drop')['default'] 20 | IHugeiconsExchange01: typeof import('~icons/hugeicons/exchange01')['default'] 21 | IHugeiconsLinkSquare01: typeof import('~icons/hugeicons/link-square01')['default'] 22 | IHugeiconsMagicWand01: typeof import('~icons/hugeicons/magic-wand01')['default'] 23 | IHugeiconsMessage01: typeof import('~icons/hugeicons/message01')['default'] 24 | IHugeiconsMessageLock01: typeof import('~icons/hugeicons/message-lock01')['default'] 25 | IHugeiconsPause: typeof import('~icons/hugeicons/pause')['default'] 26 | IHugeiconsPlay: typeof import('~icons/hugeicons/play')['default'] 27 | IHugeiconsPlaylist03: typeof import('~icons/hugeicons/playlist03')['default'] 28 | IHugeiconsRightToLeftListDash: typeof import('~icons/hugeicons/right-to-left-list-dash')['default'] 29 | IHugeiconsSmartPhone01: typeof import('~icons/hugeicons/smart-phone01')['default'] 30 | IHugeiconsSquareLock02: typeof import('~icons/hugeicons/square-lock02')['default'] 31 | IHugeiconsSquareLockPassword: typeof import('~icons/hugeicons/square-lock-password')['default'] 32 | IIconamoonLikeDuotone: typeof import('~icons/iconamoon/like-duotone')['default'] 33 | IIcTwotoneCalendarToday: typeof import('~icons/ic/twotone-calendar-today')['default'] 34 | ISolarCloseCircleOutline: typeof import('~icons/solar/close-circle-outline')['default'] 35 | ItemCard: typeof import('./src/components/itemCard.vue')['default'] 36 | ItemCardList: typeof import('./src/components/itemCardList.vue')['default'] 37 | LyricLine: typeof import('./src/components/lyricLine.vue')['default'] 38 | MarqueePlus: typeof import('./src/components/marqueePlus.vue')['default'] 39 | MusicController: typeof import('./src/components/musicController.vue')['default'] 40 | MusicList: typeof import('./src/components/musicList.vue')['default'] 41 | NAvatar: typeof import('naive-ui')['NAvatar'] 42 | Navigation: typeof import('./src/components/navigation.vue')['default'] 43 | NButton: typeof import('naive-ui')['NButton'] 44 | NCard: typeof import('naive-ui')['NCard'] 45 | NColorPicker: typeof import('naive-ui')['NColorPicker'] 46 | NConfigProvider: typeof import('naive-ui')['NConfigProvider'] 47 | NDivider: typeof import('naive-ui')['NDivider'] 48 | NDropdown: typeof import('naive-ui')['NDropdown'] 49 | NEllipsis: typeof import('naive-ui')['NEllipsis'] 50 | NIcon: typeof import('naive-ui')['NIcon'] 51 | NInput: typeof import('naive-ui')['NInput'] 52 | NMenu: typeof import('naive-ui')['NMenu'] 53 | NMessageProvider: typeof import('naive-ui')['NMessageProvider'] 54 | NModal: typeof import('naive-ui')['NModal'] 55 | NModalProvider: typeof import('naive-ui')['NModalProvider'] 56 | NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] 57 | NScrollbar: typeof import('naive-ui')['NScrollbar'] 58 | NSlider: typeof import('naive-ui')['NSlider'] 59 | NSpin: typeof import('naive-ui')['NSpin'] 60 | NTabPane: typeof import('naive-ui')['NTabPane'] 61 | NTabs: typeof import('naive-ui')['NTabs'] 62 | NTag: typeof import('naive-ui')['NTag'] 63 | Player: typeof import('./src/components/player.vue')['default'] 64 | Playinglist: typeof import('./src/components/playinglist.vue')['default'] 65 | RouterLink: typeof import('vue-router')['RouterLink'] 66 | RouterView: typeof import('vue-router')['RouterView'] 67 | SettingItem: typeof import('./src/components/settingItem.vue')['default'] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/desktopLyric.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 奶酪音乐 12 | 13 | 14 | 15 |
16 | 17 | 18 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 奶酪音乐 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 32 | 33 | 42 | 43 | 44 | 64 | 65 | -------------------------------------------------------------------------------- /src/renderer/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwzkj/CheeseMusic/23cd8c1cb9b912154d010a8e227bda0776a8c398/src/renderer/public/icon.png -------------------------------------------------------------------------------- /src/renderer/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "奶酪音乐", 3 | "icons": [ 4 | { 5 | "src": "icon.png", 6 | "type": "image/png", 7 | "sizes": "777x777" 8 | } 9 | ], 10 | "start_url": ".", 11 | "display": "fullscreen", 12 | "background_color": "#ffffff" 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/src/App.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 130 | 131 | 217 | -------------------------------------------------------------------------------- /src/renderer/src/assets/font.less: -------------------------------------------------------------------------------- 1 | @source0: 'https://cf-static.xwzkj.top/fonts'; 2 | @source1: 'https://cdn-static.xwzkj.top/fonts'; 3 | @source2: 'https://cdn.jsdelivr.net/gh/xwzkj/fonts@latest'; 4 | @source3: 'https://github.com/xwzkj/fonts/releases/download/1'; 5 | @source4: 'https://static.xwzkj.top/fonts'; 6 | 7 | .source(@file-name) { 8 | src: url('@{source0}@{file-name}'), 9 | url('@{source1}@{file-name}'), 10 | url('@{source2}@{file-name}'), 11 | url('@{source3}@{file-name}'), 12 | url('@{source4}@{file-name}'); 13 | } 14 | 15 | @font-face { 16 | font-family: "MiSans VF"; 17 | @file-name: '/MiSans.ttf'; 18 | .source(@file-name); 19 | font-display: swap; 20 | } 21 | 22 | @font-face { 23 | font-family: "HarmonyOS-sans"; 24 | @file-name: '/HarmonyOS_Sans_SC_Thin.ttf'; 25 | .source(@file-name); 26 | font-weight: 100; 27 | /* Thin */ 28 | font-display: swap; 29 | } 30 | 31 | @font-face { 32 | font-family: "HarmonyOS-sans"; 33 | @file-name: '/HarmonyOS_Sans_SC_Light.ttf'; 34 | .source(@file-name); 35 | font-weight: 300; 36 | /* Light */ 37 | font-display: swap; 38 | } 39 | 40 | @font-face { 41 | font-family: "HarmonyOS-sans"; 42 | @file-name: '/HarmonyOS_Sans_SC_Regular.ttf'; 43 | .source(@file-name); 44 | font-weight: 400; 45 | /* Regular */ 46 | font-display: swap; 47 | } 48 | 49 | 50 | @font-face { 51 | font-family: "HarmonyOS-sans"; 52 | @file-name: '/HarmonyOS_Sans_SC_Medium.ttf'; 53 | .source(@file-name); 54 | font-weight: 500; 55 | /* Medium */ 56 | font-display: swap; 57 | } 58 | 59 | @font-face { 60 | font-family: "HarmonyOS-sans"; 61 | @file-name: '/HarmonyOS_Sans_SC_Bold.ttf'; 62 | .source(@file-name); 63 | font-weight: 700; 64 | /* Bold */ 65 | font-display: swap; 66 | } 67 | 68 | @font-face { 69 | font-family: "HarmonyOS-sans"; 70 | @file-name: '/HarmonyOS_Sans_SC_Black.ttf'; 71 | .source(@file-name); 72 | font-weight: 900; 73 | /* Black */ 74 | font-display: swap; 75 | } -------------------------------------------------------------------------------- /src/renderer/src/components/commentItem.vue: -------------------------------------------------------------------------------- 1 | 44 | 73 | -------------------------------------------------------------------------------- /src/renderer/src/components/headerTop.vue: -------------------------------------------------------------------------------- 1 | 23 | 71 | -------------------------------------------------------------------------------- /src/renderer/src/components/itemCard.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 41 | 42 | -------------------------------------------------------------------------------- /src/renderer/src/components/itemCardList.vue: -------------------------------------------------------------------------------- 1 | 14 | 26 | -------------------------------------------------------------------------------- /src/renderer/src/components/lyricLine.vue: -------------------------------------------------------------------------------- 1 | 26 | 48 | -------------------------------------------------------------------------------- /src/renderer/src/components/marqueePlus.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 120 | 121 | 160 | 161 | -------------------------------------------------------------------------------- /src/renderer/src/components/musicController.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 192 | 193 | 350 | -------------------------------------------------------------------------------- /src/renderer/src/components/musicList.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 82 | 83 | -------------------------------------------------------------------------------- /src/renderer/src/components/navigation.vue: -------------------------------------------------------------------------------- 1 | 9 | 85 | -------------------------------------------------------------------------------- /src/renderer/src/components/player.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 95 | 197 | 198 | 450 | -------------------------------------------------------------------------------- /src/renderer/src/components/playinglist.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 98 | 99 | -------------------------------------------------------------------------------- /src/renderer/src/components/settingItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 37 | 38 | -------------------------------------------------------------------------------- /src/renderer/src/desktopLyric/lyricApp.vue: -------------------------------------------------------------------------------- 1 | 20 | 93 | -------------------------------------------------------------------------------- /src/renderer/src/desktopLyric/window.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | getLyric: (changeLyric: any) => void; 3 | getThemeColors: (changeThemeColors: any) => void; 4 | isLyricWindowLocked: () => Promise; 5 | lyricWindowLock: (islock: boolean) => void; 6 | } -------------------------------------------------------------------------------- /src/renderer/src/desktopLyricMain.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from '@/desktopLyric/lyricApp.vue' 3 | import 'virtual:uno.css' 4 | import './assets/font.less' 5 | let app = createApp(App) 6 | app.mount('#app') 7 | -------------------------------------------------------------------------------- /src/renderer/src/main.ts: -------------------------------------------------------------------------------- 1 | console.log('%c奶酪音乐 ©丸子',"font-family:'微软雅黑' ; font-size: 1.5rem; color: #deb237; background-color: #deb23710; padding: 0.5rem; border-radius: 0.5rem") 2 | import { createApp } from 'vue' 3 | import router from '@/router/index' 4 | import pinia from '@/stores/index' 5 | import App from './App.vue' 6 | import 'virtual:uno.css' 7 | import './assets/font.less' 8 | let app = createApp(App) 9 | app.use(pinia) 10 | app.use(router) 11 | app.mount('#app') 12 | -------------------------------------------------------------------------------- /src/renderer/src/modules/api.ts: -------------------------------------------------------------------------------- 1 | import colorThief from 'colorthief' 2 | import axios from 'axios' 3 | import type { AxiosProgressEvent } from 'axios' 4 | //pinia在request内部初始化 因为userstore和这个模块相互调用 5 | import pinia from '@/stores/index.js' 6 | import { useUserStore } from '@/stores/user.js' 7 | import { MD5 } from 'crypto-js' 8 | /* 9 | *----------------------------------------------- 10 | *以下是NeteaseCloudMusicApi接口的调用方法 11 | *----------------------------------------------- 12 | */ 13 | export let apiurl = 'https://api.xwzkj.top' 14 | let musicApi = axios.create({ 15 | baseURL: apiurl, 16 | timeout: 20000 17 | // withCredentials: true, 18 | }) 19 | /** 20 | * 网络请求函数 21 | * @param {Object} params url: 请求地址,method: 请求方式,data: 请求参数 22 | * @returns 23 | */ 24 | let request = async (params, realTimeSync = true) => { 25 | try { 26 | // 浏览器环境 27 | const userStore = useUserStore(pinia) 28 | //加时间戳避免缓存 29 | if (realTimeSync) { 30 | params.params = { ...params.params, timestamp: Date.now() } 31 | } 32 | params.params = { ...params.params, realIP: userStore.ip ?? '111.37.150.114' } 33 | //判断如果跨域就尝试手动传递cookie 34 | if (localStorage.getItem('cookie') != null && apiurl.slice(0, 4) == 'http') { 35 | if (params.method == 'post') { 36 | params.data = { ...params.data, cookie: userStore.cookie } 37 | } else if (params.method == 'get') { 38 | params.params = { ...params.params, cookie: userStore.cookie } 39 | } 40 | } 41 | let req = await musicApi.request(params) 42 | return req 43 | } catch (e) { 44 | error( 45 | `${e.name}\n${e.message}\n${e?.response?.data?.message ?? e?.response}`, 46 | `API网络请求错误!可尝试使用客户端` 47 | ) 48 | } 49 | } 50 | 51 | if (window.isElectron) { 52 | // console.log('当前是electron环境!') 53 | request = async (param, _) => { 54 | let { url, method, params, data } = param 55 | if (localStorage.getItem('cookie')) { 56 | data = { ...data, cookie: localStorage.getItem('cookie') } 57 | } 58 | console.log('%c本地api-发送请求', 'color: gray; background-color: lightcyan; padding: 0.5rem; border-radius: 0.5rem', param) 59 | let res = await window.api.netease(url, { ...data, ...params }) 60 | console.log('%c本地api-收到响应', 'color: gray; background-color: aliceblue; padding: 0.5rem; border-radius: 0.5rem', param, res) 61 | return res 62 | } 63 | } 64 | export function loginStatus() { 65 | return request({ 66 | url: '/login/status', 67 | method: 'post' 68 | }) 69 | } 70 | export function userPlaylist(uid) { 71 | return request({ 72 | url: '/user/playlist', 73 | method: 'post', 74 | params: { uid } 75 | }) 76 | } 77 | export function likelist(uid) { 78 | return request({ 79 | url: '/likelist', 80 | method: 'post', 81 | params: { uid } 82 | }) 83 | } 84 | export function vipInfo() { 85 | return request({ 86 | url: '/vip/info', 87 | method: 'post' 88 | }) 89 | } 90 | export function loginQrKey() { 91 | return request({ 92 | url: '/login/qr/key', 93 | method: 'post' 94 | }) 95 | } 96 | export function loginQrCreate(key) { 97 | return request({ 98 | url: '/login/qr/create', 99 | method: 'post', 100 | data: { key } 101 | }) 102 | } 103 | export function loginQrCheck(key, noCookie: boolean = true) { 104 | return request({ 105 | url: '/login/qr/check', 106 | method: 'post', 107 | params: { key, noCookie } 108 | }) 109 | } 110 | export function songDetail(ids: string) { 111 | return request({ 112 | url: '/song/detail', 113 | method: 'post', 114 | data: { ids } 115 | }) 116 | } 117 | /** 118 | * 参数四只有在参数三有值才生效 119 | */ 120 | export async function songUrlV1(id: string, level: string, specialApi: string | null = null, cookie: string | null = null) { 121 | id = String(id) 122 | function apiRaw() { 123 | if (specialApi) { 124 | return axios.get(specialApi, { 125 | params: { 126 | id, 127 | level, 128 | cookie 129 | } 130 | }) 131 | } else { 132 | return request({ 133 | url: '/song/url/v1', 134 | method: 'post', 135 | data: { id, level } 136 | }) 137 | } 138 | } 139 | //对结果按照参数中id的顺序排序 140 | let res = await apiRaw() 141 | // console.log(id) 142 | let idArray = id.split(',') 143 | res.data.data.sort((a, b) => { 144 | return idArray.indexOf(String(a.id)) - idArray.indexOf(String(b.id)) 145 | }) 146 | return res; 147 | } 148 | export function lyricNew(id) { 149 | return request({ 150 | url: '/lyric/new', 151 | method: 'post', 152 | params: { id } 153 | }) 154 | } 155 | export function commentNew(id: string | number, type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 = 0, pageNo: number = 1, pageSize: number = 20, sortType: 1 | 2 | 3 = 1, cursor?: number) { 156 | return request({ 157 | url: '/comment/new', 158 | method: 'post', 159 | data: { id, type, pageNo, pageSize, sortType, cursor } 160 | }) 161 | } 162 | 163 | /** 164 | * 获取楼层评论 165 | * @param parentCommentId 楼层评论id 166 | * @param id 资源id 167 | * @param type 资源类型 (0: 歌曲, 1: mv, 2: 歌单, 3: 专辑, 4: 电台节目, 5: 视频, 6: 动态, 7: 电台) 168 | * @param limit 取出评论数量,默认20 169 | * @param time 分页参数,取上一页最后一项的time 170 | */ 171 | export function commentFloor(parentCommentId: number | string, id: number | string, type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7, limit: number = 20, time?: number) { 172 | return request({ 173 | url: '/comment/floor', 174 | method: 'post', 175 | params: { parentCommentId, id, type, limit, time } 176 | }) 177 | } 178 | 179 | export function recommendSongs() { 180 | return request({ 181 | url: '/recommend/songs', 182 | method: 'post' 183 | }) 184 | } 185 | export function playlistDetail(id) { 186 | return request({ 187 | url: '/playlist/detail', 188 | method: 'post', 189 | params: { id } 190 | }) 191 | } 192 | export function cloudsearch(keywords: any, type: 1 | 10 | 100 | 1000 | 1002 | 1004 | 1006 | 1009 | 1014 | 1018 | 2000 = 1, offset: number = 0, limit: number = 60) { 193 | return request({ 194 | url: '/cloudsearch', 195 | method: 'post', 196 | data: { keywords, type, offset, limit } 197 | }) 198 | } 199 | 200 | // time的单位为秒 201 | export function scrobble(id: string | number, time: number, sourceid: number | string = 0) { 202 | return request({ 203 | url: '/scrobble', 204 | method: 'post', 205 | data: { id, time, sourceid } 206 | }) 207 | } 208 | export function loginWithPhone(phone, password = null, captcha) { 209 | if (password != null) { 210 | password = MD5(password).toString() 211 | } 212 | return request({ 213 | url: '/login/cellphone', 214 | method: 'post', 215 | data: { phone, md5_password: password, captcha } 216 | }) 217 | } 218 | export function sendCaptcha(phone) { 219 | return request({ 220 | url: '/captcha/sent', 221 | method: 'post', 222 | data: { phone } 223 | }) 224 | } 225 | export function verifyCaptcha(phone, captcha) { 226 | return request({ 227 | url: '/captcha/verify', 228 | method: 'post', 229 | data: { phone, captcha } 230 | }) 231 | } 232 | export function playlistTracks(op, pid, tracks) { 233 | if (!op || !pid || !tracks) { 234 | throw new Error('[api][歌单添加或删除歌曲]参数不能为空') 235 | } 236 | return request({ 237 | url: '/playlist/tracks', 238 | method: 'post', 239 | params: { op, pid, tracks } 240 | }) 241 | } 242 | export function like(id, like = true) { 243 | if (id == undefined || id == null) { 244 | throw new Error('[api][like]id不能为空') 245 | } 246 | // return request({ 247 | // url: '/like', 248 | // method: 'post', 249 | // params: { id, like } 250 | // }) 251 | const userStore = useUserStore(pinia) 252 | return playlistTracks(like ? 'add' : 'del', userStore.playlists?.[0]?.id, String(id)) 253 | } 254 | 255 | export async function likeAndUpdateLikelist(id, isLike = true) { 256 | const userStore = useUserStore(pinia) 257 | //先把结果更新到列表 防止操作延迟降低使用体验 258 | if (isLike) { 259 | userStore.likedSongs.push(Number(id)) 260 | } else { 261 | userStore.likedSongs.splice(userStore.likedSongs.indexOf(Number(id)), 1) 262 | } 263 | await like(id, isLike) 264 | userStore.updateLikelist() 265 | } 266 | 267 | export async function getPersonalizedPlaylist() { 268 | return request({ 269 | url: '/personalized', 270 | method: 'post' 271 | }) 272 | } 273 | 274 | /* 275 | *----------------------------------------------- 276 | *以下是colorthief包装的方法 277 | *----------------------------------------------- 278 | */ 279 | export function getColorsFromImg(imgElement, colorNum, needRaw = false) { 280 | let ColorThief = new colorThief() 281 | let rawColor = ColorThief.getPalette(imgElement, colorNum) 282 | // console.log('[api]获取图片多个主色raw', rawColor); 283 | if (needRaw) { 284 | return rawColor 285 | } 286 | let color = [] 287 | for (let i = 0; i < rawColor.length; i++) { 288 | color[i] = `rgb(${rawColor[i][0]},${rawColor[i][1]},${rawColor[i][2]})` 289 | } 290 | // console.log('[api]获取图片多个主色result', color); 291 | return color 292 | } 293 | export function getColorFromImg(imgElement, needRaw = false) { 294 | let ColorThief = new colorThief() 295 | let rawColor = ColorThief.getColor(imgElement) 296 | // console.log('[api]获取图片单个主色raw', rawColor); 297 | if (needRaw) { 298 | return rawColor 299 | } 300 | let color = `rgb(${rawColor[0]},${rawColor[1]},${rawColor[2]})` 301 | // console.log('[api]获取图片单个主色result', color); 302 | return color 303 | } 304 | export function mixColor(colorA, colorB, weight = 0.5, needRaw = false, lighter = false) { 305 | let r = Math.round(colorA[0] * weight + colorB[0] * (1 - weight)) 306 | let g = Math.round(colorA[1] * weight + colorB[1] * (1 - weight)) 307 | let b = Math.round(colorA[2] * weight + colorB[2] * (1 - weight)) 308 | r = Math.min(r, 255) 309 | g = Math.min(g, 255) 310 | b = Math.min(b, 255) 311 | r = Math.max(r, 0) 312 | g = Math.max(g, 0) 313 | b = Math.max(b, 0) 314 | // console.log('[debug][api]混合颜色', r, g, b); 315 | if (r < 155 && g < 155 && b < 155 && lighter) { 316 | let a = mixColor([r, g, b], [255, 255, 255], 0.45, true, true) as number[] 317 | r = a[0] 318 | g = a[1] 319 | b = a[2] 320 | } 321 | if (needRaw) { 322 | return [r, g, b] 323 | } 324 | return `rgb(${r},${g},${b})` 325 | } 326 | 327 | /* 328 | *----------------------------------------------- 329 | *以下是一些工具函数 330 | *----------------------------------------------- 331 | */ 332 | 333 | // 格式化次数数字,小于10000返回原数字,大于10000小于100000000返回万,大于100000000返回亿 334 | export function formatCount(count: number | string) { 335 | if (typeof count !== 'number') { 336 | return '0'; 337 | } 338 | 339 | if (count < 10000) { 340 | return count.toString(); 341 | } else if (count < 100000000) { 342 | if (count < 100000) { 343 | return (count / 10000).toFixed(1).replace('.0', '') + '万'; 344 | } else { 345 | return (count / 10000).toFixed(0).replace('.0', '') + '万'; 346 | } 347 | } else { 348 | return (count / 100000000).toFixed(1).replace('.0', '') + '亿'; 349 | } 350 | } 351 | 352 | export function textToParsedYrcLine(text: string) { 353 | return { 354 | line: [{ text, duration: 0, time: 0 }], 355 | currentWordIndex: { 356 | wordDuration: 0, 357 | wordIndex: 0 358 | }, 359 | paused: false 360 | } 361 | } 362 | 363 | export function windowBack() { 364 | window.history.back() 365 | } 366 | 367 | // function renderMessage(props) { 368 | // let { type } = props; 369 | // return h( 370 | // NAlert, 371 | // { 372 | // closable: props.closable, 373 | // onClose: props.onClose, 374 | // type: type === "loading" ? "default" : type, 375 | // title: props.title, 376 | // style: { 377 | // boxShadow: "var(--n-box-shadow)", 378 | // maxWidth: "calc(100vw - 32px)", 379 | // } 380 | // }, 381 | // { 382 | // default: () => props.content 383 | // } 384 | // ); 385 | // }; 386 | /** 387 | * @param {string} content 388 | * @param {string} title 389 | */ 390 | export function error(content: string, title?: string) { 391 | // let sad = ["(>_<)", "Σ(°ロ°)", '(つ﹏⊂)', '(・□・;)', '(o.O)', '(#`皿´)', 'ヽ(≧Д≦)ノ', '(>д<)'] 392 | // let title = sad[random(0, sad.length - 1)]; 393 | console.error('[error]', content, title) 394 | // window.$NMessageApi.error(message, { 395 | // // render: (props) => renderMessage({ ...props, title }), 396 | // closable: true, 397 | // duration: 10000 398 | // }) 399 | window.$NNotificationApi.error({ 400 | content, 401 | title, 402 | closable: true, 403 | duration: 10000, 404 | keepAliveOnHover: true 405 | }) 406 | } 407 | export function success(content: string, title?: string) { 408 | // let happy = ["o(≧▽≦)o", "(* ^ ω ^)", "(´。• ω •。`)", "ヽ(・∀・)ノ", "\(≧▽≦)/", "ヽ(o^▽^o)ノ", "\(^ヮ^)/", "(´• ω •`)", "(..>◡<..)"] 409 | // let title = happy[random(0, happy.length - 1)]; 410 | // window.$NMessageApi.success(message, { 411 | // // render: (props) => renderMessage({ ...props, title }), 412 | // closable: false, 413 | // duration: 1500 414 | // }) 415 | window.$NNotificationApi.success({ 416 | content, 417 | title, 418 | closable: true, 419 | duration: 1500, 420 | keepAliveOnHover: true 421 | }) 422 | } 423 | export function msToText(ms: number) { 424 | let m: number | string = Math.floor(ms / 60000) 425 | let s: number | string = Math.floor((ms / 1000) % 60) 426 | if (m < 10) { 427 | m = '0' + m 428 | } 429 | if (s < 10) { 430 | s = '0' + s 431 | } 432 | return `${m}:${s}` 433 | } 434 | export function parseArtist(arObj) { 435 | let ar = arObj.map((item) => item.name) 436 | return ar.join('、') 437 | } 438 | /** 439 | * 把一个对象合并到目标对象 这个函数是多级递归合并 440 | * @param {Object} target 要合并到的对象 441 | * @param {Object} source 源对象 442 | * @returns 443 | */ 444 | export function objDeepMerge(target, source) { 445 | for (const key in source) { 446 | if (source.hasOwnProperty(key)) { 447 | if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { 448 | if (!target[key]) { 449 | target[key] = {} 450 | } 451 | objDeepMerge(target[key], source[key]) 452 | } else { 453 | target[key] = source[key] 454 | } 455 | } 456 | } 457 | return target 458 | } 459 | /**把简单数组用'、'连起来 会判断是否为数组 不是的话会返回空文本 460 | * @param {Array} array 461 | */ 462 | export function parseArray(array) { 463 | if (Array.isArray(array) == true) { 464 | return array.join('、') 465 | } else { 466 | return '' 467 | } 468 | } 469 | /** 470 | * 把两个包含id属性对象数组合并 按照id对应合并 471 | * @param {Array} arr1 472 | * @param {Array} arr2 473 | */ 474 | export function mergeMusicObjArrs(arr1, arr2) { 475 | return arr1.map((item) => { 476 | let obj = arr2.find((item1) => item1.id == item.id) 477 | return obj ? { ...item, ...obj } : item 478 | }) 479 | } 480 | export function random(min, max) { 481 | return Math.floor(Math.random() * (max - min + 1) + min) 482 | } 483 | 484 | /** 485 | * 防抖函数 486 | * @param {function} fn 487 | * @param {number} delay 488 | * @param {0|1} mode 0=每delay毫秒执行一次 1=按照delay执行最后一次 489 | * @returns {function} 490 | */ 491 | export function debounce(fn, delay, mode = 0) { 492 | let timer 493 | switch (mode) { 494 | case 0: 495 | return function (...params) { 496 | if (!timer) { 497 | timer = setTimeout(() => { 498 | timer = null 499 | fn(...params) 500 | }, delay) 501 | } 502 | } 503 | 504 | case 1: 505 | return function (...params) { 506 | clearTimeout(timer) 507 | timer = setTimeout(() => { 508 | fn(...params) 509 | }, delay) 510 | } 511 | } 512 | } 513 | 514 | async function downloadFileLegacy(url: string, fileName: string, onProgress?: (progressEvent: AxiosProgressEvent) => void) { 515 | const response = await axios.get(url, { responseType: 'blob', onDownloadProgress: onProgress }); 516 | const objurl = URL.createObjectURL(response.data); 517 | const a = document.createElement('a'); 518 | a.style.display = 'none'; 519 | a.href = objurl; 520 | a.download = fileName; 521 | document.body.appendChild(a); 522 | a.click(); 523 | document.body.removeChild(a); 524 | URL.revokeObjectURL(url); 525 | return true 526 | } 527 | export async function downloadFile(url: string, fileName: string, onProgress?: (progressEvent: AxiosProgressEvent) => void, dirHandle?: FileSystemDirectoryHandle) { 528 | try { 529 | if (dirHandle) { 530 | // const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' }) 531 | const fileHandle = await dirHandle.getFileHandle(fileName, { create: true }) 532 | const writable = await fileHandle.createWritable() 533 | const res = await fetch(url); 534 | const reader = res.body.getReader(); 535 | const total = parseInt(res.headers.get('Content-Length')) 536 | let loaded = 0 537 | while (1) { 538 | // 读取数据流的第一块数据,done表示数据流是否完成,value表示当前的数 539 | const { done, value } = await reader.read(); 540 | if (done) break; 541 | writable.write(value); 542 | 543 | loaded += value.length 544 | let e: AxiosProgressEvent = { 545 | loaded, 546 | total, 547 | bytes: value.length, 548 | lengthComputable: true 549 | } 550 | if (typeof onProgress == 'function') { 551 | onProgress(e) 552 | } 553 | } 554 | writable.close(); 555 | } else { 556 | await downloadFileLegacy(url, fileName, onProgress) 557 | } 558 | return true 559 | } 560 | catch (e) { 561 | error(JSON.stringify(e), '文件下载失败') 562 | return false 563 | } 564 | } 565 | 566 | //把api返回的detail内容转换为播放列表的存储形式 567 | export function parseDetailToList(data) { 568 | return data.map((item) => { 569 | return { 570 | id: item.id, 571 | name: item.name, 572 | artist: item.ar.map(item => item.name).join('、'), 573 | picurl: item.al.picUrl, 574 | tns: parseArray(item.tns), 575 | fee: item.fee, 576 | } 577 | }); 578 | } 579 | 580 | export const areaData = { 581 | province: { 582 | 110000: '北京市', 583 | 120000: '天津市', 584 | 130000: '河北省', 585 | 140000: '山西省', 586 | 150000: '内蒙古自治区', 587 | 210000: '辽宁省', 588 | 220000: '吉林省', 589 | 230000: '黑龙江省', 590 | 310000: '上海市', 591 | 320000: '江苏省', 592 | 330000: '浙江省', 593 | 340000: '安徽省', 594 | 350000: '福建省', 595 | 360000: '江西省', 596 | 370000: '山东省', 597 | 410000: '河南省', 598 | 420000: '湖北省', 599 | 430000: '湖南省', 600 | 440000: '广东省', 601 | 450000: '广西壮族自治区', 602 | 460000: '海南省', 603 | 500000: '重庆市', 604 | 510000: '四川省', 605 | 520000: '贵州省', 606 | 530000: '云南省', 607 | 540000: '西藏自治区', 608 | 610000: '陕西省', 609 | 620000: '甘肃省', 610 | 630000: '青海省', 611 | 640000: '宁夏回族自治区', 612 | 650000: '新疆维吾尔自治区' 613 | }, 614 | city: { 615 | 110101: '东城区', 616 | 110102: '西城区', 617 | 110105: '朝阳区', 618 | 110106: '丰台区', 619 | 110107: '石景山区', 620 | 110108: '海淀区', 621 | 110109: '门头沟区', 622 | 110111: '房山区', 623 | 110112: '通州区', 624 | 110113: '顺义区', 625 | 110114: '昌平区', 626 | 110115: '大兴区', 627 | 110116: '怀柔区', 628 | 110117: '平谷区', 629 | 110118: '密云区', 630 | 110119: '延庆区', 631 | 120101: '和平区', 632 | 120102: '河东区', 633 | 120103: '河西区', 634 | 120104: '南开区', 635 | 120105: '河北区', 636 | 120106: '红桥区', 637 | 120110: '东丽区', 638 | 120111: '西青区', 639 | 120112: '津南区', 640 | 120113: '北辰区', 641 | 120114: '武清区', 642 | 120115: '宝坻区', 643 | 120116: '滨海新区', 644 | 120117: '宁河区', 645 | 120118: '静海区', 646 | 120119: '蓟州区', 647 | 130100: '石家庄市', 648 | 130200: '唐山市', 649 | 130300: '秦皇岛市', 650 | 130400: '邯郸市', 651 | 130500: '邢台市', 652 | 130600: '保定市', 653 | 130700: '张家口市', 654 | 130800: '承德市', 655 | 130900: '沧州市', 656 | 131000: '廊坊市', 657 | 131100: '衡水市', 658 | 140100: '太原市', 659 | 140200: '大同市', 660 | 140300: '阳泉市', 661 | 140400: '长治市', 662 | 140500: '晋城市', 663 | 140600: '朔州市', 664 | 140700: '晋中市', 665 | 140800: '运城市', 666 | 140900: '忻州市', 667 | 141000: '临汾市', 668 | 141100: '吕梁市', 669 | 150100: '呼和浩特市', 670 | 150200: '包头市', 671 | 150300: '乌海市', 672 | 150400: '赤峰市', 673 | 150500: '通辽市', 674 | 150600: '鄂尔多斯市', 675 | 150700: '呼伦贝尔市', 676 | 150800: '巴彦淖尔市', 677 | 150900: '乌兰察布市', 678 | 152200: '兴安盟', 679 | 152500: '锡林郭勒盟', 680 | 152900: '阿拉善盟', 681 | 210100: '沈阳市', 682 | 210200: '大连市', 683 | 210300: '鞍山市', 684 | 210400: '抚顺市', 685 | 210500: '本溪市', 686 | 210600: '丹东市', 687 | 210700: '锦州市', 688 | 210800: '营口市', 689 | 210900: '阜新市', 690 | 211000: '辽阳市', 691 | 211100: '盘锦市', 692 | 211200: '铁岭市', 693 | 211300: '朝阳市', 694 | 211400: '葫芦岛市', 695 | 220100: '长春市', 696 | 220200: '吉林市', 697 | 220300: '四平市', 698 | 220400: '辽源市', 699 | 220500: '通化市', 700 | 220600: '白山市', 701 | 220700: '松原市', 702 | 220800: '白城市', 703 | 222400: '延边朝鲜族自治州', 704 | 230100: '哈尔滨市', 705 | 230200: '齐齐哈尔市', 706 | 230300: '鸡西市', 707 | 230400: '鹤岗市', 708 | 230500: '双鸭山市', 709 | 230600: '大庆市', 710 | 230700: '伊春市', 711 | 230800: '佳木斯市', 712 | 230900: '七台河市', 713 | 231000: '牡丹江市', 714 | 231100: '黑河市', 715 | 231200: '绥化市', 716 | 232700: '大兴安岭地区', 717 | 310101: '黄浦区', 718 | 310104: '徐汇区', 719 | 310105: '长宁区', 720 | 310106: '静安区', 721 | 310107: '普陀区', 722 | 310109: '虹口区', 723 | 310110: '杨浦区', 724 | 310112: '闵行区', 725 | 310113: '宝山区', 726 | 310114: '嘉定区', 727 | 310115: '浦东新区', 728 | 310116: '金山区', 729 | 310117: '松江区', 730 | 310118: '青浦区', 731 | 310120: '奉贤区', 732 | 310151: '崇明区', 733 | 320100: '南京市', 734 | 320200: '无锡市', 735 | 320300: '徐州市', 736 | 320400: '常州市', 737 | 320500: '苏州市', 738 | 320600: '南通市', 739 | 320700: '连云港市', 740 | 320800: '淮安市', 741 | 320900: '盐城市', 742 | 321000: '扬州市', 743 | 321100: '镇江市', 744 | 321200: '泰州市', 745 | 321300: '宿迁市', 746 | 330100: '杭州市', 747 | 330200: '宁波市', 748 | 330300: '温州市', 749 | 330400: '嘉兴市', 750 | 330500: '湖州市', 751 | 330600: '绍兴市', 752 | 330700: '金华市', 753 | 330800: '衢州市', 754 | 330900: '舟山市', 755 | 331000: '台州市', 756 | 331100: '丽水市', 757 | 340100: '合肥市', 758 | 340200: '芜湖市', 759 | 340300: '蚌埠市', 760 | 340400: '淮南市', 761 | 340500: '马鞍山市', 762 | 340600: '淮北市', 763 | 340700: '铜陵市', 764 | 340800: '安庆市', 765 | 341000: '黄山市', 766 | 341100: '滁州市', 767 | 341200: '阜阳市', 768 | 341300: '宿州市', 769 | 341500: '六安市', 770 | 341600: '亳州市', 771 | 341700: '池州市', 772 | 341800: '宣城市', 773 | 350100: '福州市', 774 | 350200: '厦门市', 775 | 350300: '莆田市', 776 | 350400: '三明市', 777 | 350500: '泉州市', 778 | 350600: '漳州市', 779 | 350700: '南平市', 780 | 350800: '龙岩市', 781 | 350900: '宁德市', 782 | 360100: '南昌市', 783 | 360200: '景德镇市', 784 | 360300: '萍乡市', 785 | 360400: '九江市', 786 | 360500: '新余市', 787 | 360600: '鹰潭市', 788 | 360700: '赣州市', 789 | 360800: '吉安市', 790 | 360900: '宜春市', 791 | 361000: '抚州市', 792 | 361100: '上饶市', 793 | 370100: '济南市', 794 | 370200: '青岛市', 795 | 370300: '淄博市', 796 | 370400: '枣庄市', 797 | 370500: '东营市', 798 | 370600: '烟台市', 799 | 370700: '潍坊市', 800 | 370800: '济宁市', 801 | 370900: '泰安市', 802 | 371000: '威海市', 803 | 371100: '日照市', 804 | 371300: '临沂市', 805 | 371400: '德州市', 806 | 371500: '聊城市', 807 | 371600: '滨州市', 808 | 371700: '菏泽市', 809 | 410100: '郑州市', 810 | 410200: '开封市', 811 | 410300: '洛阳市', 812 | 410400: '平顶山市', 813 | 410500: '安阳市', 814 | 410600: '鹤壁市', 815 | 410700: '新乡市', 816 | 410800: '焦作市', 817 | 410900: '濮阳市', 818 | 411000: '许昌市', 819 | 411100: '漯河市', 820 | 411200: '三门峡市', 821 | 411300: '南阳市', 822 | 411400: '商丘市', 823 | 411500: '信阳市', 824 | 411600: '周口市', 825 | 411700: '驻马店市', 826 | 419001: '济源市', 827 | 420100: '武汉市', 828 | 420200: '黄石市', 829 | 420300: '十堰市', 830 | 420500: '宜昌市', 831 | 420600: '襄阳市', 832 | 420700: '鄂州市', 833 | 420800: '荆门市', 834 | 420900: '孝感市', 835 | 421000: '荆州市', 836 | 421100: '黄冈市', 837 | 421200: '咸宁市', 838 | 421300: '随州市', 839 | 422800: '恩施土家族苗族自治州', 840 | 429004: '仙桃市', 841 | 429005: '潜江市', 842 | 429006: '天门市', 843 | 429021: '神农架林区', 844 | 430100: '长沙市', 845 | 430200: '株洲市', 846 | 430300: '湘潭市', 847 | 430400: '衡阳市', 848 | 430500: '邵阳市', 849 | 430600: '岳阳市', 850 | 430700: '常德市', 851 | 430800: '张家界市', 852 | 430900: '益阳市', 853 | 431000: '郴州市', 854 | 431100: '永州市', 855 | 431200: '怀化市', 856 | 431300: '娄底市', 857 | 433100: '湘西土家族苗族自治州', 858 | 440100: '广州市', 859 | 440200: '韶关市', 860 | 440300: '深圳市', 861 | 440400: '珠海市', 862 | 440500: '汕头市', 863 | 440600: '佛山市', 864 | 440700: '江门市', 865 | 440800: '湛江市', 866 | 440900: '茂名市', 867 | 441200: '肇庆市', 868 | 441300: '惠州市', 869 | 441400: '梅州市', 870 | 441500: '汕尾市', 871 | 441600: '河源市', 872 | 441700: '阳江市', 873 | 441800: '清远市', 874 | 441900: '东莞市', 875 | 442000: '中山市', 876 | 445100: '潮州市', 877 | 445200: '揭阳市', 878 | 445300: '云浮市', 879 | 450100: '南宁市', 880 | 450200: '柳州市', 881 | 450300: '桂林市', 882 | 450400: '梧州市', 883 | 450500: '北海市', 884 | 450600: '防城港市', 885 | 450700: '钦州市', 886 | 450800: '贵港市', 887 | 450900: '玉林市', 888 | 451000: '百色市', 889 | 451100: '贺州市', 890 | 451200: '河池市', 891 | 451300: '来宾市', 892 | 451400: '崇左市', 893 | 460100: '海口市', 894 | 460200: '三亚市', 895 | 460300: '三沙市', 896 | 460400: '儋州市', 897 | 469001: '五指山市', 898 | 469002: '琼海市', 899 | 469005: '文昌市', 900 | 469006: '万宁市', 901 | 469007: '东方市', 902 | 469021: '定安县', 903 | 469022: '屯昌县', 904 | 469023: '澄迈县', 905 | 469024: '临高县', 906 | 469025: '白沙黎族自治县', 907 | 469026: '昌江黎族自治县', 908 | 469027: '乐东黎族自治县', 909 | 469028: '陵水黎族自治县', 910 | 469029: '保亭黎族苗族自治县', 911 | 469030: '琼中黎族苗族自治县', 912 | 500101: '万州区', 913 | 500102: '涪陵区', 914 | 500103: '渝中区', 915 | 500104: '大渡口区', 916 | 500105: '江北区', 917 | 500106: '沙坪坝区', 918 | 500107: '九龙坡区', 919 | 500108: '南岸区', 920 | 500109: '北碚区', 921 | 500110: '綦江区', 922 | 500111: '大足区', 923 | 500112: '渝北区', 924 | 500113: '巴南区', 925 | 500114: '黔江区', 926 | 500115: '长寿区', 927 | 500116: '江津区', 928 | 500117: '合川区', 929 | 500118: '永川区', 930 | 500119: '南川区', 931 | 500120: '璧山区', 932 | 500151: '铜梁区', 933 | 500152: '潼南区', 934 | 500153: '荣昌区', 935 | 500154: '开州区', 936 | 500155: '梁平区', 937 | 500156: '武隆区', 938 | 500229: '城口县', 939 | 500230: '丰都县', 940 | 500231: '垫江县', 941 | 500233: '忠县', 942 | 500235: '云阳县', 943 | 500236: '奉节县', 944 | 500237: '巫山县', 945 | 500238: '巫溪县', 946 | 500240: '石柱土家族自治县', 947 | 500241: '秀山土家族苗族自治县', 948 | 500242: '酉阳土家族苗族自治县', 949 | 500243: '彭水苗族土家族自治县', 950 | 510100: '成都市', 951 | 510300: '自贡市', 952 | 510400: '攀枝花市', 953 | 510500: '泸州市', 954 | 510600: '德阳市', 955 | 510700: '绵阳市', 956 | 510800: '广元市', 957 | 510900: '遂宁市', 958 | 511000: '内江市', 959 | 511100: '乐山市', 960 | 511300: '南充市', 961 | 511400: '眉山市', 962 | 511500: '宜宾市', 963 | 511600: '广安市', 964 | 511700: '达州市', 965 | 511800: '雅安市', 966 | 511900: '巴中市', 967 | 512000: '资阳市', 968 | 513200: '阿坝藏族羌族自治州', 969 | 513300: '甘孜藏族自治州', 970 | 513400: '凉山彝族自治州', 971 | 520100: '贵阳市', 972 | 520200: '六盘水市', 973 | 520300: '遵义市', 974 | 520400: '安顺市', 975 | 520500: '毕节市', 976 | 520600: '铜仁市', 977 | 522300: '黔西南布依族苗族自治州', 978 | 522600: '黔东南苗族侗族自治州', 979 | 522700: '黔南布依族苗族自治州', 980 | 530100: '昆明市', 981 | 530300: '曲靖市', 982 | 530400: '玉溪市', 983 | 530500: '保山市', 984 | 530600: '昭通市', 985 | 530700: '丽江市', 986 | 530800: '普洱市', 987 | 530900: '临沧市', 988 | 532300: '楚雄彝族自治州', 989 | 532500: '红河哈尼族彝族自治州', 990 | 532600: '文山壮族苗族自治州', 991 | 532800: '西双版纳傣族自治州', 992 | 532900: '大理白族自治州', 993 | 533100: '德宏傣族景颇族自治州', 994 | 533300: '怒江傈僳族自治州', 995 | 533400: '迪庆藏族自治州', 996 | 540100: '拉萨市', 997 | 540200: '日喀则市', 998 | 540300: '昌都市', 999 | 540400: '林芝市', 1000 | 540500: '山南市', 1001 | 540600: '那曲市', 1002 | 542500: '阿里地区', 1003 | 610100: '西安市', 1004 | 610200: '铜川市', 1005 | 610300: '宝鸡市', 1006 | 610400: '咸阳市', 1007 | 610500: '渭南市', 1008 | 610600: '延安市', 1009 | 610700: '汉中市', 1010 | 610800: '榆林市', 1011 | 610900: '安康市', 1012 | 611000: '商洛市', 1013 | 620100: '兰州市', 1014 | 620200: '嘉峪关市', 1015 | 620300: '金昌市', 1016 | 620400: '白银市', 1017 | 620500: '天水市', 1018 | 620600: '武威市', 1019 | 620700: '张掖市', 1020 | 620800: '平凉市', 1021 | 620900: '酒泉市', 1022 | 621000: '庆阳市', 1023 | 621100: '定西市', 1024 | 621200: '陇南市', 1025 | 622900: '临夏回族自治州', 1026 | 623000: '甘南藏族自治州', 1027 | 630100: '西宁市', 1028 | 630200: '海东市', 1029 | 632200: '海北藏族自治州', 1030 | 632300: '黄南藏族自治州', 1031 | 632500: '海南藏族自治州', 1032 | 632600: '果洛藏族自治州', 1033 | 632700: '玉树藏族自治州', 1034 | 632800: '海西蒙古族藏族自治州', 1035 | 640100: '银川市', 1036 | 640200: '石嘴山市', 1037 | 640300: '吴忠市', 1038 | 640400: '固原市', 1039 | 640500: '中卫市', 1040 | 650100: '乌鲁木齐市', 1041 | 650200: '克拉玛依市', 1042 | 650400: '吐鲁番市', 1043 | 650500: '哈密市', 1044 | 652300: '昌吉回族自治州', 1045 | 652700: '博尔塔拉蒙古自治州', 1046 | 652800: '巴音郭楞蒙古自治州', 1047 | 652900: '阿克苏地区', 1048 | 653000: '克孜勒苏柯尔克孜自治州', 1049 | 653100: '喀什地区', 1050 | 653200: '和田地区', 1051 | 654000: '伊犁哈萨克自治州', 1052 | 654200: '塔城地区', 1053 | 654300: '阿勒泰地区', 1054 | 659001: '石河子市', 1055 | 659002: '阿拉尔市', 1056 | 659003: '图木舒克市', 1057 | 659004: '五家渠市', 1058 | 659005: '北屯市', 1059 | 659006: '铁门关市', 1060 | 659007: '双河市', 1061 | 659008: '可克达拉市', 1062 | 659009: '昆玉市', 1063 | 659010: '胡杨河市', 1064 | 659011: '新星市', 1065 | 659012: '白杨市' 1066 | } 1067 | } 1068 | -------------------------------------------------------------------------------- /src/renderer/src/modules/lyric.ts: -------------------------------------------------------------------------------- 1 | import type { LyricLine, LyricWord } from './types/lyric.d.ts'; 2 | export function parseYrc(yrc: string) { 3 | let lyric: LyricLine[] = []; 4 | yrc.split('\n').forEach((line) => { 5 | let lrcline: LyricLine; 6 | let regres = /\[(\d+),(\d+)\](.*)$/.exec(line); 7 | if (regres !== null) { 8 | lrcline = { 9 | time: regres?.[1], 10 | lrc: [] 11 | }; 12 | let lineText = regres?.[3]; 13 | // 正则匹配模式:捕获 start, duration 和 data 部分,使用非贪婪匹配 14 | const regex = /\((\d+),(\d+),\d+\)(.*?)(?=\(|$)/g; 15 | while ((regres = regex.exec(lineText)) !== null) { 16 | lrcline.lrc.push({ 17 | time: regres[1], 18 | duration: regres[2], 19 | text: regres[3] 20 | }); 21 | } 22 | lyric.push(lrcline); 23 | } 24 | }) 25 | return lyric; 26 | } 27 | 28 | export function parseLrc(lrc: string) { 29 | let lyric: LyricLine[] = []; 30 | lrc.split('\n').forEach(line => { 31 | let linetext = lrcToLyric(line); 32 | let linetime = lrcToMS(line); 33 | if (linetime !== null) { 34 | lyric.push({ 35 | time: linetime, 36 | lrc: [ 37 | { 38 | time: linetime, 39 | duration: '0', 40 | text: linetext 41 | } 42 | ] 43 | }); 44 | } 45 | }); 46 | return lyric; 47 | } 48 | 49 | export function parseSecondaryLrc(secondaryLrc: string, parsedLyric: LyricLine[], keyName: 'tran' | 'roma') { 50 | // console.log('secondaryLrc解析', keyName); 51 | 52 | let lyric = parsedLyric.map(item => ({ ...item })); 53 | secondaryLrc.split('\n').forEach(line => { 54 | let linetext = lrcToLyric(line); 55 | if (linetext) { 56 | let linetime = lrcToMS(line); 57 | let lineIndex = lyric.findIndex(item => item.time == linetime); 58 | if (lineIndex >= 0) {// 找到了 59 | lyric[lineIndex][keyName] = linetext; 60 | } 61 | } 62 | }); 63 | return lyric; 64 | } 65 | 66 | /** 67 | 获取一行lrc的第一个时间标签,并转换为毫秒 68 | @param lyricLine 一行歌词 69 | */ 70 | function lrcToMS(lyricLine: string) { 71 | let express = /\[(\d+)[:.](\d+)[:.](\d+)\]/ 72 | let lineTime = express.exec(lyricLine); 73 | if (lineTime == null) { 74 | return null; 75 | } 76 | if (lineTime[3].length == 1) { 77 | lineTime[3] = '0' + lineTime[3]; 78 | } 79 | return String((parseInt(lineTime[1]) * 60 + parseInt(lineTime[2])) * 1000 + parseInt(lineTime[3].slice(0, 2)) * 10); 80 | } 81 | /** 82 | * 获取一行lrc中的歌词文本 83 | * @param lyricLine 一行lrc 84 | */ 85 | function lrcToLyric(lyricLine: string) { 86 | let express = /\[\d+[:.]\d+[:.]\d+\](.*)/ 87 | let lineTime = express.exec(lyricLine); 88 | if (lineTime == null) { 89 | return ''; 90 | } 91 | return lineTime[1]; 92 | } 93 | -------------------------------------------------------------------------------- /src/renderer/src/modules/messageApi.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/renderer/src/modules/modalApi.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/renderer/src/modules/module.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@/stores/*.js' 2 | declare module '~icons/*' 3 | // declare module '@/utils/*' 4 | declare module '@/components/*' 5 | declare module '@/pages/*' 6 | // declare module '@/modules/*' 7 | /// -------------------------------------------------------------------------------- /src/renderer/src/modules/notificationApi.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/renderer/src/modules/types/lyric.d.ts: -------------------------------------------------------------------------------- 1 | export interface LyricWord { 2 | time: number, 3 | duration: number, 4 | text: string, 5 | } 6 | export interface LyricLine { 7 | time: number, 8 | lrc: LyricWord[], 9 | tran?: string, 10 | roma?: string, 11 | } -------------------------------------------------------------------------------- /src/renderer/src/modules/types/song.d.ts: -------------------------------------------------------------------------------- 1 | import { LyricLine } from "./lyric.js"; 2 | 3 | export interface song { 4 | id: string; 5 | name: string; 6 | artist: string; 7 | picurl: string; 8 | url?: string; 9 | tns?: string; 10 | fee?: string; 11 | album?: string; 12 | lyric?: LyricLine[]; 13 | } -------------------------------------------------------------------------------- /src/renderer/src/pages/account.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | -------------------------------------------------------------------------------- /src/renderer/src/pages/comments.vue: -------------------------------------------------------------------------------- 1 | 25 | 79 | -------------------------------------------------------------------------------- /src/renderer/src/pages/container.vue: -------------------------------------------------------------------------------- 1 | 34 | 39 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /src/renderer/src/pages/login.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 163 | 164 | -------------------------------------------------------------------------------- /src/renderer/src/pages/playlist.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 104 | 105 | -------------------------------------------------------------------------------- /src/renderer/src/pages/search.vue: -------------------------------------------------------------------------------- 1 | 17 | 63 | -------------------------------------------------------------------------------- /src/renderer/src/pages/setting.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 144 | 145 | -------------------------------------------------------------------------------- /src/renderer/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | const router = createRouter({ 3 | history: createWebHashHistory(), 4 | routes: [ 5 | { 6 | name: 'home', 7 | path: '/', 8 | component: () => import('@/pages/home.vue'), 9 | props: (route) => { 10 | return route.query 11 | }, 12 | meta: { 13 | keepAlive: true 14 | } 15 | }, 16 | { 17 | name: 'search', 18 | path: '/search', 19 | component: () => import('@/pages/search.vue'), 20 | props: (route) => { 21 | return route.query 22 | } 23 | // meta: { 24 | // keepAlive: true 25 | // } 26 | }, 27 | { 28 | name: 'login', 29 | path: '/login', 30 | component: () => import('@/pages/login.vue'), 31 | props: (route) => { 32 | return route.query 33 | } 34 | }, 35 | { 36 | name: 'account', 37 | path: '/account', 38 | component: () => import('@/pages/account.vue'), 39 | props: (route) => { 40 | return route.query 41 | }, 42 | meta: { 43 | keepAlive: true 44 | } 45 | }, 46 | { 47 | name: 'playlist', 48 | path: '/playlist', 49 | component: () => import('@/pages/playlist.vue'), 50 | props: (route) => { 51 | return route.query 52 | } 53 | // meta: { 54 | // keepAlive: true 55 | // } 56 | }, 57 | { 58 | name: 'setting', 59 | path: '/setting', 60 | component: () => import('@/pages/setting.vue'), 61 | props: (route) => { 62 | return route.query 63 | } 64 | // meta: { 65 | // keepAlive: true 66 | // } 67 | }, 68 | { 69 | name: 'comments', 70 | path: '/comments', 71 | component: () => import('@/pages/comments.vue'), 72 | props: (route) => { 73 | return route.query 74 | } 75 | } 76 | ] 77 | }) 78 | 79 | export default router 80 | -------------------------------------------------------------------------------- /src/renderer/src/stores/download.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useSettingStore } from '@/stores/setting.js' 3 | import pinia from '@/stores/index.js' 4 | import * as api from '@/modules/api.js' 5 | import { song } from "@/modules/types/song.js"; 6 | 7 | interface dItem { 8 | id: string; 9 | loaded?: number; 10 | total?: number; 11 | status: 'waiting' | 'finished' | 'error'; 12 | detail: song; 13 | } 14 | 15 | 16 | 17 | export const useDownloadStore = defineStore("download", { 18 | state: () => ({ 19 | list: [] as dItem[], 20 | 21 | // 下载器工作状态 22 | status: 'waiting' as 'waiting' | 'working', 23 | current: -1, 24 | dirHandle: null as FileSystemDirectoryHandle | null, 25 | }), 26 | actions: { 27 | startTimer() { 28 | setInterval(() => { 29 | if (this.status === 'waiting') { 30 | let wIndex = this.list.findIndex(item => (item.status === 'waiting')) 31 | if (wIndex >= 0) { 32 | this.down(wIndex) 33 | } 34 | } 35 | }, 2500) 36 | }, 37 | 38 | async down(index: number) { 39 | try { 40 | if (index < 0 || index >= this.list.length) return // 数组超限 41 | if ('showDirectoryPicker' in window) { 42 | this.dirHandle = this.dirHandle || await window.showDirectoryPicker({ mode: 'readwrite' }); 43 | } 44 | this.status = 'working' 45 | this.current = index 46 | 47 | let data = this.list[index] 48 | let settingStore = useSettingStore(pinia); 49 | 50 | let res = await api.songUrlV1(data.id, settingStore.musicLevel, localStorage.getItem('specialApi'), localStorage.getItem('cookie')) 51 | let url = res.data?.data?.[0]?.url 52 | let type = res.data?.data?.[0]?.type ?? 'mp3' 53 | console.log('⬇开始下载', `${data.detail.artist} - ${data.detail.name}.${type}`); 54 | await api.downloadFile(url, `${data.detail.artist} - ${data.detail.name}.${type}`, undefined, this.dirHandle) 55 | } catch (e) { 56 | console.log('下载出现错误', e) 57 | this.list[index].status = 'error' 58 | } 59 | this.list[index].status = 'finished' 60 | this.status = 'waiting' 61 | }, 62 | async addDownloadItemByIds(ids: string[]) { 63 | if (!ids.length) return 64 | 65 | let data = [] 66 | 67 | let res = await api.songDetail(ids.join(',')) 68 | data = api.parseDetailToList(res.data.songs) 69 | data.forEach(item => { 70 | this.list.push({ 71 | id: item.id, 72 | detail: item, 73 | status: 'waiting' 74 | }) 75 | }) 76 | } 77 | } 78 | }) 79 | 80 | useDownloadStore().startTimer() -------------------------------------------------------------------------------- /src/renderer/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const pinia = createPinia() 4 | 5 | export default pinia 6 | -------------------------------------------------------------------------------- /src/renderer/src/stores/play.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import * as api from '@/modules/api.js' 3 | import * as lyricTools from '@/modules/lyric.js' 4 | import { ref, computed } from 'vue' 5 | import { useUserStore } from "./user.js"; 6 | import { useSettingStore } from "./setting.js"; 7 | import { song } from "@/modules/types/song.js"; 8 | 9 | export const usePlayStore = defineStore('play', () => { 10 | // console.log('playstore被创建 '); 11 | let player = ref(new Audio()); 12 | // player.value.crossOrigin = 'anonymous'; 13 | window.player = player.value; 14 | let lyricIndexNow = ref({ lineIndex: -1, wordIndex: -1, wordDuration: 0 });//内部变量 供给下面的计算属性使用 15 | let currentMusic = computed(() => { 16 | let userStore = useUserStore(); 17 | return { 18 | id: '0', 19 | picurl: '/icon.png', 20 | name: '暂无歌曲', 21 | artist: '', 22 | ...playlist.value[playlistIndex.value],//以上默认内容会被覆盖 23 | isLiked: userStore.likedSongs.includes(playlist.value[playlistIndex.value]?.id), 24 | currentLyricIndex: lyricIndexNow.value 25 | } 26 | }) 27 | 28 | let musicStatus = ref({ duration: 0, currentTime: 0, paused: true }) 29 | // 播放列表 30 | //{id,name,artist,tns,url,picurl,?lyric} 31 | let playlist = ref([]) 32 | // 播放列表索引 33 | let playlistIndex = ref(0) 34 | // 随机播放的顺序 35 | // 存储格式为number数组 代表对应index需要播放的播放列表里的歌曲 36 | let playOrder = ref([]) 37 | let playOrderIndex = computed(() => { 38 | return playOrder.value.findIndex(v => v == playlistIndex.value); 39 | }) 40 | let playMode = ref(0);//0为顺序播放 1为随机播放 41 | let nameWithTns = computed(() => { 42 | let tns = currentMusic.value.tns; 43 | let name = currentMusic.value.name; 44 | if (tns) { 45 | return `${name}   (${tns})` 46 | } else { 47 | return name; 48 | } 49 | }) 50 | 51 | // 设置标题 52 | watchEffect(() => { 53 | if (currentMusic.value.id != '0') { 54 | document.title = `${currentMusic.value.name} - ${currentMusic.value.artist} - 奶酪音乐`; 55 | } 56 | }) 57 | 58 | // 设置媒体会话的动作 59 | if ("mediaSession" in navigator) { 60 | navigator.mediaSession.setActionHandler("previoustrack", () => prev()); 61 | navigator.mediaSession.setActionHandler("nexttrack", () => next()); 62 | navigator.mediaSession.setActionHandler("play", () => play()); 63 | navigator.mediaSession.setActionHandler("pause", () => pause()); 64 | try { 65 | navigator.mediaSession.setActionHandler("seekto", (conf) => { 66 | seek(conf.seekTime) 67 | }); 68 | } catch (e) { 69 | console.log('媒体会话的seekto动作在当前浏览器有兼容性问题'); 70 | } 71 | } 72 | //添加监听事件 用来更新播放进度状态 73 | //注意:下面的歌词更新事件也会触发此函数 74 | player.value.addEventListener('timeupdate', () => { updateProgress() }) 75 | //添加监听事件 用来更新播放进度状态 和上面的区别是 这些事件需要交到mediaSession 76 | let eventsNeedUpdate = ['play', 'pause', 'ended', 'playing', 'waiting', 'ratechange', 'durationchange'] 77 | for (let i = 0; i < eventsNeedUpdate.length; i++) { 78 | player.value.addEventListener(eventsNeedUpdate[i], () => { updateProgress(true) }) 79 | } 80 | 81 | //播放结束后自动下一曲 82 | player.value.addEventListener('ended', () => { next() }) 83 | 84 | interface updateProgressConf { 85 | duration?: number 86 | playbackRate?: number 87 | position?: number 88 | } 89 | /** 同步播放进度状态 是回调函数 90 | * @param {boolean} updateSession - 是否更新媒体会话 91 | * @param {Object} conf - 仅参数一为true生效 媒体会话的配置对象 92 | */ 93 | function updateProgress(updateSession = false, conf: updateProgressConf = { duration: NaN, playbackRate: NaN, position: NaN }) { 94 | if (updateSession) { 95 | if ("mediaSession" in navigator) { 96 | //conf是媒体会话的配置对象 这里只是配置 下面才会应用 97 | conf.duration = conf.duration || player.value.duration 98 | if (!conf.duration) { 99 | conf.duration = 114.5141919 100 | } 101 | conf.playbackRate = conf.playbackRate || player.value.playbackRate || 1 102 | conf.position = conf.position || player.value.currentTime || 0 103 | if (conf.duration == 114.5141919) {//这么臭的数 想必正常情况不会出现吧(智将 104 | setTimeout(() => {//如果为114.5141919,则说明没有获取到duration,则延迟2秒再获取一次 105 | updateProgress(true); 106 | }, 2000); 107 | } 108 | //把配置对象传给媒体会话 109 | if ('setPositionState' in navigator.mediaSession) { 110 | if (!(isNaN(conf.duration) || isNaN(conf.position) || isNaN(conf.playbackRate))) { 111 | navigator.mediaSession.setPositionState(conf); 112 | } 113 | } 114 | //设置播放状态 是否暂停 115 | if (player.value.paused === true || player.value.paused === false) { 116 | navigator.mediaSession.playbackState = player.value.paused ? "paused" : "playing"; 117 | } else { 118 | navigator.mediaSession.playbackState = "paused"; 119 | } 120 | // ElMessage({ 121 | // message: JSON.stringify(conf) + navigator.mediaSession.playbackState, 122 | // type: "success", 123 | // duration: 10000, 124 | // }) 125 | } 126 | 127 | } 128 | 129 | musicStatus.value.paused = player.value.paused 130 | musicStatus.value.duration = player.value.duration 131 | musicStatus.value.currentTime = player.value.currentTime 132 | 133 | } 134 | //切歌后操作 需要手动调用 135 | function musicChanged() { 136 | if (playlist.value.length == 0) { 137 | return; 138 | } 139 | lyricIndexNow.value.lineIndex = -1; 140 | let value = currentMusic.value 141 | parseLyric() 142 | if (window.isElectron) { 143 | //如果是electron环境 就发送歌名给桌面歌词 144 | window.api.sendLyric(JSON.stringify({ 145 | time: 0, 146 | lrc: api.textToParsedYrcLine(value?.name), 147 | roma: api.textToParsedYrcLine(value?.artist), 148 | tran: api.textToParsedYrcLine(value?.artist) 149 | })) 150 | } 151 | if ("mediaSession" in navigator) {//更新session元数据信息 152 | navigator.mediaSession.metadata = null 153 | navigator.mediaSession.metadata = new MediaMetadata({ 154 | title: value.name, 155 | artist: value.artist, 156 | artwork: [ 157 | { src: value.picurl + '?param=96y96', sizes: '96x96' }, 158 | { src: value.picurl + '?param=128y128', sizes: '128x128' }, 159 | { src: value.picurl + '?param=192y192', sizes: '192x192' }, 160 | { src: value.picurl + '?param=256y256', sizes: '256x256' }, 161 | { src: value.picurl + '?param=512y512', sizes: '512x512' } 162 | ] 163 | }) 164 | } 165 | updateProgress(true, { position: 0, duration: player.value.duration }); 166 | //保存当前播放列表 167 | save(); 168 | 169 | } 170 | //获取并解析歌词 171 | async function parseLyric() { 172 | if (!('lyric' in currentMusic.value)) {//如果没有保存的数据才去请求 173 | let apiResult: any = await api.lyricNew(currentMusic.value.id) 174 | apiResult = apiResult.data; 175 | let lyric = []; 176 | if (apiResult.code == 200) { 177 | if (apiResult?.yrc?.lyric) { 178 | lyric = lyricTools.parseYrc(apiResult.yrc.lyric); 179 | if (apiResult?.yromalrc?.lyric) { 180 | lyric = lyricTools.parseSecondaryLrc(apiResult.yromalrc.lyric, lyric, 'roma'); 181 | } 182 | if (apiResult?.ytlrc?.lyric) { 183 | lyric = lyricTools.parseSecondaryLrc(apiResult.ytlrc.lyric, lyric, 'tran'); 184 | } 185 | 186 | } else if (apiResult?.lrc?.lyric) { 187 | lyric = lyricTools.parseLrc(apiResult.lrc.lyric); 188 | if (apiResult?.romalrc?.lyric) { 189 | lyric = lyricTools.parseSecondaryLrc(apiResult.romalrc.lyric, lyric, 'roma'); 190 | } 191 | if (apiResult?.tlyric?.lyric) { 192 | lyric = lyricTools.parseSecondaryLrc(apiResult.tlyric.lyric, lyric, 'tran'); 193 | } 194 | } 195 | } 196 | // console.log(lyric); 197 | playlist.value[playlistIndex.value].lyric = lyric;//最终赋值 198 | } 199 | } 200 | //获取并应用歌曲url 201 | async function getAudioUrl(id) { 202 | let settingStore = useSettingStore(); 203 | let res = await api.songUrlV1(id, settingStore.musicLevel, localStorage.getItem('specialApi'), localStorage.getItem('cookie')); 204 | let d = res.data.data[0].url; 205 | player.value.src = d; 206 | return d; 207 | } 208 | 209 | //清除列表 使用新的列表替换 参数一二选择一个传入 210 | async function playlistInit(ids = null, dataFromApi = null) { 211 | stop() 212 | playlist.value = []; 213 | playlistIndex.value = -1; 214 | playOrder.value = []; 215 | if (ids == null) {//没传递id列表 216 | let storageNow = JSON.parse(localStorage.getItem('playlist') || '{}') 217 | if (dataFromApi) {//传入了api数据 218 | playlist.value = api.parseDetailToList(dataFromApi); 219 | listRandom(); 220 | } else if (ids == null && 'version' in storageNow && storageNow.version == 3) {//如果没传参数 使用localstorage的数据 221 | playlistIndex.value = storageNow.current; 222 | playlist.value = storageNow.playlist; 223 | playOrder.value = storageNow.playOrder; 224 | setPlayMode(storageNow.playMode); 225 | } else {//localstorage也没有数据 226 | console.error('播放列表初始化未提供参数'); 227 | } 228 | } else {//传了id列表 229 | await addMusic(ids, '0', true); 230 | } 231 | if (playlistIndex.value == -1 && playMode.value == 1) { 232 | playlistIndex.value = playOrder.value[0]; 233 | } else if (playlistIndex.value == -1) { 234 | playlistIndex.value = 0; 235 | } 236 | save();//保存到localstorage 237 | musicChanged();//把当前音乐应用到播放器 238 | } 239 | /** 240 | * 添加音乐到播放列表 默认添加到最前面 241 | * @param {String} id 242 | * @param {Number} position 243 | * @param {Boolean} letIndexIsNew 是否让index指向新添加的音乐的第一个 244 | */ 245 | async function addMusic(ids = [], position: string | number = 'now', letIndexIsNew = false) { 246 | if (position == 'now') { 247 | position = playlistIndex.value + 1; 248 | } 249 | position = Number(position); 250 | // console.log('添加音乐到播放列表', ids, position, letIndexIsNew); 251 | if (ids.length == 0) {//如果没传id 252 | return; 253 | } 254 | let list: any = {}; 255 | let res; 256 | list = ids.map(item => { 257 | return { 258 | id: item 259 | } 260 | }) 261 | //获取detail---------------------- 262 | res = await api.songDetail(ids.join(',')); 263 | res = res.data.songs; 264 | //因为返回的数据也许不按请求的id顺序返回 所以特殊处理 265 | res = api.parseDetailToList(res); 266 | list = api.mergeMusicObjArrs(list, res);//按照唯一标识符id合并 267 | 268 | 269 | 270 | //将结果放到播放列表中--------------------- 271 | //插入列表 272 | playlist.value.splice(position, 0, ...list) 273 | //改变index 274 | if (letIndexIsNew == true) { 275 | playlistIndex.value = Math.min(position, playlist.value.length - 1); 276 | } else { 277 | if (position < playlistIndex.value) { 278 | playlistIndex.value += position; 279 | } 280 | } 281 | 282 | // 生成随机播放顺序 283 | listRandom(); 284 | return playlist.value; 285 | } 286 | // 进行随机播放列表算法 287 | function listRandom() { 288 | let listOrder = []; 289 | // 初始化列表(自然数数列 290 | for (let i = 0; i < playlist.value.length; i++) { 291 | listOrder.push(i); 292 | } 293 | // 随机排序洗牌 294 | listOrder.sort(() => Math.random() - 0.5); 295 | // 加入顺序列表 296 | playOrder.value = listOrder 297 | } 298 | function save() { 299 | let list = playlist.value.map(item => { 300 | //移除url和歌词 因为音频URL有时效性 歌词则是因为localstorage的容量限制 所以移除 301 | let { url, lyric, ...a } = item; 302 | return a; 303 | }) 304 | let storage = { version: 3, playlist: list, current: playlistIndex.value, playOrder: playOrder.value, playMode: playMode.value }; 305 | localStorage.removeItem('playlist'); 306 | localStorage.setItem('playlist', JSON.stringify(storage)); 307 | } 308 | function stop() { 309 | player.value.src = '' 310 | pause() 311 | } 312 | function pause() { 313 | player.value.pause(); 314 | updateProgress(true); 315 | } 316 | //开始/继续播放 从头播放需要传入true 调用前需要设置好audio的src 317 | async function play(isNew = false) { 318 | //如果当前没有可以播放的源 那么就现在获取 319 | if (player.value.readyState == 0) { 320 | isNew = true 321 | } 322 | if (isNew) { 323 | player.value.currentTime = 0; 324 | musicChanged(); 325 | await getAudioUrl(currentMusic.value.id) 326 | } 327 | player.value.play(); 328 | updateProgress(true); 329 | } 330 | async function playWithPlaylistIndex(index) { 331 | playlistIndex.value = index; 332 | await play(true); 333 | } 334 | function next() { 335 | pause() 336 | console.log(`⏭下一曲`); 337 | beforeMusicChanged(); 338 | const computIndex = (length, indexNow) => { 339 | if (indexNow < length - 1) { 340 | return indexNow + 1; 341 | } else { 342 | return 0; 343 | } 344 | } 345 | switch (playMode.value) { 346 | case 0://顺序播放 347 | playlistIndex.value = computIndex(playlist.value.length, playlistIndex.value); 348 | break; 349 | case 1://随机播放 350 | playlistIndex.value = playOrder.value[computIndex(playOrder.value.length, playOrderIndex.value)]; 351 | break; 352 | } 353 | play(true); 354 | } 355 | function prev() { 356 | pause() 357 | console.log(`⏮上一曲`); 358 | beforeMusicChanged(); 359 | 360 | const computIndex = (length, indexNow) => { 361 | if (indexNow > 0) { 362 | return indexNow - 1; 363 | } else { 364 | return length - 1; 365 | } 366 | } 367 | switch (playMode.value) { 368 | case 0://顺序播放 369 | playlistIndex.value = computIndex(playlist.value.length, playlistIndex.value); 370 | break; 371 | case 1://随机播放 372 | playlistIndex.value = playOrder.value[computIndex(playOrder.value.length, playOrderIndex.value)]; 373 | break; 374 | } 375 | play(true); 376 | } 377 | function beforeMusicChanged() { 378 | 379 | let scrobble = (id: string, currentTime: number) => { 380 | let userStore = useUserStore(); 381 | if (userStore.isLogin) {// 登录了 382 | if (currentTime >= 15) {// 播放位置大于15秒才上报 383 | console.log(`📋️听歌打卡: ${id} ${currentTime}`); 384 | api.scrobble(id, currentTime, 0); 385 | } 386 | } 387 | } 388 | //进行防抖处理 每10秒只能上报一次 389 | scrobble = api.debounce(scrobble, 5000, 1); 390 | ///////////////////先不做这个功能 容易被冈易t号 391 | // console.log(`[playStore]beforeMusicChanged`); 392 | //scrobble(currentMusic.value.id, Math.floor(musicStatus.value.currentTime)); 393 | 394 | } 395 | function seek(time) { 396 | // console.log(`[playStore]seek ${time}`); 397 | player.value.currentTime = time; 398 | updateProgress(true, { position: time, duration: player.value.duration }); 399 | } 400 | function setPlayMode(mode = null) { 401 | if (mode == null) { 402 | mode = playMode.value ?? 0; 403 | // console.log(playMode); 404 | } 405 | // console.log(`[playStore]setPlayMode ${mode}`); 406 | playMode.value = mode; 407 | save(); 408 | } 409 | setInterval(() => { 410 | updateLyric() 411 | updateKtvLyric(); 412 | }, 50) 413 | function updateLyric() { 414 | try { 415 | if (musicStatus.value.paused == false && 'lyric' in currentMusic.value) {//正在播放 并且有歌词 416 | // 当前播放时间(毫秒 417 | // updateProgress(); 418 | let currentTime = player.value.currentTime * 1000 + 80; 419 | let lyric = currentMusic.value.lyric; 420 | // 找当前行index 421 | let lineIndex = lyric.findIndex((_, index) => { 422 | if (index + 1 < lyric.length) { 423 | return lyric[index + 1].time > currentTime && lyric[index].time <= currentTime 424 | } else { 425 | return lyric[index].time <= currentTime 426 | } 427 | }); 428 | if (lineIndex != -1) { 429 | // 歌词滚动 430 | if (lyricIndexNow.value.lineIndex != lineIndex) { 431 | lyricIndexNow.value.lineIndex = lineIndex; 432 | updateKtvLyric(); 433 | // if (window.isElectron) { 434 | // //如果是electron环境 就发送歌词给桌面歌词 435 | // window.api.sendLyric(JSON.stringify(currentMusic.value?.lyric?.[lineIndex])) 436 | // } 437 | //////////现在是musicController发送了 438 | } 439 | } else { 440 | lyricIndexNow.value.lineIndex = -1; 441 | } 442 | } 443 | } 444 | catch (e) { 445 | api.error(`出错了!\n位置:playStore updateLyric\n错误信息:${e}`) 446 | } 447 | } 448 | function updateKtvLyric() { 449 | // 逐字歌词 450 | try { 451 | if (lyricIndexNow.value.lineIndex >= 0 || !musicStatus.value.paused || 'lyric' in currentMusic.value) { 452 | 453 | // 当前播放时间(毫秒 454 | // updateProgress(); 455 | let currentTime = player.value.currentTime * 1000 + 80; 456 | // console.log(currentTime); 457 | let lyric = currentMusic.value.lyric; 458 | 459 | /**@type {array} */ 460 | let line = lyric?.[lyricIndexNow.value.lineIndex]?.lrc ?? []; 461 | // 找逐字歌词index 462 | let wordIndex = line.findIndex((_, index) => { 463 | if (index + 1 < line.length) { 464 | return line[index + 1].time > currentTime && line[index].time <= currentTime 465 | } else { 466 | return line[index].time <= currentTime 467 | } 468 | }); 469 | if (wordIndex != -1 && wordIndex != lyricIndexNow.value.wordIndex) { 470 | lyricIndexNow.value.wordIndex = wordIndex; 471 | lyricIndexNow.value.wordDuration = line[wordIndex].duration; 472 | // lyricIndexNow.value.percent = (currentTime - line[wordIndex].time) / line[wordIndex].duration; 473 | 474 | // console.log('逐字歌词改变', lyricIndexNow.value.percent); 475 | } 476 | } 477 | } catch (e) { 478 | api.error(`出错了!\n位置:playStore updateKtvLyric\n错误信息:${e}`) 479 | } 480 | } 481 | 482 | return { 483 | player, 484 | currentMusic, 485 | playlist, 486 | playOrder, 487 | playlistIndex, 488 | playMode, 489 | musicStatus, 490 | nameWithTns, 491 | playlistInit, 492 | addMusic, 493 | stop, 494 | play, 495 | pause, 496 | next, 497 | prev, 498 | seek, 499 | musicChanged, 500 | setPlayMode, 501 | playWithPlaylistIndex, 502 | } 503 | 504 | }) 505 | -------------------------------------------------------------------------------- /src/renderer/src/stores/setting.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useSettingStore = defineStore('setting', { 4 | state: () => ({ 5 | showLyricWindow: false, 6 | lyricFontSize: '1.8rem', 7 | musicLevel: 'jymaster' 8 | }), 9 | actions: { 10 | init() { 11 | let obj = JSON.parse(localStorage.getItem('setting')!) 12 | if (obj && obj.version == 1) { 13 | let { showLyricWindow, lyricFontSize, musicLevel } = obj; 14 | this.showLyricWindow = showLyricWindow; 15 | this.lyricFontSize = lyricFontSize ?? '1.8rem'; 16 | this.musicLevel = musicLevel ?? 'jymaster' 17 | } 18 | }, 19 | setLyricWindowShow(show: boolean | 'auto' = 'auto') { 20 | if (window.isElectron) { 21 | if (show == 'auto') { 22 | show = this.showLyricWindow ?? false 23 | } 24 | window.api.setLyricWindowShow(show as boolean) 25 | this.showLyricWindow = show 26 | this.save() 27 | } 28 | }, 29 | setLyricFontSize(size: string) { 30 | this.lyricFontSize = size; 31 | this.save() 32 | }, 33 | setMusicLevel(level: string) { 34 | this.musicLevel = level; 35 | this.save() 36 | }, 37 | save() { 38 | let stringData = JSON.stringify({ 39 | version: 1, 40 | showLyricWindow: this.showLyricWindow, 41 | lyricFontSize: this.lyricFontSize, 42 | musicLevel: this.musicLevel 43 | }) 44 | localStorage.setItem('setting', stringData); 45 | } 46 | } 47 | }) -------------------------------------------------------------------------------- /src/renderer/src/stores/theme.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { generate } from '@ant-design/colors' 3 | import emitter from '@/utils/mitt'; 4 | import type { GlobalThemeOverrides } from 'naive-ui'; 5 | 6 | let timer: number 7 | 8 | export const useThemeStore = defineStore('theme', { 9 | state: () => ({ 10 | mainColor: '', 11 | mainColors: [''], 12 | }), 13 | actions: { 14 | initByLocalStorage() { 15 | let { version, mainColors, mainColor } = JSON.parse(localStorage.getItem('theme') ?? '{"version":0}') 16 | if (version == 2 && mainColors) { 17 | this.mainColors = mainColors; 18 | this.mainColor = mainColor; 19 | } else { 20 | this.mainColor = '#DEB237'; 21 | this.mainColors = generate(this.mainColor) 22 | } 23 | this.update() 24 | }, 25 | update() {//把store的数据同步到naive-ui 26 | let obj: GlobalThemeOverrides = { 27 | common: { 28 | primaryColor: this.mainColors[5], 29 | primaryColorHover: this.mainColors[4], 30 | primaryColorSuppl: this.mainColors[4], 31 | primaryColorPressed: this.mainColors[6], 32 | textColorBase: this.mainColors[8], 33 | } 34 | } 35 | emitter.emit('changeTheme', obj) 36 | this.save() 37 | }, 38 | setMainColor(color: string) { 39 | this.mainColor = color 40 | this.mainColors = generate(color) 41 | this.update() 42 | }, 43 | save() { 44 | let stringData = JSON.stringify({ 45 | version: 2, 46 | mainColors: this.mainColors, 47 | mainColor: this.mainColor, 48 | }) 49 | localStorage.setItem('theme', stringData); 50 | if (window.isElectron) { 51 | window.clearInterval(timer); 52 | timer = window.setInterval(() => { 53 | window.api.sendThemeColors(stringData); 54 | }, 500); 55 | } 56 | } 57 | }, 58 | }) -------------------------------------------------------------------------------- /src/renderer/src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import * as api from '@/modules/api.js' 3 | import { toRaw, unref } from 'vue' 4 | 5 | export const useUserStore = defineStore('user', { 6 | state: () => ({ 7 | isLogin: false, 8 | name: '', 9 | avatar: '', 10 | vipIcon: '', 11 | vipexpire: '', 12 | province: '', 13 | city: '', 14 | cookie: '', 15 | uid: '', 16 | ip: '', 17 | playlists: [], 18 | likedSongs: [], 19 | updateTime: 0, 20 | }), 21 | actions: { 22 | async updateLikelist() { 23 | let res = await api.likelist(this.uid) 24 | if (res.data.code == 200) { 25 | this.likedSongs = res.data.ids 26 | this.storeToStorage() 27 | return res.data.ids 28 | } else { 29 | throw new Error('获取喜欢列表失败') 30 | } 31 | }, 32 | async updateByCookie(cookie: string | undefined) { 33 | let match: string | undefined = cookie || localStorage.getItem('cookie') || document.cookie 34 | match = match?.match(/MUSIC_U=[^;]+/)?.[0] 35 | if (match) { 36 | cookie = match 37 | } else if (!cookie) { 38 | this.logout() 39 | console.log(cookie, document.cookie, match); 40 | api.error('[未登录]更新用户信息时:没有cookie'); 41 | return; 42 | } 43 | localStorage.setItem('cookie', cookie) 44 | this.cookie = cookie 45 | this.isLogin = true 46 | 47 | let res = await api.loginStatus(); 48 | if (res.data.data.code == 200) { 49 | this.isLogin = !(res.data.data.profile == null) 50 | if (this.isLogin) { 51 | this.name = res.data.data.profile.nickname 52 | this.avatar = res.data.data.profile.avatarUrl 53 | this.uid = res.data.data.profile.userId 54 | this.province = res.data.data.profile.province 55 | this.city = res.data.data.profile.city 56 | //this.ip = res.data.data.profile.lastLoginIP 57 | if (this.ip == '') { 58 | this.ip = `111.37.150.${api.random(0, 255)}` 59 | } 60 | } 61 | } 62 | if (!this.isLogin) { 63 | this.logout() 64 | api.error('登录状态过期,请重新登录!') 65 | return; 66 | } 67 | res = await api.userPlaylist(this.uid) 68 | if (res.data.code == 200) { 69 | this.playlists = res.data.playlist 70 | } 71 | await this.updateLikelist(); 72 | 73 | res = await api.vipInfo(); 74 | if (res.data.code == 200) { 75 | this.vipIcon = res.data.data.associator.iconUrl 76 | this.vipexpire = res.data.data.associator.expireTime 77 | 78 | // 如果有svip 就替换掉vip图标 79 | let svipIcon = res.data.data.redplus.iconUrl 80 | let svipexpire = res.data.data.redplus.expireTime 81 | if (svipexpire > Date.now()) { 82 | this.vipIcon = svipIcon 83 | } 84 | } 85 | this.updateTime = Date.now() 86 | this.storeToStorage() 87 | // api.success('用户信息更新成功') 88 | }, 89 | async updateByStorage() { 90 | let user = JSON.parse(localStorage.getItem('user') ?? '{}') 91 | this.updateByObj(user) 92 | }, 93 | updateByObj(obj: any) { 94 | for (let key in obj) { 95 | let t: any = this 96 | t[key] = obj[key] 97 | } 98 | }, 99 | storeToStorage() { 100 | //把this转成纯对象 101 | let rawObj: any = toRaw(this); 102 | let pureObj: any = {}; 103 | for (let key in rawObj) { 104 | if (key.slice(0, 1) != '_' && key.slice(0, 1) != '$' && typeof (rawObj[key]) != 'function') { 105 | pureObj[key] = unref(rawObj[key]); 106 | } 107 | } 108 | localStorage.setItem('user', JSON.stringify(pureObj)); 109 | }, 110 | clearStorage() { 111 | localStorage.removeItem('user') 112 | localStorage.removeItem('cookie') 113 | }, 114 | logout() { 115 | this.$reset(); 116 | this.clearStorage(); 117 | document.cookie = "MUSIC_U=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; 118 | } 119 | } 120 | 121 | }) 122 | -------------------------------------------------------------------------------- /src/renderer/src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | import mitt, { Emitter } from 'mitt' 2 | const emitter:Emitter = (mitt as any)(); 3 | export default emitter 4 | -------------------------------------------------------------------------------- /src/renderer/src/window.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | isElectron: boolean, 3 | api: { 4 | sendLyric: (Lyric: any) => void, 5 | sendThemeColors: (themeColor: string) => void, 6 | setLyricWindowShow: (show: boolean) => void, 7 | netease: (url: string, data: string) => Promise, 8 | openUrl: (url: string) => Promise, 9 | appVersion: string, 10 | windowClose: () => void 11 | }, 12 | $NMessageApi: import('naive-ui').MessageApi, 13 | $NNotificationApi: import('naive-ui').NotificationApi, 14 | $NModalApi: import('naive-ui').ModalApi, 15 | player: HTMLAudioElement 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "NodeNext", 5 | "moduleResolution": "nodenext", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "sourceMap": true, 9 | "noImplicitThis": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": [ 13 | "src/renderer/src/*" 14 | ], 15 | } 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "**/node_modules/*" 20 | ] 21 | } -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | // uno.config.ts 2 | import { defineConfig, presetAttributify, presetUno } from 'unocss' 3 | 4 | export default defineConfig({ 5 | presets: [ 6 | presetAttributify({ 7 | /* preset 选项 */ 8 | }), 9 | presetUno() 10 | // ...自定义 presets 11 | ] 12 | }) 13 | --------------------------------------------------------------------------------