Compare commits

...

157 commits

Author SHA1 Message Date
71817baad0 去除听歌打卡功能 2025-06-17 09:13:43 +08:00
f6735c9c26
Update README.md
Some checks failed
Release / release (macos-latest) (push) Has been cancelled
Release / release (ubuntu-22.04) (push) Has been cancelled
Release / release (windows-latest) (push) Has been cancelled
更换demo的url
2025-06-15 22:39:46 +08:00
708a1a8eb8 去除签到功能、邮箱和登录入口 2025-06-14 12:36:22 +08:00
runnableAir
70ab357799
fix: 随机模式下当前播放歌曲于列表中的位置出现错误和不同步的情况 (#2378)
* fix(Player.js): 随机模式下列表初始化时当前歌曲的下标异常

异常情况:假设歌曲A其在歌单中的位置为10,随机模式下双击该歌曲后,this.current != 10
原因:随机模式下,通过 indexOf 计算 current 时,调用的数组与真实的列表数组不一致

* fix(Player.js): 随机模式切换时未同步当前歌曲下标

* fix: 随机播放顺序与实际列表不一致

使用 filter 不保证返回的数组的元素顺序与传入的id 顺序对应
2025-05-30 19:59:52 +08:00
李洋
c7e69158d2
Update package.json (#2347)
将版本号从0.4.8改成了0.4.9(有点强迫症)
2025-03-03 19:08:59 +08:00
Yi-Jyun Pan
022009b140
chore: upgrade dependencies 2024-11-01 15:01:39 +08:00
Yi-Jyun Pan
6849632abe
chore: add AppKit framework for macOS 2024-11-01 15:00:45 +08:00
Yi-Jyun Pan
c929beaf6c
Merge branch 'feat-realip' 2024-11-01 14:55:35 +08:00
Yi-Jyun Pan
dfb09b3ccd
chore: add devenv configuration 2024-11-01 14:55:06 +08:00
CuSO4Deposit
9809a758b4
style: Reformat with Prettier (#2296) 2024-11-01 14:20:55 +08:00
Frz
c9739b2d0e
feat: use cover image for Discord RPC (#2294) 2024-11-01 14:20:30 +08:00
bestlaw66
25e274a4f8
docs: 宝塔面板部署教程 2024-11-01 14:19:23 +08:00
pan93412
fc1c8d8512
Merge pull request #2292 from Younglina/master
添加复制歌词功能
2024-11-01 14:18:45 +08:00
StarsEnd
e27879aa94 update: NeteaseCloudMusicApi (iPhone OS fix) 2024-10-16 00:16:23 +08:00
StarsEnd
d219e48541 fix: real-ip config disabled css style 2024-10-16 00:14:38 +08:00
StarsEnd33A2D17
107f5765b0 Update to the latest NeteaseCloudMusicApi 2024-10-08 20:29:05 +08:00
StarsEnd33A2D17
adafffd86b feat: add RealIP setting 2024-09-30 13:35:22 +08:00
Younglina
9d807d1d63 feat: 复制歌词 2024-08-27 13:21:01 +08:00
Younglina
481ba6bce3
fix: 相似歌手接口需要登录;未登录获取艺人热门歌曲没有图片 (#2286) 2024-08-13 18:20:52 +08:00
StarsEnd
df82c7cd22
feat(ui): add fullscreen button (#2276)
* feat(ui): add fullscreen button

* fix: fullscreen图标修改,添加exit icon
2024-08-13 14:28:05 +08:00
Younglina
bd5af9c721
fix: 默认值错误导致加载空节点 (#2280) 2024-08-13 14:26:02 +08:00
5unV
7cb063d511
fix: 纠正脏标的判断 (#2268) 2024-08-07 22:37:29 +08:00
Felix_SANA
904e61bee6
fix: 阻止搜索引擎爬虫爬取网站的内容 (#2239) 2024-08-07 22:36:52 +08:00
Younglina
24477694f8
fix: 手机号placeholder显示问题 (#2197) 2024-03-18 15:16:48 +08:00
pan93412
87ef48b826
Merge pull request #2193 from rainbowflesh/master
fix github action missing env
2024-03-05 06:51:01 -06:00
是虹川肉
da8afc12cf
Update request.js 2024-03-05 19:00:27 +08:00
是虹川肉
84613dcf8a
Update build.yaml 2024-03-05 18:45:29 +08:00
pan93412
dd8aa175d1
chore: bump to 0.4.8 2024-03-05 00:16:14 +08:00
pan93412
e3caa24ca4
Merge pull request #2185 from undefined-ux/master
fix typo
2024-03-04 10:15:14 -06:00
pan93412
ae352f27e1
Merge pull request #2187 from DaiQiangReal/master
Fix: Fix build error and  /tmp/anonymous_token not exist.
2024-03-04 10:15:06 -06:00
代强
552a1d4b44 chore: remove useless space 2024-03-04 19:24:19 +08:00
代强
b0ed85689b chore: remove useless space 2024-03-04 19:24:10 +08:00
代强
a18e093d4a fix: fix build && adapte for bugs in NeteasyCloudMusicAPI 2024-03-04 19:22:47 +08:00
undefined
79a7c6d991
fix typo 2024-03-02 13:08:49 +08:00
pan93412
1a2c3e2843
Merge pull request #2178 from thedavidweng/master
Upstream Sync
2024-03-01 00:24:57 -06:00
pan93412
741fdc973c
Merge pull request #2127 from runnableAir/fix_player_enable
Fix: 右键播放歌曲无法激活播放栏
2024-03-01 00:13:14 -06:00
pan93412
1400636201
Merge pull request #2129 from jsonleex/patch
fix: play icon not appear in Safari
2024-03-01 00:12:22 -06:00
GH Action - Upstream Sync
6e737b50ee Merge branch 'master' of https://github.com/qier222/YesPlayMusic 2024-03-01 06:12:01 +00:00
pan93412
c409e3b6ed
Merge pull request #2167 from colawithsauce/master
[Feature] support UnblockNeteaseMusic with docker-compose deploy.
2024-03-01 00:11:16 -06:00
pan93412
e738d1e46d
Merge pull request #2176 from krishukr/master
fix(components): 修正描述小字内艺人链接
2024-03-01 00:10:25 -06:00
Davy
380c55a653
Create sync.yml 2024-02-05 17:05:06 -08:00
Kris Hu
9241b3a26a
same thing on MvRow 2024-01-31 12:33:12 +00:00
Kris Hu
3093b6f386
修正专辑下描述内艺人链接 2024-01-31 12:25:38 +00:00
colawithsauce
42366f4a32 remove unneeded empty line. 2023-12-09 01:01:04 +08:00
colawithsauce
6d6fd9a88c Make docker works 2023-12-09 00:55:11 +08:00
colawithsauce
dc1e0aaf90 Make docker works 2023-12-09 00:28:00 +08:00
colawithsauce
a5cb1f729d Remove unneeded env setting 2023-12-08 18:54:23 +08:00
colawithsauce
e997cd9907 Support unblock via docker. 2023-12-08 18:46:10 +08:00
leex
c5c7ccc89e
fix: play icon not appear in Safari 2023-09-04 22:49:21 +08:00
runnableAir
61d0b5953f refactor(Player.js): 确保在播放时播放器处于enabled状态 2023-08-31 06:16:05 +08:00
runnableAir
a5bf5c7dfd fix(Player.js): 右键播放不显示播放器(#1965) 2023-08-31 06:16:04 +08:00
pan93412
fd40a29180
Merge pull request #1818 from Revincx/revincx-pr 2023-08-26 11:17:42 +08:00
Revincx
486b04b70b
feat(player): sync playing progress to mpris service on linux
Co-authored-by: alex3236 <45303195+alex3236@users.noreply.github.com>
2023-08-26 10:31:43 +08:00
Revincx
6ad756b215
fix(ui): add max-width attr for settings selector 2023-08-26 10:31:43 +08:00
Revincx
ed1daab1f6
feat: use osdlyrics dbus interface to send lyric contents 2023-08-26 10:31:23 +08:00
runnableAir
f2f4e2ce58
fix(player): 插队曲目切换后下一首曲目丢失 (#2118) 2023-08-26 10:15:33 +08:00
Lvc Revincx
845bc8a921
Merge branch 'qier222:master' into revincx-pr 2023-08-25 23:28:35 +08:00
拆家大主教
f2efc4e682
feat(lyrics): Add pronunciation lyric mode (#2091)
* feat: Add pronunciation lyric mode

* fix(lyrics): Fixed issue where lyric-mode-switch displays when the translation setting is off
2023-08-07 23:32:28 +08:00
poly000
f4d3d67132
feat(mpris): Add xesam:url field (#2095)
chore: do not use fuo scheme, only netease music id is preserved

fix: lint prettier error
2023-08-07 23:31:38 +08:00
poly000
e14e6d73c6
ci: Use Ubuntu 22.04 for Packaging (#2107) 2023-08-07 23:30:37 +08:00
Siykt
4ec550dc46
fix(login): clear last interval when calling checkQrCodeLogin (#2094)
Fixed #2093
2023-07-24 13:01:49 +08:00
Anmizi
dd6d4bf1c6
feat(settings): Internationalize some strings (#2016) 2023-07-03 12:45:08 +08:00
guaqiu
a6e433bdc5
chore(deps): Add prettier to devDependencies (#2071) 2023-06-18 13:47:43 +08:00
Arthals
59898c7883
fix(navbar): Fixed the issue of overlapping with the control bar (#2073) 2023-06-18 13:47:20 +08:00
Kris Hu
1b7e33c222
fix: 艺人页面专辑区不显示精选集 (#2046) 2023-05-02 00:24:08 +08:00
Lvc Revincx
221ca63d3d
fix(player): Skip track when audio source not supported (#2033) 2023-04-15 10:26:38 +08:00
洩氏诹诹子
b7f7ac8d31
fix(player): 修复歌曲时长过长时的进度显示问题 (#1936)
原先进度条遇到时长超过 1hr 的歌曲,
不会呈现小时数的部分。这个 commit
将歌曲时长小时数加到分钟数中。
2023-04-08 23:16:01 +08:00
Holger
65f5df8a60
fix(request): cross domain api issue (#2026)
Fix the issue when NCMapi is not under the same domain
as the one frontend uses. The original method using
Vercel to proxy requests may cause latency under
some circumstances.
2023-04-08 23:13:28 +08:00
Younglina
8a50337854
fix(tracklist): TrackListItem 序号问题 (#2011)
直接使用 track.no 可能导致歌曲编号重复。改使用曲目在
阵列中的实际索引位置。
2023-04-08 23:12:13 +08:00
moonrailgun
7b97ac0139
chore: define node engines (#1943)
The `@achrinza/node-ipc` version we use in 1.x does not allow
a version of Node.js greater than 17. As YesPlayMusic has been in
maintenance mode, we define our supported Node.js engine version
rather than upgrade this dependency.

It may indicate the Vercel platform to not use 18 (or greater) in their
deployment.

The error message is:

    error @achrinza/node-ipc@9.2.2: The engine "node" is incompatible with this module. Expected version "8 || 10 || 12 || 14 || 16 || 17". Got "18.12.1"
2023-04-08 23:08:52 +08:00
qier222
1cb3e4b29f
Update README.md 2023-03-27 00:09:47 +08:00
Younglina
c89ebbdd22
fix: album.company显示问题 (#2009) 2023-03-22 14:16:47 +08:00
qier222
ce738f6b40
revert: change port to workaround 21H2's port 2023-01-30 12:38:38 +08:00
qier222
2a0af8f975
fix: update version 2023-01-28 12:16:47 +08:00
qier222
2f452dbe74
fix: bugs 2023-01-28 12:06:57 +08:00
タイムライン
622f95439d
fix: player volume bug (#1918)
* fix: player volume bug

* Update Player.js

* Update Player.js

* Update Player.js

* Update Player.js
2023-01-28 11:53:54 +08:00
タイムライン
210e65dd9a
update copyright year (#1917) 2023-01-27 01:11:48 +08:00
草方块
241de709da
fix: 更新依赖以解决手机号登录问题;UnblockNeteaseMusic 更新 (#1915)
* fix: 更新api以尝试解决邮箱登录问题

* fix: 又双更新api版本以修复手机号登录问题

(可能

* deps: [测试]更新UNM到0.4.0
2023-01-24 20:09:33 +08:00
草方块
c6804decfc fix: 更新api以尝试解决邮箱登录问题 2022-12-30 17:02:49 +08:00
Karbob
75d3e28ef8 feat: mount local time and time zone 2022-12-15 09:36:25 +08:00
Karbob
99371def54 refactor(dockerfile): separate nginx config from Dockerfile 2022-12-15 09:36:25 +08:00
Karbob
70d2713643 refactor(dockerfile): use awk to find NCMAPI version 2022-12-15 09:36:25 +08:00
Revincx
41b72563ff
refactor: improve lyric file download implement 2022-10-22 13:59:49 +08:00
Revincx
345f3588bd
chore: improve translations 2022-10-22 12:36:57 +08:00
Revincx
022f740c3f
feat: osdlyrics desktop lyrics support 2022-10-05 16:21:18 +08:00
Revincx
ce778afff6
feat: Tray icon theme now follows system theme 2022-10-05 15:23:11 +08:00
Kay W
9fcb6da960
feat: Add status text to like button when hover event triggers (#1789)
Signed-off-by: Kay Wei <weikaiii@sina.cn>
2022-09-09 12:45:22 +08:00
jbwfu
b589f82b6c
feat: add tutorial to deploy on Replit (#1731) 2022-08-31 12:08:13 +08:00
pan93412
f9e6164245
fix(ipcMain): unexpected “本草綱目“ from migu
This is just a workaround, and the core issue is on UNM.
However, refactoring UNM is time-comsuming and also requires
a lot of hard work. Therefore, we disable "migu" at this moment
simply.

Fixed #1713
Fixed #1711 (?)
2022-06-28 11:34:20 +08:00
pan93412
43c5bda806
chore(deps): upgrade dependencies (#1708) 2022-06-22 14:29:38 +08:00
pan93412
6d3508c62a
fix: change port to workaround 21H2's port (#1706)
* fix: change port to 35216 → workaround 21H2's port
* style: with restyled (#1707)
* Restyled by clang-format
* Restyled by prettier
* fix: change 21232 to 41342

Co-authored-by: restyled-io[bot] <32688539+restyled-io[bot]@users.noreply.github.com>
Co-authored-by: Restyled.io <commits@restyled.io>
2022-06-22 14:29:18 +08:00
marcus
9e64222bdf
feat: Add time to lyric page (#1676)
* feat: Add time on lyrics page

* feat: Add the setting item of whether to display the time on the lyrics page

* fix: fix some issue
2022-06-21 13:45:43 +08:00
marcus
7b911c1658
feat: Add "Volume Control" to Lyrics Page (#1672)
* feat: Add "Volume Control" to Lyrics Page

* fix: fix mute button
2022-06-21 13:45:32 +08:00
Viyerelu23333
a31d552788
fix(dockerfile): 设置 NCMAPI 版本 (#1689)
* fix: dockerfile ncapi pinning

* feat: ncmapi follows version in package.json
2022-06-19 20:54:08 +08:00
marcus
000cfda922
feat: Added "Add to Playlist" on lyrics page (#1671) 2022-06-19 20:53:28 +08:00
Changjian Gao
0abd616ca1
feat: Add context menu on MV page (#1670) 2022-06-19 20:52:26 +08:00
memorydream
2a2ac5a37d
fix: 私人推荐歌单 (#1665)
* fix: 私人推荐歌单

* update
2022-06-19 20:51:42 +08:00
memorydream
1496a8a0d0
fix: 歌名翻译文本位置 & FMCard背景颜色 (#1650)
* fix: 歌名翻译文本位置

* fix: 使FMCard的背景颜色永远随着歌曲改变
2022-06-19 20:51:30 +08:00
memorydream
6b690baef6
fix: 修复在长时间暂停后无法播放的问题 (#1627)
* fix: 修复在长时间暂停后无法播放的问题

* 增加doc
2022-06-19 20:51:16 +08:00
郭桓桓
b9cdade832 chore(ci/cd): bump actions/upload-artifact to v3 2022-06-03 14:45:50 +08:00
郭桓桓
fbc1e9903e chore(ci/cd): bump actions/checkout to v3 2022-06-03 14:45:50 +08:00
memorydream
439f368fd6
fix: 音乐库收藏的歌单少了第一张歌单 (#1657) 2022-05-23 01:27:49 +08:00
memorydream
bbbd729fdf
fix: 部分linux发行版环境的托盘行为 (#1647) 2022-05-21 06:03:24 +08:00
memorydream
c1efcb895c
fix: 歌词页视觉效果缺陷 (#1649) 2022-05-21 06:03:11 +08:00
qier222
cb59eb94a1
docs: update vercel links 2022-05-20 00:07:55 +08:00
pan93412
f355d5da50
fix(dockerfile): 限制最大標頭大小
原本的Nginx.conf没有定义最大标头大小。若不手动更改,则会出现无法登陆的bug,且Nginx会返回Header too big错误。

Fixed #1604

Co-Authored-By: huangyinhaow <64564727+huangyinhaow@users.noreply.github.com>
2022-05-14 20:07:13 +08:00
memorydream
6e1d58964e
fix: 专辑分碟排序错误 (#1630)
* fix: 专辑分碟排序错误

* update
2022-05-12 01:23:13 +08:00
qier222
c3aea5ee8d
fix: bug 2022-05-10 12:15:03 +08:00
qier222
f064859a27
docs: add powered by vercel 2022-05-10 12:10:47 +08:00
Hawtim
9e787bab03
Merge pull request #1598 from hawtim/fix-history-list-error
fix: render weekData error
2022-05-02 14:44:48 +08:00
Hawtim
97ac4117db
Merge pull request #1600 from hawtim/fix-node-vibrant-worker
Fix-node-vibrant-worker
2022-05-02 14:38:08 +08:00
Hawtim
8cd8ae4255
Merge pull request #1597 from hawtim/feature/support-image-lazy-loading
feat: add img tag with loading attribute for lazy loading
2022-05-02 14:36:06 +08:00
hawtimzhang
35edd84c22 fix: vibrant worker error 2022-05-02 14:30:11 +08:00
hawtimzhang
4613feff18 fix: render weekData error 2022-05-02 11:27:08 +08:00
hawtimzhang
fab099c6fb feat: add img tag with loading attribute for lazy loading 2022-05-02 00:47:42 +08:00
memorydream
107bf53a39
fix: 错误的选择了音源质量 (#1589) 2022-05-01 14:13:11 +08:00
qier222
e0f2d3fd57
build: release 0.4.5 2022-05-01 01:25:16 +08:00
memorydream
93c7ba2fd8
feat: 支持Hi-Res (#1585) 2022-05-01 01:24:20 +08:00
memorydream
5dd00bec87
fix: 修复因为使用了electron的clipboard导致的异常 (#1582) 2022-04-30 17:24:13 +08:00
memorydream
21c7b5ae44
feat: UNM 设置页面 l10n (#1578)
* feat: UNM 设置页面 l10n

* update translate

* update
2022-04-30 14:49:24 +08:00
memorydream
a9b05d66a6
fix(components/Titlebar): 还原/最大化按钮在双击标题栏时不会更新的问题 (#1575)
* fix(components/Titlebar): `还原/最大化`按钮在双击标题栏时不会更新的问题

* update
2022-04-29 19:52:22 +08:00
memorydream
c85af59b21
fix: bilibili音源无法播放的问题 (#1573) 2022-04-28 14:45:00 +08:00
pan93412
93ae57adbe
feat: switch to UNM (Rust) (#1536)
* refactor: use unm-rust-napi

* ci(build): install UNM dependencies for certain platforms

* feat: add the ability to configure UNM

* feat: add the UNM configuration in settings page

* refactor(jsconfig): jsx -> preserve

* fix(ci/build): use bash to get unm version

* chore(deps): upgrade UNM to 0.3.0-pre.0

* refactor(electron/ipcMain): update default sources

* fix(views/settings): remove duplicated config entry

* feat(settings): allow configuring QQ cookie

We also removed some duplicate entries in views/settings.vue.

* chore(deps): UNM -> 0.3.0-pre.1

* refactor: remove unused old UNM

* fix(utils/player): do not include rust-napi in client code

As we only imported the constant, I just expand it as the integer.

Co-authored-by: qier222 <qier222@outlook.com>
2022-04-28 01:02:41 +08:00
pan93412
e1f7618cbd refactor(views/lyrics): tweak size 2022-04-27 20:53:49 +08:00
memorydream
3c798a5606
feat: 使用svg输出登陆二维码 (#1568) 2022-04-27 18:38:37 +08:00
pan93412
d87c4bad21
chore(deps): update some dependencies to latest (#1564)
* chore(deps): upgrade dependencies

* chore(deps): upgrade some deps to major version

已經查過這些 packages 的 Changelog,確定是安全、可升級的。此外也進行了簡單的 E2E 測試,可正常使用。
2022-04-27 18:37:52 +08:00
pan93412
177a8c8eff fix(views/loginAccount): improve clarity of QR Code
渲染 384px 的 QR Code,而顯示在 192px 的方框中。這樣 QR Code 大小不變,但清晰度為原本的兩倍。

Fixed #1562
2022-04-26 07:10:47 +08:00
pan93412
aeda63faf7
fix(deps): correct the vue-gtag version (#1556) 2022-04-23 17:19:43 +08:00
pan93412
0a1c847b4b
fix(views/lyrics): use scale to make animation smooth (#1554) 2022-04-23 17:19:31 +08:00
pan93412
ab85c51831
fix(utils/player): preload & autoUnlock (#1557)
* fix(utils/player): preload & autoUnlock

* style: clang-format & prettier
2022-04-23 17:19:16 +08:00
qier222
f88addc95d
feat: update ga 2022-04-16 03:06:16 +08:00
GalvinGao
ea4b20755d
feat: add copy track link & fixed various visual defects (#1489)
* feat: add copy track link

* fix: various visual defects

* feat: add track low res fallback

* chore: remove redundant locale strings

* chore: separate playbackState for a new PR

* build: fix netlify failing to build site
2022-04-12 16:16:46 +08:00
amphineko
16b525915e
feat(utils/request): override realIP param with env var (#1514) 2022-04-12 00:08:44 +08:00
pan93412
af0a997609
ci: configure restyled.io better (#1499) 2022-04-09 01:15:30 +08:00
memorydream
3fca7d16bb fix: APP启动时检查窗口位置,避免窗口启动在屏幕外 2022-04-05 14:41:48 +08:00
memorydream
7d64dea29b fix: 禁止在TrackListItem上导航到id为0的歌手页 2022-04-05 14:41:48 +08:00
pan93412
999bf6fdb4
chore(release): 0.4.4-1 2022-04-04 00:47:53 +08:00
pan93412
871217713a
revert: isCreateTray 在瀏覽器環境應永遠為應永遠為 false
部分環境沒有 process.env.IS_ELECTRON,以致發生錯誤。

This reverts commit 748db54f52.
2022-04-04 00:46:08 +08:00
qier222
e5d1af49bf
revert: "fix(ci/build): 暫時不要登入 (#1452)"
This reverts commit fe660cf1a9.
2022-04-03 16:10:24 +08:00
pan93412
fe660cf1a9
fix(ci/build): 暫時不要登入 (#1452)
目前 YPM 的 snapcraft_token 似乎失效了?
2022-03-28 17:57:17 +08:00
pan93412
5ff8868d3e
fix(utils/request): token 過期 (301) 時重新導向至登入頁面 (#1448) 2022-03-27 15:16:55 +08:00
HengYi Wei
1c25f11821
Update README.md: 添加了 Scoop 安装方式介绍 (#1445) 2022-03-26 21:52:35 +08:00
Jiang Menghao
626786a008
fix(components/DailyTracksCard): 修复了每日推荐在 Safari 不显示圆角的bug (#1444) 2022-03-26 18:55:42 +08:00
memorydream
b1c5873bd6
fix: firefox scrollbar (#1425) 2022-03-20 18:05:35 +08:00
memorydream
748db54f52 fix(utils/platform): isCreateTray 在瀏覽器環境應永遠為應永遠為 false 2022-03-16 15:50:20 +08:00
pan93412
fbf695eb16 feat(utils/player): 播放、暫停時淡入淡出 2022-03-16 15:50:20 +08:00
pan93412
98068f55cb
chore: upgrade dependencies (#1390)
- UnblockNeteaseMusic@0.27.0-rc.6
- dayjs@1.10.8
- electron-builder@23.0.2
- music-metadata@7.12.2
- NeteaseCloudMusicApi@4.5.8
- eslint-config-prettier@8.5.0
2022-03-10 15:37:57 +08:00
memorydream
ef7f51ecf0
feat(library): i18n & l10n (#1386)
* Update library translations

* Update zh-TW.js
2022-03-08 12:22:27 +08:00
pan93412
be35a8ded4
feat(ci): 允許手動觸發執行檔編譯流程 [skip ci] 2022-03-07 20:52:17 +08:00
memorydream
7dad7d810a
fix: 修复部分按钮失效的问题 (#1380) 2022-03-06 11:55:57 +08:00
pan93412
596204dc58
build(ci): 加上 dependencies 快取
順帶將 setup-node 升級至 v3。
2022-02-26 16:05:18 +08:00
pan93412
6ed5f274e6
build(ci): 將 Node.js 的版本切換至 v16 2022-02-26 16:04:57 +08:00
pan93412
b65a8b9da0
chore: 更新 lockfile
使用 yarn upgrade 更新部分 dependencies。
2022-02-26 16:04:00 +08:00
郭桓桓
8e68d7e796
chore(license): update copyright year (#1354) 2022-02-26 12:29:18 +08:00
pan93412
3d5d40c476
feat(components/Navbar): 自訂和微調各系統的 titlebar (#1343)
* feat: linux custom titlebar
* add settings init
* Update zh-TW.js
* fix: color

Co-authored-by: memorydream <34763046+memorydream@users.noreply.github.com>
2022-02-25 07:36:56 +08:00
pan93412
3e1dc62fa0
chore: 更新依賴、更換為 prettier 新規則 (#1340)
* chore: upgrade dependencies
* style: prettier
2022-02-25 07:36:13 +08:00
Chuwen
76b358445e
build: 优化打包体积 (#1323)
* 移除生产环境不必要的 map 文件
* webpack 添加 LimitChunkCountPlugin 用以解决 chunk 包太多的问题
2022-02-23 23:26:37 +08:00
80 changed files with 8819 additions and 5560 deletions

4
.envrc Normal file
View file

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

View file

@ -1,12 +1,15 @@
name: Release
env:
YARN_INSTALL_NOPT: yarn add --ignore-platform --ignore-optional
on:
push:
branches:
- master
- "ci/*"
tags:
- v*
workflow_dispatch:
jobs:
release:
@ -14,25 +17,26 @@ jobs:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-18.04]
os: [macos-latest, windows-latest, ubuntu-22.04]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
submodules: "recursive"
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 14.16.0
node-version: 16
cache: 'yarn'
- name: Install RPM & Pacman (on Ubuntu)
if: runner.os == 'Linux'
run: |
sudo apt-get update &&
sudo apt-get install --no-install-recommends -y rpm &&
sudo apt-get install --no-install-recommends -y bsdtar &&
sudo apt-get install --no-install-recommends -y libarchive-tools &&
sudo apt-get install --no-install-recommends -y libopenjp2-tools
- name: Install Snapcraft (on Ubuntu)
@ -41,9 +45,43 @@ jobs:
with:
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
uses: samuelmeuli/action-electron-builder@v1.6.0
env:
VUE_APP_NETEASE_API_URL: /api
VUE_APP_ELECTRON_API_URL: /api
VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754
VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c
@ -59,19 +97,19 @@ jobs:
use_vue_cli: true
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: YesPlayMusic-mac
path: dist_electron/*-universal.dmg
if-no-files-found: ignore
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: YesPlayMusic-win
path: dist_electron/*Setup*.exe
if-no-files-found: ignore
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: YesPlayMusic-linux
path: dist_electron/*.AppImage

48
.github/workflows/sync.yml vendored Normal file
View file

@ -0,0 +1,48 @@
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,3 +32,12 @@ NeteaseCloudMusicApi-master.zip
# Local Netlify folder
.netlify
vercel.json
# Devenv
.devenv*
devenv.local.nix
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
14

View file

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

View file

@ -1,43 +1,25 @@
FROM node:16.13.1-alpine as build
ENV VUE_APP_NETEASE_API_URL=/api
WORKDIR /app
RUN apk add --no-cache python3 make g++ git
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\
apk add --no-cache python3 make g++ git
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
RUN yarn config set electron_mirror https://npmmirror.com/mirrors/electron/ && \
yarn build
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
RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv \
COPY --from=build /app/package.json /usr/local/lib/
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 \
&& npm i -g NeteaseCloudMusicApi
&& npm i -g $(awk -F \" '{if($2=="NeteaseCloudMusicApi") print $2"@"$4}' /usr/local/lib/package.json) \
&& 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
CMD nginx ; exec npx NeteaseCloudMusicApi

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 qier222
Copyright (c) 2020-2023 qier222
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -8,7 +8,7 @@
<p align="center">
高颜值的第三方网易云播放器
<br />
<a href="https://music.qier222.com" target="blank"><strong>🌎 访问DEMO</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://music.ineko.cc" 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="https://t.me/yesplaymusic" target="blank"><strong>💬 加入交流群</strong></a>
<br />
@ -16,7 +16,12 @@
</p>
</p>
[![Library][library-screenshot]](https://music.qier222.com)
[![Library][library-screenshot]](https://music.ineko.cc)
## 全新版本
全新2.0 Alpha测试版已发布欢迎前往 [Releases](https://github.com/qier222/YesPlayMusic/releases) 页面下载。
当前版本将会进入维护模式除重大bug修复外不会再更新新功能。
## ✨ 特性
@ -47,12 +52,18 @@ Electron 版本由 [@hawtim](https://github.com/hawtim) 和 [@qier222](https://g
访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases)
页面下载安装包。
macOS 用户也可以通过 `brew install --cask yesplaymusic` 来安装。
- macOS 用户可以通过 Homebrew 来安装:`brew install --cask yesplaymusic`
- Windows 用户可以通过 Scoop 来安装:`scoop install extras/yesplaymusic`
## ⚙️ 部署至 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)
。你也可以将 API 部署到 Vercel。
@ -114,6 +125,16 @@ yarn run build
7. 将 `/dist` 目录下的文件上传到你的 Web 服务器
## ⚙️ 宝塔面板 docker应用商店 部署
1. 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/new/download.html) ,选择正式版的脚本下载安装。
2. 安装后登录宝塔面板,在左侧导航栏中点击 Docker首次进入会提示安装Docker服务点击立即安装按提示完成安装
3. 安装完成后在应用商店中找到YesPlayMusic点击安装配置域名、端口等基本信息即可完成安装。
4. 安装后在浏览器输入上一步骤设置的域名即可访问。
## ⚙️ Docker 部署
1. 构建 Docker Image
@ -136,6 +157,24 @@ docker-compose up -d
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 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。

132
devenv.lock Normal file
View file

@ -0,0 +1,132 @@
{
"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
}

53
devenv.nix Normal file
View file

@ -0,0 +1,53 @@
{ 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/
}

19
devenv.yaml Normal file
View file

@ -0,0 +1,19 @@
# 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,6 +4,36 @@ services:
context: .
image: 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:
- 80:80
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

28
docker/nginx.conf.example Normal file
View file

@ -0,0 +1,28 @@
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/;
}
}

28
install-replit.sh Normal file
View file

@ -0,0 +1,28 @@
#!/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,7 +7,8 @@
},
"target": "ES6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"jsx": "preserve"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]

View file

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

Binary file not shown.

After

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.

After

Width:  |  Height:  |  Size: 750 B

View file

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

7
restyled.yml Normal file
View file

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

View file

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

View file

@ -1,5 +1,7 @@
import request from '@/utils/request';
import { mapTrackPlayableStatus } from '@/utils/common';
import { isAccountLoggedIn } from '@/utils/auth';
import { getTrackDetail } from '@/api/track';
/**
* 获取歌手单曲
@ -14,7 +16,13 @@ export function getArtist(id) {
id,
timestamp: new Date().getTime(),
},
}).then(data => {
}).then(async 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);
return data;
});

View file

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

View file

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

View file

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 396 B

View file

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 396 B

View file

@ -7,6 +7,7 @@ import {
dialog,
globalShortcut,
nativeTheme,
screen,
} from 'electron';
import {
isWindows,
@ -30,7 +31,8 @@ import { EventEmitter } from 'events';
import express from 'express';
import expressProxy from 'express-http-proxy';
import Store from 'electron-store';
import { createMpris } from '@/electron/mpris';
import { createMpris, createDbus } from '@/electron/mpris';
import { spawn } from 'child_process';
const clc = require('cli-color');
const log = text => {
console.log(`${clc.blueBright('[background.js]')} ${text}`);
@ -180,7 +182,10 @@ class Background {
minWidth: 1080,
minHeight: 720,
titleBarStyle: 'hiddenInset',
frame: !isWindows,
frame: !(
isWindows ||
(isLinux && this.store.get('settings.linuxEnableCustomTitlebar'))
),
title: 'YesPlayMusic',
show: false,
webPreferences: {
@ -198,8 +203,42 @@ class Background {
};
if (this.store.get('window.x') && this.store.get('window.y')) {
options.x = this.store.get('window.x');
options.y = this.store.get('window.y');
let x = this.store.get('window.x');
let 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);
@ -258,6 +297,7 @@ class Background {
this.window.once('ready-to-show', () => {
log('window ready-to-show event');
this.window.show();
this.store.set('window', this.window.getBounds());
});
this.window.on('close', e => {
@ -293,6 +333,14 @@ class Background {
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) {
e.preventDefault();
log('open url');
@ -373,6 +421,21 @@ class Background {
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
if (isCreateMpris) {
createMpris(this.window);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,130 @@
<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,11 +39,16 @@ export default {
type: Boolean,
default: false,
},
minWidth: {
type: String,
default: 'calc(min(23rem, 100vw))',
},
},
computed: {
modalStyles() {
return {
width: this.width,
minWidth: this.minWidth,
};
},
},

View file

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

View file

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

View file

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

View file

@ -1,27 +1,8 @@
<template>
<div>
<nav>
<div class="win32-titlebar">
<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>
<nav :class="{ 'has-custom-titlebar': hasCustomTitlebar }">
<Win32Titlebar v-if="enableWin32Titlebar" />
<LinuxTitlebar v-if="enableLinuxTitlebar" />
<div class="navigation-buttons">
<button-icon @click.native="go('back')"
><svg-icon icon-class="arrow-left"
@ -62,7 +43,12 @@
</div>
</div>
</div>
<img class="avatar" :src="avatarUrl" @click="showUserProfileMenu" />
<img
class="avatar"
:src="avatarUrl"
@click="showUserProfileMenu"
loading="lazy"
/>
</div>
</nav>
@ -96,17 +82,16 @@ import { isLooseLoggedIn, doLogout } from '@/utils/auth';
// icons by https://github.com/microsoft/vscode-codicons
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 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 {
name: 'Navbar',
components: {
Win32Titlebar,
LinuxTitlebar,
ButtonIcon,
ContextMenu,
},
@ -115,7 +100,8 @@ export default {
inputFocus: false,
langs: ['zh-CN', 'zh-TW', 'en', 'tr'],
keywords: '',
isWindowMaximized: false,
enableWin32Titlebar: false,
enableLinuxTitlebar: false,
};
},
computed: {
@ -128,12 +114,18 @@ export default {
? `${this.data?.user?.avatarUrl}?param=512y512`
: 'http://s4.music.126.net/style/web2/img/default/default_avatar.jpg?param=60y60';
},
hasCustomTitlebar() {
return this.enableWin32Titlebar || this.enableLinuxTitlebar;
},
},
created() {
if (process.env.IS_ELECTRON === true) {
ipcRenderer.on('isMaximized', (event, value) => {
this.isWindowMaximized = value;
});
if (process.platform === 'win32') {
this.enableWin32Titlebar = true;
} else if (
process.platform === 'linux' &&
this.settings.linuxEnableCustomTitlebar
) {
this.enableLinuxTitlebar = true;
}
},
methods: {
@ -175,15 +167,6 @@ export default {
this.$router.push({ name: 'login' });
}
},
windowMinimize() {
ipcRenderer.send('minimize');
},
windowMaxRestore() {
ipcRenderer.send('maximizeOrUnmaximize');
},
windowClose() {
ipcRenderer.send('close');
},
},
};
</script>
@ -211,7 +194,7 @@ nav {
@media (max-width: 1336px) {
nav {
padding: 0 5vw;
padding: 0 max(5vw, 90px);
}
}
@ -221,69 +204,10 @@ nav {
}
}
.win32-titlebar {
display: none;
}
[data-electron-os='win32'] {
nav {
nav.has-custom-titlebar {
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 {
flex: 1;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import dbus from 'dbus-next';
import { ipcMain, app } from 'electron';
export function createMpris(window) {
@ -28,6 +29,8 @@ export function createMpris(window) {
});
ipcMain.on('metadata', (e, metadata) => {
// 更新 Mpris 状态前将位置设为0, 否则 OSDLyrics 获取到的进度是上首音乐切换时的进度
player.getPosition = () => 0;
player.metadata = {
'mpris:trackid': player.objectPath('track/' + metadata.trackId),
'mpris:artUrl': metadata.artwork[0].src,
@ -35,11 +38,17 @@ export function createMpris(window) {
'xesam:title': metadata.title,
'xesam:album': metadata.album,
'xesam:artist': metadata.artist.split(','),
'xesam:url': metadata.url,
};
});
ipcMain.on('playerCurrentTrackTime', (e, position) => {
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) => {
@ -60,3 +69,26 @@ export function createMpris(window) {
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,4 +1,5 @@
import clc from 'cli-color';
import checkAuthToken from '../utils/checkAuthToken';
import server from 'NeteaseCloudMusicApi/server';
export async function startNeteaseMusicApi() {

View file

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

View file

@ -28,7 +28,14 @@ export default {
albums: 'Albums',
artists: 'Artists',
mvs: 'MVs',
cloudDisk: 'Cloud Disk',
newPlayList: 'New Playlist',
uploadSongs: 'Upload Songs',
playHistory: {
title: 'Play History',
week: 'Latest Week',
all: 'All Time',
},
userProfileMenu: {
settings: 'Settings',
logout: 'Logout',
@ -95,6 +102,7 @@ export default {
},
player: {
like: 'Like',
unlike: 'Unlike',
previous: 'Previous Song',
next: 'Next Song',
repeat: 'Repeat',
@ -105,6 +113,8 @@ export default {
pause: 'Pause',
mute: 'Mute',
nextUp: 'Next Up',
translationLyric: 'lyric (trans)',
PronunciationLyric: 'lyric (pronounce)',
},
modal: {
close: 'Close',
@ -122,6 +132,17 @@ export default {
settings: 'Settings',
logout: 'LOGOUT',
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: {
text: 'Music Quality',
low: 'Low',
@ -158,6 +179,8 @@ export default {
showLibraryDefault: 'Show Library after App Launched',
subTitleDefault: 'Show Alias for Subtitle by default',
enableReversedMode: 'Enable Reversed Mode (Experimental)',
enableCustomTitlebar: 'Enable custom title bar (Need restart)',
showLyricsTime: 'Display current time',
lyricsBackground: {
text: 'Show Lyrics Background',
off: 'Off',
@ -170,6 +193,41 @@ export default {
exit: 'Exit',
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: {
play: 'Play',
@ -185,6 +243,9 @@ export default {
allPlaylists: 'All Playlists',
minePlaylists: 'My Playlists',
likedPlaylists: 'Liked Playlists',
cardiacMode: 'Cardiac Mode',
copyLyric: 'Copy Lyric',
copyLyricWithTranslation: 'Copy Lyric With Translation',
},
toast: {
savedToPlaylist: 'Saved to playlist',

View file

@ -28,7 +28,14 @@ export default {
albums: 'Albümler',
artists: 'Sanatçılar',
mvs: 'MVs',
cloudDisk: 'Cloud Disk',
newPlayList: 'Yeni Çalma Listesi',
uploadSongs: 'Upload Songs',
playHistory: {
title: 'Play History',
week: 'Latest Week',
all: 'All Time',
},
userProfileMenu: {
settings: 'Ayarlar',
logout: ıkış Yap',
@ -91,6 +98,7 @@ export default {
},
player: {
like: 'Beğen',
unlike: 'Aksine',
previous: 'Önceki Müzik',
next: 'Sonraki Müzik',
repeat: 'Tekrarla',
@ -100,6 +108,8 @@ export default {
pause: 'Durdur',
mute: 'Sesi kapat',
nextUp: 'Sıradaki',
translationLyric: 'şarkı sözleri (çeviri)',
PronunciationLyric: 'şarkı sözleri (çeviri)',
},
modal: {
close: 'Kapat',
@ -117,6 +127,17 @@ export default {
settings: 'Ayarlar',
logout: 'ÇIKIŞ YAP',
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: {
text: 'Müzik Kalitesi',
low: 'Düşük',
@ -150,7 +171,9 @@ export default {
showPlaylistsByAppleMusic: "Apple Music'in Çalma Listelerini Göster",
enableDiscordRichPresence: 'Discord gösterimini aktifleştir',
showLibraryDefault: 'Kitaplık Varsayılanını göster',
subTitleDefault: 'Sub title alia default',
subTitleDefault: 'Show Alias for Subtitle by default',
enableReversedMode: 'Enable Reversed Mode (Experimental)',
enableCustomTitlebar: 'Enable custom title bar (Need restart)',
lyricsBackground: {
text: 'Şarkı Sözleri Arka Planını Göster',
off: 'kapalı',
@ -163,12 +186,52 @@ export default {
exit: 'Exit',
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: {
play: 'Oynat',
addToQueue: 'Sonrakini Oynat',
saveToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',
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: {
savedToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',

View file

@ -25,7 +25,14 @@ export default {
albums: '专辑',
artists: '艺人',
mvs: 'MV',
cloudDisk: '云盘',
newPlayList: '新建歌单',
uploadSongs: '上传歌曲',
playHistory: {
title: '听歌排行',
week: '最近一周',
all: '所有时间',
},
userProfileMenu: {
settings: '设置',
logout: '登出',
@ -96,6 +103,7 @@ export default {
},
player: {
like: '喜欢',
unlike: '取消喜欢',
previous: '上一首',
next: '下一首',
repeat: '循环播放',
@ -106,6 +114,8 @@ export default {
pause: '暂停',
mute: '静音',
nextUp: '播放列表',
translationLyric: '歌词(译)',
PronunciationLyric: '歌词(音)',
},
modal: {
close: '关闭',
@ -123,6 +133,17 @@ export default {
settings: '设置',
logout: '登出',
language: '语言',
lyric: '歌词',
others: '其他',
customization: '自定义',
MusicGenrePreference: {
text: '音乐语种偏好',
none: '无偏好',
mandarin: '华语',
western: '欧美',
korean: '韩语',
japanese: '日语',
},
musicQuality: {
text: '音质选择',
low: '普通',
@ -159,18 +180,53 @@ export default {
showLibraryDefault: '启动后显示音乐库',
subTitleDefault: '副标题使用别名',
enableReversedMode: '启用倒序播放功能 (实验性功能)',
enableCustomTitlebar: '启用自定义标题栏 (重启后生效)',
lyricsBackground: {
text: '显示歌词背景',
off: '关闭',
on: '打开',
dynamic: '动态GPU 占用较高)',
},
showLyricsTime: '显示当前时间',
closeAppOption: {
text: '关闭主面板时...',
ask: '询问',
exit: '退出',
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: {
play: '播放',
@ -186,6 +242,9 @@ export default {
allPlaylists: '全部歌单',
minePlaylists: '创建的歌单',
likedPlaylists: '收藏的歌单',
cardiacMode: '心动模式',
copyLyric: '复制歌词',
copyLyricWithTranslation: '复制歌词(含翻译)',
},
toast: {
savedToPlaylist: '已添加到歌单',

View file

@ -25,7 +25,14 @@ export default {
albums: '專輯',
artists: '藝人',
mvs: 'MV',
cloudDisk: '雲端硬碟',
newPlayList: '新增歌單',
uploadSongs: '上傳音樂',
playHistory: {
title: '聽歌排行',
week: '最近一周',
all: '所有時間',
},
userProfileMenu: {
settings: '設定',
logout: '登出',
@ -92,6 +99,7 @@ export default {
},
player: {
like: '喜歡',
unlike: '取消喜歡',
previous: '上一首',
next: '下一首',
repeat: '循環播放',
@ -102,6 +110,8 @@ export default {
pause: '暫停',
mute: '靜音',
nextUp: '播放清單',
translationLyric: '歌詞(譯)',
PronunciationLyric: '歌詞(音)',
},
modal: {
close: '關閉',
@ -119,6 +129,17 @@ export default {
settings: '設定',
logout: '登出',
language: '語言',
lyric: '歌詞',
others: '其他',
customization: '自訂',
MusicGenrePreference: {
text: '音樂語種偏好',
none: '無偏好',
mandarin: '華語',
western: '歐美',
korean: '韓語',
japanese: '日語',
},
musicQuality: {
text: '音質選擇',
low: '普通',
@ -156,6 +177,8 @@ export default {
showLibraryDefault: '啟動後顯示音樂庫',
subTitleDefault: '副標題使用別名',
enableReversedMode: '啟用倒序播放功能 (實驗性功能)',
enableCustomTitlebar: '啟用自訂標題列(重新啟動後生效)',
showLyricsTime: '顯示目前時間',
lyricsBackground: {
text: '顯示歌詞背景',
off: '關閉',
@ -168,6 +191,39 @@ export default {
exit: '退出',
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: {
play: '播放',
@ -183,6 +239,9 @@ export default {
allPlaylists: '全部歌單',
minePlaylists: '我建立的歌單',
likedPlaylists: '收藏的歌單',
cardiacMode: '心動模式',
copyLyric: '複製歌詞',
copyLyricWithTranslation: '複製歌詞(含翻譯)',
},
toast: {
savedToPlaylist: '已新增至歌單',

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,29 @@
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 { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
import { getArtist } from '@/api/artist';
import { personalFM, fmTrash } from '@/api/others';
import { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm';
import { fmTrash, personalFM } from '@/api/others';
import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
import { getLyric, getMP3, getTrackDetail } from '@/api/track';
import store from '@/store';
import { isAccountLoggedIn } from '@/utils/auth';
import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm';
import { cacheTrackSource, getTrackSource } from '@/utils/db';
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 =
process.env.IS_ELECTRON === true ? window.require('electron') : null;
@ -32,13 +46,14 @@ function setTitle(track) {
? `${track.name} · ${track.ar[0].name} - YesPlayMusic`
: 'YesPlayMusic';
if (isCreateTray) {
ipcRenderer.send('updateTrayTooltip', document.title);
ipcRenderer?.send('updateTrayTooltip', document.title);
}
store.commit('updateTitle', document.title);
}
function setTrayLikeState(isLiked) {
if (isCreateTray) {
ipcRenderer.send('updateTrayLikeState', isLiked);
ipcRenderer?.send('updateTrayLikeState', isLiked);
}
}
@ -66,7 +81,9 @@ export default class {
this._playNextList = []; // 当这个list不为空时会优先播放这个list的歌
this._isPersonalFM = false; // 是否是私人FM模式
this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲
this._personalFMNextTrack = { id: 0 }; // 私人FM下一首歌曲信息为了快速加载下一首
this._personalFMNextTrack = {
id: 0,
}; // 私人FM下一首歌曲信息为了快速加载下一首
/**
* The blob records for cleanup.
@ -113,6 +130,8 @@ export default class {
if (shuffle) {
this._shuffleTheList();
}
// 同步当前歌曲在列表中的下标
this.current = this.list.indexOf(this.currentTrackID);
}
get reversed() {
return this._reversed;
@ -131,7 +150,7 @@ export default class {
}
set volume(volume) {
this._volume = volume;
Howler.volume(volume);
this._howler?.volume(volume);
}
get list() {
return this.shuffle ? this._shuffledList : this._list;
@ -158,6 +177,9 @@ export default class {
get currentTrack() {
return this._currentTrack;
}
get currentTrackID() {
return this._currentTrack?.id ?? 0;
}
get playlistSource() {
return this._playlistSource;
}
@ -181,6 +203,9 @@ export default class {
set progress(value) {
if (this._howler) {
this._howler.seek(value);
if (isCreateMpris) {
ipcRenderer?.send('seeked', this._howler.seek());
}
}
}
get isCurrentTrackLiked() {
@ -189,13 +214,11 @@ export default class {
_init() {
this._loadSelfFromLocalStorage();
Howler.autoUnlock = false;
Howler.usingWebAudio = true;
Howler.volume(this.volume);
this._howler?.volume(this.volume);
if (this._enabled) {
// 恢复当前播放歌曲
this._replaceCurrentTrack(this._currentTrack.id, false).then(() => {
this._replaceCurrentTrack(this.currentTrackID, false).then(() => {
this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0);
}); // update audio source and init howler
this._initMediaSession();
@ -219,18 +242,19 @@ export default class {
_setPlaying(isPlaying) {
this._playing = isPlaying;
if (isCreateTray) {
ipcRenderer.send('updateTrayPlayState', this._playing);
ipcRenderer?.send('updateTrayPlayState', this._playing);
}
}
_setIntervals() {
// 同步播放进度
// TODO: 如果 _progress 在别的地方被改变了这个定时器会覆盖之前改变的值是bug
// TODO: 如果 _progress 在别的地方被改变了,
// 这个定时器会覆盖之前改变的值是bug
setInterval(() => {
if (this._howler === null) return;
this._progress = this._howler.seek();
localStorage.setItem('playerCurrentTrackTime', this._progress);
if (isCreateMpris) {
ipcRenderer.send('playerCurrentTrackTime', this._progress);
ipcRenderer?.send('playerCurrentTrackTime', this._progress);
}
}, 1000);
}
@ -238,8 +262,8 @@ export default class {
const next = this._reversed ? this.current - 1 : this.current + 1;
if (this._playNextList.length > 0) {
let trackID = this._playNextList.shift();
return [trackID, this.current];
let trackID = this._playNextList[0];
return [trackID, INDEX_IN_PLAY_NEXT];
}
// 循环模式开启,则重新播放当前模式下的相对的下一首
@ -273,7 +297,7 @@ export default class {
// 返回 [trackID, index]
return [this.list[next], next];
}
async _shuffleTheList(firstTrackID = this._currentTrack.id) {
async _shuffleTheList(firstTrackID = this.currentTrackID) {
let list = this._list.filter(tid => tid !== firstTrackID);
if (firstTrackID === 'first') list = this._list;
this._shuffledList = shuffle(list);
@ -285,11 +309,6 @@ export default class {
);
const trackDuration = ~~(track.dt / 1000);
time = completed ? trackDuration : ~~time;
scrobble({
id: track.id,
sourceid: this.playlistSource.id,
time,
});
if (
store.state.lastfm.key !== undefined &&
(time >= trackDuration / 2 || time >= 240)
@ -310,11 +329,35 @@ export default class {
this._howler = new Howl({
src: [source],
html5: true,
preload: true,
format: ['mp3', 'flac'],
onend: () => {
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) {
this.play();
if (this._currentTrack.name) {
@ -324,12 +367,9 @@ export default class {
}
this.setOutputDevice();
}
_getAudioSourceFromCache(id) {
return getTrackSource(id).then(t => {
if (!t) return null;
_getAudioSourceBlobURL(data) {
// Create a new object URL.
const source = URL.createObjectURL(new Blob([t.source]));
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,
@ -343,6 +383,11 @@ export default class {
this.createdBlobRecords = [source];
return source;
}
_getAudioSourceFromCache(id) {
return getTrackSource(id).then(t => {
if (!t) return null;
return this._getAudioSourceBlobURL(t.source);
});
}
_getAudioSourceFromNetease(track) {
@ -365,22 +410,71 @@ export default class {
}
async _getAudioSourceFromUnblockMusic(track) {
console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`);
if (
process.env.IS_ELECTRON !== true ||
store.state.settings.enableUnblockNeteaseMusic === false
) {
return null;
}
const source = await ipcRenderer.invoke(
'unblock-music',
track,
store.state.settings.unmSource
);
if (store.state.settings.automaticallyCacheSongs && source?.url) {
// TODO: 将unblockMusic字样换成真正的来源比如酷我咪咕等
cacheTrackSource(track, source.url, 128000, 'unblockMusic');
/**
*
* @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;
}
return source?.url;
};
const retrieveSongInfo = await ipcRenderer.invoke(
'unblock-music',
store.state.settings.unmSource,
track,
{
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 && retrieveSongInfo?.url) {
// 对于来自 bilibili 的音源
// 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}`);
}
if (!retrieveSongInfo) {
return null;
}
if (retrieveSongInfo.source !== 'bilibili') {
return retrieveSongInfo.url;
}
const buffer = base642Buffer(retrieveSongInfo.url);
return this._getAudioSourceBlobURL(buffer);
}
_getAudioSource(track) {
return this._getAudioSourceFromCache(String(track.id))
@ -394,34 +488,62 @@ export default class {
_replaceCurrentTrack(
id,
autoplay = true,
ifUnplayableThen = 'playNextTrack'
ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK
) {
if (autoplay && this._currentTrack.name) {
this._scrobble(this.currentTrack, this._howler?.seek());
}
return getTrackDetail(id).then(data => {
let track = data.songs[0];
const track = data.songs[0];
this._currentTrack = track;
this._updateMediaSessionMetaData(track);
return this._replaceCurrentTrackAudio(
track,
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);
replaced = true;
}
if (isCacheNextTrack) {
this._cacheNextTrack();
return source;
}
return replaced;
} else {
store.dispatch('showToast', `无法播放 ${track.name}`);
if (ifUnplayableThen === 'playNextTrack') {
if (this.isPersonalFM) {
this.playNextFMTrack();
} else {
this.playNextTrack();
}
} else {
switch (ifUnplayableThen) {
case UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK:
this._playNextTrack(this.isPersonalFM);
break;
case UNPLAYABLE_CONDITION.PLAY_PREV_TRACK:
this.playPrevTrack();
break;
default:
store.dispatch(
'showToast',
`undefined Unplayable condition: ${ifUnplayableThen}`
);
break;
}
return false;
}
});
});
}
_cacheNextTrack() {
let nextTrackID = this._isPersonalFM
@ -453,11 +575,7 @@ export default class {
this.playPrevTrack();
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
if (this.isPersonalFM) {
this.playNextFMTrack();
} else {
this.playNextTrack();
}
this._playNextTrack(this.isPersonalFM);
});
navigator.mediaSession.setActionHandler('stop', () => {
this.pause();
@ -486,6 +604,11 @@ export default class {
artist: artists.join(','),
album: track.al.name,
artwork: [
{
src: track.al.picUrl + '?param=224y224',
type: 'image/jpg',
sizes: '224x224',
},
{
src: track.al.picUrl + '?param=512y512',
type: 'image/jpg',
@ -494,13 +617,35 @@ export default class {
],
length: this.currentTrackDuration,
trackId: this.current,
url: '/trackid/' + track.id,
};
navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
if (isCreateMpris) {
ipcRenderer.send('metadata', metadata);
this._updateMprisState(track, 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() {
if ('mediaSession' in navigator === false) {
return;
@ -516,11 +661,9 @@ export default class {
_nextTrackCallback() {
this._scrobble(this._currentTrack, 0, true);
if (!this.isPersonalFM && this.repeatMode === 'one') {
this._replaceCurrentTrack(this._currentTrack.id);
} else if (this.isPersonalFM) {
this.playNextFMTrack();
this._replaceCurrentTrack(this.currentTrackID);
} else {
this.playNextTrack();
this._playNextTrack(this.isPersonalFM);
}
}
_loadPersonalFMNextTrack() {
@ -554,7 +697,7 @@ export default class {
}
let copyTrack = { ...track };
copyTrack.dt -= seekTime * 1000;
ipcRenderer.send('playDiscordPresence', copyTrack);
ipcRenderer?.send('playDiscordPresence', copyTrack);
}
_pauseDiscordPresence(track) {
if (
@ -563,13 +706,16 @@ export default class {
) {
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) {
this.list.append(trackID);
}
@ -581,7 +727,12 @@ export default class {
this._setPlaying(false);
return false;
}
this.current = index;
let next = index;
if (index === INDEX_IN_PLAY_NEXT) {
this._playNextList.shift();
next = this.current;
}
this.current = next;
this._replaceCurrentTrack(trackID);
return true;
}
@ -634,7 +785,11 @@ export default class {
const [trackID, index] = this._getPrevTrack();
if (trackID === undefined) return false;
this.current = index;
this._replaceCurrentTrack(trackID, true, 'playPrevTrack');
this._replaceCurrentTrack(
trackID,
true,
UNPLAYABLE_CONDITION.PLAY_PREV_TRACK
);
return true;
}
saveSelfToLocalStorage() {
@ -648,14 +803,26 @@ export default class {
}
pause() {
this._howler?.fade(this.volume, 0, PLAY_PAUSE_FADE_DURATION);
this._howler?.once('fade', () => {
this._howler?.pause();
this._setPlaying(false);
setTitle(null);
this._pauseDiscordPresence(this._currentTrack);
});
}
play() {
if (this._howler?.playing()) return;
this._howler?.play();
this._howler?.once('play', () => {
this._howler?.fade(0, this.volume, PLAY_PAUSE_FADE_DURATION);
// 播放时确保开启player.
// 避免因"忘记设置"导致在播放时播放器不显示的Bug
this._enabled = true;
this._setPlaying(true);
if (this._currentTrack.name) {
setTitle(this._currentTrack);
@ -670,6 +837,7 @@ export default class {
duration: ~~(this.currentTrack.dt / 1000),
});
}
});
}
playOrPause() {
if (this._howler?.playing()) {
@ -678,11 +846,14 @@ export default class {
this.play();
}
}
seek(time = null) {
seek(time = null, sendMpris = true) {
if (isCreateMpris && sendMpris && time) {
ipcRenderer?.send('seeked', time);
}
if (time !== null) {
this._howler?.seek(time);
if (this._playing)
this._playDiscordPresence(this._currentTrack, this.seek());
this._playDiscordPresence(this._currentTrack, this.seek(null, false));
}
return this._howler === null ? 0 : this._howler.seek();
}
@ -708,7 +879,6 @@ export default class {
autoPlayTrackID = 'first'
) {
this._isPersonalFM = false;
if (!this._enabled) this._enabled = true;
this.list = trackIDs;
this.current = 0;
this._playlistSource = {
@ -719,7 +889,7 @@ export default class {
if (autoPlayTrackID === 'first') {
this._replaceCurrentTrack(this.list[0]);
} else {
this.current = trackIDs.indexOf(autoPlayTrackID);
this.current = this.list.indexOf(autoPlayTrackID);
this._replaceCurrentTrack(autoPlayTrackID);
}
}
@ -765,20 +935,13 @@ export default class {
addTrackToPlayNext(trackID, playNow = false) {
this._playNextList.push(trackID);
if (playNow) {
if (this.isPersonalFM) {
this.playNextFMTrack();
} else {
this.playNextTrack();
}
}
}
playPersonalFM() {
this._isPersonalFM = true;
if (!this._enabled) this._enabled = true;
if (this._currentTrack.id !== this._personalFMTrack.id) {
this._replaceCurrentTrack(this._personalFMTrack.id).then(() =>
this.playOrPause()
);
if (this.currentTrackID !== this._personalFMTrack.id) {
this._replaceCurrentTrack(this._personalFMTrack.id, true);
} else {
this.playOrPause();
}
@ -794,7 +957,7 @@ export default class {
sendSelfToIpcMain() {
if (process.env.IS_ELECTRON !== true) return false;
let liked = store.state.liked.songs.includes(this.currentTrack.id);
ipcRenderer.send('player', {
ipcRenderer?.send('player', {
playing: this.playing,
likedCurrentTrack: liked,
});
@ -810,13 +973,13 @@ export default class {
this.repeatMode = 'on';
}
if (isCreateMpris) {
ipcRenderer.send('switchRepeatMode', this.repeatMode);
ipcRenderer?.send('switchRepeatMode', this.repeatMode);
}
}
switchShuffle() {
this.shuffle = !this.shuffle;
if (isCreateMpris) {
ipcRenderer.send('switchShuffle', this.shuffle);
ipcRenderer?.send('switchShuffle', this.shuffle);
}
}
switchReversed() {

67
src/utils/base64.js Normal file
View file

@ -0,0 +1,67 @@
// 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

@ -0,0 +1,8 @@
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) {
if (!value) return '';
let min = ~~((value / 60) % 60);
let min = ~~(value / 60);
let sec = (~~(value % 60)).toString().padStart(2, '0');
return `${min}:${sec}`;
}

View file

@ -2,14 +2,17 @@ export function lyricParser(lrc) {
return {
lyric: parseLyric(lrc?.lrc?.lyric || ''),
tlyric: parseLyric(lrc?.tlyric?.lyric || ''),
romalyric: parseLyric(lrc?.romalrc?.lyric || ''),
lyricuser: lrc.lyricUser,
transuser: lrc.transUser,
};
}
// regexr.com/6e52n
const extractLrcRegex = /^(?<lyricTimestamps>(?:\[.+?\])+)(?!\[)(?<content>.+)$/gm;
const extractTimestampRegex = /\[(?<min>\d+):(?<sec>\d+)(?:\.|:)*(?<ms>\d+)*\]/g;
const extractLrcRegex =
/^(?<lyricTimestamps>(?:\[.+?\])+)(?!\[)(?<content>.+)$/gm;
const extractTimestampRegex =
/\[(?<min>\d+):(?<sec>\d+)(?:\.|:)*(?<ms>\d+)*\]/g;
/**
* @typedef {{time: number, rawTime: string, content: string}} ParsedLyric
@ -81,3 +84,30 @@ function trimContent(content) {
let t = content.trim();
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,5 +1,11 @@
import router from '../router';
import state from '../store/state';
import {
recommendPlaylist,
dailyRecommendPlaylist,
getPlaylistDetail,
} from '@/api/playlist';
import { isAccountLoggedIn } from '@/utils/auth';
export function hasListSource() {
return !state.player.isPersonalFM && state.player.playlistSource.id !== 0;
@ -20,3 +26,36 @@ export function getListSourcePath() {
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,5 +1,6 @@
import router from '@/router';
import { doLogout, getCookie } from '@/utils/auth';
import axios from 'axios';
import { getCookie } from '@/utils/auth';
let baseURL = '';
// Web 和 Electron 跑在不同端口避免同时启动时冲突
@ -21,14 +22,33 @@ const service = axios.create({
service.interceptors.request.use(function (config) {
if (!config.params) config.params = {};
if (baseURL[0] !== '/' && !process.env.IS_ELECTRON) {
if (baseURL.length) {
if (
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')) {
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;
if (['HTTP', 'HTTPS'].includes(proxy.protocol)) {
config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`;
@ -42,8 +62,37 @@ service.interceptors.response.use(
const res = response.data;
return res;
},
error => {
return Promise.reject(error);
async error => {
/** @type {import('axios').AxiosResponse | null} */
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,7 +28,9 @@
<span v-else>Compilation by Various Artists</span>
</div>
<div class="date-and-count">
<span v-if="album.mark === 1056768" class="explicit-symbol"
<span
v-if="(album.mark & 1048576) === 1048576"
class="explicit-symbol"
><ExplicitSymbol
/></span>
<span :title="album.publishTime | formatDate">{{
@ -71,12 +73,12 @@
</div>
</div>
</div>
<div v-if="Object.keys(tracksByDisc).length !== 1">
<div v-for="(disc, cd) in tracksByDisc" :key="cd">
<h2 class="disc">Disc {{ cd }}</h2>
<div v-if="tracksByDisc.length > 1">
<div v-for="item in tracksByDisc" :key="item.disc">
<h2 class="disc">Disc {{ item.disc }}</h2>
<TrackList
:id="album.id"
:tracks="disc"
:tracks="item.tracks"
:type="'album'"
:album-object="album"
/>
@ -96,9 +98,7 @@
{{ $t('album.released') }}
{{ album.publishTime | formatDate('MMMM D, YYYY') }}
</div>
<div v-if="album.company !== null" class="copyright">
© {{ album.company }}
</div>
<div v-if="album.company" class="copyright"> © {{ album.company }} </div>
</div>
<div v-if="filteredMoreAlbums.length !== 0" class="more-by">
<div class="section-title">
@ -153,7 +153,7 @@ import locale from '@/locale';
import { splitSoundtrackAlbumTitle, splitAlbumTitle } from '@/utils/common';
import NProgress from 'nprogress';
import { isAccountLoggedIn } from '@/utils/auth';
import { groupBy } from 'lodash';
import { groupBy, toPairs, sortBy } from 'lodash';
import ExplicitSymbol from '@/components/ExplicitSymbol.vue';
import ButtonTwoTone from '@/components/ButtonTwoTone.vue';
@ -222,7 +222,12 @@ export default {
}
},
tracksByDisc() {
return groupBy(this.tracks, 'cd');
if (this.tracks.length <= 1) return [];
const pairs = toPairs(groupBy(this.tracks, 'cd'));
return sortBy(pairs, p => p[0]).map(items => ({
disc: items[0],
tracks: items[1],
}));
},
},
created() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,9 +32,12 @@
<div class="left-side">
<div>
<div v-if="settings.showLyricsTime" class="date">
{{ date }}
</div>
<div class="cover">
<div class="cover-container">
<img :src="imageUrl" />
<img :src="imageUrl" loading="lazy" />
<div
class="shadow"
:style="{ backgroundImage: `url(${imageUrl})` }"
@ -76,6 +79,29 @@
</span>
</div>
</div>
<div class="top-right">
<div class="volume-control">
<button-icon :title="$t('player.mute')" @click.native="mute">
<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.5 && volume !== 0"
icon-class="volume-half"
/>
</button-icon>
<div class="volume-bar">
<vue-slider
v-model="volume"
:min="0"
: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')"
@ -87,11 +113,18 @@
"
/>
</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 class="progress-bar">
<span>{{ formatTrackTime(player.progress) || '0:00' }}</span>
<div class="slider">
@ -120,7 +153,7 @@
: $t('player.repeat')
"
:class="{ active: player.repeatMode !== 'off' }"
@click.native="player.switchRepeatMode"
@click.native="switchRepeatMode"
>
<svg-icon
v-show="player.repeatMode !== 'one'"
@ -135,21 +168,21 @@
<button-icon
v-show="!player.isPersonalFM"
:title="$t('player.previous')"
@click.native="player.playPrevTrack"
@click.native="playPrevTrack"
>
<svg-icon icon-class="previous" />
</button-icon>
<button-icon
v-show="player.isPersonalFM"
title="不喜欢"
@click.native="player.moveToFMTrash"
@click.native="moveToFMTrash"
>
<svg-icon icon-class="thumbs-down" />
</button-icon>
<button-icon
id="play"
:title="$t(player.playing ? 'player.pause' : 'player.play')"
@click.native="player.playOrPause"
@click.native="playOrPause"
>
<svg-icon :icon-class="player.playing ? 'pause' : 'play'" />
</button-icon>
@ -164,10 +197,32 @@
v-show="!player.isPersonalFM"
:title="$t('player.shuffle')"
:class="{ active: player.shuffle }"
@click.native="player.switchShuffle"
@click.native="switchShuffle"
>
<svg-icon icon-class="shuffle" />
</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>
@ -182,7 +237,7 @@
>
<div id="line-1" class="line"></div>
<div
v-for="(line, index) in lyricWithTranslation"
v-for="(line, index) in lyricToShow"
:id="`line${index}`"
:key="index"
class="line"
@ -192,7 +247,12 @@
@click="clickLyricLine(line.time)"
@dblclick="clickLyricLine(line.time, true)"
>
<span v-if="line.contents[0]">{{ line.contents[0] }}</span>
<div class="content">
<span
v-if="line.contents[0]"
@click.right="openLyricMenu($event, line, 0)"
>{{ line.contents[0] }}</span
>
<br />
<span
v-if="
@ -200,10 +260,27 @@
$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="
rightClickLyric &&
rightClickLyric.contents[1] &&
$store.state.settings.showLyricsTranslation
"
class="item"
@click="copyLyric(true)"
>{{ $t('contextMenu.copyLyricWithTranslation') }}</div
>
</ContextMenu>
</div>
</transition>
</div>
<div class="close-button" @click="toggleLyrics">
@ -211,6 +288,12 @@
<svg-icon icon-class="arrow-down" />
</button>
</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>
</transition>
</template>
@ -221,28 +304,37 @@
import { mapState, mapMutations, mapActions } from 'vuex';
import VueSlider from 'vue-slider-component';
import ContextMenu from '@/components/ContextMenu.vue';
import { formatTrackTime } from '@/utils/common';
import { getLyric } from '@/api/track';
import { lyricParser } from '@/utils/lyrics';
import { lyricParser, copyLyric } from '@/utils/lyrics';
import ButtonIcon from '@/components/ButtonIcon.vue';
import * as Vibrant from 'node-vibrant';
import * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js';
import Color from 'color';
import { isAccountLoggedIn } from '@/utils/auth';
import { hasListSource, getListSourcePath } from '@/utils/playList';
import locale from '@/locale';
export default {
name: 'Lyrics',
components: {
VueSlider,
ButtonIcon,
ContextMenu,
},
data() {
return {
lyricsInterval: null,
lyric: [],
tlyric: [],
romalyric: [],
lyricType: 'translation', // or 'romaPronunciation'
highlightLyricIndex: -1,
minimize: true,
background: '',
date: this.formatTime(new Date()),
isFullscreen: !!document.fullscreenElement,
rightClickLyric: null,
};
},
computed: {
@ -250,12 +342,28 @@ export default {
currentTrack() {
return this.player.currentTrack;
},
volume: {
get() {
return this.player.volume;
},
set(value) {
this.player.volume = value;
},
},
imageUrl() {
return this.player.currentTrack?.al?.picUrl + '?param=1024y1024';
},
bgImageUrl() {
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() {
let ret = [];
//
@ -287,6 +395,37 @@ export default {
}
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() {
return {
fontSize: `${this.$store.state.settings.lyricFontSize || 28}px`,
@ -325,13 +464,77 @@ export default {
created() {
this.getLyric();
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() {
clearInterval(this.lyricsInterval);
},
methods: {
...mapMutations(['toggleLyrics']),
...mapMutations(['toggleLyrics', 'updateModal']),
...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() {
if (this.player.isPersonalFM) {
this.player.playNextFMTrack();
@ -345,9 +548,10 @@ export default {
if (!data?.lrc?.lyric) {
this.lyric = [];
this.tlyric = [];
this.romalyric = [];
return false;
} else {
let { lyric, tlyric } = lyricParser(data);
let { lyric, tlyric, romalyric } = lyricParser(data);
lyric = lyric.filter(
l => !/^作(词|曲)\s*(:|)\s*无$/.exec(l.content)
);
@ -367,15 +571,27 @@ export default {
if (lyric.length === 1 && includeAM) {
this.lyric = [];
this.tlyric = [];
this.romalyric = [];
return false;
} else {
this.lyric = lyric;
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;
}
}
});
},
switchLyricType() {
this.lyricType =
this.lyricType === 'translation' ? 'romaPronunciation' : 'translation';
},
formatTrackTime(value) {
return formatTrackTime(value);
},
@ -394,9 +610,24 @@ export default {
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() {
this.lyricsInterval = setInterval(() => {
const progress = this.player.seek() ?? 0;
const progress = this.player.seek(null, false) ?? 0;
let oldHighlightLyricIndex = this.highlightLyricIndex;
this.highlightLyricIndex = this.lyric.findIndex((l, index) => {
const nextLyric = this.lyric[index + 1];
@ -417,15 +648,21 @@ export default {
moveToFMTrash() {
this.player.moveToFMTrash();
},
switchRepeatMode() {
this.player.switchRepeatMode();
},
switchShuffle() {
this.player.switchShuffle();
},
getCoverColor() {
if (this.settings.lyricsBackground !== true) return;
const cover = this.currentTrack.al?.picUrl + '?param=1024y1024';
const cover = this.currentTrack.al?.picUrl + '?param=256y256';
Vibrant.from(cover, { colorCount: 1 })
.getPalette()
.then(palette => {
const orignColor = Color.rgb(palette.DarkMuted._rgb);
const color = orignColor.darken(0.1).rgb().string();
const color2 = orignColor.lighten(0.28).rotate(-30).rgb().string();
const originColor = Color.rgb(palette.DarkMuted._rgb);
const color = originColor.darken(0.1).rgb().string();
const color2 = originColor.lighten(0.28).rotate(-30).rgb().string();
this.background = `linear-gradient(to top left, ${color}, ${color2})`;
});
},
@ -435,6 +672,9 @@ export default {
getListPath() {
return getListSourcePath();
},
mute() {
this.player.mute();
},
},
};
</script>
@ -522,6 +762,20 @@ export default {
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 {
max-width: 54vh;
margin-top: 24px;
@ -552,6 +806,19 @@ export default {
display: flex;
justify-content: space-between;
.top-right {
display: flex;
justify-content: space-between;
.volume-control {
margin: 0 10px;
display: flex;
align-items: center;
.volume-bar {
width: 84px;
}
}
.buttons {
display: flex;
align-items: center;
@ -566,6 +833,7 @@ export default {
}
}
}
}
.progress-bar {
margin-top: 22px;
@ -627,6 +895,12 @@ export default {
width: 22px;
}
}
.lyric-switch-icon {
color: var(--color-text);
font-size: 14px;
line-height: 14px;
opacity: 0.88;
}
}
}
}
@ -685,9 +959,12 @@ export default {
&:hover {
background: var(--color-secondary-bg-for-transparent);
}
&:active {
.content {
transform-origin: center left;
transform: scale(0.95);
}
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
user-select: none;
span {
opacity: 0.28;
@ -698,7 +975,8 @@ export default {
span.translation {
opacity: 0.2;
font-size: 0.95em;
font-size: 0.925em;
}
}
}
@ -710,15 +988,16 @@ export default {
margin-top: 0.1em;
}
.highlight span {
.highlight div.content {
transform: scale(1);
span {
opacity: 0.98;
display: inline-block;
font-size: 1.25em;
}
.highlight span.translation {
span.translation {
opacity: 0.65;
font-size: 1.1em;
}
}
}

View file

@ -11,11 +11,14 @@
}}</router-link>
-
{{ mv.data.name }}
<div class="like-button">
<button-icon @click.native="likeMV">
<div class="buttons">
<button-icon class="button" @click.native="likeMV">
<svg-icon v-if="mv.subed" icon-class="heart-solid"></svg-icon>
<svg-icon v-else icon-class="heart"></svg-icon>
</button-icon>
<button-icon class="button" @click.native="openMenu">
<svg-icon icon-class="more"></svg-icon>
</button-icon>
</div>
</div>
<div class="info">
@ -28,6 +31,14 @@
<div class="section-title">{{ $t('mv.moreVideo') }}</div>
<MvRow :mvs="simiMvs" />
</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>
</template>
@ -40,6 +51,7 @@ import '@/assets/css/plyr.css';
import Plyr from 'plyr';
import ButtonIcon from '@/components/ButtonIcon.vue';
import ContextMenu from '@/components/ContextMenu.vue';
import MvRow from '@/components/MvRow.vue';
import { mapActions } from 'vuex';
@ -48,6 +60,7 @@ export default {
components: {
MvRow,
ButtonIcon,
ContextMenu,
},
beforeRouteUpdate(to, from, next) {
this.getData(to.params.id);
@ -127,6 +140,23 @@ export default {
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>
@ -181,8 +211,11 @@ export default {
}
}
.like-button {
.buttons {
display: inline-block;
.button {
display: inline-block;
}
.svg-icon {
height: 18px;
width: 18px;

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,12 @@
const webpack = require('webpack');
const path = require('path');
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
// 生产环境打包不输出 map
productionSourceMap: false,
devServer: {
disableHostCheck: true,
port: process.env.DEV_SERVER_PORT || 8080,
@ -53,16 +56,37 @@ module.exports = {
symbolId: 'icon-[name]',
})
.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: {
// electron-builder的配置文件
electronBuilder: {
nodeIntegration: true,
externals: [
'@unblockneteasemusic/server',
'@unblockneteasemusic/server/src/consts',
],
externals: ['@unblockneteasemusic/rust-napi'],
builderOptions: {
productName: 'YesPlayMusic',
copyright: 'Copyright © YesPlayMusic',
@ -155,6 +179,16 @@ module.exports = {
'jsbi',
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 => {

10963
yarn.lock

File diff suppressed because it is too large Load diff