Compare commits

..

No commits in common. "master" and "v0.4.4-beta.1" have entirely different histories.

80 changed files with 5567 additions and 8826 deletions

4
.envrc
View file

@ -1,4 +0,0 @@
source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k="
export NIXPKGS_ALLOW_INSECURE=1
use devenv

View file

@ -1,15 +1,12 @@
name: Release name: Release
env:
YARN_INSTALL_NOPT: yarn add --ignore-platform --ignore-optional
on: on:
push: push:
branches: branches:
- master - master
- "ci/*"
tags: tags:
- v* - v*
workflow_dispatch:
jobs: jobs:
release: release:
@ -17,26 +14,25 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [macos-latest, windows-latest, ubuntu-22.04] os: [macos-latest, windows-latest, ubuntu-18.04]
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
submodules: "recursive" submodules: "recursive"
- name: Install Node.js, NPM and Yarn - name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: 16 node-version: 14.16.0
cache: 'yarn'
- name: Install RPM & Pacman (on Ubuntu) - name: Install RPM & Pacman (on Ubuntu)
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
sudo apt-get update && sudo apt-get update &&
sudo apt-get install --no-install-recommends -y rpm && sudo apt-get install --no-install-recommends -y rpm &&
sudo apt-get install --no-install-recommends -y libarchive-tools && sudo apt-get install --no-install-recommends -y bsdtar &&
sudo apt-get install --no-install-recommends -y libopenjp2-tools sudo apt-get install --no-install-recommends -y libopenjp2-tools
- name: Install Snapcraft (on Ubuntu) - name: Install Snapcraft (on Ubuntu)
@ -45,43 +41,9 @@ jobs:
with: with:
snapcraft_token: ${{ secrets.snapcraft_token }} snapcraft_token: ${{ secrets.snapcraft_token }}
- id: get_unm_version
name: Get the installed UNM version
run: |
yarn --ignore-optional
unm_version=$(node -e "console.log(require('./node_modules/@unblockneteasemusic/rust-napi/package.json').version)")
echo "::set-output name=unmver::${unm_version}"
shell: bash
- name: Install UNM dependencies for Windows
if: runner.os == 'Windows'
run: |
${{ env.YARN_INSTALL_NOPT }} \
@unblockneteasemusic/rust-napi-win32-x64-msvc@${{steps.get_unm_version.outputs.unmver}}
shell: bash
- name: Install UNM dependencies for macOS
if: runner.os == 'macOS'
run: |
${{ env.YARN_INSTALL_NOPT }} \
@unblockneteasemusic/rust-napi-darwin-x64@${{steps.get_unm_version.outputs.unmver}} \
@unblockneteasemusic/rust-napi-darwin-arm64@${{steps.get_unm_version.outputs.unmver}} \
dmg-license
shell: bash
- name: Install UNM dependencies for Linux
if: runner.os == 'Linux'
run: |
${{ env.YARN_INSTALL_NOPT }} \
@unblockneteasemusic/rust-napi-linux-x64-gnu@${{steps.get_unm_version.outputs.unmver}} \
@unblockneteasemusic/rust-napi-linux-arm64-gnu@${{steps.get_unm_version.outputs.unmver}} \
@unblockneteasemusic/rust-napi-linux-arm-gnueabihf@${{steps.get_unm_version.outputs.unmver}}
shell: bash
- name: Build/release Electron app - name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1.6.0 uses: samuelmeuli/action-electron-builder@v1.6.0
env: env:
VUE_APP_NETEASE_API_URL: /api
VUE_APP_ELECTRON_API_URL: /api VUE_APP_ELECTRON_API_URL: /api
VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754 VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754
VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c
@ -97,19 +59,19 @@ jobs:
use_vue_cli: true use_vue_cli: true
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v2
with: with:
name: YesPlayMusic-mac name: YesPlayMusic-mac
path: dist_electron/*-universal.dmg path: dist_electron/*-universal.dmg
if-no-files-found: ignore if-no-files-found: ignore
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v2
with: with:
name: YesPlayMusic-win name: YesPlayMusic-win
path: dist_electron/*Setup*.exe path: dist_electron/*Setup*.exe
if-no-files-found: ignore if-no-files-found: ignore
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v2
with: with:
name: YesPlayMusic-linux name: YesPlayMusic-linux
path: dist_electron/*.AppImage path: dist_electron/*.AppImage

View file

@ -1,48 +0,0 @@
name: Upstream Sync
permissions:
contents: write
issues: write
actions: write
on:
schedule:
- cron: '0 * * * *' # every hour
workflow_dispatch:
jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
steps:
- uses: actions/checkout@v4
- name: Clean issue notice
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
labels: '🚨 Sync Fail'
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: qier222/YesPlayMusic
upstream_sync_branch: master
target_sync_branch: master
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
test_mode: false
- name: Sync check
if: failure()
uses: actions-cool/issues-helper@v3
with:
actions: 'create-issue'
title: '🚨 同步失败 | Sync Fail'
labels: '🚨 Sync Fail'
body: |
Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork.
由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次。

9
.gitignore vendored
View file

@ -32,12 +32,3 @@ NeteaseCloudMusicApi-master.zip
# Local Netlify folder # Local Netlify folder
.netlify .netlify
vercel.json vercel.json
# Devenv
.devenv*
devenv.local.nix
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml

1
.nvmrc
View file

@ -1 +0,0 @@
14

View file

@ -5,6 +5,7 @@
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"jsxSingleQuote": true, "jsxSingleQuote": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid", "arrowParens": "avoid",
"endOfLine": "lf", "endOfLine": "lf",
"bracketSpacing": true, "bracketSpacing": true,

View file

@ -1,25 +1,43 @@
FROM node:16.13.1-alpine as build FROM node:16.13.1-alpine as build
ENV VUE_APP_NETEASE_API_URL=/api ENV VUE_APP_NETEASE_API_URL=/api
WORKDIR /app WORKDIR /app
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\ RUN apk add --no-cache python3 make g++ git
apk add --no-cache python3 make g++ git
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN yarn install RUN yarn install
COPY . . COPY . .
RUN yarn config set electron_mirror https://npmmirror.com/mirrors/electron/ && \ RUN yarn build
yarn build
FROM nginx:1.20.2-alpine as app FROM nginx:1.20.2-alpine as app
RUN echo $'server { \n\
gzip on;\n\
listen 80; \n\
listen [::]:80; \n\
server_name localhost; \n\
\n\
location / { \n\
root /usr/share/nginx/html; \n\
index index.html; \n\
try_files $uri $uri/ /index.html; \n\
} \n\
\n\
location @rewrites { \n\
rewrite ^(.*)$ /index.html last; \n\
} \n\
\n\
location /api/ { \n\
proxy_set_header Host $host; \n\
proxy_set_header X-Real-IP $remote_addr; \n\
proxy_set_header X-Forwarded-For $remote_addr; \n\
proxy_set_header X-Forwarded-Host $remote_addr; \n\
proxy_set_header X-NginX-Proxy true; \n\
proxy_pass http://localhost:3000/; \n\
} \n\
}' > /etc/nginx/conf.d/default.conf
COPY --from=build /app/package.json /usr/local/lib/ RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv \
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\
apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv \
&& apk add --no-cache --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main nodejs npm \ && apk add --no-cache --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main nodejs npm \
&& npm i -g $(awk -F \" '{if($2=="NeteaseCloudMusicApi") print $2"@"$4}' /usr/local/lib/package.json) \ && npm i -g NeteaseCloudMusicApi
&& rm -f /usr/local/lib/package.json
COPY --from=build /app/docker/nginx.conf.example /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
CMD nginx ; exec npx NeteaseCloudMusicApi CMD nginx ; exec npx NeteaseCloudMusicApi

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020-2023 qier222 Copyright (c) 2020 qier222
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View file

@ -8,7 +8,7 @@
<p align="center"> <p align="center">
高颜值的第三方网易云播放器 高颜值的第三方网易云播放器
<br /> <br />
<a href="https://music.ineko.cc" target="blank"><strong>🌎 访问DEMO</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp; <a href="https://music.qier222.com" target="blank"><strong>🌎 访问DEMO</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#%EF%B8%8F-安装" target="blank"><strong>📦️ 下载安装包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp; <a href="#%EF%B8%8F-安装" target="blank"><strong>📦️ 下载安装包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://t.me/yesplaymusic" target="blank"><strong>💬 加入交流群</strong></a> <a href="https://t.me/yesplaymusic" target="blank"><strong>💬 加入交流群</strong></a>
<br /> <br />
@ -16,12 +16,7 @@
</p> </p>
</p> </p>
[![Library][library-screenshot]](https://music.ineko.cc) [![Library][library-screenshot]](https://music.qier222.com)
## 全新版本
全新2.0 Alpha测试版已发布欢迎前往 [Releases](https://github.com/qier222/YesPlayMusic/releases) 页面下载。
当前版本将会进入维护模式除重大bug修复外不会再更新新功能。
## ✨ 特性 ## ✨ 特性
@ -42,7 +37,7 @@
- 🟥 支持 Last.fm Scrobble - 🟥 支持 Last.fm Scrobble
- ☁️ 支持音乐云盘 - ☁️ 支持音乐云盘
- ⌨️ 自定义快捷键和全局快捷键 - ⌨️ 自定义快捷键和全局快捷键
- 🎧 支持 Mpris - 🎧 支持Mpris
- 🛠 更多特性开发中 - 🛠 更多特性开发中
## 📦️ 安装 ## 📦️ 安装
@ -52,18 +47,12 @@ Electron 版本由 [@hawtim](https://github.com/hawtim) 和 [@qier222](https://g
访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases) 访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases)
页面下载安装包。 页面下载安装包。
- macOS 用户可以通过 Homebrew 来安装:`brew install --cask yesplaymusic` macOS 用户也可以通过 `brew install --cask yesplaymusic` 来安装。
- Windows 用户可以通过 Scoop 来安装:`scoop install extras/yesplaymusic`
## ⚙️ 部署至 Vercel ## ⚙️ 部署至 Vercel
除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。 除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。
本项目的 Demo (https://music.qier222.com) 就是部署在 Vercel 上的网站。
[![Powered by Vercel](https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg)](https://vercel.com/?utm_source=ohmusic&utm_campaign=oss)
1. 部署网易云 API详情参见 [Binaryify/NeteaseCloudMusicApi](https://neteasecloudmusicapi.vercel.app/#/?id=%e5%ae%89%e8%a3%85) 1. 部署网易云 API详情参见 [Binaryify/NeteaseCloudMusicApi](https://neteasecloudmusicapi.vercel.app/#/?id=%e5%ae%89%e8%a3%85)
。你也可以将 API 部署到 Vercel。 。你也可以将 API 部署到 Vercel。
@ -125,16 +114,6 @@ yarn run build
7. 将 `/dist` 目录下的文件上传到你的 Web 服务器 7. 将 `/dist` 目录下的文件上传到你的 Web 服务器
## ⚙️ 宝塔面板 docker应用商店 部署
1. 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/new/download.html) ,选择正式版的脚本下载安装。
2. 安装后登录宝塔面板,在左侧导航栏中点击 Docker首次进入会提示安装Docker服务点击立即安装按提示完成安装
3. 安装完成后在应用商店中找到YesPlayMusic点击安装配置域名、端口等基本信息即可完成安装。
4. 安装后在浏览器输入上一步骤设置的域名即可访问。
## ⚙️ Docker 部署 ## ⚙️ Docker 部署
1. 构建 Docker Image 1. 构建 Docker Image
@ -157,24 +136,6 @@ docker-compose up -d
YesPlayMusic 地址为 `http://localhost` YesPlayMusic 地址为 `http://localhost`
## ⚙️ 部署至 Replit
1. 新建 Repl选择 Bash 模板
2. 在 Replit shell 中运行以下命令
```sh
bash <(curl -s -L https://raw.githubusercontent.com/qier222/YesPlayMusic/main/install-replit.sh)
```
3. 首次运行成功后,只需点击绿色按钮 `Run` 即可再次运行
4. 由于 replit 个人版限制内存为 1G教育版为 3G构建过程中可能会失败请再次运行上述命令或运行以下命令
```sh
cd /home/runner/${REPL_SLUG}/music && yarn install && yarn run build
```
## 👷‍♂️ 打包客户端 ## 👷‍♂️ 打包客户端
如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。 如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。

View file

@ -1,132 +0,0 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1730412360,
"owner": "cachix",
"repo": "devenv",
"rev": "45847cb1f14a6d8cfa86ea943703c54a8798ae7e",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1730272153,
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1730327045,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "080166c15633801df010977d9d7474b4a6c549d7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nodejs16": {
"locked": {
"lastModified": 1700230496,
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a71323f68d4377d12c04a5410e214495ec598d4c",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a71323f68d4377d12c04a5410e214495ec598d4c",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1730302582,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"nixpkgs": "nixpkgs",
"nodejs16": "nodejs16",
"pre-commit-hooks": "pre-commit-hooks"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,53 +0,0 @@
{ pkgs, lib, config, inputs, ... }:
let
nodejs16 = import inputs.nodejs16 { system = pkgs.stdenv.system; };
in
{
# https://devenv.sh/basics/
env.GREET = "devenv";
# https://devenv.sh/packages/
packages = [ pkgs.git ] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk; [
frameworks.AppKit
]);
# https://devenv.sh/languages/
languages.javascript.enable = true;
languages.javascript.package = nodejs16.pkgs.nodejs_16;
languages.javascript.corepack.enable = true;
# languages.rust.enable = true;
# https://devenv.sh/processes/
# processes.cargo-watch.exec = "cargo-watch";
# https://devenv.sh/services/
# services.postgres.enable = true;
# https://devenv.sh/scripts/
scripts.hello.exec = ''
echo hello from $GREET
'';
enterShell = ''
hello
git --version
'';
# https://devenv.sh/tasks/
# tasks = {
# "myproj:setup".exec = "mytool build";
# "devenv:enterShell".after = [ "myproj:setup" ];
# };
# https://devenv.sh/tests/
enterTest = ''
echo "Running tests"
git --version | grep --color=auto "${pkgs.git.version}"
'';
# https://devenv.sh/pre-commit-hooks/
# pre-commit.hooks.shellcheck.enable = true;
# See full reference at https://devenv.sh/reference/options/
}

View file

@ -1,19 +0,0 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:nixos/nixpkgs/nixpkgs-unstable
nodejs16:
url: github:nixos/nixpkgs/a71323f68d4377d12c04a5410e214495ec598d4c
# https://github.com/cachix/devenv/issues/792#issuecomment-2043166453
impure: true
# If you're using non-OSS software, you can set allowUnfree to true.
# allowUnfree: true
# If you're willing to use a package that's vulnerable
# permittedInsecurePackages:
# - "openssl-1.1.1w"
# If you have more than one devenv you can merge them
#imports:
# - ./backend

View file

@ -4,36 +4,6 @@ services:
context: . context: .
image: yesplaymusic image: yesplaymusic
container_name: YesPlayMusic container_name: YesPlayMusic
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- ./docker/nginx.conf.example:/etc/nginx/conf.d/default.conf:ro
ports: ports:
- 80:80 - 80:80
restart: always restart: always
depends_on:
- UnblockNeteaseMusic
environment:
- NODE_TLS_REJECT_UNAUTHORIZED=0
networks:
my_network:
UnblockNeteaseMusic:
image: pan93412/unblock-netease-music-enhanced
command: -o kugou kuwo migu bilibili pyncmd -p 80:443 -f 45.127.129.53 -e -
# environment:
# JSON_LOG: true
# LOG_LEVEL: debug
networks:
my_network:
aliases:
- music.163.com
- interface.music.163.com
- interface3.music.163.com
- interface.music.163.com.163jiasu.com
- interface3.music.163.com.163jiasu.com
restart: always
networks:
my_network:
driver: bridge

View file

@ -1,28 +0,0 @@
server {
gzip on;
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location @rewrites {
rewrite ^(.*)$ /index.html last;
}
location /api/ {
proxy_buffers 16 32k;
proxy_buffer_size 128k;
proxy_busy_buffers_size 128k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $remote_addr;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:3000/;
}
}

View file

@ -1,28 +0,0 @@
#!/usr/bin/bash
# 初始化 .replit 和 replit.nix
if [[ $1 == i ]];then
echo -e 'run = ["bash", "main.sh"]\n\nentrypoint = "main.sh"' >.replit
echo -e "{ pkgs }: {\n\t\tdeps = [\n\t\t\tpkgs.nodejs-16_x\n\t\t\tpkgs.yarn\n\t\t\tpkgs.bashInteractive\n\t\t];\n}" > replit.nix
exit
fi
# 安装
if [[ ! -d api ]];then
mkdir api
git clone https://github.com/Binaryify/NeteaseCloudMusicApi ./api && \
cd api && npm install && cd ..
fi
if [[ ! -d music ]];then
mkdir music
git clone https://github.com/qier222/YesPlayMusic ./music && \
cd music && cp .env.example .env && npm install --force && npm run build && cd ..
fi
# 启动
PID=`ps -ef | grep npm | awk '{print $2}' | sed '$d'`
if [[ ! -z ${PID} ]];then echo $PID | xargs kill;fi
nohup bash -c 'cd api && PORT=35216 node app.js' > api.log 2>&1
nohup bash -c 'npx serve music/dist/' > music.log 2>&1

View file

@ -7,8 +7,7 @@
}, },
"target": "ES6", "target": "ES6",
"module": "commonjs", "module": "commonjs",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true
"jsx": "preserve"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View file

@ -1,6 +1,6 @@
{ {
"name": "yesplaymusic", "name": "yesplaymusic",
"version": "0.4.9", "version": "0.4.4",
"private": true, "private": true,
"description": "A third party music player for Netease Music", "description": "A third party music player for Netease Music",
"author": "qier222<qier222@outlook.com>", "author": "qier222<qier222@outlook.com>",
@ -22,33 +22,28 @@
"netease_api:run": "npx NeteaseCloudMusicApi" "netease_api:run": "npx NeteaseCloudMusicApi"
}, },
"main": "background.js", "main": "background.js",
"engines": {
"node": "14 || 16"
},
"dependencies": { "dependencies": {
"@unblockneteasemusic/rust-napi": "^0.4.0", "@unblockneteasemusic/server": "v0.27.0-rc.4",
"NeteaseCloudMusicApi": "^4.23.3", "NeteaseCloudMusicApi": "^4.5.2",
"axios": "^0.26.1", "axios": "^0.21.0",
"change-case": "^4.1.2", "change-case": "^4.1.2",
"cli-color": "^2.0.0", "cli-color": "^2.0.0",
"color": "^4.2.3", "color": "^3.1.3",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"dayjs": "^1.8.36", "dayjs": "^1.8.36",
"dexie": "^3.0.3", "dexie": "^3.0.3",
"discord-rich-presence": "^0.0.8", "discord-rich-presence": "^0.0.8",
"electron": "^13.6.7", "electron": "^13.6.7",
"electron-builder": "^23.0.0", "electron-builder": "^22.10.5",
"electron-context-menu": "^3.1.2", "electron-context-menu": "^2.3.0",
"electron-debug": "^3.1.0", "electron-debug": "^3.1.0",
"electron-devtools-installer": "^3.2", "electron-devtools-installer": "^3.2",
"electron-icon-builder": "^2.0.1", "electron-icon-builder": "^1.0.2",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^1.2.0",
"electron-log": "^4.3.0", "electron-log": "^4.3.0",
"electron-store": "^8.0.1", "electron-store": "^6.0.1",
"electron-updater": "^5.0.1", "electron-updater": "^4.3.5",
"esbuild": "^0.20.1",
"esbuild-loader": "^4.0.3",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.2.0", "express-fileupload": "^1.2.0",
"express-http-proxy": "^1.6.2", "express-http-proxy": "^1.6.2",
@ -60,18 +55,19 @@
"md5": "^2.3.0", "md5": "^2.3.0",
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"music-metadata": "^7.5.3", "music-metadata": "^7.5.3",
"node-vibrant": "^3.2.1-alpha.1", "node-vibrant": "^3.1.6",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pac-proxy-agent": "^4.1.0", "pac-proxy-agent": "^4.1.0",
"plyr": "^3.6.2", "plyr": "^3.6.2",
"prettier": "2.1.2",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"register-service-worker": "^1.7.1", "register-service-worker": "^1.7.1",
"svg-sprite-loader": "^6.0.11", "svg-sprite-loader": "^5.0.0",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"vscode-codicons": "^0.0.17", "vscode-codicons": "^0.0.14",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-analytics": "^5.22.1",
"vue-clipboard2": "^0.3.1", "vue-clipboard2": "^0.3.1",
"vue-gtag": "1",
"vue-i18n": "^8.22.0", "vue-i18n": "^8.22.0",
"vue-router": "^3.4.3", "vue-router": "^3.4.3",
"vue-slider-component": "^3.2.5", "vue-slider-component": "^3.2.5",
@ -91,16 +87,14 @@
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.9.0", "eslint-plugin-vue": "^7.9.0",
"husky": "^4.3.0", "husky": "^4.3.0",
"prettier": "2.5.1",
"sass": "^1.26.11", "sass": "^1.26.11",
"sass-loader": "^10.0.2", "sass-loader": "^10.0.2",
"vue-cli-plugin-electron-builder": "~2.1.1", "vue-cli-plugin-electron-builder": "~2.0.0-rc.4",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
}, },
"resolutions": { "resolutions": {
"icon-gen": "3.0.0", "icon-gen": "3.0.0",
"degenerator": "2.2.0", "degenerator": "2.2.0"
"electron-builder": "^23.0.0"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

View file

@ -1,2 +1,2 @@
User-agent: * User-agent: *
Disallow: / Disallow:

View file

@ -1,7 +0,0 @@
commit_template: 'style: with ${restyler.name}'
restylers:
- prettier
- prettier-json
- prettier-markdown
- prettier-yaml
- whitespace

View file

@ -124,7 +124,6 @@ main {
overflow: auto; overflow: auto;
padding: 64px 10vw 96px 10vw; padding: 64px 10vw 96px 10vw;
box-sizing: border-box; box-sizing: border-box;
scrollbar-width: none; // firefox
} }
@media (max-width: 1336px) { @media (max-width: 1336px) {

View file

@ -1,7 +1,5 @@
import request from '@/utils/request'; import request from '@/utils/request';
import { mapTrackPlayableStatus } from '@/utils/common'; import { mapTrackPlayableStatus } from '@/utils/common';
import { isAccountLoggedIn } from '@/utils/auth';
import { getTrackDetail } from '@/api/track';
/** /**
* 获取歌手单曲 * 获取歌手单曲
@ -16,13 +14,7 @@ export function getArtist(id) {
id, id,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
}, },
}).then(async data => { }).then(data => {
if (!isAccountLoggedIn()) {
const trackIDs = data.hotSongs.map(t => t.id);
const tracks = await getTrackDetail(trackIDs.join(','));
data.hotSongs = tracks.songs;
return data;
}
data.hotSongs = mapTrackPlayableStatus(data.hotSongs); data.hotSongs = mapTrackPlayableStatus(data.hotSongs);
return data; return data;
}); });

View file

@ -26,10 +26,7 @@ export function dailyRecommendPlaylist(params) {
return request({ return request({
url: '/recommend/resource', url: '/recommend/resource',
method: 'get', method: 'get',
params: { params,
params,
timestamp: Date.now(),
},
}); });
} }
/** /**

View file

@ -15,18 +15,16 @@ import {
* @param {string} id - 音乐的 id例如 id=405998841,33894312 * @param {string} id - 音乐的 id例如 id=405998841,33894312
*/ */
export function getMP3(id) { export function getMP3(id) {
const getBr = () => { let br =
// 当返回的 quality >= 400000时就会优先返回 hi-res store.state.settings?.musicQuality !== undefined
const quality = store.state.settings?.musicQuality ?? '320000'; ? store.state.settings.musicQuality
return quality === 'flac' ? '350000' : quality; : 320000;
};
return request({ return request({
url: '/song/url', url: '/song/url',
method: 'get', method: 'get',
params: { params: {
id, id,
br: getBr(), br,
}, },
}); });
} }

View file

@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" data-darkreader-inline-fill="" xmlns="http://www.w3.org/2000/svg">
<path id="path" d="M1 11L3 11L3 13C3 13.55 3.45 14 4 14C4.55 14 5 13.55 5 13L5 10C5 9.45 4.55 9 4 9L1 9C0.45 9 0 9.45 0 10C0 10.55 0.45 11 1 11ZM11 3L11 1C11 0.45 10.55 0 10 0C9.45 0 9 0.45 9 1L9 4C9 4.54 9.45 5 10 5L13 5C13.55 5 14 4.54 14 4C14 3.45 13.55 3 13 3L11 3Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 396 B

View file

@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" data-darkreader-inline-fill="" xmlns="http://www.w3.org/2000/svg">
<path id="path" d="M1 9C0.45 9 0 9.45 0 10L0 13C0 13.55 0.45 14 1 14L4 14C4.55 14 5 13.55 5 13C5 12.45 4.55 12 4 12L2 12L2 10C2 9.45 1.55 9 1 9ZM9 1C9 1.54 9.45 2 10 2L12 2L12 4C12 4.54 12.45 5 13 5C13.55 5 14 4.54 14 4L14 1C14 0.45 13.55 0 13 0L10 0C9.45 0 9 0.45 9 1Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 396 B

View file

@ -7,7 +7,6 @@ import {
dialog, dialog,
globalShortcut, globalShortcut,
nativeTheme, nativeTheme,
screen,
} from 'electron'; } from 'electron';
import { import {
isWindows, isWindows,
@ -31,8 +30,7 @@ import { EventEmitter } from 'events';
import express from 'express'; import express from 'express';
import expressProxy from 'express-http-proxy'; import expressProxy from 'express-http-proxy';
import Store from 'electron-store'; import Store from 'electron-store';
import { createMpris, createDbus } from '@/electron/mpris'; import { createMpris } from '@/electron/mpris';
import { spawn } from 'child_process';
const clc = require('cli-color'); const clc = require('cli-color');
const log = text => { const log = text => {
console.log(`${clc.blueBright('[background.js]')} ${text}`); console.log(`${clc.blueBright('[background.js]')} ${text}`);
@ -182,10 +180,7 @@ class Background {
minWidth: 1080, minWidth: 1080,
minHeight: 720, minHeight: 720,
titleBarStyle: 'hiddenInset', titleBarStyle: 'hiddenInset',
frame: !( frame: !isWindows,
isWindows ||
(isLinux && this.store.get('settings.linuxEnableCustomTitlebar'))
),
title: 'YesPlayMusic', title: 'YesPlayMusic',
show: false, show: false,
webPreferences: { webPreferences: {
@ -203,42 +198,8 @@ class Background {
}; };
if (this.store.get('window.x') && this.store.get('window.y')) { if (this.store.get('window.x') && this.store.get('window.y')) {
let x = this.store.get('window.x'); options.x = this.store.get('window.x');
let y = this.store.get('window.y'); options.y = this.store.get('window.y');
let displays = screen.getAllDisplays();
let isResetWindiw = false;
if (displays.length === 1) {
let { bounds } = displays[0];
if (
x < bounds.x ||
x > bounds.x + bounds.width - 50 ||
y < bounds.y ||
y > bounds.y + bounds.height - 50
) {
isResetWindiw = true;
}
} else {
isResetWindiw = true;
for (let i = 0; i < displays.length; i++) {
let { bounds } = displays[i];
if (
x > bounds.x &&
x < bounds.x + bounds.width &&
y > bounds.y &&
y < bounds.y - bounds.height
) {
// 检测到APP窗口当前处于一个可用的屏幕里break
isResetWindiw = false;
break;
}
}
}
if (!isResetWindiw) {
options.x = x;
options.y = y;
}
} }
this.window = new BrowserWindow(options); this.window = new BrowserWindow(options);
@ -297,7 +258,6 @@ class Background {
this.window.once('ready-to-show', () => { this.window.once('ready-to-show', () => {
log('window ready-to-show event'); log('window ready-to-show event');
this.window.show(); this.window.show();
this.store.set('window', this.window.getBounds());
}); });
this.window.on('close', e => { this.window.on('close', e => {
@ -333,14 +293,6 @@ class Background {
this.store.set('window', this.window.getBounds()); this.store.set('window', this.window.getBounds());
}); });
this.window.on('maximize', () => {
this.window.webContents.send('isMaximized', true);
});
this.window.on('unmaximize', () => {
this.window.webContents.send('isMaximized', false);
});
this.window.webContents.on('new-window', function (e, url) { this.window.webContents.on('new-window', function (e, url) {
e.preventDefault(); e.preventDefault();
log('open url'); log('open url');
@ -421,21 +373,6 @@ class Background {
registerGlobalShortcut(this.window, this.store); registerGlobalShortcut(this.window, this.store);
} }
// try to start osdlyrics process on start
if (this.store.get('settings.enableOsdlyricsSupport')) {
await createDbus(this.window);
log('try to start osdlyrics process');
const osdlyricsProcess = spawn('osdlyrics');
osdlyricsProcess.on('error', err => {
log(`failed to start osdlyrics: ${err.message}`);
});
osdlyricsProcess.on('exit', (code, signal) => {
log(`osdlyrics process exited with code ${code}, signal ${signal}`);
});
}
// create mpris // create mpris
if (isCreateMpris) { if (isCreateMpris) {
createMpris(this.window); createMpris(this.window);

View file

@ -2,13 +2,11 @@
<span class="artist-in-line"> <span class="artist-in-line">
{{ computedPrefix }} {{ computedPrefix }}
<span v-for="(ar, index) in filteredArtists" :key="index"> <span v-for="(ar, index) in filteredArtists" :key="index">
<router-link v-if="ar.id !== 0" :to="`/artist/${ar.id}`">{{ <router-link v-if="ar.id !== 0" :to="`/artist/${ar.id}`">
ar.name {{ ar.name }}
}}</router-link> </router-link>
<span v-else>{{ ar.name }}</span> <span v-else>{{ ar.name }}</span>
<span v-if="index !== filteredArtists.length - 1" class="separator" <span v-if="index !== filteredArtists.length - 1">, </span>
>,</span
>
</span> </span>
</span> </span>
</template> </template>
@ -42,12 +40,4 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped></style>
.separator {
/* make separator distinct enough in long list */
margin-left: 1px;
margin-right: 4px;
position: relative;
top: 0.5px;
}
</style>

View file

@ -80,13 +80,11 @@ export default {
box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.08); box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border-radius: 12px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
padding: 6px; padding: 6px;
z-index: 1000; z-index: 1000;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
transition: background 125ms ease-out, opacity 125ms ease-out,
transform 125ms ease-out;
&:focus { &:focus {
outline: none; outline: none;
@ -96,9 +94,8 @@ export default {
[data-theme='dark'] { [data-theme='dark'] {
.menu { .menu {
background: rgba(36, 36, 36, 0.78); background: rgba(36, 36, 36, 0.78);
backdrop-filter: blur(16px) contrast(120%) brightness(60%); backdrop-filter: blur(16px) contrast(120%);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 0 6px rgba(255, 255, 255, 0.08);
} }
.menu .item:hover { .menu .item:hover {
color: var(--color-text); color: var(--color-text);
@ -115,7 +112,7 @@ export default {
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
padding: 10px 14px; padding: 10px 14px;
border-radius: 8px; border-radius: 7px;
cursor: default; cursor: default;
color: var(--color-text); color: var(--color-text);
display: flex; display: flex;
@ -123,11 +120,6 @@ export default {
&:hover { &:hover {
color: var(--color-primary); color: var(--color-primary);
background: var(--color-primary-bg-for-transparent); background: var(--color-primary-bg-for-transparent);
transition: opacity 125ms ease-out, transform 125ms ease-out;
}
&:active {
opacity: 0.75;
transform: scale(0.95);
} }
.svg-icon { .svg-icon {
@ -157,7 +149,7 @@ hr {
border-radius: 4px; border-radius: 4px;
} }
.info { .info {
margin-left: 10px; margin-left: 8px;
} }
.title { .title {
font-size: 16px; font-size: 16px;

View file

@ -16,7 +16,7 @@
><svg-icon icon-class="play" /> ><svg-icon icon-class="play" />
</button> </button>
</div> </div>
<img :src="imageUrl" :style="imageStyles" loading="lazy" /> <img :src="imageUrl" :style="imageStyles" />
<transition v-if="coverHover || alwaysShowShadow" name="fade"> <transition v-if="coverHover || alwaysShowShadow" name="fade">
<div <div
v-show="focus || alwaysShowShadow" v-show="focus || alwaysShowShadow"
@ -135,7 +135,7 @@ img {
cursor: default; cursor: default;
transition: 0.2s; transition: 0.2s;
.svg-icon { .svg-icon {
width: 50%; height: 44%;
margin: { margin: {
left: 4px; left: 4px;
} }

View file

@ -50,7 +50,7 @@ export default {
props: { props: {
items: { type: Array, required: true }, items: { type: Array, required: true },
type: { type: String, required: true }, type: { type: String, required: true },
subText: { type: String, default: 'none' }, subText: { type: String, default: 'null' },
subTextFontSize: { type: String, default: '16px' }, subTextFontSize: { type: String, default: '16px' },
showPlayCount: { type: Boolean, default: false }, showPlayCount: { type: Boolean, default: false },
columnNumber: { type: Number, default: 5 }, columnNumber: { type: Number, default: 5 },
@ -75,9 +75,9 @@ export default {
return new Date(item.publishTime).getFullYear(); return new Date(item.publishTime).getFullYear();
if (this.subText === 'artist') { if (this.subText === 'artist') {
if (item.artist !== undefined) if (item.artist !== undefined)
return `<a href="/artist/${item.artist.id}">${item.artist.name}</a>`; return `<a href="/#/artist/${item.artist.id}">${item.artist.name}</a>`;
if (item.artists !== undefined) if (item.artists !== undefined)
return `<a href="/artist/${item.artists[0].id}">${item.artists[0].name}</a>`; return `<a href="/#/artist/${item.artists[0].id}">${item.artists[0].name}</a>`;
} }
if (this.subText === 'albumType+releaseYear') { if (this.subText === 'albumType+releaseYear') {
let albumType = item.type; let albumType = item.type;
@ -96,7 +96,7 @@ export default {
return this.type === 'playlist' && item.privacy === 10; return this.type === 'playlist' && item.privacy === 10;
}, },
isExplicit(item) { isExplicit(item) {
return this.type === 'album' && (item.mark & 1048576) === 1048576; return this.type === 'album' && item.mark === 1056768;
}, },
getTitleLink(item) { getTitleLink(item) {
return `/${this.type}/${item.id}`; return `/${this.type}/${item.id}`;

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="daily-recommend-card" @click="goToDailyTracks"> <div class="daily-recommend-card" @click="goToDailyTracks">
<img :src="coverUrl" loading="lazy" /> <img :src="coverUrl" />
<div class="container"> <div class="container">
<div class="title-box"> <div class="title-box">
<div class="title"> <div class="title">
@ -84,7 +84,6 @@ export default {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
z-index: 1;
} }
img { img {

View file

@ -25,8 +25,6 @@ export default {
this.svgStyle = { this.svgStyle = {
height: this.size + 'px', height: this.size + 'px',
width: this.size + 'px', width: this.size + 'px',
position: 'relative',
left: '-1px',
}; };
}, },
}; };

View file

@ -1,10 +1,9 @@
<template> <template>
<div class="fm" :style="{ background }" data-theme="dark"> <div class="fm" :style="{ background }" data-theme="dark">
<img :src="nextTrackCover" style="display: none" loading="lazy" /> <img :src="nextTrackCover" style="display: none" />
<img <img
class="cover" class="cover"
:src="track.album && track.album.picUrl | resizeImage(512)" :src="track.album && track.album.picUrl | resizeImage(512)"
loading="lazy"
@click="goToAlbum" @click="goToAlbum"
/> />
<div class="right-part"> <div class="right-part">
@ -14,20 +13,19 @@
</div> </div>
<div class="controls"> <div class="controls">
<div class="buttons"> <div class="buttons">
<button-icon title="不喜欢" @click.native="moveToFMTrash"> <button-icon title="不喜欢" @click.native="moveToFMTrash"
<svg-icon id="thumbs-down" icon-class="thumbs-down" /> ><svg-icon id="thumbs-down" icon-class="thumbs-down"
</button-icon> /></button-icon>
<button-icon <button-icon
:title="$t(isPlaying ? 'player.pause' : 'player.play')" :title="$t(isPlaying ? 'player.pause' : 'player.play')"
class="play" class="play"
@click.native="play" @click.native="play"
> >
<svg-icon :icon-class="isPlaying ? 'pause' : 'play'" /> <svg-icon :icon-class="isPlaying ? 'pause' : 'play'"
</button-icon> /></button-icon>
<button-icon :title="$t('player.next')" @click.native="next"> <button-icon :title="$t('player.next')" @click.native="next"
<svg-icon icon-class="next" /> ><svg-icon icon-class="next" /></button-icon
</button-icon> ></div>
</div>
<div class="card-name"><svg-icon icon-class="fm" />私人FM</div> <div class="card-name"><svg-icon icon-class="fm" />私人FM</div>
</div> </div>
</div> </div>
@ -38,7 +36,7 @@
import ButtonIcon from '@/components/ButtonIcon.vue'; import ButtonIcon from '@/components/ButtonIcon.vue';
import ArtistsInLine from '@/components/ArtistsInLine.vue'; import ArtistsInLine from '@/components/ArtistsInLine.vue';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js'; import * as Vibrant from 'node-vibrant';
import Color from 'color'; import Color from 'color';
export default { export default {
@ -67,11 +65,6 @@ export default {
)}?param=512y512`; )}?param=512y512`;
}, },
}, },
watch: {
track() {
this.getColor();
},
},
created() { created() {
this.getColor(); this.getColor();
window.ok = this.getColor; window.ok = this.getColor;
@ -81,7 +74,11 @@ export default {
this.player.playPersonalFM(); this.player.playPersonalFM();
}, },
next() { next() {
this.player.playNextFMTrack(); this.player.playNextFMTrack().then(result => {
if (result) {
this.getColor();
}
});
}, },
goToAlbum() { goToAlbum() {
if (this.track.album.id === 0) return; if (this.track.album.id === 0) return;
@ -89,6 +86,7 @@ export default {
}, },
moveToFMTrash() { moveToFMTrash() {
this.player.moveToFMTrash(); this.player.moveToFMTrash();
this.getColor();
}, },
getColor() { getColor() {
if (!this.player.personalFMTrack?.album?.picUrl) return; if (!this.player.personalFMTrack?.album?.picUrl) return;

View file

@ -1,130 +0,0 @@
<template>
<div class="linux-titlebar">
<div class="logo">
<img src="img/logos/yesplaymusic-white24x24.png" />
</div>
<div class="title">{{ title }}</div>
<div class="controls">
<div
class="button minimize codicon codicon-chrome-minimize"
@click="windowMinimize"
></div>
<div
class="button max-restore codicon"
:class="{
'codicon-chrome-restore': isMaximized,
'codicon-chrome-maximize': !isMaximized,
}"
@click="windowMaxRestore"
></div>
<div
class="button close codicon codicon-chrome-close"
@click="windowClose"
></div>
</div>
</div>
</template>
<script>
// icons by https://github.com/microsoft/vscode-codicons
import 'vscode-codicons/dist/codicon.css';
import { mapState } from 'vuex';
const electron =
process.env.IS_ELECTRON === true ? window.require('electron') : null;
const ipcRenderer =
process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;
export default {
name: 'LinuxTitlebar',
data() {
return {
isMaximized: false,
};
},
computed: {
...mapState(['title']),
},
created() {
if (process.env.IS_ELECTRON === true) {
ipcRenderer.on('isMaximized', (_, value) => {
this.isMaximized = value;
});
}
},
methods: {
windowMinimize() {
ipcRenderer.send('minimize');
},
windowMaxRestore() {
ipcRenderer.send('maximizeOrUnmaximize');
},
windowClose() {
ipcRenderer.send('close');
},
},
};
</script>
<style lang="scss" scoped>
.linux-titlebar {
color: var(--color-text);
position: fixed;
left: 0;
top: 0;
right: 0;
-webkit-app-region: drag;
display: flex;
align-items: center;
--hover: #e6e6e6;
--active: #cccccc;
.logo {
padding: 0 8px;
}
.title {
padding: 8px;
font-size: 12px;
font-family: 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif;
justify-self: center;
margin: 0 auto;
}
.controls {
height: 32px;
//margin-left: auto;
justify-content: flex-end;
display: flex;
.button {
height: 100%;
width: 46px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
-webkit-app-region: no-drag;
&:hover {
background: var(--hover);
}
&:active {
background: var(--active);
}
&.close {
&:hover {
background: #c42c1b;
color: rgba(255, 255, 255, 0.8);
}
&:active {
background: #f1707a;
color: #000;
}
}
}
}
}
[data-theme='dark'] .linux-titlebar {
--hover: #191919;
--active: #333333;
}
</style>

View file

@ -39,16 +39,11 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
minWidth: {
type: String,
default: 'calc(min(23rem, 100vw))',
},
}, },
computed: { computed: {
modalStyles() { modalStyles() {
return { return {
width: this.width, width: this.width,
minWidth: this.minWidth,
}; };
}, },
}, },

View file

@ -17,7 +17,7 @@
class="playlist" class="playlist"
@click="addTrackToPlaylist(playlist.id)" @click="addTrackToPlaylist(playlist.id)"
> >
<img :src="playlist.coverImgUrl | resizeImage(224)" loading="lazy" /> <img :src="playlist.coverImgUrl | resizeImage(224)" />
<div class="info"> <div class="info">
<div class="title">{{ playlist.name }}</div> <div class="title">{{ playlist.name }}</div>
<div class="track-count">{{ playlist.trackCount }} </div> <div class="track-count">{{ playlist.trackCount }} </div>

View file

@ -147,7 +147,6 @@ export default {
label { label {
font-size: 12px; font-size: 12px;
} }
user-select: none;
} }
} }
} }

View file

@ -7,7 +7,7 @@
@mouseleave="hoverVideoID = 0" @mouseleave="hoverVideoID = 0"
@click="goToMv(getID(mv))" @click="goToMv(getID(mv))"
> >
<img :src="getUrl(mv)" loading="lazy" /> <img :src="getUrl(mv)" />
<transition name="fade"> <transition name="fade">
<div <div
v-show="hoverVideoID === getID(mv)" v-show="hoverVideoID === getID(mv)"
@ -73,7 +73,7 @@ export default {
artistName = mv.creator[0].userName; artistName = mv.creator[0].userName;
artistID = mv.creator[0].userId; artistID = mv.creator[0].userId;
} }
return `<a href="/artist/${artistID}">${artistName}</a>`; return `<a href="/#/artist/${artistID}">${artistName}</a>`;
} else if (this.subtitle === 'publishTime') { } else if (this.subtitle === 'publishTime') {
return mv.publishTime; return mv.publishTime;
} }

View file

@ -1,8 +1,27 @@
<template> <template>
<div> <div>
<nav :class="{ 'has-custom-titlebar': hasCustomTitlebar }"> <nav>
<Win32Titlebar v-if="enableWin32Titlebar" /> <div class="win32-titlebar">
<LinuxTitlebar v-if="enableLinuxTitlebar" /> <div class="title">YesPlayMusic</div>
<div class="controls">
<div
class="button minimize codicon codicon-chrome-minimize"
@click="windowMinimize"
></div>
<div
class="button max-restore codicon"
:class="{
'codicon-chrome-restore': !isWindowMaximized,
'codicon-chrome-maximize': isWindowMaximized,
}"
@click="windowMaxRestore"
></div>
<div
class="button close codicon codicon-chrome-close"
@click="windowClose"
></div>
</div>
</div>
<div class="navigation-buttons"> <div class="navigation-buttons">
<button-icon @click.native="go('back')" <button-icon @click.native="go('back')"
><svg-icon icon-class="arrow-left" ><svg-icon icon-class="arrow-left"
@ -43,12 +62,7 @@
</div> </div>
</div> </div>
</div> </div>
<img <img class="avatar" :src="avatarUrl" @click="showUserProfileMenu" />
class="avatar"
:src="avatarUrl"
@click="showUserProfileMenu"
loading="lazy"
/>
</div> </div>
</nav> </nav>
@ -82,16 +96,17 @@ import { isLooseLoggedIn, doLogout } from '@/utils/auth';
// icons by https://github.com/microsoft/vscode-codicons // icons by https://github.com/microsoft/vscode-codicons
import 'vscode-codicons/dist/codicon.css'; import 'vscode-codicons/dist/codicon.css';
import Win32Titlebar from '@/components/Win32Titlebar.vue';
import LinuxTitlebar from '@/components/LinuxTitlebar.vue';
import ContextMenu from '@/components/ContextMenu.vue'; import ContextMenu from '@/components/ContextMenu.vue';
import ButtonIcon from '@/components/ButtonIcon.vue'; import ButtonIcon from '@/components/ButtonIcon.vue';
const electron =
process.env.IS_ELECTRON === true ? window.require('electron') : null;
const ipcRenderer =
process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;
export default { export default {
name: 'Navbar', name: 'Navbar',
components: { components: {
Win32Titlebar,
LinuxTitlebar,
ButtonIcon, ButtonIcon,
ContextMenu, ContextMenu,
}, },
@ -100,8 +115,7 @@ export default {
inputFocus: false, inputFocus: false,
langs: ['zh-CN', 'zh-TW', 'en', 'tr'], langs: ['zh-CN', 'zh-TW', 'en', 'tr'],
keywords: '', keywords: '',
enableWin32Titlebar: false, isWindowMaximized: false,
enableLinuxTitlebar: false,
}; };
}, },
computed: { computed: {
@ -114,18 +128,12 @@ export default {
? `${this.data?.user?.avatarUrl}?param=512y512` ? `${this.data?.user?.avatarUrl}?param=512y512`
: 'http://s4.music.126.net/style/web2/img/default/default_avatar.jpg?param=60y60'; : 'http://s4.music.126.net/style/web2/img/default/default_avatar.jpg?param=60y60';
}, },
hasCustomTitlebar() {
return this.enableWin32Titlebar || this.enableLinuxTitlebar;
},
}, },
created() { created() {
if (process.platform === 'win32') { if (process.env.IS_ELECTRON === true) {
this.enableWin32Titlebar = true; ipcRenderer.on('isMaximized', (event, value) => {
} else if ( this.isWindowMaximized = value;
process.platform === 'linux' && });
this.settings.linuxEnableCustomTitlebar
) {
this.enableLinuxTitlebar = true;
} }
}, },
methods: { methods: {
@ -167,6 +175,15 @@ export default {
this.$router.push({ name: 'login' }); this.$router.push({ name: 'login' });
} }
}, },
windowMinimize() {
ipcRenderer.send('minimize');
},
windowMaxRestore() {
ipcRenderer.send('maximizeOrUnmaximize');
},
windowClose() {
ipcRenderer.send('close');
},
}, },
}; };
</script> </script>
@ -194,7 +211,7 @@ nav {
@media (max-width: 1336px) { @media (max-width: 1336px) {
nav { nav {
padding: 0 max(5vw, 90px); padding: 0 5vw;
} }
} }
@ -204,9 +221,68 @@ nav {
} }
} }
nav.has-custom-titlebar { .win32-titlebar {
padding-top: 20px; display: none;
-webkit-app-region: no-drag; }
[data-electron-os='win32'] {
nav {
padding-top: 20px;
-webkit-app-region: no-drag;
}
.win32-titlebar {
color: var(--color-text);
position: fixed;
left: 0;
top: 0;
right: 0;
-webkit-app-region: drag;
display: flex;
align-items: center;
--hover: #e6e6e6;
--active: #cccccc;
.title {
padding: 8px;
font-size: 12px;
font-family: 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei',
sans-serif;
}
.controls {
height: 32px;
margin-left: auto;
justify-content: flex-end;
display: flex;
.button {
height: 100%;
width: 46px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
-webkit-app-region: no-drag;
&:hover {
background: var(--hover);
}
&:active {
background: var(--active);
}
&.close {
&:hover {
background: rgba(232, 17, 35, 0.9);
}
&:active {
background: #f1707a;
color: #000;
}
}
}
}
}
&[data-theme='dark'] .win32-titlebar {
--hover: #191919;
--active: #333333;
}
} }
.navigation-buttons { .navigation-buttons {

View file

@ -27,7 +27,6 @@
<div class="container" @click.stop> <div class="container" @click.stop>
<img <img
:src="currentTrack.al && currentTrack.al.picUrl | resizeImage(224)" :src="currentTrack.al && currentTrack.al.picUrl | resizeImage(224)"
loading="lazy"
@click="goToAlbum" @click="goToAlbum"
/> />
<div class="track-info" :title="audioSource"> <div class="track-info" :title="audioSource">
@ -50,11 +49,7 @@
</div> </div>
<div class="like-button"> <div class="like-button">
<button-icon <button-icon
:title=" :title="$t('player.like')"
player.isCurrentTrackLiked
? $t('player.unlike')
: $t('player.like')
"
@click.native="likeATrack(player.currentTrack.id)" @click.native="likeATrack(player.currentTrack.id)"
> >
<svg-icon <svg-icon
@ -76,19 +71,19 @@
<button-icon <button-icon
v-show="!player.isPersonalFM" v-show="!player.isPersonalFM"
:title="$t('player.previous')" :title="$t('player.previous')"
@click.native="playPrevTrack" @click.native="player.playPrevTrack"
><svg-icon icon-class="previous" ><svg-icon icon-class="previous"
/></button-icon> /></button-icon>
<button-icon <button-icon
v-show="player.isPersonalFM" v-show="player.isPersonalFM"
title="不喜欢" title="不喜欢"
@click.native="moveToFMTrash" @click.native="player.moveToFMTrash"
><svg-icon icon-class="thumbs-down" ><svg-icon icon-class="thumbs-down"
/></button-icon> /></button-icon>
<button-icon <button-icon
class="play" class="play"
:title="$t(player.playing ? 'player.pause' : 'player.play')" :title="$t(player.playing ? 'player.pause' : 'player.play')"
@click.native="playOrPause" @click.native="player.playOrPause"
> >
<svg-icon :icon-class="player.playing ? 'pause' : 'play'" <svg-icon :icon-class="player.playing ? 'pause' : 'play'"
/></button-icon> /></button-icon>
@ -120,7 +115,7 @@
? $t('player.repeatTrack') ? $t('player.repeatTrack')
: $t('player.repeat') : $t('player.repeat')
" "
@click.native="switchRepeatMode" @click.native="player.switchRepeatMode"
> >
<svg-icon <svg-icon
v-show="player.repeatMode !== 'one'" v-show="player.repeatMode !== 'one'"
@ -134,18 +129,18 @@
<button-icon <button-icon
:class="{ active: player.shuffle, disabled: player.isPersonalFM }" :class="{ active: player.shuffle, disabled: player.isPersonalFM }"
:title="$t('player.shuffle')" :title="$t('player.shuffle')"
@click.native="switchShuffle" @click.native="player.switchShuffle"
><svg-icon icon-class="shuffle" ><svg-icon icon-class="shuffle"
/></button-icon> /></button-icon>
<button-icon <button-icon
v-if="settings.enableReversedMode" v-if="settings.enableReversedMode"
:class="{ active: player.reversed, disabled: player.isPersonalFM }" :class="{ active: player.reversed, disabled: player.isPersonalFM }"
:title="$t('player.reversed')" :title="$t('player.reversed')"
@click.native="switchReversed" @click.native="player.switchReversed"
><svg-icon icon-class="sort-up" ><svg-icon icon-class="sort-up"
/></button-icon> /></button-icon>
<div class="volume-control"> <div class="volume-control">
<button-icon :title="$t('player.mute')" @click.native="mute"> <button-icon :title="$t('player.mute')" @click.native="player.mute">
<svg-icon v-show="volume > 0.5" icon-class="volume" /> <svg-icon v-show="volume > 0.5" icon-class="volume" />
<svg-icon v-show="volume === 0" icon-class="volume-mute" /> <svg-icon v-show="volume === 0" icon-class="volume-mute" />
<svg-icon <svg-icon
@ -187,7 +182,6 @@ import '@/assets/css/slider.css';
import ButtonIcon from '@/components/ButtonIcon.vue'; import ButtonIcon from '@/components/ButtonIcon.vue';
import VueSlider from 'vue-slider-component'; import VueSlider from 'vue-slider-component';
import { goToListSource, hasListSource } from '@/utils/playList'; import { goToListSource, hasListSource } from '@/utils/playList';
import { formatTrackTime } from '@/utils/common';
export default { export default {
name: 'Player', name: 'Player',
@ -220,12 +214,6 @@ export default {
methods: { methods: {
...mapMutations(['toggleLyrics']), ...mapMutations(['toggleLyrics']),
...mapActions(['showToast', 'likeATrack']), ...mapActions(['showToast', 'likeATrack']),
playPrevTrack() {
this.player.playPrevTrack();
},
playOrPause() {
this.player.playOrPause();
},
playNextTrack() { playNextTrack() {
if (this.player.isPersonalFM) { if (this.player.isPersonalFM) {
this.player.playNextFMTrack(); this.player.playNextFMTrack();
@ -240,7 +228,10 @@ export default {
: this.$router.push({ name: 'next' }); : this.$router.push({ name: 'next' });
}, },
formatTrackTime(value) { formatTrackTime(value) {
return formatTrackTime(value); if (!value) return '';
let min = ~~((value / 60) % 60);
let sec = (~~(value % 60)).toString().padStart(2, '0');
return `${min}:${sec}`;
}, },
hasList() { hasList() {
return hasListSource(); return hasListSource();
@ -255,21 +246,6 @@ export default {
goToArtist(id) { goToArtist(id) {
this.$router.push({ path: '/artist/' + id }); this.$router.push({ path: '/artist/' + id });
}, },
moveToFMTrash() {
this.player.moveToFMTrash();
},
switchRepeatMode() {
this.player.switchRepeatMode();
},
switchShuffle() {
this.player.switchShuffle();
},
switchReversed() {
this.player.switchReversed();
},
mute() {
this.player.mute();
},
}, },
}; };
</script> </script>

View file

@ -2,10 +2,7 @@
<div class="track-list"> <div class="track-list">
<ContextMenu ref="menu"> <ContextMenu ref="menu">
<div v-show="type !== 'cloudDisk'" class="item-info"> <div v-show="type !== 'cloudDisk'" class="item-info">
<img <img :src="rightClickedTrackComputed.al.picUrl | resizeImage(224)" />
:src="rightClickedTrackComputed.al.picUrl | resizeImage(224)"
loading="lazy"
/>
<div class="info"> <div class="info">
<div class="title">{{ rightClickedTrackComputed.name }}</div> <div class="title">{{ rightClickedTrackComputed.name }}</div>
<div class="subtitle">{{ rightClickedTrackComputed.ar[0].name }}</div> <div class="subtitle">{{ rightClickedTrackComputed.ar[0].name }}</div>
@ -49,9 +46,6 @@
@click="addTrackToPlaylist" @click="addTrackToPlaylist"
>{{ $t('contextMenu.addToPlaylist') }}</div >{{ $t('contextMenu.addToPlaylist') }}</div
> >
<div v-show="type !== 'cloudDisk'" class="item" @click="copyLink">{{
$t('contextMenu.copyUrl')
}}</div>
<div <div
v-if="extraContextMenuItem.includes('removeTrackFromCloudDisk')" v-if="extraContextMenuItem.includes('removeTrackFromCloudDisk')"
class="item" class="item"
@ -65,7 +59,6 @@
v-for="(track, index) in tracks" v-for="(track, index) in tracks"
:key="itemKey === 'id' ? track.id : `${track.id}${index}`" :key="itemKey === 'id' ? track.id : `${track.id}${index}`"
:track-prop="track" :track-prop="track"
:track-no="index + 1"
:highlight-playing-track="highlightPlayingTrack" :highlight-playing-track="highlightPlayingTrack"
@dblclick.native="playThisList(track.id || track.songId)" @dblclick.native="playThisList(track.id || track.songId)"
@click.right.native="openMenu($event, track, index)" @click.right.native="openMenu($event, track, index)"
@ -272,17 +265,6 @@ export default {
}); });
} }
}, },
copyLink() {
this.$copyText(
`https://music.163.com/song?id=${this.rightClickedTrack.id}`
)
.then(() => {
this.showToast(locale.t('toast.copied'));
})
.catch(err => {
this.showToast(`${locale.t('toast.copyFailed')}${err}`);
});
},
removeTrackFromQueue() { removeTrackFromQueue() {
this.$store.state.player.removeTrackFromQueue( this.$store.state.player.removeTrackFromQueue(
this.rightClickedTrackIndex this.rightClickedTrackIndex

View file

@ -10,7 +10,6 @@
<img <img
v-if="!isAlbum" v-if="!isAlbum"
:src="imgUrl" :src="imgUrl"
loading="lazy"
:class="{ hover: focus }" :class="{ hover: focus }"
@click="goToAlbum" @click="goToAlbum"
/> />
@ -21,7 +20,7 @@
style="height: 14px; width: 14px" style="height: 14px; width: 14px"
></svg-icon> ></svg-icon>
</button> </button>
<span v-show="(!focus || !playable) && !isPlaying">{{ trackNo }}</span> <span v-show="(!focus || !playable) && !isPlaying">{{ track.no }}</span>
<button v-show="isPlaying"> <button v-show="isPlaying">
<svg-icon <svg-icon
icon-class="volume" icon-class="volume"
@ -33,24 +32,22 @@
<div class="container"> <div class="container">
<div class="title"> <div class="title">
{{ track.name }} {{ track.name }}
<span v-if="isSubTitle" :title="subTitle" class="sub-title">
({{ subTitle }})
</span>
<span v-if="isAlbum" class="featured"> <span v-if="isAlbum" class="featured">
<ArtistsInLine <ArtistsInLine
:artists="track.ar" :artists="track.ar"
:exclude="$parent.albumObject.artist.name" :exclude="$parent.albumObject.artist.name"
prefix="-" prefix="-"
/></span> /></span>
<span <span v-if="isAlbum && track.mark === 1318912" class="explicit-symbol"
v-if="isAlbum && (track.mark & 1048576) === 1048576"
class="explicit-symbol"
><ExplicitSymbol ><ExplicitSymbol
/></span> /></span>
<span v-if="isSubTitle" :title="subTitle" class="sub-title">
({{ subTitle }})
</span>
</div> </div>
<div v-if="!isAlbum" class="artist"> <div v-if="!isAlbum" class="artist">
<span <span
v-if="(track.mark & 1048576) === 1048576" v-if="track.mark === 1318912"
class="explicit-symbol before-artist" class="explicit-symbol before-artist"
><ExplicitSymbol :size="15" ><ExplicitSymbol :size="15"
/></span> /></span>
@ -98,7 +95,6 @@ export default {
props: { props: {
trackProp: Object, trackProp: Object,
trackNo: Number,
highlightPlayingTrack: { highlightPlayingTrack: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -212,7 +208,6 @@ export default {
methods: { methods: {
goToAlbum() { goToAlbum() {
if (this.track.al.id === 0) return;
this.$router.push({ path: '/album/' + this.track.al.id }); this.$router.push({ path: '/album/' + this.track.al.id });
}, },
playTrack() { playTrack() {
@ -277,6 +272,7 @@ button {
} }
.explicit-symbol.before-artist { .explicit-symbol.before-artist {
margin-right: 2px;
.svg-icon { .svg-icon {
margin-bottom: -3px; margin-bottom: -3px;
} }
@ -320,8 +316,7 @@ button {
opacity: 0.72; opacity: 0.72;
} }
.sub-title { .sub-title {
color: #7a7a7a; color: #aeaeae;
opacity: 0.7;
margin-left: 4px; margin-left: 4px;
} }
} }
@ -369,11 +364,6 @@ button {
opacity: 0.88; opacity: 0.88;
color: var(--color-text); color: var(--color-text);
} }
.count {
font-weight: bold;
font-size: 22px;
line-height: 22px;
}
} }
.track.focus { .track.focus {
@ -435,8 +425,7 @@ button {
} }
.title .featured, .title .featured,
.artist, .artist,
.explicit-symbol, .explicit-symbol {
.count {
color: var(--color-primary); color: var(--color-primary);
opacity: 0.88; opacity: 0.88;
} }

View file

@ -1,121 +0,0 @@
<template>
<div class="win32-titlebar">
<div class="title">{{ title }}</div>
<div class="controls">
<div
class="button minimize codicon codicon-chrome-minimize"
@click="windowMinimize"
></div>
<div
class="button max-restore codicon"
:class="{
'codicon-chrome-restore': isMaximized,
'codicon-chrome-maximize': !isMaximized,
}"
@click="windowMaxRestore"
></div>
<div
class="button close codicon codicon-chrome-close"
@click="windowClose"
></div>
</div>
</div>
</template>
<script>
// icons by https://github.com/microsoft/vscode-codicons
import 'vscode-codicons/dist/codicon.css';
import { mapState } from 'vuex';
const electron =
process.env.IS_ELECTRON === true ? window.require('electron') : null;
const ipcRenderer =
process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;
export default {
name: 'Win32Titlebar',
data() {
return {
isMaximized: false,
};
},
computed: {
...mapState(['title']),
},
created() {
if (process.env.IS_ELECTRON === true) {
ipcRenderer.on('isMaximized', (_, value) => {
this.isMaximized = value;
});
}
},
methods: {
windowMinimize() {
ipcRenderer.send('minimize');
},
windowMaxRestore() {
ipcRenderer.send('maximizeOrUnmaximize');
},
windowClose() {
ipcRenderer.send('close');
},
},
};
</script>
<style lang="scss" scoped>
.win32-titlebar {
color: var(--color-text);
position: fixed;
left: 0;
top: 0;
right: 0;
-webkit-app-region: drag;
display: flex;
align-items: center;
--hover: #e6e6e6;
--active: #cccccc;
.title {
padding: 8px 12px;
font-size: 12px;
font-family: 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif;
}
.controls {
height: 32px;
margin-left: auto;
justify-content: flex-end;
display: flex;
.button {
height: 100%;
width: 46px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
-webkit-app-region: no-drag;
&:hover {
background: var(--hover);
}
&:active {
background: var(--active);
}
&.close {
&:hover {
background: #c42c1b;
color: rgba(255, 255, 255, 0.8);
}
&:active {
background: #f1707a;
color: #000;
}
}
}
}
}
[data-theme='dark'] .win32-titlebar {
--hover: #191919;
--active: #333333;
}
</style>

View file

@ -1,5 +1,5 @@
import { app, dialog, globalShortcut, ipcMain } from 'electron'; import { app, dialog, globalShortcut, ipcMain } from 'electron';
import UNM from '@unblockneteasemusic/rust-napi'; import match from '@unblockneteasemusic/server';
import { registerGlobalShortcut } from '@/electron/globalShortcut'; import { registerGlobalShortcut } from '@/electron/globalShortcut';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import shortcuts from '@/utils/shortcuts'; import shortcuts from '@/utils/shortcuts';
@ -88,10 +88,10 @@ function toBuffer(data) {
} }
/** /**
* Get the file base64 data from bilivideo. * Get the file URI from bilivideo.
* *
* @param {string} url The URL to fetch. * @param {string} url The URL to fetch.
* @returns {Promise<string>} The file base64 data. * @returns {Promise<string>} The file URI.
*/ */
async function getBiliVideoFile(url) { async function getBiliVideoFile(url) {
const axios = await import('axios').then(m => m.default); const axios = await import('axios').then(m => m.default);
@ -106,97 +106,61 @@ async function getBiliVideoFile(url) {
const buffer = toBuffer(response.data); const buffer = toBuffer(response.data);
const encodedData = buffer.toString('base64'); const encodedData = buffer.toString('base64');
return encodedData; return `data:application/octet-stream;base64,${encodedData}`;
} }
/** /**
* Parse the source string (`a, b`) to source list `['a', 'b']`. * Parse the source string (`a, b`) to source list `['a', 'b']`.
* *
* @param {import("@unblockneteasemusic/rust-napi").Executor} executor
* @param {string} sourceString The source string. * @param {string} sourceString The source string.
* @returns {string[]} The source list. * @returns {string[]} The source list.
*/ */
function parseSourceStringToList(executor, sourceString) { function parseSourceStringToList(sourceString) {
const availableSource = executor.list(); return sourceString.split(',').map(s => s.trim());
return sourceString
.split(',')
.map(s => s.trim().toLowerCase())
.filter(s => {
const isAvailable = availableSource.includes(s);
if (!isAvailable) {
log(`This source is not one of the supported source: ${s}`);
}
return isAvailable;
});
} }
export function initIpcMain(win, store, trayEventEmitter) { export function initIpcMain(win, store, trayEventEmitter) {
// WIP: Do not enable logging as it has some issues in non-blocking I/O environment. ipcMain.handle('unblock-music', async (_, track, source) => {
// UNM.enableLogging(UNM.LoggingType.ConsoleEnv); // 兼容 unblockneteasemusic 所使用的 api 字段
const unmExecutor = new UNM.Executor(); track.alias = track.alia || [];
track.duration = track.dt || 0;
track.album = track.al || [];
track.artists = track.ar || [];
ipcMain.handle( const timeoutPromise = new Promise((_, reject) => {
'unblock-music', setTimeout(() => {
/** reject('timeout');
* }, 5000);
* @param {*} _ });
* @param {string | null} sourceListString
* @param {Record<string, any>} ncmTrack
* @param {UNM.Context} context
*/
async (_, sourceListString, ncmTrack, context) => {
// Formt the track input
// FIXME: Figure out the structure of Track
const song = {
id: ncmTrack.id && ncmTrack.id.toString(),
name: ncmTrack.name,
duration: ncmTrack.dt,
album: ncmTrack.al && {
id: ncmTrack.al.id && ncmTrack.al.id.toString(),
name: ncmTrack.al.name,
},
artists: ncmTrack.ar
? ncmTrack.ar.map(({ id, name }) => ({
id: id && id.toString(),
name,
}))
: [],
};
const sourceList = const sourceList =
typeof sourceListString === 'string' typeof source === 'string' ? parseSourceStringToList(source) : null;
? parseSourceStringToList(unmExecutor, sourceListString) log(`[UNM] using source: ${sourceList || '<default>'}`);
: ['ytdl', 'bilibili', 'pyncm', 'kugou'];
log(`[UNM] using source: ${sourceList.join(', ')}`);
log(`[UNM] using configuration: ${JSON.stringify(context)}`);
try { try {
const matchedAudio = await Promise.race([
// TODO: tell users to install yt-dlp. // TODO: tell users to install yt-dlp.
const matchedAudio = await unmExecutor.search( // we passed "null" to source, to let UNM choose the default source.
sourceList, match(track.id, sourceList, track),
song, timeoutPromise,
context ]);
);
const retrievedSong = await unmExecutor.retrieve(matchedAudio, context);
// bilibili's audio file needs some special treatment if (!matchedAudio || !matchedAudio.url) {
if (retrievedSong.url.includes('bilivideo.com')) { throw new Error('no such a song found');
retrievedSong.url = await getBiliVideoFile(retrievedSong.url);
}
log(`respond with retrieve song…`);
log(JSON.stringify(matchedAudio));
return retrievedSong;
} catch (err) {
const errorMessage = err instanceof Error ? `${err.message}` : `${err}`;
log(`UnblockNeteaseMusic failed: ${errorMessage}`);
return null;
} }
// bilibili's audio file needs some special treatment
if (matchedAudio.url.includes('bilivideo.com')) {
matchedAudio.url = await getBiliVideoFile(matchedAudio.url);
}
return matchedAudio;
} catch (err) {
const errorMessage = err instanceof Error ? `${err.message}` : `${err}`;
log(`UnblockNeteaseMusic failed: ${errorMessage}`);
return null;
} }
); });
ipcMain.on('close', e => { ipcMain.on('close', e => {
if (isMac) { if (isMac) {
@ -222,7 +186,9 @@ export function initIpcMain(win, store, trayEventEmitter) {
}); });
ipcMain.on('maximizeOrUnmaximize', () => { ipcMain.on('maximizeOrUnmaximize', () => {
win.isMaximized() ? win.unmaximize() : win.maximize(); const isMaximized = win.isMaximized();
isMaximized ? win.unmaximize() : win.maximize();
win.webContents.send('isMaximized', isMaximized);
}); });
ipcMain.on('settings', (event, options) => { ipcMain.on('settings', (event, options) => {
@ -240,7 +206,7 @@ export function initIpcMain(win, store, trayEventEmitter) {
details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','), details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),
state: track.al.name, state: track.al.name,
endTimestamp: Date.now() + track.dt, endTimestamp: Date.now() + track.dt,
largeImageKey: track.al.picUrl, largeImageKey: 'logo',
largeImageText: 'Listening ' + track.name, largeImageText: 'Listening ' + track.name,
smallImageKey: 'play', smallImageKey: 'play',
smallImageText: 'Playing', smallImageText: 'Playing',
@ -252,7 +218,7 @@ export function initIpcMain(win, store, trayEventEmitter) {
client.updatePresence({ client.updatePresence({
details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','), details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),
state: track.al.name, state: track.al.name,
largeImageKey: track.al.picUrl, largeImageKey: 'logo',
largeImageText: 'YesPlayMusic', largeImageText: 'YesPlayMusic',
smallImageKey: 'pause', smallImageKey: 'pause',
smallImageText: 'Pause', smallImageText: 'Pause',

View file

@ -1,4 +1,3 @@
import dbus from 'dbus-next';
import { ipcMain, app } from 'electron'; import { ipcMain, app } from 'electron';
export function createMpris(window) { export function createMpris(window) {
@ -29,8 +28,6 @@ export function createMpris(window) {
}); });
ipcMain.on('metadata', (e, metadata) => { ipcMain.on('metadata', (e, metadata) => {
// 更新 Mpris 状态前将位置设为0, 否则 OSDLyrics 获取到的进度是上首音乐切换时的进度
player.getPosition = () => 0;
player.metadata = { player.metadata = {
'mpris:trackid': player.objectPath('track/' + metadata.trackId), 'mpris:trackid': player.objectPath('track/' + metadata.trackId),
'mpris:artUrl': metadata.artwork[0].src, 'mpris:artUrl': metadata.artwork[0].src,
@ -38,17 +35,11 @@ export function createMpris(window) {
'xesam:title': metadata.title, 'xesam:title': metadata.title,
'xesam:album': metadata.album, 'xesam:album': metadata.album,
'xesam:artist': metadata.artist.split(','), 'xesam:artist': metadata.artist.split(','),
'xesam:url': metadata.url,
}; };
}); });
ipcMain.on('playerCurrentTrackTime', (e, position) => { ipcMain.on('playerCurrentTrackTime', (e, position) => {
player.getPosition = () => position * 1000 * 1000; player.getPosition = () => position * 1000 * 1000;
player.seeked(position * 1000 * 1000);
});
ipcMain.on('seeked', (e, position) => {
player.seeked(position * 1000 * 1000);
}); });
ipcMain.on('switchRepeatMode', (e, mode) => { ipcMain.on('switchRepeatMode', (e, mode) => {
@ -69,26 +60,3 @@ export function createMpris(window) {
player.shuffle = shuffle; player.shuffle = shuffle;
}); });
} }
export async function createDbus(window) {
const bus = dbus.sessionBus();
const Variant = dbus.Variant;
const osdService = await bus.getProxyObject(
'org.osdlyrics.Daemon',
'/org/osdlyrics/Lyrics'
);
const osdInterface = osdService.getInterface('org.osdlyrics.Lyrics');
ipcMain.on('sendLyrics', async (e, { track, lyrics }) => {
const metadata = {
title: new Variant('s', track.name),
artist: new Variant('s', track.ar.map(ar => ar.name).join(', ')),
};
await osdInterface.SetLyricContent(metadata, Buffer.from(lyrics));
window.webContents.send('saveLyricFinished');
});
}

View file

@ -1,5 +1,4 @@
import clc from 'cli-color'; import clc from 'cli-color';
import checkAuthToken from '../utils/checkAuthToken';
import server from 'NeteaseCloudMusicApi/server'; import server from 'NeteaseCloudMusicApi/server';
export async function startNeteaseMusicApi() { export async function startNeteaseMusicApi() {

View file

@ -1,6 +1,6 @@
/* global __static */ /* global __static */
import path from 'path'; import path from 'path';
import { app, nativeImage, Tray, Menu, nativeTheme } from 'electron'; import { app, nativeImage, Tray, Menu } from 'electron';
import { isLinux } from '@/utils/platform'; import { isLinux } from '@/utils/platform';
function createMenuTemplate(win) { function createMenuTemplate(win) {
@ -97,10 +97,6 @@ function createMenuTemplate(win) {
// click在默认行为下会弹出一个contextMenu里面的唯一选项才会调用click事件 // click在默认行为下会弹出一个contextMenu里面的唯一选项才会调用click事件
// setContextMenu应该是目前唯一能在linux下使用托盘菜单api // setContextMenu应该是目前唯一能在linux下使用托盘菜单api
// 但是无法区分鼠标左右键 // 但是无法区分鼠标左右键
// 发现openSUSE KDE环境可以区分鼠标左右键
// 添加左键支持
// 2022.05.17
class YPMTrayLinuxImpl { class YPMTrayLinuxImpl {
constructor(tray, win, emitter) { constructor(tray, win, emitter) {
this.tray = tray; this.tray = tray;
@ -131,10 +127,6 @@ class YPMTrayLinuxImpl {
} }
handleEvents() { handleEvents() {
this.tray.on('click', () => {
this.win.show();
});
this.emitter.on('updateTooltip', title => this.tray.setToolTip(title)); this.emitter.on('updateTooltip', title => this.tray.setToolTip(title));
this.emitter.on('updatePlayState', isPlaying => { this.emitter.on('updatePlayState', isPlaying => {
this.contextMenu.getMenuItemById('play').visible = !isPlaying; this.contextMenu.getMenuItemById('play').visible = !isPlaying;
@ -197,11 +189,8 @@ class YPMTrayWindowsImpl {
} }
export function createTray(win, eventEmitter) { export function createTray(win, eventEmitter) {
// 感觉图标颜色应该不属于界面主题范畴,只需要跟随系统主题
let iconTheme = nativeTheme.shouldUseDarkColors ? 'light' : 'dark';
let icon = nativeImage let icon = nativeImage
.createFromPath(path.join(__static, `img/icons/menu-${iconTheme}@88.png`)) .createFromPath(path.join(__static, 'img/icons/menu@88.png'))
.resize({ .resize({
height: 20, height: 20,
width: 20, width: 20,

View file

@ -28,14 +28,7 @@ export default {
albums: 'Albums', albums: 'Albums',
artists: 'Artists', artists: 'Artists',
mvs: 'MVs', mvs: 'MVs',
cloudDisk: 'Cloud Disk',
newPlayList: 'New Playlist', newPlayList: 'New Playlist',
uploadSongs: 'Upload Songs',
playHistory: {
title: 'Play History',
week: 'Latest Week',
all: 'All Time',
},
userProfileMenu: { userProfileMenu: {
settings: 'Settings', settings: 'Settings',
logout: 'Logout', logout: 'Logout',
@ -102,7 +95,6 @@ export default {
}, },
player: { player: {
like: 'Like', like: 'Like',
unlike: 'Unlike',
previous: 'Previous Song', previous: 'Previous Song',
next: 'Next Song', next: 'Next Song',
repeat: 'Repeat', repeat: 'Repeat',
@ -113,8 +105,6 @@ export default {
pause: 'Pause', pause: 'Pause',
mute: 'Mute', mute: 'Mute',
nextUp: 'Next Up', nextUp: 'Next Up',
translationLyric: 'lyric (trans)',
PronunciationLyric: 'lyric (pronounce)',
}, },
modal: { modal: {
close: 'Close', close: 'Close',
@ -132,17 +122,6 @@ export default {
settings: 'Settings', settings: 'Settings',
logout: 'LOGOUT', logout: 'LOGOUT',
language: 'Languages', language: 'Languages',
lyric: 'Lyric',
others: 'Others',
customization: 'Customization',
MusicGenrePreference: {
text: 'Music Language Preference',
none: 'No preferences',
mandarin: 'Mandarin',
western: 'Europe & America',
korean: 'Korean',
japanese: 'Japanese',
},
musicQuality: { musicQuality: {
text: 'Music Quality', text: 'Music Quality',
low: 'Low', low: 'Low',
@ -179,8 +158,6 @@ export default {
showLibraryDefault: 'Show Library after App Launched', showLibraryDefault: 'Show Library after App Launched',
subTitleDefault: 'Show Alias for Subtitle by default', subTitleDefault: 'Show Alias for Subtitle by default',
enableReversedMode: 'Enable Reversed Mode (Experimental)', enableReversedMode: 'Enable Reversed Mode (Experimental)',
enableCustomTitlebar: 'Enable custom title bar (Need restart)',
showLyricsTime: 'Display current time',
lyricsBackground: { lyricsBackground: {
text: 'Show Lyrics Background', text: 'Show Lyrics Background',
off: 'Off', off: 'Off',
@ -193,41 +170,6 @@ export default {
exit: 'Exit', exit: 'Exit',
minimizeToTray: 'Minimize to tray', minimizeToTray: 'Minimize to tray',
}, },
enableOsdlyricsSupport: {
title: 'desktop lyrics support',
desc1:
'Only takes effect under Linux. After enabled, it downloads the lyrics file to the local, and tries to launch OSDLyrics at startup.',
desc2:
'Please ensure that you have installed OSDLyrics before turning on this.',
},
unm: {
enable: 'Enable',
audioSource: {
title: 'Audio Sources',
},
enableFlac: {
title: 'Enable FLAC Sources',
desc: 'To take effect, it may be required to clear the cache after enabling this function.',
},
searchMode: {
title: 'Audio Search Mode',
fast: 'Speed Priority',
order: 'Order Priority',
},
cookie: {
joox: 'Cookie for Joox use',
qq: 'Cookie for QQ use',
desc1: 'Click here for the configuration instruction. ',
desc2: 'Leave empty to pick up the default value',
},
ytdl: 'The youtube-dl Executable File for YtDl',
proxy: {
title: 'Proxy Server for UNM',
desc1:
'The proxy server to use for requesting services such as YouTube',
desc2: 'Leave empty to pick up the default value',
},
},
}, },
contextMenu: { contextMenu: {
play: 'Play', play: 'Play',
@ -243,9 +185,6 @@ export default {
allPlaylists: 'All Playlists', allPlaylists: 'All Playlists',
minePlaylists: 'My Playlists', minePlaylists: 'My Playlists',
likedPlaylists: 'Liked Playlists', likedPlaylists: 'Liked Playlists',
cardiacMode: 'Cardiac Mode',
copyLyric: 'Copy Lyric',
copyLyricWithTranslation: 'Copy Lyric With Translation',
}, },
toast: { toast: {
savedToPlaylist: 'Saved to playlist', savedToPlaylist: 'Saved to playlist',

View file

@ -28,14 +28,7 @@ export default {
albums: 'Albümler', albums: 'Albümler',
artists: 'Sanatçılar', artists: 'Sanatçılar',
mvs: 'MVs', mvs: 'MVs',
cloudDisk: 'Cloud Disk',
newPlayList: 'Yeni Çalma Listesi', newPlayList: 'Yeni Çalma Listesi',
uploadSongs: 'Upload Songs',
playHistory: {
title: 'Play History',
week: 'Latest Week',
all: 'All Time',
},
userProfileMenu: { userProfileMenu: {
settings: 'Ayarlar', settings: 'Ayarlar',
logout: ıkış Yap', logout: ıkış Yap',
@ -98,7 +91,6 @@ export default {
}, },
player: { player: {
like: 'Beğen', like: 'Beğen',
unlike: 'Aksine',
previous: 'Önceki Müzik', previous: 'Önceki Müzik',
next: 'Sonraki Müzik', next: 'Sonraki Müzik',
repeat: 'Tekrarla', repeat: 'Tekrarla',
@ -108,8 +100,6 @@ export default {
pause: 'Durdur', pause: 'Durdur',
mute: 'Sesi kapat', mute: 'Sesi kapat',
nextUp: 'Sıradaki', nextUp: 'Sıradaki',
translationLyric: 'şarkı sözleri (çeviri)',
PronunciationLyric: 'şarkı sözleri (çeviri)',
}, },
modal: { modal: {
close: 'Kapat', close: 'Kapat',
@ -127,17 +117,6 @@ export default {
settings: 'Ayarlar', settings: 'Ayarlar',
logout: 'ÇIKIŞ YAP', logout: 'ÇIKIŞ YAP',
language: 'Diller', language: 'Diller',
lyric: 'Şarkı Sözleri',
others: 'Diğerleri',
customization: 'Özelleştirme',
MusicGenrePreference: {
text: 'Müzik Dili Tercihi',
none: 'Tercih yok',
mandarin: 'Çince dili',
western: 'Avrupa ve Amerika',
korean: 'Korece',
japanese: 'Japonca',
},
musicQuality: { musicQuality: {
text: 'Müzik Kalitesi', text: 'Müzik Kalitesi',
low: 'Düşük', low: 'Düşük',
@ -171,9 +150,7 @@ export default {
showPlaylistsByAppleMusic: "Apple Music'in Çalma Listelerini Göster", showPlaylistsByAppleMusic: "Apple Music'in Çalma Listelerini Göster",
enableDiscordRichPresence: 'Discord gösterimini aktifleştir', enableDiscordRichPresence: 'Discord gösterimini aktifleştir',
showLibraryDefault: 'Kitaplık Varsayılanını göster', showLibraryDefault: 'Kitaplık Varsayılanını göster',
subTitleDefault: 'Show Alias for Subtitle by default', subTitleDefault: 'Sub title alia default',
enableReversedMode: 'Enable Reversed Mode (Experimental)',
enableCustomTitlebar: 'Enable custom title bar (Need restart)',
lyricsBackground: { lyricsBackground: {
text: 'Şarkı Sözleri Arka Planını Göster', text: 'Şarkı Sözleri Arka Planını Göster',
off: 'kapalı', off: 'kapalı',
@ -186,52 +163,12 @@ export default {
exit: 'Exit', exit: 'Exit',
minimizeToTray: 'Küçült', minimizeToTray: 'Küçült',
}, },
unm: {
enable: 'Enable',
audioSource: {
title: 'Audio Sources',
},
enableFlac: {
title: 'Enable FLAC Sources',
desc: 'To take effect, it may be required to clear the cache after enabling this function.',
},
searchMode: {
title: 'Audio Search Mode',
fast: 'Speed Priority',
order: 'Order Priority',
},
cookie: {
joox: 'Cookie for Joox use',
qq: 'Cookie for QQ use',
desc1: 'Click here for the configuration instruction. ',
desc2: 'Leave empty to pick up the default value',
},
ytdl: 'The youtube-dl Executable File for YtDl',
proxy: {
title: 'Proxy Server for UNM',
desc1:
'The proxy server to use for requesting services such as YouTube',
desc2: 'Leave empty to pick up the default value',
},
},
}, },
contextMenu: { contextMenu: {
play: 'Oynat', play: 'Oynat',
addToQueue: 'Sonrakini Oynat', addToQueue: 'Sonrakini Oynat',
saveToMyLikedSongs: 'Beğendiğim Müziklere Kaydet', saveToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',
removeFromMyLikedMüzikler: 'Beğendiğim Müziklerden Kaldır', removeFromMyLikedMüzikler: 'Beğendiğim Müziklerden Kaldır',
saveToLibrary: 'Save to library',
removeFromLibrary: 'Remove from library',
addToPlaylist: 'Add to playlist',
searchInPlaylist: 'Search in playlist',
copyUrl: 'Copy URL',
openInBrowser: 'Open in Browser',
allPlaylists: 'All Playlists',
minePlaylists: 'My Playlists',
likedPlaylists: 'Liked Playlists',
cardiacMode: 'Cardiac Mode',
copyLyric: 'Copy Lyric',
copyLyricWithTranslation: 'Copy Lyric With Translation',
}, },
toast: { toast: {
savedToMyLikedSongs: 'Beğendiğim Müziklere Kaydet', savedToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',

View file

@ -25,14 +25,7 @@ export default {
albums: '专辑', albums: '专辑',
artists: '艺人', artists: '艺人',
mvs: 'MV', mvs: 'MV',
cloudDisk: '云盘',
newPlayList: '新建歌单', newPlayList: '新建歌单',
uploadSongs: '上传歌曲',
playHistory: {
title: '听歌排行',
week: '最近一周',
all: '所有时间',
},
userProfileMenu: { userProfileMenu: {
settings: '设置', settings: '设置',
logout: '登出', logout: '登出',
@ -103,7 +96,6 @@ export default {
}, },
player: { player: {
like: '喜欢', like: '喜欢',
unlike: '取消喜欢',
previous: '上一首', previous: '上一首',
next: '下一首', next: '下一首',
repeat: '循环播放', repeat: '循环播放',
@ -114,8 +106,6 @@ export default {
pause: '暂停', pause: '暂停',
mute: '静音', mute: '静音',
nextUp: '播放列表', nextUp: '播放列表',
translationLyric: '歌词(译)',
PronunciationLyric: '歌词(音)',
}, },
modal: { modal: {
close: '关闭', close: '关闭',
@ -133,17 +123,6 @@ export default {
settings: '设置', settings: '设置',
logout: '登出', logout: '登出',
language: '语言', language: '语言',
lyric: '歌词',
others: '其他',
customization: '自定义',
MusicGenrePreference: {
text: '音乐语种偏好',
none: '无偏好',
mandarin: '华语',
western: '欧美',
korean: '韩语',
japanese: '日语',
},
musicQuality: { musicQuality: {
text: '音质选择', text: '音质选择',
low: '普通', low: '普通',
@ -180,53 +159,18 @@ export default {
showLibraryDefault: '启动后显示音乐库', showLibraryDefault: '启动后显示音乐库',
subTitleDefault: '副标题使用别名', subTitleDefault: '副标题使用别名',
enableReversedMode: '启用倒序播放功能 (实验性功能)', enableReversedMode: '启用倒序播放功能 (实验性功能)',
enableCustomTitlebar: '启用自定义标题栏 (重启后生效)',
lyricsBackground: { lyricsBackground: {
text: '显示歌词背景', text: '显示歌词背景',
off: '关闭', off: '关闭',
on: '打开', on: '打开',
dynamic: '动态GPU 占用较高)', dynamic: '动态GPU 占用较高)',
}, },
showLyricsTime: '显示当前时间',
closeAppOption: { closeAppOption: {
text: '关闭主面板时...', text: '关闭主面板时...',
ask: '询问', ask: '询问',
exit: '退出', exit: '退出',
minimizeToTray: '最小化到托盘', minimizeToTray: '最小化到托盘',
}, },
enableOsdlyricsSupport: {
title: '桌面歌词支持',
desc1:
'仅 Linux 下生效。启用后会将歌词文件下载到本地,并在开启播放器时尝试拉起 OSDLyrics。',
desc2: '请在开启之前确保您已经正确安装了 OSDLyrics。',
},
unm: {
enable: '启用',
audioSource: {
title: '备选音源',
},
enableFlac: {
title: '启用 FLAC',
desc: '启用后需要清除歌曲缓存才能生效',
},
searchMode: {
title: '音源搜索模式',
fast: '速度优先',
order: '顺序优先',
},
cookie: {
joox: 'Joox 引擎的 Cookie',
qq: 'QQ 引擎的 Cookie',
desc1: '设置说明请参见此处',
desc2: ',留空则不进行相关设置',
},
ytdl: 'YtDl 引擎要使用的 youtube-dl 可执行文件',
proxy: {
title: '用于 UNM 的代理服务器',
desc1: '请求如 YouTube 音源服务时要使用的代理服务器',
desc2: '留空则不进行相关设置',
},
},
}, },
contextMenu: { contextMenu: {
play: '播放', play: '播放',
@ -242,9 +186,6 @@ export default {
allPlaylists: '全部歌单', allPlaylists: '全部歌单',
minePlaylists: '创建的歌单', minePlaylists: '创建的歌单',
likedPlaylists: '收藏的歌单', likedPlaylists: '收藏的歌单',
cardiacMode: '心动模式',
copyLyric: '复制歌词',
copyLyricWithTranslation: '复制歌词(含翻译)',
}, },
toast: { toast: {
savedToPlaylist: '已添加到歌单', savedToPlaylist: '已添加到歌单',

View file

@ -25,14 +25,7 @@ export default {
albums: '專輯', albums: '專輯',
artists: '藝人', artists: '藝人',
mvs: 'MV', mvs: 'MV',
cloudDisk: '雲端硬碟',
newPlayList: '新增歌單', newPlayList: '新增歌單',
uploadSongs: '上傳音樂',
playHistory: {
title: '聽歌排行',
week: '最近一周',
all: '所有時間',
},
userProfileMenu: { userProfileMenu: {
settings: '設定', settings: '設定',
logout: '登出', logout: '登出',
@ -99,7 +92,6 @@ export default {
}, },
player: { player: {
like: '喜歡', like: '喜歡',
unlike: '取消喜歡',
previous: '上一首', previous: '上一首',
next: '下一首', next: '下一首',
repeat: '循環播放', repeat: '循環播放',
@ -110,8 +102,6 @@ export default {
pause: '暫停', pause: '暫停',
mute: '靜音', mute: '靜音',
nextUp: '播放清單', nextUp: '播放清單',
translationLyric: '歌詞(譯)',
PronunciationLyric: '歌詞(音)',
}, },
modal: { modal: {
close: '關閉', close: '關閉',
@ -129,17 +119,6 @@ export default {
settings: '設定', settings: '設定',
logout: '登出', logout: '登出',
language: '語言', language: '語言',
lyric: '歌詞',
others: '其他',
customization: '自訂',
MusicGenrePreference: {
text: '音樂語種偏好',
none: '無偏好',
mandarin: '華語',
western: '歐美',
korean: '韓語',
japanese: '日語',
},
musicQuality: { musicQuality: {
text: '音質選擇', text: '音質選擇',
low: '普通', low: '普通',
@ -177,8 +156,6 @@ export default {
showLibraryDefault: '啟動後顯示音樂庫', showLibraryDefault: '啟動後顯示音樂庫',
subTitleDefault: '副標題使用別名', subTitleDefault: '副標題使用別名',
enableReversedMode: '啟用倒序播放功能 (實驗性功能)', enableReversedMode: '啟用倒序播放功能 (實驗性功能)',
enableCustomTitlebar: '啟用自訂標題列(重新啟動後生效)',
showLyricsTime: '顯示目前時間',
lyricsBackground: { lyricsBackground: {
text: '顯示歌詞背景', text: '顯示歌詞背景',
off: '關閉', off: '關閉',
@ -191,39 +168,6 @@ export default {
exit: '退出', exit: '退出',
minimizeToTray: '最小化到工作列角落', minimizeToTray: '最小化到工作列角落',
}, },
enableOsdlyricsSupport: {
title: '桌面歌詞支援',
desc1:
'只在 Linux 環境下生效。啟用後會將歌詞檔案下載至本機位置,並在開啟播放器時嘗試連帶啟動 OSDLyrics。',
desc2: '請在開啟之前確保您已經正確安裝了 OSDLyrics。',
},
unm: {
enable: '啟用',
audioSource: {
title: '備選音源',
},
enableFlac: {
title: '啟用 FLAC',
desc: '啟用後需要清除歌曲快取才能生效',
},
searchMode: {
title: '音源搜尋模式',
fast: '速度優先',
order: '順序優先',
},
cookie: {
joox: 'Joox 引擎的 Cookie',
qq: 'QQ 引擎的 Cookie',
desc1: '設定說明請參見此處',
desc2: ',留空則不進行相關設定',
},
ytdl: 'YtDl 引擎要使用的 youtube-dl 執行檔',
proxy: {
title: '用於 UNM 的 Proxy 伺服器',
desc1: '請求如 YouTube 音源服務時要使用的 Proxy 伺服器',
desc2: '留空則不進行相關設定',
},
},
}, },
contextMenu: { contextMenu: {
play: '播放', play: '播放',
@ -239,9 +183,6 @@ export default {
allPlaylists: '全部歌單', allPlaylists: '全部歌單',
minePlaylists: '我建立的歌單', minePlaylists: '我建立的歌單',
likedPlaylists: '收藏的歌單', likedPlaylists: '收藏的歌單',
cardiacMode: '心動模式',
copyLyric: '複製歌詞',
copyLyricWithTranslation: '複製歌詞(含翻譯)',
}, },
toast: { toast: {
savedToPlaylist: '已新增至歌單', savedToPlaylist: '已新增至歌單',

View file

@ -1,5 +1,5 @@
import Vue from 'vue'; import Vue from 'vue';
import VueGtag from 'vue-gtag'; import VueAnalytics from 'vue-analytics';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import store from './store'; import store from './store';
@ -7,6 +7,7 @@ import i18n from '@/locale';
import '@/assets/icons'; import '@/assets/icons';
import '@/utils/filters'; import '@/utils/filters';
import './registerServiceWorker'; import './registerServiceWorker';
import { dailyTask } from '@/utils/common';
import '@/assets/css/global.scss'; import '@/assets/css/global.scss';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import '@/assets/css/nprogress.css'; import '@/assets/css/nprogress.css';
@ -27,16 +28,14 @@ console.log(
'background:unset;color:unset;' 'background:unset;color:unset;'
); );
Vue.use( Vue.use(VueAnalytics, {
VueGtag, id: 'UA-180189423-1',
{ router,
config: { id: 'G-KMJJCFZDKF' }, });
},
router
);
Vue.config.productionTip = false; Vue.config.productionTip = false;
NProgress.configure({ showSpinner: false, trickleSpeed: 100 }); NProgress.configure({ showSpinner: false, trickleSpeed: 100 });
dailyTask();
new Vue({ new Vue({
i18n, i18n,

View file

@ -23,21 +23,17 @@ let localStorage = {
nyancatStyle: false, nyancatStyle: false,
showLyricsTranslation: true, showLyricsTranslation: true,
lyricsBackground: true, lyricsBackground: true,
enableOsdlyricsSupport: false,
closeAppOption: 'ask', closeAppOption: 'ask',
enableDiscordRichPresence: false, enableDiscordRichPresence: false,
enableGlobalShortcut: true, enableGlobalShortcut: true,
showLibraryDefault: false, showLibraryDefault: false,
subTitleDefault: false, subTitleDefault: false,
linuxEnableCustomTitlebar: false,
enabledPlaylistCategories, enabledPlaylistCategories,
proxyConfig: { proxyConfig: {
protocol: 'noProxy', protocol: 'noProxy',
server: '', server: '',
port: null, port: null,
}, },
enableRealIP: false,
realIP: null,
shortcuts: shortcuts, shortcuts: shortcuts,
}, },
data: { data: {

View file

@ -31,8 +31,9 @@ export default {
c => c === name c => c === name
); );
if (index !== -1) { if (index !== -1) {
state.settings.enabledPlaylistCategories = state.settings.enabledPlaylistCategories = state.settings.enabledPlaylistCategories.filter(
state.settings.enabledPlaylistCategories.filter(c => c !== name); c => c !== name
);
} else { } else {
state.settings.enabledPlaylistCategories.push(name); state.settings.enabledPlaylistCategories.push(name);
} }
@ -72,7 +73,4 @@ export default {
enableScrolling(state, status = null) { enableScrolling(state, status = null) {
state.enableScrolling = status ? status : !state.enableScrolling; state.enableScrolling = status ? status : !state.enableScrolling;
}, },
updateTitle(state, title) {
state.title = title;
},
}; };

View file

@ -13,7 +13,6 @@ updateApp();
export default { export default {
showLyrics: false, showLyrics: false,
enableScrolling: true, enableScrolling: true,
title: 'YesPlayMusic',
liked: { liked: {
songs: [], songs: [],
songsWithDetails: [], // 只有前12首 songsWithDetails: [], // 只有前12首
@ -22,10 +21,6 @@ export default {
artists: [], artists: [],
mvs: [], mvs: [],
cloudDisk: [], cloudDisk: [],
playHistory: {
weekData: [],
allData: [],
},
}, },
contextMenu: { contextMenu: {
clickObjectID: 0, clickObjectID: 0,

View file

@ -1,29 +1,15 @@
import { getTrackDetail, scrobble, getMP3 } from '@/api/track';
import shuffle from 'lodash/shuffle';
import { Howler, Howl } from 'howler';
import { cacheTrackSource, getTrackSource } from '@/utils/db';
import { getAlbum } from '@/api/album'; import { getAlbum } from '@/api/album';
import { getArtist } from '@/api/artist';
import { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm';
import { fmTrash, personalFM } from '@/api/others';
import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist'; import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
import { getLyric, getMP3, getTrackDetail } from '@/api/track'; import { getArtist } from '@/api/artist';
import { personalFM, fmTrash } from '@/api/others';
import store from '@/store'; import store from '@/store';
import { isAccountLoggedIn } from '@/utils/auth'; import { isAccountLoggedIn } from '@/utils/auth';
import { cacheTrackSource, getTrackSource } from '@/utils/db'; import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm';
import { isCreateMpris, isCreateTray } from '@/utils/platform'; import { isCreateMpris, isCreateTray } from '@/utils/platform';
import { Howl, Howler } from 'howler';
import shuffle from 'lodash/shuffle';
import { decode as base642Buffer } from '@/utils/base64';
const PLAY_PAUSE_FADE_DURATION = 200;
const INDEX_IN_PLAY_NEXT = -1;
/**
* @readonly
* @enum {string}
*/
const UNPLAYABLE_CONDITION = {
PLAY_NEXT_TRACK: 'playNextTrack',
PLAY_PREV_TRACK: 'playPrevTrack',
};
const electron = const electron =
process.env.IS_ELECTRON === true ? window.require('electron') : null; process.env.IS_ELECTRON === true ? window.require('electron') : null;
@ -46,14 +32,13 @@ function setTitle(track) {
? `${track.name} · ${track.ar[0].name} - YesPlayMusic` ? `${track.name} · ${track.ar[0].name} - YesPlayMusic`
: 'YesPlayMusic'; : 'YesPlayMusic';
if (isCreateTray) { if (isCreateTray) {
ipcRenderer?.send('updateTrayTooltip', document.title); ipcRenderer.send('updateTrayTooltip', document.title);
} }
store.commit('updateTitle', document.title);
} }
function setTrayLikeState(isLiked) { function setTrayLikeState(isLiked) {
if (isCreateTray) { if (isCreateTray) {
ipcRenderer?.send('updateTrayLikeState', isLiked); ipcRenderer.send('updateTrayLikeState', isLiked);
} }
} }
@ -81,9 +66,7 @@ export default class {
this._playNextList = []; // 当这个list不为空时会优先播放这个list的歌 this._playNextList = []; // 当这个list不为空时会优先播放这个list的歌
this._isPersonalFM = false; // 是否是私人FM模式 this._isPersonalFM = false; // 是否是私人FM模式
this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲 this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲
this._personalFMNextTrack = { this._personalFMNextTrack = { id: 0 }; // 私人FM下一首歌曲信息为了快速加载下一首
id: 0,
}; // 私人FM下一首歌曲信息为了快速加载下一首
/** /**
* The blob records for cleanup. * The blob records for cleanup.
@ -130,8 +113,6 @@ export default class {
if (shuffle) { if (shuffle) {
this._shuffleTheList(); this._shuffleTheList();
} }
// 同步当前歌曲在列表中的下标
this.current = this.list.indexOf(this.currentTrackID);
} }
get reversed() { get reversed() {
return this._reversed; return this._reversed;
@ -150,7 +131,7 @@ export default class {
} }
set volume(volume) { set volume(volume) {
this._volume = volume; this._volume = volume;
this._howler?.volume(volume); Howler.volume(volume);
} }
get list() { get list() {
return this.shuffle ? this._shuffledList : this._list; return this.shuffle ? this._shuffledList : this._list;
@ -177,9 +158,6 @@ export default class {
get currentTrack() { get currentTrack() {
return this._currentTrack; return this._currentTrack;
} }
get currentTrackID() {
return this._currentTrack?.id ?? 0;
}
get playlistSource() { get playlistSource() {
return this._playlistSource; return this._playlistSource;
} }
@ -203,9 +181,6 @@ export default class {
set progress(value) { set progress(value) {
if (this._howler) { if (this._howler) {
this._howler.seek(value); this._howler.seek(value);
if (isCreateMpris) {
ipcRenderer?.send('seeked', this._howler.seek());
}
} }
} }
get isCurrentTrackLiked() { get isCurrentTrackLiked() {
@ -214,11 +189,13 @@ export default class {
_init() { _init() {
this._loadSelfFromLocalStorage(); this._loadSelfFromLocalStorage();
this._howler?.volume(this.volume); Howler.autoUnlock = false;
Howler.usingWebAudio = true;
Howler.volume(this.volume);
if (this._enabled) { if (this._enabled) {
// 恢复当前播放歌曲 // 恢复当前播放歌曲
this._replaceCurrentTrack(this.currentTrackID, false).then(() => { this._replaceCurrentTrack(this._currentTrack.id, false).then(() => {
this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0); this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0);
}); // update audio source and init howler }); // update audio source and init howler
this._initMediaSession(); this._initMediaSession();
@ -242,19 +219,18 @@ export default class {
_setPlaying(isPlaying) { _setPlaying(isPlaying) {
this._playing = isPlaying; this._playing = isPlaying;
if (isCreateTray) { if (isCreateTray) {
ipcRenderer?.send('updateTrayPlayState', this._playing); ipcRenderer.send('updateTrayPlayState', this._playing);
} }
} }
_setIntervals() { _setIntervals() {
// 同步播放进度 // 同步播放进度
// TODO: 如果 _progress 在别的地方被改变了, // TODO: 如果 _progress 在别的地方被改变了这个定时器会覆盖之前改变的值是bug
// 这个定时器会覆盖之前改变的值是bug
setInterval(() => { setInterval(() => {
if (this._howler === null) return; if (this._howler === null) return;
this._progress = this._howler.seek(); this._progress = this._howler.seek();
localStorage.setItem('playerCurrentTrackTime', this._progress); localStorage.setItem('playerCurrentTrackTime', this._progress);
if (isCreateMpris) { if (isCreateMpris) {
ipcRenderer?.send('playerCurrentTrackTime', this._progress); ipcRenderer.send('playerCurrentTrackTime', this._progress);
} }
}, 1000); }, 1000);
} }
@ -262,8 +238,8 @@ export default class {
const next = this._reversed ? this.current - 1 : this.current + 1; const next = this._reversed ? this.current - 1 : this.current + 1;
if (this._playNextList.length > 0) { if (this._playNextList.length > 0) {
let trackID = this._playNextList[0]; let trackID = this._playNextList.shift();
return [trackID, INDEX_IN_PLAY_NEXT]; return [trackID, this.current];
} }
// 循环模式开启,则重新播放当前模式下的相对的下一首 // 循环模式开启,则重新播放当前模式下的相对的下一首
@ -297,7 +273,7 @@ export default class {
// 返回 [trackID, index] // 返回 [trackID, index]
return [this.list[next], next]; return [this.list[next], next];
} }
async _shuffleTheList(firstTrackID = this.currentTrackID) { async _shuffleTheList(firstTrackID = this._currentTrack.id) {
let list = this._list.filter(tid => tid !== firstTrackID); let list = this._list.filter(tid => tid !== firstTrackID);
if (firstTrackID === 'first') list = this._list; if (firstTrackID === 'first') list = this._list;
this._shuffledList = shuffle(list); this._shuffledList = shuffle(list);
@ -309,6 +285,11 @@ export default class {
); );
const trackDuration = ~~(track.dt / 1000); const trackDuration = ~~(track.dt / 1000);
time = completed ? trackDuration : ~~time; time = completed ? trackDuration : ~~time;
scrobble({
id: track.id,
sourceid: this.playlistSource.id,
time,
});
if ( if (
store.state.lastfm.key !== undefined && store.state.lastfm.key !== undefined &&
(time >= trackDuration / 2 || time >= 240) (time >= trackDuration / 2 || time >= 240)
@ -329,35 +310,11 @@ export default class {
this._howler = new Howl({ this._howler = new Howl({
src: [source], src: [source],
html5: true, html5: true,
preload: true,
format: ['mp3', 'flac'], format: ['mp3', 'flac'],
onend: () => { onend: () => {
this._nextTrackCallback(); this._nextTrackCallback();
}, },
}); });
this._howler.on('loaderror', (_, errCode) => {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
// code 3: MEDIA_ERR_DECODE
if (errCode === 3) {
this._playNextTrack(this._isPersonalFM);
} else if (errCode === 4) {
// code 4: MEDIA_ERR_SRC_NOT_SUPPORTED
store.dispatch('showToast', `无法播放: 不支持的音频格式`);
this._playNextTrack(this._isPersonalFM);
} else {
const t = this.progress;
this._replaceCurrentTrackAudio(this.currentTrack, false, false).then(
replaced => {
// 如果 replaced 为 false代表当前的 track 已经不是这里想要替换的track
// 此时则不修改当前的歌曲进度
if (replaced) {
this._howler?.seek(t);
this.play();
}
}
);
}
});
if (autoplay) { if (autoplay) {
this.play(); this.play();
if (this._currentTrack.name) { if (this._currentTrack.name) {
@ -367,27 +324,25 @@ export default class {
} }
this.setOutputDevice(); this.setOutputDevice();
} }
_getAudioSourceBlobURL(data) {
// Create a new object URL.
const source = URL.createObjectURL(new Blob([data]));
// Clean up the previous object URLs since we've created a new one.
// Revoke object URLs can release the memory taken by a Blob,
// which occupied a large proportion of memory.
for (const url in this.createdBlobRecords) {
URL.revokeObjectURL(url);
}
// Then, we replace the createBlobRecords with new one with
// our newly created object URL.
this.createdBlobRecords = [source];
return source;
}
_getAudioSourceFromCache(id) { _getAudioSourceFromCache(id) {
return getTrackSource(id).then(t => { return getTrackSource(id).then(t => {
if (!t) return null; if (!t) return null;
return this._getAudioSourceBlobURL(t.source);
// Create a new object URL.
const source = URL.createObjectURL(new Blob([t.source]));
// Clean up the previous object URLs since we've created a new one.
// Revoke object URLs can release the memory taken by a Blob,
// which occupied a large proportion of memory.
for (const url in this.createdBlobRecords) {
URL.revokeObjectURL(url);
}
// Then, we replace the createBlobRecords with new one with
// our newly created object URL.
this.createdBlobRecords = [source];
return source;
}); });
} }
_getAudioSourceFromNetease(track) { _getAudioSourceFromNetease(track) {
@ -410,71 +365,22 @@ export default class {
} }
async _getAudioSourceFromUnblockMusic(track) { async _getAudioSourceFromUnblockMusic(track) {
console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`); console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`);
if ( if (
process.env.IS_ELECTRON !== true || process.env.IS_ELECTRON !== true ||
store.state.settings.enableUnblockNeteaseMusic === false store.state.settings.enableUnblockNeteaseMusic === false
) { ) {
return null; return null;
} }
const source = await ipcRenderer.invoke(
/**
*
* @param {string=} searchMode
* @returns {import("@unblockneteasemusic/rust-napi").SearchMode}
*/
const determineSearchMode = searchMode => {
/**
* FastFirst = 0
* OrderFirst = 1
*/
switch (searchMode) {
case 'fast-first':
return 0;
case 'order-first':
return 1;
default:
return 0;
}
};
const retrieveSongInfo = await ipcRenderer.invoke(
'unblock-music', 'unblock-music',
store.state.settings.unmSource,
track, track,
{ store.state.settings.unmSource
enableFlac: store.state.settings.unmEnableFlac || null,
proxyUri: store.state.settings.unmProxyUri || null,
searchMode: determineSearchMode(store.state.settings.unmSearchMode),
config: {
'joox:cookie': store.state.settings.unmJooxCookie || null,
'qq:cookie': store.state.settings.unmQQCookie || null,
'ytdl:exe': store.state.settings.unmYtDlExe || null,
},
}
); );
if (store.state.settings.automaticallyCacheSongs && source?.url) {
if (store.state.settings.automaticallyCacheSongs && retrieveSongInfo?.url) { // TODO: 将unblockMusic字样换成真正的来源比如酷我咪咕等
// 对于来自 bilibili 的音源 cacheTrackSource(track, source.url, 128000, 'unblockMusic');
// retrieveSongInfo.url 是音频数据的base64编码
// 其他音源为实际url
const url =
retrieveSongInfo.source === 'bilibili'
? `data:application/octet-stream;base64,${retrieveSongInfo.url}`
: retrieveSongInfo.url;
cacheTrackSource(track, url, 128000, `unm:${retrieveSongInfo.source}`);
} }
return source?.url;
if (!retrieveSongInfo) {
return null;
}
if (retrieveSongInfo.source !== 'bilibili') {
return retrieveSongInfo.url;
}
const buffer = base642Buffer(retrieveSongInfo.url);
return this._getAudioSourceBlobURL(buffer);
} }
_getAudioSource(track) { _getAudioSource(track) {
return this._getAudioSourceFromCache(String(track.id)) return this._getAudioSourceFromCache(String(track.id))
@ -488,61 +394,33 @@ export default class {
_replaceCurrentTrack( _replaceCurrentTrack(
id, id,
autoplay = true, autoplay = true,
ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK ifUnplayableThen = 'playNextTrack'
) { ) {
if (autoplay && this._currentTrack.name) { if (autoplay && this._currentTrack.name) {
this._scrobble(this.currentTrack, this._howler?.seek()); this._scrobble(this.currentTrack, this._howler?.seek());
} }
return getTrackDetail(id).then(data => { return getTrackDetail(id).then(data => {
const track = data.songs[0]; let track = data.songs[0];
this._currentTrack = track; this._currentTrack = track;
this._updateMediaSessionMetaData(track); this._updateMediaSessionMetaData(track);
return this._replaceCurrentTrackAudio( return this._getAudioSource(track).then(source => {
track, if (source) {
autoplay,
true,
ifUnplayableThen
);
});
}
/**
* @returns 是否成功加载音频并使用加载完成的音频替换了howler实例
*/
_replaceCurrentTrackAudio(
track,
autoplay,
isCacheNextTrack,
ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK
) {
return this._getAudioSource(track).then(source => {
if (source) {
let replaced = false;
if (track.id === this.currentTrackID) {
this._playAudioSource(source, autoplay); this._playAudioSource(source, autoplay);
replaced = true;
}
if (isCacheNextTrack) {
this._cacheNextTrack(); this._cacheNextTrack();
} return source;
return replaced; } else {
} else { store.dispatch('showToast', `无法播放 ${track.name}`);
store.dispatch('showToast', `无法播放 ${track.name}`); if (ifUnplayableThen === 'playNextTrack') {
switch (ifUnplayableThen) { if (this.isPersonalFM) {
case UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK: this.playNextFMTrack();
this._playNextTrack(this.isPersonalFM); } else {
break; this.playNextTrack();
case UNPLAYABLE_CONDITION.PLAY_PREV_TRACK: }
} else {
this.playPrevTrack(); this.playPrevTrack();
break; }
default:
store.dispatch(
'showToast',
`undefined Unplayable condition: ${ifUnplayableThen}`
);
break;
} }
return false; });
}
}); });
} }
_cacheNextTrack() { _cacheNextTrack() {
@ -575,7 +453,11 @@ export default class {
this.playPrevTrack(); this.playPrevTrack();
}); });
navigator.mediaSession.setActionHandler('nexttrack', () => { navigator.mediaSession.setActionHandler('nexttrack', () => {
this._playNextTrack(this.isPersonalFM); if (this.isPersonalFM) {
this.playNextFMTrack();
} else {
this.playNextTrack();
}
}); });
navigator.mediaSession.setActionHandler('stop', () => { navigator.mediaSession.setActionHandler('stop', () => {
this.pause(); this.pause();
@ -604,11 +486,6 @@ export default class {
artist: artists.join(','), artist: artists.join(','),
album: track.al.name, album: track.al.name,
artwork: [ artwork: [
{
src: track.al.picUrl + '?param=224y224',
type: 'image/jpg',
sizes: '224x224',
},
{ {
src: track.al.picUrl + '?param=512y512', src: track.al.picUrl + '?param=512y512',
type: 'image/jpg', type: 'image/jpg',
@ -617,35 +494,13 @@ export default class {
], ],
length: this.currentTrackDuration, length: this.currentTrackDuration,
trackId: this.current, trackId: this.current,
url: '/trackid/' + track.id,
}; };
navigator.mediaSession.metadata = new window.MediaMetadata(metadata); navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
if (isCreateMpris) { if (isCreateMpris) {
this._updateMprisState(track, metadata); ipcRenderer.send('metadata', metadata);
} }
} }
// OSDLyrics 会检测 Mpris 状态并寻找对应歌词文件,所以要在更新 Mpris 状态之前保证歌词下载完成
async _updateMprisState(track, metadata) {
if (!store.state.settings.enableOsdlyricsSupport) {
return ipcRenderer?.send('metadata', metadata);
}
let lyricContent = await getLyric(track.id);
if (!lyricContent.lrc || !lyricContent.lrc.lyric) {
return ipcRenderer?.send('metadata', metadata);
}
ipcRenderer.send('sendLyrics', {
track,
lyrics: lyricContent.lrc.lyric,
});
ipcRenderer.on('saveLyricFinished', () => {
ipcRenderer?.send('metadata', metadata);
});
}
_updateMediaSessionPositionState() { _updateMediaSessionPositionState() {
if ('mediaSession' in navigator === false) { if ('mediaSession' in navigator === false) {
return; return;
@ -661,9 +516,11 @@ export default class {
_nextTrackCallback() { _nextTrackCallback() {
this._scrobble(this._currentTrack, 0, true); this._scrobble(this._currentTrack, 0, true);
if (!this.isPersonalFM && this.repeatMode === 'one') { if (!this.isPersonalFM && this.repeatMode === 'one') {
this._replaceCurrentTrack(this.currentTrackID); this._replaceCurrentTrack(this._currentTrack.id);
} else if (this.isPersonalFM) {
this.playNextFMTrack();
} else { } else {
this._playNextTrack(this.isPersonalFM); this.playNextTrack();
} }
} }
_loadPersonalFMNextTrack() { _loadPersonalFMNextTrack() {
@ -697,7 +554,7 @@ export default class {
} }
let copyTrack = { ...track }; let copyTrack = { ...track };
copyTrack.dt -= seekTime * 1000; copyTrack.dt -= seekTime * 1000;
ipcRenderer?.send('playDiscordPresence', copyTrack); ipcRenderer.send('playDiscordPresence', copyTrack);
} }
_pauseDiscordPresence(track) { _pauseDiscordPresence(track) {
if ( if (
@ -706,16 +563,13 @@ export default class {
) { ) {
return null; return null;
} }
ipcRenderer?.send('pauseDiscordPresence', track); ipcRenderer.send('pauseDiscordPresence', track);
}
_playNextTrack(isPersonal) {
if (isPersonal) {
this.playNextFMTrack();
} else {
this.playNextTrack();
}
} }
currentTrackID() {
const { list, current } = this._getListAndCurrent();
return list[current];
}
appendTrack(trackID) { appendTrack(trackID) {
this.list.append(trackID); this.list.append(trackID);
} }
@ -727,12 +581,7 @@ export default class {
this._setPlaying(false); this._setPlaying(false);
return false; return false;
} }
let next = index; this.current = index;
if (index === INDEX_IN_PLAY_NEXT) {
this._playNextList.shift();
next = this.current;
}
this.current = next;
this._replaceCurrentTrack(trackID); this._replaceCurrentTrack(trackID);
return true; return true;
} }
@ -785,11 +634,7 @@ export default class {
const [trackID, index] = this._getPrevTrack(); const [trackID, index] = this._getPrevTrack();
if (trackID === undefined) return false; if (trackID === undefined) return false;
this.current = index; this.current = index;
this._replaceCurrentTrack( this._replaceCurrentTrack(trackID, true, 'playPrevTrack');
trackID,
true,
UNPLAYABLE_CONDITION.PLAY_PREV_TRACK
);
return true; return true;
} }
saveSelfToLocalStorage() { saveSelfToLocalStorage() {
@ -803,41 +648,28 @@ export default class {
} }
pause() { pause() {
this._howler?.fade(this.volume, 0, PLAY_PAUSE_FADE_DURATION); this._howler?.pause();
this._setPlaying(false);
this._howler?.once('fade', () => { setTitle(null);
this._howler?.pause(); this._pauseDiscordPresence(this._currentTrack);
this._setPlaying(false);
setTitle(null);
this._pauseDiscordPresence(this._currentTrack);
});
} }
play() { play() {
if (this._howler?.playing()) return; if (this._howler?.playing()) return;
this._howler?.play(); this._howler?.play();
this._setPlaying(true);
this._howler?.once('play', () => { if (this._currentTrack.name) {
this._howler?.fade(0, this.volume, PLAY_PAUSE_FADE_DURATION); setTitle(this._currentTrack);
}
// 播放时确保开启player. this._playDiscordPresence(this._currentTrack, this.seek());
// 避免因"忘记设置"导致在播放时播放器不显示的Bug if (store.state.lastfm.key !== undefined) {
this._enabled = true; trackUpdateNowPlaying({
this._setPlaying(true); artist: this.currentTrack.ar[0].name,
if (this._currentTrack.name) { track: this.currentTrack.name,
setTitle(this._currentTrack); album: this.currentTrack.al.name,
} trackNumber: this.currentTrack.no,
this._playDiscordPresence(this._currentTrack, this.seek()); duration: ~~(this.currentTrack.dt / 1000),
if (store.state.lastfm.key !== undefined) { });
trackUpdateNowPlaying({ }
artist: this.currentTrack.ar[0].name,
track: this.currentTrack.name,
album: this.currentTrack.al.name,
trackNumber: this.currentTrack.no,
duration: ~~(this.currentTrack.dt / 1000),
});
}
});
} }
playOrPause() { playOrPause() {
if (this._howler?.playing()) { if (this._howler?.playing()) {
@ -846,14 +678,11 @@ export default class {
this.play(); this.play();
} }
} }
seek(time = null, sendMpris = true) { seek(time = null) {
if (isCreateMpris && sendMpris && time) {
ipcRenderer?.send('seeked', time);
}
if (time !== null) { if (time !== null) {
this._howler?.seek(time); this._howler?.seek(time);
if (this._playing) if (this._playing)
this._playDiscordPresence(this._currentTrack, this.seek(null, false)); this._playDiscordPresence(this._currentTrack, this.seek());
} }
return this._howler === null ? 0 : this._howler.seek(); return this._howler === null ? 0 : this._howler.seek();
} }
@ -879,6 +708,7 @@ export default class {
autoPlayTrackID = 'first' autoPlayTrackID = 'first'
) { ) {
this._isPersonalFM = false; this._isPersonalFM = false;
if (!this._enabled) this._enabled = true;
this.list = trackIDs; this.list = trackIDs;
this.current = 0; this.current = 0;
this._playlistSource = { this._playlistSource = {
@ -889,7 +719,7 @@ export default class {
if (autoPlayTrackID === 'first') { if (autoPlayTrackID === 'first') {
this._replaceCurrentTrack(this.list[0]); this._replaceCurrentTrack(this.list[0]);
} else { } else {
this.current = this.list.indexOf(autoPlayTrackID); this.current = trackIDs.indexOf(autoPlayTrackID);
this._replaceCurrentTrack(autoPlayTrackID); this._replaceCurrentTrack(autoPlayTrackID);
} }
} }
@ -935,13 +765,20 @@ export default class {
addTrackToPlayNext(trackID, playNow = false) { addTrackToPlayNext(trackID, playNow = false) {
this._playNextList.push(trackID); this._playNextList.push(trackID);
if (playNow) { if (playNow) {
this.playNextTrack(); if (this.isPersonalFM) {
this.playNextFMTrack();
} else {
this.playNextTrack();
}
} }
} }
playPersonalFM() { playPersonalFM() {
this._isPersonalFM = true; this._isPersonalFM = true;
if (this.currentTrackID !== this._personalFMTrack.id) { if (!this._enabled) this._enabled = true;
this._replaceCurrentTrack(this._personalFMTrack.id, true); if (this._currentTrack.id !== this._personalFMTrack.id) {
this._replaceCurrentTrack(this._personalFMTrack.id).then(() =>
this.playOrPause()
);
} else { } else {
this.playOrPause(); this.playOrPause();
} }
@ -957,7 +794,7 @@ export default class {
sendSelfToIpcMain() { sendSelfToIpcMain() {
if (process.env.IS_ELECTRON !== true) return false; if (process.env.IS_ELECTRON !== true) return false;
let liked = store.state.liked.songs.includes(this.currentTrack.id); let liked = store.state.liked.songs.includes(this.currentTrack.id);
ipcRenderer?.send('player', { ipcRenderer.send('player', {
playing: this.playing, playing: this.playing,
likedCurrentTrack: liked, likedCurrentTrack: liked,
}); });
@ -973,13 +810,13 @@ export default class {
this.repeatMode = 'on'; this.repeatMode = 'on';
} }
if (isCreateMpris) { if (isCreateMpris) {
ipcRenderer?.send('switchRepeatMode', this.repeatMode); ipcRenderer.send('switchRepeatMode', this.repeatMode);
} }
} }
switchShuffle() { switchShuffle() {
this.shuffle = !this.shuffle; this.shuffle = !this.shuffle;
if (isCreateMpris) { if (isCreateMpris) {
ipcRenderer?.send('switchShuffle', this.shuffle); ipcRenderer.send('switchShuffle', this.shuffle);
} }
} }
switchReversed() { switchReversed() {

View file

@ -1,67 +0,0 @@
// https://github.com/niklasvh/base64-arraybuffer/blob/master/src/index.ts
// Copyright (c) 2012 Niklas von Hertzen Licensed under the MIT license.
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
export const encode = arraybuffer => {
let bytes = new Uint8Array(arraybuffer),
i,
len = bytes.length,
base64 = '';
for (i = 0; i < len; i += 3) {
base64 += chars[bytes[i] >> 2];
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += chars[bytes[i + 2] & 63];
}
if (len % 3 === 2) {
base64 = base64.substring(0, base64.length - 1) + '=';
} else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2) + '==';
}
return base64;
};
export const decode = base64 => {
let bufferLength = base64.length * 0.75,
len = base64.length,
i,
p = 0,
encoded1,
encoded2,
encoded3,
encoded4;
if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}
const arraybuffer = new ArrayBuffer(bufferLength),
bytes = new Uint8Array(arraybuffer);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)];
encoded2 = lookup[base64.charCodeAt(i + 1)];
encoded3 = lookup[base64.charCodeAt(i + 2)];
encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arraybuffer;
};

View file

@ -1,8 +0,0 @@
import os from 'os';
import fs from 'fs';
import path from 'path';
// extract from NeteasyCloudMusicAPI/generateConfig.js and avoid bugs in there (generateConfig require main.js but the main.js has bugs)
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
}

View file

@ -221,7 +221,7 @@ export function bytesToSize(bytes) {
export function formatTrackTime(value) { export function formatTrackTime(value) {
if (!value) return ''; if (!value) return '';
let min = ~~(value / 60); let min = ~~((value / 60) % 60);
let sec = (~~(value % 60)).toString().padStart(2, '0'); let sec = (~~(value % 60)).toString().padStart(2, '0');
return `${min}:${sec}`; return `${min}:${sec}`;
} }

View file

@ -2,17 +2,14 @@ export function lyricParser(lrc) {
return { return {
lyric: parseLyric(lrc?.lrc?.lyric || ''), lyric: parseLyric(lrc?.lrc?.lyric || ''),
tlyric: parseLyric(lrc?.tlyric?.lyric || ''), tlyric: parseLyric(lrc?.tlyric?.lyric || ''),
romalyric: parseLyric(lrc?.romalrc?.lyric || ''),
lyricuser: lrc.lyricUser, lyricuser: lrc.lyricUser,
transuser: lrc.transUser, transuser: lrc.transUser,
}; };
} }
// regexr.com/6e52n // regexr.com/6e52n
const extractLrcRegex = const extractLrcRegex = /^(?<lyricTimestamps>(?:\[.+?\])+)(?!\[)(?<content>.+)$/gm;
/^(?<lyricTimestamps>(?:\[.+?\])+)(?!\[)(?<content>.+)$/gm; const extractTimestampRegex = /\[(?<min>\d+):(?<sec>\d+)(?:\.|:)*(?<ms>\d+)*\]/g;
const extractTimestampRegex =
/\[(?<min>\d+):(?<sec>\d+)(?:\.|:)*(?<ms>\d+)*\]/g;
/** /**
* @typedef {{time: number, rawTime: string, content: string}} ParsedLyric * @typedef {{time: number, rawTime: string, content: string}} ParsedLyric
@ -84,30 +81,3 @@ function trimContent(content) {
let t = content.trim(); let t = content.trim();
return t.length < 1 ? content : t; return t.length < 1 ? content : t;
} }
/**
* @param {string} lyric
*/
export async function copyLyric(lyric) {
const textToCopy = lyric;
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(textToCopy);
} catch (err) {
alert('复制失败,请手动复制!');
}
} else {
const tempInput = document.createElement('textarea');
tempInput.value = textToCopy;
tempInput.style.position = 'absolute';
tempInput.style.left = '-9999px';
document.body.appendChild(tempInput);
tempInput.select();
try {
document.execCommand('copy');
} catch (err) {
alert('复制失败,请手动复制!');
}
document.body.removeChild(tempInput);
}
}

View file

@ -1,11 +1,5 @@
import router from '../router'; import router from '../router';
import state from '../store/state'; import state from '../store/state';
import {
recommendPlaylist,
dailyRecommendPlaylist,
getPlaylistDetail,
} from '@/api/playlist';
import { isAccountLoggedIn } from '@/utils/auth';
export function hasListSource() { export function hasListSource() {
return !state.player.isPersonalFM && state.player.playlistSource.id !== 0; return !state.player.isPersonalFM && state.player.playlistSource.id !== 0;
@ -26,36 +20,3 @@ export function getListSourcePath() {
return `/${state.player.playlistSource.type}/${state.player.playlistSource.id}`; return `/${state.player.playlistSource.type}/${state.player.playlistSource.id}`;
} }
} }
export async function getRecommendPlayList(limit, removePrivateRecommand) {
if (isAccountLoggedIn()) {
const playlists = await Promise.all([
dailyRecommendPlaylist(),
recommendPlaylist({ limit }),
]);
let recommend = playlists[0].recommend ?? [];
if (recommend.length) {
if (removePrivateRecommand) recommend = recommend.slice(1);
await replaceRecommendResult(recommend);
}
return recommend.concat(playlists[1].result).slice(0, limit);
} else {
const response = await recommendPlaylist({ limit });
return response.result;
}
}
async function replaceRecommendResult(recommend) {
for (let r of recommend) {
if (specialPlaylist.indexOf(r.id) > -1) {
const data = await getPlaylistDetail(r.id, true);
const playlist = data.playlist;
if (playlist) {
r.name = playlist.name;
r.picUrl = playlist.coverImgUrl;
}
}
}
}
const specialPlaylist = [3136952023, 2829883282, 2829816518, 2829896389];

View file

@ -1,6 +1,5 @@
import router from '@/router';
import { doLogout, getCookie } from '@/utils/auth';
import axios from 'axios'; import axios from 'axios';
import { getCookie } from '@/utils/auth';
let baseURL = ''; let baseURL = '';
// Web 和 Electron 跑在不同端口避免同时启动时冲突 // Web 和 Electron 跑在不同端口避免同时启动时冲突
@ -22,33 +21,14 @@ const service = axios.create({
service.interceptors.request.use(function (config) { service.interceptors.request.use(function (config) {
if (!config.params) config.params = {}; if (!config.params) config.params = {};
if (baseURL.length) { if (baseURL[0] !== '/' && !process.env.IS_ELECTRON) {
if ( config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`;
baseURL[0] !== '/' &&
!process.env.IS_ELECTRON &&
getCookie('MUSIC_U') !== null
) {
config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`;
}
} else {
console.error("You must set up the baseURL in the service's config");
} }
if (!process.env.IS_ELECTRON && !config.url.includes('/login')) { if (!process.env.IS_ELECTRON && !config.url.includes('/login')) {
config.params.realIP = '211.161.244.70'; config.params.realIP = '211.161.244.70';
} }
// Force real_ip
const enableRealIP = JSON.parse(
localStorage.getItem('settings')
).enableRealIP;
const realIP = JSON.parse(localStorage.getItem('settings')).realIP;
if (process.env.VUE_APP_REAL_IP) {
config.params.realIP = process.env.VUE_APP_REAL_IP;
} else if (enableRealIP) {
config.params.realIP = realIP;
}
const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig; const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig;
if (['HTTP', 'HTTPS'].includes(proxy.protocol)) { if (['HTTP', 'HTTPS'].includes(proxy.protocol)) {
config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`; config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`;
@ -62,37 +42,8 @@ service.interceptors.response.use(
const res = response.data; const res = response.data;
return res; return res;
}, },
async error => { error => {
/** @type {import('axios').AxiosResponse | null} */ return Promise.reject(error);
let response;
let data;
if (error === 'TypeError: baseURL is undefined') {
response = error;
data = error;
console.error("You must set up the baseURL in the service's config");
} else if (error.response) {
response = error.response;
data = response.data;
}
if (
response &&
typeof data === 'object' &&
data.code === 301 &&
data.msg === '需要登录'
) {
console.warn('Token has expired. Logout now!');
// 登出帳戶
doLogout();
// 導向登入頁面
if (process.env.IS_ELECTRON === true) {
router.push({ name: 'loginAccount' });
} else {
router.push({ name: 'login' });
}
}
} }
); );

View file

@ -28,9 +28,7 @@
<span v-else>Compilation by Various Artists</span> <span v-else>Compilation by Various Artists</span>
</div> </div>
<div class="date-and-count"> <div class="date-and-count">
<span <span v-if="album.mark === 1056768" class="explicit-symbol"
v-if="(album.mark & 1048576) === 1048576"
class="explicit-symbol"
><ExplicitSymbol ><ExplicitSymbol
/></span> /></span>
<span :title="album.publishTime | formatDate">{{ <span :title="album.publishTime | formatDate">{{
@ -73,12 +71,12 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="tracksByDisc.length > 1"> <div v-if="Object.keys(tracksByDisc).length !== 1">
<div v-for="item in tracksByDisc" :key="item.disc"> <div v-for="(disc, cd) in tracksByDisc" :key="cd">
<h2 class="disc">Disc {{ item.disc }}</h2> <h2 class="disc">Disc {{ cd }}</h2>
<TrackList <TrackList
:id="album.id" :id="album.id"
:tracks="item.tracks" :tracks="disc"
:type="'album'" :type="'album'"
:album-object="album" :album-object="album"
/> />
@ -98,7 +96,9 @@
{{ $t('album.released') }} {{ $t('album.released') }}
{{ album.publishTime | formatDate('MMMM D, YYYY') }} {{ album.publishTime | formatDate('MMMM D, YYYY') }}
</div> </div>
<div v-if="album.company" class="copyright"> © {{ album.company }} </div> <div v-if="album.company !== null" class="copyright">
© {{ album.company }}
</div>
</div> </div>
<div v-if="filteredMoreAlbums.length !== 0" class="more-by"> <div v-if="filteredMoreAlbums.length !== 0" class="more-by">
<div class="section-title"> <div class="section-title">
@ -153,7 +153,7 @@ import locale from '@/locale';
import { splitSoundtrackAlbumTitle, splitAlbumTitle } from '@/utils/common'; import { splitSoundtrackAlbumTitle, splitAlbumTitle } from '@/utils/common';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import { isAccountLoggedIn } from '@/utils/auth'; import { isAccountLoggedIn } from '@/utils/auth';
import { groupBy, toPairs, sortBy } from 'lodash'; import { groupBy } from 'lodash';
import ExplicitSymbol from '@/components/ExplicitSymbol.vue'; import ExplicitSymbol from '@/components/ExplicitSymbol.vue';
import ButtonTwoTone from '@/components/ButtonTwoTone.vue'; import ButtonTwoTone from '@/components/ButtonTwoTone.vue';
@ -222,12 +222,7 @@ export default {
} }
}, },
tracksByDisc() { tracksByDisc() {
if (this.tracks.length <= 1) return []; return groupBy(this.tracks, 'cd');
const pairs = toPairs(groupBy(this.tracks, 'cd'));
return sortBy(pairs, p => p[0]).map(items => ({
disc: items[0],
tracks: items[1],
}));
}, },
}, },
created() { created() {

View file

@ -2,7 +2,7 @@
<div v-show="show" class="artist-page"> <div v-show="show" class="artist-page">
<div class="artist-info"> <div class="artist-info">
<div class="head"> <div class="head">
<img :src="artist.img1v1Url | resizeImage(1024)" loading="lazy" /> <img :src="artist.img1v1Url | resizeImage(1024)" />
</div> </div>
<div> <div>
<div class="name">{{ artist.name }}</div> <div class="name">{{ artist.name }}</div>
@ -75,7 +75,7 @@
@mouseleave="mvHover = false" @mouseleave="mvHover = false"
@click="goToMv(latestMV.id)" @click="goToMv(latestMV.id)"
> >
<img :src="latestMV.coverUrl" loading="lazy" /> <img :src="latestMV.coverUrl" />
<transition name="fade"> <transition name="fade">
<div <div
v-show="mvHover" v-show="mvHover"
@ -127,7 +127,7 @@
<div v-if="mvs.length !== 0" id="mvs" class="mvs"> <div v-if="mvs.length !== 0" id="mvs" class="mvs">
<div class="section-title" <div class="section-title"
>MVs >MVs
<router-link v-show="hasMoreMV" :to="`/artist/${artist.id}/mv`">{{ <router-link v-show="hasMoreMV" :to="`/artist/${this.artist.id}/mv`">{{
$t('home.seeMore') $t('home.seeMore')
}}</router-link> }}</router-link>
</div> </div>
@ -185,7 +185,6 @@ import {
followAArtist, followAArtist,
similarArtists, similarArtists,
} from '@/api/artist'; } from '@/api/artist';
import { getTrackDetail } from '@/api/track';
import locale from '@/locale'; import locale from '@/locale';
import { isAccountLoggedIn } from '@/utils/auth'; import { isAccountLoggedIn } from '@/utils/auth';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
@ -242,9 +241,7 @@ export default {
computed: { computed: {
...mapState(['player']), ...mapState(['player']),
albums() { albums() {
return this.albumsData.filter( return this.albumsData.filter(a => a.type === '专辑');
a => a.type === '专辑' || a.type === '精选集'
);
}, },
eps() { eps() {
return this.albumsData.filter(a => return this.albumsData.filter(a =>
@ -279,7 +276,7 @@ export default {
this.$parent.$refs.main.scrollTo({ top: 0 }); this.$parent.$refs.main.scrollTo({ top: 0 });
getArtist(id).then(data => { getArtist(id).then(data => {
this.artist = data.artist; this.artist = data.artist;
this.setPopularTracks(data.hotSongs); this.popularTracks = data.hotSongs;
if (next !== undefined) next(); if (next !== undefined) next();
NProgress.done(); NProgress.done();
this.show = true; this.show = true;
@ -292,16 +289,8 @@ export default {
this.mvs = data.mvs; this.mvs = data.mvs;
this.hasMoreMV = data.hasMore; this.hasMoreMV = data.hasMore;
}); });
if (isAccountLoggedIn()) { similarArtists(id).then(data => {
similarArtists(id).then(data => { this.similarArtists = data.artists;
this.similarArtists = data.artists;
});
}
},
setPopularTracks(hotSongs) {
const trackIDs = hotSongs.map(t => t.id);
getTrackDetail(trackIDs.join(',')).then(data => {
this.popularTracks = data.songs;
}); });
}, },
goToAlbum(id) { goToAlbum(id) {

View file

@ -1,11 +1,9 @@
<template> <template>
<div v-show="show"> <div v-show="show">
<h1> <h1>
<img <img class="avatar" :src="artist.img1v1Url | resizeImage(1024)" />{{
class="avatar" artist.name
:src="artist.img1v1Url | resizeImage(1024)" }}'s Music Videos
loading="lazy"
/>{{ artist.name }}'s Music Videos
</h1> </h1>
<MvRow :mvs="mvs" subtitle="publishTime" /> <MvRow :mvs="mvs" subtitle="publishTime" />
<div class="load-more"> <div class="load-more">

View file

@ -66,9 +66,13 @@
<script> <script>
import { mapState, mapMutations } from 'vuex'; import { mapState, mapMutations } from 'vuex';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import { topPlaylist, highQualityPlaylist, toplists } from '@/api/playlist'; import {
topPlaylist,
highQualityPlaylist,
recommendPlaylist,
toplists,
} from '@/api/playlist';
import { playlistCategories } from '@/utils/staticData'; import { playlistCategories } from '@/utils/staticData';
import { getRecommendPlayList } from '@/utils/playList';
import ButtonTwoTone from '@/components/ButtonTwoTone.vue'; import ButtonTwoTone from '@/components/ButtonTwoTone.vue';
import CoverRow from '@/components/CoverRow.vue'; import CoverRow from '@/components/CoverRow.vue';
@ -120,13 +124,10 @@ export default {
setTimeout(() => { setTimeout(() => {
if (!this.show) NProgress.start(); if (!this.show) NProgress.start();
}, 1000); }, 1000);
const queryCategory = this.$route.query.category; this.activeCategory =
if (queryCategory === undefined) { this.$route.query.category === undefined
this.playlists = []; ? '全部'
this.activeCategory = '全部'; : this.$route.query.category;
} else {
this.activeCategory = queryCategory;
}
this.getPlaylist(); this.getPlaylist();
}, },
goToCategory(Category) { goToCategory(Category) {
@ -154,9 +155,9 @@ export default {
return this.getTopPlayList(); return this.getTopPlayList();
}, },
getRecommendPlayList() { getRecommendPlayList() {
getRecommendPlayList(100, true).then(list => { recommendPlaylist({ limit: 100 }).then(data => {
this.playlists = []; this.playlists = [];
this.updatePlaylist(list); this.updatePlaylist(data.result);
}); });
}, },
getHighQualityPlaylist() { getHighQualityPlaylist() {

View file

@ -69,11 +69,10 @@
</template> </template>
<script> <script>
import { toplists } from '@/api/playlist'; import { toplists, recommendPlaylist } from '@/api/playlist';
import { toplistOfArtists } from '@/api/artist'; import { toplistOfArtists } from '@/api/artist';
import { newAlbums } from '@/api/album';
import { byAppleMusic } from '@/utils/staticData'; import { byAppleMusic } from '@/utils/staticData';
import { getRecommendPlayList } from '@/utils/playList'; import { newAlbums } from '@/api/album';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import CoverRow from '@/components/CoverRow.vue'; import CoverRow from '@/components/CoverRow.vue';
@ -113,8 +112,10 @@ export default {
setTimeout(() => { setTimeout(() => {
if (!this.show) NProgress.start(); if (!this.show) NProgress.start();
}, 1000); }, 1000);
getRecommendPlayList(10, false).then(items => { recommendPlaylist({
this.recommendPlaylist.items = items; limit: 10,
}).then(data => {
this.recommendPlaylist.items = data.result;
NProgress.done(); NProgress.done();
this.show = true; this.show = true;
}); });

View file

@ -1,11 +1,9 @@
<template> <template>
<div v-show="show" ref="library"> <div v-show="show" ref="library">
<h1> <h1>
<img <img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
class="avatar" data.user.nickname
:src="data.user.avatarUrl | resizeImage" }}{{ $t('library.sLibrary') }}
loading="lazy"
/>{{ data.user.nickname }}{{ $t('library.sLibrary') }}
</h1> </h1>
<div class="section-one"> <div class="section-one">
<div class="liked-songs" @click="goToLikedSongsList"> <div class="liked-songs" @click="goToLikedSongsList">
@ -87,14 +85,14 @@
:class="{ active: currentTab === 'cloudDisk' }" :class="{ active: currentTab === 'cloudDisk' }"
@click="updateCurrentTab('cloudDisk')" @click="updateCurrentTab('cloudDisk')"
> >
{{ $t('library.cloudDisk') }} 云盘
</div> </div>
<div <div
class="tab" class="tab"
:class="{ active: currentTab === 'playHistory' }" :class="{ active: currentTab === 'playHistory' }"
@click="updateCurrentTab('playHistory')" @click="updateCurrentTab('playHistory')"
> >
{{ $t('library.playHistory.title') }} 听歌排行
</div> </div>
</div> </div>
<button <button
@ -107,14 +105,14 @@
v-show="currentTab === 'cloudDisk'" v-show="currentTab === 'cloudDisk'"
class="tab-button" class="tab-button"
@click="selectUploadFiles" @click="selectUploadFiles"
><svg-icon icon-class="arrow-up-alt" />{{ $t('library.uploadSongs') }} ><svg-icon icon-class="arrow-up-alt" /> 上传歌曲
</button> </button>
</div> </div>
<div v-show="currentTab === 'playlists'"> <div v-show="currentTab === 'playlists'">
<div v-if="liked.playlists.length > 1"> <div v-if="liked.playlists.length > 1">
<CoverRow <CoverRow
:items="filterPlaylists" :items="filterPlaylists.slice(1)"
type="playlist" type="playlist"
sub-text="creator" sub-text="creator"
:show-play-button="true" :show-play-button="true"
@ -155,23 +153,11 @@
</div> </div>
<div v-show="currentTab === 'playHistory'"> <div v-show="currentTab === 'playHistory'">
<button <button class="playHistory-button" @click="playHistoryMode = 'week'">
:class="{ 最近一周
'playHistory-button': true,
'playHistory-button--selected': playHistoryMode === 'week',
}"
@click="playHistoryMode = 'week'"
>
{{ $t('library.playHistory.week') }}
</button> </button>
<button <button class="playHistory-button" @click="playHistoryMode = 'all'">
:class="{ 所有時間
'playHistory-button': true,
'playHistory-button--selected': playHistoryMode === 'all',
}"
@click="playHistoryMode = 'all'"
>
{{ $t('library.playHistory.all') }}
</button> </button>
<TrackList <TrackList
:tracks="playHistoryList" :tracks="playHistoryList"
@ -206,16 +192,14 @@
$t('library.likedSongs') $t('library.likedSongs')
}}</div> }}</div>
<hr /> <hr />
<div class="item" @click="playIntelligenceList">{{ <div class="item" @click="playIntelligenceList"> 心动模式 </div>
$t('contextMenu.cardiacMode')
}}</div>
</ContextMenu> </ContextMenu>
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapMutations, mapState } from 'vuex'; import { mapActions, mapMutations, mapState } from 'vuex';
import { randomNum } from '@/utils/common'; import { randomNum, dailyTask } from '@/utils/common';
import { isAccountLoggedIn } from '@/utils/auth'; import { isAccountLoggedIn } from '@/utils/auth';
import { uploadSong } from '@/api/user'; import { uploadSong } from '@/api/user';
import { getLyric } from '@/api/track'; import { getLyric } from '@/api/track';
@ -269,7 +253,7 @@ export default {
// Pick 3 or fewer lyrics based on the lyric lines. // Pick 3 or fewer lyrics based on the lyric lines.
const lyricsToPick = Math.min(lyricLine.length, 3); const lyricsToPick = Math.min(lyricLine.length, 3);
// The upperBound of the lyric line to pick // The upperbound of the lyric line to pick
const randomUpperBound = lyricLine.length - lyricsToPick; const randomUpperBound = lyricLine.length - lyricsToPick;
const startLyricLineIndex = randomNum(0, randomUpperBound - 1); const startLyricLineIndex = randomNum(0, randomUpperBound - 1);
@ -282,7 +266,7 @@ export default {
return this.data.libraryPlaylistFilter || 'all'; return this.data.libraryPlaylistFilter || 'all';
}, },
filterPlaylists() { filterPlaylists() {
const playlists = this.liked.playlists.slice(1); const playlists = this.liked.playlists;
const userId = this.data.user.userId; const userId = this.data.user.userId;
if (this.playlistFilter === 'mine') { if (this.playlistFilter === 'mine') {
return playlists.filter(p => p.creator.userId === userId); return playlists.filter(p => p.creator.userId === userId);
@ -294,8 +278,7 @@ export default {
playHistoryList() { playHistoryList() {
if (this.show && this.playHistoryMode === 'week') { if (this.show && this.playHistoryMode === 'week') {
return this.liked.playHistory.weekData; return this.liked.playHistory.weekData;
} } else if (this.show && this.playHistoryMode === 'all') {
if (this.show && this.playHistoryMode === 'all') {
return this.liked.playHistory.allData; return this.liked.playHistory.allData;
} }
return []; return [];
@ -310,6 +293,7 @@ export default {
activated() { activated() {
this.$parent.$refs.scrollbar.restorePosition(); this.$parent.$refs.scrollbar.restorePosition();
this.loadData(); this.loadData();
dailyTask();
}, },
methods: { methods: {
...mapActions(['showToast']), ...mapActions(['showToast']),
@ -595,29 +579,13 @@ button.tab-button {
button.playHistory-button { button.playHistory-button {
color: var(--color-text); color: var(--color-text);
border-radius: 8px; border-radius: 8px;
padding: 6px 8px; padding: 10px;
margin-bottom: 12px;
margin-right: 4px;
transition: 0.2s; transition: 0.2s;
opacity: 0.68; opacity: 0.68;
font-weight: 500; font-weight: 500;
cursor: pointer;
&:hover { &:hover {
opacity: 1; opacity: 1;
background: var(--color-secondary-bg); background: var(--color-secondary-bg);
} }
&:active {
transform: scale(0.95);
}
}
button.playHistory-button--selected {
color: var(--color-text);
background: var(--color-secondary-bg);
opacity: 1;
font-weight: 700;
&:active {
transform: none;
}
} }
</style> </style>

View file

@ -7,19 +7,16 @@
<div class="title">{{ $t('login.loginText') }}</div> <div class="title">{{ $t('login.loginText') }}</div>
<div class="section-2"> <div class="section-2">
<div v-show="mode === 'phone'" class="input-box"> <div v-show="mode === 'phone'" class="input-box">
<div <div class="container" :class="{ active: inputFocus === 'phone' }">
class="container"
:class="{ active: ['phone', 'countryCode'].includes(inputFocus) }"
>
<svg-icon icon-class="mobile" /> <svg-icon icon-class="mobile" />
<div class="inputs"> <div class="inputs">
<input <input
id="countryCode" id="countryCode"
v-model="countryCode" v-model="countryCode"
:placeholder=" :placeholder="
inputFocus === 'countryCode' ? '' : $t('login.countryCode') inputFocus === 'phone' ? '' : $t('login.countryCode')
" "
@focus="inputFocus = 'countryCode'" @focus="inputFocus = 'phone'"
@blur="inputFocus = ''" @blur="inputFocus = ''"
@keyup.enter="login" @keyup.enter="login"
/> />
@ -71,8 +68,8 @@
</div> </div>
<div v-show="mode == 'qrCode'"> <div v-show="mode == 'qrCode'">
<div v-show="qrCodeSvg" class="qr-code-container"> <div v-show="qrCodeImage" class="qr-code-container">
<img :src="qrCodeSvg" loading="lazy" /> <img :src="qrCodeImage" />
</div> </div>
<div class="qr-code-info"> <div class="qr-code-info">
{{ qrCodeInformation }} {{ qrCodeInformation }}
@ -90,14 +87,14 @@
</button> </button>
</div> </div>
<div class="other-login"> <div class="other-login">
<!-- <a v-show="mode !== 'email'" @click="changeMode('email')">{{ <a v-show="mode !== 'email'" @click="changeMode('email')">{{
$t('login.loginWithEmail') $t('login.loginWithEmail')
}}</a> }}</a>
<span v-show="mode === 'qrCode'">|</span> <span v-show="mode === 'qrCode'">|</span>
<a v-show="mode !== 'phone'" @click="changeMode('phone')">{{ <a v-show="mode !== 'phone'" @click="changeMode('phone')">{{
$t('login.loginWithPhone') $t('login.loginWithPhone')
}}</a> }}</a>
<span v-show="mode !== 'qrCode'">|</span> --> <span v-show="mode !== 'qrCode'">|</span>
<a v-show="mode !== 'qrCode'" @click="changeMode('qrCode')"> <a v-show="mode !== 'qrCode'" @click="changeMode('qrCode')">
二维码登录 二维码登录
</a> </a>
@ -138,7 +135,7 @@ export default {
smsCode: '', smsCode: '',
inputFocus: '', inputFocus: '',
qrCodeKey: '', qrCodeKey: '',
qrCodeSvg: '', qrCodeImage: '',
qrCodeCheckInterval: null, qrCodeCheckInterval: null,
qrCodeInformation: '打开网易云音乐APP扫码登录', qrCodeInformation: '打开网易云音乐APP扫码登录',
}; };
@ -172,8 +169,7 @@ export default {
return true; return true;
}, },
validateEmail() { validateEmail() {
const emailReg = const emailReg = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if ( if (
this.email === '' || this.email === '' ||
this.password === '' || this.password === '' ||
@ -236,7 +232,7 @@ export default {
return loginQrCodeKey().then(result => { return loginQrCodeKey().then(result => {
if (result.code === 200) { if (result.code === 200) {
this.qrCodeKey = result.data.unikey; this.qrCodeKey = result.data.unikey;
QRCode.toString( QRCode.toDataURL(
`https://music.163.com/login?codekey=${this.qrCodeKey}`, `https://music.163.com/login?codekey=${this.qrCodeKey}`,
{ {
width: 192, width: 192,
@ -245,13 +241,10 @@ export default {
dark: '#335eea', dark: '#335eea',
light: '#00000000', light: '#00000000',
}, },
type: 'svg',
} }
) )
.then(svg => { .then(url => {
this.qrCodeSvg = `data:image/svg+xml;utf8,${encodeURIComponent( this.qrCodeImage = url;
svg
)}`;
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
@ -264,8 +257,6 @@ export default {
}); });
}, },
checkQrCodeLogin() { checkQrCodeLogin() {
//
clearInterval(this.qrCodeCheckInterval);
this.qrCodeCheckInterval = setInterval(() => { this.qrCodeCheckInterval = setInterval(() => {
if (this.qrCodeKey === '') return; if (this.qrCodeKey === '') return;
loginQrCodeCheck(this.qrCodeKey).then(result => { loginQrCodeCheck(this.qrCodeKey).then(result => {
@ -280,7 +271,7 @@ export default {
clearInterval(this.qrCodeCheckInterval); clearInterval(this.qrCodeCheckInterval);
this.qrCodeInformation = '登录成功,请稍等...'; this.qrCodeInformation = '登录成功,请稍等...';
result.code = 200; result.code = 200;
result.cookie = result.cookie.replaceAll(' HTTPOnly', ''); result.cookie = result.cookie.replace('HTTPOnly', '');
this.handleLoginResponse(result); this.handleLoginResponse(result);
} }
}); });

View file

@ -31,11 +31,7 @@
:class="{ active: user.nickname === activeUser.nickname }" :class="{ active: user.nickname === activeUser.nickname }"
@click="activeUser = user" @click="activeUser = user"
> >
<img <img class="head" :src="user.avatarUrl | resizeImage" />
class="head"
:src="user.avatarUrl | resizeImage"
loading="lazy"
/>
<div class="nickname"> <div class="nickname">
{{ user.nickname }} {{ user.nickname }}
</div> </div>

View file

@ -32,12 +32,9 @@
<div class="left-side"> <div class="left-side">
<div> <div>
<div v-if="settings.showLyricsTime" class="date">
{{ date }}
</div>
<div class="cover"> <div class="cover">
<div class="cover-container"> <div class="cover-container">
<img :src="imageUrl" loading="lazy" /> <img :src="imageUrl" />
<div <div
class="shadow" class="shadow"
:style="{ backgroundImage: `url(${imageUrl})` }" :style="{ backgroundImage: `url(${imageUrl})` }"
@ -79,50 +76,20 @@
</span> </span>
</div> </div>
</div> </div>
<div class="top-right"> <div class="buttons">
<div class="volume-control"> <button-icon
<button-icon :title="$t('player.mute')" @click.native="mute"> :title="$t('player.like')"
<svg-icon v-show="volume > 0.5" icon-class="volume" /> @click.native="likeATrack(player.currentTrack.id)"
<svg-icon v-show="volume === 0" icon-class="volume-mute" /> >
<svg-icon <svg-icon
v-show="volume <= 0.5 && volume !== 0" :icon-class="
icon-class="volume-half" player.isCurrentTrackLiked ? 'heart-solid' : 'heart'
/> "
</button-icon> />
<div class="volume-bar"> </button-icon>
<vue-slider <!-- <button-icon @click.native="openMenu" title="Menu"
v-model="volume" ><svg-icon icon-class="more"
:min="0" /></button-icon> -->
:max="1"
:interval="0.01"
:drag-on-click="true"
:duration="0"
tooltip="none"
:dot-size="12"
></vue-slider>
</div>
</div>
<div class="buttons">
<button-icon
:title="$t('player.like')"
@click.native="likeATrack(player.currentTrack.id)"
>
<svg-icon
:icon-class="
player.isCurrentTrackLiked ? 'heart-solid' : 'heart'
"
/>
</button-icon>
<button-icon
:title="$t('contextMenu.addToPlaylist')"
@click.native="addToPlaylist"
>
<svg-icon icon-class="plus" />
</button-icon>
<!-- <button-icon @click.native="openMenu" title="Menu"
><svg-icon icon-class="more"
/></button-icon> -->
</div>
</div> </div>
</div> </div>
<div class="progress-bar"> <div class="progress-bar">
@ -153,7 +120,7 @@
: $t('player.repeat') : $t('player.repeat')
" "
:class="{ active: player.repeatMode !== 'off' }" :class="{ active: player.repeatMode !== 'off' }"
@click.native="switchRepeatMode" @click.native="player.switchRepeatMode"
> >
<svg-icon <svg-icon
v-show="player.repeatMode !== 'one'" v-show="player.repeatMode !== 'one'"
@ -168,21 +135,21 @@
<button-icon <button-icon
v-show="!player.isPersonalFM" v-show="!player.isPersonalFM"
:title="$t('player.previous')" :title="$t('player.previous')"
@click.native="playPrevTrack" @click.native="player.playPrevTrack"
> >
<svg-icon icon-class="previous" /> <svg-icon icon-class="previous" />
</button-icon> </button-icon>
<button-icon <button-icon
v-show="player.isPersonalFM" v-show="player.isPersonalFM"
title="不喜欢" title="不喜欢"
@click.native="moveToFMTrash" @click.native="player.moveToFMTrash"
> >
<svg-icon icon-class="thumbs-down" /> <svg-icon icon-class="thumbs-down" />
</button-icon> </button-icon>
<button-icon <button-icon
id="play" id="play"
:title="$t(player.playing ? 'player.pause' : 'player.play')" :title="$t(player.playing ? 'player.pause' : 'player.play')"
@click.native="playOrPause" @click.native="player.playOrPause"
> >
<svg-icon :icon-class="player.playing ? 'pause' : 'play'" /> <svg-icon :icon-class="player.playing ? 'pause' : 'play'" />
</button-icon> </button-icon>
@ -197,32 +164,10 @@
v-show="!player.isPersonalFM" v-show="!player.isPersonalFM"
:title="$t('player.shuffle')" :title="$t('player.shuffle')"
:class="{ active: player.shuffle }" :class="{ active: player.shuffle }"
@click.native="switchShuffle" @click.native="player.switchShuffle"
> >
<svg-icon icon-class="shuffle" /> <svg-icon icon-class="shuffle" />
</button-icon> </button-icon>
<button-icon
v-show="
isShowLyricTypeSwitch &&
$store.state.settings.showLyricsTranslation &&
lyricType === 'translation'
"
:title="$t('player.translationLyric')"
@click.native="switchLyricType"
>
<span class="lyric-switch-icon"></span>
</button-icon>
<button-icon
v-show="
isShowLyricTypeSwitch &&
$store.state.settings.showLyricsTranslation &&
lyricType === 'romaPronunciation'
"
:title="$t('player.PronunciationLyric')"
@click.native="switchLyricType"
>
<span class="lyric-switch-icon"></span>
</button-icon>
</div> </div>
</div> </div>
</div> </div>
@ -237,7 +182,7 @@
> >
<div id="line-1" class="line"></div> <div id="line-1" class="line"></div>
<div <div
v-for="(line, index) in lyricToShow" v-for="(line, index) in lyricWithTranslation"
:id="`line${index}`" :id="`line${index}`"
:key="index" :key="index"
class="line" class="line"
@ -247,39 +192,17 @@
@click="clickLyricLine(line.time)" @click="clickLyricLine(line.time)"
@dblclick="clickLyricLine(line.time, true)" @dblclick="clickLyricLine(line.time, true)"
> >
<div class="content"> <span v-if="line.contents[0]">{{ line.contents[0] }}</span>
<span <br />
v-if="line.contents[0]" <span
@click.right="openLyricMenu($event, line, 0)"
>{{ line.contents[0] }}</span
>
<br />
<span
v-if="
line.contents[1] &&
$store.state.settings.showLyricsTranslation
"
class="translation"
@click.right="openLyricMenu($event, line, 1)"
>{{ line.contents[1] }}</span
>
</div>
</div>
<ContextMenu v-if="!noLyric" ref="lyricMenu">
<div class="item" @click="copyLyric(false)">{{
$t('contextMenu.copyLyric')
}}</div>
<div
v-if=" v-if="
rightClickLyric && line.contents[1] &&
rightClickLyric.contents[1] &&
$store.state.settings.showLyricsTranslation $store.state.settings.showLyricsTranslation
" "
class="item" class="translation"
@click="copyLyric(true)" >{{ line.contents[1] }}</span
>{{ $t('contextMenu.copyLyricWithTranslation') }}</div
> >
</ContextMenu> </div>
</div> </div>
</transition> </transition>
</div> </div>
@ -288,12 +211,6 @@
<svg-icon icon-class="arrow-down" /> <svg-icon icon-class="arrow-down" />
</button> </button>
</div> </div>
<div class="close-button" style="left: 24px" @click="fullscreen">
<button>
<svg-icon v-if="isFullscreen" icon-class="fullscreen-exit" />
<svg-icon v-else icon-class="fullscreen" />
</button>
</div>
</div> </div>
</transition> </transition>
</template> </template>
@ -304,37 +221,28 @@
import { mapState, mapMutations, mapActions } from 'vuex'; import { mapState, mapMutations, mapActions } from 'vuex';
import VueSlider from 'vue-slider-component'; import VueSlider from 'vue-slider-component';
import ContextMenu from '@/components/ContextMenu.vue';
import { formatTrackTime } from '@/utils/common'; import { formatTrackTime } from '@/utils/common';
import { getLyric } from '@/api/track'; import { getLyric } from '@/api/track';
import { lyricParser, copyLyric } from '@/utils/lyrics'; import { lyricParser } from '@/utils/lyrics';
import ButtonIcon from '@/components/ButtonIcon.vue'; import ButtonIcon from '@/components/ButtonIcon.vue';
import * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js'; import * as Vibrant from 'node-vibrant';
import Color from 'color'; import Color from 'color';
import { isAccountLoggedIn } from '@/utils/auth';
import { hasListSource, getListSourcePath } from '@/utils/playList'; import { hasListSource, getListSourcePath } from '@/utils/playList';
import locale from '@/locale';
export default { export default {
name: 'Lyrics', name: 'Lyrics',
components: { components: {
VueSlider, VueSlider,
ButtonIcon, ButtonIcon,
ContextMenu,
}, },
data() { data() {
return { return {
lyricsInterval: null, lyricsInterval: null,
lyric: [], lyric: [],
tlyric: [], tlyric: [],
romalyric: [],
lyricType: 'translation', // or 'romaPronunciation'
highlightLyricIndex: -1, highlightLyricIndex: -1,
minimize: true, minimize: true,
background: '', background: '',
date: this.formatTime(new Date()),
isFullscreen: !!document.fullscreenElement,
rightClickLyric: null,
}; };
}, },
computed: { computed: {
@ -342,28 +250,12 @@ export default {
currentTrack() { currentTrack() {
return this.player.currentTrack; return this.player.currentTrack;
}, },
volume: {
get() {
return this.player.volume;
},
set(value) {
this.player.volume = value;
},
},
imageUrl() { imageUrl() {
return this.player.currentTrack?.al?.picUrl + '?param=1024y1024'; return this.player.currentTrack?.al?.picUrl + '?param=1024y1024';
}, },
bgImageUrl() { bgImageUrl() {
return this.player.currentTrack?.al?.picUrl + '?param=512y512'; return this.player.currentTrack?.al?.picUrl + '?param=512y512';
}, },
isShowLyricTypeSwitch() {
return this.romalyric.length > 0 && this.tlyric.length > 0;
},
lyricToShow() {
return this.lyricType === 'translation'
? this.lyricWithTranslation
: this.lyricWithRomaPronunciation;
},
lyricWithTranslation() { lyricWithTranslation() {
let ret = []; let ret = [];
// //
@ -395,37 +287,6 @@ export default {
} }
return ret; return ret;
}, },
lyricWithRomaPronunciation() {
let ret = [];
//
const lyricFiltered = this.lyric.filter(({ content }) =>
Boolean(content)
);
// content
if (lyricFiltered.length) {
lyricFiltered.forEach(l => {
const { rawTime, time, content } = l;
const lyricItem = { time, content, contents: [content] };
const sameTimeRomaLyric = this.romalyric.find(
({ rawTime: tLyricRawTime }) => tLyricRawTime === rawTime
);
if (sameTimeRomaLyric) {
const { content: romaLyricContent } = sameTimeRomaLyric;
if (content) {
lyricItem.contents.push(romaLyricContent);
}
}
ret.push(lyricItem);
});
} else {
ret = lyricFiltered.map(({ time, content }) => ({
time,
content,
contents: [content],
}));
}
return ret;
},
lyricFontSize() { lyricFontSize() {
return { return {
fontSize: `${this.$store.state.settings.lyricFontSize || 28}px`, fontSize: `${this.$store.state.settings.lyricFontSize || 28}px`,
@ -464,77 +325,13 @@ export default {
created() { created() {
this.getLyric(); this.getLyric();
this.getCoverColor(); this.getCoverColor();
this.initDate();
document.addEventListener('keydown', e => {
if (e.key === 'F11') {
e.preventDefault();
this.fullscreen();
}
});
document.addEventListener('fullscreenchange', () => {
this.isFullscreen = !!document.fullscreenElement;
});
},
beforeDestroy: function () {
if (this.timer) {
clearInterval(this.timer);
}
}, },
destroyed() { destroyed() {
clearInterval(this.lyricsInterval); clearInterval(this.lyricsInterval);
}, },
methods: { methods: {
...mapMutations(['toggleLyrics', 'updateModal']), ...mapMutations(['toggleLyrics']),
...mapActions(['likeATrack']), ...mapActions(['likeATrack']),
initDate() {
var _this = this;
clearInterval(this.timer);
this.timer = setInterval(function () {
_this.date = _this.formatTime(new Date());
}, 1000);
},
formatTime(value) {
let hour = value.getHours().toString();
let minute = value.getMinutes().toString();
let second = value.getSeconds().toString();
return (
hour.padStart(2, '0') +
':' +
minute.padStart(2, '0') +
':' +
second.padStart(2, '0')
);
},
fullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen();
}
},
addToPlaylist() {
if (!isAccountLoggedIn()) {
this.showToast(locale.t('toast.needToLogin'));
return;
}
this.$store.dispatch('fetchLikedPlaylist');
this.updateModal({
modalName: 'addTrackToPlaylistModal',
key: 'show',
value: true,
});
this.updateModal({
modalName: 'addTrackToPlaylistModal',
key: 'selectedTrackID',
value: this.currentTrack?.id,
});
},
playPrevTrack() {
this.player.playPrevTrack();
},
playOrPause() {
this.player.playOrPause();
},
playNextTrack() { playNextTrack() {
if (this.player.isPersonalFM) { if (this.player.isPersonalFM) {
this.player.playNextFMTrack(); this.player.playNextFMTrack();
@ -548,10 +345,9 @@ export default {
if (!data?.lrc?.lyric) { if (!data?.lrc?.lyric) {
this.lyric = []; this.lyric = [];
this.tlyric = []; this.tlyric = [];
this.romalyric = [];
return false; return false;
} else { } else {
let { lyric, tlyric, romalyric } = lyricParser(data); let { lyric, tlyric } = lyricParser(data);
lyric = lyric.filter( lyric = lyric.filter(
l => !/^作(词|曲)\s*(:|)\s*无$/.exec(l.content) l => !/^作(词|曲)\s*(:|)\s*无$/.exec(l.content)
); );
@ -571,27 +367,15 @@ export default {
if (lyric.length === 1 && includeAM) { if (lyric.length === 1 && includeAM) {
this.lyric = []; this.lyric = [];
this.tlyric = []; this.tlyric = [];
this.romalyric = [];
return false; return false;
} else { } else {
this.lyric = lyric; this.lyric = lyric;
this.tlyric = tlyric; this.tlyric = tlyric;
this.romalyric = romalyric;
if (tlyric.length * romalyric.length > 0) {
this.lyricType = 'translation';
} else {
this.lyricType =
lyric.length > 0 ? 'translation' : 'romaPronunciation';
}
return true; return true;
} }
} }
}); });
}, },
switchLyricType() {
this.lyricType =
this.lyricType === 'translation' ? 'romaPronunciation' : 'translation';
},
formatTrackTime(value) { formatTrackTime(value) {
return formatTrackTime(value); return formatTrackTime(value);
}, },
@ -610,24 +394,9 @@ export default {
this.player.play(); this.player.play();
} }
}, },
openLyricMenu(e, lyric, idx) {
this.rightClickLyric = { ...lyric, idx };
this.$refs.lyricMenu.openMenu(e);
e.preventDefault();
},
copyLyric(withTranslation) {
if (this.rightClickLyric) {
const idx = this.rightClickLyric.idx;
if (!withTranslation) {
copyLyric(this.rightClickLyric.contents[idx]);
} else {
copyLyric(this.rightClickLyric.contents.join(' '));
}
}
},
setLyricsInterval() { setLyricsInterval() {
this.lyricsInterval = setInterval(() => { this.lyricsInterval = setInterval(() => {
const progress = this.player.seek(null, false) ?? 0; const progress = this.player.seek() ?? 0;
let oldHighlightLyricIndex = this.highlightLyricIndex; let oldHighlightLyricIndex = this.highlightLyricIndex;
this.highlightLyricIndex = this.lyric.findIndex((l, index) => { this.highlightLyricIndex = this.lyric.findIndex((l, index) => {
const nextLyric = this.lyric[index + 1]; const nextLyric = this.lyric[index + 1];
@ -648,21 +417,15 @@ export default {
moveToFMTrash() { moveToFMTrash() {
this.player.moveToFMTrash(); this.player.moveToFMTrash();
}, },
switchRepeatMode() {
this.player.switchRepeatMode();
},
switchShuffle() {
this.player.switchShuffle();
},
getCoverColor() { getCoverColor() {
if (this.settings.lyricsBackground !== true) return; if (this.settings.lyricsBackground !== true) return;
const cover = this.currentTrack.al?.picUrl + '?param=256y256'; const cover = this.currentTrack.al?.picUrl + '?param=1024y1024';
Vibrant.from(cover, { colorCount: 1 }) Vibrant.from(cover, { colorCount: 1 })
.getPalette() .getPalette()
.then(palette => { .then(palette => {
const originColor = Color.rgb(palette.DarkMuted._rgb); const orignColor = Color.rgb(palette.DarkMuted._rgb);
const color = originColor.darken(0.1).rgb().string(); const color = orignColor.darken(0.1).rgb().string();
const color2 = originColor.lighten(0.28).rotate(-30).rgb().string(); const color2 = orignColor.lighten(0.28).rotate(-30).rgb().string();
this.background = `linear-gradient(to top left, ${color}, ${color2})`; this.background = `linear-gradient(to top left, ${color}, ${color2})`;
}); });
}, },
@ -672,9 +435,6 @@ export default {
getListPath() { getListPath() {
return getListSourcePath(); return getListSourcePath();
}, },
mute() {
this.player.mute();
},
}, },
}; };
</script> </script>
@ -762,20 +522,6 @@ export default {
z-index: 1; z-index: 1;
.date {
max-width: 54vh;
margin: 24px 0;
color: var(--color-text);
text-align: center;
font-size: 4rem;
font-weight: 600;
opacity: 0.88;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.controls { .controls {
max-width: 54vh; max-width: 54vh;
margin-top: 24px; margin-top: 24px;
@ -806,31 +552,17 @@ export default {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.top-right { .buttons {
display: flex; display: flex;
justify-content: space-between; align-items: center;
.volume-control { button {
margin: 0 10px; margin: 0 0 0 4px;
display: flex;
align-items: center;
.volume-bar {
width: 84px;
}
} }
.buttons { .svg-icon {
display: flex; height: 18px;
align-items: center; width: 18px;
button {
margin: 0 0 0 4px;
}
.svg-icon {
height: 18px;
width: 18px;
}
} }
} }
} }
@ -895,12 +627,6 @@ export default {
width: 22px; width: 22px;
} }
} }
.lyric-switch-icon {
color: var(--color-text);
font-size: 14px;
line-height: 14px;
opacity: 0.88;
}
} }
} }
} }
@ -959,24 +685,20 @@ export default {
&:hover { &:hover {
background: var(--color-secondary-bg-for-transparent); background: var(--color-secondary-bg-for-transparent);
} }
&:active {
.content {
transform-origin: center left;
transform: scale(0.95); transform: scale(0.95);
}
span {
opacity: 0.28;
cursor: default;
font-size: 1em;
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94); transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
user-select: none; }
span { span.translation {
opacity: 0.28; opacity: 0.2;
cursor: default; font-size: 0.95em;
font-size: 1em;
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
span.translation {
opacity: 0.2;
font-size: 0.925em;
}
} }
} }
@ -988,16 +710,15 @@ export default {
margin-top: 0.1em; margin-top: 0.1em;
} }
.highlight div.content { .highlight span {
transform: scale(1); opacity: 0.98;
span { display: inline-block;
opacity: 0.98; font-size: 1.25em;
display: inline-block; }
}
span.translation { .highlight span.translation {
opacity: 0.65; opacity: 0.65;
} font-size: 1.1em;
} }
} }

View file

@ -11,14 +11,11 @@
}}</router-link> }}</router-link>
- -
{{ mv.data.name }} {{ mv.data.name }}
<div class="buttons"> <div class="like-button">
<button-icon class="button" @click.native="likeMV"> <button-icon @click.native="likeMV">
<svg-icon v-if="mv.subed" icon-class="heart-solid"></svg-icon> <svg-icon v-if="mv.subed" icon-class="heart-solid"></svg-icon>
<svg-icon v-else icon-class="heart"></svg-icon> <svg-icon v-else icon-class="heart"></svg-icon>
</button-icon> </button-icon>
<button-icon class="button" @click.native="openMenu">
<svg-icon icon-class="more"></svg-icon>
</button-icon>
</div> </div>
</div> </div>
<div class="info"> <div class="info">
@ -31,14 +28,6 @@
<div class="section-title">{{ $t('mv.moreVideo') }}</div> <div class="section-title">{{ $t('mv.moreVideo') }}</div>
<MvRow :mvs="simiMvs" /> <MvRow :mvs="simiMvs" />
</div> </div>
<ContextMenu ref="mvMenu">
<div class="item" @click="copyUrl(mv.data.id)">{{
$t('contextMenu.copyUrl')
}}</div>
<div class="item" @click="openInBrowser(mv.data.id)">{{
$t('contextMenu.openInBrowser')
}}</div>
</ContextMenu>
</div> </div>
</template> </template>
@ -51,7 +40,6 @@ import '@/assets/css/plyr.css';
import Plyr from 'plyr'; import Plyr from 'plyr';
import ButtonIcon from '@/components/ButtonIcon.vue'; import ButtonIcon from '@/components/ButtonIcon.vue';
import ContextMenu from '@/components/ContextMenu.vue';
import MvRow from '@/components/MvRow.vue'; import MvRow from '@/components/MvRow.vue';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
@ -60,7 +48,6 @@ export default {
components: { components: {
MvRow, MvRow,
ButtonIcon, ButtonIcon,
ContextMenu,
}, },
beforeRouteUpdate(to, from, next) { beforeRouteUpdate(to, from, next) {
this.getData(to.params.id); this.getData(to.params.id);
@ -140,23 +127,6 @@ export default {
if (data.code === 200) this.mv.subed = !this.mv.subed; if (data.code === 200) this.mv.subed = !this.mv.subed;
}); });
}, },
openMenu(e) {
this.$refs.mvMenu.openMenu(e);
},
copyUrl(id) {
let showToast = this.showToast;
this.$copyText(`https://music.163.com/#/mv?id=${id}`)
.then(function () {
showToast(locale.t('toast.copied'));
})
.catch(error => {
showToast(`${locale.t('toast.copyFailed')}${error}`);
});
},
openInBrowser(id) {
const url = `https://music.163.com/#/mv?id=${id}`;
window.open(url);
},
}, },
}; };
</script> </script>
@ -211,11 +181,8 @@ export default {
} }
} }
.buttons { .like-button {
display: inline-block; display: inline-block;
.button {
display: inline-block;
}
.svg-icon { .svg-icon {
height: 18px; height: 18px;
width: 18px; width: 18px;

View file

@ -57,9 +57,7 @@ export default {
this.player.current + 1, this.player.current + 1,
this.player.current + 100 this.player.current + 100
); );
return trackIDs return this.tracks.filter(t => trackIDs.includes(t.id));
.map(tid => this.tracks.find(t => t.id === tid))
.filter(t => t);
}, },
playNextList() { playNextList() {
return this.player.playNextList; return this.player.playNextList;

View file

@ -27,7 +27,11 @@
<span <span
v-if=" v-if="
[ [
5277771961, 5277965913, 5277969451, 5277778542, 5278068783, 5277771961,
5277965913,
5277969451,
5277778542,
5278068783,
].includes(playlist.id) ].includes(playlist.id)
" "
style="font-weight: 600" style="font-weight: 600"
@ -139,12 +143,9 @@
<div v-if="isLikeSongsPage" class="user-info"> <div v-if="isLikeSongsPage" class="user-info">
<h1> <h1>
<img <img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
class="avatar" data.user.nickname
:src="data.user.avatarUrl | resizeImage" }}{{ $t('library.sLikedSongs') }}
loading="lazy"
/>
{{ data.user.nickname }}{{ $t('library.sLikedSongs') }}
</h1> </h1>
<div class="search-box-likepage" @click="searchInPlaylist()"> <div class="search-box-likepage" @click="searchInPlaylist()">
<div class="container" :class="{ active: inputFocus }"> <div class="container" :class="{ active: inputFocus }">
@ -318,10 +319,6 @@ const specialPlaylist = {
name: '一周原创发现', name: '一周原创发现',
gradient: 'gradient-blue-purple', gradient: 'gradient-blue-purple',
}, },
2829883282: {
name: '华语私人雷达',
gradient: 'gradient-yellow-red',
},
3136952023: { 3136952023: {
name: '私人雷达', name: '私人雷达',
gradient: 'gradient-radar', gradient: 'gradient-radar',

File diff suppressed because one or more lines are too long

View file

@ -1,12 +1,9 @@
const webpack = require('webpack');
const path = require('path'); const path = require('path');
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, dir); return path.join(__dirname, dir);
} }
module.exports = { module.exports = {
// 生产环境打包不输出 map
productionSourceMap: false,
devServer: { devServer: {
disableHostCheck: true, disableHostCheck: true,
port: process.env.DEV_SERVER_PORT || 8080, port: process.env.DEV_SERVER_PORT || 8080,
@ -56,37 +53,16 @@ module.exports = {
symbolId: 'icon-[name]', symbolId: 'icon-[name]',
}) })
.end(); .end();
config.module
.rule('napi')
.test(/\.node$/)
.use('node-loader')
.loader('node-loader')
.end();
config.module
.rule('webpack4_es_fallback')
.test(/\.js$/)
.include.add(/node_modules/)
.end()
.use('esbuild-loader')
.loader('esbuild-loader')
.options({ target: 'es2015', format: "cjs" })
.end();
// LimitChunkCountPlugin 可以通过合并块来对块进行后期处理。用以解决 chunk 包太多的问题
config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [
{
maxChunks: 3,
minChunkSize: 10_000,
},
]);
}, },
// 添加插件的配置 // 添加插件的配置
pluginOptions: { pluginOptions: {
// electron-builder的配置文件 // electron-builder的配置文件
electronBuilder: { electronBuilder: {
nodeIntegration: true, nodeIntegration: true,
externals: ['@unblockneteasemusic/rust-napi'], externals: [
'@unblockneteasemusic/server',
'@unblockneteasemusic/server/src/consts',
],
builderOptions: { builderOptions: {
productName: 'YesPlayMusic', productName: 'YesPlayMusic',
copyright: 'Copyright © YesPlayMusic', copyright: 'Copyright © YesPlayMusic',
@ -179,16 +155,6 @@ module.exports = {
'jsbi', 'jsbi',
path.join(__dirname, 'node_modules/jsbi/dist/jsbi-cjs.js') path.join(__dirname, 'node_modules/jsbi/dist/jsbi-cjs.js')
); );
config.module
.rule('webpack4_es_fallback')
.test(/\.js$/)
.include.add(/node_modules/)
.end()
.use('esbuild-loader')
.loader('esbuild-loader')
.options({ target: 'es2015', format: "cjs" })
.end();
}, },
// 渲染线程的配置文件 // 渲染线程的配置文件
chainWebpackRendererProcess: config => { chainWebpackRendererProcess: config => {

10981
yarn.lock

File diff suppressed because it is too large Load diff