├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── background.js ├── img ├── home.png ├── icon.png ├── lyric.png ├── music_video.png └── playlist.png ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── api │ ├── album.js │ ├── artist.js │ ├── cloud.js │ ├── login.js │ ├── mv.js │ ├── other.js │ ├── playlist.js │ ├── song.js │ └── user.js ├── assets │ ├── css │ │ ├── common.css │ │ ├── fonts.css │ │ ├── plyr.css │ │ └── slider.css │ ├── fonts │ │ ├── Bender-Bold.woff │ │ ├── Geometos.woff │ │ ├── Gilroy-ExtraBold.woff │ │ ├── SourceHanSansCN-Bold.otf │ │ └── SourceHanSansCN-Heavy.otf │ ├── icon │ │ ├── icon.ico │ │ ├── last.png │ │ ├── next.png │ │ ├── pause.png │ │ └── play.png │ └── img │ │ ├── halftone.png │ │ └── netease-music.png ├── components │ ├── Banner.vue │ ├── ChildrenFolder.vue │ ├── CloudFileList.vue │ ├── ContextMenu.vue │ ├── DataCheckAnimaton.vue │ ├── DownloadList.vue │ ├── GlobalDialog.vue │ ├── GlobalNotice.vue │ ├── LibraryAlbumList.vue │ ├── LibraryDetail.vue │ ├── LibraryList.vue │ ├── LibraryMVList.vue │ ├── LibrarySongList.vue │ ├── LibraryType.vue │ ├── LocalMusicClassify.vue │ ├── LocalMusicDetail.vue │ ├── LocalMusicList.vue │ ├── LoginByAccount.vue │ ├── LoginByQRCode.vue │ ├── LoginContent.vue │ ├── Lyric.vue │ ├── MusicVideo.vue │ ├── MusicWidget.vue │ ├── NewestSong.vue │ ├── PlayList.vue │ ├── Player.vue │ ├── PlayerVideo.vue │ ├── RecList.vue │ ├── RecListItem.vue │ ├── RecommendSongs.vue │ ├── Recommendation.vue │ ├── SearchInput.vue │ ├── SearchResultList.vue │ ├── Selector.vue │ ├── Title.vue │ ├── Update.vue │ ├── VideoPlayer.vue │ └── WindowControl.vue ├── electron │ ├── dirTree.js │ ├── download.js │ ├── ipcMain.js │ ├── localmusic.js │ ├── preload.js │ ├── services.js │ ├── shortcuts.js │ └── tray.js ├── main.js ├── router │ └── router.js ├── store │ ├── cloudStore.js │ ├── libraryStore.js │ ├── localStore.js │ ├── otherStore.js │ ├── pinia.js │ ├── playerStore.js │ └── userStore.js ├── style.css ├── utils │ ├── authority.js │ ├── dialog.js │ ├── domHandler.js │ ├── handle.js │ ├── initApp.js │ ├── lazy.js │ ├── locaMusic.js │ ├── player.js │ ├── request.js │ └── songStatus.js └── views │ ├── CloudDisk.vue │ ├── Home.vue │ ├── HomePage.vue │ ├── LoginPage.vue │ ├── MusicPlayer.vue │ ├── MyMusic.vue │ ├── SearchResult.vue │ └── Settings.vue ├── target ├── .gitignore ├── README.md ├── background.js ├── index.html ├── npmlist.json ├── package-lock.json ├── package.json └── vite.config.js └── vite.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | jobs: 10 | release: 11 | name: build and release electron app 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [windows-latest] 18 | 19 | steps: 20 | - name: Check out git repository 21 | uses: actions/checkout@v3.0.0 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@v3.0.0 25 | with: 26 | node-version: "18.12.0" 27 | 28 | - name: Install Dependencies 29 | run: npm install 30 | 31 | - name: Build Electron App 32 | run: npm run dist 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 35 | 36 | - name: Cleanup Artifacts for Windows 37 | if: matrix.os == 'windows-latest' 38 | run: | 39 | npx rimraf "dist/!(*.exe)" 40 | 41 | - name: Cleanup Artifacts for MacOS 42 | if: matrix.os == 'macos-latest' 43 | run: | 44 | npx rimraf "dist/!(*.dmg)" 45 | 46 | - name: upload artifacts 47 | uses: actions/upload-artifact@v3.0.0 48 | with: 49 | name: ${{ matrix.os }} 50 | path: dist 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | 11 | dist-ssr 12 | 13 | # Editor directories and files 14 | node_modules/* 15 | release/* 16 | dist/* 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kaidesuyo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

Helium Music

5 | 6 |

7 | ⚠️请注意:本项目是已经被删库的 [Hydrogen-Music](https://github.com/Kaidesuyo/Hydrogen-Music) 的分支。 8 |
9 | 因NE增加了云盾验证,密码登录可能无法使用,请使用二维码登录。 10 |
11 | 请尽量不要使用云盘中的上传功能,目前上传失败概率大且内存无法得到释放。 12 |
13 | 欢迎各位前往本仓库继续维护这个分支。 14 |
15 | 📦️ 下载安装包 16 |
17 |
18 |

19 |

20 | 21 | ## 📦️ 安装 22 | 23 | 访问 [Releases](https://github.com/mtr-static-official/Helium-Music/releases) 24 | 页面下载安装包。 25 | 26 | ## 👷‍♂️ 打包客户端 27 | 28 | ~~由于个人设备限制,只打包了Windows平台的安装包且并未适配macOs与Linux平台。~~ 29 | 本分支已经完成了Windows平台和Linux平台的打包,macOS由于个人设备限制暂未打包。 30 | 如有可能,您可以在开发环境中自行适配。 31 | 32 | ```shell 33 | # 打包 34 | npm run dist 35 | ``` 36 | 37 | ## :computer: 配置开发环境 38 | 39 | 运行本项目 40 | 41 | ```shell 42 | # 安装依赖 43 | npm install 44 | 45 | # 运行Vue服务 46 | npm run dev 47 | 48 | # 运行Electron客户端 49 | npm start 50 | ``` 51 | 52 | ## 📜 开源许可 53 | 54 | 本项目仅供个人学习研究使用,禁止用于商业及非法用途。 55 | 56 | 基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。 57 | 58 | ## 灵感来源 59 | 60 | 基于Hydrogen Music修改而来,感谢[Hydrogen-Music](https://github.com/Kaidesuyo/Hydrogen-Music)。 61 | 62 | 63 | ## 🖼️ 截图 64 | 65 | ![home][home-screenshot] 66 | ![playlist][playlist-screenshot] 67 | ![lyric][lyric-screenshot] 68 | ![music_video][music_video-screenshot] 69 | 70 | 71 | 72 | 73 | [home-screenshot]: img/home.png 74 | [playlist-screenshot]: img/playlist.png 75 | [lyric-screenshot]: img/lyric.png 76 | [music_video-screenshot]: img/music_video.png 77 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const startNeteaseMusicApi = require('./src/electron/services') 2 | const IpcMainEvent = require('./src/electron/ipcMain') 3 | const MusicDownload = require('./src/electron/download') 4 | const LocalFiles = require('./src/electron/localmusic') 5 | const InitTray = require('./src/electron/tray') 6 | const registerShortcuts = require('./src/electron/shortcuts') 7 | 8 | const { app, BrowserWindow, globalShortcut } = require('electron') 9 | const Winstate = require('electron-win-state').default 10 | const { autoUpdater } = require("electron-updater"); 11 | const path = require('path') 12 | const Store = require('electron-store'); 13 | const settingsStore = new Store({name: 'settings'}); 14 | 15 | let myWindow = null 16 | //electron单例 17 | const gotTheLock = app.requestSingleInstanceLock() 18 | 19 | if (!gotTheLock) { 20 | app.quit() 21 | } else { 22 | app.on('second-instance', (event, commandLine, workingDirectory) => { 23 | if (myWindow) { 24 | if (myWindow.isMinimized()) myWindow.restore() 25 | if (!myWindow.isVisible()) myWindow.show() 26 | myWindow.focus() 27 | } 28 | }) 29 | 30 | app.whenReady().then(() => { 31 | createWindow() 32 | app.on('activate', () => { 33 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 34 | }) 35 | }) 36 | 37 | app.on('window-all-closed', () => { 38 | if (process.platform !== 'darwin') app.quit() 39 | }) 40 | 41 | app.on('will-quit', () => { 42 | // 注销所有快捷键 43 | globalShortcut.unregisterAll() 44 | }) 45 | } 46 | const createWindow = () => { 47 | process.env.DIST = path.join(__dirname, './') 48 | const indexHtml = path.join(process.env.DIST, 'dist/index.html') 49 | const winstate = new Winstate({ 50 | //自定义默认窗口大小 51 | defaultWidth: 1024, 52 | defaultHeight: 672, 53 | }) 54 | const win = new BrowserWindow({ 55 | minWidth: 1024, 56 | minHeight: 672, 57 | frame: false, 58 | title: "Helium Music", 59 | icon: path.resolve(__dirname, './src/assets/icon/icon.ico'), 60 | backgroundColor: '#fff', 61 | //记录窗口大小 62 | ...winstate.winOptions, 63 | show: false, 64 | webPreferences: { 65 | //预加载脚本 66 | preload: path.resolve(__dirname, './src/electron/preload.js'), 67 | webSecurity: false, 68 | } 69 | }) 70 | myWindow = win 71 | if(process.resourcesPath.indexOf('\\node_modules\\') != -1) 72 | win.loadURL('http://localhost:5173/') 73 | else 74 | win.loadFile(indexHtml) 75 | win.once('ready-to-show', () => { 76 | win.show() 77 | if(process.resourcesPath.indexOf('\\node_modules\\') == -1) { 78 | autoUpdater.autoDownload = false 79 | autoUpdater.on('update-available', info => { 80 | win.webContents.send('check-update', info.version) 81 | }); 82 | autoUpdater.checkForUpdatesAndNotify() 83 | } 84 | }) 85 | winstate.manage(win) 86 | win.on('close', async (event) => { 87 | event.preventDefault() 88 | const settings = await settingsStore.get('settings') 89 | if(settings.other.quitApp == 'minimize') { 90 | win.hide() 91 | } else if(settings.other.quitApp == 'quit') { 92 | win.webContents.send('player-save') 93 | } 94 | }) 95 | //api初始化 96 | startNeteaseMusicApi() 97 | //ipcMain初始化 98 | IpcMainEvent(win, app) 99 | MusicDownload(win) 100 | LocalFiles(win, app) 101 | InitTray(win, app, path.resolve(__dirname, './src/assets/icon/icon.ico')) 102 | registerShortcuts(win) 103 | } 104 | 105 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; -------------------------------------------------------------------------------- /img/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/img/home.png -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/img/icon.png -------------------------------------------------------------------------------- /img/lyric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/img/lyric.png -------------------------------------------------------------------------------- /img/music_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/img/music_video.png -------------------------------------------------------------------------------- /img/playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/img/playlist.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Helium Music 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heliummusic", 3 | "private": true, 4 | "version": "0.4.0", 5 | "main": "background.js", 6 | "author": "mtrstatic", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "start": "nodemon --exec electron . --watch ./ --ext .html", 12 | "dist": "vite build && electron-builder -p always" 13 | }, 14 | "dependencies": { 15 | "axios": "^1.2.0", 16 | "dayjs": "^1.11.6", 17 | "electron-store": "^8.1.0", 18 | "electron-updater": "^5.3.0", 19 | "electron-win-state": "^1.1.22", 20 | "fs-extra": "^11.1.0", 21 | "howler": "^2.2.3", 22 | "js-cookie": "^3.0.1", 23 | "js-md5": "^0.7.3", 24 | "music-metadata": "^7.13.0", 25 | "nanoid": "^3.3.4", 26 | "NeteaseCloudMusicApi": "^4.8.9", 27 | "normalize.css": "^8.0.1", 28 | "pinia": "^2.0.23", 29 | "pinia-plugin-persistedstate": "^3.0.1", 30 | "plyr": "^3.7.3", 31 | "qrcode": "^1.5.1", 32 | "vue": "^3.2.41", 33 | "vue-router": "^4.1.6", 34 | "vue-slider-component": "^4.1.0-beta.6", 35 | "vue-virtual-scroller": "^2.0.0-beta.8" 36 | }, 37 | "devDependencies": { 38 | "@vitejs/plugin-vue": "^3.2.0", 39 | "electron": "^21.2.0", 40 | "electron-builder": "^23.6.0", 41 | "nodemon": "^2.0.20", 42 | "sass": "^1.56.1", 43 | "vite": "^3.2.0" 44 | }, 45 | "build": { 46 | "productName": "Helium Music", 47 | "appId": "114514", 48 | "asar": true, 49 | "directories": { 50 | "output": "release/${version}" 51 | }, 52 | "files": [], 53 | "nsis": { 54 | "oneClick": false, 55 | "allowToChangeInstallationDirectory": true 56 | }, 57 | "mac": { 58 | "category": "" 59 | }, 60 | "win": { 61 | "icon": "./src/assets/icon/icon.ico", 62 | "target": ["nsis","portable","zip"], 63 | "verifyUpdateCodeSignature": false 64 | }, 65 | "linux": {}, 66 | "publish": [ 67 | { 68 | "provider": "github", 69 | "owner": "heliummusic", 70 | "repo": "Helium-Music", 71 | "releaseType": "draft" 72 | } 73 | ] 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 69 | 70 | 221 | -------------------------------------------------------------------------------- /src/api/album.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 登录后调用此接口 ,可获取全部新碟 5 | *可选参数 : 6 | *limit : 返回数量 , 默认为 30 7 | *offset : 偏移数量,用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 8 | *area : ALL:全部,ZH:华语,EA:欧美,KR:韩国,JP:日本 9 | */ 10 | export function getNewAlbum(params) { 11 | return request({ 12 | url: '/album/new', 13 | method: 'get', 14 | params, 15 | }) 16 | } 17 | 18 | /** 19 | * 调用此接口 ,获取云音乐首页新碟上架数据 20 | */ 21 | export function getNewestAlbum(params) { 22 | return request({ 23 | url: '/album/newest', 24 | method: 'get', 25 | params 26 | }) 27 | } 28 | 29 | /** 30 | * 说明 : 调用此接口 , 可获得已收藏专辑列表 31 | * 可选参数 : 32 | * limit: 取出数量 , 默认为 25 33 | * offset: 偏移数量 , 用于分页 , 如 :( 页数 -1)*25, 其中 25 为 limit 的值 , 默认 为 0 34 | * @param {*} params 35 | * @returns 36 | */ 37 | export function getUserSubAlbum(params) { 38 | return request({ 39 | url: '/album/sublist', 40 | method: 'get', 41 | params, 42 | }) 43 | } 44 | 45 | /** 46 | * 说明 : 调用此接口 , 传入专辑 id, 可获得专辑内容 47 | * 必选参数 : id: 专辑 id 48 | * @param {*} params 49 | * @returns 50 | */ 51 | export function getAlbumDetail(params) { 52 | return request({ 53 | url: '/album', 54 | method: 'get', 55 | params, 56 | }) 57 | } 58 | 59 | /** 60 | * 说明 : 调用此接口,可收藏/取消收藏专辑 61 | * 必选参数 : 62 | * id : 专辑 id 63 | * t : 1 为收藏,其他为取消收藏 64 | * @param {*} params 65 | * @returns 66 | */ 67 | export function subAlbum(params) { 68 | return request({ 69 | url: '/album/sub', 70 | method: 'get', 71 | params, 72 | }) 73 | } 74 | 75 | /** 76 | * 说明 : 调用此接口 , 传入专辑 id, 可获得专辑动态信息,如是否收藏,收藏数,评论数,分享数 77 | * 必选参数 : id: 专辑 id 78 | * @param {*} params 79 | * @returns 80 | */ 81 | export function albumDynamic(id) { 82 | return request({ 83 | url: '/album/detail/dynamic', 84 | method: 'get', 85 | params: { 86 | id: id, 87 | timestamp: new Date().getTime(), 88 | } 89 | }) 90 | } -------------------------------------------------------------------------------- /src/api/artist.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 调用此接口获取排行榜中的歌手榜 5 | * type : 1:华语 2:欧美 3:韩国 4:日本 6 | * @param {number} type 7 | * @returns 8 | */ 9 | export function getRecommendedArtists(type) { 10 | return request({ 11 | url: '/top/artists', 12 | method: 'get', 13 | params: { 14 | type: type, 15 | } 16 | }) 17 | } 18 | 19 | /** 20 | * 收藏的歌手列表 21 | * 说明 : 调用此接口,可获取收藏的歌手列表 22 | * @param {*} type 23 | * @returns 24 | */ 25 | export function getUserSubArtists() { 26 | return request({ 27 | url: '/artist/sublist', 28 | method: 'get', 29 | params: { 30 | // timestamp: new Date().getTime(), 31 | } 32 | }) 33 | } 34 | 35 | /** 36 | * 说明 : 调用此接口 , 传入歌手 id, 可获得获取歌手详情 37 | * @returns 38 | */ 39 | // export function getArtistDetail(params) { 40 | // return request({ 41 | // url: '/artist/detail', 42 | // method: 'get', 43 | // params, 44 | // }) 45 | // } 46 | 47 | /** 48 | * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手部分信息和热门歌曲 49 | * 必选参数 : id: 歌手 id, 可由搜索接口获得 50 | * @param {*} params 51 | * @returns 52 | */ 53 | export function getArtistDetail(params) { 54 | return request({ 55 | url: '/artists', 56 | method: 'get', 57 | params, 58 | }) 59 | } 60 | 61 | /** 62 | * 说明 : 调用此接口,可获取歌手热门 50 首歌曲 63 | * @param {*} params 64 | * @returns 65 | */ 66 | export function getArtistTopSong(params) { 67 | return request({ 68 | url: '/artist/top/song', 69 | method: 'get', 70 | params, 71 | }) 72 | } 73 | 74 | /** 75 | * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手专辑内容 76 | * 必选参数 : id: 歌手 id 77 | * 可选参数 : limit: 取出数量 , 默认为 30 78 | * offset: 偏移数量 , 用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认 为 0 79 | * @param {*} params 80 | * @returns 81 | */ 82 | export function getArtistAlbum(params) { 83 | return request({ 84 | url: '/artist/album', 85 | method: 'get', 86 | params, 87 | }) 88 | } 89 | 90 | /** 91 | * 说明 : 调用此接口 , 传入歌手 id, 可获取歌手粉丝数量 92 | * @param {*} params 93 | * @returns 94 | */ 95 | export function getArtistFansCount(id) { 96 | return request({ 97 | url: '/artist/follow/count', 98 | method: 'get', 99 | params: { 100 | id: id, 101 | timestamp: new Date().getTime(), 102 | } 103 | }) 104 | } 105 | 106 | /** 107 | * 说明 : 调用此接口,可收藏歌手 108 | * 必选参数 : 109 | * id : 歌手 id 110 | * t:操作,1 为收藏,其他为取消收藏 111 | * @param {*} params 112 | * @returns 113 | */ 114 | export function subArtist(params) { 115 | return request({ 116 | url: '/artist/sub', 117 | method: 'get', 118 | params, 119 | }) 120 | } -------------------------------------------------------------------------------- /src/api/cloud.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 说明 : 登录后调用此接口 , 可获取云盘数据 , 获取的数据没有对应 url, 需要再调用一 次 /song/url 获取 url 5 | * 可选参数 : 6 | * limit : 返回数量 , 默认为 30 7 | * offset : 偏移数量,用于分页 , 如 :( 页数 -1)*200, 其中 200 为 limit 的值 , 默认为 0 8 | * @param {*} params 9 | * @returns 10 | */ 11 | export function getCloudDiskData(params) { 12 | return request({ 13 | url: '/user/cloud', 14 | method: 'get', 15 | params, 16 | }) 17 | } 18 | 19 | /** 20 | * 说明 : 登录后调用此接口 , 传入云盘歌曲 id,可获取云盘数据详情 21 | * 必选参数 : id: 歌曲 id,可多个,用逗号隔开 22 | * @param {*} params 23 | * @returns 24 | */ 25 | export function getCloudDiskDrtail(params) { 26 | return request({ 27 | url: '/user/cloud/detail', 28 | method: 'get', 29 | params, 30 | }) 31 | } 32 | 33 | /** 34 | * 说明 : 登录后调用此接口 , 可删除云盘歌曲 35 | * 必选参数 : id: 歌曲 id,可多个,用逗号隔开 36 | * @param {*} params 37 | * @returns 38 | */ 39 | export function deleteCloudSong(params) { 40 | return request({ 41 | url: '/user/cloud/del', 42 | method: 'get', 43 | params, 44 | }) 45 | } 46 | 47 | /** 48 | * 说明 : 登录后调用此接口,使用'Content-Type': 'multipart/form-data'上传 mp3 formData(name 为'songFile'),可上传歌曲到云盘 49 | * 参考: https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/public/cloud.html 50 | * 支持命令行调用,参考 module_example 目录下song_upload.js 51 | * @param {*} params 52 | * @returns 53 | */ 54 | export function uploadCloudSong(formData) { 55 | return request({ 56 | url: '/cloud', 57 | method: 'post', 58 | headers: { 59 | 'Content-Type': 'multipart/form-data', 60 | }, 61 | data: formData, 62 | params: { 63 | timestamp: new Date().getTime(), 64 | }, 65 | timeout: 99999999, 66 | }) 67 | } -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 调用此接口可生成一个 key 5 | * @returns 6 | */ 7 | export function getQRcode() { 8 | return request({ 9 | url: '/login/qr/key', 10 | method: 'get', 11 | params: { 12 | timestamp: new Date().getTime() 13 | } 14 | }) 15 | } 16 | /** 17 | * 轮询此接口可获取二维码扫码状态,800 为二维码过期,801 为等待扫码,802 为待确认,803 为授权登录成功(803 状态码下会返回 cookies) 18 | * 必选参数: key,由第一个接口生成 19 | * @param {String} key 20 | * @returns 21 | */ 22 | export function checkQRcodeStatus(key) { 23 | return request({ 24 | url: '/login/qr/check', 25 | method: 'get', 26 | params: { 27 | key: key, 28 | timestamp: new Date().getTime(), 29 | } 30 | }) 31 | } 32 | 33 | /** 34 | * 必选参数 : 35 | * email: 163 网易邮箱 36 | * password: 密码 37 | * 可选参数 : 38 | * md5_password: md5 加密后的密码,传入后 password 将失效 39 | * @returns 40 | */ 41 | export function loginByEmail(params) { 42 | return request({ 43 | url: '/login', 44 | method: 'post', 45 | params, 46 | }); 47 | } 48 | 49 | /** 50 | * 必选参数 : 51 | * phone: 手机号码 52 | * password: 密码 53 | * 可选参数 : 54 | * countrycode: 国家码,用于国外手机号登录,例如美国传入:1 55 | * md5_password: md5 加密后的密码,传入后 password 参数将失效 56 | * captcha: 验证码,使用 /captcha/sent接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 password 参数将失效 57 | * @returns 58 | */ 59 | export function loginByPhone(params) { 60 | return request({ 61 | url: '/login/cellphone', 62 | method: 'post', 63 | params, 64 | }); 65 | } 66 | 67 | 68 | /** 69 | * 调用此接口 , 可退出登录 70 | * @returns 71 | */ 72 | export function logout() { 73 | return request({ 74 | url: '/logout', 75 | method: 'post', 76 | params: { 77 | 78 | }, 79 | }); 80 | } -------------------------------------------------------------------------------- /src/api/mv.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 收藏的 MV 列表 5 | * 说明 : 调用此接口,可获取收藏的 MV 列表 6 | * @returns 7 | */ 8 | export function getUserSubMV() { 9 | return request({ 10 | url: '/mv/sublist', 11 | method: 'get', 12 | params: { 13 | // timestamp: new Date().getTime(), 14 | } 15 | }) 16 | } 17 | 18 | /** 19 | * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手 mv 信息 , 具体 mv 播放地址可调 用/mv传入此接口获得的 mvid 来拿到 , 20 | * 如 : /artist/mv?id=6452,/mv?mvid=5461064 21 | * @returns 22 | */ 23 | export function getArtistMV(params) { 24 | return request({ 25 | url: '/artist/mv', 26 | method: 'get', 27 | params, 28 | }) 29 | } 30 | 31 | /** 32 | * 说明 : 调用此接口 , 传入 mvid ( 在搜索音乐的时候传 type=1004 获得 ) , 可获取对应 MV 数据 , 数据包含 mv 名字 , 歌手 , 发布时间 , mv 视频地址等数据 , 其中 mv 视频 网易做了防盗链处理 , 可能不能直接播放 , 需要播放的话需要调用 ' mv 地址' 接口 33 | * 必选参数 : mvid: mv 的 id 34 | * @param {*} mvid 35 | * @returns 36 | */ 37 | export function getMVDetail(id) { 38 | return request({ 39 | url: '/mv/detail', 40 | method: 'get', 41 | params: { 42 | mvid: id, 43 | } 44 | }) 45 | } 46 | /** 47 | * 说明 : 调用此接口 , 传入 mv id,可获取 mv 播放地址 48 | * 必选参数 : id: mv id 49 | * 可选参数 : r: 分辨率,默认 1080,可从 /mv/detail 接口获取分辨率列表 50 | * 接口地址 : /mv/url 51 | * @param {*} id 52 | * @returns 53 | */ 54 | export function getMVUrl(id, r) { 55 | return request({ 56 | url: '/mv/url', 57 | method: 'get', 58 | params: { 59 | id: id, 60 | r: r, 61 | } 62 | }) 63 | } -------------------------------------------------------------------------------- /src/api/other.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 调用此接口获取轮播图 5 | * @param {number} id 6 | * @returns 7 | */ 8 | export function getBanner(id) { 9 | return request({ 10 | url: '/banner', 11 | method: 'get', 12 | params: { 13 | type: id 14 | } 15 | }).then(data => { 16 | return data 17 | }) 18 | } 19 | 20 | /** 21 | * 说明 : 调用此接口 , 传入搜索关键词可以搜索该音乐 / 专辑 / 歌手 / 歌单 / 用户 , 关键词可以多个 , 以空格隔开 , 如 " 周杰伦 搁浅 "( 不需要登录 ), 可通过 /song/url 接口传入歌曲 id 获取具体的播放链接 22 | * 必选参数 : keywords : 关键词 23 | * 可选参数 : limit : 返回数量 , 默认为 30 offset : 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 24 | * type: 搜索类型;默认为 1 即单曲 , 取值意义 : 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合, 2000:声音(搜索声音返回字段格式会不一样) 25 | * @param {*} id 26 | * @returns 27 | */ 28 | export function search(params) { 29 | return request({ 30 | url: '/cloudsearch', 31 | method: 'get', 32 | params, 33 | }) 34 | } -------------------------------------------------------------------------------- /src/api/playlist.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 获取推荐歌单 5 | * @param {number} num 6 | */ 7 | export function getRecommendedSongList(num) { 8 | return request({ 9 | url: '/personalized', 10 | method: 'get', 11 | params: { 12 | limit: num 13 | } 14 | }) 15 | } 16 | 17 | export function getTopList() { 18 | return request({ 19 | url: '/toplist', 20 | method: 'get', 21 | params: { 22 | 23 | } 24 | }); 25 | } 26 | 27 | /** 28 | * 获取歌单详情 29 | * 说明 : 歌单能看到歌单名字, 但看不到具体歌单内容 , 调用此接口 , 传入歌单 id, 30 | * 可以获取对应歌单内的所有的音乐(未登录状态只能获取不完整的歌单,登录后是完整的), 31 | * 但是返回的 trackIds 是完整的,tracks 则是不完整的,可拿全部 trackIds 请求一次 song/detail 32 | * 接口获取所有歌曲的详情 (https://github.com/Binaryify/NeteaseCloudMusicApi/issues/452) 33 | * @returns 34 | */ 35 | export function getPlaylistDetail(params) { 36 | return request({ 37 | url: '/playlist/detail', 38 | method: 'get', 39 | params, 40 | }); 41 | } 42 | 43 | /** 44 | * 说明 : 由于网易云接口限制,歌单详情只会提供 10 首歌,通过调用此接口,传入对应的歌单id,即可获得对应的所有歌曲 45 | * 必选参数 : id : 歌单 id 46 | * 可选参数 : limit : 限制获取歌曲的数量,默认值为当前歌单的歌曲数量 47 | * 可选参数 : offset : 默认值为0 48 | * @param {*} params 49 | * @returns 50 | */ 51 | export function getPlaylistAll(params) { 52 | return request({ 53 | url: '/playlist/track/all', 54 | method: 'get', 55 | params, 56 | }); 57 | } 58 | 59 | /** 60 | * 调用此接口 , 可获得每日推荐歌曲 ( 需要登录 ) 61 | * @returns 62 | */ 63 | export function getRecommendSongs(params) { 64 | return request({ 65 | url: '/recommend/songs', 66 | method: 'get', 67 | params: { 68 | 69 | }, 70 | }); 71 | } 72 | 73 | /** 74 | * 说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单 75 | * 必选参数 : 76 | * t : 类型,1:收藏,2:取消收藏 id : 歌单 id 77 | * @param {*} params 78 | * @returns 79 | */ 80 | export function subPlaylist(params) { 81 | return request({ 82 | url: '/playlist/subscribe', 83 | method: 'get', 84 | params, 85 | }); 86 | } 87 | 88 | /** 89 | * 说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数 90 | * 必选参数 : id : 歌单 id 91 | * @param {*} params 92 | * @returns 93 | */ 94 | export function playlistDynamic(id) { 95 | return request({ 96 | url: '/playlist/detail/dynamic', 97 | method: 'get', 98 | params: { 99 | id: id, 100 | timestamp: new Date().getTime(), 101 | } 102 | }); 103 | } 104 | 105 | /** 106 | * 说明 : 调用此接口 , 传入歌单名字可新建歌单 107 | * 必选参数 : name : 歌单名 108 | * 可选参数 : 109 | * privacy : 是否设置为隐私歌单,默认否,传'10'则设置成隐私歌单 110 | * type : 歌单类型,默认'NORMAL',传 'VIDEO'则为视频歌单,传 'SHARED'则为共享歌单 111 | * @param {*} params 112 | * @returns 113 | */ 114 | export function createPlaylist(params) { 115 | return request({ 116 | url: '/playlist/create', 117 | method: 'post', 118 | params, 119 | }); 120 | } 121 | 122 | /** 123 | * 必选参数 : 124 | * op: 从歌单增加单曲为 add, 删除为 del 125 | * pid: 歌单 id tracks: 歌曲 id,可多个,用逗号隔开 126 | * @param {*} params 127 | * @returns 128 | */ 129 | export function updatePlaylist(params) { 130 | return request({ 131 | url: '/playlist/tracks', 132 | method: 'post', 133 | params, 134 | }); 135 | } 136 | 137 | /** 138 | * 说明 : 调用此接口 , 传入歌单 id 可删除歌单 139 | * 必选参数 : id : 歌单 id,可多个,用逗号隔开 140 | * @param {*} params 141 | * @returns 142 | */ 143 | export function deletePlaylist(params) { 144 | return request({ 145 | url: '/playlist/delete', 146 | method: 'post', 147 | params, 148 | }); 149 | } -------------------------------------------------------------------------------- /src/api/song.js: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | 3 | /** 4 | * 调用此接口 , 可获取推荐新音乐 5 | * 可选参数 : limit: 取出数量 , 默认为 10 (不支持 offset) 6 | * @param {number} limit 7 | */ 8 | export function getNewestSong() { 9 | return request({ 10 | url: '/personalized/newsong', 11 | method: 'get', 12 | params: { 13 | limit: 10 14 | } 15 | }) 16 | } 17 | 18 | /** 19 | * 说明: 调用此接口,传入歌曲 id, 可获取音乐是否可用,返回 { success: true, message: 'ok' } 20 | * 或者 { success: false, message: '亲爱的,暂无版权' } 21 | * @param {number} id 22 | * @returns 23 | */ 24 | export function checkMusic(id) { 25 | return request({ 26 | url: '/check/music', 27 | method: 'get', 28 | params: { 29 | id: id 30 | } 31 | }) 32 | } 33 | 34 | /** 35 | * 必选参数 : id : 音乐 id level: 播放音质等级, 36 | * 分为 standard => 标准,higher => 较高, exhigh=>极高, lossless=>无损, hires=>Hi-Res 37 | * @param {number} id 38 | * @returns 39 | */ 40 | export function getMusicUrl(id,level) { 41 | return request({ 42 | url: '/song/url/v1', 43 | method: 'get', 44 | params: { 45 | id: id, 46 | level: level 47 | } 48 | }) 49 | } 50 | 51 | /** 52 | * 说明 : 调用此接口 , 传入音乐 id, 可喜欢该音乐 53 | * 必选参数 : id: 歌曲 id 54 | * 可选参数 : like: 布尔值 , 默认为 true 即喜欢 , 若传 false, 则取消喜欢 55 | * @param {*} id 56 | * @returns 57 | */ 58 | export function likeMusic(id,like) { 59 | return request({ 60 | url: '/like', 61 | method: 'get', 62 | params: { 63 | id: id, 64 | like: like, 65 | timestamp: new Date().getTime() 66 | } 67 | }) 68 | } 69 | 70 | /** 71 | * 说明 : 调用此接口 , 传入音乐 id 可获得对应音乐的歌词 ( 不需要登录 ) 72 | * 必选参数 : id: 音乐 id 73 | * @param {*} id 74 | * @returns 75 | */ 76 | export function getLyric(id) { 77 | return request({ 78 | url: '/lyric', 79 | method: 'get', 80 | params: { 81 | id: id, 82 | } 83 | }) 84 | } -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request' 2 | /** 3 | * 登录后调用此接口 ,可获取用户账号信息 4 | * @returns 5 | */ 6 | export function getUserProfile() { 7 | return request({ 8 | url: '/user/account', 9 | method: 'get', 10 | params: { 11 | timestamp: new Date().getTime(), 12 | }, 13 | }); 14 | } 15 | 16 | /** 17 | * 登录后调用此接口 , 传入用户 id, 可以获取用户歌单 18 | * 必选参数 : uid : 用户 id 19 | * 可选参数 : 20 | * limit : 返回数量 , 默认为 30 21 | * offset : 偏移数量,用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 22 | * @returns 23 | */ 24 | export function getUserPlaylist(params) { 25 | return request({ 26 | url: '/user/playlist', 27 | method: 'get', 28 | params, 29 | }); 30 | } 31 | 32 | /** 33 | * 获取用户信息 , 歌单,收藏,mv, dj 数量 34 | * 说明 : 登录后调用此接口 , 可以获取用户信息 35 | * @param {*} params 36 | * @returns 37 | */ 38 | export function getUserPlaylistCount() { 39 | return request({ 40 | url: '/user/subcount', 41 | method: 'get', 42 | params: { 43 | timestamp: new Date().getTime(), 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * 说明 : 调用此接口 , 可退出登录 50 | * @returns 51 | */ 52 | export function logout() { 53 | return request({ 54 | url: '/logout', 55 | method: 'post', 56 | params: { 57 | 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * 说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐 id 列表(id 数组) 64 | * @param {*} id 65 | * @returns 66 | */ 67 | export function getLikelist(id) { 68 | return request({ 69 | url: '/likelist', 70 | method: 'get', 71 | params: { 72 | id: id, 73 | timestamp: new Date().getTime(), 74 | } 75 | }); 76 | } 77 | 78 | /** 79 | * 说明: 登录后调用此接口,可获取当前 VIP 信息。 80 | * @param {*} id 81 | * @returns 82 | */ 83 | export function getVipInfo() { 84 | return request({ 85 | url: '/vip/info', 86 | method: 'get', 87 | params: { 88 | timestamp: new Date().getTime(), 89 | } 90 | }); 91 | } -------------------------------------------------------------------------------- /src/assets/css/common.css: -------------------------------------------------------------------------------- 1 | html, body{ 2 | height: 100%; 3 | } 4 | *{ 5 | box-sizing: border-box; 6 | } -------------------------------------------------------------------------------- /src/assets/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: SourceHanSansCN-Heavy; 3 | src: url('../fonts/SourceHanSansCN-Heavy.otf'); 4 | } 5 | @font-face { 6 | font-family: SourceHanSansCN-Bold; 7 | src: url('../fonts/SourceHanSansCN-Bold.otf'); 8 | } 9 | @font-face { 10 | font-family: Bender-Bold; 11 | src: url('../fonts/Bender-Bold.woff'); 12 | } 13 | @font-face { 14 | font-family: Geometos; 15 | src: url('../fonts/Geometos.woff'); 16 | } 17 | @font-face { 18 | font-family: Gilroy-ExtraBold; 19 | src: url('../fonts/Gilroy-ExtraBold.woff'); 20 | } -------------------------------------------------------------------------------- /src/assets/css/slider.css: -------------------------------------------------------------------------------- 1 | /* component style */ 2 | .vue-slider { 3 | padding: 0 !important; 4 | } 5 | .vue-slider:hover{ 6 | cursor: pointer; 7 | } 8 | .vue-slider-disabled { 9 | opacity: 0.5; 10 | cursor: not-allowed; 11 | } 12 | /* rail style */ 13 | .vue-slider-rail { 14 | /* height: 7Px; 15 | border: 1Px solid black; 16 | box-shadow: 0 0 0 0.5Px black; */ 17 | } 18 | 19 | /* process style */ 20 | .vue-slider-process { 21 | background-color: #000000; 22 | } 23 | 24 | /* mark style */ 25 | .vue-slider-mark { 26 | z-index: 4; 27 | } 28 | .vue-slider-mark:first-child .vue-slider-mark-step, .vue-slider-mark:last-child .vue-slider-mark-step { 29 | display: none; 30 | } 31 | .vue-slider-mark-step { 32 | width: 100%; 33 | height: 100%; 34 | border-radius: 50%; 35 | background-color: rgba(0, 0, 0, 0.16); 36 | } 37 | .vue-slider-mark-label { 38 | 39 | display: none; 40 | 41 | font-size: 14px; 42 | white-space: nowrap; 43 | } 44 | /* dot style */ 45 | .vue-slider-dot-handle { 46 | 47 | display: none; 48 | 49 | cursor: pointer; 50 | width: 100%; 51 | height: 100%; 52 | border-radius: 50%; 53 | background-color: #fff; 54 | box-sizing: border-box; 55 | box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.32); 56 | } 57 | .vue-slider-dot-handle-focus { 58 | box-shadow: 0px 0px 1px 2px rgba(52, 152, 219, 0.36); 59 | } 60 | 61 | .vue-slider-dot-handle-disabled { 62 | cursor: not-allowed; 63 | background-color: #ccc; 64 | } 65 | 66 | .vue-slider-dot-tooltip-inner { 67 | font-family: Bender-Bold; 68 | font-size: 12px; 69 | white-space: nowrap; 70 | padding: 2px 5px; 71 | min-width: 20px; 72 | text-align: center; 73 | color: #fff; 74 | border-radius: 0; 75 | border-color: transparent; 76 | background-color: rgba(0, 0, 0, 0.6); 77 | box-sizing: content-box; 78 | } 79 | .vue-slider-dot-tooltip-inner::after { 80 | content: ""; 81 | position: absolute; 82 | } 83 | .vue-slider-dot-tooltip-inner-top::after { 84 | top: 100%; 85 | left: 50%; 86 | transform: translate(-50%, 0); 87 | height: 0; 88 | width: 0; 89 | border-color: transparent; 90 | border-style: solid; 91 | border-width: 5px; 92 | border-top-color: inherit; 93 | } 94 | .vue-slider-dot-tooltip-inner-bottom::after { 95 | bottom: 100%; 96 | left: 50%; 97 | transform: translate(-50%, 0); 98 | height: 0; 99 | width: 0; 100 | border-color: transparent; 101 | border-style: solid; 102 | border-width: 5px; 103 | border-bottom-color: inherit; 104 | } 105 | .vue-slider-dot-tooltip-inner-left::after { 106 | left: 100%; 107 | top: 50%; 108 | transform: translate(0, -50%); 109 | height: 0; 110 | width: 0; 111 | border-color: transparent; 112 | border-style: solid; 113 | border-width: 5px; 114 | border-left-color: inherit; 115 | } 116 | .vue-slider-dot-tooltip-inner-right::after { 117 | right: 100%; 118 | top: 50%; 119 | transform: translate(0, -50%); 120 | height: 0; 121 | width: 0; 122 | border-color: transparent; 123 | border-style: solid; 124 | border-width: 5px; 125 | border-right-color: inherit; 126 | } 127 | 128 | .vue-slider-dot-tooltip-wrapper { 129 | opacity: 0; 130 | transition: all 0.3s; 131 | } 132 | .vue-slider-dot-tooltip-wrapper-show { 133 | opacity: 1; 134 | } -------------------------------------------------------------------------------- /src/assets/fonts/Bender-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/fonts/Bender-Bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/Geometos.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/fonts/Geometos.woff -------------------------------------------------------------------------------- /src/assets/fonts/Gilroy-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/fonts/Gilroy-ExtraBold.woff -------------------------------------------------------------------------------- /src/assets/fonts/SourceHanSansCN-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/fonts/SourceHanSansCN-Bold.otf -------------------------------------------------------------------------------- /src/assets/fonts/SourceHanSansCN-Heavy.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/fonts/SourceHanSansCN-Heavy.otf -------------------------------------------------------------------------------- /src/assets/icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/icon/icon.ico -------------------------------------------------------------------------------- /src/assets/icon/last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/icon/last.png -------------------------------------------------------------------------------- /src/assets/icon/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/icon/next.png -------------------------------------------------------------------------------- /src/assets/icon/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/icon/pause.png -------------------------------------------------------------------------------- /src/assets/icon/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/icon/play.png -------------------------------------------------------------------------------- /src/assets/img/halftone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/img/halftone.png -------------------------------------------------------------------------------- /src/assets/img/netease-music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtr-static-official/Helium-Music/ac561d633e70c9d16dddec46981b98c212dc3890/src/assets/img/netease-music.png -------------------------------------------------------------------------------- /src/components/ChildrenFolder.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/DataCheckAnimaton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/GlobalDialog.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | 39 | 147 | 148 | -------------------------------------------------------------------------------- /src/components/GlobalNotice.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/LibraryAlbumList.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/LibraryMVList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/LocalMusicClassify.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/LocalMusicList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/LoginContent.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 96 | 97 | -------------------------------------------------------------------------------- /src/components/NewestSong.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/PlayerVideo.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/RecList.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/RecommendSongs.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/SearchInput.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/SearchResultList.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/Selector.vue: -------------------------------------------------------------------------------- 1 | 32 | 76 | 77 | 159 | 181 | -------------------------------------------------------------------------------- /src/components/Title.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/VideoPlayer.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/WindowControl.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | -------------------------------------------------------------------------------- /src/electron/dirTree.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { parseFile } = require('music-metadata'); 4 | const { nanoid } = require('nanoid') 5 | /** 6 | * 函数作用: 初始化 7 | * @returns 处理完的对象 8 | */ 9 | const musicType = ['.aiff', '.aac', '.ape', '.asf', '.bwf', '.dsdiff', '.dsf', '.flac', '.mp2','.matroska', '.mp3', '.mpc', '.mpeg4', '.ogg', '.opus', '.speex', '.theora', '.vorbis', '.wav', '.webm', '.wv', '.wma', '.m4a'] 10 | let getType = null 11 | 12 | module.exports = async function getDirTree(baseDir, type, win) { 13 | getType = type 14 | let dirPath = path.resolve(__dirname, baseDir); 15 | let all = { 16 | 'name': baseDir, 17 | "children":[], 18 | 'type': 'folder', 19 | 'dirPath': dirPath 20 | } 21 | let count = 0 22 | // 文件数组 23 | let res = fs.readdirSync(dirPath); 24 | //all里的children数组 25 | let temp = await getFileJson(res, all.children, dirPath) 26 | all.children = temp; 27 | all.count = count 28 | count = null 29 | return all 30 | 31 | /** 32 | * @param {A路径下的文件数组} res 33 | * @param {children数组} arr 34 | * @param {A路径} dir 35 | * @returns children数组 36 | */ 37 | async function getFileJson(res,arr,dir) { 38 | for (let i = 0; i < res.length; i++) { 39 | let tempDir = `${dir}\\${res[i]}`; 40 | await newObj(tempDir, res[i]).then(async (obj) => { 41 | if(obj != null) 42 | arr.push(obj); 43 | if(obj != null && obj.children?.length == 0) { 44 | let dirValArr = fs.readdirSync(tempDir); 45 | return await getFileJson(dirValArr,obj.children, obj.dirPath) 46 | } 47 | }) 48 | } 49 | return arr 50 | } 51 | // 处理该目录下生成的obj是否带有children 52 | /** 53 | * 处理添加到children数组下的对象属性 54 | * @param {B路径 = A路径 + 文件名} tempDir 55 | * @param {文件名} item 56 | * @returns 返回处理好的对象 57 | */ 58 | async function newObj(tempDir,item) { 59 | let obj = { 60 | name: item, 61 | dirPath: tempDir 62 | } 63 | //判断路径是否为文件夹 64 | if(! fs.statSync(tempDir).isFile()){ 65 | obj.children = []; 66 | obj.type = 'folder' 67 | } else { 68 | if(getType == 'dir') return null 69 | if(musicType.indexOf(path.extname(tempDir).toLowerCase()) == -1) return null 70 | const result = await parseFile(tempDir) 71 | obj.id = nanoid() 72 | obj.common = { 73 | localTitle: path.basename(tempDir, path.extname(tempDir)), 74 | fileUrl: tempDir, 75 | title: result.common.title, 76 | artists: result.common.artists, 77 | album: result.common.album, 78 | albumartist: result.common.albumartist, 79 | date: result.common.date, 80 | genre: result.common.genre, 81 | year: result.common.year, 82 | } 83 | obj.format = { 84 | bitrate: result.format.bitrate, 85 | bitsPerSample: result.format.bitsPerSample, 86 | container: result.format.container, 87 | duration: result.format.duration, 88 | sampleRate: result.format.sampleRate, 89 | } 90 | win.webContents.send('local-music-count', count++) 91 | } 92 | return obj 93 | } 94 | } -------------------------------------------------------------------------------- /src/electron/download.js: -------------------------------------------------------------------------------- 1 | const { ipcMain } = require('electron') 2 | const Store = require('electron-store'); 3 | module.exports = MusicDownload = (win) => { 4 | const settingsStore = new Store({name: 'settings'}) 5 | let isClose = false 6 | let downloadObj = { 7 | downloadUrl: '', 8 | fileName: '', 9 | type: '', 10 | savePath: '' 11 | } 12 | ipcMain.on('download', async (event, args) => { 13 | downloadObj.fileName = args.name 14 | downloadObj.downloadUrl = args.url 15 | downloadObj.type = args.type 16 | const savePath = await settingsStore.get('settings') 17 | downloadObj.savePath = savePath.local.downloadFolder + '\\' 18 | win.webContents.downloadURL(downloadObj.downloadUrl) 19 | }) 20 | 21 | win.webContents.session.on('will-download', (event, item, webContents) => { 22 | item.setSavePath(downloadObj.savePath + downloadObj.fileName + '.' + downloadObj.type) 23 | 24 | const totalBytes = item.getTotalBytes(); 25 | 26 | console.log(item.getURL()) 27 | console.log(totalBytes) 28 | console.log(item.getSavePath()) 29 | 30 | let interruptedTimes = 0 31 | item.on('updated', (event, state) => { 32 | let progress = item.getReceivedBytes() / totalBytes 33 | progress = Math.round(progress * 100) 34 | win.setProgressBar(progress / 100); 35 | 36 | if (state === 'interrupted') { 37 | console.log('Download is interrupted but can be resumed') 38 | let alterPath = (downloadObj.savePath + downloadObj.fileName) 39 | if(alterPath.indexOf('"') != -1 || alterPath.indexOf('?') != -1 || alterPath.indexOf('<') != -1 || alterPath.indexOf('>') != -1 || alterPath.indexOf(':') != -1) { 40 | interruptedTimes++ 41 | alterPath = alterPath.replaceAll('"', """) 42 | alterPath = alterPath.replaceAll('?', "?") 43 | alterPath = alterPath.replaceAll('<', "<") 44 | alterPath = alterPath.replaceAll('>', ">") 45 | alterPath = alterPath.replaceAll(':', ":") 46 | item.setSavePath(alterPath + '.' + downloadObj.type) 47 | if(interruptedTimes > 3) { 48 | item.setSavePath(downloadObj.savePath + 'undefined_name' + Math.trunc(Math.random(0,1) * 10000) + '.' + downloadObj.type) 49 | interruptedTimes = 0 50 | } 51 | item.resume() 52 | } 53 | 54 | } else if (state === 'progressing') { 55 | if (item.isPaused()) { 56 | console.log('Download is paused') 57 | } else { 58 | console.log(progress) 59 | } 60 | } 61 | win.webContents.send('download-progress', progress) 62 | }) 63 | item.once('done', (event, state) => { 64 | if (state === 'completed') { 65 | console.log('Download successfully') 66 | } else { 67 | console.log(`Download failed: ${state}`) 68 | } 69 | if (!win.isDestroyed()) { 70 | win.setProgressBar(-1); 71 | } 72 | if(!isClose) win.webContents.send('download-next') 73 | }) 74 | ipcMain.on('download-resume', () => { 75 | item.resume() 76 | }) 77 | ipcMain.on('download-pause', (close) => { 78 | if(close == 'shutdown') { 79 | isClose = true 80 | item.cancel() 81 | } 82 | else item.pause() 83 | }) 84 | ipcMain.on('download-cancel', () => { 85 | item.cancel() 86 | }) 87 | }) 88 | } -------------------------------------------------------------------------------- /src/electron/localmusic.js: -------------------------------------------------------------------------------- 1 | const { ipcMain }= require('electron') 2 | const getDirTree = require('./dirTree') 3 | const Store = require('electron-store'); 4 | 5 | 6 | module.exports = function LocalFiles(win, app) { 7 | const settingsStore = new Store({name: 'settings'}) 8 | const localStore = new Store({name: 'localMusic'}) 9 | 10 | function sendLocalFiles(dirTree, metadata, type, count) { 11 | let localData = { 12 | dirTree: dirTree, 13 | locaFilesMetadata: metadata, 14 | type: type, 15 | count: count 16 | } 17 | win.webContents.send('local-music-files', localData) 18 | } 19 | 20 | async function readLocalFiles(type, refresh) { 21 | let dirTree = [] 22 | let metadata = [] 23 | if(refresh || (!localStore.get('localFiles.downloaded') && type == 'downloaded') || (!localStore.get('localFiles.local') && type == 'local')) { 24 | let baseUrl = [] 25 | if(type == 'downloaded') { 26 | let url = await settingsStore.get('settings') 27 | baseUrl.push(url.local.downloadFolder) 28 | } else if(type == 'local') { 29 | let url = await settingsStore.get('settings') 30 | baseUrl = url.local.localFolder 31 | } 32 | let count = 0 33 | for (let i = 0; i < baseUrl.length; i++) { 34 | dirTree.push(await getDirTree(baseUrl[i], 'dir')) 35 | metadata.push(await getDirTree(baseUrl[i], 'data', win)) 36 | count += metadata[i].count 37 | } 38 | sendLocalFiles(dirTree, metadata, type, count) 39 | count = null 40 | let localData = { 41 | dirTree: dirTree, 42 | locaFilesMetadata: metadata 43 | } 44 | if(type == 'downloaded') { 45 | localStore.set('localFiles.downloaded', localData) 46 | } else if(type == 'local') { 47 | localStore.set('localFiles.local', localData) 48 | } 49 | } else { 50 | if(type == 'downloaded') { 51 | const data = await localStore.get('localFiles.downloaded') 52 | data.type = 'downloaded' 53 | win.webContents.send('local-music-files', data) 54 | } else if(type == 'local') { 55 | const data =await localStore.get('localFiles.local') 56 | data.type = 'local' 57 | win.webContents.send('local-music-files', data) 58 | } 59 | 60 | } 61 | } 62 | ipcMain.on('scan-local-music', (e, params) => { 63 | readLocalFiles(params.type, params.refresh) 64 | }) 65 | ipcMain.on('clear-local-music-data', (e, type) => { 66 | if(type == 'downloaded') { 67 | localStore.set('localFiles.downloaded', null) 68 | } else if(type == 'local') { 69 | localStore.set('localFiles.local', null) 70 | } 71 | }) 72 | } -------------------------------------------------------------------------------- /src/electron/preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron') 2 | 3 | function windowMin() { 4 | ipcRenderer.send('window-min') 5 | } 6 | function windowMax() { 7 | ipcRenderer.send('window-max') 8 | } 9 | function windowClose() { 10 | ipcRenderer.send('window-close') 11 | } 12 | function toRegister(url) { 13 | ipcRenderer.send('to-register', url) 14 | } 15 | function beforeQuit(callback) { 16 | ipcRenderer.on('player-save', callback) 17 | } 18 | function exitApp(playlist) { 19 | ipcRenderer.send('exit-app', playlist) 20 | } 21 | function startDownload() { 22 | ipcRenderer.send('download-start') 23 | } 24 | function download(url) { 25 | ipcRenderer.send('download', url) 26 | } 27 | function downloadNext(callback) { 28 | ipcRenderer.on('download-next', callback) 29 | } 30 | function downloadProgress(callback) { 31 | ipcRenderer.on('download-progress', callback) 32 | } 33 | function downloadPause(close) { 34 | ipcRenderer.send('download-pause', close) 35 | } 36 | function downloadResume() { 37 | ipcRenderer.send('download-resume') 38 | } 39 | function downloadCancel() { 40 | ipcRenderer.send('download-cancel') 41 | } 42 | function lyricControl(callback) { 43 | ipcRenderer.on('lyric-control', callback) 44 | } 45 | function scanLocalMusic(type) { 46 | ipcRenderer.send('scan-local-music', type) 47 | } 48 | function localMusicFiles(callback) { 49 | ipcRenderer.on('local-music-files', callback) 50 | callback = null 51 | } 52 | function localMusicCount(callback) { 53 | ipcRenderer.on('local-music-count', callback) 54 | } 55 | function playOrPauseMusic(callback) { 56 | ipcRenderer.on('music-playing-control', callback) 57 | } 58 | function playOrPauseMusicCheck(playing) { 59 | ipcRenderer.send('music-playing-check', playing) 60 | } 61 | function lastOrNextMusic(callback) { 62 | ipcRenderer.on('music-song-control', callback) 63 | } 64 | function changeMusicPlaymode(callback) { 65 | ipcRenderer.on('music-playmode-control', callback) 66 | } 67 | function changeTrayMusicPlaymode(mode) { 68 | ipcRenderer.send('music-playmode-tray-change', mode) 69 | } 70 | function volumeUp(callback) { 71 | ipcRenderer.on('music-volume-up', callback) 72 | } 73 | function volumeDown(callback) { 74 | ipcRenderer.on('music-volume-down', callback) 75 | } 76 | function musicProcessControl(callback) { 77 | ipcRenderer.on('music-process-control', callback) 78 | } 79 | function hidePlayer(callback) { 80 | ipcRenderer.on('hide-player', callback) 81 | } 82 | function setSettings(settings) { 83 | ipcRenderer.send('set-settings', settings) 84 | } 85 | function clearLocalMusicData(type) { 86 | ipcRenderer.send('clear-local-music-data', type) 87 | } 88 | function registerShortcuts() { 89 | ipcRenderer.send('register-shortcuts') 90 | } 91 | function unregisterShortcuts() { 92 | ipcRenderer.send('unregister-shortcuts') 93 | } 94 | function openLocalFolder(path) { 95 | ipcRenderer.send('open-local-folder', path) 96 | } 97 | function saveLastPlaylist(playlist) { 98 | ipcRenderer.send('save-last-playlist', playlist) 99 | } 100 | function downloadVideoProgress(callback) { 101 | ipcRenderer.on('download-video-progress', callback) 102 | } 103 | function cancelDownloadMusicVideo() { 104 | ipcRenderer.send('cancel-download-music-video') 105 | } 106 | function copyTxt(txt) { 107 | ipcRenderer.send('copy-txt', txt) 108 | } 109 | function checkUpdate(callback) { 110 | ipcRenderer.on('check-update', callback) 111 | } 112 | function setWindowTile(title) { 113 | ipcRenderer.send('set-window-title', title) 114 | } 115 | contextBridge.exposeInMainWorld('windowApi', { 116 | windowMin, 117 | windowMax, 118 | windowClose, 119 | toRegister, 120 | beforeQuit, 121 | exitApp, 122 | startDownload, 123 | download, 124 | downloadNext, 125 | downloadProgress, 126 | downloadPause, 127 | downloadResume, 128 | downloadCancel, 129 | lyricControl, 130 | scanLocalMusic, 131 | localMusicFiles, 132 | localMusicCount, 133 | getLocalMusicImage: (filePath) => ipcRenderer.invoke('get-image-base64', filePath), 134 | playOrPauseMusic, 135 | playOrPauseMusicCheck, 136 | lastOrNextMusic, 137 | changeMusicPlaymode, 138 | changeTrayMusicPlaymode, 139 | volumeUp, 140 | volumeDown, 141 | musicProcessControl, 142 | hidePlayer, 143 | setSettings, 144 | getSettings: () => ipcRenderer.invoke('get-settings'), 145 | openFile: () => ipcRenderer.invoke('dialog:openFile'), 146 | clearLocalMusicData, 147 | registerShortcuts, 148 | unregisterShortcuts, 149 | getLastPlaylist: () => ipcRenderer.invoke('get-last-playlist'), 150 | openLocalFolder, 151 | saveLastPlaylist, 152 | getRequestData: (request) => ipcRenderer.invoke('get-request-data', request), 153 | getBiliVideo: (request) => ipcRenderer.invoke('get-bili-video', request), 154 | downloadVideoProgress, 155 | cancelDownloadMusicVideo, 156 | musicVideoIsExists: (obj) => ipcRenderer.invoke('music-video-isexists', obj), 157 | clearUnusedVideo: (state) => ipcRenderer.invoke('clear-unused-video', state), 158 | deleteMusicVideo: (id) => ipcRenderer.invoke('delete-music-video', id), 159 | getLocalMusicLyric: (filePath) => ipcRenderer.invoke('get-local-music-lyric', filePath), 160 | copyTxt, 161 | checkUpdate, 162 | setWindowTile, 163 | }) -------------------------------------------------------------------------------- /src/electron/services.js: -------------------------------------------------------------------------------- 1 | const server = require('NeteaseCloudMusicApi/server') 2 | 3 | //启动网易云音乐API 4 | module.exports = async function startNeteaseMusicApi() { 5 | await server.serveNcmApi({ 6 | checkVersion: true, 7 | port: 36530, 8 | }); 9 | } -------------------------------------------------------------------------------- /src/electron/shortcuts.js: -------------------------------------------------------------------------------- 1 | const { Menu, globalShortcut } = require('electron') 2 | const Store = require('electron-store'); 3 | 4 | module.exports = async function registerShortcuts(win) { 5 | const settingsStore = new Store({name: 'settings'}); 6 | const shortcuts = await settingsStore.get('settings.shortcuts'); 7 | if(!shortcuts) return 8 | else if(!(shortcuts.find(shortcut => shortcut.id == 'processForward') || shortcuts.find(shortcut => shortcut.id == 'processBack'))){ 9 | shortcuts.push({ 10 | id: 'processForward', 11 | name: '快进(3s)', 12 | shortcut: 'CommandOrControl+]', 13 | globalShortcut: 'CommandOrControl+Alt+]', 14 | }, 15 | { 16 | id: 'processBack', 17 | name: '后退(3s)', 18 | shortcut: 'CommandOrControl+[', 19 | globalShortcut: 'CommandOrControl+Alt+[', 20 | }) 21 | settingsStore.set('settings.shortcuts', shortcuts); 22 | } 23 | const menu = [ 24 | { 25 | label: 'music', 26 | submenu: [ 27 | { 28 | role: 'playorpause', 29 | accelerator: 'Space', 30 | click: () => { win.webContents.send('music-playing-control') } 31 | }, 32 | { 33 | role: 'playorpause', 34 | accelerator: 'F5', 35 | click: () => { win.webContents.send('music-playing-control') } 36 | }, 37 | { 38 | role: 'playorpause', 39 | accelerator: shortcuts.find(shortcut => shortcut.id == 'play').shortcut, 40 | click: () => { win.webContents.send('music-playing-control') } 41 | }, 42 | { 43 | role: 'last', 44 | accelerator: shortcuts.find(shortcut => shortcut.id == 'last').shortcut, 45 | click: () => { win.webContents.send('music-song-control', 'last') } 46 | }, 47 | { 48 | role: 'next', 49 | accelerator: shortcuts.find(shortcut => shortcut.id == 'next').shortcut, 50 | click: () => { win.webContents.send('music-song-control', 'next') } 51 | }, 52 | { 53 | role: 'volumeUp', 54 | accelerator: shortcuts.find(shortcut => shortcut.id == 'volumeUp').shortcut, 55 | click: () => { win.webContents.send('music-volume-up', 'volumeUp') } 56 | }, 57 | { 58 | role: 'volumeDown', 59 | accelerator: shortcuts.find(shortcut => shortcut.id == 'volumeDown').shortcut, 60 | click: () => { win.webContents.send('music-volume-down', 'volumeDown') } 61 | }, 62 | { 63 | role: 'processForward', 64 | accelerator: shortcuts.find(shortcut => shortcut.id == 'processForward').shortcut, 65 | click: () => { win.webContents.send('music-process-control', 'forward') } 66 | }, 67 | { 68 | role: 'processBack', 69 | accelerator: shortcuts.find(shortcut => shortcut.id == 'processBack').shortcut, 70 | click: () => { win.webContents.send('music-process-control', 'back') } 71 | }, 72 | { 73 | role: 'hidePlayer', 74 | accelerator: 'Escape', 75 | click: () => { win.webContents.send('hide-player') } 76 | }, 77 | ] 78 | }, 79 | { 80 | label: 'Window', 81 | submenu: [ 82 | { role: 'close' }, 83 | { role: 'minimize' }, 84 | { role: 'zoom' }, 85 | { role: 'reload' }, 86 | { role: 'forcereload' }, 87 | { role: 'toggledevtools' }, 88 | { type: 'separator' }, 89 | { role: 'togglefullscreen' }, 90 | ] 91 | } 92 | ] 93 | 94 | Menu.setApplicationMenu(Menu.buildFromTemplate(menu)) 95 | 96 | globalShortcut.register('CommandOrControl+Shift+F12', () => { 97 | // 获取当前窗口并打开控制台 98 | win.webContents.openDevTools({mode: 'detach'}); 99 | }); 100 | 101 | if(!settingsStore.get('settings.other.globalShortcuts')) return 102 | globalShortcut.register(shortcuts.find(shortcut => shortcut.id == 'play').globalShortcut, () => { 103 | win.webContents.send('music-playing-control') 104 | }) 105 | globalShortcut.register(shortcuts.find(shortcut => shortcut.id == 'last').globalShortcut, () => { 106 | win.webContents.send('music-song-control', 'last') 107 | }) 108 | globalShortcut.register(shortcuts.find(shortcut => shortcut.id == 'next').globalShortcut, () => { 109 | win.webContents.send('music-song-control', 'next') 110 | }) 111 | globalShortcut.register(shortcuts.find(shortcut => shortcut.id == 'volumeUp').globalShortcut, () => { 112 | win.webContents.send('music-volume-up', 'volumeUp') 113 | }) 114 | globalShortcut.register(shortcuts.find(shortcut => shortcut.id == 'volumeDown').globalShortcut, () => { 115 | win.webContents.send('music-volume-down', 'volumeDown') 116 | }) 117 | globalShortcut.register(shortcuts.find(shortcut => shortcut.id == 'processForward').globalShortcut, () => { 118 | win.webContents.send('music-process-control', 'forward') 119 | }) 120 | globalShortcut.register(shortcuts.find(shortcut => shortcut.id == 'processBack').globalShortcut, () => { 121 | win.webContents.send('music-process-control', 'back') 122 | }) 123 | } -------------------------------------------------------------------------------- /src/electron/tray.js: -------------------------------------------------------------------------------- 1 | const { Menu, Tray, ipcMain, nativeImage } = require('electron') 2 | const path = require('path') 3 | 4 | module.exports = function InitTray(win, app, iconPath) { 5 | let tray = null 6 | let winIsShow = false 7 | let Buttons = [ 8 | { 9 | icon: nativeImage.createFromPath(path.resolve(__dirname, '../assets/icon/last.png')), 10 | tooltip: '上一首', 11 | click() { 12 | win.webContents.send('music-song-control', 'last') 13 | } 14 | }, 15 | { 16 | icon: nativeImage.createFromPath(path.resolve(__dirname, '../assets/icon/play.png')), 17 | tooltip: '播放', 18 | click() { 19 | win.webContents.send('music-playing-control') 20 | } 21 | }, 22 | { 23 | icon: nativeImage.createFromPath(path.resolve(__dirname, '../assets/icon/pause.png')), 24 | tooltip: '暂停', 25 | flags: ['hidden'], 26 | click() { 27 | win.webContents.send('music-playing-control') 28 | } 29 | }, 30 | { 31 | icon: nativeImage.createFromPath(path.resolve(__dirname, '../assets/icon/next.png')), 32 | tooltip: '下一首', 33 | click() { 34 | win.webContents.send('music-song-control', 'next') 35 | } 36 | } 37 | 38 | ] 39 | win.on('show', () => { 40 | win.setThumbarButtons(Buttons) 41 | winIsShow = true 42 | }) 43 | app.whenReady().then(() => { 44 | tray = new Tray(iconPath) 45 | const contextMenu = Menu.buildFromTemplate([ 46 | { 47 | label: '播放', 48 | click: () => { 49 | win.webContents.send('music-playing-control') 50 | } 51 | }, 52 | { 53 | label: '暂停', 54 | visible: false, 55 | click: () => { 56 | win.webContents.send('music-playing-control') 57 | } 58 | }, 59 | { 60 | label: '上一首', 61 | click: () => { 62 | win.webContents.send('music-song-control', 'last') 63 | } 64 | }, 65 | { 66 | label: '下一首', 67 | click: () => { 68 | win.webContents.send('music-song-control', 'next') 69 | } 70 | }, 71 | { 72 | label: '播放模式', 73 | submenu: [ 74 | { 75 | label: '顺序播放', 76 | type: 'radio', 77 | click: () => { 78 | win.webContents.send('music-playmode-control', 0) 79 | }, 80 | }, 81 | { 82 | label: '列表循环', 83 | type: 'radio', 84 | click: () => { 85 | win.webContents.send('music-playmode-control', 1) 86 | }, 87 | }, 88 | { 89 | label: '单曲循环', 90 | type: 'radio', 91 | click: () => { 92 | win.webContents.send('music-playmode-control', 2) 93 | }, 94 | }, 95 | { 96 | label: '随机播放', 97 | type: 'radio', 98 | click: () => { 99 | win.webContents.send('music-playmode-control', 3) 100 | } 101 | }, 102 | ] 103 | }, 104 | { 105 | label: '退出', 106 | click: () => { 107 | win.webContents.send('player-save') 108 | } 109 | } 110 | ]) 111 | tray.setToolTip('Helium Music') 112 | tray.setContextMenu(contextMenu) 113 | tray.on('double-click', function () { 114 | win.show(); 115 | }); 116 | ipcMain.on('music-playmode-tray-change', (e, mode) => { 117 | contextMenu.items[4].submenu.items[0].checked = false 118 | contextMenu.items[4].submenu.items[1].checked = false 119 | contextMenu.items[4].submenu.items[2].checked = false 120 | contextMenu.items[4].submenu.items[3].checked = false 121 | contextMenu.items[4].submenu.items[mode].checked = true 122 | }) 123 | ipcMain.on('music-playing-check', (e, playing) => { 124 | if(playing) { 125 | contextMenu.items[0].visible = false 126 | contextMenu.items[1].visible = true 127 | Buttons[1].flags = ['hidden'] 128 | Buttons[2].flags = [] 129 | } 130 | else { 131 | contextMenu.items[0].visible = true 132 | contextMenu.items[1].visible = false 133 | Buttons[1].flags = [] 134 | Buttons[2].flags = ['hidden'] 135 | } 136 | if(winIsShow) 137 | win.setThumbarButtons(Buttons) 138 | }) 139 | }) 140 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router/router.js' 4 | import pinia from './store/pinia' 5 | import { init } from './utils/initApp' 6 | import lazy from './utils/lazy' 7 | import './style.css' 8 | import 'normalize.css' 9 | import './assets/css/common.css' 10 | import './assets/css/fonts.css' 11 | const app = createApp(App) 12 | app.use(router) 13 | app.use(pinia) 14 | app.directive('lazy', lazy) 15 | app.mount('#app') 16 | init() -------------------------------------------------------------------------------- /src/router/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import { isLogin } from '../utils/authority' 3 | import { noticeOpen } from '../utils/dialog' 4 | import HomePage from '../views/HomePage.vue' 5 | import CloudDisk from '../views/CloudDisk.vue' 6 | import LoginPage from '../views/LoginPage.vue' 7 | import LoginContent from '../components/LoginContent.vue' 8 | import MyMusic from '../views/MyMusic.vue' 9 | import LibraryDetail from '../components/LibraryDetail.vue' 10 | import RecommendSongs from '../components/RecommendSongs.vue' 11 | import LocalMusicDetail from '../components/LocalMusicDetail.vue' 12 | import SearchResult from '../views/SearchResult.vue' 13 | import Settings from '../views/Settings.vue' 14 | 15 | import { useUserStore } from '../store/userStore' 16 | import { useLibraryStore } from '../store/libraryStore' 17 | import { useLocalStore } from '../store/localStore' 18 | import { storeToRefs } from 'pinia' 19 | import { useOtherStore } from '../store/otherStore' 20 | const userStore = useUserStore() 21 | const libraryStore = useLibraryStore() 22 | const { updateLibraryDetail } = libraryStore 23 | const { libraryInfo } = storeToRefs(libraryStore) 24 | const localStore = useLocalStore() 25 | const otherStore = useOtherStore() 26 | 27 | const routes = [ 28 | { 29 | path: '/', 30 | name: 'homepage', 31 | component: HomePage, 32 | beforeEnter: (to, from, next) => { 33 | if(!userStore.homePage) next({name: 'mymusic'}) 34 | else next() 35 | }, 36 | }, 37 | { 38 | path: '/cloud', 39 | name: 'clouddisk', 40 | component: CloudDisk, 41 | beforeEnter: (to, from, next) => { 42 | if(!userStore.cloudDiskPage) next({name: 'mymusic'}) 43 | else if(isLogin()) next() 44 | else {next({name: 'login'});noticeOpen("请先登录", 2)} 45 | }, 46 | }, 47 | { 48 | path: '/login', 49 | name: 'login', 50 | component: LoginPage 51 | }, 52 | { 53 | path: '/mymusic', 54 | name: 'mymusic', 55 | component: MyMusic, 56 | children: [ 57 | { 58 | path: '/mymusic/playlist/:id', 59 | name: 'playlist', 60 | component: LibraryDetail, 61 | beforeEnter: (to, from, next) => { 62 | if(!libraryInfo.value || from.name != 'playlist') updateLibraryDetail(to.params.id, to.name) 63 | next() 64 | } 65 | }, 66 | { 67 | path: '/mymusic/album/:id', 68 | name: 'album', 69 | component: LibraryDetail, 70 | beforeEnter: (to, from, next) => { 71 | if(!libraryInfo.value || from.name != 'album') updateLibraryDetail(to.params.id, to.name).then(() => { 72 | next() 73 | if(document.getElementById('libraryScroll')) 74 | document.getElementById('libraryScroll').scrollTop = 0 75 | }) 76 | } 77 | }, 78 | { 79 | path: '/mymusic/artist/:id', 80 | name: 'artist', 81 | component: LibraryDetail, 82 | beforeEnter: (to, from, next) => { 83 | if(!libraryInfo.value || from.name != 'artist') updateLibraryDetail(to.params.id, to.name) 84 | next() 85 | } 86 | }, 87 | { 88 | path: '/mymusic/playlist/rec', 89 | name: 'rec', 90 | component: RecommendSongs, 91 | beforeEnter: (to, from, next) => { 92 | if(isLogin()) { 93 | libraryStore.updateRecommendSongs().then(() => { 94 | next() 95 | }) 96 | } else { 97 | noticeOpen("请先登录", 2) 98 | next({name: 'login'}) 99 | } 100 | } 101 | }, 102 | { 103 | path: '/mymusic/local/files', 104 | name: 'localFiles', 105 | component: LocalMusicDetail, 106 | beforeEnter: (to, from, next) => { 107 | if(from.name != 'localFiles') localStore.updateLocalMusicDetail(to.name, to.query) 108 | next() 109 | } 110 | }, 111 | { 112 | path: '/mymusic/local/album/:id', 113 | name: 'localAlbum', 114 | component: LocalMusicDetail, 115 | beforeEnter: (to, from, next) => { 116 | if(from.name != 'localAlbum') localStore.updateLocalMusicDetail(to.name, null, to.params.id) 117 | next() 118 | } 119 | }, 120 | { 121 | path: '/mymusic/local/artist/:id', 122 | name: 'localArtist', 123 | component: LocalMusicDetail, 124 | beforeEnter: (to, from, next) => { 125 | if(from.name != 'localArtist') localStore.updateLocalMusicDetail(to.name, null, to.params.id) 126 | next() 127 | } 128 | }, 129 | ], 130 | beforeEnter: (to, from, next) => { 131 | if(isLogin()) next() 132 | else if((from.name == 'homepage' || from.name == 'search') && to.fullPath != '/mymusic') next() 133 | else next({name: 'login'}) 134 | }, 135 | }, 136 | { 137 | path: '/login/account', 138 | name: 'account', 139 | component: LoginContent 140 | }, 141 | { 142 | path: '/library', 143 | name: 'library', 144 | component: LibraryDetail 145 | }, 146 | { 147 | path: '/search', 148 | name: 'search', 149 | component: SearchResult, 150 | beforeEnter: (to, from, next) => { 151 | otherStore.getSearchInfo(to.query.keywords) 152 | next() 153 | } 154 | }, 155 | { 156 | path: '/settings', 157 | name: 'settings', 158 | component: Settings, 159 | beforeEnter: (to, from, next) => { 160 | next() 161 | } 162 | }, 163 | ] 164 | 165 | const router = createRouter({ 166 | history: createWebHashHistory(), 167 | routes, 168 | }) 169 | 170 | export default router -------------------------------------------------------------------------------- /src/store/cloudStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | export const useCloudStore= defineStore('cloudStore', { 4 | state: () => { 5 | return { 6 | count: null, 7 | size: null, 8 | maxSize: null, 9 | cloudSongs: null 10 | } 11 | }, 12 | actions: { 13 | updateCloudSongs(list) { 14 | this.cloudSongs = list 15 | } 16 | }, 17 | }) -------------------------------------------------------------------------------- /src/store/libraryStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { getPlaylistDetail, getPlaylistAll, getRecommendSongs, playlistDynamic } from '../api/playlist' 3 | import { getAlbumDetail, albumDynamic } from '../api/album' 4 | import { getArtistDetail, getArtistFansCount, getArtistTopSong, getArtistAlbum } from '../api/artist' 5 | import { getArtistMV } from '../api/mv' 6 | import { mapSongsPlayableStatus } from "../utils/songStatus"; 7 | 8 | export const useLibraryStore = defineStore('libraryStore', { 9 | state: () => { 10 | return { 11 | listType1: 0, 12 | listType2: 0, 13 | artistPageType: 0, 14 | libraryList: null, 15 | libraryListAlbum: null, 16 | libraryListAritist: null, 17 | playlistCount: null, 18 | playlistUserCreated: null, 19 | playlistUserSub: null, 20 | libraryInfo: null, 21 | librarySongs: null, 22 | libraryAlbum: null, 23 | libraryMV: null, 24 | needTimestamp: [], 25 | libraryChangeAnimation: false, 26 | } 27 | }, 28 | actions: { 29 | changeAnimation() { 30 | this.libraryChangeAnimation = true 31 | }, 32 | changeLibraryList(type) { 33 | if(type == 0) this.libraryList = this.playlistUserCreated 34 | else if (type == 1) this.libraryList = this.playlistUserSub 35 | }, 36 | updateLibrary(libraryData) { 37 | this.libraryData = libraryData 38 | }, 39 | updateUserPlaylistCount(listCount) { 40 | this.playlistCount = listCount 41 | }, 42 | updateUserPlaylist(playlist) { 43 | this.playlistUserCreated = playlist.splice(0, this.playlistCount.createdPlaylistCount) 44 | this.playlistUserSub = playlist.splice(0, this.playlistCount.subPlaylistCount) 45 | }, 46 | async updateLibraryDetail(id, routerName) { 47 | this.changeAnimation() 48 | if(routerName == 'playlist') await this.updatePlaylistDetail(id) 49 | if(routerName == 'album') await this.updateAlbumDetail(id) 50 | if(routerName == 'artist') await this.updateArtistDetail(id) 51 | this.artistPageType = 0 52 | this.libraryAlbum = null 53 | this.libraryMV = null 54 | }, 55 | async updatePlaylistDetail(id) { 56 | let params = { 57 | id: id, 58 | limit: 1000, 59 | offset: 0, 60 | // timestamp: new Date().getTime() 61 | } 62 | await Promise.all([getPlaylistDetail(params), getPlaylistAll(params), playlistDynamic(id)]).then(async results => { 63 | this.libraryInfo = results[0].playlist 64 | this.librarySongs = results[1].songs 65 | this.librarySongs = mapSongsPlayableStatus(results[1].songs, results[1].privileges) 66 | if(results[0].playlist.trackIds.length > 1000) { 67 | for (let i = 1; i < (results[0].playlist.trackIds.length / 1000); i++) { 68 | const params = { 69 | id: id, 70 | limit: 1000, 71 | offset: i * 1000, 72 | } 73 | const res = await getPlaylistAll(params) 74 | const songs = mapSongsPlayableStatus(res.songs, res.privileges) 75 | this.librarySongs = this.librarySongs.concat(songs) 76 | } 77 | } 78 | this.libraryInfo.followed = results[2].subscribed 79 | this.libraryChangeAnimation = false 80 | }) 81 | }, 82 | async updateAlbumDetail(id) { 83 | let params = { 84 | id: id, 85 | // timestamp: new Date().getTime() 86 | } 87 | await Promise.all([getAlbumDetail(params), albumDynamic(id)]).then(results => { 88 | this.libraryInfo = results[0].album 89 | this.librarySongs = mapSongsPlayableStatus(results[0].songs) 90 | this.libraryInfo.followed = results[1].isSub 91 | this.libraryChangeAnimation = false 92 | }) 93 | }, 94 | async updateArtistDetail(id) { 95 | let params = { 96 | id: id, 97 | // timestamp: new Date().getTime() 98 | } 99 | await Promise.all([getArtistDetail(params), getArtistFansCount(id)]).then(results => { 100 | results[0].artist.follow = results[1].data 101 | results[0].artist.followed = results[1].data.follow 102 | this.libraryInfo = results[0].artist 103 | this.librarySongs = mapSongsPlayableStatus(results[0].hotSongs) 104 | this.libraryChangeAnimation = false 105 | }) 106 | }, 107 | //获取歌手热门歌曲前50首,并更新Store数据 108 | async updateArtistTopSong(id) { 109 | let params = { 110 | id: id, 111 | // timestamp: new Date().getTime() 112 | } 113 | await getArtistTopSong(params).then(result => { 114 | this.librarySongs = mapSongsPlayableStatus(result.songs) 115 | }) 116 | }, 117 | //获取歌手专辑,并更新Store数据 118 | async updateArtistAlbum(id) { 119 | let params = { 120 | id: id, 121 | limit: 500, 122 | offset: 0 123 | // timestamp: new Date().getTime() 124 | } 125 | await getArtistAlbum(params).then(result => { 126 | this.libraryAlbum = result.hotAlbums 127 | }) 128 | }, 129 | //获取歌手MV,并更新Store数据 130 | async updateArtistsMV(id) { 131 | let params = { 132 | id: id, 133 | limit: 500, 134 | offset: 0 135 | // timestamp: new Date().getTime() 136 | } 137 | await getArtistMV(params).then(result => { 138 | this.libraryMV = result.mvs 139 | }) 140 | }, 141 | async updateRecommendSongs() { 142 | await getRecommendSongs().then(result => { 143 | this.librarySongs = mapSongsPlayableStatus(result.data.dailySongs) 144 | }) 145 | }, 146 | }, 147 | }) -------------------------------------------------------------------------------- /src/store/localStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { noticeOpen } from "../utils/dialog"; 3 | 4 | export const useLocalStore = defineStore('localStore', { 5 | state: () => { 6 | return { 7 | isFirstDownload: true, 8 | isDownloading: false, 9 | downloadList: [], 10 | downloadedFolderSettings: null, 11 | downloadedMusicFolder: null, 12 | downloadedFiles: null, 13 | localFolderSettings: [], 14 | localMusicFolder: null, 15 | localMusicList: null, 16 | localMusicClassify: null, 17 | 18 | currentSelectedFile: {name: null}, 19 | 20 | currentType: null, 21 | currentSelectedInfo: null, 22 | currentSelectedSongs: null, 23 | currentSelectedFilePicUrl: null, 24 | isRefreshLocalFile: false, 25 | 26 | quitApp: null, 27 | } 28 | }, 29 | actions: { 30 | //对象数组去重(根据batch参数去重) 31 | removedup(arr, batch) { 32 | if (!Array.isArray(arr)) { 33 | return arr; 34 | } 35 | if (arr.length == 0) { 36 | return []; 37 | } 38 | let obj = {}; 39 | let uniqueArr = arr.reduce(function (total, item) { 40 | obj[item[batch]] ? '' : (obj[item[batch]] = true && total.push(item)); 41 | return total; 42 | }, []); 43 | return uniqueArr; 44 | }, 45 | updateDownloadList(list) { 46 | if(!this.downloadedFolderSettings) {noticeOpen("请先在设置中设置下载目录", 2);return} 47 | this.downloadList = this.downloadList.concat(list) 48 | this.downloadList = this.removedup(this.downloadList, 'id') 49 | if(!this.isDownloading && this.isFirstDownload) { 50 | windowApi.startDownload() 51 | this.isFirstDownload = false 52 | } 53 | noticeOpen('已添加到下载列表', 2) 54 | }, 55 | getSongs(arr) { 56 | arr.forEach(song => { 57 | if(song.children) this.getSongs(song.children) 58 | else { 59 | this.currentSelectedSongs.push(song) 60 | } 61 | }) 62 | }, 63 | getFolderSongs(arr, folderName) { 64 | arr.forEach(item => { 65 | if(item.name == folderName) { 66 | this.currentSelectedInfo = { 67 | name: item.name, 68 | dirPath: item.dirPath 69 | } 70 | this.currentSelectedSongs = [] 71 | this.getSongs(item.children) 72 | return 73 | } else if(item.children) this.getFolderSongs(item.children, folderName) 74 | 75 | }); 76 | }, 77 | async getImgBase64(fileUrl) { 78 | return await windowApi.getLocalMusicImage(fileUrl) 79 | }, 80 | updateLocalMusicDetail(type, query, id) { 81 | this.currentType = type 82 | if(type == 'localFiles') { 83 | if(query.type == 'downloaded') 84 | this.getFolderSongs(this.downloadedFiles, query.name) 85 | if(query.type == 'local') 86 | this.getFolderSongs(this.localMusicList, query.name) 87 | } 88 | if(type == 'localAlbum') { 89 | const index = (this.localMusicClassify.albums || []).findIndex((item) => item.id == id) 90 | this.currentSelectedInfo = { 91 | id: this.localMusicClassify.albums[index].id, 92 | name: this.localMusicClassify.albums[index].name 93 | } 94 | this.currentSelectedSongs = this.localMusicClassify.albums[index].songs 95 | if(this.currentSelectedSongs) 96 | this.getImgBase64(this.currentSelectedSongs[0].common.fileUrl).then(res => { 97 | this.currentSelectedFilePicUrl = res 98 | }) 99 | } 100 | if(type == 'localArtist') { 101 | const index = (this.localMusicClassify.artists || []).findIndex((item) => item.id == id) 102 | this.currentSelectedInfo = { 103 | id: this.localMusicClassify.artists[index].id, 104 | name: this.localMusicClassify.artists[index].name 105 | } 106 | this.currentSelectedSongs = this.localMusicClassify.artists[index].songs 107 | if(this.currentSelectedSongs) 108 | this.getImgBase64(this.currentSelectedSongs[0].common.fileUrl).then(res => { 109 | this.currentSelectedFilePicUrl = res 110 | }) 111 | } 112 | } 113 | }, 114 | }) -------------------------------------------------------------------------------- /src/store/otherStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getMVDetail, getMVUrl } from '../api/mv' 3 | import { search } from '../api/other'; 4 | import { mapSongsPlayableStatus } from '../utils/songStatus'; 5 | 6 | export const useOtherStore = defineStore('otherStore', { 7 | state: () => { 8 | return { 9 | screenWidth: 1056, 10 | contextMenuShow: false, 11 | menuTree: null, 12 | tree1: [ 13 | { 14 | id: 1, 15 | name: '播放' 16 | }, 17 | { 18 | id: 2, 19 | name: '下一首播放' 20 | }, 21 | { 22 | id: 3, 23 | name: '下载' 24 | }, 25 | { 26 | id: 4, 27 | name: '添加到歌单' 28 | }, 29 | { 30 | id: 5, 31 | name: '从歌单中删除' 32 | } 33 | ], 34 | tree2: [ 35 | { 36 | id: 1, 37 | name: '播放' 38 | }, 39 | { 40 | id: 2, 41 | name: '下一首播放' 42 | }, 43 | { 44 | id: 3, 45 | name: '下载' 46 | }, 47 | { 48 | id: 4, 49 | name: '添加到歌单' 50 | } 51 | ], 52 | tree3: [ 53 | { 54 | id: 6, 55 | name: '新建歌单' 56 | }, 57 | { 58 | id: 7, 59 | name: '删除此歌单' 60 | } 61 | ], 62 | tree4: [ 63 | { 64 | id: 8, 65 | name: '播放' 66 | }, 67 | { 68 | id: 9, 69 | name: '下一首播放' 70 | }, 71 | { 72 | id: 10, 73 | name: '打开本地文件夹' 74 | } 75 | ], 76 | selectedPlaylist: null, 77 | selectedItem: null, 78 | addPlaylistShow: false, 79 | dialogShow: false, 80 | dialogHeader: null, 81 | dialogText: null, 82 | noticeShow: false, 83 | noticeText: null, 84 | niticeOutAnimation: false, 85 | videoPlayerShow: false, 86 | player: null, 87 | videoIsBlur: false, 88 | currentVideoId: null, 89 | videoIsFull: false, 90 | searchResult: {}, 91 | toUpdate: false, 92 | newVersion: null, 93 | } 94 | }, 95 | actions: { 96 | // setRem() { 97 | // const scale = this.screenWidth / 16 98 | // const htmlWidth = document.documentElement.clientWidth || document.body.clientWidth 99 | // const htmlDom = document.getElementsByTagName('html')[0] 100 | // htmlDom.style.fontSize = htmlWidth / scale + 'px' 101 | // }, 102 | getMvData(id) { 103 | this.videoPlayerShow = true 104 | this.currentVideoId = id 105 | getMVDetail(id).then(async(detail) => { 106 | let sources = [] 107 | let brs = detail.data.brs 108 | for (let i = 0; i < brs.length; i++) { 109 | await getMVUrl(id, brs[i].br).then(res => { 110 | sources.push({ 111 | src: res.data.url, 112 | type: "video/mp4", 113 | size: res.data.r 114 | }) 115 | }) 116 | } 117 | this.player.source = { 118 | type: 'video', 119 | title: detail.data.name, 120 | sources: sources, 121 | poster: detail.data.cover 122 | } 123 | }) 124 | }, 125 | getSearchInfo(keywords) { 126 | let types = [1, 10, 100, 1000, 1004] 127 | types.forEach(type => { 128 | let params = { 129 | keywords: keywords, 130 | type: type, 131 | } 132 | if(type == 100) params.limit = 10 133 | if(type == 1000) params.limit = 10 134 | if(type == 1004) params.limit = 10 135 | search(params).then(data => { 136 | if(type == 1) { 137 | this.searchResult.searchSongs = mapSongsPlayableStatus(data.result.songs) 138 | } 139 | if(type == 10) this.searchResult.searchAlbums = data.result.albums 140 | if(type == 100) this.searchResult.searchArtists = data.result.artists 141 | if(type == 1000) this.searchResult.searchPlaylists = data.result.playlists 142 | if(type == 1004) this.searchResult.searchMvs = data.result.mvs 143 | }) 144 | }); 145 | } 146 | }, 147 | }) 148 | -------------------------------------------------------------------------------- /src/store/pinia.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 4 | 5 | const pinia = createPinia() 6 | const app = createApp() 7 | 8 | pinia.use(piniaPluginPersistedstate) 9 | app.use(pinia) 10 | 11 | export default pinia -------------------------------------------------------------------------------- /src/store/playerStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | export const usePlayerStore = defineStore('playerStore', { 4 | state: () => { 5 | return { 6 | widgetState: true,//是否开启widget 7 | currentMusic: null,//播放列表的索引 8 | playing: false,//是否正在播放 9 | progress: 0,//进度条 10 | volume: 0.3,//音量 11 | // volumeBeforeMuted: 0,//静音前音量 12 | playMode: 0,//0为顺序播放,1为列表循环,2为单曲循环,3为随机播放 13 | listInfo: null, 14 | songList: null,//播放列表 15 | shuffledList: null,//随机播放列表 16 | shuffleIndex: 0,//随机播放列表的索引 17 | songId: null, 18 | currentIndex: 0, 19 | time: 0, //歌曲总时长 20 | quality: null, 21 | playlistWidgetShow: false, 22 | playerChangeSong: false, //player页面切换歌曲更换歌名动画, 23 | lyric: null, 24 | lyricsObjArr: null, 25 | lyricSize: null, 26 | tlyricSize: null, 27 | rlyricSize: null, 28 | lyricType: ['original'], 29 | lyricInterludeTime: null, //歌词间奏等待时间 30 | lyricShow: false, //歌词是否显示 31 | lyricEle: null,//歌词DOM 32 | isLyricDelay: true, //调整进度的时候禁止赋予delay属性 33 | localBase64Img: null, //如果是本地歌曲,获取封面 34 | forbidLastRouter: false, //在主动跳转router时禁用回到上次离开的路由的地址功能 35 | musicVideo: false, 36 | addMusicVideo: false, 37 | currentMusicVideo: null, 38 | musicVideoDOM: null, 39 | videoIsPlaying: false, 40 | playerShow: true, 41 | lyricBlur: false, 42 | } 43 | }, 44 | actions: { 45 | }, 46 | persist: { 47 | storage: localStorage, 48 | paths: ['progress','volume','playMode','shuffleIndex','listInfo','songId','currentIndex','time','quality','lyricType','musicVideo','lyricBlur'] 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /src/store/userStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | export const useUserStore = defineStore('userStore', { 4 | state: () => { 5 | return { 6 | user: null, 7 | loginMode: null, 8 | likelist: null, 9 | appOptionShow: false, 10 | biliUser: null, 11 | homePage: true, 12 | cloudDiskPage: true, 13 | } 14 | }, 15 | actions: { 16 | updateUser(userinfo) { 17 | this.user = userinfo 18 | }, 19 | }, 20 | persist: { 21 | storage: localStorage, 22 | paths: ['user','biliUser','homePage','cloudDiskPage'] 23 | }, 24 | }) -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | font-weight: 400; 5 | 6 | color: rgb(0, 0, 0); 7 | 8 | font-synthesis: none; 9 | text-rendering: optimizeLegibility; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | -webkit-text-size-adjust: 100%; 13 | } 14 | 15 | a { 16 | font-weight: 500; 17 | text-decoration: inherit; 18 | } 19 | 20 | a { 21 | font-weight: 500; 22 | text-decoration: inherit; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | 50 | button:focus, 51 | button:focus-visible { 52 | outline: 4px auto -webkit-focus-ring-color; 53 | } 54 | 55 | .card { 56 | padding: 2em; 57 | } 58 | 59 | #app { 60 | text-align: center; 61 | } -------------------------------------------------------------------------------- /src/utils/authority.js: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | 3 | export function setCookies(data, type) { 4 | console.log(data) 5 | if(type == 'account') { 6 | const cookies = data.cookie.split(';;') 7 | cookies.map(cookie => { 8 | document.cookie = cookie; 9 | const temCookie = cookie.split(';')[0].split('='); 10 | localStorage.setItem('cookie:' + temCookie[0], temCookie[1]) 11 | }); 12 | } 13 | if(type == 'qr') { 14 | const cookies = data.cookie.split(';') 15 | cookies.map(cookie => { 16 | const temCookie = cookie.split('='); 17 | if(temCookie[0] == 'MUSIC_U' || temCookie[0] == 'MUSIC_A_T' || temCookie[0] == 'MUSIC_R_T') { 18 | document.cookie = cookie; 19 | localStorage.setItem('cookie:' + temCookie[0], temCookie[1]) 20 | } 21 | }); 22 | } 23 | } 24 | 25 | //获取Cookie 26 | export function getCookie(key) { 27 | return Cookies.get(key) ?? localStorage.getItem('cookie:' + key) 28 | } 29 | 30 | //判断是否登录 31 | export function isLogin() { 32 | return (getCookie('MUSIC_U') != undefined) 33 | } -------------------------------------------------------------------------------- /src/utils/dialog.js: -------------------------------------------------------------------------------- 1 | import { useOtherStore } from '../store/otherStore'; 2 | import { storeToRefs } from 'pinia'; 3 | const otherStore = useOtherStore() 4 | const { dialogShow, dialogHeader, dialogText, noticeShow, noticeText, niticeOutAnimation } = storeToRefs(otherStore) 5 | 6 | let currentCallback = null 7 | 8 | export function dialogOpen(header, text, callback) { 9 | dialogShow.value = true 10 | currentCallback = callback 11 | dialogSetter(header, text) 12 | } 13 | export function dialogClose() { 14 | dialogShow.value = false 15 | } 16 | export function dialogSetter(header, text) { 17 | dialogHeader.value = header 18 | dialogText.value = text 19 | } 20 | export function dialogClear() { 21 | dialogHeader.value = null 22 | dialogText.value = null 23 | } 24 | export function dialogCancel() { 25 | currentCallback(false) 26 | dialogClose() 27 | } 28 | export function dialogConfirm() { 29 | currentCallback(true) 30 | dialogClose() 31 | } 32 | 33 | let noticeTimer1 = null 34 | let noticeTimer2 = null 35 | export function noticeOpen(text, duration) { 36 | noticeShow.value = false 37 | niticeOutAnimation.value = false 38 | clearTimeout(noticeTimer1) 39 | clearTimeout(noticeTimer2) 40 | noticeShow.value = true 41 | noticeText.value = text 42 | 43 | noticeTimer1 = setTimeout(() => { 44 | niticeOutAnimation.value = true 45 | clearTimeout(noticeTimer1) 46 | noticeTimer2 = setTimeout(() => { 47 | noticeShow.value = false 48 | niticeOutAnimation.value = false 49 | clearTimeout(noticeTimer2) 50 | }, 300); 51 | }, duration * 1000); 52 | } -------------------------------------------------------------------------------- /src/utils/domHandler.js: -------------------------------------------------------------------------------- 1 | export const absolutePosition = (element, target) => { 2 | if (element) { 3 | let elementDimensions = { 4 | width: element.offsetWidth, 5 | height: element.offsetHeight || Math.min(parseInt(element.style.maxHeight.slice(0, -2)), element.scrollHeight + 16), 6 | }; 7 | let elementOuterHeight = elementDimensions.height; 8 | let elementOuterWidth = elementDimensions.width; 9 | let targetOuterHeight = target.offsetHeight; 10 | let targetOuterWidth = target.offsetWidth; 11 | let targetOffset = target.getBoundingClientRect(); 12 | let windowScrollTop = getWindowScrollTop(); 13 | let windowScrollLeft = getWindowScrollLeft(); 14 | let viewport = getViewport(); 15 | let top, left; 16 | 17 | if ( 18 | targetOffset.top + targetOuterHeight + elementOuterHeight > 19 | viewport.height 20 | ) { 21 | top = targetOffset.top + windowScrollTop - elementOuterHeight; 22 | element.style.transformOrigin = "bottom"; 23 | 24 | if (top < 0) { 25 | top = windowScrollTop; 26 | } 27 | } else { 28 | top = targetOuterHeight + targetOffset.top + windowScrollTop + 1; 29 | element.style.transformOrigin = "top"; 30 | } 31 | 32 | if (targetOffset.left + elementOuterWidth > viewport.width) 33 | left = Math.max( 34 | 0, 35 | targetOffset.left + 36 | windowScrollLeft + 37 | targetOuterWidth - 38 | elementOuterWidth 39 | ); 40 | else left = targetOffset.left + windowScrollLeft; 41 | 42 | element.style.top = top + "px"; 43 | element.style.left = left + "px"; 44 | } 45 | }; 46 | export const getWindowScrollTop = () => { 47 | let doc = document.documentElement; 48 | 49 | return (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); 50 | }; 51 | export const getWindowScrollLeft = () => { 52 | let doc = document.documentElement; 53 | 54 | return (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); 55 | }; 56 | export const getViewport = () => { 57 | let win = window, 58 | d = document, 59 | e = d.documentElement, 60 | g = d.getElementsByTagName("body")[0], 61 | w = win.innerWidth || e.clientWidth || g.clientWidth, 62 | h = win.innerHeight || e.clientHeight || g.clientHeight; 63 | 64 | return { width: w, height: h }; 65 | }; -------------------------------------------------------------------------------- /src/utils/handle.js: -------------------------------------------------------------------------------- 1 | import pinia from '../store/pinia' 2 | import { setCookies } from '../utils/authority' 3 | import { getUserProfile } from '../api/user' 4 | import { getUserLikelist } from './initApp' 5 | import { useUserStore } from '../store/userStore' 6 | 7 | const userStore = useUserStore(pinia) 8 | const { updateUser } = userStore 9 | 10 | //处理登录后的用户数据 11 | export function loginHandle(data, type) { 12 | setCookies(data, type) 13 | getUserProfile().then(result => { 14 | updateUser(result.profile) 15 | getUserLikelist() 16 | }) 17 | } -------------------------------------------------------------------------------- /src/utils/initApp.js: -------------------------------------------------------------------------------- 1 | import pinia from '../store/pinia' 2 | import { isLogin } from '../utils/authority' 3 | import { loadLastSong } from './player' 4 | import { scanMusic } from './locaMusic' 5 | import { getUserProfile, getLikelist } from '../api/user' 6 | import { useUserStore } from '../store/userStore' 7 | import { usePlayerStore } from '../store/playerStore' 8 | import { useLocalStore } from '../store/localStore' 9 | import { storeToRefs } from 'pinia' 10 | 11 | const userStore = useUserStore(pinia) 12 | const playerStore = usePlayerStore() 13 | const { quality, lyricSize, tlyricSize, rlyricSize, lyricInterludeTime } = storeToRefs(playerStore) 14 | const localSotre = useLocalStore() 15 | const { updateUser } = userStore 16 | 17 | export const initSettings = () => { 18 | windowApi.getSettings().then(settings => { 19 | quality.value = settings.music.level 20 | lyricSize.value = settings.music.lyricSize 21 | tlyricSize.value = settings.music.tlyricSize 22 | rlyricSize.value = settings.music.rlyricSize 23 | lyricInterludeTime.value = settings.music.lyricInterlude 24 | localSotre.downloadedFolderSettings = settings.local.downloadFolder 25 | localSotre.localFolderSettings = settings.local.localFolder 26 | localSotre.quitApp = settings.other.quitApp 27 | if(localSotre.downloadedFolderSettings && !localSotre.downloadedMusicFolder) { 28 | scanMusic({type:'downloaded',refresh:false}) 29 | } 30 | if(localSotre.localFolderSettings.length != 0 && !localSotre.localMusicFolder) { 31 | scanMusic({type:'local',refresh:false}) 32 | } 33 | if(!localSotre.downloadedFolderSettings && localSotre.downloadedMusicFolder) { 34 | localSotre.downloadedMusicFolder = null 35 | localSotre.downloadedFiles = null 36 | windowApi.clearLocalMusicData('downloaded') 37 | } 38 | if(localSotre.localFolderSettings.length == 0 && localSotre.localMusicFolder) { 39 | localSotre.localMusicFolder = null, 40 | localSotre.localMusicList = null 41 | localSotre.localMusicClassify = null 42 | windowApi.clearLocalMusicData('local') 43 | } 44 | }) 45 | } 46 | export const getUserLikelist = () => { 47 | if(userStore.user.userId) 48 | getLikelist(userStore.user.userId).then(result => { 49 | userStore.likelist = result.ids 50 | }) 51 | else { 52 | userStore.likelist = [] 53 | } 54 | } 55 | //初始化 56 | export const init = () => { 57 | initSettings() 58 | loadLastSong() 59 | if(isLogin()) { 60 | getUserProfile().then(result => { 61 | updateUser(result.profile) 62 | getUserLikelist() 63 | }) 64 | } else { 65 | window.localStorage.clear() 66 | } 67 | } -------------------------------------------------------------------------------- /src/utils/lazy.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted(el) { 3 | const imgSrc = el.src 4 | el.src = '' 5 | const observer = new IntersectionObserver(([{isIntersecting}]) => { 6 | if(isIntersecting) { 7 | el.src = imgSrc 8 | observer.unobserve(el) 9 | } 10 | }) 11 | observer.observe(el) 12 | } 13 | } -------------------------------------------------------------------------------- /src/utils/locaMusic.js: -------------------------------------------------------------------------------- 1 | import pinia from '../store/pinia' 2 | import { useLocalStore } from '../store/localStore' 3 | import { storeToRefs } from 'pinia' 4 | import { nanoid } from 'nanoid' 5 | import { noticeOpen } from './dialog' 6 | 7 | const localStore = useLocalStore(pinia) 8 | const { downloadedMusicFolder, downloadedFiles, localMusicFolder, localMusicList, localMusicClassify, isRefreshLocalFile } = storeToRefs(localStore) 9 | let artistArr = [] 10 | let albumArr = [] 11 | 12 | function classifyAdd(song) { 13 | if(song.common.artists == undefined) song.common.artists = ['其他'] 14 | else if(song.common.artists[0].indexOf(',') != -1) song.common.artists = song.common.artists[0].split(', ') 15 | song.common.artists.forEach(artist => { 16 | let index = (artistArr || []).findIndex((item) => item.name == artist) 17 | if(index == -1) { 18 | artistArr.push({ 19 | id: nanoid(), 20 | type: 'artist', 21 | name: artist, 22 | songs: [song] 23 | }) 24 | } else { 25 | artistArr[index].songs.push(song) 26 | } 27 | }) 28 | 29 | if(song.common.album == undefined) song.common.album = '其他' 30 | let index = (albumArr || []).findIndex((item) => item.name == song.common.album) 31 | if(index == -1) { 32 | albumArr.push({ 33 | id: nanoid(), 34 | type: 'album', 35 | name: song.common.album, 36 | songs: [song] 37 | }) 38 | } else { 39 | albumArr[index].songs.push(song) 40 | } 41 | } 42 | 43 | function classify(arr) { 44 | arr.forEach(item => { 45 | if(item.children) classify(item.children) 46 | else classifyAdd(item) 47 | }) 48 | let result = { 49 | artists: artistArr, 50 | albums: albumArr 51 | } 52 | return result 53 | } 54 | 55 | export function scanMusic(params) { 56 | if(isRefreshLocalFile.value) 57 | noticeOpen("正在扫描本地音乐,请稍等", 3) 58 | windowApi.scanLocalMusic(params) 59 | } 60 | windowApi.localMusicCount((event, count) => { 61 | noticeOpen('已扫描' + count + '首', 2) 62 | }) 63 | windowApi.localMusicFiles((event, localData) => { 64 | if(localData.type == 'downloaded') { 65 | downloadedMusicFolder.value = localData.dirTree 66 | downloadedFiles.value = localData.locaFilesMetadata 67 | } 68 | if(localData.type == 'local') { 69 | localMusicFolder.value = localData.dirTree 70 | localMusicList.value = localData.locaFilesMetadata 71 | artistArr = [] 72 | albumArr = [] 73 | localMusicClassify.value = classify(localData.locaFilesMetadata) 74 | } 75 | if(isRefreshLocalFile.value) { 76 | noticeOpen("扫描完毕 共" + localData.count + '首', 3) 77 | isRefreshLocalFile.value = false 78 | } 79 | }) -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getCookie, isLogin } from '../utils/authority' 3 | import pinia from "../store/pinia"; 4 | import { useLibraryStore } from '../store/libraryStore' 5 | 6 | const libraryStore = useLibraryStore(pinia) 7 | 8 | import { noticeOpen } from "./dialog"; 9 | const request = axios.create({ 10 | baseURL: 'http://localhost:36530', 11 | withCredentials: true, 12 | timeout: 10000, 13 | }); 14 | 15 | // 请求拦截器 16 | request.interceptors.request.use(function (config) { 17 | if(config.url != '/login/qr/check' && isLogin()) 18 | config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`; 19 | if(libraryStore.needTimestamp.indexOf(config.url) != -1) { 20 | config.params.timestamp = new Date().getTime() 21 | } 22 | // 在发送请求之前做些什么 23 | return config; 24 | }, function (error) { 25 | // 对请求错误做些什么 26 | noticeOpen("发起请求错误", 2) 27 | return Promise.reject(error); 28 | }); 29 | 30 | // 响应拦截器 31 | request.interceptors.response.use(function (response) { 32 | return response.data 33 | }, function (error) { 34 | noticeOpen("请求错误", 2) 35 | return error; 36 | }); 37 | 38 | export default request; -------------------------------------------------------------------------------- /src/utils/songStatus.js: -------------------------------------------------------------------------------- 1 | import { useUserStore } from "../store/userStore" 2 | import { isLogin } from "./authority" 3 | 4 | const { user } = useUserStore() 5 | function checkSongPlayable(song, _privilege) { 6 | let privilege = _privilege; 7 | if(privilege === undefined){ 8 | privilege = song?.privilege 9 | } 10 | 11 | let status = { 12 | playable: true, 13 | reason: '' 14 | } 15 | if (song?.privilege?.pl > 0) 16 | return status 17 | 18 | if (song.fee === 1 || privilege?.fee === 1) { 19 | status.vipOnly = true 20 | // 非VIP会员 21 | if (!(isLogin() && user.vipType === 11)) { 22 | status.playable = false 23 | status.reason = '仅限 VIP 会员' 24 | } 25 | } else if (song.fee === 4 || privilege?.fee === 4) { 26 | status.playable = false 27 | status.reason = '付费专辑' 28 | } else if (song.noCopyrightRcmd !== null && song.noCopyrightRcmd !== undefined) { 29 | status.playable = false 30 | status.reason = '无版权' 31 | } else if ( privilege?.st < 0 && isLogin()) { 32 | status.playable = false 33 | status.reason = '已下架' 34 | } 35 | return status 36 | } 37 | 38 | // 只有调用 getPlaylistAll接口时,才需传入privileges数组 39 | export function mapSongsPlayableStatus(songs, privilegeList = []) { 40 | if(songs?.length === undefined) return 41 | 42 | if(privilegeList.length === 0){ 43 | return songs.map(song => { 44 | Object.assign(song, { ...checkSongPlayable(song) }) 45 | return song 46 | }) 47 | } 48 | 49 | return songs.map((song, i) => { 50 | Object.assign(song, { ...checkSongPlayable(song, privilegeList[i]) }) 51 | return song 52 | }) 53 | } -------------------------------------------------------------------------------- /src/views/HomePage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | 40 | -------------------------------------------------------------------------------- /src/views/MusicPlayer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | -------------------------------------------------------------------------------- /src/views/SearchResult.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 79 | 80 | -------------------------------------------------------------------------------- /target/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | release 12 | dist 13 | dist-ssr 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /target/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

Helium Music

5 | 6 |

7 | ⚠️请注意:本项目是已经被删库的[Hydrogen-Music](https://github.com/Kaidesuyo/Hydrogen-Music)的分支。 8 |
9 | 因网易增加了网易云盾验证,密码登录可能无法使用,请使用二维码登录。 10 |
11 | 请尽量不要使用云盘中的上传功能,目前上传失败概率大且内存无法得到释放。 12 |
13 | 📦️ 下载安装包 14 |
15 |
16 |

17 |

18 | 19 | ## 📦️ 安装 20 | 21 | 访问 [Releases](https://github.com/mtr-static-official/Helium-Music/releases) 22 | 页面下载安装包。 23 | 24 | ## 👷‍♂️ 打包客户端 25 | 26 | 由于个人设备限制,只打包了Windows平台的安装包且并未适配macOs与Linux平台。 27 | 如有可能,您可以在开发环境中自行适配。 28 | 29 | ```shell 30 | # 打包 31 | npm run dist 32 | ``` 33 | 34 | ## :computer: 配置开发环境 35 | 36 | 运行本项目 37 | 38 | ```shell 39 | # 安装依赖 40 | npm install 41 | 42 | # 运行Vue服务 43 | npm run dev 44 | 45 | # 运行Electron客户端 46 | npm start 47 | ``` 48 | 49 | ## 📜 开源许可 50 | 51 | 本项目仅供个人学习研究使用,禁止用于商业及非法用途。 52 | 53 | 基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。 54 | 55 | ## 灵感来源 56 | 57 | 基于Hydrogen Music修改而来,感谢[Hydrogen-Music](https://github.com/Kaidesuyo/Hydrogen-Music)。 58 | 网易云音乐API:[Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
59 | 哔哩哔哩API:[SocialSisterYi/bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) 60 | 61 | - [qier222/YesPlayMusic](https://github.com/qier222/YesPlayMusic) 62 | - [Apple Music](https://music.apple.com) 63 | - [网易云音乐](https://music.163.com) 64 | 65 | ## 🖼️ 截图 66 | 67 | ![home][home-screenshot] 68 | ![playlist][playlist-screenshot] 69 | ![lyric][lyric-screenshot] 70 | ![music_video][music_video-screenshot] 71 | 72 | 73 | 74 | 75 | [home-screenshot]: img/home.png 76 | [playlist-screenshot]: img/playlist.png 77 | [lyric-screenshot]: img/lyric.png 78 | [music_video-screenshot]: img/music_video.png 79 | -------------------------------------------------------------------------------- /target/background.js: -------------------------------------------------------------------------------- 1 | const startNeteaseMusicApi = require('./src/electron/services') 2 | const IpcMainEvent = require('./src/electron/ipcMain') 3 | const MusicDownload = require('./src/electron/download') 4 | const LocalFiles = require('./src/electron/localmusic') 5 | const InitTray = require('./src/electron/tray') 6 | const registerShortcuts = require('./src/electron/shortcuts') 7 | 8 | const { app, BrowserWindow, globalShortcut } = require('electron') 9 | const Winstate = require('electron-win-state').default 10 | const path = require('path') 11 | const Store = require('electron-store'); 12 | const settingsStore = new Store({name: 'settings'}); 13 | 14 | let myWindow = null 15 | //electron单例 16 | const gotTheLock = app.requestSingleInstanceLock() 17 | 18 | if (!gotTheLock) { 19 | app.quit() 20 | } else { 21 | app.on('second-instance', (event, commandLine, workingDirectory) => { 22 | if (myWindow) { 23 | if (myWindow.isMinimized()) myWindow.restore() 24 | if (!myWindow.isVisible()) myWindow.show() 25 | myWindow.focus() 26 | } 27 | }) 28 | 29 | app.whenReady().then(() => { 30 | createWindow() 31 | app.on('activate', () => { 32 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 33 | }) 34 | }) 35 | 36 | app.on('window-all-closed', () => { 37 | if (process.platform !== 'darwin') app.quit() 38 | }) 39 | 40 | app.on('will-quit', () => { 41 | // 注销所有快捷键 42 | globalShortcut.unregisterAll() 43 | }) 44 | } 45 | const createWindow = () => { 46 | process.env.DIST = path.join(__dirname, './') 47 | const indexHtml = path.join(process.env.DIST, 'dist/index.html') 48 | const winstate = new Winstate({ 49 | //自定义默认窗口大小 50 | defaultWidth: 1024, 51 | defaultHeight: 672, 52 | }) 53 | const win = new BrowserWindow({ 54 | minWidth: 1024, 55 | minHeight: 672, 56 | frame: false, 57 | title: "Helium Music", 58 | icon: path.resolve(__dirname, './src/assets/icon/icon.ico'), 59 | //记录窗口大小 60 | ...winstate.winOptions, 61 | show: false, 62 | webPreferences: { 63 | //预加载脚本 64 | preload: path.resolve(__dirname, './src/electron/preload.js'), 65 | webSecurity: false, 66 | } 67 | }) 68 | myWindow = win 69 | if(process.resourcesPath.indexOf('\\node_modules\\') != -1) 70 | win.loadURL('http://localhost:5173/') 71 | else 72 | win.loadFile(indexHtml) 73 | win.once('ready-to-show', () => { 74 | win.show() 75 | // BrowserWindow.getFocusedWindow().webContents.openDevTools({mode: 'detach'}); 76 | }) 77 | winstate.manage(win) 78 | win.on('close', async (event) => { 79 | event.preventDefault() 80 | const settings = await settingsStore.get('settings') 81 | if(settings.other.quitApp == 'minimize') { 82 | win.hide() 83 | } else if(settings.other.quitApp == 'quit') { 84 | win.webContents.send('player-save') 85 | } 86 | }) 87 | //api初始化 88 | startNeteaseMusicApi() 89 | //ipcMain初始化 90 | IpcMainEvent(win, app) 91 | MusicDownload(win) 92 | LocalFiles(win, app) 93 | InitTray(win, app, path.resolve(__dirname, './src/assets/icon/icon.ico')) 94 | registerShortcuts(win) 95 | } 96 | 97 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; -------------------------------------------------------------------------------- /target/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Helium Music 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /target/npmlist.json: -------------------------------------------------------------------------------- 1 | {"version":"0.0.0","name":"electron-vue-music","dependencies":{"vue":{"version":"3.2.41"}}} -------------------------------------------------------------------------------- /target/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heliummusic", 3 | "private": true, 4 | "version": "0.1.0", 5 | "main": "background.js", 6 | "author": "mtr@mtr.pub", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "start": "nodemon --exec electron . --watch ./ --ext .html", 12 | "dist": "vite build && electron-builder" 13 | }, 14 | "dependencies": { 15 | "axios": "^1.2.0", 16 | "dayjs": "^1.11.6", 17 | "electron-store": "^8.1.0", 18 | "electron-win-state": "^1.1.22", 19 | "fs-extra": "^11.1.0", 20 | "howler": "^2.2.3", 21 | "js-cookie": "^3.0.1", 22 | "js-md5": "^0.7.3", 23 | "music-metadata": "^7.13.0", 24 | "nanoid": "^3.3.4", 25 | "NeteaseCloudMusicApi": "^4.8.9", 26 | "normalize.css": "^8.0.1", 27 | "pinia": "^2.0.23", 28 | "pinia-plugin-persistedstate": "^3.0.1", 29 | "plyr": "^3.7.3", 30 | "qrcode": "^1.5.1", 31 | "vue": "^3.2.41", 32 | "vue-router": "^4.1.6", 33 | "vue-slider-component": "^4.1.0-beta.6", 34 | "vue-virtual-scroller": "^2.0.0-beta.7" 35 | }, 36 | "devDependencies": { 37 | "@vitejs/plugin-vue": "^3.2.0", 38 | "electron": "^21.2.0", 39 | "electron-builder": "^23.6.0", 40 | "nodemon": "^2.0.20", 41 | "sass": "^1.56.1", 42 | "vite": "^3.2.0" 43 | }, 44 | "build": { 45 | "productName": "Helium Music", 46 | "appId": "114514", 47 | "asar": true, 48 | "directories": { 49 | "output": "release/${version}" 50 | }, 51 | "files": [], 52 | "nsis": { 53 | "oneClick": false, 54 | "allowToChangeInstallationDirectory": true 55 | }, 56 | "mac": { 57 | "category": "" 58 | }, 59 | "win": { 60 | "icon": "./src/assets/icon/icon.ico", 61 | "target": [ 62 | { 63 | "target": "nsis", 64 | "arch": [] 65 | } 66 | ] 67 | }, 68 | "linux": {} 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /target/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import {resolve} from 'path' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | base:'./', 9 | manifest:true, 10 | resolve: { 11 | alias: { 12 | '@': resolve(__dirname, './src') 13 | } 14 | }, 15 | optimizeDeps: { 16 | exclude: [] 17 | } 18 | }) -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import {resolve} from 'path' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | base:'./', 9 | manifest:true, 10 | resolve: { 11 | alias: { 12 | '@': resolve(__dirname, './src') 13 | } 14 | }, 15 | optimizeDeps: { 16 | exclude: [] 17 | } 18 | }) --------------------------------------------------------------------------------