├── .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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
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 |
21 |
22 |
23 |
26 |
27 | {{ item.name }}
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/DataCheckAnimaton.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
/
16 |
/
17 |
/
18 |
/
19 |
*
20 |
/
21 |
*
22 |
/
23 |
*
24 |
/
25 |
*
26 |
/
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/GlobalDialog.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 | {{dialogText}}
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
147 |
148 |
--------------------------------------------------------------------------------
/src/components/GlobalNotice.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
{{otherStore.noticeText}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/LibraryAlbumList.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
![]()
30 |
31 |
32 | {{item.name}}
33 | {{item.size}}首
34 |
35 |
36 |
37 | {{item.size}}首
38 | {{publishTime(item.publishTime)}}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/components/LibraryMVList.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
![]()
24 |
25 |
{{item.name}}
26 |
27 |
{{publishTime(item.publishTime)}}
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/LocalMusicClassify.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
{{item.name}}
21 |
22 | {{item.songs.length}}首
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/LocalMusicList.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 | {{ item.name }}
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/components/LoginContent.vue:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
65 |
66 |
72 |
73 |
74 |
75 |
76 |
77 |
打开网易云APP扫码登录
78 |
79 | 邮箱登录
80 | |
81 | 手机登录
82 |
83 |
84 | 邮箱登录
85 | 手机登录
86 | |
87 | 二维码登录
88 |
89 |
90 | 没有账号?去注册
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/components/NewestSong.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
最新歌曲
41 |
42 |
43 |
44 |
45 |
![]()
46 |
47 |
48 |
{{item.name}}
49 |
50 | {{singer.name}}{{index == item.song.artists.length -1 ? '' : '/'}}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/PlayerVideo.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/RecList.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/RecommendSongs.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/SearchInput.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/SearchResultList.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
![]()
24 |
25 |
{{item.name}}
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/Selector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ current?.label }}
5 |
6 |
7 |
8 |
17 |
25 | {{ item?.label }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
76 |
77 |
159 |
181 |
--------------------------------------------------------------------------------
/src/components/Title.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 | Helium
26 |
27 |
28 |
29 |
34 |
35 |
{{songList[currentIndex].name || songList[currentIndex].localName}}
36 |
37 |
38 | {{songTime2(time - progress)}}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/VideoPlayer.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/WindowControl.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
24 |
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 |
13 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/views/LoginPage.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

18 |
19 |
20 | 云音乐
21 | 以云账号登录
22 |
23 |
24 |
25 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/views/MusicPlayer.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/views/SearchResult.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
搜索内容:{{router.currentRoute.value.query.keywords}}
40 |
41 |
42 |
43 |
44 |
歌曲
45 |
46 |
47 |
48 |
49 |
50 |
专辑
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
歌手
59 |
60 |
61 |
62 |
63 |
64 |
歌单
65 |
66 |
67 |
68 |
69 |
70 |
视频
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
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 | })
--------------------------------------------------------------------------------