├── .nvmrc
├── .prettierignore
├── public
├── robots.txt
├── favicon.ico
├── img
│ ├── icons
│ │ ├── 16x16.png
│ │ ├── 24x24.png
│ │ ├── 32x32.png
│ │ ├── 48x48.png
│ │ ├── 64x64.png
│ │ ├── exit.png
│ │ ├── icon.icns
│ │ ├── icon.ico
│ │ ├── left.png
│ │ ├── like.png
│ │ ├── menu.png
│ │ ├── pause.png
│ │ ├── play.png
│ │ ├── repeat.png
│ │ ├── right.png
│ │ ├── unlike.png
│ │ ├── 128x128.png
│ │ ├── 256x256.png
│ │ ├── 512x512.png
│ │ ├── favicon.ico
│ │ ├── menu@88.png
│ │ ├── 1024x1024.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── apple-touch-icon.png
│ │ ├── mstile-150x150.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── msapplication-icon-144x144.png
│ │ └── safari-pinned-tab.svg
│ ├── logos
│ │ ├── lastfm.png
│ │ ├── nyancat.gif
│ │ ├── netease-music.png
│ │ ├── nyancat-stop.png
│ │ ├── yesplaymusic.png
│ │ └── yesplaymusic-white24x24.png
│ └── touchbar
│ │ ├── like.png
│ │ ├── pause.png
│ │ ├── play.png
│ │ ├── backward.png
│ │ ├── forward.png
│ │ ├── next_up.png
│ │ ├── search.png
│ │ ├── like_fill.png
│ │ ├── page_next.png
│ │ └── page_prev.png
└── index.html
├── images
├── album.png
├── artist.png
├── home-2.png
├── home.png
├── logo.png
├── lyrics.png
├── search.png
├── explore.png
├── library.png
└── library-dark.png
├── src
├── assets
│ ├── fonts
│ │ ├── Barlow-Bold.ttf
│ │ ├── Barlow-Black.ttf
│ │ ├── Barlow-Bold.woff2
│ │ ├── Barlow-Medium.ttf
│ │ ├── Barlow-Black.woff2
│ │ ├── Barlow-ExtraBold.ttf
│ │ ├── Barlow-Medium.woff2
│ │ ├── Barlow-Regular.ttf
│ │ ├── Barlow-Regular.woff2
│ │ ├── Barlow-SemiBold.ttf
│ │ ├── Barlow-ExtraBold.woff2
│ │ └── Barlow-SemiBold.woff2
│ ├── icons
│ │ ├── explicit.svg
│ │ ├── index.js
│ │ ├── play.svg
│ │ ├── dropdown.svg
│ │ ├── heart-solid.svg
│ │ ├── next.svg
│ │ ├── arrow-left.svg
│ │ ├── previous.svg
│ │ ├── arrow-right.svg
│ │ ├── pause.svg
│ │ ├── more.svg
│ │ ├── arrow-up-alt.svg
│ │ ├── mobile.svg
│ │ ├── lock.svg
│ │ ├── search.svg
│ │ ├── login.svg
│ │ ├── logout.svg
│ │ ├── plus.svg
│ │ ├── x.svg
│ │ ├── arrow-down.svg
│ │ ├── arrow-up.svg
│ │ ├── volume-half.svg
│ │ ├── mail.svg
│ │ ├── fm.svg
│ │ ├── list.svg
│ │ ├── volume-mute.svg
│ │ ├── sort-up.svg
│ │ ├── shuffle.svg
│ │ ├── volume.svg
│ │ ├── settings.svg
│ │ ├── thumbs-down.svg
│ │ ├── repeat.svg
│ │ ├── github.svg
│ │ ├── heart.svg
│ │ └── repeat-1.svg
│ └── css
│ │ ├── nprogress.css
│ │ ├── slider.css
│ │ └── global.scss
├── store
│ ├── plugins
│ │ ├── localStorage.js
│ │ └── sendSettings.js
│ ├── state.js
│ ├── initLocalStorage.js
│ ├── index.js
│ └── mutations.js
├── utils
│ ├── platform.js
│ ├── nativeAlert.js
│ ├── shortcuts.js
│ ├── auth.js
│ ├── updateApp.js
│ ├── playList.js
│ ├── base64.js
│ ├── request.js
│ ├── lyrics.js
│ ├── filters.js
│ └── db.js
├── electron
│ ├── services.js
│ ├── dockMenu.js
│ ├── globalShortcut.js
│ ├── mpris.js
│ ├── ipcRenderer.js
│ └── touchBar.js
├── locale
│ └── index.js
├── components
│ ├── ExplicitSymbol.vue
│ ├── ButtonIcon.vue
│ ├── SvgIcon.vue
│ ├── Toast.vue
│ ├── ArtistsInLine.vue
│ ├── ButtonTwoTone.vue
│ ├── Win32Titlebar.vue
│ ├── LinuxTitlebar.vue
│ ├── ModalNewPlaylist.vue
│ ├── MvRow.vue
│ ├── Modal.vue
│ ├── ModalAddTrackToPlaylist.vue
│ ├── DailyTracksCard.vue
│ ├── Cover.vue
│ └── Scrollbar.vue
├── views
│ ├── newAlbum.vue
│ ├── lastfmCallback.vue
│ ├── artistMV.vue
│ ├── dailyTracks.vue
│ ├── login.vue
│ ├── next.vue
│ ├── searchType.vue
│ └── loginUsername.vue
├── registerServiceWorker.js
├── main.js
├── api
│ ├── others.js
│ ├── mv.js
│ ├── album.js
│ ├── lastfm.js
│ ├── artist.js
│ ├── auth.js
│ ├── track.js
│ └── user.js
├── router
│ └── index.js
└── App.vue
├── .editorconfig
├── restyled.yml
├── vercel.example.json
├── .gitattributes
├── docker-compose.yml
├── babel.config.js
├── .dockerignore
├── .prettierrc
├── jsconfig.json
├── .gitignore
├── LICENSE
├── Dockerfile
└── package.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | 14
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build
2 | coverage
3 | dist
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/images/album.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/album.png
--------------------------------------------------------------------------------
/images/artist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/artist.png
--------------------------------------------------------------------------------
/images/home-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/home-2.png
--------------------------------------------------------------------------------
/images/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/home.png
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/logo.png
--------------------------------------------------------------------------------
/images/lyrics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/lyrics.png
--------------------------------------------------------------------------------
/images/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/search.png
--------------------------------------------------------------------------------
/images/explore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/explore.png
--------------------------------------------------------------------------------
/images/library.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/library.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/images/library-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/images/library-dark.png
--------------------------------------------------------------------------------
/public/img/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/16x16.png
--------------------------------------------------------------------------------
/public/img/icons/24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/24x24.png
--------------------------------------------------------------------------------
/public/img/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/32x32.png
--------------------------------------------------------------------------------
/public/img/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/48x48.png
--------------------------------------------------------------------------------
/public/img/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/64x64.png
--------------------------------------------------------------------------------
/public/img/icons/exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/exit.png
--------------------------------------------------------------------------------
/public/img/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/icon.icns
--------------------------------------------------------------------------------
/public/img/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/icon.ico
--------------------------------------------------------------------------------
/public/img/icons/left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/left.png
--------------------------------------------------------------------------------
/public/img/icons/like.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/like.png
--------------------------------------------------------------------------------
/public/img/icons/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/menu.png
--------------------------------------------------------------------------------
/public/img/icons/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/pause.png
--------------------------------------------------------------------------------
/public/img/icons/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/play.png
--------------------------------------------------------------------------------
/public/img/icons/repeat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/repeat.png
--------------------------------------------------------------------------------
/public/img/icons/right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/right.png
--------------------------------------------------------------------------------
/public/img/icons/unlike.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/unlike.png
--------------------------------------------------------------------------------
/public/img/logos/lastfm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/logos/lastfm.png
--------------------------------------------------------------------------------
/public/img/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/128x128.png
--------------------------------------------------------------------------------
/public/img/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/256x256.png
--------------------------------------------------------------------------------
/public/img/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/512x512.png
--------------------------------------------------------------------------------
/public/img/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/favicon.ico
--------------------------------------------------------------------------------
/public/img/icons/menu@88.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/menu@88.png
--------------------------------------------------------------------------------
/public/img/logos/nyancat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/logos/nyancat.gif
--------------------------------------------------------------------------------
/public/img/touchbar/like.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/like.png
--------------------------------------------------------------------------------
/public/img/touchbar/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/pause.png
--------------------------------------------------------------------------------
/public/img/touchbar/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/play.png
--------------------------------------------------------------------------------
/public/img/icons/1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/1024x1024.png
--------------------------------------------------------------------------------
/public/img/touchbar/backward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/backward.png
--------------------------------------------------------------------------------
/public/img/touchbar/forward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/forward.png
--------------------------------------------------------------------------------
/public/img/touchbar/next_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/next_up.png
--------------------------------------------------------------------------------
/public/img/touchbar/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/search.png
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Bold.ttf
--------------------------------------------------------------------------------
/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/logos/netease-music.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/logos/netease-music.png
--------------------------------------------------------------------------------
/public/img/logos/nyancat-stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/logos/nyancat-stop.png
--------------------------------------------------------------------------------
/public/img/logos/yesplaymusic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/logos/yesplaymusic.png
--------------------------------------------------------------------------------
/public/img/touchbar/like_fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/like_fill.png
--------------------------------------------------------------------------------
/public/img/touchbar/page_next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/page_next.png
--------------------------------------------------------------------------------
/public/img/touchbar/page_prev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/touchbar/page_prev.png
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Black.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Bold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Medium.ttf
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Black.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-ExtraBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Medium.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-Regular.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-SemiBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-ExtraBold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Barlow-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/src/assets/fonts/Barlow-SemiBold.woff2
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/public/img/logos/yesplaymusic-white24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/logos/yesplaymusic-white24x24.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/public/img/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Groupguanfang/TVYesPlayMusic/HEAD/public/img/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = false
8 | insert_final_newline = false
--------------------------------------------------------------------------------
/restyled.yml:
--------------------------------------------------------------------------------
1 | commit_template: 'style: with ${restyler.name}'
2 | restylers:
3 | - prettier
4 | - prettier-json
5 | - prettier-markdown
6 | - prettier-yaml
7 | - whitespace
8 |
--------------------------------------------------------------------------------
/vercel.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/api/:match*",
5 | "destination": "https://your-netease-api.example.com/:match*"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | # Denote all files that are truly binary and should not be modified.
3 | *.png binary
4 | *.jpg binary
5 | *.mp3 binary
6 | *.icns binary
7 | *.gif binary
8 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | YesPlayMusic:
3 | build:
4 | context: .
5 | image: yesplaymusic
6 | container_name: YesPlayMusic
7 | ports:
8 | - 80:80
9 | restart: always
10 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@vue/cli-plugin-babel/preset',
5 | {
6 | useBuiltIns: 'usage',
7 | shippedProposals: true,
8 | },
9 | ],
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | Dockerfile*
4 | docker-compose*
5 | .dockerignore
6 | .git
7 | .github
8 | .gitignore
9 | README.md
10 | LICENSE
11 | .vscode
12 | dist
13 | dist_electron
14 | build
15 | images
16 | script
--------------------------------------------------------------------------------
/public/img/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/explicit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/store/plugins/localStorage.js:
--------------------------------------------------------------------------------
1 | export default store => {
2 | store.subscribe((mutation, state) => {
3 | // console.log(mutation);
4 | localStorage.setItem('settings', JSON.stringify(state.settings));
5 | localStorage.setItem('data', JSON.stringify(state.data));
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/src/assets/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import SvgIcon from '@/components/SvgIcon';
3 |
4 | Vue.component('svg-icon', SvgIcon);
5 | const requireAll = requireContext => requireContext.keys().map(requireContext);
6 | const req = require.context('./', true, /\.svg$/);
7 | requireAll(req);
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "jsxSingleQuote": true,
8 | "arrowParens": "avoid",
9 | "endOfLine": "lf",
10 | "bracketSpacing": true,
11 | "htmlWhitespaceSensitivity": "strict"
12 | }
13 |
--------------------------------------------------------------------------------
/src/assets/icons/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // 支持 @ 的别名解析
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["src/*"]
7 | },
8 | "target": "ES6",
9 | "module": "commonjs",
10 | "allowSyntheticDefaultImports": true,
11 | "jsx": "preserve"
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/icons/dropdown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/platform.js:
--------------------------------------------------------------------------------
1 | export const isWindows = process.platform === 'win32';
2 | export const isMac = process.platform === 'darwin';
3 | export const isLinux = process.platform === 'linux';
4 | export const isDevelopment = process.env.NODE_ENV === 'development';
5 |
6 | export const isCreateTray = isWindows || isLinux || isDevelopment;
7 | export const isCreateMpris = isLinux;
8 |
--------------------------------------------------------------------------------
/src/assets/icons/heart-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/store/plugins/sendSettings.js:
--------------------------------------------------------------------------------
1 | export function getSendSettingsPlugin() {
2 | const electron = window.require('electron');
3 | const ipcRenderer = electron.ipcRenderer;
4 | return store => {
5 | store.subscribe((mutation, state) => {
6 | // console.log(mutation);
7 | if (mutation.type !== 'updateSettings') return;
8 | ipcRenderer.send('settings', state.settings);
9 | });
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/assets/icons/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/previous.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/electron/services.js:
--------------------------------------------------------------------------------
1 | import clc from 'cli-color';
2 | import server from 'NeteaseCloudMusicApi/server';
3 |
4 | export async function startNeteaseMusicApi() {
5 | // Let user know that the service is starting
6 | console.log(`${clc.redBright('[NetEase API]')} initiating NCM API`);
7 |
8 | // Load the NCM API.
9 | await server.serveNcmApi({
10 | port: 35216,
11 | moduleDefs: require('../ncmModDef'),
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/pause.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-up-alt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/mobile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
25 | .vercel
26 |
27 | #Electron-builder output
28 | /dist_electron
29 | NeteaseCloudMusicApi-master
30 | NeteaseCloudMusicApi-master.zip
31 |
32 | # Local Netlify folder
33 | .netlify
34 | vercel.json
35 |
--------------------------------------------------------------------------------
/src/assets/icons/lock.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/electron/dockMenu.js:
--------------------------------------------------------------------------------
1 | const { Menu } = require('electron');
2 |
3 | export function createDockMenu(win) {
4 | return Menu.buildFromTemplate([
5 | {
6 | label: 'Play',
7 | click() {
8 | win.webContents.send('play');
9 | },
10 | },
11 | { type: 'separator' },
12 | {
13 | label: 'Next',
14 | click() {
15 | win.webContents.send('next');
16 | },
17 | },
18 | {
19 | label: 'Previous',
20 | click() {
21 | win.webContents.send('previous');
22 | },
23 | },
24 | ]);
25 | }
26 |
--------------------------------------------------------------------------------
/src/assets/icons/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/login.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/logout.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/locale/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueClipboard from 'vue-clipboard2';
3 | import VueI18n from 'vue-i18n';
4 | import store from '@/store';
5 |
6 | import en from './lang/en.js';
7 | import zhCN from './lang/zh-CN.js';
8 | import zhTW from './lang/zh-TW.js';
9 | import tr from './lang/tr.js';
10 |
11 | Vue.use(VueClipboard);
12 | Vue.use(VueI18n);
13 |
14 | const i18n = new VueI18n({
15 | locale: store.state.settings.lang,
16 | messages: {
17 | en,
18 | 'zh-CN': zhCN,
19 | 'zh-TW': zhTW,
20 | tr,
21 | },
22 | silentTranslationWarn: true,
23 | });
24 |
25 | export default i18n;
26 |
--------------------------------------------------------------------------------
/src/assets/icons/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/arrow-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/volume-half.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/mail.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/fm.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ExplicitSymbol.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/ButtonIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
37 |
--------------------------------------------------------------------------------
/src/assets/icons/volume-mute.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/sort-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/SvgIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
34 |
35 |
40 |
--------------------------------------------------------------------------------
/src/assets/css/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #335eea;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px #335eea, 0 0 5px #335eea;
26 | opacity: 1;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | .nprogress-custom-parent {
34 | overflow: hidden;
35 | position: relative;
36 | }
37 |
38 | .nprogress-custom-parent #nprogress .bar {
39 | position: absolute;
40 | }
41 |
--------------------------------------------------------------------------------
/src/assets/icons/shuffle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/volume.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/settings.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/thumbs-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/newAlbum.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('home.newAlbum') }}
4 |
14 |
15 |
16 |
17 |
43 |
44 |
50 |
--------------------------------------------------------------------------------
/src/utils/nativeAlert.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns an alert-like function that fits current runtime environment
3 | *
4 | * This function is amid to solve a electron bug on Windows, that, when
5 | * user dismissed a browser alert, elements cannot be focused
6 | * for further editing unless switching to another window and then back
7 | *
8 | * @returns { (message:string) => void }
9 | * Built-in alert function for browser environment
10 | * A function wrapping {@link dialog.showMessageBoxSync} for electron environment
11 | *
12 | * @see {@link https://github.com/electron/electron/issues/19977} for upstream electron issue
13 | */
14 | const nativeAlert = (() => {
15 | if (process.env.IS_ELECTRON === true) {
16 | const { dialog } = require('electron');
17 | if (dialog) {
18 | return message => {
19 | var options = {
20 | type: 'warning',
21 | message,
22 | };
23 | dialog.showMessageBoxSync(null, options);
24 | };
25 | }
26 | }
27 | return alert;
28 | })();
29 |
30 | export default nativeAlert;
31 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from 'register-service-worker';
4 |
5 | if (!process.env.IS_ELECTRON) {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | // console.log(
9 | // "App is being served from cache by a service worker.\n" +
10 | // "For more details, visit https://goo.gl/AFskqB"
11 | // );
12 | },
13 | registered() {
14 | // console.log("Service worker has been registered.");
15 | },
16 | cached() {
17 | // console.log("Content has been cached for offline use.");
18 | },
19 | updatefound() {
20 | // console.log("New content is downloading.");
21 | },
22 | updated() {
23 | // console.log("New content is available; please refresh.");
24 | },
25 | offline() {
26 | // console.log(
27 | // "No internet connection found. App is running in offline mode."
28 | // );
29 | },
30 | error(error) {
31 | console.error('Error during service worker registration:', error);
32 | },
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2022 qier222
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 |
--------------------------------------------------------------------------------
/src/utils/shortcuts.js:
--------------------------------------------------------------------------------
1 | // default shortcuts
2 | // for more info, check https://www.electronjs.org/docs/api/accelerator
3 |
4 | export default [
5 | {
6 | id: 'play',
7 | name: '播放/暂停',
8 | shortcut: 'CommandOrControl+P',
9 | globalShortcut: 'Alt+CommandOrControl+P',
10 | },
11 | {
12 | id: 'next',
13 | name: '下一首',
14 | shortcut: 'CommandOrControl+Right',
15 | globalShortcut: 'Alt+CommandOrControl+Right',
16 | },
17 | {
18 | id: 'previous',
19 | name: '上一首',
20 | shortcut: 'CommandOrControl+Left',
21 | globalShortcut: 'Alt+CommandOrControl+Left',
22 | },
23 | {
24 | id: 'increaseVolume',
25 | name: '增加音量',
26 | shortcut: 'CommandOrControl+Up',
27 | globalShortcut: 'Alt+CommandOrControl+Up',
28 | },
29 | {
30 | id: 'decreaseVolume',
31 | name: '减少音量',
32 | shortcut: 'CommandOrControl+Down',
33 | globalShortcut: 'Alt+CommandOrControl+Down',
34 | },
35 | {
36 | id: 'like',
37 | name: '喜欢歌曲',
38 | shortcut: 'CommandOrControl+L',
39 | globalShortcut: 'Alt+CommandOrControl+L',
40 | },
41 | {
42 | id: 'minimize',
43 | name: '隐藏/显示播放器',
44 | shortcut: 'CommandOrControl+M',
45 | globalShortcut: 'Alt+CommandOrControl+M',
46 | },
47 | ];
48 |
--------------------------------------------------------------------------------
/src/components/Toast.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ toast.text }}
4 |
5 |
6 |
7 |
17 |
18 |
52 |
--------------------------------------------------------------------------------
/src/assets/icons/repeat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ArtistsInLine.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ computedPrefix }}
4 |
5 | {{
6 | ar.name
7 | }}
8 | {{ ar.name }}
9 | ,
12 |
13 |
14 |
15 |
16 |
44 |
45 |
54 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueGtag from 'vue-gtag';
3 | import App from './App.vue';
4 | import router from './router';
5 | import store from './store';
6 | import i18n from '@/locale';
7 | import '@/assets/icons';
8 | import '@/utils/filters';
9 | import './registerServiceWorker';
10 | import { dailyTask } from '@/utils/common';
11 | import '@/assets/css/global.scss';
12 | import NProgress from 'nprogress';
13 | import '@/assets/css/nprogress.css';
14 | import focusable from 'vue-tv-focusable';
15 |
16 | window.resetApp = () => {
17 | localStorage.clear();
18 | indexedDB.deleteDatabase('yesplaymusic');
19 | document.cookie.split(';').forEach(function (c) {
20 | document.cookie = c
21 | .replace(/^ +/, '')
22 | .replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
23 | });
24 | return '已重置应用,请刷新页面(按Ctrl/Command + R)';
25 | };
26 | console.log(
27 | '如出现问题,可尝试在本页输入 %cresetApp()%c 然后按回车重置应用。',
28 | 'background: #eaeffd;color:#335eea;padding: 4px 6px;border-radius:3px;',
29 | 'background:unset;color:unset;'
30 | );
31 |
32 | Vue.use(
33 | focusable,
34 | VueGtag,
35 | {
36 | config: { id: 'G-KMJJCFZDKF' },
37 | },
38 | router
39 | );
40 | Vue.config.productionTip = false;
41 |
42 | NProgress.configure({ showSpinner: false, trickleSpeed: 100 });
43 | dailyTask();
44 |
45 | new Vue({
46 | i18n,
47 | store,
48 | router,
49 | render: h => h(App),
50 | }).$mount('#app');
51 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | import initLocalStorage from './initLocalStorage';
2 | import pkg from '../../package.json';
3 | import updateApp from '@/utils/updateApp';
4 |
5 | if (localStorage.getItem('appVersion') === null) {
6 | localStorage.setItem('settings', JSON.stringify(initLocalStorage.settings));
7 | localStorage.setItem('data', JSON.stringify(initLocalStorage.data));
8 | localStorage.setItem('appVersion', pkg.version);
9 | }
10 |
11 | updateApp();
12 |
13 | export default {
14 | showLyrics: false,
15 | enableScrolling: true,
16 | title: 'YesPlayMusic',
17 | liked: {
18 | songs: [],
19 | songsWithDetails: [], // 只有前12首
20 | playlists: [],
21 | albums: [],
22 | artists: [],
23 | mvs: [],
24 | cloudDisk: [],
25 | playHistory: {
26 | weekData: [],
27 | allData: [],
28 | },
29 | },
30 | contextMenu: {
31 | clickObjectID: 0,
32 | showMenu: false,
33 | },
34 | toast: {
35 | show: false,
36 | text: '',
37 | timer: null,
38 | },
39 | modals: {
40 | addTrackToPlaylistModal: {
41 | show: false,
42 | selectedTrackID: 0,
43 | },
44 | newPlaylistModal: {
45 | show: false,
46 | afterCreateAddTrackID: 0,
47 | },
48 | },
49 | dailyTracks: [],
50 | lastfm: JSON.parse(localStorage.getItem('lastfm')) || {},
51 | player: JSON.parse(localStorage.getItem('player')),
52 | settings: JSON.parse(localStorage.getItem('settings')),
53 | data: JSON.parse(localStorage.getItem('data')),
54 | };
55 |
--------------------------------------------------------------------------------
/src/assets/icons/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/store/initLocalStorage.js:
--------------------------------------------------------------------------------
1 | import { playlistCategories } from '@/utils/staticData';
2 | import shortcuts from '@/utils/shortcuts';
3 |
4 | console.debug('[debug][initLocalStorage.js]');
5 | const enabledPlaylistCategories = playlistCategories
6 | .filter(c => c.enable)
7 | .map(c => c.name);
8 |
9 | let localStorage = {
10 | player: {},
11 | settings: {
12 | lang: null,
13 | musicLanguage: 'all',
14 | appearance: 'auto',
15 | musicQuality: 320000,
16 | lyricFontSize: 28,
17 | outputDevice: 'default',
18 | showPlaylistsByAppleMusic: true,
19 | enableUnblockNeteaseMusic: true,
20 | automaticallyCacheSongs: true,
21 | cacheLimit: 8192,
22 | enableReversedMode: false,
23 | nyancatStyle: false,
24 | showLyricsTranslation: true,
25 | lyricsBackground: true,
26 | closeAppOption: 'ask',
27 | enableDiscordRichPresence: false,
28 | enableGlobalShortcut: true,
29 | showLibraryDefault: false,
30 | subTitleDefault: false,
31 | linuxEnableCustomTitlebar: false,
32 | enabledPlaylistCategories,
33 | proxyConfig: {
34 | protocol: 'noProxy',
35 | server: '',
36 | port: null,
37 | },
38 | shortcuts: shortcuts,
39 | },
40 | data: {
41 | user: {},
42 | likedSongPlaylistID: 0,
43 | lastRefreshCookieDate: 0,
44 | loginMode: null,
45 | },
46 | };
47 |
48 | if (process.env.IS_ELECTRON === true) {
49 | localStorage.settings.automaticallyCacheSongs = true;
50 | }
51 |
52 | export default localStorage;
53 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | <%= htmlWebpackPlugin.options.title %>
12 |
13 |
14 |
15 |
16 |
23 |
24 | 加载中
25 |
26 |
27 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/api/others.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request';
2 | import { mapTrackPlayableStatus } from '@/utils/common';
3 |
4 | /**
5 | * 搜索
6 | * 说明 : 调用此接口 , 传入搜索关键词可以搜索该音乐 / 专辑 / 歌手 / 歌单 / 用户 , 关键词可以多个 , 以空格隔开 ,
7 | * 如 " 周杰伦 搁浅 "( 不需要登录 ), 搜索获取的 mp3url 不能直接用 , 可通过 /song/url 接口传入歌曲 id 获取具体的播放链接
8 | * - keywords : 关键词
9 | * - limit : 返回数量 , 默认为 30
10 | * - offset : 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
11 | * - type: 搜索类型;默认为 1 即单曲 , 取值意义 : 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合
12 | * - 调用例子 : /search?keywords=海阔天空 /cloudsearch?keywords=海阔天空(更全)
13 | * @param {Object} params
14 | * @param {string} params.keywords
15 | * @param {number=} params.limit
16 | * @param {number=} params.offset
17 | * @param {number=} params.type
18 | */
19 | export function search(params) {
20 | return request({
21 | url: '/search',
22 | method: 'get',
23 | params,
24 | }).then(data => {
25 | if (data.result?.song !== undefined)
26 | data.result.song.songs = mapTrackPlayableStatus(data.result.song.songs);
27 | return data;
28 | });
29 | }
30 |
31 | export function personalFM() {
32 | return request({
33 | url: '/personal_fm',
34 | method: 'get',
35 | params: {
36 | timestamp: new Date().getTime(),
37 | },
38 | });
39 | }
40 |
41 | export function fmTrash(id) {
42 | return request({
43 | url: '/fm_trash',
44 | method: 'post',
45 | params: {
46 | timestamp: new Date().getTime(),
47 | id,
48 | },
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/src/assets/icons/heart.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/api/mv.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request';
2 |
3 | /**
4 | * 获取 mv 数据
5 | * 说明 : 调用此接口 , 传入 mvid ( 在搜索音乐的时候传 type=1004 获得 ) , 可获取对应 MV 数据 , 数据包含 mv 名字 , 歌手 , 发布时间 , mv 视频地址等数据 ,
6 | * 其中 mv 视频 网易做了防盗链处理 , 可能不能直接播放 , 需要播放的话需要调用 ' mv 地址' 接口
7 | * - 调用例子 : /mv/detail?mvid=5436712
8 | * @param {number} mvid mv 的 id
9 | */
10 | export function mvDetail(mvid) {
11 | return request({
12 | url: '/mv/detail',
13 | method: 'get',
14 | params: {
15 | mvid,
16 | timestamp: new Date().getTime(),
17 | },
18 | });
19 | }
20 |
21 | /**
22 | * mv 地址
23 | * 说明 : 调用此接口 , 传入 mv id,可获取 mv 播放地址
24 | * - id: mv id
25 | * - r: 分辨率,默认1080,可从 /mv/detail 接口获取分辨率列表
26 | * - 调用例子 : /mv/url?id=5436712 /mv/url?id=10896407&r=1080
27 | * @param {Object} params
28 | * @param {number} params.id
29 | * @param {number=} params.r
30 | */
31 | export function mvUrl(params) {
32 | return request({
33 | url: '/mv/url',
34 | method: 'get',
35 | params,
36 | });
37 | }
38 |
39 | /**
40 | * 相似 mv
41 | * 说明 : 调用此接口 , 传入 mvid 可获取相似 mv
42 | * @param {number} mvid
43 | */
44 | export function simiMv(mvid) {
45 | return request({
46 | url: '/simi/mv',
47 | method: 'get',
48 | params: { mvid },
49 | });
50 | }
51 |
52 | /**
53 | * 收藏/取消收藏 MV
54 | * 说明 : 调用此接口,可收藏/取消收藏 MV
55 | * - mvid: mv id
56 | * - t: 1 为收藏,其他为取消收藏
57 | * @param {Object} params
58 | * @param {number} params.mvid
59 | * @param {number=} params.t
60 | */
61 |
62 | export function likeAMV(params) {
63 | params.timestamp = new Date().getTime();
64 | return request({
65 | url: '/mv/sub',
66 | method: 'post',
67 | params,
68 | });
69 | }
70 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.13.1-alpine as build
2 | ENV VUE_APP_NETEASE_API_URL=/api
3 | WORKDIR /app
4 | RUN apk add --no-cache python3 make g++ git
5 | COPY package.json yarn.lock ./
6 | RUN yarn install
7 | COPY . .
8 | RUN yarn build
9 |
10 | FROM nginx:1.20.2-alpine as app
11 | RUN echo $'server { \n\
12 | gzip on;\n\
13 | listen 80; \n\
14 | listen [::]:80; \n\
15 | server_name localhost; \n\
16 | \n\
17 | location / { \n\
18 | root /usr/share/nginx/html; \n\
19 | index index.html; \n\
20 | try_files $uri $uri/ /index.html; \n\
21 | } \n\
22 | \n\
23 | location @rewrites { \n\
24 | rewrite ^(.*)$ /index.html last; \n\
25 | } \n\
26 | \n\
27 | location /api/ { \n\
28 | proxy_buffer_size 128k; \n\
29 | proxy_buffers 16 32k; \n\
30 | proxy_busy_buffers_size 128k; \n\
31 | proxy_set_header Host $host; \n\
32 | proxy_set_header X-Real-IP $remote_addr; \n\
33 | proxy_set_header X-Forwarded-For $remote_addr; \n\
34 | proxy_set_header X-Forwarded-Host $remote_addr; \n\
35 | proxy_set_header X-NginX-Proxy true; \n\
36 | proxy_pass http://localhost:3000/; \n\
37 | } \n\
38 | }' > /etc/nginx/conf.d/default.conf
39 |
40 | COPY --from=build /app/package.json /usr/local/lib/
41 |
42 | RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv jq \
43 | && apk add --no-cache --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main nodejs npm \
44 | && npm i -g NeteaseCloudMusicApi@"$(jq -r '.dependencies.NeteaseCloudMusicApi' /usr/local/lib/package.json)"
45 |
46 | COPY --from=build /app/dist /usr/share/nginx/html
47 |
48 | CMD nginx ; exec npx NeteaseCloudMusicApi
49 |
--------------------------------------------------------------------------------
/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import { logout } from '@/api/auth';
3 | import store from '@/store';
4 |
5 | export function setCookies(string) {
6 | const cookies = string.split(';;');
7 | cookies.map(cookie => {
8 | document.cookie = cookie;
9 | const cookieKeyValue = cookie.split(';')[0].split('=');
10 | localStorage.setItem(`cookie-${cookieKeyValue[0]}`, cookieKeyValue[1]);
11 | });
12 | }
13 |
14 | export function getCookie(key) {
15 | return Cookies.get(key) ?? localStorage.getItem(`cookie-${key}`);
16 | }
17 |
18 | export function removeCookie(key) {
19 | Cookies.remove(key);
20 | localStorage.removeItem(`cookie-${key}`);
21 | }
22 |
23 | // MUSIC_U 只有在账户登录的情况下才有
24 | export function isLoggedIn() {
25 | return getCookie('MUSIC_U') !== undefined;
26 | }
27 |
28 | // 账号登录
29 | export function isAccountLoggedIn() {
30 | return (
31 | getCookie('MUSIC_U') !== undefined &&
32 | store.state.data.loginMode === 'account'
33 | );
34 | }
35 |
36 | // 用户名搜索(用户数据为只读)
37 | export function isUsernameLoggedIn() {
38 | return store.state.data.loginMode === 'username';
39 | }
40 |
41 | // 账户登录或者用户名搜索都判断为登录,宽松检查
42 | export function isLooseLoggedIn() {
43 | return isAccountLoggedIn() || isUsernameLoggedIn();
44 | }
45 |
46 | export function doLogout() {
47 | logout();
48 | removeCookie('MUSIC_U');
49 | removeCookie('__csrf');
50 | // 更新状态仓库中的用户信息
51 | store.commit('updateData', { key: 'user', value: {} });
52 | // 更新状态仓库中的登录状态
53 | store.commit('updateData', { key: 'loginMode', value: null });
54 | // 更新状态仓库中的喜欢列表
55 | store.commit('updateData', { key: 'likedSongPlaylistID', value: undefined });
56 | }
57 |
--------------------------------------------------------------------------------
/src/assets/icons/repeat-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/electron/globalShortcut.js:
--------------------------------------------------------------------------------
1 | import defaultShortcuts from '@/utils/shortcuts';
2 | const { globalShortcut } = require('electron');
3 |
4 | const clc = require('cli-color');
5 | const log = text => {
6 | console.log(`${clc.blueBright('[globalShortcut.js]')} ${text}`);
7 | };
8 |
9 | export function registerGlobalShortcut(win, store) {
10 | log('registerGlobalShortcut');
11 | let shortcuts = store.get('settings.shortcuts');
12 | if (shortcuts === undefined) {
13 | shortcuts = defaultShortcuts;
14 | }
15 |
16 | globalShortcut.register(
17 | shortcuts.find(s => s.id === 'play').globalShortcut,
18 | () => {
19 | win.webContents.send('play');
20 | }
21 | );
22 | globalShortcut.register(
23 | shortcuts.find(s => s.id === 'next').globalShortcut,
24 | () => {
25 | win.webContents.send('next');
26 | }
27 | );
28 | globalShortcut.register(
29 | shortcuts.find(s => s.id === 'previous').globalShortcut,
30 | () => {
31 | win.webContents.send('previous');
32 | }
33 | );
34 | globalShortcut.register(
35 | shortcuts.find(s => s.id === 'increaseVolume').globalShortcut,
36 | () => {
37 | win.webContents.send('increaseVolume');
38 | }
39 | );
40 | globalShortcut.register(
41 | shortcuts.find(s => s.id === 'decreaseVolume').globalShortcut,
42 | () => {
43 | win.webContents.send('decreaseVolume');
44 | }
45 | );
46 | globalShortcut.register(
47 | shortcuts.find(s => s.id === 'like').globalShortcut,
48 | () => {
49 | win.webContents.send('like');
50 | }
51 | );
52 | globalShortcut.register(
53 | shortcuts.find(s => s.id === 'minimize').globalShortcut,
54 | () => {
55 | win.isVisible() ? win.hide() : win.show();
56 | }
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/api/album.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request';
2 | import { mapTrackPlayableStatus } from '@/utils/common';
3 | import { cacheAlbum, getAlbumFromCache } from '@/utils/db';
4 |
5 | /**
6 | * 获取专辑内容
7 | * 说明 : 调用此接口 , 传入专辑 id, 可获得专辑内容
8 | * @param {number} id
9 | */
10 | export function getAlbum(id) {
11 | const fetchLatest = () => {
12 | return request({
13 | url: '/album',
14 | method: 'get',
15 | params: {
16 | id,
17 | },
18 | }).then(data => {
19 | cacheAlbum(id, data);
20 | data.songs = mapTrackPlayableStatus(data.songs);
21 | return data;
22 | });
23 | };
24 | fetchLatest();
25 |
26 | return getAlbumFromCache(id).then(result => {
27 | return result ?? fetchLatest();
28 | });
29 | }
30 |
31 | /**
32 | * 全部新碟
33 | * 说明 : 登录后调用此接口 ,可获取全部新碟
34 | * - limit - 返回数量 , 默认为 30
35 | * - offset - 偏移数量,用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
36 | * - area - ALL:全部,ZH:华语,EA:欧美,KR:韩国,JP:日本
37 | * @param {Object} params
38 | * @param {number} params.limit
39 | * @param {number=} params.offset
40 | * @param {string} params.area
41 | */
42 | export function newAlbums(params) {
43 | return request({
44 | url: '/album/new',
45 | method: 'get',
46 | params,
47 | });
48 | }
49 |
50 | /**
51 | * 专辑动态信息
52 | * 说明 : 调用此接口 , 传入专辑 id, 可获得专辑动态信息,如是否收藏,收藏数,评论数,分享数
53 | * - id - 专辑id
54 | * @param {number} id
55 | */
56 | export function albumDynamicDetail(id) {
57 | return request({
58 | url: '/album/detail/dynamic',
59 | method: 'get',
60 | params: { id, timestamp: new Date().getTime() },
61 | });
62 | }
63 |
64 | /**
65 | * 收藏/取消收藏专辑
66 | * 说明 : 调用此接口,可收藏/取消收藏专辑
67 | * - id - 返专辑 id
68 | * - t - 1 为收藏,其他为取消收藏
69 | * @param {Object} params
70 | * @param {number} params.id
71 | * @param {number} params.t
72 | */
73 | export function likeAAlbum(params) {
74 | return request({
75 | url: '/album/sub',
76 | method: 'post',
77 | params,
78 | });
79 | }
80 |
--------------------------------------------------------------------------------
/src/utils/updateApp.js:
--------------------------------------------------------------------------------
1 | import initLocalStorage from '@/store/initLocalStorage.js';
2 | import pkg from '../../package.json';
3 |
4 | const updateSetting = () => {
5 | const parsedSettings = JSON.parse(localStorage.getItem('settings'));
6 | const settings = {
7 | ...initLocalStorage.settings,
8 | ...parsedSettings,
9 | };
10 |
11 | if (
12 | settings.shortcuts.length !== initLocalStorage.settings.shortcuts.length
13 | ) {
14 | // 当新增 shortcuts 时
15 | const oldShortcutsId = settings.shortcuts.map(s => s.id);
16 | const newShortcutsId = initLocalStorage.settings.shortcuts.filter(
17 | s => oldShortcutsId.includes(s.id) === false
18 | );
19 | newShortcutsId.map(id => {
20 | settings.shortcuts.push(
21 | initLocalStorage.settings.shortcuts.find(s => s.id === id)
22 | );
23 | });
24 | }
25 |
26 | if (localStorage.getItem('appVersion') === '"0.3.9"') {
27 | settings.lyricsBackground = true;
28 | }
29 |
30 | localStorage.setItem('settings', JSON.stringify(settings));
31 | };
32 |
33 | const updateData = () => {
34 | const parsedData = JSON.parse(localStorage.getItem('data'));
35 | const data = {
36 | ...parsedData,
37 | };
38 | localStorage.setItem('data', JSON.stringify(data));
39 | };
40 |
41 | const updatePlayer = () => {
42 | let parsedData = JSON.parse(localStorage.getItem('player'));
43 | let appVersion = localStorage.getItem('appVersion');
44 | if (appVersion === `"0.2.5"`) parsedData = {}; // 0.2.6版本重构了player
45 | const data = {
46 | ...parsedData,
47 | };
48 | localStorage.setItem('player', JSON.stringify(data));
49 | };
50 |
51 | const removeOldStuff = () => {
52 | // remove old indexedDB databases created by localforage
53 | indexedDB.deleteDatabase('tracks');
54 | };
55 |
56 | export default function () {
57 | updateSetting();
58 | updateData();
59 | updatePlayer();
60 | removeOldStuff();
61 | localStorage.setItem('appVersion', JSON.stringify(pkg.version));
62 | }
63 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 | import state from './state';
4 | import mutations from './mutations';
5 | import actions from './actions';
6 | import { changeAppearance } from '@/utils/common';
7 | import Player from '@/utils/Player';
8 | // vuex 自定义插件
9 | import saveToLocalStorage from './plugins/localStorage';
10 | import { getSendSettingsPlugin } from './plugins/sendSettings';
11 |
12 | Vue.use(Vuex);
13 |
14 | let plugins = [saveToLocalStorage];
15 | if (process.env.IS_ELECTRON === true) {
16 | let sendSettings = getSendSettingsPlugin();
17 | plugins.push(sendSettings);
18 | }
19 | const options = {
20 | state,
21 | mutations,
22 | actions,
23 | plugins,
24 | };
25 |
26 | const store = new Vuex.Store(options);
27 |
28 | if ([undefined, null].includes(store.state.settings.lang)) {
29 | const defaultLang = 'en';
30 | const langMapper = new Map()
31 | .set('zh', 'zh-CN')
32 | .set('zh-TW', 'zh-TW')
33 | .set('en', 'en')
34 | .set('tr', 'tr');
35 | store.state.settings.lang =
36 | langMapper.get(
37 | langMapper.has(navigator.language)
38 | ? navigator.language
39 | : navigator.language.slice(0, 2)
40 | ) || defaultLang;
41 | localStorage.setItem('settings', JSON.stringify(store.state.settings));
42 | }
43 |
44 | changeAppearance(store.state.settings.appearance);
45 |
46 | window
47 | .matchMedia('(prefers-color-scheme: dark)')
48 | .addEventListener('change', () => {
49 | if (store.state.settings.appearance === 'auto') {
50 | changeAppearance(store.state.settings.appearance);
51 | }
52 | });
53 |
54 | let player = new Player();
55 | player = new Proxy(player, {
56 | set(target, prop, val) {
57 | // console.log({ prop, val });
58 | target[prop] = val;
59 | if (prop === '_howler') return true;
60 | target.saveSelfToLocalStorage();
61 | target.sendSelfToIpcMain();
62 | return true;
63 | },
64 | });
65 | store.state.player = player;
66 |
67 | export default store;
68 |
--------------------------------------------------------------------------------
/src/utils/playList.js:
--------------------------------------------------------------------------------
1 | import router from '../router';
2 | import state from '../store/state';
3 | import {
4 | recommendPlaylist,
5 | dailyRecommendPlaylist,
6 | getPlaylistDetail,
7 | } from '@/api/playlist';
8 | import { isAccountLoggedIn } from '@/utils/auth';
9 |
10 | export function hasListSource() {
11 | return !state.player.isPersonalFM && state.player.playlistSource.id !== 0;
12 | }
13 |
14 | export function goToListSource() {
15 | router.push({ path: getListSourcePath() });
16 | }
17 |
18 | export function getListSourcePath() {
19 | if (state.player.playlistSource.id === state.data.likedSongPlaylistID) {
20 | return '/library/liked-songs';
21 | } else if (state.player.playlistSource.type === 'url') {
22 | return state.player.playlistSource.id;
23 | } else if (state.player.playlistSource.type === 'cloudDisk') {
24 | return '/library';
25 | } else {
26 | return `/${state.player.playlistSource.type}/${state.player.playlistSource.id}`;
27 | }
28 | }
29 |
30 | export async function getRecommendPlayList(limit, removePrivateRecommand) {
31 | if (isAccountLoggedIn()) {
32 | const playlists = await Promise.all([
33 | dailyRecommendPlaylist(),
34 | recommendPlaylist({ limit }),
35 | ]);
36 | let recommend = playlists[0].recommend ?? [];
37 | if (recommend.length) {
38 | if (removePrivateRecommand) recommend = recommend.slice(1);
39 | await replaceRecommendResult(recommend);
40 | }
41 | return recommend.concat(playlists[1].result).slice(0, limit);
42 | } else {
43 | const response = await recommendPlaylist({ limit });
44 | return response.result;
45 | }
46 | }
47 |
48 | async function replaceRecommendResult(recommend) {
49 | for (let r of recommend) {
50 | if (specialPlaylist.indexOf(r.id) > -1) {
51 | const data = await getPlaylistDetail(r.id, true);
52 | const playlist = data.playlist;
53 | if (playlist) {
54 | r.name = playlist.name;
55 | r.picUrl = playlist.coverImgUrl;
56 | }
57 | }
58 | }
59 | }
60 |
61 | const specialPlaylist = [3136952023, 2829883282, 2829816518, 2829896389];
62 |
--------------------------------------------------------------------------------
/src/electron/mpris.js:
--------------------------------------------------------------------------------
1 | import { ipcMain, app } from 'electron';
2 |
3 | export function createMpris(window) {
4 | const Player = require('mpris-service');
5 | const renderer = window.webContents;
6 |
7 | const player = Player({
8 | name: 'yesplaymusic',
9 | identity: 'YesPlayMusic',
10 | });
11 |
12 | player.on('next', () => renderer.send('next'));
13 | player.on('previous', () => renderer.send('previous'));
14 | player.on('playpause', () => renderer.send('play'));
15 | player.on('play', () => renderer.send('play'));
16 | player.on('pause', () => renderer.send('play'));
17 | player.on('quit', () => app.exit());
18 | player.on('position', args =>
19 | renderer.send('setPosition', args.position / 1000 / 1000)
20 | );
21 | player.on('loopStatus', () => renderer.send('repeat'));
22 | player.on('shuffle', () => renderer.send('shuffle'));
23 |
24 | ipcMain.on('player', (e, { playing }) => {
25 | player.playbackStatus = playing
26 | ? Player.PLAYBACK_STATUS_PLAYING
27 | : Player.PLAYBACK_STATUS_PAUSED;
28 | });
29 |
30 | ipcMain.on('metadata', (e, metadata) => {
31 | player.metadata = {
32 | 'mpris:trackid': player.objectPath('track/' + metadata.trackId),
33 | 'mpris:artUrl': metadata.artwork[0].src,
34 | 'mpris:length': metadata.length * 1000 * 1000,
35 | 'xesam:title': metadata.title,
36 | 'xesam:album': metadata.album,
37 | 'xesam:artist': metadata.artist.split(','),
38 | };
39 | });
40 |
41 | ipcMain.on('playerCurrentTrackTime', (e, position) => {
42 | player.getPosition = () => position * 1000 * 1000;
43 | });
44 |
45 | ipcMain.on('switchRepeatMode', (e, mode) => {
46 | switch (mode) {
47 | case 'off':
48 | player.loopStatus = Player.LOOP_STATUS_NONE;
49 | break;
50 | case 'one':
51 | player.loopStatus = Player.LOOP_STATUS_TRACK;
52 | break;
53 | case 'on':
54 | player.loopStatus = Player.LOOP_STATUS_PLAYLIST;
55 | break;
56 | }
57 | });
58 |
59 | ipcMain.on('switchShuffle', (e, shuffle) => {
60 | player.shuffle = shuffle;
61 | });
62 | }
63 |
--------------------------------------------------------------------------------
/src/utils/base64.js:
--------------------------------------------------------------------------------
1 | // https://github.com/niklasvh/base64-arraybuffer/blob/master/src/index.ts
2 | // Copyright (c) 2012 Niklas von Hertzen Licensed under the MIT license.
3 |
4 | const chars =
5 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
6 |
7 | // Use a lookup table to find the index.
8 | const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
9 | for (let i = 0; i < chars.length; i++) {
10 | lookup[chars.charCodeAt(i)] = i;
11 | }
12 |
13 | export const encode = arraybuffer => {
14 | let bytes = new Uint8Array(arraybuffer),
15 | i,
16 | len = bytes.length,
17 | base64 = '';
18 |
19 | for (i = 0; i < len; i += 3) {
20 | base64 += chars[bytes[i] >> 2];
21 | base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
22 | base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
23 | base64 += chars[bytes[i + 2] & 63];
24 | }
25 |
26 | if (len % 3 === 2) {
27 | base64 = base64.substring(0, base64.length - 1) + '=';
28 | } else if (len % 3 === 1) {
29 | base64 = base64.substring(0, base64.length - 2) + '==';
30 | }
31 |
32 | return base64;
33 | };
34 |
35 | export const decode = base64 => {
36 | let bufferLength = base64.length * 0.75,
37 | len = base64.length,
38 | i,
39 | p = 0,
40 | encoded1,
41 | encoded2,
42 | encoded3,
43 | encoded4;
44 |
45 | if (base64[base64.length - 1] === '=') {
46 | bufferLength--;
47 | if (base64[base64.length - 2] === '=') {
48 | bufferLength--;
49 | }
50 | }
51 |
52 | const arraybuffer = new ArrayBuffer(bufferLength),
53 | bytes = new Uint8Array(arraybuffer);
54 |
55 | for (i = 0; i < len; i += 4) {
56 | encoded1 = lookup[base64.charCodeAt(i)];
57 | encoded2 = lookup[base64.charCodeAt(i + 1)];
58 | encoded3 = lookup[base64.charCodeAt(i + 2)];
59 | encoded4 = lookup[base64.charCodeAt(i + 3)];
60 |
61 | bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
62 | bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
63 | bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
64 | }
65 |
66 | return arraybuffer;
67 | };
68 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import router from '@/router';
2 | import { doLogout, getCookie } from '@/utils/auth';
3 | import axios from 'axios';
4 |
5 | let baseURL = '';
6 | // Web 和 Electron 跑在不同端口避免同时启动时冲突
7 | if (process.env.IS_ELECTRON) {
8 | if (process.env.NODE_ENV === 'production') {
9 | baseURL = process.env.VUE_APP_ELECTRON_API_URL;
10 | } else {
11 | baseURL = process.env.VUE_APP_ELECTRON_API_URL_DEV;
12 | }
13 | } else {
14 | baseURL = process.env.VUE_APP_NETEASE_API_URL;
15 | }
16 |
17 | const service = axios.create({
18 | baseURL,
19 | withCredentials: true,
20 | timeout: 15000,
21 | });
22 |
23 | service.interceptors.request.use(function (config) {
24 | if (!config.params) config.params = {};
25 | if (baseURL.length) {
26 | if (baseURL[0] !== '/' && !process.env.IS_ELECTRON) {
27 | config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`;
28 | }
29 | } else {
30 | console.error("You must set up the baseURL in the service's config");
31 | }
32 |
33 | if (!process.env.IS_ELECTRON && !config.url.includes('/login')) {
34 | config.params.realIP = '211.161.244.70';
35 | }
36 |
37 | if (process.env.VUE_APP_REAL_IP) {
38 | config.params.realIP = process.env.VUE_APP_REAL_IP;
39 | }
40 |
41 | const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig;
42 | if (['HTTP', 'HTTPS'].includes(proxy.protocol)) {
43 | config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`;
44 | }
45 |
46 | return config;
47 | });
48 |
49 | service.interceptors.response.use(
50 | response => {
51 | const res = response.data;
52 | return res;
53 | },
54 | async error => {
55 | /** @type {import('axios').AxiosResponse | null} */
56 | const response = error.response;
57 | const data = response.data;
58 |
59 | if (
60 | response &&
61 | typeof data === 'object' &&
62 | data.code === 301 &&
63 | data.msg === '需要登录'
64 | ) {
65 | console.warn('Token has expired. Logout now!');
66 |
67 | // 登出帳戶
68 | doLogout();
69 |
70 | // 導向登入頁面
71 | if (process.env.IS_ELECTRON === true) {
72 | router.push({ name: 'loginAccount' });
73 | } else {
74 | router.push({ name: 'login' });
75 | }
76 | }
77 | }
78 | );
79 |
80 | export default service;
81 |
--------------------------------------------------------------------------------
/src/components/ButtonTwoTone.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
61 |
62 |
97 |
--------------------------------------------------------------------------------
/src/api/lastfm.js:
--------------------------------------------------------------------------------
1 | // Last.fm API documents 👉 https://www.last.fm/api
2 |
3 | import axios from 'axios';
4 | import md5 from 'crypto-js/md5';
5 |
6 | const apiKey = process.env.VUE_APP_LASTFM_API_KEY;
7 | const apiSharedSecret = process.env.VUE_APP_LASTFM_API_SHARED_SECRET;
8 | const baseUrl = window.location.origin;
9 | const url = 'https://ws.audioscrobbler.com/2.0/';
10 |
11 | const sign = params => {
12 | const sortParamsKeys = Object.keys(params).sort();
13 | const sortedParams = sortParamsKeys.reduce((acc, key) => {
14 | acc[key] = params[key];
15 | return acc;
16 | }, {});
17 | let signature = '';
18 | for (const [key, value] of Object.entries(sortedParams)) {
19 | signature += `${key}${value}`;
20 | }
21 | return md5(signature + apiSharedSecret).toString();
22 | };
23 |
24 | export function auth() {
25 | const url = process.env.IS_ELECTRON
26 | ? `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${baseUrl}/#/lastfm/callback`
27 | : `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${baseUrl}/lastfm/callback`;
28 | window.open(url);
29 | }
30 |
31 | export function authGetSession(token) {
32 | const signature = md5(
33 | `api_key${apiKey}methodauth.getSessiontoken${token}${apiSharedSecret}`
34 | ).toString();
35 | return axios({
36 | url,
37 | method: 'GET',
38 | params: {
39 | method: 'auth.getSession',
40 | format: 'json',
41 | api_key: apiKey,
42 | api_sig: signature,
43 | token,
44 | },
45 | });
46 | }
47 |
48 | export function trackUpdateNowPlaying(params) {
49 | params.api_key = apiKey;
50 | params.method = 'track.updateNowPlaying';
51 | params.sk = JSON.parse(localStorage.getItem('lastfm'))['key'];
52 | const signature = sign(params);
53 |
54 | return axios({
55 | url,
56 | method: 'POST',
57 | params: {
58 | ...params,
59 | api_sig: signature,
60 | format: 'json',
61 | },
62 | });
63 | }
64 |
65 | export function trackScrobble(params) {
66 | params.api_key = apiKey;
67 | params.method = 'track.scrobble';
68 | params.sk = JSON.parse(localStorage.getItem('lastfm'))['key'];
69 | const signature = sign(params);
70 |
71 | return axios({
72 | url,
73 | method: 'POST',
74 | params: {
75 | ...params,
76 | api_sig: signature,
77 | format: 'json',
78 | },
79 | });
80 | }
81 |
--------------------------------------------------------------------------------
/src/views/lastfmCallback.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |

7 |
8 |
{{ message }}
9 |
10 |
11 |
12 |
13 |
47 |
48 |
97 |
--------------------------------------------------------------------------------
/src/utils/lyrics.js:
--------------------------------------------------------------------------------
1 | export function lyricParser(lrc) {
2 | return {
3 | lyric: parseLyric(lrc?.lrc?.lyric || ''),
4 | tlyric: parseLyric(lrc?.tlyric?.lyric || ''),
5 | lyricuser: lrc.lyricUser,
6 | transuser: lrc.transUser,
7 | };
8 | }
9 |
10 | // regexr.com/6e52n
11 | const extractLrcRegex =
12 | /^(?(?:\[.+?\])+)(?!\[)(?.+)$/gm;
13 | const extractTimestampRegex =
14 | /\[(?\d+):(?\d+)(?:\.|:)*(?\d+)*\]/g;
15 |
16 | /**
17 | * @typedef {{time: number, rawTime: string, content: string}} ParsedLyric
18 | */
19 |
20 | /**
21 | * Parse the lyric string.
22 | *
23 | * @param {string} lrc The `lrc` input.
24 | * @returns {ParsedLyric[]} The parsed lyric.
25 | * @example parseLyric("[00:00.00] Hello, World!\n[00:00.10] Test\n");
26 | */
27 | function parseLyric(lrc) {
28 | /**
29 | * A sorted list of parsed lyric and its timestamp.
30 | *
31 | * @type {ParsedLyric[]}
32 | * @see binarySearch
33 | */
34 | const parsedLyrics = [];
35 |
36 | /**
37 | * Find the appropriate index to push our parsed lyric.
38 | * @param {ParsedLyric} lyric
39 | */
40 | const binarySearch = lyric => {
41 | let time = lyric.time;
42 |
43 | let low = 0;
44 | let high = parsedLyrics.length - 1;
45 |
46 | while (low <= high) {
47 | const mid = Math.floor((low + high) / 2);
48 | const midTime = parsedLyrics[mid].time;
49 | if (midTime === time) {
50 | return mid;
51 | } else if (midTime < time) {
52 | low = mid + 1;
53 | } else {
54 | high = mid - 1;
55 | }
56 | }
57 |
58 | return low;
59 | };
60 |
61 | for (const line of lrc.trim().matchAll(extractLrcRegex)) {
62 | const { lyricTimestamps, content } = line.groups;
63 |
64 | for (const timestamp of lyricTimestamps.matchAll(extractTimestampRegex)) {
65 | const { min, sec, ms } = timestamp.groups;
66 | const rawTime = timestamp[0];
67 | const time = Number(min) * 60 + Number(sec) + Number(ms ?? 0) * 0.001;
68 |
69 | /** @type {ParsedLyric} */
70 | const parsedLyric = { rawTime, time, content: trimContent(content) };
71 | parsedLyrics.splice(binarySearch(parsedLyric), 0, parsedLyric);
72 | }
73 | }
74 |
75 | return parsedLyrics;
76 | }
77 |
78 | /**
79 | * @param {string} content
80 | * @returns {string}
81 | */
82 | function trimContent(content) {
83 | let t = content.trim();
84 | return t.length < 1 ? content : t;
85 | }
86 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import shortcuts from '@/utils/shortcuts';
2 | import cloneDeep from 'lodash/cloneDeep';
3 |
4 | export default {
5 | updateLikedXXX(state, { name, data }) {
6 | state.liked[name] = data;
7 | if (name === 'songs') {
8 | state.player.sendSelfToIpcMain();
9 | }
10 | },
11 | changeLang(state, lang) {
12 | state.settings.lang = lang;
13 | },
14 | changeMusicQuality(state, value) {
15 | state.settings.musicQuality = value;
16 | },
17 | changeLyricFontSize(state, value) {
18 | state.settings.lyricFontSize = value;
19 | },
20 | changeOutputDevice(state, deviceId) {
21 | state.settings.outputDevice = deviceId;
22 | },
23 | updateSettings(state, { key, value }) {
24 | state.settings[key] = value;
25 | },
26 | updateData(state, { key, value }) {
27 | state.data[key] = value;
28 | },
29 | togglePlaylistCategory(state, name) {
30 | const index = state.settings.enabledPlaylistCategories.findIndex(
31 | c => c === name
32 | );
33 | if (index !== -1) {
34 | state.settings.enabledPlaylistCategories =
35 | state.settings.enabledPlaylistCategories.filter(c => c !== name);
36 | } else {
37 | state.settings.enabledPlaylistCategories.push(name);
38 | }
39 | },
40 | updateToast(state, toast) {
41 | state.toast = toast;
42 | },
43 | updateModal(state, { modalName, key, value }) {
44 | state.modals[modalName][key] = value;
45 | if (key === 'show') {
46 | // 100ms的延迟是为等待右键菜单blur之后再disableScrolling
47 | value === true
48 | ? setTimeout(() => (state.enableScrolling = false), 100)
49 | : (state.enableScrolling = true);
50 | }
51 | },
52 | toggleLyrics(state) {
53 | state.showLyrics = !state.showLyrics;
54 | },
55 | updateDailyTracks(state, dailyTracks) {
56 | state.dailyTracks = dailyTracks;
57 | },
58 | updateLastfm(state, session) {
59 | state.lastfm = session;
60 | },
61 | updateShortcut(state, { id, type, shortcut }) {
62 | let newShortcut = state.settings.shortcuts.find(s => s.id === id);
63 | newShortcut[type] = shortcut;
64 | state.settings.shortcuts = state.settings.shortcuts.map(s => {
65 | if (s.id !== id) return s;
66 | return newShortcut;
67 | });
68 | },
69 | restoreDefaultShortcuts(state) {
70 | state.settings.shortcuts = cloneDeep(shortcuts);
71 | },
72 | enableScrolling(state, status = null) {
73 | state.enableScrolling = status ? status : !state.enableScrolling;
74 | },
75 | updateTitle(state, title) {
76 | state.title = title;
77 | },
78 | };
79 |
--------------------------------------------------------------------------------
/src/views/artistMV.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ artist.name }}'s Music Videos
9 |
10 |
11 |
12 | {{
13 | $t('explore.loadMore')
14 | }}
15 |
16 |
17 |
18 |
19 |
83 |
84 |
101 |
--------------------------------------------------------------------------------
/src/api/artist.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request';
2 | import { mapTrackPlayableStatus } from '@/utils/common';
3 |
4 | /**
5 | * 获取歌手单曲
6 | * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手部分信息和热门歌曲
7 | * @param {number} id - 歌手 id, 可由搜索接口获得
8 | */
9 | export function getArtist(id) {
10 | return request({
11 | url: '/artists',
12 | method: 'get',
13 | params: {
14 | id,
15 | timestamp: new Date().getTime(),
16 | },
17 | }).then(data => {
18 | data.hotSongs = mapTrackPlayableStatus(data.hotSongs);
19 | return data;
20 | });
21 | }
22 |
23 | /**
24 | * 获取歌手专辑
25 | * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手专辑内容
26 | * - id: 歌手 id
27 | * - limit: 取出数量 , 默认为 50
28 | * - offset: 偏移数量 , 用于分页 , 如 :( 页数 -1)*50, 其中 50 为 limit 的值 , 默认为 0
29 | * @param {Object} params
30 | * @param {number} params.id
31 | * @param {number=} params.limit
32 | * @param {number=} params.offset
33 | */
34 | export function getArtistAlbum(params) {
35 | return request({
36 | url: '/artist/album',
37 | method: 'get',
38 | params,
39 | });
40 | }
41 |
42 | /**
43 | * 歌手榜
44 | * 说明 : 调用此接口 , 可获取排行榜中的歌手榜
45 | * - type : 地区
46 | * 1: 华语
47 | * 2: 欧美
48 | * 3: 韩国
49 | * 4: 日本
50 | * @param {number=} type
51 | */
52 | export function toplistOfArtists(type = null) {
53 | let params = {};
54 | if (type) {
55 | params.type = type;
56 | }
57 | return request({
58 | url: '/toplist/artist',
59 | method: 'get',
60 | params,
61 | });
62 | }
63 | /**
64 | * 获取歌手 mv
65 | * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手 mv 信息 , 具体 mv 播放地址可调 用/mv传入此接口获得的 mvid 来拿到 , 如 : /artist/mv?id=6452,/mv?mvid=5461064
66 | * @param {number} params.id 歌手 id, 可由搜索接口获得
67 | * @param {number} params.offset
68 | * @param {number} params.limit
69 | */
70 | export function artistMv(params) {
71 | return request({
72 | url: '/artist/mv',
73 | method: 'get',
74 | params,
75 | });
76 | }
77 |
78 | /**
79 | * 收藏歌手
80 | * 说明 : 调用此接口 , 传入歌手 id, 可收藏歌手
81 | * - id: 歌手 id
82 | * - t: 操作,1 为收藏,其他为取消收藏
83 | * @param {Object} params
84 | * @param {number} params.id
85 | * @param {number} params.t
86 | */
87 | export function followAArtist(params) {
88 | return request({
89 | url: '/artist/sub',
90 | method: 'post',
91 | params,
92 | });
93 | }
94 |
95 | /**
96 | * 相似歌手
97 | * 说明 : 调用此接口 , 传入歌手 id, 可获得相似歌手
98 | * - id: 歌手 id
99 | * @param {number} id
100 | */
101 | export function similarArtists(id) {
102 | return request({
103 | url: '/simi/artist',
104 | method: 'post',
105 | params: { id },
106 | });
107 | }
108 |
--------------------------------------------------------------------------------
/src/api/auth.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request';
2 |
3 | /**
4 | * 手机登录
5 | * - phone: 手机号码
6 | * - password: 密码
7 | * - countrycode: 国家码,用于国外手机号登录,例如美国传入:1
8 | * - md5_password: md5加密后的密码,传入后 password 将失效
9 | * @param {Object} params
10 | * @param {string} params.phone
11 | * @param {string} params.password
12 | * @param {string=} params.countrycode
13 | * @param {string=} params.md5_password
14 | */
15 | export function loginWithPhone(params) {
16 | return request({
17 | url: '/login/cellphone',
18 | method: 'post',
19 | params,
20 | });
21 | }
22 |
23 | /**
24 | * 邮箱登录
25 | * - email: 163 网易邮箱
26 | * - password: 密码
27 | * - md5_password: md5加密后的密码,传入后 password 将失效
28 | * @param {Object} params
29 | * @param {string} params.email
30 | * @param {string} params.password
31 | * @param {string=} params.md5_password
32 | */
33 | export function loginWithEmail(params) {
34 | return request({
35 | url: '/login',
36 | method: 'post',
37 | params,
38 | });
39 | }
40 |
41 | /**
42 | * 二维码key生成接口
43 | */
44 | export function loginQrCodeKey() {
45 | return request({
46 | url: '/login/qr/key',
47 | method: 'get',
48 | params: {
49 | timestamp: new Date().getTime(),
50 | },
51 | });
52 | }
53 |
54 | /**
55 | * 二维码生成接口
56 | * 说明: 调用此接口传入上一个接口生成的key可生成二维码图片的base64和二维码信息,
57 | * 可使用base64展示图片,或者使用二维码信息内容自行使用第三方二维码生产库渲染二维码
58 | * @param {Object} params
59 | * @param {string} params.key
60 | * @param {string=} params.qrimg 传入后会额外返回二维码图片base64编码
61 | */
62 | export function loginQrCodeCreate(params) {
63 | return request({
64 | url: '/login/qr/create',
65 | method: 'get',
66 | params: {
67 | ...params,
68 | timestamp: new Date().getTime(),
69 | },
70 | });
71 | }
72 |
73 | /**
74 | * 二维码检测扫码状态接口
75 | * 说明: 轮询此接口可获取二维码扫码状态,800为二维码过期,801为等待扫码,802为待确认,803为授权登录成功(803状态码下会返回cookies)
76 | * @param {string} key
77 | */
78 | export function loginQrCodeCheck(key) {
79 | return request({
80 | url: '/login/qr/check',
81 | method: 'get',
82 | params: {
83 | key,
84 | timestamp: new Date().getTime(),
85 | },
86 | });
87 | }
88 |
89 | /**
90 | * 刷新登录
91 | * 说明 : 调用此接口 , 可刷新登录状态
92 | * - 调用例子 : /login/refresh
93 | */
94 | export function refreshCookie() {
95 | return request({
96 | url: '/login/refresh',
97 | method: 'post',
98 | });
99 | }
100 |
101 | /**
102 | * 退出登录
103 | * 说明 : 调用此接口 , 可退出登录
104 | */
105 | export function logout() {
106 | return request({
107 | url: '/logout',
108 | method: 'post',
109 | });
110 | }
111 |
--------------------------------------------------------------------------------
/src/electron/ipcRenderer.js:
--------------------------------------------------------------------------------
1 | import store from '@/store';
2 |
3 | const player = store.state.player;
4 |
5 | export function ipcRenderer(vueInstance) {
6 | const self = vueInstance;
7 | // 添加专有的类名
8 | document.body.setAttribute('data-electron', 'yes');
9 | document.body.setAttribute(
10 | 'data-electron-os',
11 | window.require('os').platform()
12 | );
13 | // ipc message channel
14 | const electron = window.require('electron');
15 | const ipcRenderer = electron.ipcRenderer;
16 |
17 | // listens to the main process 'changeRouteTo' event and changes the route from
18 | // inside this Vue instance, according to what path the main process requires.
19 | // responds to Menu click() events at the main process and changes the route accordingly.
20 |
21 | ipcRenderer.on('changeRouteTo', (event, path) => {
22 | self.$router.push(path);
23 | if (store.state.showLyrics) {
24 | store.commit('toggleLyrics');
25 | }
26 | });
27 |
28 | ipcRenderer.on('search', () => {
29 | // 触发数据响应
30 | self.$refs.navbar.$refs.searchInput.focus();
31 | self.$refs.navbar.inputFocus = true;
32 | });
33 |
34 | ipcRenderer.on('play', () => {
35 | player.playOrPause();
36 | });
37 |
38 | ipcRenderer.on('next', () => {
39 | if (player.isPersonalFM) {
40 | player.playNextFMTrack();
41 | } else {
42 | player.playNextTrack();
43 | }
44 | });
45 |
46 | ipcRenderer.on('previous', () => {
47 | player.playPrevTrack();
48 | });
49 |
50 | ipcRenderer.on('increaseVolume', () => {
51 | if (player.volume + 0.1 >= 1) {
52 | return (player.volume = 1);
53 | }
54 | player.volume += 0.1;
55 | });
56 |
57 | ipcRenderer.on('decreaseVolume', () => {
58 | if (player.volume - 0.1 <= 0) {
59 | return (player.volume = 0);
60 | }
61 | player.volume -= 0.1;
62 | });
63 |
64 | ipcRenderer.on('like', () => {
65 | store.dispatch('likeATrack', player.currentTrack.id);
66 | });
67 |
68 | ipcRenderer.on('repeat', () => {
69 | player.switchRepeatMode();
70 | });
71 |
72 | ipcRenderer.on('shuffle', () => {
73 | player.switchShuffle();
74 | });
75 |
76 | ipcRenderer.on('routerGo', (event, where) => {
77 | self.$refs.navbar.go(where);
78 | });
79 |
80 | ipcRenderer.on('nextUp', () => {
81 | self.$refs.player.goToNextTracksPage();
82 | });
83 |
84 | ipcRenderer.on('rememberCloseAppOption', (event, value) => {
85 | store.commit('updateSettings', {
86 | key: 'closeAppOption',
87 | value,
88 | });
89 | });
90 |
91 | ipcRenderer.on('setPosition', (event, position) => {
92 | player._howler.seek(position);
93 | });
94 | }
95 |
--------------------------------------------------------------------------------
/src/electron/touchBar.js:
--------------------------------------------------------------------------------
1 | const { TouchBar, nativeImage, ipcMain } = require('electron');
2 | const { TouchBarButton, TouchBarSpacer } = TouchBar;
3 | const path = require('path');
4 |
5 | export function createTouchBar(window) {
6 | const renderer = window.webContents;
7 |
8 | // Icon follow touchbar design guideline.
9 | // See: https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/
10 | // Icon Resource: https://devimages-cdn.apple.com/design/resources/
11 | function getNativeIcon(name) {
12 | return nativeImage.createFromPath(
13 | // eslint-disable-next-line no-undef
14 | path.join(__static, 'img/touchbar/', name)
15 | );
16 | }
17 |
18 | const previousPage = new TouchBarButton({
19 | click: () => {
20 | renderer.send('routerGo', 'back');
21 | },
22 | icon: getNativeIcon('page_prev.png'),
23 | });
24 |
25 | const nextPage = new TouchBarButton({
26 | click: () => {
27 | renderer.send('routerGo', 'forward');
28 | },
29 | icon: getNativeIcon('page_next.png'),
30 | });
31 |
32 | const searchButton = new TouchBarButton({
33 | click: () => {
34 | renderer.send('search');
35 | },
36 | icon: getNativeIcon('search.png'),
37 | });
38 |
39 | const playButton = new TouchBarButton({
40 | click: () => {
41 | renderer.send('play');
42 | },
43 | icon: getNativeIcon('play.png'),
44 | });
45 |
46 | const previousTrackButton = new TouchBarButton({
47 | click: () => {
48 | renderer.send('previous');
49 | },
50 | icon: getNativeIcon('backward.png'),
51 | });
52 |
53 | const nextTrackButton = new TouchBarButton({
54 | click: () => {
55 | renderer.send('next');
56 | },
57 | icon: getNativeIcon('forward.png'),
58 | });
59 |
60 | const likeButton = new TouchBarButton({
61 | click: () => {
62 | renderer.send('like');
63 | },
64 | icon: getNativeIcon('like.png'),
65 | });
66 |
67 | const nextUpButton = new TouchBarButton({
68 | click: () => {
69 | renderer.send('nextUp');
70 | },
71 | icon: getNativeIcon('next_up.png'),
72 | });
73 |
74 | ipcMain.on('player', (e, { playing, likedCurrentTrack }) => {
75 | playButton.icon =
76 | playing === true ? getNativeIcon('pause.png') : getNativeIcon('play.png');
77 | likeButton.icon = likedCurrentTrack
78 | ? getNativeIcon('like_fill.png')
79 | : getNativeIcon('like.png');
80 | });
81 |
82 | const touchBar = new TouchBar({
83 | items: [
84 | previousPage,
85 | nextPage,
86 | searchButton,
87 | new TouchBarSpacer({ size: 'flexible' }),
88 | previousTrackButton,
89 | playButton,
90 | nextTrackButton,
91 | new TouchBarSpacer({ size: 'flexible' }),
92 | likeButton,
93 | nextUpButton,
94 | ],
95 | });
96 | return touchBar;
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/Win32Titlebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
66 |
67 |
122 |
--------------------------------------------------------------------------------
/src/views/dailyTracks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
每日歌曲推荐
5 |
根据你的音乐口味生成 · 每天6:00更新
6 |
7 |
8 |
13 |
14 |
15 |
16 |
59 |
60 |
133 |
--------------------------------------------------------------------------------
/src/components/LinuxTitlebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
{{ title }}
7 |
25 |
26 |
27 |
28 |
69 |
70 |
131 |
--------------------------------------------------------------------------------
/src/assets/css/slider.css:
--------------------------------------------------------------------------------
1 | /* rail style */
2 | .vue-slider-rail {
3 | background-color: rgba(128, 128, 128, 0.18);
4 | border-radius: 15px;
5 | }
6 |
7 | /* process style */
8 | .vue-slider-process {
9 | background-color: #335eea;
10 | border-radius: 15px;
11 | }
12 |
13 | /* dot style */
14 | .vue-slider-dot-handle {
15 | cursor: pointer;
16 | width: 100%;
17 | height: 100%;
18 | border-radius: 50%;
19 | background-color: #fff;
20 | box-sizing: border-box;
21 | box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.12);
22 | visibility: hidden;
23 | }
24 |
25 | /* tooltip style */
26 | .vue-slider-dot-tooltip-wrapper {
27 | opacity: 0;
28 | transition: all 1s;
29 | }
30 |
31 | .vue-slider-dot-tooltip-wrapper-show {
32 | opacity: 1;
33 | }
34 |
35 | .vue-slider-dot-tooltip-inner {
36 | font-size: 14px;
37 | white-space: nowrap;
38 | padding: 2px 6px;
39 | min-width: 20px;
40 | text-align: center;
41 | color: #000;
42 | border-radius: 5px;
43 | border-color: #fff;
44 | background-color: #fff;
45 | box-sizing: content-box;
46 | box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.08);
47 | }
48 |
49 | /* hover */
50 | .vue-slider:hover .vue-slider-dot-handle,
51 | .vue-slider:active .vue-slider-dot-handle {
52 | visibility: visible;
53 | }
54 |
55 | /* volume style */
56 | .volume-control .vue-slider-process {
57 | opacity: 0.8;
58 | background-color: var(--color-text);
59 | border-radius: 15px;
60 | }
61 |
62 | .volume-control:hover .vue-slider-process {
63 | background-color: #335eea;
64 | }
65 |
66 | /* nyancat */
67 |
68 | .nyancat .vue-slider-rail {
69 | background-color: rgba(128, 128, 128, 0.18);
70 | padding: 2.5px 0px;
71 | border-radius: 0;
72 | }
73 |
74 | .nyancat .vue-slider-process {
75 | padding: 0px 1px;
76 | top: -2px;
77 | border-radius: 0;
78 | background: -webkit-gradient(
79 | linear,
80 | left top,
81 | left bottom,
82 | color-stop(0, #f00),
83 | color-stop(17%, #f90),
84 | color-stop(33%, #ff0),
85 | color-stop(50%, #3f0),
86 | color-stop(67%, #09f),
87 | color-stop(83%, #63f)
88 | );
89 | }
90 |
91 | .nyancat .vue-slider-dot-handle {
92 | background: url('/img/logos/nyancat.gif');
93 | background-size: 36px;
94 | width: 36px;
95 | height: 24px;
96 | margin-top: -6px;
97 | box-shadow: none;
98 | border-radius: 0;
99 | box-sizing: border-box;
100 | visibility: visible;
101 | }
102 |
103 | .nyancat-stop .vue-slider-dot-handle {
104 | background-image: url('/img/logos/nyancat-stop.png');
105 | transition: 300ms;
106 | }
107 |
108 | /* lyrics */
109 | .lyrics-page .vue-slider-rail {
110 | background-color: rgba(128, 128, 128, 0.18);
111 | border-radius: 2px;
112 | height: 4px;
113 | opacity: 0.88;
114 | }
115 |
116 | .lyrics-page .vue-slider-process {
117 | background-color: #060606;
118 | }
119 |
120 | .lyrics-page .vue-slider-dot-handle {
121 | background-color: #060606;
122 | box-shadow: unset;
123 | }
124 |
125 | .lyrics-page .vue-slider-dot-tooltip {
126 | display: none;
127 | }
128 |
129 | body[data-theme='dark'] .lyrics-page .vue-slider-process {
130 | background-color: #fafafa;
131 | }
132 |
133 | body[data-theme='dark'] .lyrics-page .vue-slider-dot-handle {
134 | background-color: #fff;
135 | }
136 |
137 | .lyrics-page[data-theme='dark'] .vue-slider-rail {
138 | background-color: rgba(255, 255, 255, 0.18);
139 | }
140 |
141 | .lyrics-page[data-theme='dark'] .vue-slider-process,
142 | .lyrics-page[data-theme='dark'] .vue-slider-dot-handle {
143 | background-color: #fff;
144 | }
145 |
--------------------------------------------------------------------------------
/src/utils/filters.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import dayjs from 'dayjs';
3 | import duration from 'dayjs/plugin/duration';
4 | import relativeTime from 'dayjs/plugin/relativeTime';
5 | import locale from '@/locale';
6 |
7 | Vue.filter('formatTime', (Milliseconds, format = 'HH:MM:SS') => {
8 | if (!Milliseconds) return '';
9 | dayjs.extend(duration);
10 | dayjs.extend(relativeTime);
11 |
12 | let time = dayjs.duration(Milliseconds);
13 | let hours = time.hours().toString();
14 | let mins = time.minutes().toString();
15 | let seconds = time.seconds().toString().padStart(2, '0');
16 |
17 | if (format === 'HH:MM:SS') {
18 | return hours !== '0'
19 | ? `${hours}:${mins.padStart(2, '0')}:${seconds}`
20 | : `${mins}:${seconds}`;
21 | } else if (format === 'Human') {
22 | let hoursUnit, minitesUnit;
23 | switch (locale.locale) {
24 | case 'zh-CN':
25 | hoursUnit = '小时';
26 | minitesUnit = '分钟';
27 | break;
28 | case 'zh-TW':
29 | hoursUnit = '小時';
30 | minitesUnit = '分鐘';
31 | break;
32 | default:
33 | hoursUnit = 'hr';
34 | minitesUnit = 'min';
35 | break;
36 | }
37 | return hours !== '0'
38 | ? `${hours} ${hoursUnit} ${mins} ${minitesUnit}`
39 | : `${mins} ${minitesUnit}`;
40 | }
41 | });
42 |
43 | Vue.filter('formatDate', (timestamp, format = 'MMM D, YYYY') => {
44 | if (!timestamp) return '';
45 | if (locale.locale === 'zh-CN') format = 'YYYY年MM月DD日';
46 | else if (locale.locale === 'zh-TW') format = 'YYYY年MM月DD日';
47 | return dayjs(timestamp).format(format);
48 | });
49 |
50 | Vue.filter('formatAlbumType', (type, album) => {
51 | if (!type) return '';
52 | if (type === 'EP/Single') {
53 | return album.size === 1 ? 'Single' : 'EP';
54 | } else if (type === 'Single') {
55 | return 'Single';
56 | } else if (type === '专辑') {
57 | return 'Album';
58 | } else {
59 | return type;
60 | }
61 | });
62 |
63 | Vue.filter('resizeImage', (imgUrl, size = 512) => {
64 | if (!imgUrl) return '';
65 | let httpsImgUrl = imgUrl;
66 | if (imgUrl.slice(0, 5) !== 'https') {
67 | httpsImgUrl = 'https' + imgUrl.slice(4);
68 | }
69 | return `${httpsImgUrl}?param=${size}y${size}`;
70 | });
71 |
72 | Vue.filter('formatPlayCount', count => {
73 | if (!count) return '';
74 | if (locale.locale === 'zh-CN') {
75 | if (count > 100000000) {
76 | return `${Math.floor((count / 100000000) * 100) / 100}亿`; // 2.32 亿
77 | }
78 | if (count > 100000) {
79 | return `${Math.floor((count / 10000) * 10) / 10}万`; // 232.1 万
80 | }
81 | if (count > 10000) {
82 | return `${Math.floor((count / 10000) * 100) / 100}万`; // 2.3 万
83 | }
84 | return count;
85 | } else if (locale.locale === 'zh-TW') {
86 | if (count > 100000000) {
87 | return `${Math.floor((count / 100000000) * 100) / 100}億`; // 2.32 億
88 | }
89 | if (count > 100000) {
90 | return `${Math.floor((count / 10000) * 10) / 10}萬`; // 232.1 萬
91 | }
92 | if (count > 10000) {
93 | return `${Math.floor((count / 10000) * 100) / 100}萬`; // 2.3 萬
94 | }
95 | return count;
96 | } else {
97 | if (count > 10000000) {
98 | return `${Math.floor((count / 1000000) * 10) / 10}M`; // 233.2M
99 | }
100 | if (count > 1000000) {
101 | return `${Math.floor((count / 1000000) * 100) / 100}M`; // 2.3M
102 | }
103 | if (count > 1000) {
104 | return `${Math.floor((count / 1000) * 100) / 100}K`; // 233.23K
105 | }
106 | return count;
107 | }
108 | });
109 |
110 | Vue.filter('toHttps', url => {
111 | if (!url) return '';
112 | return url.replace(/^http:/, 'https:');
113 | });
114 |
--------------------------------------------------------------------------------
/src/views/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |

7 |
8 |
9 |
15 |
16 |
17 |
{{ $t('login.loginText') }}
18 |
{{ $t('login.accessToAll') }}
19 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
{{ $t('login.search') }}
32 |
{{ $t('login.readonly') }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
66 |
67 |
152 |
--------------------------------------------------------------------------------
/src/assets/css/global.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Barlow';
3 | font-weight: normal;
4 | src: url('~@/assets/fonts/Barlow-Regular.woff2') format('woff2'),
5 | url('~@/assets/fonts/Barlow-Regular.ttf') format('truetype');
6 | }
7 | @font-face {
8 | font-family: 'Barlow';
9 | font-weight: medium;
10 | src: url('~@/assets/fonts/Barlow-Medium.woff2') format('woff2'),
11 | url('~@/assets/fonts/Barlow-Medium.ttf') format('truetype');
12 | }
13 | @font-face {
14 | font-family: 'Barlow';
15 | font-weight: 600;
16 | src: url('~@/assets/fonts/Barlow-SemiBold.woff2') format('woff2'),
17 | url('~@/assets/fonts/Barlow-SemiBold.ttf') format('truetype');
18 | }
19 | @font-face {
20 | font-family: 'Barlow';
21 | font-weight: bold;
22 | src: url('~@/assets/fonts/Barlow-Bold.woff2') format('woff2'),
23 | url('~@/assets/fonts/Barlow-Bold.ttf') format('truetype');
24 | }
25 | @font-face {
26 | font-family: 'Barlow';
27 | font-weight: 800;
28 | src: url('~@/assets/fonts/Barlow-ExtraBold.woff2') format('woff2'),
29 | url('~@/assets/fonts/Barlow-ExtraBold.ttf') format('truetype');
30 | }
31 | @font-face {
32 | font-family: 'Barlow';
33 | font-weight: 900;
34 | src: url('~@/assets/fonts/Barlow-Black.woff2') format('woff2'),
35 | url('~@/assets/fonts/Barlow-Black.ttf') format('truetype');
36 | }
37 |
38 | :root {
39 | --color-body-bg: #ffffff;
40 | --color-text: #000;
41 | --color-primary: #335eea;
42 | --color-primary-bg: #eaeffd;
43 | --color-secondary: #7a7a7b;
44 | --color-secondary-bg: #f5f5f7;
45 | --color-navbar-bg: rgba(255, 255, 255, 0.86);
46 | --color-primary-bg-for-transparent: rgba(189, 207, 255, 0.28);
47 | --color-secondary-bg-for-transparent: rgba(209, 209, 214, 0.28);
48 | --html-overflow-y: overlay;
49 | }
50 |
51 | [data-theme='dark'] {
52 | --color-body-bg: #222222;
53 | --color-text: #ffffff;
54 | --color-primary: #335eea;
55 | --color-primary-bg: #bbcdff;
56 | --color-secondary: #7a7a7b;
57 | --color-secondary-bg: #323232;
58 | --color-navbar-bg: rgba(34, 34, 34, 0.86);
59 | --color-primary-bg-for-transparent: rgba(255, 255, 255, 0.12);
60 | --color-secondary-bg-for-transparent: rgba(255, 255, 255, 0.08);
61 | }
62 |
63 | #app,
64 | input {
65 | font-family: 'Barlow', ui-sans-serif, system-ui, -apple-system,
66 | BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei,
67 | Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif,
68 | microsoft uighur;
69 | }
70 | body {
71 | background-color: var(--color-body-bg);
72 | }
73 |
74 | html {
75 | overflow-y: var(--html-overflow-y);
76 | min-width: 768px;
77 | overscroll-behavior: none;
78 | }
79 |
80 | select,
81 | button {
82 | font-family: inherit;
83 | }
84 | button {
85 | background: none;
86 | border: none;
87 | cursor: pointer;
88 | user-select: none;
89 | }
90 | input,
91 | button {
92 | &:focus {
93 | outline: none;
94 | }
95 | }
96 | a {
97 | color: inherit;
98 | text-decoration: none;
99 | cursor: pointer;
100 | &:hover {
101 | text-decoration: underline;
102 | }
103 | }
104 |
105 | [data-electron='yes'] {
106 | button,
107 | .navigation-links a,
108 | .playlist-info .description {
109 | cursor: default !important;
110 | }
111 | }
112 |
113 | ::-webkit-scrollbar {
114 | width: 8px;
115 | }
116 |
117 | ::-webkit-scrollbar-track {
118 | background: transparent;
119 | border-left: 1px solid rgba(128, 128, 128, 0.18);
120 | background: var(--color-body-bg);
121 | }
122 |
123 | ::-webkit-scrollbar-thumb {
124 | -webkit-border-radius: 10px;
125 | border-radius: 10px;
126 | background: rgba(128, 128, 128, 0.38);
127 | }
128 |
129 | [data-theme='dark'] ::-webkit-scrollbar-thumb {
130 | background: var(--color-secondary-bg);
131 | }
132 |
133 | .user-select-none {
134 | user-select: none;
135 | }
136 |
--------------------------------------------------------------------------------
/src/views/next.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('next.nowPlaying') }}
4 |
9 | 插队播放
11 |
12 |
13 |
22 | {{ $t('next.nextUp') }}
23 |
29 |
30 |
31 |
32 |
113 |
114 |
143 |
--------------------------------------------------------------------------------
/src/api/track.js:
--------------------------------------------------------------------------------
1 | import store from '@/store';
2 | import request from '@/utils/request';
3 | import { mapTrackPlayableStatus } from '@/utils/common';
4 | import {
5 | cacheTrackDetail,
6 | getTrackDetailFromCache,
7 | cacheLyric,
8 | getLyricFromCache,
9 | } from '@/utils/db';
10 |
11 | /**
12 | * 获取音乐 url
13 | * 说明 : 使用歌单详情接口后 , 能得到的音乐的 id, 但不能得到的音乐 url, 调用此接口, 传入的音乐 id( 可多个 , 用逗号隔开 ), 可以获取对应的音乐的 url,
14 | * !!!未登录状态返回试听片段(返回字段包含被截取的正常歌曲的开始时间和结束时间)
15 | * @param {string} id - 音乐的 id,例如 id=405998841,33894312
16 | */
17 | export function getMP3(id) {
18 | const getBr = () => {
19 | // 当返回的 quality >= 400000时,就会优先返回 hi-res
20 | const quality = store.state.settings?.musicQuality ?? '320000';
21 | return quality === 'flac' ? '350000' : quality;
22 | };
23 |
24 | return request({
25 | url: '/song/url',
26 | method: 'get',
27 | params: {
28 | id,
29 | br: getBr(),
30 | },
31 | });
32 | }
33 |
34 | /**
35 | * 获取歌曲详情
36 | * 说明 : 调用此接口 , 传入音乐 id(支持多个 id, 用 , 隔开), 可获得歌曲详情(注意:歌曲封面现在需要通过专辑内容接口获取)
37 | * @param {string} ids - 音乐 id, 例如 ids=405998841,33894312
38 | */
39 | export function getTrackDetail(ids) {
40 | const fetchLatest = () => {
41 | return request({
42 | url: '/song/detail',
43 | method: 'get',
44 | params: {
45 | ids,
46 | },
47 | }).then(data => {
48 | data.songs.map(song => {
49 | const privileges = data.privileges.find(t => t.id === song.id);
50 | cacheTrackDetail(song, privileges);
51 | });
52 | data.songs = mapTrackPlayableStatus(data.songs, data.privileges);
53 | return data;
54 | });
55 | };
56 | fetchLatest();
57 |
58 | let idsInArray = [String(ids)];
59 | if (typeof ids === 'string') {
60 | idsInArray = ids.split(',');
61 | }
62 |
63 | return getTrackDetailFromCache(idsInArray).then(result => {
64 | if (result) {
65 | result.songs = mapTrackPlayableStatus(result.songs, result.privileges);
66 | }
67 | return result ?? fetchLatest();
68 | });
69 | }
70 |
71 | /**
72 | * 获取歌词
73 | * 说明 : 调用此接口 , 传入音乐 id 可获得对应音乐的歌词 ( 不需要登录 )
74 | * @param {number} id - 音乐 id
75 | */
76 | export function getLyric(id) {
77 | const fetchLatest = () => {
78 | return request({
79 | url: '/lyric',
80 | method: 'get',
81 | params: {
82 | id,
83 | },
84 | }).then(result => {
85 | cacheLyric(id, result);
86 | return result;
87 | });
88 | };
89 |
90 | fetchLatest();
91 |
92 | return getLyricFromCache(id).then(result => {
93 | return result ?? fetchLatest();
94 | });
95 | }
96 |
97 | /**
98 | * 新歌速递
99 | * 说明 : 调用此接口 , 可获取新歌速递
100 | * @param {number} type - 地区类型 id, 对应以下: 全部:0 华语:7 欧美:96 日本:8 韩国:16
101 | */
102 | export function topSong(type) {
103 | return request({
104 | url: '/top/song',
105 | method: 'get',
106 | params: {
107 | type,
108 | },
109 | });
110 | }
111 |
112 | /**
113 | * 喜欢音乐
114 | * 说明 : 调用此接口 , 传入音乐 id, 可喜欢该音乐
115 | * - id - 歌曲 id
116 | * - like - 默认为 true 即喜欢 , 若传 false, 则取消喜欢
117 | * @param {Object} params
118 | * @param {number} params.id
119 | * @param {boolean=} [params.like]
120 | */
121 | export function likeATrack(params) {
122 | params.timestamp = new Date().getTime();
123 | return request({
124 | url: '/like',
125 | method: 'get',
126 | params,
127 | });
128 | }
129 |
130 | /**
131 | * 听歌打卡
132 | * 说明 : 调用此接口 , 传入音乐 id, 来源 id,歌曲时间 time,更新听歌排行数据
133 | * - id - 歌曲 id
134 | * - sourceid - 歌单或专辑 id
135 | * - time - 歌曲播放时间,单位为秒
136 | * @param {Object} params
137 | * @param {number} params.id
138 | * @param {number} params.sourceid
139 | * @param {number=} params.time
140 | */
141 | export function scrobble(params) {
142 | params.timestamp = new Date().getTime();
143 | return request({
144 | url: '/scrobble',
145 | method: 'get',
146 | params,
147 | });
148 | }
149 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import { isLooseLoggedIn, isAccountLoggedIn } from '@/utils/auth';
4 |
5 | Vue.use(VueRouter);
6 | const routes = [
7 | {
8 | path: '/',
9 | name: 'home',
10 | component: () => import('@/views/home.vue'),
11 | meta: {
12 | keepAlive: true,
13 | savePosition: true,
14 | },
15 | },
16 | {
17 | path: '/login',
18 | name: 'login',
19 | component: () => import('@/views/login.vue'),
20 | },
21 | {
22 | path: '/login/username',
23 | name: 'loginUsername',
24 | component: () => import('@/views/loginUsername.vue'),
25 | },
26 | {
27 | path: '/login/account',
28 | name: 'loginAccount',
29 | component: () => import('@/views/loginAccount.vue'),
30 | },
31 | {
32 | path: '/playlist/:id',
33 | name: 'playlist',
34 | component: () => import('@/views/playlist.vue'),
35 | },
36 | {
37 | path: '/album/:id',
38 | name: 'album',
39 | component: () => import('@/views/album.vue'),
40 | },
41 | {
42 | path: '/artist/:id',
43 | name: 'artist',
44 | component: () => import('@/views/artist.vue'),
45 | meta: {
46 | keepAlive: true,
47 | savePosition: true,
48 | },
49 | },
50 | {
51 | path: '/artist/:id/mv',
52 | name: 'artistMV',
53 | component: () => import('@/views/artistMV.vue'),
54 | meta: {
55 | keepAlive: true,
56 | },
57 | },
58 | {
59 | path: '/mv/:id',
60 | name: 'mv',
61 | component: () => import('@/views/mv.vue'),
62 | },
63 | {
64 | path: '/next',
65 | name: 'next',
66 | component: () => import('@/views/next.vue'),
67 | meta: {
68 | keepAlive: true,
69 | savePosition: true,
70 | },
71 | },
72 | {
73 | path: '/search/:keywords?',
74 | name: 'search',
75 | component: () => import('@/views/search.vue'),
76 | meta: {
77 | keepAlive: true,
78 | },
79 | },
80 | {
81 | path: '/search/:keywords/:type',
82 | name: 'searchType',
83 | component: () => import('@/views/searchType.vue'),
84 | },
85 | {
86 | path: '/new-album',
87 | name: 'newAlbum',
88 | component: () => import('@/views/newAlbum.vue'),
89 | },
90 | {
91 | path: '/explore',
92 | name: 'explore',
93 | component: () => import('@/views/explore.vue'),
94 | meta: {
95 | keepAlive: true,
96 | savePosition: true,
97 | },
98 | },
99 | {
100 | path: '/library',
101 | name: 'library',
102 | component: () => import('@/views/library.vue'),
103 | meta: {
104 | requireLogin: true,
105 | keepAlive: true,
106 | savePosition: true,
107 | },
108 | },
109 | {
110 | path: '/library/liked-songs',
111 | name: 'likedSongs',
112 | component: () => import('@/views/playlist.vue'),
113 | meta: {
114 | requireLogin: true,
115 | },
116 | },
117 | {
118 | path: '/settings',
119 | name: 'settings',
120 | component: () => import('@/views/settings.vue'),
121 | },
122 | {
123 | path: '/daily/songs',
124 | name: 'dailySongs',
125 | component: () => import('@/views/dailyTracks.vue'),
126 | meta: {
127 | requireAccountLogin: true,
128 | },
129 | },
130 | {
131 | path: '/lastfm/callback',
132 | name: 'lastfmCallback',
133 | component: () => import('@/views/lastfmCallback.vue'),
134 | },
135 | ];
136 |
137 | const router = new VueRouter({
138 | mode: 'hash',
139 | routes,
140 | });
141 |
142 | const originalPush = VueRouter.prototype.push;
143 | VueRouter.prototype.push = function push(location) {
144 | return originalPush.call(this, location).catch(err => err);
145 | };
146 |
147 | router.beforeEach((to, from, next) => {
148 | // 需要登录的逻辑
149 | if (to.meta.requireAccountLogin) {
150 | if (isAccountLoggedIn()) {
151 | next();
152 | } else {
153 | next({ path: '/login/account' });
154 | }
155 | }
156 | if (to.meta.requireLogin) {
157 | if (isLooseLoggedIn()) {
158 | next();
159 | } else {
160 | if (process.env.IS_ELECTRON === true) {
161 | next({ path: '/login/account' });
162 | } else {
163 | next({ path: '/login' });
164 | }
165 | }
166 | } else {
167 | next();
168 | }
169 | });
170 |
171 | export default router;
172 |
--------------------------------------------------------------------------------
/src/components/ModalNewPlaylist.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
113 |
114 |
155 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yesplaymusic",
3 | "version": "0.4.5",
4 | "private": true,
5 | "description": "A third party music player for Netease Music",
6 | "author": "qier222",
7 | "scripts": {
8 | "serve": "vue-cli-service serve",
9 | "build": "vue-cli-service build",
10 | "lint": "vue-cli-service lint",
11 | "electron:build": "vue-cli-service electron:build -p never",
12 | "electron:build-all": "vue-cli-service electron:build -p never -mwl",
13 | "electron:build-mac": "vue-cli-service electron:build -p never -m",
14 | "electron:build-win": "vue-cli-service electron:build -p never -w",
15 | "electron:build-linux": "vue-cli-service electron:build -p never -l",
16 | "electron:serve": "vue-cli-service electron:serve",
17 | "electron:buildicon": "electron-icon-builder --input=./build/icons/icon.png --output=build --flatten",
18 | "electron:publish": "vue-cli-service electron:build -mwl -p always",
19 | "postinstall": "electron-builder install-app-deps",
20 | "postuninstall": "electron-builder install-app-deps",
21 | "prettier": "npx prettier --write ./src",
22 | "netease_api:run": "npx NeteaseCloudMusicApi"
23 | },
24 | "main": "background.js",
25 | "dependencies": {
26 | "@unblockneteasemusic/rust-napi": "^0.3.0-pre.1",
27 | "NeteaseCloudMusicApi": "^4.5.2",
28 | "axios": "^0.26.1",
29 | "change-case": "^4.1.2",
30 | "cli-color": "^2.0.0",
31 | "color": "^4.2.3",
32 | "core-js": "^3.6.5",
33 | "crypto-js": "^4.0.0",
34 | "dayjs": "^1.8.36",
35 | "dexie": "^3.0.3",
36 | "discord-rich-presence": "^0.0.8",
37 | "electron": "^13.6.7",
38 | "electron-builder": "^23.0.0",
39 | "electron-context-menu": "^3.1.2",
40 | "electron-debug": "^3.1.0",
41 | "electron-devtools-installer": "^3.2",
42 | "electron-icon-builder": "^2.0.1",
43 | "electron-is-dev": "^2.0.0",
44 | "electron-log": "^4.3.0",
45 | "electron-store": "^8.0.1",
46 | "electron-updater": "^5.0.1",
47 | "express": "^4.17.1",
48 | "express-fileupload": "^1.2.0",
49 | "express-http-proxy": "^1.6.2",
50 | "extract-zip": "^2.0.1",
51 | "howler": "^2.2.3",
52 | "js-cookie": "^2.2.1",
53 | "jsbi": "^4.1.0",
54 | "lodash": "^4.17.20",
55 | "md5": "^2.3.0",
56 | "mpris-service": "^2.1.2",
57 | "music-metadata": "^7.5.3",
58 | "node-vibrant": "^3.2.1-alpha.1",
59 | "nprogress": "^0.2.0",
60 | "pac-proxy-agent": "^4.1.0",
61 | "plyr": "^3.6.2",
62 | "prettier": "2.5.1",
63 | "qrcode": "^1.4.4",
64 | "register-service-worker": "^1.7.1",
65 | "svg-sprite-loader": "^6.0.11",
66 | "tunnel": "^0.0.6",
67 | "vscode-codicons": "^0.0.17",
68 | "vue": "^2.6.11",
69 | "vue-clipboard2": "^0.3.1",
70 | "vue-gtag": "1",
71 | "vue-i18n": "^8.22.0",
72 | "vue-router": "^3.4.3",
73 | "vue-slider-component": "^3.2.5",
74 | "vue-tv-focusable": "^1.1.0",
75 | "vuex": "^3.4.0",
76 | "x11": "^2.3.0"
77 | },
78 | "devDependencies": {
79 | "@types/node": "^17.0.0",
80 | "@vue/cli-plugin-babel": "~4.5.0",
81 | "@vue/cli-plugin-eslint": "~4.5.0",
82 | "@vue/cli-plugin-pwa": "~4.5.0",
83 | "@vue/cli-plugin-vuex": "~4.5.0",
84 | "@vue/cli-service": "~4.5.0",
85 | "babel-eslint": "^10.1.0",
86 | "eslint": "^6.7.2",
87 | "eslint-config-prettier": "^8.1.0",
88 | "eslint-plugin-prettier": "^3.3.1",
89 | "eslint-plugin-vue": "^7.9.0",
90 | "husky": "^4.3.0",
91 | "sass": "^1.26.11",
92 | "sass-loader": "^10.0.2",
93 | "vue-cli-plugin-electron-builder": "~2.1.1",
94 | "vue-template-compiler": "^2.6.11"
95 | },
96 | "resolutions": {
97 | "icon-gen": "3.0.0",
98 | "degenerator": "2.2.0",
99 | "electron-builder": "^23.0.0"
100 | },
101 | "eslintConfig": {
102 | "root": true,
103 | "env": {
104 | "node": true,
105 | "browser": true
106 | },
107 | "extends": [
108 | "plugin:vue/essential",
109 | "plugin:vue/recommended",
110 | "plugin:prettier/recommended",
111 | "eslint:recommended"
112 | ],
113 | "parserOptions": {
114 | "parser": "babel-eslint"
115 | },
116 | "globals": {
117 | "ipcRenderer": "off"
118 | },
119 | "rules": {}
120 | },
121 | "browserslist": [
122 | "> 1%",
123 | "last 2 versions",
124 | "not dead"
125 | ],
126 | "husky": {
127 | "hooks": {
128 | "pre-commit": "npm run prettier"
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/MvRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
![]()
11 |
12 |
17 |
18 |
19 |
20 |
21 | {{ getTitle(mv) }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
84 |
85 |
178 |
--------------------------------------------------------------------------------
/src/views/searchType.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('search.searchFor') }} {{ typeNameTable[type] }} "{{
5 | keywords
6 | }}"
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{
36 | $t('explore.loadMore')
37 | }}
38 |
39 |
40 |
41 |
42 |
139 |
140 |
162 |
--------------------------------------------------------------------------------
/src/components/Modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
64 |
65 |
194 |
--------------------------------------------------------------------------------
/src/components/ModalAddTrackToPlaylist.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | 新建歌单
14 |
20 |
![]()
21 |
22 |
{{ playlist.name }}
23 |
{{ playlist.trackCount }} 首
24 |
25 |
26 |
27 |
28 |
29 |
30 |
109 |
110 |
175 |
--------------------------------------------------------------------------------
/src/components/DailyTracksCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 | 每
8 | 日
9 | 推
10 | 荐
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
79 |
80 |
189 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
132 |
133 |
172 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request';
2 |
3 | /**
4 | * 获取用户详情
5 | * 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户详情
6 | * - uid : 用户 id
7 | * @param {number} uid
8 | */
9 | export function userDetail(uid) {
10 | return request({
11 | url: '/user/detail',
12 | method: 'get',
13 | params: {
14 | uid,
15 | timestamp: new Date().getTime(),
16 | },
17 | });
18 | }
19 |
20 | /**
21 | * 获取账号详情
22 | * 说明 : 登录后调用此接口 ,可获取用户账号信息
23 | */
24 | export function userAccount() {
25 | return request({
26 | url: '/user/account',
27 | method: 'get',
28 | params: {
29 | timestamp: new Date().getTime(),
30 | },
31 | });
32 | }
33 |
34 | /**
35 | * 获取用户歌单
36 | * 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户歌单
37 | * - uid : 用户 id
38 | * - limit : 返回数量 , 默认为 30
39 | * - offset : 偏移数量,用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
40 | * @param {Object} params
41 | * @param {number} params.uid
42 | * @param {number} params.limit
43 | * @param {number=} params.offset
44 | */
45 | export function userPlaylist(params) {
46 | return request({
47 | url: '/user/playlist',
48 | method: 'get',
49 | params,
50 | });
51 | }
52 |
53 | /**
54 | * 获取用户播放记录
55 | * 说明 : 登录后调用此接口 , 传入用户 id, 可获取用户播放记录
56 | * - uid : 用户 id
57 | * - type : type=1 时只返回 weekData, type=0 时返回 allData
58 | * @param {Object} params
59 | * @param {number} params.uid
60 | * @param {number} params.type
61 | */
62 | export function userPlayHistory(params) {
63 | return request({
64 | url: '/user/record',
65 | method: 'get',
66 | params,
67 | });
68 | }
69 |
70 | /**
71 | * 喜欢音乐列表(需要登录)
72 | * 说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐id列表(id数组)
73 | * - uid: 用户 id
74 | * @param {number} uid
75 | */
76 | export function userLikedSongsIDs(uid) {
77 | return request({
78 | url: '/likelist',
79 | method: 'get',
80 | params: {
81 | uid,
82 | timestamp: new Date().getTime(),
83 | },
84 | });
85 | }
86 |
87 | /**
88 | * 每日签到
89 | * 说明 : 调用此接口可签到获取积分
90 | * - type: 签到类型 , 默认 0, 其中 0 为安卓端签到 ,1 为 web/PC 签到
91 | * @param {number} type
92 | */
93 | export function dailySignin(type = 0) {
94 | return request({
95 | url: '/daily_signin',
96 | method: 'post',
97 | params: {
98 | type,
99 | timestamp: new Date().getTime(),
100 | },
101 | });
102 | }
103 |
104 | /**
105 | * 获取收藏的专辑(需要登录)
106 | * 说明 : 调用此接口可获取到用户收藏的专辑
107 | * - limit : 返回数量 , 默认为 25
108 | * - offset : 偏移数量,用于分页 , 如 :( 页数 -1)*25, 其中 25 为 limit 的值 , 默认为 0
109 | * @param {Object} params
110 | * @param {number} params.limit
111 | * @param {number=} params.offset
112 | */
113 | export function likedAlbums(params) {
114 | return request({
115 | url: '/album/sublist',
116 | method: 'get',
117 | params: {
118 | limit: params.limit,
119 | timestamp: new Date().getTime(),
120 | },
121 | });
122 | }
123 |
124 | /**
125 | * 获取收藏的歌手(需要登录)
126 | * 说明 : 调用此接口可获取到用户收藏的歌手
127 | */
128 | export function likedArtists(params) {
129 | return request({
130 | url: '/artist/sublist',
131 | method: 'get',
132 | params: {
133 | limit: params.limit,
134 | timestamp: new Date().getTime(),
135 | },
136 | });
137 | }
138 |
139 | /**
140 | * 获取收藏的MV(需要登录)
141 | * 说明 : 调用此接口可获取到用户收藏的MV
142 | */
143 | export function likedMVs(params) {
144 | return request({
145 | url: '/mv/sublist',
146 | method: 'get',
147 | params: {
148 | limit: params.limit,
149 | timestamp: new Date().getTime(),
150 | },
151 | });
152 | }
153 |
154 | /**
155 | * 上传歌曲到云盘(需要登录)
156 | */
157 | export function uploadSong(file) {
158 | let formData = new FormData();
159 | formData.append('songFile', file);
160 | return request({
161 | url: '/cloud',
162 | method: 'post',
163 | params: {
164 | timestamp: new Date().getTime(),
165 | },
166 | data: formData,
167 | headers: {
168 | 'Content-Type': 'multipart/form-data',
169 | },
170 | timeout: 200000,
171 | }).catch(error => {
172 | alert(`上传失败,Error: ${error}`);
173 | });
174 | }
175 |
176 | /**
177 | * 获取云盘歌曲(需要登录)
178 | * 说明 : 登录后调用此接口 , 可获取云盘数据 , 获取的数据没有对应 url, 需要再调用一 次 /song/url 获取 url
179 | * - limit : 返回数量 , 默认为 200
180 | * - offset : 偏移数量,用于分页 , 如 :( 页数 -1)*200, 其中 200 为 limit 的值 , 默认为 0
181 | * @param {Object} params
182 | * @param {number} params.limit
183 | * @param {number=} params.offset
184 | */
185 | export function cloudDisk(params = {}) {
186 | params.timestamp = new Date().getTime();
187 | return request({
188 | url: '/user/cloud',
189 | method: 'get',
190 | params,
191 | });
192 | }
193 |
194 | /**
195 | * 获取云盘歌曲详情(需要登录)
196 | */
197 | export function cloudDiskTrackDetail(id) {
198 | return request({
199 | url: '/user/cloud/detail',
200 | method: 'get',
201 | params: {
202 | timestamp: new Date().getTime(),
203 | id,
204 | },
205 | });
206 | }
207 |
208 | /**
209 | * 删除云盘歌曲(需要登录)
210 | * @param {Array} id
211 | */
212 | export function cloudDiskTrackDelete(id) {
213 | return request({
214 | url: '/user/cloud/del',
215 | method: 'get',
216 | params: {
217 | timestamp: new Date().getTime(),
218 | id,
219 | },
220 | });
221 | }
222 |
--------------------------------------------------------------------------------
/src/views/loginUsername.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ $t('login.usernameLogin') }}
5 |
19 |
20 |
21 | {{ $t('login.enterTip') }}
22 |
23 |
24 | {{ $t('login.choose') }}
25 |
26 |
27 |
34 |
![]()
39 |
40 | {{ user.nickname }}
41 |
42 |
43 |
44 |
45 |
49 | {{ $t('login.confirm') }}
50 |
51 |
52 |
53 |
54 |
55 |
108 |
109 |
206 |
--------------------------------------------------------------------------------
/src/utils/db.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import Dexie from 'dexie';
3 | import store from '@/store';
4 | // import pkg from "../../package.json";
5 |
6 | const db = new Dexie('yesplaymusic');
7 |
8 | db.version(4).stores({
9 | trackDetail: '&id, updateTime',
10 | lyric: '&id, updateTime',
11 | album: '&id, updateTime',
12 | });
13 |
14 | db.version(3)
15 | .stores({
16 | trackSources: '&id, createTime',
17 | })
18 | .upgrade(tx =>
19 | tx
20 | .table('trackSources')
21 | .toCollection()
22 | .modify(
23 | track => !track.createTime && (track.createTime = new Date().getTime())
24 | )
25 | );
26 |
27 | db.version(1).stores({
28 | trackSources: '&id',
29 | });
30 |
31 | let tracksCacheBytes = 0;
32 |
33 | async function deleteExcessCache() {
34 | if (
35 | store.state.settings.cacheLimit === false ||
36 | tracksCacheBytes < store.state.settings.cacheLimit * Math.pow(1024, 2)
37 | ) {
38 | return;
39 | }
40 | try {
41 | const delCache = await db.trackSources.orderBy('createTime').first();
42 | await db.trackSources.delete(delCache.id);
43 | tracksCacheBytes -= delCache.source.byteLength;
44 | console.debug(
45 | `[debug][db.js] deleteExcessCacheSucces, track: ${delCache.name}, size: ${delCache.source.byteLength}, cacheSize:${tracksCacheBytes}`
46 | );
47 | deleteExcessCache();
48 | } catch (error) {
49 | console.debug('[debug][db.js] deleteExcessCacheFailed', error);
50 | }
51 | }
52 |
53 | export function cacheTrackSource(trackInfo, url, bitRate, from = 'netease') {
54 | if (!process.env.IS_ELECTRON) return;
55 | const name = trackInfo.name;
56 | const artist =
57 | (trackInfo.ar && trackInfo.ar[0]?.name) ||
58 | (trackInfo.artists && trackInfo.artists[0]?.name) ||
59 | 'Unknown';
60 | let cover = trackInfo.al.picUrl;
61 | if (cover.slice(0, 5) !== 'https') {
62 | cover = 'https' + cover.slice(4);
63 | }
64 | axios.get(`${cover}?param=512y512`);
65 | axios.get(`${cover}?param=224y224`);
66 | axios.get(`${cover}?param=1024y1024`);
67 | return axios
68 | .get(url, {
69 | responseType: 'arraybuffer',
70 | })
71 | .then(response => {
72 | db.trackSources.put({
73 | id: trackInfo.id,
74 | source: response.data,
75 | bitRate,
76 | from,
77 | name,
78 | artist,
79 | createTime: new Date().getTime(),
80 | });
81 | console.debug(`[debug][db.js] cached track 👉 ${name} by ${artist}`);
82 | tracksCacheBytes += response.data.byteLength;
83 | deleteExcessCache();
84 | return { trackID: trackInfo.id, source: response.data, bitRate };
85 | });
86 | }
87 |
88 | export function getTrackSource(id) {
89 | return db.trackSources.get(Number(id)).then(track => {
90 | if (!track) return null;
91 | console.debug(
92 | `[debug][db.js] get track from cache 👉 ${track.name} by ${track.artist}`
93 | );
94 | return track;
95 | });
96 | }
97 |
98 | export function cacheTrackDetail(track, privileges) {
99 | db.trackDetail.put({
100 | id: track.id,
101 | detail: track,
102 | privileges: privileges,
103 | updateTime: new Date().getTime(),
104 | });
105 | }
106 |
107 | export function getTrackDetailFromCache(ids) {
108 | return db.trackDetail
109 | .filter(track => {
110 | return ids.includes(String(track.id));
111 | })
112 | .toArray()
113 | .then(tracks => {
114 | const result = { songs: [], privileges: [] };
115 | ids.map(id => {
116 | const one = tracks.find(t => String(t.id) === id);
117 | result.songs.push(one?.detail);
118 | result.privileges.push(one?.privileges);
119 | });
120 | if (result.songs.includes(undefined)) {
121 | return undefined;
122 | }
123 | return result;
124 | });
125 | }
126 |
127 | export function cacheLyric(id, lyrics) {
128 | db.lyric.put({
129 | id,
130 | lyrics,
131 | updateTime: new Date().getTime(),
132 | });
133 | }
134 |
135 | export function getLyricFromCache(id) {
136 | return db.lyric.get(Number(id)).then(result => {
137 | if (!result) return undefined;
138 | return result.lyrics;
139 | });
140 | }
141 |
142 | export function cacheAlbum(id, album) {
143 | db.album.put({
144 | id: Number(id),
145 | album,
146 | updateTime: new Date().getTime(),
147 | });
148 | }
149 |
150 | export function getAlbumFromCache(id) {
151 | return db.album.get(Number(id)).then(result => {
152 | if (!result) return undefined;
153 | return result.album;
154 | });
155 | }
156 |
157 | export function countDBSize() {
158 | const trackSizes = [];
159 | return db.trackSources
160 | .each(track => {
161 | trackSizes.push(track.source.byteLength);
162 | })
163 | .then(() => {
164 | const res = {
165 | bytes: trackSizes.reduce((s1, s2) => s1 + s2, 0),
166 | length: trackSizes.length,
167 | };
168 | tracksCacheBytes = res.bytes;
169 | console.debug(
170 | `[debug][db.js] load tracksCacheBytes: ${tracksCacheBytes}`
171 | );
172 | return res;
173 | });
174 | }
175 |
176 | export function clearDB() {
177 | return new Promise(resolve => {
178 | db.tables.forEach(function (table) {
179 | table.clear();
180 | });
181 | resolve();
182 | });
183 | }
184 |
--------------------------------------------------------------------------------
/src/components/Cover.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
19 |
20 |
![]()
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
101 |
102 |
207 |
--------------------------------------------------------------------------------
/src/components/Scrollbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
152 |
153 |
199 |
--------------------------------------------------------------------------------