Repository: listen1/listen1_chrome_extension Branch: master Commit: 3f24efa04512 Files: 58 Total size: 783.2 KB Directory structure: gitextract__a87_dzy/ ├── .eslintrc.json ├── .github/ │ └── workflows/ │ └── eslint.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── README_EN.md ├── css/ │ ├── common.css │ ├── common2.css │ ├── cover.css │ ├── hotkeys.css │ ├── icon.css │ ├── iparanoid.css │ ├── iparanoid2.css │ ├── notyf_custom.css │ ├── origin.css │ ├── origin2.css │ ├── player.css │ └── reset.css ├── i18n/ │ ├── en-US.json │ ├── fr-FR.json │ ├── ko-KR.json │ ├── pt-BR.json │ ├── zh-CN.json │ └── zh-TC.json ├── js/ │ ├── app.js │ ├── background.js │ ├── bridge.js │ ├── controller/ │ │ ├── auth.js │ │ ├── instant_search.js │ │ ├── my_playlist.js │ │ ├── navigation.js │ │ ├── platform.js │ │ ├── play.js │ │ ├── playlist.js │ │ └── profile.js │ ├── github.js │ ├── l1_player.js │ ├── lastfm.js │ ├── loweb.js │ ├── lowebutil.js │ ├── myplaylist.js │ ├── oauth_callback.js │ ├── player_thread.js │ └── provider/ │ ├── bilibili.js │ ├── kugou.js │ ├── kuwo.js │ ├── localmusic.js │ ├── migu.js │ ├── netease.js │ ├── qq.js │ ├── taihe.js │ └── xiami.js ├── listen1.html ├── manifest.json ├── manifest_firefox.json ├── package.json └── rules_1.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "parserOptions": { "ecmaVersion": 11 }, "extends": [ "airbnb-base", "prettier" ], "rules": { "camelcase": "off", "linebreak-style": "off" }, "globals": { "axios": "readonly", "window": "readonly", "browser": "readonly", "chrome": "readonly" }, "env": { "browser": true, "node": false }, "ignorePatterns": [ "**/vendor/*.js" ] } ================================================ FILE: .github/workflows/eslint.yml ================================================ name: ESLint on: push: branches: [master] pull_request: branches: [master] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: 16 - name: Install Dependencies run: npm ci - name: Run ESLint run: npx eslint . ================================================ FILE: .gitignore ================================================ # Folder view configuration files .DS_Store .vscode .eslintcache node_modules dist _metadata ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "trailingComma": "es5" } ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Listen 1 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Listen 1 (Chrome Extension) V2.33.0 (最后更新于 2025 年 6 月 17 日) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) [English Version](https://github.com/listen1/listen1_chrome_extension/blob/master/README_EN.md) ## 缘起 当我发现找个想听的歌因为版权听不了,需要打开好几个网站开始搜索,来回切换让我抓狂的时候,我知道是时候该做点什么了。 妈妈再也不用担心我找不到我想听的歌了。 支持音乐平台 - 网易云音乐 - QQ 音乐 - 酷狗音乐 - 酷我音乐 - bilibili - 咪咕音乐 - 千千音乐 搜歌,听歌,就用 `Listen1`。 [![imgur](https://i.imgur.com/dIVFtor.gif)]() V2.9.0 新特性:自动切换播放源(Beta) 当一首歌的播放源不可用时,会自动搜索其他平台,获得可用的播放源。避免了用户手动搜索的麻烦。 还有精选歌单哦。 ## 官方商店安装(推荐) 按你的浏览器类型点击下面的链接安装 - [Chrome Web Store 安装](https://chrome.google.com/webstore/detail/listen-1/indecfegkejajpaipjipfkkbedgaodbp) - [FireFox 安装](https://addons.mozilla.org/zh-CN/firefox/addon/listen1/) - [Microsoft Edge 安装](https://microsoftedge.microsoft.com/addons/detail/bcneiehcbgahghfmgigmblcgkhihehad) 感谢 [@TNT-c](https://github.com/TNT-c) 维护 Firefox 的发布渠道 感谢 [@dhxh](https://github.com/dhxh) 维护 Microsoft Edge 的发布渠道 ## Chrome 下载安装 1. 下载项目的 zip 文件,在右上方有个 `Download ZIP`, 解压到本地 2. chrome 右上角的设置按钮下找到更多工具,打开`扩展程序` 3. 选择 `加载已解压的扩展程序`(如果没有显示先选中`开发者模式`),选中解压后的文件夹,完成! ## Firefox 打包安装 1. 将根目录下 manifest_firefox.json 替换 manifest.json 2. `cd listen1_chrome_extension` 3. `zip -r ../listen1.xpi *`, 完成打包 xpi 文件 4. 打开 Firefox,加载 xpi 文件,完成安装 ## QQ 音乐举报 Listen1 导致代码库临时关闭事件 (2017 年 11 月) Listen1 的用户,有个坏消息希望和大家分享。Listen1 最近收到了[QQ 音乐的 DMCA Takedown Notice](https://github.com/github/dmca/blob/master/2017/2017-11-17-Listen1.md), 主要代码库已经因为此原因而临时关闭。悲观一点看,Listen1 项目可能会在今年内彻底消失。 Listen1 诞生的初衷从不是和大公司的争夺版权利益,而是为了给予热爱音乐的人更好的收听体验,所以,Listen1 是开源,免费的,并且不接受任何形式的捐助。正是因为有热爱音乐的 Listen1 的你们,Listen1 才发展到今天这一步。不管结果如何,Listen1 团队感谢所有支持过这个项目的人们。 在这个关系项目生死存亡的时刻,我寻求项目因为 DMCA 被 github 关闭的援助。如果有对这个比较了解如何解决的人,或者你想对这个事情发表看法和建议,可以在[issue](https://github.com/listen1/listen1_chrome_extension/issues/113)留言,或者发送邮件到 githublisten1@gmail.com。我们会尽最大努力,来守护 Listen1,即使可能它即将成为历史。 ## 更新日志 `2025-06` 修复: - 修复咪咕无法播放的问题 - 修复播放部分歌曲时播放控制失灵的问题(#1200) (#1201) (感谢 @reserveword 的提交) - 修复 electron remote 模块以支持更多的 electron 版本 (感谢 @xihale 的提交) - 修复新主题性能问题 (#1216) (感谢 @mikelxk 的提交) - 修复 electron 的 cookie 未发送问题 - 修复文档拼写错误 (感谢 Nicholas Wilson) 功能改进: - 将 chrome 插件的 manifest 升级到 V3 `2023-08 ~ 2024-04` 修复: - 迁移哔哩哔哩用户信息获取接口 (感谢@wuhao-igno 的提交) - 修复 QQ 音乐播放问题 - 修复酷我播放和搜索问题 - 修复酷狗音乐播放问题 `2023-07` 修复: - 酷我音乐搜索功能错误 (感谢@yhsj0919 提供解决方案) - 控制台报错 (感谢 @caoxiemeihao 的提交) `2022-12 ~ 2023-03` 功能改进: - 增加了葡萄牙语(巴西)的翻译 (感谢 @nailtonvital 的提交) - 增加了 qq 音乐 320kbps 音质的支持 (感谢 @fecet 的提交) 修复: - 修复了酷我音乐播放接口失效的问题 (感谢 @NickeyLin 的提交) - 修复导出歌单失败的问题 (感谢 @@IcedWatermelonJuice 的提交) - 修复了现在播放页面打开后图标状态的问题 (感谢 @@mnyon 的提交) - 修复了 lastfm 不记录最近播放的问题 (感谢 @@Anmizi 的提交) `2022-11` 功能改进: - 增加 bilibili 歌单搜索功能(感谢 @Wei-bin-Wu 的提交) - 优化现代主题的显示风格 (感谢 @814959822 的提交) 修复: - 修复 bilibili 部分音乐无法播放的问题 - 修复本地音乐播放文件类型的问题 (感谢 @mikelxk 的提交) - 修复播放 b 站音乐时的图片显示问题 (感谢 @mikelxk 的提交) `2022-09` 功能改进 - 添加现代白,现代黑两款主题(感谢 @814959822 的提交) - 优化了新主题性能 修复: - 修复 bilibili 的搜索错误 (感谢 @mikelxk 的提交) - 修复选取音乐平台时发生的闪动问题(感谢 @814959822 的提交) - 修复新主题在歌单少于 5 首时的显示问题 `2022-08` 修复: - 修复开启一段时间后无法播放的问题 #902 (感谢 @reserveword 的提交) - 修复 QQ 音乐无法搜索的问题 (感谢 lx-music-desktop 提供技术方案) - 修复 bilibili 搜索没有响应的问题 `2022-07` 功能改进: - 增加 bilibili 视频音源搜索功能 修复: - 修复酷狗音乐的播放错误 - 修复酷狗音乐热门歌单加载更多时的错误 - 修复 qq 音乐歌手页的错误 - 修复咪咕音乐排行榜打开时的错误 `2022-06` 功能改进 - 添加双击歌单列表和搜索结果列表播放(感谢 @piz-ewing 的提交) - 桌面版本地音乐增加 wav 格式支持 (感谢 @mikelxk 的提交) 修复: - 修复清空列表当前播放音乐不停止的问题(感谢 @leca 的提交) - 酷狗音乐列表无法打开的问题 - 修复随机播放模式出现重复音乐的 bug (感谢 @piz-ewing 的提交) `2022-02 ~ 2022-03` 功能改进: - 增加韩语支持(感谢 @kkange 的提交) 修复: - 酷狗音乐无法播放的问题 - 千千音乐列表接口无法访问的问题(感谢 @mikelxk 的提交) `2021-08 ~ 2022-01` 修复: - 修复音乐分类按钮显示没有间距的问题 (感谢 @yinzhenyu-su 的提交) - 修复在 firefox 无法打开 bilibili 音乐的问题 (感谢 @ktmzcpl 的提交) - 修复在 electron 环境启动时的 UI 崩溃问题 优化: - 更平滑的当前播放切换效果 (感谢 @mikelxk 的提交) `2021-07` 修复: - 禁止图片拖动 - 增加快捷键中放大缩小功能的描述 - 修改 windows 用户的窗口控制按钮位置到右上角 (感谢 @mikelxk 的提交) - 升级 howler 库 (感谢 @mikelxk 的提交) - 修复 QQ 音乐无法搜索的问题 - 修复 chrome 浏览器媒体控制中进度条拖动的问题 (感谢 @mikelxk 的提交) - 增加本地音乐的本地 lrc 歌词文件支持 (感谢 @mikelxk 的提交) `2021-04` 功能改进: - 增加 QQ 音乐的登录支持 - 增加拖拽支持,支持歌单内歌曲调整顺序,歌单调整顺序,正在播放歌曲调整顺序,以及拖动歌曲加入歌单的操作 - 支持歌单内搜索 - 桌面版支持代理设置 - 支持配置自动切换源的搜索平台 - 增加显示当前最新版本 - 增加对网易云平台的默认高码率音源支持 重构和优化: - 将音乐平台接口做 class 改造 #553 - github 模块去除 angular 依赖 #532 (感谢 @Dumeng 的提交) - lastfm 模块去除 angular 依赖 #532 (感谢 @Dumeng 的提交) - 优化 UI 细节,提升用户体验 #537 修复: - 修复需要登录才能获取咪咕播放链接,并增加码率数据 #536 (感谢 @RecluseWind 的提交) - 修复音乐榜和影视榜在 Firefox 上的不能正确获取的 bug #536 (感谢 @RecluseWind 的提交) - 修复某些情况下歌曲在播放前总是等待 15 秒的 bug - 修复 QQ 音乐短链接歌单分享地址不被识别的问题 - 修复开启关闭静音功能失效的问题 - 修复 GitHub 账户无法退出的问题 - 修复 kugou 部分音乐因专辑缺失导致的播放错误 - 修复多首歌曲重复播放的问题 `2021-03` 功能改进: - 新增千千音乐平台 (感谢 @Dumeng 的提交) - 支持咪咕音乐的分类歌单和排行榜歌单功能 (感谢 @RecluseWind 的提交) - 桌面版支持放大功能 (感谢 @mikelxk 的提交) - 支持网易登录功能,支持打开我的歌单和推荐歌单 - 支持咪咕登录功能 - 支持在正在播放页面显示当前播放歌曲的码率和平台 - 移除虾米平台 重构和优化: - 替换了对 translate,i18n, hotkeys 的 angular 模块依赖,替换为纯 js 库 (感谢 @Dumeng 的提交) - 优化载入 feather 图标库的效率 (感谢 @Dumeng 的提交) - 改善了多个平台默认码率,默认播放高码率音乐文件 - 将 app.js 按多个 controller 模块分为多个文件 - 优化显示了因为版权问题无法播放的通知 - 将大部分链接改成 https 协议 修复: - 修复新语法导致媒体控制在某些系统中不可用的问题 (感谢 @mikelxk 的提交) - 修复音量控制快捷键失效的问题 (感谢 @mikelxk 的提交) - 修复了在 firefox 上的滚动条样式 (感谢 @RecluseWind 的提交) - 修复酷狗音乐封面的错误 - 修复酷狗某些歌曲不能播放的问题 - 修复通知无法显示的问题 - 修复了删除当前播放列表歌曲后导致的各种异常 `2021-02` 功能改进: - 支持分类歌单和排行榜(感谢 https://github.com/lyswhut/lx-music-desktop 提供 QQ 音乐排行实现) - 增加繁体中文翻译 (感谢 @yujiangqaq 提供翻译) - 增加 chrome 媒体控制上一曲,下一曲和快进快退 (感谢 @mikelxk 的提交) - 改进桌面版桌面歌词,增加字体大小颜色设置和背景透明度调整 重构: - 将媒体资源服务重构成 MediaService 模块,除去对 angularjs 的依赖 (特别感谢 @Dumeng 的提交) - 增加 prettier 配置文件和 commit 前检查 (感谢 @mikelxk 的提交) - 修正一些过往代码的格式错误 (感谢 @mikelxk 的提交) 修复: - 修复 Github API (感谢 @NoDocCat 和 @Dumeng 的提交) - 修复因 svg 动画导致的性能问题 (感谢 @Dumeng 的提交) - 修复虾米部分失效 API(感谢 @RecluseWind 的提交) - 修复 Mac 桌面版无法导入本地音乐的问题 (感谢 @virgil1996 的提交) - 修复酷我搜索出错的问题 `2021-01` 功能改进: - 支持插件版后台播放功能 (特别感谢 @Dumeng 的提交) - 优化酷我代码 (感谢 @RecluseWind 的提交) - 优化咪咕音乐代码 (感谢 @RecluseWind 的提交) - 本地音乐支持 flac 格式 (感谢 @mikelxk 的提交) - 在软件中增加反馈链接 (感谢 @mikelxk 的提交) - 增加虾米歌单搜索,统一端口代码 (感谢 @RecluseWind 的提交) - 优化了歌单访问,增加本地缓存 重构: - 更换所有加解密库到 forge (感谢 @Dumeng 的提交) - 去除对 jquery 库的依赖 (感谢 @Dumeng 的提交) - 更换音频播放库到 howler.js (感谢 @Dumeng 的提交) - 更换 http 请求库到 axios (感谢 @Dumeng 的提交) - 支持 eslint 的 github action 语法检查 (感谢 @Dumeng 的提交) bug 修复: - 修复 MediaSession 不支持时的报错问题 (感谢 @Jyuaan 的提交) - 修复咪咕歌单的 404 错误 - 修复正在播放窗口点击空白处弹回的功能 (感谢 @Demeng 的提交) `2020-12-28` - 修复最大,最小,关闭按钮在桌面版失效的问题 `2020-12-27` - 修复无法显示收藏歌单的 bug - 支持一次输入搜索所有平台(Beta) - 修复咪咕音乐歌单只显示前 20 首歌的 bug - 修复网易和酷狗音乐搜索错误未处理的 bug - 修复虾米音乐歌词解析错误导致无法显示的 bug - 根据 chrome web store 上架要求修改部分权限 `2020-12-22` - 修复酷我音乐无法播放的问题 - 修复我创建的歌单升级后无法播放的问题 `2020-12-20` - 修复版权问题造成的播放中断和循环弹出提示通知的 bug - 修改歌曲封面为背景时歌词看不清的问题 - 修复 qq 搜索的一个错误,优化接口返回时处理(感谢@RecluseWind 的提交) `2020-12-12` - 支持 QQ 音乐歌单搜索 (感谢@RecluseWind 的提交) - 修复网易云音乐无法打开手机分享的歌单链接的 bug (感谢@RecluseWind 的提交) - 修复咪咕音乐无法搜索的 bug `2020-10-28` - 增加本地音乐(仅限桌面版) `2020-10-27` - 增加歌单搜索功能(暂时只支持网易云) - 优化歌词显示 - 修复 blili 歌手 API 错误,修复歌词时间轴格式不统一产生的错误 (感谢@RecluseWind 的提交) - 优化 UI,正在播放页增加翻译按钮 `2020-10-26` - 增加歌词翻译功能 QQ 音乐和虾米音乐的支持(感谢@RecluseWind 的提交) - 更新了虾米音乐获取歌曲播放地址,获取歌单,搜索 API 的获取方式,增加可靠性 (感谢@RecluseWind 的提交) - 修复安装插件后 qq 音乐网页部分歌单无法打开的 bug `2020-10-18` - 增加歌词翻译功能,暂时只支持网易云音乐 (感谢@reserveword 的提交) - 修复 bilibili 音乐无法播放的 bug - 修复虾米播放页歌曲封面无法显示的 bug - 修复酷我音乐歌单无法打开的 bug `2020-09-12` - 修复网易歌单超过 1000 首时导入失败的 bug (感谢@YueShangGuan 的提交) - 支持显示歌曲封面作为正在播放背景 (感谢@YueShangGuan 的提交) `2020-08-24` - 修复虾米歌单歌曲只显示部分歌曲的 bug (感谢@RecluseWind 的提交) - 修复歌单图片和标题显示问题 (感谢@RecluseWind 的提交) - 支持桌面版点击链接打开系统默认浏览器 `2020-08-04` - 增加正在播放窗口和播放列表弹窗的动画效果 - 修复虾米艺人封面图片无法显示的问题 (感谢@RecluseWind 的提交) - 优化打开歌单功能,支持网易云排行榜单,艺人页面,专辑页面网址(感谢@whtiehack 的提交) - 优化专辑图片显示,避免图片被压缩 (感谢@RecluseWind 的提交) `2020-07-10` - 修复咪咕音乐无法播放的问题 - 支持顶部搜索栏回车触发 (感谢@kangbb 的提交) - 支持歌单歌曲数显示,支持播放/暂停全局快捷键(桌面版)(感谢@x2009again 的提交) - 支持返回时回到滚动条历史位置(感谢@x2009again 参与完成) - 优化 firefox 滑动条,修改 qq 音乐图标网址,解决 firefox 上架 jquery 代码问题 (感谢@RecluseWind 的提交) `2020-06-29` - 支持播放失败时自动切换播放源(Beta) `2020-06-28` - 修复网易歌单仅显示 10 首歌曲的问题 `2020-04-30` - 修复咪咕音质较差的问题 `2020-04-27` - 增加收藏歌单功能,特别感谢 @zhenyiLiang - 修复咪咕音乐无法播放的 bug - 一些细节优化 `2019-11-27` - 加入法语支持, 特别感谢 @Leoche `2019-09-07` - 修复 migu 无法播放的 bug `2019-08-09` - 增加深色主题 `2019-07-03` - 修复咪咕音乐无法播放的 bug `2019-06-24` - 增加咪咕音乐 - 修复网易音乐无法播放的 bug - 修复酷狗音乐无法播放的 bug `2019-06-23` - 修复无法连接到 github 的 bug `2019-05-26` - 修复酷狗音乐无法播放的 bug `2019-04-26` - 修复虾米音乐无法播放的 bug - 修复播放器未在页面底端显示的 bug `2019-03-03` - 修复删除单个歌曲导致歌单所有歌曲消失的 bug - 修复删除单个歌单导致所有歌单消失的 bug `2019-02-26` - 修复 qq 音乐歌单无法显示的 bug `2018-12-30` - 修复酷我音乐歌单缺失歌曲的问题 - 自动检测客户端语言 `2018-12-29` - 修复虾米音乐搜索失败的问题 - 修复部分 QQ 音乐歌曲无法播放的问题 - 修复使用插件时 QQ 官方网站无法使用的问题 `2018-12-24` - 多语言支持,支持英文 - 新添加到歌单的歌曲将出现在歌单头部 - 修复版权通知占满屏幕的 bug `2018-12-22` - 全新版本 2.0 发布,更新界面(特别感谢@iparanoid 提供主题设计) - 升级 jquery 和 angular 版本 `2018-12-21` - 修复虾米音乐歌单访问的问题 - 修复网易云音乐歌单只有一首歌的问题 - 修复 bilibili 滚动时加载重复歌单的问题 - 修复酷狗部分音乐无法播放的问题 - 修复 Github Gist 备份无法导入的问题 - 升级 soundmanager2 库到最新版本 `2018-12-05` - 完全修复虾米音乐歌单访问的问题 `2018-08-25` - 修复虾米音乐无法播放的 bug `2018-06-15` - 增加酷我音乐的支持(特别感谢@WinterXMQ 的提交) `2018-06-10` - 修复酷狗音乐收藏歌单后可能显示空歌单的 bug `2018-06-10` - 修复虾米音乐无法显示歌词的 bug `2018-06-05` - 增加酷狗音乐的支持(感谢@WinterXMQ ) `2018-05-30` - 修复 QQ 音乐无法播放的问题(感谢@noschoollee 提供修复方案) `2018-04-23` - 修复虾米音乐无法播放的问题 `2018-02-18` - 修复无法创建歌单的 bug - 修复点击关闭歌单按钮后无法再打开歌单的 bug - 增加歌曲主页,点击封面可进入(特别感谢@iparanoid 提供歌曲页面 UI 设计) `2018-02-15` - 修复随机播放在播放列表播放结束后自动停止的问题,开启无限洗脑循环(感谢@sunjie21 的提交) - 增加将当前播放列表全部添加到歌单的功能 (感谢@sunjie21 的提交) - 修复标题播放状态不实时更新的 bug (感谢@sibojia 的提交) `2018-02-14` - 修复主页在加载更多数据时出现双重滚动条的 bug,并修改了滚动条样式(感谢@zhuzhuyule 的提交) - 修复打开歌单时,网易云音乐个人歌单地址无法解析的 bug(感谢@zhuzhuyule 的提交) `2017-12-26` - 增加同步歌单到 Github Gist 功能。(特别感谢@ConstLhq 提供创意和部分代码实现) `2017-12-20` - 增加搜索翻页功能,你可以看到更多的搜索结果了。(感谢@ConstLhq 的提交) - 增加合并歌单功能。可以快速的把其它你创建的歌曲合并到当前的歌单中了。(感谢@Dumeng 的提交) `2017-11-27` - 修复网易云音乐歌单只显示第一首歌的 Bug(感谢[@Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)提供接口实现) `2017-11-18` - 修复版权原因无法播放歌曲时自动暂停的问题 `2017-11-17` - 在我的歌单页面增加“打开歌单”功能,可打开支持网页的歌单链接地址。这样就可以导入你喜欢的歌单了。 - HTTP 请求头部的 Origin 字段设置为正常网址 `2017-10-16` - 修复 QQ 音乐歌单翻页显示重复的问题(感谢@Moobusy 的提交) `2017-10-03` - 修复网易云音乐歌单无法显示的问题(感谢@Moobusy 的提交) `2017-09-14` - 修复 QQ 音乐无法播放的 bug `2016-05-27` - 增加快捷键功能(输入?查看快捷键设置) - 支持同步播放记录到 last.fm - 增加搜索 loading 时的图标(感谢@richdho 的提交) - 页面标题增加显示当前播放信息 - 修复了在收藏对话框点击取消出现新建歌单的 bug - 重新组织代码文件夹结构 `2016-05-21` - 增加歌单分页加载功能(感谢@wild-flame 的提交) - 修复关闭按钮随网页滚动的 bug - 修复点击暂停按钮会重置进度条和歌词的 bug - 修复点击歌单名称不跳转的 bug - 调整歌单水平位置居中 `2016-05-14` - 增加 firefox 插件支持(感谢 fulesdle 的提交) `2016-05-13` - 增加我的歌单功能,可以收藏现有歌单,并创建自己的歌单 - 点击 Listen 1 和图标可以回到首页 - 标记了部分因版权无法播放的歌曲,增加版权提示 - 重构了音乐平台代码,使用统一的接口规范 - 重构了歌单接口,合并歌手,专辑和歌单接口 - 修复了阿里云歌手链接点击错误的 bug `2016-05-08` - 增加歌词显示 - 精选歌单:添加歌单到当前播放列表,可点击跳转到原始链接 - 修复了搜索 qq 音乐时的乱码问题 - 修复了循环播放网易歌曲一段时间后暂停的 bug - 修复了可能导致微信公众号无法登录的 bug - 优化性能,删除了不必要的事件消息触发 `2016-05-02` - 增加音量控制 ## License MIT ================================================ FILE: README_EN.md ================================================ # Listen 1 (Chrome Extension) V2.33.0 (Last Update June 17th, 2025) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) ## One for all free music in China When I found many songs are unavailable because copyright issue, I realized there's something I should do. Mom never need to worry about I can't listen my favorite songs. Supported music platform: - Netease - QQ - Kugou - Kuwo - Bilibili - Migu - Qianqian (taihe) Search songs, listen songs from multiple platforms, that's `Listen 1`. V2.9.0 New Feature: Auto choose source when music play source url is not available, auto choose source from other sources. Making your own playlist is also supported. ## How to change language ? 1. Click Settings icon in right top of application 2. Click `English` under `Language` or `语言` ## Install (Chrome) 1. download zip file from github and uncompress to local. 2. open Extensions from chrome. 3. Choose `Load unpacked`(Open Develop Mode first),Click folder you just uncompressed, finish! ## Install (Firefox) 1. Visit Listen1 Firefox Page https://addons.mozilla.org/zh-CN/firefox/addon/listen-1/ 2. Click Add to Firefox button ## Changelog `2025-06` Fix bugs: - fix migu playing error - fix playing control problem (#1200) (#1201) (thanks @reserveword ) - fix electron remote to support more electron version (thanks @xihale ) - fix performance issue for new theme (#1216) (thanks @mikelxk ) - fix electron cookie send problem - fix spell error (thanks Nicholas Wilson) Features: - migrate chrome extension manifest version to V3 `2023-08 ~ 2024-04` Fix bugs: - Migrate bilibili artist info api (thanks @wuhao-igno) - fix qq music - fix kugou music - fix kuwo music `2023-07` Fix bugs: - kuwo search api error(thanks @yhsj0919) - console output error(thanks @caoxiemeihao) `2022-12 ~ 2023-03` Features: - Add language support for Brazilian portuguese (thanks @nailtonvital) - Add new quality 320kpbs for qq music (thanks @fecet) Fix bugs: - fix kuwo music play url error (thanks @NickeyLin) - fix songlist export problem (thanks @@IcedWatermelonJuice) - fix icon state when toggle now playing page (thanks @@mnyon) - fix lastfm record error (thanks @@Anmizi) `2022-11` Features: - Search playlist for bilibili video (thanks @Wei-bin-Wu) - Optimize modern theme style (thanks @814959822) Fix bugs: - Fix bilibili tracks play error - Fix local music play problem related to file types (thanks @mikelxk) - Fix image related problem when playing bilibili music (thanks @mikelxk) `2022-09` Features: - Add new themes: modern white, modern black(thanks @814959822) - Optimize new theme performance Fix bugs: - fix bilibili search problem (thanks @mikelxk) - fix hover shaking problem when choose platform(thanks @814959822) - fix player bar empty when playlist contains less than five songs in modern theme `2022-08` Fix bugs: - fix music can't play after idle for a while #902 (thanks @reserveword) - fix qq music search not working (thanks to lx-music-desktop for solution) - fix bilibili search response error `2022-07` Features: - add bilibili search page for audio in video search Fix bugs: - fix kugou music play error - fix kugou hot playlist load more bug - fix qq singer page error - fix migu top list open error `2022-06` Features: - Double click to play in playlist and search result page(thanks @piz-ewing) - Support wav format local file in desktop version (thanks @mikelxk) Fix bugs: - fix shuffle mode play duplicate music bug (thanks @piz-ewing) - fix music continue to play when clear now playing playlist (thanks @leca) - fix kugou music play fail `2022-02 ~ 2022-03` Features: - Add Korean language support(thanks @kkange) Fix bugs: - fix kugou api play song error - fix qianqian music api error(thanks @mikelxk) `2021-08 ~ 2022-01` Fix bugs: - fix music category line height (thanks @yinzhenyu-su) - fix bilibili play issue in firefox (thanks @ktmzcpl) - fix UI crash in electron environment Optimaze: - More fluent effect for current playing switching (thanks @mikelxk) `2021-07` Fix Bugs: - disable image drag - add shortcuts description for zoom in/out - move window control panel to top right for windows users (thanks @mikelxk) - upgrade howler lib (thanks @mikelxk) - fix QQ search problem - fix media center progress bar control for chrome users - add local lrc file support when import local music (thanks @mikelxk) `2021-04` Features: - QQ Login - Drag and drop to reorder songs in playlist, reorder playlist and quick add song to playlist - Search in playlist - Proxy setting (desktop version only) - Configure auto detect playable source list - Display latest version in setting page - Highest bitrate for netease music Refactor: - Change music platform resource API to class #553 - remove angular dependency for github module #532 (thanks @Dumeng) - remove angular dependency for lastfm module #532 (thanks @Dumeng) - UX optimaze #537 Fix Bugs: - Fix migu resource api to use without login, add bitrate info #536 (thanks @RecluseWind) - Fix display error in firefox for migu hot rank #536 (thanks @RecluseWind) - Fix sometimes song keep waiting for 15 seconds before playing bug - Fix qq short link parse error - Fix toggle mute error - Fix GitHub logout error - Fix some kugou music without album play error - Fix two songs play in same time `2021-03` Features: - Add qianqian music platform (thanks @Dumeng) - Support playlist filters and top list in migu (thanks @RecluseWind) - Zoom in/out function for desktop version (thanks @mikelxk) - Support netease login, show my playlist and recommend playlist - Support migu login - Show bitrate and music platform in now playing page - deprecated xiami Refactor: - Replace angular module dependencies: translate,i18n, hotkeys,replace with js library (thanks @Dumeng) - Optimaze feather load performance (thanks @Dumeng) - Optimaze bitrate for qq and kugou platform, default high bitrate - Split app.js into files by controller - Optimaze copyright notice show - Change http to https for several links Fix bugs: - Fix media control invalid because new es6 optional chain (thanks @mikelxk) - Fix volume control not working (thanks @mikelxk) - Fix scroll bar style in firefox (thanks @RecluseWind) - Fix kugou music cover url - Fix kugou music play url - Fix notification not shown bug - Fix delete songs in current playlist mess up playing bug `2021-02` Features: - Support playlist filters and top playlist (special thanks [lyswhut/lx-music-desktop](https://github.com/lyswhut/lx-music-desktop) ) - Add Traditional Chinese language (thanks @yujiangqaq) - Add chrome media panel function: prev/next track, back/forward (thanks @mikelxk) - New lyric floating window, support config font size, color and background transparency Refactor: - Build MediaService module,remove dependency on angularjs(special thanks @Dumeng) - Add prettier config file, add pre-commit style check(thanks @mikelxk) - Fix history code style problems(thanks @mikelxk) Fix bugs: - Fix Github API (thanks @NoDocCat 和 @Dumeng) - Fix svg animation performance issue (thanks @Dumeng) - Fix xiami API(thanks @RecluseWind) - Fix import local music error for mac desktop version(thanks @virgil1996) - Fix kuwo search error `2021-01` Features: - support play music background (thanks @Dumeng) - optimaze kugo related code (thanks @RecluseWind) - optimaze migu related code (thanks @RecluseWind) - support flac for local music (thanks @mikelxk) - add feedback link (thanks @mikelxk) - optimaze xiami music, add playlist search (thanks @RecluseWind) - optimaze cache for playlist Refactor: - replace encrypt lib to forge (thanks @Dumeng) - remove jquery (thanks @Dumeng) - replace ngsoundmanager2 to howler.js (thanks @Dumeng) - replace angular http to axios (thanks @Dumeng) - support eslint check in github action (thanks @Dumeng) Fix bugs: - fix MediaSession error when not supported (thanks @Jyuaan) - fix migu playlist 404 link - fix current playing music list modal (thanks @Demeng) `2020-12-28` - fix bug for desktop: max,min,close button not available `2020-12-27` - fix bug: can't play favorite playlist - feature: search all music (beta) - fix bug: migu playlist shows first 20 tracks - fix bug: netease/kugou search error not handle - fix bug: xiami lyric parse error - change manitest permission config to pass chrome web store review `2020-12-22` - fix bug: kuwo music can't be played - fix bug: after upgrade v2.17.2, my playlist can't be played `2020-12-20` - fix play interrupted by copyright notice bug, infinite notice popup bug - change style for now playing page when using album cover as background - fix minor bug for qq search and optimaze api handler(thanks @RecluseWind) `2020-12-12` - support search songlist for qq music (thanks @RecluseWind) - fix bug: netease songlist shared by mobile open error (thanks @RecluseWind) - fix bug: migu search song error `2020-10-28` - add local music (desktop version only) `2020-10-27` - support search playlist (only for netease by now) - optimaze lyric display - fix bilibili artist api, fix lyric time tag format parse error (thanks @RecluseWind) - optimaze UI, add translate button in now playing page `2020-10-26` - add lyric translation support for qq music, xiami music (thanks @RecluseWind) - update xiami api including get playlist, search, play music (thanks @RecluseWind) - fix bug some playlist not response in qq music website after installed extension `2020-10-18` - add lyric translation, now for netease music only (thanks @reserveword) - fix bilibili play fail bug - fix xiami now playing page music cover missing bug - fix kuwo music can't open bug `2020-09-12` - fix netease songlist contains more than 1k tracks import error (thanks @YueShangGuan) - support album cover as nowplaying background (thanks @YueShangGuan) `2020-08-24` - fix xiami songlist only shows part of songs bug (thanks @RecluseWind) - fix songlist cover and title display bug (thanks @RecluseWind) - support open url using system default browser for desktop version `2020-08-04` - add animation for now playing and current playlist window - fix xiami cover image not loaded bug (thanks @RecluseWind) - optimaze open songlist url, support netease toplist, artist, album (thanks @whtiehack) - optimaze cover image display, avoid resize (thanks @RecluseWind) `2020-07-10` - fix migu play fail bug - support press enter key to search in search bar thanks @kangbb) - support playlist song count show, support play/pause shortcut, desktop only(thanks @x2009again) - support restore scrollbar offset when go back(thanks @x2009again for discuss solution) - optimaze firefox scorlling bar, modify source image url for qq music, fix firefox jquery lib md5 error(thanks @RecluseWind) `2020-06-29` - support auto choose source when play fail `2020-06-28` - fix netease music only show 10 tracks bug `2020-04-30` - fix migu poor music quality bug `2020-04-27` - support adding playlist to favorite, special thanks to @zhenyiLiang - fix migu music - some minor optimaze `2019-11-27` - add frech language, special thanks to @Leoche `2019-09-07` - fix migu `2019-08-09` - add dark theme `2019-07-03` - fix migu play error `2019-06-24` - add migu music - fix kugou play bug - fix netease play bug `2019-06-23` - fix connect to github.com error `2019-05-26` - fix kugou music can't play bug `2019-04-26` - fix xiami music can't play bug - fix footer player out of page bug `2019-03-03` - fix delete single playlist destroy all playlists bug `2019-02-26` - fix qq music songlist not shown bug `2018-12-30` - fix songs missing in kuwo playlist - auto detect language `2018-12-29` - fix fail on xiami search - fix some qq songs fail to play - fix qq music web visit problem after extension installed `2018-12-24` - i18n support, support English language. - new song will now add to top of playlist - copyright notification will not mess up the screen `2018-12-22` - Version 2.0 released. New UI(Special Thanks to @iparanoid) - Upgrade jquery, Angular `2018-12-21` - Fix xiami playlist bug - Fix netease playlist only shows one song bug - Fix bilibili first load duplicate playlists - Fix can't play some kugou songs - Fix github gist backup recover bug - Upgrade soundmanager2 ## License MIT ================================================ FILE: css/common.css ================================================ html, body { margin: 0; padding: 0; font-size: var(--text-default-size); color: var(--text-default-color); font-family: system-ui, 'PingFang SC', STHeiti, sans-serif; } a { cursor: pointer; } .wrap { /* https://stackoverflow.com/questions/28897089/z-index-on-borders */ outline: solid 1px var(--windows-border-color); box-sizing: border-box; } /* remove focus highlight */ input:focus, select:focus, textarea:focus, button:focus { outline: none; } ul { list-style: none; margin: 0; padding: 0; } input, svg, .icon { -webkit-app-region: no-drag; } button { background-color: var(--button-background-color); color: var(--text-default-color); cursor: pointer; border: solid 1px var(--button-background-color); border-radius: var(--default-border-radius); padding: 5px; min-width: 80px; min-height: 32px; } button:hover { background-color: var(--button-hover-background-color); } img { -webkit-user-drag: none; } .l1-button { background-color: var(--button-background-color); color: var(--text-default-color); border-radius: var(--default-border-radius); padding: 5px; margin-right: 4px; color: var(--text-default-color); cursor: pointer; display: inline-block; } .l1-button:hover { background: var(--button-hover-background-color); } svg { width: 24px; height: 24px; stroke: currentColor; stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; fill: none; cursor: pointer; /* stroke: var(--icon-default-color);*/ } /* svg:hover { fill: var(--icon-highlight-color); stroke: var(--icon-highlight-color); } */ .icon { /* default icon settings */ font-size: 16px; cursor: pointer; } /* tools utils */ .flex-scroll-wrapper { flex: 1; height: 100px; overflow-y: scroll; scrollbar-width: thin; scrollbar-color: var(--scroll-color) var(--content-background-color); } /* scroll bar style */ ::-webkit-scrollbar { width: 14px; height: 18px; background: transparent; } ::-webkit-scrollbar-thumb { height: 49px; border: 5px solid rgba(0, 0, 0, 0); background-clip: padding-box; border-radius: 7px; -webkit-border-radius: 7px; background-color: var(--scroll-color); /*rgba(151, 151, 151, 0.4);*/ /* -webkit-box-shadow: inset -1px -1px 0px rgba(0, 0, 0, 0.05), inset 1px 1px 0px rgba(0, 0, 0, 0.05);*/ } ::-webkit-scrollbar-button { width: 0; height: 0; display: none; } ::-webkit-scrollbar-corner { background-color: transparent; } /* main framework start */ .wrap { display: flex; height: 100vh; flex-direction: column; margin: auto; } /* split screen to up/down 2 parts */ .main { flex: 1; display: flex; overflow: hidden; } .footer { background: var(--foot-background-color); height: 60px; border-top: solid 1px var(--line-default-color); display: flex; position: relative; z-index: 99; } /* split main to left/right 2 parts */ .main .sidebar { flex: 0 0 200px; display: flex; flex-direction: column; background: var(--sidebar-background-color); } .main .content { background: var(--content-background-color); flex: 1; display: flex; flex-direction: column; } /* split content to up/down 2 parts */ .main .content .navigation { height: 46px; flex: 0 0 46px; border-bottom: solid 1px var(--line-default-color); display: flex; align-items: center; -webkit-app-region: drag; } .main .content .browser { flex: 1; } /* main framework end */ /*****************************************************************/ /* main sidebar start */ .sidebar .menu-control { height: 43px; width: 125px; -webkit-app-region: drag; } .sidebar .menu-title { height: 28px; line-height: 28px; margin: 0 12px 4px 12px; color: var(--link-default-color); padding-left: 10px; display: flex; align-items: center; font-size: 12px; } .sidebar .menu-title .title { flex: 1; } .sidebar .menu-title svg { flex: 0 0 18px; } .sidebar ul li { cursor: pointer; padding-left: 10px; border-top: solid 2px transparent; border-bottom: solid 2px transparent; margin-bottom: -2px; } .sidebar ul li .sidebar-block { display: flex; align-items: center; line-height: 28px; padding-left: 12px; margin: 3px 0; color: var(--text-default-color); border-radius: var(--default-border-radius); } .sidebar svg { width: 18px; height: 18px; } .sidebar ul li a { margin-left: 10px; width: 125px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .sidebar ul li:hover .sidebar-block { background: var(--sidebar-hover-background-color); color: var(--sidebar-hover-text-color); } .sidebar ul li.active .sidebar-block, .sidebar ul li.active:hover .sidebar-block { background: var(--sidebar-highlight-background-color); color: var(--sidebar-highlight-text-color); } .sidebar ul li.dragover .sidebar-block { background: var(--sidebar-highlight-background-color); color: var(--sidebar-highlight-text-color); } /* avoid hover effect trigger dragleave event https://stackoverflow.com/questions/19889615/can-an-angular-directive-pass-arguments-to-functions-in-expressions-specified-in */ .sidebar ul li * { pointer-events: none; } /* main sidebar end */ /* widget navigation start */ .navigation svg { width: 18px; height: 18px; color: var(--icon-default-color); } .navigation .icon svg { color: var(--text-default-color); } .navigation .backfront { flex: 0 0 45px; line-height: 46px; vertical-align: middle; padding: 0 13px; } .navigation .search { flex: 1; } .navigation .settings { flex: 0 0 32px; } .navigation .icon { color: var(--text-default-color); opacity: 0.5; } .navigation .icon:hover { opacity: 1; } .navigation .backfront .icon { display: inline-block; vertical-align: middle; margin-bottom: 4px; } .navigation .backfront .icon:nth-of-type(1) { margin-right: 8px; } .navigation .search-input { width: 270px; height: 23px; background: var(--search-input-background-color); border-style: none; border-radius: var(--default-border-radius); padding-left: 10px; font-size: 12px; color: var(--text-default-color); } .navigation .window-control { flex: 0 0 105px; border-left: solid 1px var(--window-control-border-color); margin-left: 15px; } .navigation .window-control svg { margin-left: 8px; } .navigation .window-control svg:first-of-type { margin-left: 15px; } /* navigation end */ /* page hot-playlist start */ .page-hot-playlist { max-width: 850px; margin: 0 auto; } .playlist-covers { margin: 0; padding: 0 13px; display: flex; flex-flow: row wrap; position: relative; } .playlist-covers li { flex: 0 1 calc(20% - 26px); min-height: 156px; color: var(--text-default-color); margin: 0 13px; } .playlist-covers .u-cover { display: flex; position: relative; } .playlist-covers .u-cover img { height: 136px; min-width: 136px; max-width: 100%; object-fit: cover; margin: auto; border: solid 1px var(--line-default-color); margin-bottom: 2px; cursor: pointer; } .playlist-covers .u-cover .bottom { position: absolute; right: 5px; bottom: 10px; height: 30px; width: 30px; cursor: pointer; opacity: 0; transition: opacity 0.2s linear; } .playlist-covers .u-cover:hover .bottom { opacity: 1; } .playlist-covers .u-cover .bottom svg { height: 30px; width: 30px; fill: rgba(200, 200, 200, 0.5); stroke-width: 1; stroke: #ffffff; } .playlist-covers .u-cover .bottom svg:hover { fill: rgba(100, 100, 100, 0.5); } .playlist-covers .desc { cursor: pointer; } .playlist-covers .desc .title { display: flex; min-height: 32px; margin: 0 0 5px; } /* page hot-playlist end */ /* page playlist-detail start */ .page .playlist-detail { padding-bottom: 37px; } .page .playlist-detail .detail-head { display: flex; } .page .playlist-detail .detail-head img { height: 150px; } .page .playlist-detail .detail-head .detail-head-cover { flex: 0 0 150px; padding: 26px 26px 8px 26px; } .page .playlist-detail .detail-head .detail-head-title { flex: 1; } .playlist-button-list { display: flex; flex-flow: row wrap; } .playlist-button-list .playlist-button { height: 26px; border: solid 1px var(--button-border-color); cursor: pointer; border-radius: 2px; display: flex; margin: 0 20px 20px 0; } .playlist-button-list .playlist-button.playadd-button { flex: 0 0 136px; } .playlist-button-list .playlist-button .play-list { flex: 1; padding: 0 18px; display: flex; align-items: center; } .playlist-button-list .playlist-button .play-list svg { margin-right: 4px; } .playlist-button-list .playlist-button.playadd-button .play-list svg { width: 14px; height: 14px; flex: 0 0 14px; margin-right: 4px; stroke: var(--important-color); fill: var(--important-color); } .playlist-button-list .playlist-button .play-list .icon { margin-right: 8px; } .playlist-button-list .playlist-button.playadd-button .play-list .icon { flex: 0 0 14px; margin-right: 4px; color: var(--important-color); } .playlist-button-list .playlist-button.playadd-button .add-list { flex: 0 0 26px; height: 26px; width: 26px; border-left: solid 1px var(--button-border-color); cursor: pointer; display: flex; align-items: center; justify-content: center; } .playlist-button-list .playlist-button.edit-button .play-list.favorited { color: var(--text-default-color); } .playlist-button-list .playlist-button.edit-button .play-list.notfavorite { color: var(--text-default-color); } .playlist-button-list .playlist-button .play-list:hover, .playlist-button-list .playlist-button.playadd-button .add-list:hover { background: var(--button-hover-background-color); } .playlist-button-list .playlist-button.playadd-button .add-list svg { width: 14px; height: 14px; } .playlist-button-list .playlist-button.clone-button, .playlist-button-list .playlist-button.edit-button, .playlist-button-list .playlist-button.fav-button { flex: 0 0 auto; } .playlist-button-list .playlist-button.clone-button .play-list svg, .playlist-button-list .playlist-button.edit-button .play-list svg, .playlist-button-list .playlist-button.fav-button .play-list svg { width: 16px; height: 16px; flex: 0 0 16px; margin-right: 8px; stroke: rgb(102, 102, 102); } .playlist-button-list .playlist-button.fav-button .play-list.favorited svg { fill: rgb(102, 102, 102); } .page .playlist-detail .detail-head .detail-head-title h2 { font-size: var(--h2-title-font-size); } /* page playlist detail end */ /* page song detail start */ .page .songdetail-wrapper { position: absolute; top: 0; left: 0; right: 0; bottom: 60px; background: var(--now-playing-page-background-color); overflow: hidden; border: solid 1px var(--windows-border-color); -webkit-app-region: no-drag; transition: all 0.3s; } .page .songdetail-wrapper .draggable-zone { position: absolute; left: 0; top: 0; right: 0; -webkit-app-region: drag; height: 80px; } .page .songdetail-wrapper.slidedown .draggable-zone { display: none; -webkit-app-region: no-drag; } .page .songdetail-wrapper .translate-switch { border: solid 1px; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; position: absolute; bottom: 30px; right: 30px; color: #888888; cursor: pointer; -webkit-app-region: no-drag; } .page .songdetail-wrapper .translate-switch:hover { color: var(--text-default-color); } .page .songdetail-wrapper .translate-switch.selected { color: var(--text-default-color); } .page .songdetail-wrapper.slidedown { top: calc(100% - 60px); } .page .songdetail-wrapper .close { position: absolute; top: 24px; left: 24px; height: 24px; width: 24px; cursor: pointer; -webkit-app-region: no-drag; } .page .songdetail-wrapper .close.mac { top: 44px; } .page .songdetail-wrapper .window-control { position: absolute; top: 24px; right: 24px; height: 24px; cursor: pointer; -webkit-app-region: no-drag; z-index: 99; } .page .songdetail-wrapper .window-control svg { margin-left: 8px; stroke: var(--now-playing-close-icon-color); } .page .songdetail-wrapper .close svg { stroke: var(--now-playing-close-icon-color); } .page .bg { opacity: 0.5; height: 100%; text-align: center; line-height: 100%; float: left; width: 100%; background-repeat: no-repeat; background-position: center; background-size: cover; filter: blur(10px) brightness(0.6); transition: background ease-in-out 1.5s; } .page .playsong-detail { position: absolute; left: 10px; right: 10px; max-width: 770px; margin: 0 auto; display: flex; height: 100%; } .page .playsong-detail .detail-head { flex: 0 0 350px; overflow: hidden; } .page .playsong-detail .detail-head .detail-head-cover { width: 250px; height: 250px; margin-top: 110px; } .page .playsong-detail .detail-head img { width: 250px; height: 250px; object-fit: cover; } .page .playsong-detail .detail-songinfo { flex: 1; margin-top: 80px; display: flex; flex-direction: column; overflow: hidden; -webkit-app-region: no-drag; } .page .playsong-detail .detail-songinfo .title { display: flex; align-items: center; } .page .playsong-detail .detail-songinfo .title h2 { font-size: var(--h2-title-font-size); font-weight: 400; } .page .playsong-detail .detail-songinfo .title .badge { font-size: var(--badge-font-size); color: var(--badge-font-color); border: solid 1px var(--badge-border-color); border-radius: 3px; margin-left: 5px; padding-left: 4px; padding-right: 4px; margin-top: 4px; box-sizing: border-box; height: 20px; display: flex; align-items: center; justify-content: center; white-space: nowrap; } .page .playsong-detail .detail-songinfo .title .badge.platform { padding-top: 1px; } .page .playsong-detail .detail-songinfo .title .badge:first-of-type { margin-left: 15px; } .page .playsong-detail .detail-songinfo .info { border-bottom: solid 1px var(--line-default-color); padding-bottom: 6px; flex: 0 0 20px; display: flex; } .page .playsong-detail .detail-songinfo .info a { cursor: pointer; } .page .playsong-detail .detail-songinfo .info .singer { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .page .playsong-detail .detail-songinfo .info .album { flex: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .page .playsong-detail .detail-songinfo .info span { color: var(--lyric-default-color); } .page .coverbg .playsong-detail .detail-songinfo .info span { color: var(--lyric-on-cover-color); } .page .playsong-detail .detail-songinfo .lyric { position: relative; flex: 0 0 380px; overflow-y: scroll; color: var(--lyric-default-color); scrollbar-width: thin; scrollbar-color: var(--scroll-color) transparent; font-size: var(--lyric-font-size); } .page .coverbg .playsong-detail .detail-songinfo .lyric { color: var(--lyric-on-cover-color); } .page .playsong-detail .detail-songinfo .lyric p { margin: var(--lyric-line-margin) 0 0 0; } .page .playsong-detail .detail-songinfo .lyric p.translate { margin: 5px 0 0 0; } .page .playsong-detail .detail-songinfo .lyric p.hide { display: none; } .page .playsong-detail .detail-songinfo .lyric p.highlight { color: var(--lyric-important-color); } .page .coverbg .playsong-detail .detail-songinfo .lyric p.highlight { color: var(--lyric-important-on-cover-color); } ul.detail-songlist { padding: 0 25px; position: relative; } ul.detail-songlist .playlist-search { position: absolute; right: 0; top: -30px; } ul.detail-songlist .playlist-search .playlist-search-icon { width: 14px; position: absolute; left: 7px; top: 1px; } ul.detail-songlist .playlist-search .playlist-clear-icon { width: 14px; position: absolute; left: 158px; } ul.detail-songlist .playlist-search .playlist-search-input { margin-right: 28px; margin-bottom: 10px; border: none; height: 24px; border-radius: 12px; padding: 0 30px; background: var(--content-background-color); color: #bbbbbb; width: 120px; } ul.detail-songlist .playlist-search .playlist-search-input:hover { background-color: var(--songlist-odd-background-color); } ul.detail-songlist .playlist-search .playlist-search-input::placeholder { color: #bbbbbb; } ul.detail-songlist li { /* https://stackoverflow.com/questions/4157005/css-positioning-z-index-negative-margins */ position: relative; display: flex; border-top: solid 2px var(--songlist-border-color); border-bottom: solid 2px var(--songlist-border-color); height: 37px; align-items: center; padding: 0 20px; font-size: 14px; margin-bottom: -2px; } ul.detail-songlist li.playlist-result { height: 80px; padding: 0 10px; } ul.detail-songlist li.odd { background-color: var(--songlist-odd-background-color); } ul.detail-songlist li:hover, ul.detail-songlist li.odd:hover { background-color: var(--songlist-hover-background-color); } ul.detail-songlist li a { cursor: pointer; } ul.detail-songlist li a.disabled { color: var(--disable-song-title-color); } ul.detail-songlist li a span.source { border: solid 1px #ccc; border-radius: 4px; margin-right: 10px; display: inline-block; padding: 0 4px; color: #ccc; font-size: 12px; width: 24px; text-align: center; } ul.detail-songlist li a span.source.playlist { margin-left: 10px; margin-right: 0; } ul.detail-songlist li.head { height: 28px; color: var(--text-disable-color); border-top: none; padding-bottom: 2px; } ul.detail-songlist li.head:hover { background-color: transparent; } ul.detail-songlist li .title { flex: 2; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; line-height: 17px; max-height: 38px; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } ul.detail-songlist li.playlist-result .title { max-height: 80px; } ul.detail-songlist li.playlist-result .title a { display: flex; align-items: center; } ul.detail-songlist li.playlist-result .title img { height: 60px; width: 60px; display: block; margin-right: 10px; } ul.detail-songlist li .artist { flex: 1; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; line-height: 17px; max-height: 38px; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } ul.detail-songlist li .album { flex: 1; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; line-height: 17px; max-height: 38px; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } ul.detail-songlist li .tools { flex: 0 0 110px; display: flex; align-items: center; } ul.detail-songlist li .tools .icon { height: 16px; width: 16px; color: #9d9d9d; margin-top: 2px; margin-right: 10px; } /* page song detail end */ /* page login start */ .page .login { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: calc(100vh - 192px); } .page .login .login-logo { margin-bottom: 16px; display: flex; align-items: center; } .page .login .login-logo img { height: 64px; margin: 20px; } .page .login .login-title { font-size: 18px; margin-bottom: 10px; } .page .login .login-form .login-form_field { display: flex; align-items: center; height: 40px; margin: 24px; width: 270px; border: solid 1px var(--button-background-color); } .page .login .login-form .login-form_field input { background: var(--content-background-color); color: var(--text-default-color); } .page .login .login-form .login-form_field input.login-form_field_countrycode { flex: 0 0 40px; width: 40px; } .page .login .login-form .login-form_field svg { margin-left: 12px; margin-right: 12px; color: var(--icon-default-color); width: 18px; height: 18px; } .page .login .login-form .login-form_field input { border: none; flex: 1; font-size: 16px; } .page .login .login-submit_button { display: flex; align-items: center; justify-content: center; font-size: 14px; margin-top: 24px; padding: 8px; width: 270px; cursor: pointer; border: solid 1px var(--button-border-color); } .page .login .login-switcher { margin-top: 24px; cursor: pointer; } .page .login .login-notice { width: 270px; border-top: 1px solid var(--button-border-color); margin-top: 30px; padding-top: 12px; font-size: 12px; color: var(--text-subtitle-color); } .page .login .usercard { display: flex; align-items: center; width: 400px; border: solid 1px var(--button-border-color); margin-bottom: 20px; } .page .login .usercard img { width: 60px; height: 60px; margin: 10px; } .page .login .usercard .usercard-title { flex: 1; height: 50px; font-size: 16px; font-weight: 700; } .page .login .usercard .usercard-title .usercard-info { color: var(--link-inactive-color); font-size: 12px; } .page .login .usercard button { margin: 10px; } /* page login end */ /* page setting start */ .page .settings-title { border-bottom: solid 1px var(--line-default-color); padding-bottom: 10px; max-width: 800px; margin: 0 25px; font-size: 17px; margin-bottom: 10px; } .page .settings-title:first-of-type { margin-top: 20px; } .page .settings-content { margin: 0 25px 25px 25px; } .page .settings-content label.upload-button, .page .settings-content .language-button { padding: 5px; background: var(--button-background-color); margin-right: 4px; color: var(--text-default-color); cursor: pointer; } .page .settings-content label.upload-button:hover, .page .settings-content .language-button:hover { background: var(--button-hover-background-color); } .page .settings-content .shortcut { display: flex; margin-top: 10px; } .page .settings-content .shortcut svg { width: 18px; height: 18px; margin-right: 10px; } .page .searchbox .search-pagination { text-align: center; padding: 15px; } .page .settings-content .shortcut_table .shortcut_table-header, .page .settings-content .shortcut_table .shortcut_table-line { display: flex; color: var(--text-default-color); box-sizing: border-box; align-items: center; height: 40px; } .page .settings-content .shortcut_table .shortcut_table-header { color: var(--link-default-color); height: 30px; } .page .settings-content .shortcut_table .shortcut_table-function { flex: 0 140px; padding: 0 10px; box-sizing: border-box; } .page .settings-content .shortcut_table .shortcut_table-key { flex: 0 200px; margin-right: 20px; box-sizing: border-box; } .page .settings-content .shortcut_table .shortcut_table-globalkey { flex: 0 240px; box-sizing: border-box; } .page .settings-content .shortcut_table .shortcut_table-line .shortcut_table-key { border: solid 1px var(--button-border-color); border-radius: 5px; padding: 0 10px; height: 30px; display: flex; align-items: center; } .page .settings-content .shortcut_table .shortcut_table-line .shortcut_table-globalkey { border: solid 1px var(--button-border-color); border-radius: 5px; height: 30px; padding: 0 10px; display: flex; align-items: center; box-sizing: border-box; } .page .settings-content .custom-proxy { margin-top: 10px; } .page .settings-content .custom-proxy .rule-input { margin-top: 8px; } .page .settings-content .custom-proxy input { margin-right: 15px; height: 24px; width: 200px; } .page .settings-content .search-description { margin: 10px 0 5px 0; } .page .settings-content .search-source-list { display: flex; align-items: center; flex-wrap: wrap; line-height: 30px; } .page .settings-content .search-source-list .search-source { display: flex; align-items: center; width: 130px; } .page .settings-content .search-source-list .search-source svg { width: 18px; height: 18px; margin-right: 4px; } /* page setting end */ .loading_bottom { display: block; width: 40px; margin: 0 auto; } svg.searchspinner { width: 20px; height: 20px; vertical-align: top; margin-left: 15px; } /* footer start */ .footer { background: var(--foot-background-color); height: 60px; border-top: solid 1px var(--foot-border-color); display: flex; position: relative; } .footer .left-control { flex: 0 0 300px; display: flex; align-items: center; } .footer .left-control .icon { font-size: 22px; color: var(--player-left-icon-color); margin: 0 13px; } .footer .left-control .icon.play { margin-right: 10px; } .footer .left-control .icon:first-of-type { margin-left: 42px; } .footer .left-control .icon.play { color: var(--player-icon-color); } .footer .left-control .icon.play:hover { color: var(--player-icon-hover-color); } .footer .main-info { flex: 1; background: var(--footer-main-background-color); display: flex; overflow: hidden; z-index: 1; } .footer .main-info .logo-banner { text-align: center; flex: 1; display: flex; align-items: center; } .footer .main-info .logo-banner svg.logo { height: 48px; width: 48px; fill: #666666; stroke: #666666; margin: 0 auto; } .footer .main-info .cover { height: 60px; width: 60px; object-fit: cover; flex: 0 0 60px; cursor: pointer; position: relative; color: #ffffff; } .footer .main-info .cover img { height: 60px; width: 60px; object-fit: cover; } .footer .main-info .cover .mask { display: none; } .footer .main-info .cover:hover .mask { display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.6); position: absolute; top: 0; right: 0; left: 0; bottom: 0; } .footer .main-info .detail { flex: 1; position: relative; overflow: hidden; } .footer .main-info .detail .ctrl { position: absolute; right: 0px; top: 4px; padding-right: 6px; /* background: #eeeeee; */ } .footer .main-info .detail .ctrl:first-of-type .icon { margin-right: 5px; } .footer .main-info .detail .ctrl .icon { color: var(--text-default-color); opacity: 0.5; } .footer .main-info .detail .ctrl .icon:hover { opacity: 1; } .footer .main-info .detail .title { text-align: center; font-size: 14px; color: var(--text-default-color); min-width: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin: 3px 60px 0 60px; } .footer .main-info .detail .more-info { padding: 0 10px; display: flex; color: var(--text-subtitle-color); } .footer .main-info .detail .more-info .singer { flex: 1; text-align: center; font-size: 12px; min-width: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .footer .main-info .detail .more-info .singer a { cursor: pointer; } .footer .main-info .detail .more-info .current { width: 50px; font-size: 12px; } .footer .main-info .detail .more-info .total { width: 50px; text-align: right; font-size: 12px; } .footer .main-info .detail .playbar { width: 100%; } .footer .main-info .detail .playbar .playbar-clickable { padding: 8px 10px; } .footer .main-info .detail .playbar .barbg { height: 3px; background: var(--footer-player-bar-background-color); } .footer .main-info .detail .playbar .barbg .cur { height: 100%; background: var(--footer-player-bar-cur-background-color); position: relative; } .footer .main-info .detail .playbar .barbg .cur .btn { background: var(--footer-player-bar-cur-button-color); height: 8px; width: 2px; position: absolute; right: -2px; top: -5px; } .footer .main-info .detail .playbar .playbar-clickable:hover .barbg .cur .btn { width: 10px; height: 10px; border-radius: 5px; top: -3px; } .footer .menu-modal { left: 0; right: 0; top: 0; position: fixed; background: rgba(255, 255, 255, 0.2); } .footer .menu-modal.slideup { bottom: 60px; } .footer .menu { background: var(--foot-background-color); border: solid 1px var(--foot-border-color); border-radius: 3px; position: fixed; height: 370px; bottom: -311px; left: 300px; right: 300px; -webkit-app-region: no-drag; transition: all 0.3s; overflow: hidden; } .footer .menu.slideup { bottom: 60px; } .footer .menu .menu-header { height: 30px; border-bottom: solid 1px var(--footer-header-background-color); display: flex; align-items: center; color: #9e9e9e; font-size: 12px; } .footer .menu .menu-header .menu-title { flex: 1; padding: 20px; } .footer .menu .menu-header .add-all { border-right: solid 1px #e5e5e5; flex: 0 0 auto; display: flex; align-items: center; padding-right: 10px; } .footer .menu .menu-header .remove-all { margin-left: 10px; flex: 0 0 auto; display: flex; align-items: center; } .footer .menu .menu-header .close { margin-left: 10px; flex: 0 0 25px; align-items: center; cursor: pointer; } .footer .menu .menu-header .add-all span, .footer .menu .menu-header .remove-all span { cursor: pointer; } .footer .menu .menu-header .add-all .icon, .footer .menu .menu-header .remove-all .icon { margin-right: 7px; } .footer .menu .menu-header .close svg { margin-right: 3px; height: 16px; width: 16px; cursor: pointer; } .footer .menu ul.menu-list { overflow-y: scroll; height: 340px; font-size: 12px; } .footer .menu ul.menu-list li { display: flex; align-items: center; height: 30px; padding-right: 20px; position: relative; margin-bottom: -2px; border-top: solid 2px var(--songlist-border-color); border-bottom: solid 2px var(--songlist-border-color); } .footer .menu ul.menu-list li.even { background: var(--footer-menu-even-background-color); } .footer .menu ul.menu-list li:hover { background: var(--footer-menu-hover-background-color); } .footer .menu ul.menu-list li.playing { color: var(--important-color); } .footer .menu ul.menu-list li .song-status-icon { flex: 0 0 20px; width: 20px; height: 30px; text-align: center; display: flex; align-items: center; } .footer .menu ul.menu-list li .song-status-icon svg { width: 10px; height: 10px; fill: var(--important-color); stroke: var(--important-color); flex: 1; } .footer .menu ul.menu-list li .song-title { flex: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .footer .menu ul.menu-list li .song-title.disabled { color: #777777; } .footer .menu ul.menu-list li .song-title a { cursor: pointer; } .footer .menu ul.menu-list li .song-singer { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer; } /* div.visited{ color: green; } */ .footer .menu ul.menu-list li .tools { flex: 0 0 42px; width: 42px; } .footer .menu ul.menu-list li .tools .icon { color: #666666; cursor: pointer; opacity: 0.5; } .footer .menu ul.menu-list li .tools .icon:first-of-type { margin-right: 5px; } .footer .menu ul.menu-list li .tools .icon:hover { opacity: 1; } .footer .menu ul.menu-list li .song-time { flex: 1; text-align: right; } .footer .right-control { flex: 0 0 300px; background: var(--foot-background-color); display: flex; align-items: center; } .footer .right-control .playlist-toggle { margin-left: 29px; cursor: pointer; } .footer .right-control .playlist-toggle .icon { color: var(--player-right-icon-color); } .footer .right-control .playlist-toggle .icon:hover { color: var(--player-right-icon-hover-color); } .footer .right-control .lyric-toggle { margin-right: 30px; cursor: pointer; } .footer .right-control .lyric-toggle .lyric-icon, .footer .right-control .lyric-toggle .lyric-icon.selected:hover { border: solid 1px #7f7f7f; height: 16px; line-height: 16px; font-size: 14px; color: #7f7f7f; background-color: var(--lyric-icon-background-color); user-select: none; } .footer .right-control .lyric-toggle .lyric-icon.selected { border: solid 1px #7f7f7f; background-color: #7f7f7f; color: #fff; } .footer .right-control .volume-ctrl { flex: 1; display: flex; } .footer .right-control .volume-ctrl .icon { flex: 0 0 24px; color: var(--volume-icon-color); cursor: pointer; margin-left: 21px; } .footer .right-control .volume-ctrl .m-pbar { flex: 1; } .footer .right-control .volume-ctrl .barbg { height: 3px; background: var(--volume-bar-background-color); margin-top: 7px; width: 140px; } .footer .right-control .volume-ctrl .barbg .cur { height: 100%; background: var(--volume-bar-current-background-color); position: relative; } .footer .right-control .volume-ctrl .barbg .cur .btn { background: #ffffff; height: 13px; width: 13px; border: solid 1px #e4e4e4; border-radius: 13px; position: absolute; right: -13px; top: -6px; } /* footer end */ /* dialog start */ .shadow { position: fixed; background: rgba(30, 30, 30, 0.9); _position: absolute; z-index: 9999; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; background-image: url(data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==); } .dialog { position: absolute; top: 120px; width: 400px; height: 430px; z-index: 10000; overflow: hidden; border-radius: 4px; background-color: var(--dialog-background-color); color: var(--dialog-text-color); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); } .dialog-header { height: 30px; font-size: 15px; font-weight: bold; text-align: left; padding: 12px; } .dialog-header .dialog-close { float: right; font-size: 26px; cursor: pointer; margin-top: -10px; } .dialog-body { padding: 0 20px; height: 370px; overflow-y: auto; background-color: var(--dialog-background-color); } .dialog-body .buttons { display: flex; justify-content: center; margin-top: 20px; } .dialog-body .buttons .confirm-button { margin-right: 20px; } .dialog .detail-songlist li:hover { background-color: #e3e3e5; cursor: pointer; } .dialog-body input { width: 100%; } .dialog-playlist, .dialog-backuplist, .dialog-merge-playlist { padding-left: 0px; text-align: left; } .dialog-playlist li, .dialog-backuplist li, .dialog-merge-playlist li { cursor: pointer; height: 48px; padding: 6px; } .dialog-backuplist li { height: 96px; padding: 6px; } .dialog-playlist li:hover, .dialog-backuplist li:hover, .dialog-merge-playlist li:hover { background-color: var(--dialog-highlight-color); } .dialog-playlist li img, .dialog-backuplist li img, .dialog-merge-playlist li img { float: left; height: 48px; width: 48px; } .dialog-playlist li h2, .dialog-backuplist li h2, .dialog-merge-playlist li h2 { margin: 0 0 0 58px; font-size: 13px; font-weight: inherit; } .dialog-backuplist li h2 { margin-top: 0; } .dialog-newplaylist { padding: 10px; } .dialog-newbackup { text-align: center; } .dialog-editplaylist label, .dialog-open-url label { display: block; height: 30px; line-height: 30px; } .dialog-editplaylist .dialog-footer { position: absolute; bottom: 20px; } .dialog-body .field-name { margin: 10px 0 5px 0; } /* dialog end */ /* widget source-list start */ .source-list { margin: 20px 26px 10px 26px; } .source-list .source-button { display: inline-block; color: var(--link-inactive-color); cursor: pointer; padding-bottom: 4px; font-size: 14px; } .source-list .source-button.active, .source-list .source-button:hover { color: var(--link-active-color); border-bottom: solid 1px var(--link-active-color); } .source-list .splitter { display: inline-block; background: #a9a9a9; margin-top: 1px; height: 12px; width: 1px; margin: 0 10px; } .source-list .search-type { float: right; } /* widget source-list end */ /* widget playlist-filter start */ .playlist-filter { line-height: 38px; margin: 0 26px 10px 26px; } .playlist-filter .filter-item { line-height: 20px; padding: 5px 15px; margin-right: 10px; } .playlist-filter .filter-item.active { font-weight: 600; background: var(--button-hover-background-color); } /* widget playlist-filter end */ /* widget all-playlist-filter start */ .all-playlist-filter .category { margin-bottom: 10px; display: flex; } .all-playlist-filter .category .category-title { margin-left: 30px; margin-top: 12px; min-width: 50px; font-size: 18px; } .all-playlist-filter .category .category-filters { margin-left: 10px; display: flex; flex-wrap: wrap; } .all-playlist-filter .category .category-filters .filter-item { min-width: 80px; margin-top: 10px; display: flex; } .all-playlist-filter .category .category-filters .filter-item span { display: flex; justify-content: center; align-items: center; cursor: pointer; padding: 5px 10px; } .all-playlist-filter .category .category-filters .filter-item span:hover { background-color: var(--button-background-color); border-radius: var(--default-border-radius); } /* widget all-playlist-filter end */ ================================================ FILE: css/common2.css ================================================ html, body { margin: 0; padding: 0; font-size: var(--text-default-size); color: var(--text-default-color); font-family: system-ui, 'PingFang SC', STHeiti, sans-serif; } body .body{ background-color: var(--color-body-bg); transition: background 0.2s; border-radius: 8px; } a { cursor: pointer; color: inherit; text-decoration: none; } a:hover { text-decoration: underline; } .wrap { /* https://stackoverflow.com/questions/28897089/z-index-on-borders */ outline: solid 1px var(--windows-border-color); box-sizing: border-box; } /* remove focus highlight */ input:focus, select:focus, textarea:focus, button:focus { outline: none; } input:focus, textarea:focus { background-color: var(--theme-color-hover); color: var(--theme-color); } ul { list-style: none; margin: 0; padding: 0; } input, svg, .icon { -webkit-app-region: no-drag; } button { background-color: var(--button-background-color); color: var(--text-default-color); cursor: pointer; border: solid 1px var(--button-background-color); border-radius: var(--default-border-radius); padding: 5px; min-width: 80px; min-height: 32px; } button:hover { background-color: var(--button-hover-background-color); } img { -webkit-user-drag: none; object-fit: cover; } .l1-button { background-color: var(--button-background-color); color: var(--text-default-color); border-radius: var(--default-border-radius); padding: 5px; margin-right: 4px; color: var(--text-default-color); cursor: pointer; display: inline-block; } .l1-button:hover { background: var(--button-hover-background-color); color: var(--text-default-color); } svg { width: 24px; height: 24px; stroke: currentColor; stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; fill: none; cursor: pointer; /* stroke: var(--icon-default-color);*/ } .icon { /* default icon settings */ font-size: 16px; cursor: pointer; } /* tools utils */ .flex-scroll-wrapper { flex: 1; height: 100px; /* overflow-y: scroll; */ scrollbar-width: thin; scrollbar-color: var(--scroll-color) var(--content-background-color); } /* scroll bar style */ ::-webkit-scrollbar { width: 8px; background: transparent; transition: background 0.4s; } ::-webkit-scrollbar-thumb { transition: background 0.4s; width: 8px; border-radius: 4px; background: hsla(0, 0%, 50.2%, 0.38); /*rgba(151, 151, 151, 0.4);*/ /* -webkit-box-shadow: inset -1px -1px 0px rgba(0, 0, 0, 0.05), inset 1px 1px 0px rgba(0, 0, 0, 0.05);*/ } ::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active { background-color: hsla(0, 0%, 50.2%, 0.58); transition: background 0.4s; } ::-webkit-scrollbar-button { width: 0; height: 0; display: none; } ::-webkit-scrollbar-corner { background-color: transparent; } /* main framework start */ .wrap { display: flex; height: 100vh; flex-direction: column; margin: auto; } /* split screen to up/down 2 parts */ .main { flex: 1; display: flex; overflow: hidden; } /* split main to left/right 2 parts */ .main .sidebar { padding-left: 1vw; display: flex; flex-direction: column; } .main .content { flex: 1; display: flex; flex-direction: column; position: relative; } /* split content to up/down 2 parts */ .main .content .navigation { user-select: none; height: 64px; flex: 0 0 64px; display: flex; align-items: center; -webkit-app-region: drag; margin-right: 20px; position: absolute; top: 0; right: 0; left: 0; display: flex; justify-content: space-between; align-items: center; z-index: 100; -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px); background-color: var(--nav-background-color); transition: background 0.2s; } .main .content .browser { flex: 1; } /* main framework end */ /*****************************************************************/ /* main sidebar start */ .main .sidebar-content { height: calc(100vh - 210px); overflow: hidden; width: 60px; transition: 0.2s; background: var(--sidebar-background); border-radius: 10px; cursor: default; } .main .sidebar-content.footerdef { height: calc(100vh - 130px); } .main .sidebar-content.opensidebar { width: 200px; } .sidebar-content .logo-content { display: flex; align-items: center; justify-content: flex-start; margin: 10px; margin-bottom: 0; padding-bottom: 10px; border-bottom: 1px solid var(--sidebar-splitter); transition: 0.2s; cursor: pointer; } .sidebar-content .logo-content .logo-svg { padding: 10px; padding-right: 0; transition: 0.2s; } .sidebar-content .logo-content .logo-title { padding-right: 10px; display: flex; } .sidebar-content .logo-content .logo-title svg { color: var(--sidebar-splitter); opacity: 0; width: 0; transition: 0.2s; } .opensidebar > .logo-content { border-bottom: 1px solid transparent; } .opensidebar > .logo-content .logo-title svg { opacity: 1; width: 90px; } .sidebar .sidebar-scroll-content { overflow-x: hidden; overflow-y: overlay; height: calc(100% - 80px); } .sidebar .sidebar-scroll-content::-webkit-scrollbar { display: none; } .sidebar .sidebar-scroll-content::-webkit-scrollbar-thumb { background: var(--theme-color-ope); } .sidebar .opensidebar > .sidebar-scroll-content:hover::-webkit-scrollbar { display: block; width: 2px; } .sidebar .menu-control { height: 74px; -webkit-app-region: drag; } .sidebar .menu-title { height: 28px; line-height: 28px; margin: 8px 12px 8px 12px; color: var(--link-default-color); padding-left: 10px; display: flex; align-items: center; font-size: 12px; } .sidebar .menu-title .title { user-select: none; white-space: nowrap; opacity: 0; transition: 0.2s; width: 0; flex: 0; } .sidebar .menu-title .title.opensidebar { opacity: 1; flex: 1; width: auto; } .sidebar ul li .sidebar-block > div { display: flex; align-items: center; justify-content: center; margin-right: 10px; } .sidebar ul li .sidebar-block .sidebar .menu-title svg { flex: 0 0 18px; } .sidebar ul li { cursor: pointer; padding: 2px 10px; } .sidebar ul li .sidebar-block { display: flex; align-items: center; line-height: 28px; padding: 6px 12px; margin: 1px 0; transition: all 0.2s; color: var(--sidebar-hover-text-color); border-radius: var(--default-border-radius); background-color: var(--sidebar-button-background); } .sidebar ul li .sidebar-block.opensidebar { background-color: transparent; } .sidebar svg { width: 18px; height: 18px; z-index: 10; } .sidebar .icon { margin-right: 10px; font-size: 18px; } .sidebar ul li a { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .sidebar ul li:hover .sidebar-block { background: var(--theme-color-hover); color: var(--text-default-color); border-radius: 10px; transition: all 0.3s; } .sidebar ul li:hover .sidebar-block.opensidebar { background: var(--sidebar-button-background); } .sidebar ul li.active .sidebar-block, .sidebar ul li.active:hover .sidebar-block { background: var(--theme-color); color: #fff; border-radius: 10px; } .sidebar ul li.dragover .sidebar-block { background: var(--sidebar-highlight-background-color); color: var(--text-default-color); border-radius: 10px; } /* avoid hover effect trigger dragleave event https://stackoverflow.com/questions/19889615/can-an-angular-directive-pass-arguments-to-functions-in-expressions-specified-in */ .sidebar ul li * { pointer-events: none; } /* main sidebar end */ /* widget navigation start */ .navigation svg { width: 24px; height: 24px; } .navigation .icon svg { color: var(--text-default-color); } .navigation .backfront { flex: 0 0 45px; line-height: 46px; vertical-align: middle; padding: 0 13px; flex: 1; } .navigation .search { display: flex; width: 200px; height: 32px; background: var(--search-input-background-color); border-style: none; border-radius: var(--default-border-radius); padding-left: 10px; margin-right: 20px; align-items: center; } .navigation .settings { margin-right: 8px; } .navigation .settings.is-setting { display: flex; justify-content: center; align-items: center; padding: 8px; margin-right: 1vw; transition: 0.2s; } .navigation .settings.is-setting:hover { opacity: 1; background-color: var(--songlist-hover-background-color); border-radius: 25%; } .navigation .settings.is-setting:hover svg { color: var(--text-default-color); } .navigation .icon { color: var(--text-default-color); background-color: transparent; /* opacity: 0.5; */ display: flex; align-items: center; justify-content: center; padding: 8px; border-radius: 25%; transition: 0.2s; } .navigation .icon img { border-radius: 50%; width: 30px; height: 30px; } .navigation .icon:hover { opacity: 1; background-color: var(--songlist-hover-background-color); } .navigation .backfront .icon { display: inline-block; vertical-align: middle; } .navigation .backfront .icon:nth-of-type(1) { margin-right: 8px; } .navigation .search-input { width: 174px; background-color: transparent; border-style: none; font-size: 16px; font-weight: 600; color: var(--text-default-color); } .navigation .window-control { display: flex; } .navigation .window-control svg { margin-left: 8px; } .navigation .window-control svg:first-of-type { margin-left: 15px; } /* navigation end */ /* page hot-playlist start */ .playlist-covers, .detail-songlist.isSearch { padding-right: 2vw; padding-top: 30px; display: flex; flex-flow: row wrap; position: relative; margin: 0 14px; grid-template-columns: repeat(5, 1fr); gap: 40px 0px; } .playlist-covers { transition: padding 0.3s; } .playlist-covers li { color: var(--text-default-color); margin: 0 12px; } @media screen and (max-width:1000px){ .playlist-covers li { flex: 0 1 calc(25% - 26px); } } @media screen and (min-width:1000px) and (max-width:1480px){ .playlist-covers li { flex: 0 1 calc(20% - 26px); } } @media screen and (min-width:1480px){ .playlist-covers li { flex: 0 1 calc(16.66% - 26px); } } .playlist-covers .u-cover, ul.detail-songlist li .u-cover { display: flex; position: relative; justify-content: center; align-items: center; user-select: none; } .playlist-covers .u-cover img, ul.detail-songlist li .u-cover img { transition: all 0.1s ease-in-out 0s; box-sizing: border-box; width: 100%; border-radius: 0.75em; /* min-width: 136px; max-width: 100%; */ object-fit: cover; border: solid 1px rgba(0, 0, 0, 0.04); cursor: pointer; z-index: 1; } .playlist-covers .u-cover .bottom, .detail-head-cover .bottom, ul.detail-songlist li .u-cover .bottom { position: absolute; z-index: 2; cursor: pointer; opacity: 0; transition: all 0.2s ease 0s; display: flex; align-items: center; justify-content: center; color: rgb(255, 255, 255); backdrop-filter: blur(8px); background: hsla(0, 0%, 100%, 0.14); border: 1px solid hsla(0, 0%, 100%, 0.08); height: 22%; width: 22%; border-radius: 50%; } .playlist-covers .u-cover .covershadow, .detail-head-cover .covershadow, ul.detail-songlist li .u-cover .covershadow { transition: all 0.4s; opacity: 0; position: absolute; top: 12px; height: 100%; width: 100%; filter: blur(16px) opacity(0.6); transform: scale(0.92, 0.96); z-index: 0; background-size: cover; border-radius: 0.75em; } .playlist-covers .u-cover:hover img, ul.detail-songlist li .u-cover:hover img { margin-top: -10px; margin-bottom: 10px; padding-bottom: 0; } .playlist-covers .u-cover:hover .covershadow, ul.detail-songlist li .u-cover:hover .covershadow { display: block; opacity: 1; } .playlist-covers .u-cover:hover .bottom, .detail-head-cover:hover .bottom, ul.detail-songlist li .u-cover:hover .bottom { opacity: 1; } .playlist-covers .u-cover:hover .bottom:hover, .detail-head-cover:hover .bottom:hover, ul.detail-songlist li .u-cover .bottom:hover { background: hsla(0, 0%, 100%, 0.28); } .playlist-covers .u-cover .bottom svg, ul.detail-songlist li .u-cover .bottom svg { height: 30px; width: 30px; fill: rgba(200, 200, 200, 0.5); stroke-width: 1; stroke: #ffffff; } .playlist-covers .u-cover .bottom svg:hover, ul.detail-songlist li .u-cover .bottom svg:hover { fill: rgba(100, 100, 100, 0.5); } .playlist-covers .desc, ul.detail-songlist li .desc { cursor: default; padding-top: 8px; height: 65px; } .playlist-covers .desc .title, ul.detail-songlist li .desc div.title { word-break: break-all; font-size: 16px; font-weight: 600; line-height: 20px; word-break: break-all; display: flex; margin: 0 0 5px; z-index: 1; text-overflow: ellipsis; -webkit-line-clamp: 3; -webkit-box-orient: vertical; display: -webkit-box; text-decoration: none; overflow: hidden; } /* .playlist-covers .desc .title:hover, ul.detail-songlist li .desc div.title:hover{ text-decoration: underline; } */ /* page hot-playlist end */ /* page playlist-detail start */ .page .playlist-detail { padding-bottom: 37px; } .page .playlist-detail .detail-head { display: flex; margin-top: 11px; margin-bottom: 72px; } .page .playlist-detail .detail-head img { position: relative; z-index: 1; height: 100%; width: 100%; border-radius: 0.75em; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; aspect-ratio: 1/1; } .page .playlist-detail .detail-head .covershadow { transition: all 0.4s; opacity: 0; position: absolute; top: 12px; height: 100%; width: 100%; filter: blur(16px) opacity(0.6); transform: scale(0.92, 0.96); z-index: 0; background-size: cover; border-radius: 0.75em; } .page .playlist-detail .detail-head .detail-head-cover { position: relative; margin-left: 2vw; display: flex; justify-content: center; align-items: center; z-index: 1; height: 25vh; width: 25vh; } .page .playlist-detail .detail-head .detail-head-title { flex: 1; display: flex; flex-direction: column; justify-content: space-between; margin-left: 56px; margin-right: 2vw; } .page .playlist-detail .detail-head .detail-head-title h2 { font-size: 36px; font-weight: 700; margin-top: 10px; } .playlist-button-list { display: flex; flex-flow: row wrap; } .playlist-button-list .playlist-button { margin-top: 10px; height: 26px; cursor: pointer; display: flex; margin-right: 16px; border-radius: 8px; padding: 8px 16px; width: auto; background-color: var(--button-background-color); } .playlist-button-list .playlist-button.favorited { background-color: var(--theme-color-hover); color: var(--theme-color); } .playlist-button-list .playlist-button.playadd-button { flex: 0 0 136px; } .playlist-button-list .playlist-button .play-list { flex: 1; display: flex; align-items: center; font-size: 17px; line-height: 17px; font-weight: 500; user-select: none; } .playlist-button-list .playlist-button .play-list svg { margin-right: 4px; } .playlist-button-list .playlist-button.playadd-button .play-list svg { width: 14px; height: 14px; flex: 0 0 14px; margin-right: 4px; stroke: var(--important-color); fill: var(--important-color); } .playlist-button-list .playlist-button .play-list .icon { margin-right: 8px; } .playlist-button-list .playlist-button.playadd-button .play-list .icon { flex: 0 0 14px; margin-right: 4px; color: var(--important-color); transition: 0.2s; } .playlist-button-list .playlist-button.playadd-button .add-list { flex: 0 0 26px; height: 26px; width: 26px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: 0.2s; font-size: 17px; line-height: 17px; font-weight: 500; } .playlist-button-list .playlist-button.edit-button .play-list.favorited { color: var(--text-default-color); } .playlist-button-list .playlist-button.edit-button .play-list.notfavorite { color: var(--text-default-color); } .playlist-button-list .playlist-button:hover, .playlist-button-list .playlist-button.playadd-button .add-list:hover, .playlist-button-list .playlist-button.playadd-button .play-list:hover { transform: scale(1.1); transition: 0.2s; } .playlist-button-list .playlist-button.playadd-button .add-list svg { width: 14px; height: 14px; } .playlist-button-list .playlist-button.clone-button, .playlist-button-list .playlist-button.edit-button, .playlist-button-list .playlist-button.fav-button { flex: 0 0 auto; } .playlist-button-list .playlist-button.clone-button .play-list svg, .playlist-button-list .playlist-button.edit-button .play-list svg, .playlist-button-list .playlist-button.fav-button .play-list svg { width: 16px; height: 16px; flex: 0 0 16px; margin-right: 8px; stroke: rgb(102, 102, 102); } .playlist-button-list .playlist-button.fav-button .play-list.favorited svg { fill: rgb(102, 102, 102); } /* page playlist detail end */ /* page song detail start */ .songdetail-wrapper { position: absolute; top: 0; left: 0; right: 0; bottom: 100px; overflow: hidden; -webkit-app-region: no-drag; transition: all 0.5s; z-index: 100; opacity: 1; } .songdetail-wrapper .draggable-zone { position: absolute; left: 0; top: 0; right: 0; -webkit-app-region: drag; height: 80px; } .songdetail-wrapper.slidedown .draggable-zone { display: none; -webkit-app-region: no-drag; } .songdetail-wrapper.slidedown { top: calc(100% - 100px); /* opacity: 0; */ } .songdetail-wrapper .close { position: absolute; top: 24px; left: 24px; height: 19px; width: 19px; cursor: pointer; padding: 5px; border-radius: 50%; -webkit-app-region: no-drag; transition: 0.2s; z-index: 100; } .songdetail-wrapper .close:hover { background-color: var(--songlist-hover-background-color); } .songdetail-wrapper .close.mac { top: 44px; } .songdetail-wrapper .window-control { position: absolute; top: 24px; right: 24px; height: 24px; cursor: pointer; -webkit-app-region: no-drag; z-index: 99; } .songdetail-wrapper .window-control svg { margin-left: 8px; stroke: var(--now-playing-close-icon-color); } .songdetail-wrapper .close svg { stroke: var(--now-playing-close-icon-color); } .bgwrapper { overflow: hidden; border-radius: 10px; width: 100%; } .bg { opacity: 0.6; width: 100%; height: 100%; filter: blur(200px) contrast(75%) brightness(150%); float: left; background-repeat: no-repeat; background-position: center; background-size: cover; transition: background ease-in-out 1.5s; } .playsong-detail { position: absolute; top: 0; right: 0; left: 0; bottom: 0; display: flex; clip: rect(auto, auto, auto, auto); } .playsong-detail .detail-head { flex: 1; display: flex; justify-content: flex-end; margin-right: 32px; margin-top: 24px; align-items: center; transition: all 0.5s; z-index: 1; } .playsong-detail .detail-head-cover { position: relative; } .playsong-detail .detail-head img { border-radius: 10px; width: 54vh; height: 54vh; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -o-object-fit: cover; object-fit: cover; } .playsong-detail .detail-songinfo { flex: 1; font-weight: 600; color: var(--color-text); margin-right: 24px; z-index: 0; } .playsong-detail .detail-head-title { max-width: 54vh; margin-top: 24px; } .playsong-detail .title { display: flex; align-items: center; } .playsong-detail .title h2 { font-size: var(--h2-title-font-size); margin-top: 8px; margin-bottom: 0; font-weight: 600; opacity: 0.88; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; } .playsong-detail .title .badge { font-size: var(--badge-font-size); color: var(--theme-color); border: solid 1px var(--theme-color); border-radius: 5px; margin-left: 10px; padding-left: 4px; padding-right: 4px; margin-top: 4px; box-sizing: border-box; height: 20px; display: flex; align-items: center; justify-content: center; white-space: nowrap; } .playsong-detail .title .badge.platform { padding-top: 1px; } .playsong-detail .title .badge:first-of-type { margin-left: 15px; } .playsong-detail .info { margin-top: 4px; font-size: 16px; opacity: 0.58; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; } .playsong-detail .info .singer { display: inline; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .playsong-detail .info .album { display: inline; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .coverbg .playsong-detail .info span { color: var(--lyric-on-cover-color); } .playsong-detail .detail-songinfo .lyric { font-size: 16px; height: 100%; display: flex; flex-direction: column; padding-left: 78px; max-width: 460px; overflow-y: auto; transition: 0.5s; color: var(--lyric-default-color); } .playsong-detail .detail-songinfo .lyric::-webkit-scrollbar { display: none; } .coverbg .playsong-detail .detail-songinfo .lyric { color: var(--lyric-on-cover-color); } .playsong-detail .detail-songinfo .lyric .placeholder { margin-top: 50vh; } .playsong-detail .detail-songinfo .lyric p { padding: 18px; transition: all 0.2s; border-radius: 12px; margin: 0; opacity: 0.28; cursor: default; background: transparent; color: var(--text-default-color); } .playsong-detail .detail-songinfo .lyric p:hover { background: hsla(0, 0%, 100%, 0.08); opacity: 0.6; color: var(--text-default-color); } .playsong-detail .detail-songinfo .lyric p.translate { margin: 5px 0 0 0; } .playsong-detail .detail-songinfo .lyric p.hide { display: none; } .playsong-detail .detail-songinfo .lyric p.highlight { color: var(--text-default-color); opacity: 1; font-size: 26px; } .coverbg .playsong-detail .detail-songinfo .lyric p.highlight { color: var(--lyric-important-on-cover-color); opacity: 1; } ul.detail-songlist { position: relative; } .detail-songlist.playlist-songlist { margin: 0 2vw; padding-top: 13px; transition: 0.3s; } ul.detail-songlist.isSearchOne { padding: 0 25px; } ul.detail-songlist .playlist-search { position: absolute; right: 0; top: -50px; display: flex; width: 200px; height: 32px; background: var(--songlist-odd-background-color); border-style: none; border-radius: var(--default-border-radius); padding-left: 10px; margin-right: 40px; align-items: center; } ul.detail-songlist .playlist-search .playlist-search-icon { width: 14px; position: absolute; left: 7px; top: 1px; } ul.detail-songlist .playlist-search .playlist-clear-icon { width: 14px; position: absolute; left: 158px; } ul.detail-songlist .playlist-search .playlist-search-input { width: 174px; background-color: transparent; border-style: none; font-size: 16px; font-weight: 600; color: var(--text-default-color); } ul.detail-songlist .playlist-search:hover, ul.detail-songlist .playlist-search:active { background-color: var(--search-input-background-color); } ul.detail-songlist li.isSearchType { /* https://stackoverflow.com/questions/4157005/css-positioning-z-index-negative-margins */ position: relative; transition: all 0.3s; display: flex; align-items: center; padding: 8px; border-radius: 12px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } ul.detail-songlist li.isSearchGeDan { flex: 0 1 calc(20% - 26px); min-height: 156px; color: var(--text-default-color); margin: 0 12px; cursor: default; } ul.detail-songlist li.playlist-result.isSearchType { height: 80px; padding: 0 10px; } ul.detail-songlist li > img { object-fit: cover; border-radius: 8px; height: 46px; width: 46px; margin-right: 20px; border: 1px solid rgba(0, 0, 0, 0.04); cursor: pointer; } ul.detail-songlist li.isSearchType:hover { background-color: var(--songlist-hover-background-color); } ul.detail-songlist li.isSearchType.playing, ul.detail-songlist li.isSearchType.playing:hover { background-color: var(--theme-color-hover); color: var(--theme-color); } ul.detail-songlist li .title-and-artist { flex: 1; display: flex; } ul.detail-songlist li .container { display: flex; flex-direction: column; } ul.detail-songlist li a { cursor: pointer; } ul.detail-songlist li a.disabled { color: var(--disable-song-title-color); } ul.detail-songlist li a span.source, ul.detail-songlist span { border: solid 1px #ccc; border-radius: 4px; margin-right: 10px; display: inline-block; padding: 0 4px; color: #ccc; font-size: 12px; width: 24px; text-align: center; white-space: nowrap; height: min-content; } ul.detail-songlist li a span.source.playlist { margin-left: 10px; margin-right: 0; } ul.detail-songlist li .title { font-size: 18px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; word-break: break-all; } ul.detail-songlist li.playlist-result .title { max-height: 80px; } ul.detail-songlist li.playlist-result .title img { height: 60px; width: 60px; display: block; margin-right: 10px; } ul.detail-songlist li .artist { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; /* line-height: 17px; max-height: 38px; */ -webkit-line-clamp: 1; -webkit-box-orient: vertical; font-size: 13px; margin-top: 2px; font-size: 13px; opacity: 0.68; } ul.detail-songlist li .album { flex: 1; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; /* line-height: 17px; max-height: 38px; */ -webkit-line-clamp: 1; -webkit-box-orient: vertical; display: flex; font-size: 16px; opacity: 0.88; } ul.detail-songlist li .tools { flex: 0 0 110px; display: flex; align-items: center; } ul.detail-songlist li .tools a:hover { color: var(--text-default-color) !important; } ul.detail-songlist li .tools .icon { height: 16px; width: 16px; color: #9d9d9d; margin-top: 2px; margin-right: 10px; } /* page song detail end */ /* page login start */ .page .login { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: calc(100vh - 192px); } .page .login .login-logo { margin-bottom: 16px; display: flex; align-items: center; } .page .login .login-logo img { height: 64px; margin: 20px; } .page .login .login-title { font-size: 18px; margin-bottom: 10px; } .page .login .login-form .login-form_field { display: flex; align-items: center; height: 40px; margin: 24px; width: 270px; border: solid 1px var(--button-background-color); } .page .login .login-form .login-form_field input { background: var(--content-background-color); color: var(--text-default-color); } .page .login .login-form .login-form_field input.login-form_field_countrycode { flex: 0 0 40px; width: 40px; } .page .login .login-form .login-form_field svg { margin-left: 12px; margin-right: 12px; color: var(--icon-default-color); width: 18px; height: 18px; } .page .login .login-form .login-form_field input { border: none; flex: 1; font-size: 16px; } .page .login .login-form .login-form_field input:focus { background-color: var(--theme-color-hover); color: var(--theme-color); } .page .login .login-submit_button { display: flex; align-items: center; justify-content: center; font-size: 14px; margin-top: 24px; padding: 8px; width: 270px; cursor: pointer; border: solid 1px var(--button-border-color); } .page .login .login-switcher { margin-top: 24px; cursor: pointer; } .page .login .login-notice { width: 270px; border-top: 1px solid var(--button-border-color); margin-top: 30px; padding-top: 12px; font-size: 12px; color: var(--text-subtitle-color); } .page .login .usercard { padding: 20px; display: flex; align-items: center; width: 400px; background-color: var(--button-background-color); margin-bottom: 20px; border-radius: 10px; cursor: pointer; transition: all 0.3s; } .page .login .usercard .logoin-icon { width: 60px; height: 60px; margin: 10px; display: flex; justify-content: center; align-items: center; } .page .login .usercard .logoin-icon svg { width: 35px; height: 35px; } .page .login .usercard:hover, .page .login .usercard:active { background-color: var(--theme-color-hover); color: var(--theme-color); width: 500px; } .page .login .usercard:hover .usercard-info, .page .login .usercard:active .usercard-info { color: var(--theme-color); } .page .login .usercard img { border-radius: 10px; width: 60px; height: 60px; margin: 10px; } .page .login .usercard .usercard-title { margin-left: 10px; flex: 1; height: 50px; font-size: 18px; font-weight: 700; } .page .login .usercard .usercard-title .usercard-info { color: var(--text-subtitle-color); font-size: 12px; } .page .login .usercard button { margin: 10px; margin: 10px; font-size: 14px; font-weight: 600; padding: 8px 16px; transition: 0.2s; } .page .login .usercard button:hover { background-color: var(--theme-color-hover); color: var(--theme-color); } /* page login end */ /* page setting start */ .page .settings-title { max-width: 800px; margin: 0 auto; padding: 0 20px; font-weight: bold; padding-bottom: 12px; font-size: 26px; user-select: none; border-bottom: 1px solid rgba(128, 128, 128, 0.18); } .page .settings-content { max-width: 800px; margin: 25px auto; padding: 0 20px; font-size: 16px; font-weight: 500; opacity: 0.78; } .page .settings-content label.upload-button, .page .settings-content .language-button, .page .settings-content .theme-button, .page .settings-content button { background: var(--button-background-color); margin-right: 4px; color: var(--text-default-color); cursor: pointer; opacity: 1; font-weight: 600; border: none; padding: 8px 12px; border-radius: 8px; appearance: none; transition: all 0.2s; } .page .settings-content label.upload-button:hover, .page .settings-content .language-button:hover, .page .settings-content button:hover { transform: scale(1.1); background: var(--button-hover-background-color); } .page .settings-content .shortcut { display: flex; margin-bottom: 25px; } .page .settings-content .shortcut svg { width: 18px; height: 18px; margin-right: 10px; } .page .searchbox .search-pagination { text-align: center; padding: 32px; } .page .searchbox .search-pagination .btn-pagination { padding: 8px 16px; width: auto; height: 40px; min-width: 40px; font-size: 18px; line-height: 18px; font-weight: 600; transition: 0.2s all; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; opacity: 0.78; } .page .searchbox .search-pagination .ng-binding { font-size: 18px; line-height: 18px; font-weight: 600; padding: 0 20px; } .page .settings-content .shortcut_table .shortcut_table-header, .page .settings-content .shortcut_table .shortcut_table-line { display: flex; color: var(--text-default-color); box-sizing: border-box; align-items: center; height: 40px; } .page .settings-content .shortcut_table .shortcut_table-header { color: var(--link-default-color); height: 30px; } .page .settings-content .shortcut_table .shortcut_table-function { flex: 0 140px; padding: 0 10px; box-sizing: border-box; } .page .settings-content .shortcut_table .shortcut_table-key { flex: 0 200px; margin-right: 20px; box-sizing: border-box; } .page .settings-content .shortcut_table .shortcut_table-globalkey { flex: 0 240px; box-sizing: border-box; } .page .settings-content .shortcut_table .shortcut_table-line .shortcut_table-key { border: solid 1px var(--button-border-color); border-radius: 5px; padding: 0 10px; height: 30px; display: flex; align-items: center; } .page .settings-content .shortcut_table .shortcut_table-line .shortcut_table-globalkey { border: solid 1px var(--button-border-color); border-radius: 5px; height: 30px; padding: 0 10px; display: flex; align-items: center; box-sizing: border-box; } .page .settings-content .custom-proxy { margin-top: 10px; } .page .settings-content .custom-proxy .rule-input { margin-top: 8px; } .page .settings-content .custom-proxy input { margin-right: 15px; height: 24px; width: 200px; } .page .settings-content .custom-proxy input:focus { background-color: var(--theme-color-hover); color: var(--theme-color); } .page .settings-content .search-description, .page .settings-content p { margin: 0 0 25px 0; } .page .settings-content .search-source-list { display: flex; align-items: center; flex-wrap: wrap; line-height: 30px; } .page .settings-content .search-source-list .search-source { display: flex; align-items: center; width: 130px; } .page .settings-content .search-source-list .search-source svg { width: 18px; height: 18px; margin-right: 4px; } /* page setting end */ .loading_bottom { display: block; width: 40px; margin: 0 auto; } svg.searchspinner { width: 20px; height: 20px; vertical-align: top; margin-left: 15px; } /* footer start */ .footer { height: 100px; display: flex; align-items: flex-end; z-index: 130; margin: 1vh 1vw; border-radius: 10px; position: fixed; bottom: 0; width: 98vw; transition: 0.5s; color: var(--text-default-color); } .footer.footerdef { opacity: 0; bottom: -140px; transition: 0.5s; } .footer .footer-main { position: relative; z-index: 140; height: 100px; border-radius: 10px; display: flex; flex: 1; transition: 0.5s; backdrop-filter: saturate(180%) blur(20px); background-color: var(--nav-background-color); border: 1px solid rgba(255, 255, 255, 0.08); box-shadow: 0px 0px 16px rgb(0 0 0 / 10%); border-top: solid 1px var(--line-default-color); } .footer .footer-main.slidedown { height: calc(98vh - 2px); } .footer .footerwrap { width: 100%; display: flex; height: 100px; position: absolute; bottom: 0; } .footer .left-control { flex: 0 0 36%; display: flex; align-items: center; overflow: hidden; transition: 0.5s; opacity: 1; } .footer .left-control.slidedown { flex: 0 0 0; opacity: 0; transform: scaleX(0); } .footer .left-control .icon { display: flex; font-size: 22px; border-radius: 10px; padding: 7px; margin: 37px; transition: all 0.3s; background: transparent; } .footer .left-control .icon:hover { background-color: var(--songlist-hover-background-color); } .footer .left-control .icon.playlistactive { background-color: var(--theme-color-hover); color: var(--theme-color); } .footer .left-control .splitter { height: 20px; width: 1px; display: inline-block; background: #a9a9a9; } .footer .left-control .icon.play { margin-right: 10px; } .footer .left-control .icon.play { color: var(--player-icon-color); } .footer .left-control .icon.play:hover { color: var(--player-icon-hover-color); } .footer .main-info { flex: 1; display: flex; z-index: 1; justify-content: center; align-items: center; flex-direction: column; z-index: 110; } .footer .main-info .logo-banner { text-align: center; flex: 1; display: flex; align-items: center; } .footer .main-info .logo-banner svg.logo { height: 48px; width: 48px; fill: #666666; stroke: #666666; margin: 0 auto; } .footer .circlemark { display: flex; justify-content: center; width: 100px; height: 50px; position: absolute; top: 45px; z-index: -1; overflow: hidden; transform-origin: top center; } .rotatemark { animation: rotatemark 1s 1 forwards ease-in-out; } .circlmark { animation: circlmark 1s 1 forwards ease-in-out; } .rotatecircl { animation: rotatecircl 16s 0.5s infinite forwards linear; } .lipause { animation-play-state: paused; } .liplay { animation-play-state: running; } /* .circlopacity{ animation:circlopacity 1s forwards; } */ @keyframes rotatemark { 0% { transform-origin: top center; transform: rotate(0deg); } 50% { transform-origin: top center; transform: rotate(180deg); } 100% { transform-origin: top center; transform: rotate(360deg); } } @keyframes circlmark { 0% { transform: rotate(0deg); } 50% { transform: rotate(180deg); } 100% { transform: rotate(360deg); } } /* @keyframes circlopacity{ 0%{ opacity: 1; } 50%{ opacity: 0; } 100%{ opacity: 1; } } */ @keyframes rotatecircl { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .footer .cover { height: 90px; width: 90px; object-fit: cover; position: relative; color: transparent; top: -30px; display: flex; justify-content: center; } .footer .cover ul, .footer .cover .cover-list { width: 220px; height: 90px; position: absolute; } .footer .cover .cover-list span { bottom: 0; cursor: pointer; transition: 0.3s; color: var(--white--black); display: flex; justify-content: center; align-items: center; opacity: 0; } .footer .cover .cover-list span:hover, .footer .cover .cover-list span.show { opacity: 1; background-color: var(--white--black-background); } .footer .cover li { transition: all 0.3s; left: 0; bottom: 0; height: 45px; width: 30px; } .footer .cover li.hid { opacity: 0; z-index: -1; height: 45px; width: 32px; display: none; float: left; position: absolute; overflow: hidden; transition: all 0.3s; border-radius: 16px; left: 0; bottom: 0; } .footer .cover li.def { display: block; } .footer .cover ul .a, .footer .cover-list .a { height: 45px; width: 32px; left: 0; position: absolute; overflow: hidden; border-radius: 16px; opacity: 1; z-index: 100; display: block; } .footer .cover ul .b, .footer .cover-list .b { height: 90px; width: 90px; left: 65px; position: absolute; overflow: hidden; border-radius: 50%; opacity: 1; z-index: 101; display: block; } .footer .cover ul .c, .footer .cover-list .c { height: 45px; width: 32px; left: 190px; position: absolute; overflow: hidden; border-radius: 16px; opacity: 1; z-index: 99; display: block; } .footer .cover img { height: 100%; width: 100%; object-fit: cover; /* border-radius: 50%; */ /* position: absolute; */ /* border: 1px solid transparent; */ box-sizing: border-box; } .footer .cover .circle { width: 100px; height: 100px; position: relative; top: -50px; z-index: -1; overflow: hidden; transition: all 0.1s linear; } .footer .circlemark .topmark { width: 100px; height: 50px; /* position: absolute; */ z-index: -1; overflow: hidden; } .footer .cover .top { width: 96px; height: 96px; z-index: -1; border-radius: 50%; border: 2px solid; border-color: var(--text-default-color); } .footer .cover .bottom { width: 100px; height: 50px; /* position: absolute; */ overflow: hidden; } .footer .cover .bottom .bottomcircle { width: 96px; height: 96px; transform: translateY(-50px); z-index: -1; border-radius: 50%; border: 2px solid; border-color: var(--footer-player-bar-background-color); } .footer .footertime { padding-bottom: 15px; font-size: 12px; flex: 0; cursor: default; font-weight: 500; display: flex; justify-content: center; align-items: center; flex-direction: column; transition: 0.3s; width: 100%; max-width: 30vw; } .footer .footertime:hover { padding: 0; } .footer .footertime:hover .timeswitch { display: none; } .footer .footertime:hover .bottomprogressbar { display: flex; } .footer .left-control .detail { max-width: 356px; margin-left: 37px; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: center; } .footer .left-control .detail .ctrl { position: absolute; right: 0px; top: 4px; padding-right: 6px; } .footer .left-control .detail .ctrl:first-of-type .icon { margin-right: 5px; } .footer .left-control .detail .ctrl .icon { color: var(--text-default-color); opacity: 0.5; } .footer .left-control .detail .ctrl .icon:hover { opacity: 1; } .footer .left-control .detail .title { font-size: 14px; color: var(--text-default-color); min-width: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin: 5px 0; font-size: 18px; font-weight: 600; } .footer .left-control .detail .more-info { margin: 5px 0; display: flex; color: var(--text-subtitle-color); } .footer .left-control .detail .more-info .singer { flex: 1; font-size: 12px; min-width: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .footer .left-control .detail .more-info .singer a { cursor: pointer; } .footer .left-control .detail .more-info .current { width: 50px; font-size: 12px; } .footer .left-control .detail .more-info .total { width: 50px; text-align: right; font-size: 12px; } .footer .main-info .bottomprogressbar svg { cursor: default; } .footer .main-info .volume-ctrl .icon svg { cursor: pointer; } .footer .main-info .bottomprogressbar { justify-content: center; align-items: center; flex-wrap: nowrap; width: 100%; display: none; } .footer .main-info .playbar { display: flex; justify-content: center; align-items: center; width: 50%; } .footer .main-info .playbar .playbar-clickable { margin: 5px 10px 5px 0; padding: 5px 0; flex: 1; cursor: pointer; } .footer .main-info .barbg { height: 3px; background: var(--footer-player-bar-background-color); } .footer .main-info .barbg .cur { height: 100%; background: var(--footer-player-bar-cur-background-color); position: relative; } .footer .main-info .playbar .playbar-clickable:hover .cur, .footer .main-info .m-pbar:hover .barbg .cur { background: var(--theme-color); } .footer .main-info .barbg .cur .btn { background: var(--footer-player-bar-cur-button-color); height: 8px; width: 2px; position: absolute; right: -2px; top: -5px; transition: 0.3s; } .footer .main-info .playbar .playbar-clickable:hover .barbg .cur .btn, .footer .main-info .m-pbar:hover .barbg .cur .btn { width: 10px; height: 10px; border-radius: 5px; top: -3px; } .volume-ctrl { width: 50%; display: flex; justify-content: center; align-items: center; } .bottomprogressbar .icon { flex: 0 0 24px; color: var(--text-default-color); cursor: default; padding: 7px; display: flex; } .volume-ctrl .m-pbar { flex: 1; margin: 5px 0; padding: 5px 0; cursor: pointer; } .footer .menu-modal { border-radius: 10px; transition: 0.3s; left: 0; right: 0; top: 0; position: fixed; opacity: 0; background: var(--shadow-mask); } .footer .menu-modal.slideup { bottom: 0px; opacity: 1; transition: 0.3s; } .footer .menu { border-radius: 10px; position: absolute; z-index: 120; bottom: 120px; height: 0; opacity: 0; box-sizing: border-box; border: 1px solid var(--white--black); left: 0; -webkit-app-region: no-drag; transition: all 0.3s; overflow: hidden; width: 530px; -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px); background-color: var(--nav-background-color); box-shadow: 0px 0px 16px rgb(0 0 0 / 10%); padding-bottom: 20px; } .footer .menu.slideup { bottom: 125px; height: 500px; opacity: 1; box-sizing: border-box; border: 1px solid rgba(255, 255, 255, 0.08); } .footer .menu .menu-header { height: 30px; display: flex; align-items: center; color: #9e9e9e; padding: 30px; user-select: none; } .footer .menu .menu-header .menu-title { flex: 1; padding: 0 20px; font-size: 24px; font-weight: 600; color: var(--text-default-color); } .footer .menu .menu-header .add-all { border-right: solid 1px #e5e5e5; flex: 0 0 auto; display: flex; align-items: center; padding-right: 10px; font-size: 14px; } .footer .menu .menu-header .remove-all { margin-left: 10px; flex: 0 0 auto; display: flex; align-items: center; font-size: 14px; } .footer .menu .menu-header .remove-all:hover .ng-binding, .footer .menu .menu-header .add-all:hover .ng-binding { text-decoration: underline; color: var(--theme-color); } .footer .menu .menu-header .remove-all:hover, .footer .menu .menu-header .add-all:hover { text-decoration: none; color: var(--theme-color); } .footer .menu .menu-header .close { margin-left: 15px; flex: 0 0 25px; align-items: center; cursor: pointer; color: var(--icon-default-color); } .footer .menu .menu-header .close:hover { color: var(--theme-color); } .footer .menu .menu-header .add-all span, .footer .menu .menu-header .remove-all span { cursor: pointer; } .footer .menu .menu-header .add-all .icon, .footer .menu .menu-header .remove-all .icon { margin-right: 7px; width: 18px; height: 18px; } .footer .menu .menu-header .close svg { margin-right: 3px; width: 20px; height: 20px; display: flex; justify-content: center; align-items: center; } .footer .menu ul.menu-list { overflow-y: scroll; height: 370px; padding: 0 30px; font-size: 14px; } .footer .menu ul.menu-list li { border-radius: 10px; display: flex; align-items: center; height: 30px; position: relative; margin-bottom: -2px; padding: 10px 20px 10px 0; transition: 0.3s; } .footer .menu ul.menu-list li:hover { background: var(--songlist-hover-background-color); } .footer .menu ul.menu-list li.playing { color: var(--important-color); background: var(--songlist-hover-background-color); } .footer .menu ul.menu-list li .song-status-icon { flex: 0 0 30px; width: 20px; height: 30px; text-align: center; display: flex; align-items: center; } .footer .menu ul.menu-list li .song-status-icon svg { width: 10px; height: 10px; fill: var(--important-color); stroke: var(--important-color); flex: 1; } .footer .menu ul.menu-list li .song-title { flex: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 15px; font-weight: 400; padding-right: 10px; } .footer .menu ul.menu-list li .song-title.disabled { color: #777777; } .footer .menu ul.menu-list li .song-title a { cursor: pointer; } .footer .menu ul.menu-list li .song-singer { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer; padding: 0 10px; font-weight: 300; } .footer .menu ul.menu-list li .tools { flex: 0 0 42px; width: 42px; } .footer .menu ul.menu-list li .tools .icon { cursor: pointer; opacity: 0.5; } .footer .menu ul.menu-list li .tools .icon:first-of-type { margin-right: 5px; } .footer .menu ul.menu-list li .tools .icon:hover { opacity: 1; } .footer .menu ul.menu-list li .song-time { flex: 1; text-align: right; } .footer .right-control { flex: 0 0 36%; display: flex; align-items: center; justify-content: flex-end; } .footer .right-control .playlist-toggle { cursor: pointer; display: flex; justify-content: center; align-items: center; margin-right: 37px; padding: 7px; } .footer .right-control .ctrl { display: flex; justify-content: center; align-items: center; } .footer .right-control .ctrl a { margin-right: 32px; padding: 7px; display: flex; transition: 0.3s; border-radius: 10px; } .footer .right-control .ctrl a:hover { text-decoration: none; background-color: var(--songlist-hover-background-color); } .footer .right-control .translate-switch { border-radius: 10px; display: flex; cursor: pointer; -webkit-app-region: no-drag; height: 0px; box-sizing: border-box; width: 0px; transition: 0.3s; overflow: hidden; margin: 0; padding: 0; } .footer .right-control .translate-switch.slidedown { padding: 7px; margin-right: 32px; height: 35px; box-sizing: border-box; width: 32px; } .footer .right-control .translate-switch:hover { background-color: var(--songlist-hover-background-color); } .footer .right-control .translate-switch.selected { color: var(--theme-color); } .footer .right-control .mask { margin-right: 32px; padding: 7px; display: flex; transition: 0.3s; border-radius: 50%; } .footer .right-control .mask.slidedown { transform: rotate(180deg); } .footer .right-control .mask:hover { background-color: var(--songlist-hover-background-color); } .footer .right-control .playlist-toggle .icon { color: var(--player-right-icon-color); } .footer .right-control .playlist-toggle .icon:hover { color: var(--player-right-icon-hover-color); } .footer .right-control .lyric-toggle { margin-right: 32px; padding: 7px; display: flex; cursor: pointer; transition: 0.3s; border-radius: 10px; } .footer .right-control .lyric-toggle:hover { background-color: var(--songlist-hover-background-color); } /* footer end */ /* dialog start */ .shadow { position: fixed; background: var(--shadow-mask); _position: absolute; z-index: 9999; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; background-image: url(data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==); } .dialog { position: absolute; top: 120px; width: 400px; height: 430px; z-index: 10000; overflow: hidden; border-radius: 4px; background-color: var(--dialog-background-color); color: var(--dialog-text-color); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); } .dialog-header { height: 30px; font-size: 24px; font-weight: bold; text-align: center; padding: 20px; } .dialog-header .dialog-close { float: right; font-size: 26px; cursor: pointer; margin-top: -10px; } .dialog-body { padding: 0 20px; height: 370px; overflow-y: auto; background-color: var(--dialog-background-color); } .dialog-body .buttons { display: flex; justify-content: center; margin-top: 20px; } .dialog-body .buttons button, .dialog-body .dialog-footer button { margin-right: 20px; font-size: 16px; font-weight: 500; transition: 0.2s; } .dialog-body .buttons .btn-primary { background-color: var(--theme-color-hover); color: var(--theme-color); } .dialog-body .buttons button:hover, .dialog-body .dialog-footer button:hover { transform: scale(1.2); } .dialog .detail-songlist li:hover { background-color: #e3e3e5; cursor: pointer; } .dialog-body input { width: calc(100% - 20px); height: 30px; border-radius: 10px; background: #eee; color: #666; border: transparent; padding: 5px 10px; margin-bottom: 10px; font-weight: 500; } .dialog-body input:focus { background-color: var(--theme-color-hover); color: var(--theme-color); } .dialog-playlist, .dialog-backuplist, .dialog-merge-playlist { padding-left: 0px; text-align: left; } .dialog-playlist li, .dialog-backuplist li { cursor: pointer; height: 48px; padding: 6px; border-radius: 10px; } .dialog-merge-playlist li { cursor: pointer; height: 48px; padding: 10px; border-radius: 10px; transition: 0.2s; } .dialog-backuplist li { height: 96px; padding: 6px; } .dialog-playlist li:hover, .dialog-backuplist li:hover, .dialog-merge-playlist li:hover { background-color: var(--dialog-highlight-color); } .dialog-playlist li img, .dialog-backuplist li img, .dialog-merge-playlist li img { float: left; height: 48px; width: 48px; object-fit: cover; } .dialog-playlist li h2, .dialog-backuplist li h2, .dialog-merge-playlist li h2 { margin: 0 0 0 58px; font-size: 13px; font-weight: inherit; } .dialog-backuplist li h2 { margin-top: 0; } .dialog-newplaylist { padding: 10px; } .dialog-newbackup { text-align: center; } .dialog-editplaylist label, .dialog-open-url label { font-size: 18px; font-weight: 500; padding: 10px 0; display: block; height: 30px; line-height: 30px; } .dialog-editplaylist .dialog-footer { position: absolute; bottom: 20px; } .dialog-body .field-name { margin: 10px 0 5px 0; } /* dialog end */ /* widget source-list start */ .searchbox { margin-bottom: 150px; transition: 0.3s; } .searchbox.footerdef { margin-bottom: 0; } .source-list { margin: 10px 26px 24px 26px; user-select: none; display: flex; align-items: center; flex-wrap: nowrap; } .source-list .source-button { display: inline-block; cursor: pointer; transition: 0.1s; } .source-list .source-button:hover, .source-list .source-button.active { transition: 0.2s; padding: 0; } .source-list .source-button a:hover { text-decoration: none; } .source-list .source-button.active .buttontext, .source-list .source-button:hover .buttontext, .source-list .source-button .buttontext:hover, .source-list .source-button .buttontext.active { color: var(--text-default-color); -webkit-app-region: no-drag; font-size: 24px; font-weight: 700; text-decoration: none; border-radius: 10px; padding: 6px 10px; transition: all 0.2s, background 0.3s; -webkit-user-drag: none; margin: 0px 12px; white-space: nowrap; } .source-list .source-button.active .buttontext, .source-list .source-button .buttontext.active { border-bottom: solid 2px var(--text-default-color); } .source-list .source-button:hover .buttontext, .source-list .source-button .buttontext:hover { background-color: var(--button-background-color); } .source-list .source-button .buttontext { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; font-size: 14px; margin: 4px 10px; color: var(--text-subtitle-color); transition: 0.1s; } .source-list .splitter { display: inline-block; background: #a9a9a9; margin-top: 1px; height: 12px; width: 1px; } .source-list .search-type { float: right; flex: 1; display: flex; align-items: center; flex-wrap: nowrap; justify-content: flex-end; } /* widget source-list end */ /* widget playlist-filter start */ .playlist-filter { display: flex; flex-wrap: wrap; line-height: 38px; margin: 0 26px 0px 26px; } .playlist-filter .filter-item { display: flex; justify-content: center; align-items: center; line-height: 20px; padding: 8px 16px; margin: 10px 16px 6px 0; color: var(--black--white); font-weight: 600; font-size: 16px; border-radius: 10px; transition: all 0.2s; } .playlist-filter .filter-item.active, .playlist-filter .filter-item:hover { background: var(--theme-color-hover); color: var(--theme-color); transition: all 0.2s; } /* widget playlist-filter end */ /* widget all-playlist-filter start */ .all-playlist-filter { margin: 10px 26px 0px 26px; border-radius: 10px; padding: 8px; background-color: var(--button-background-color); } .all-playlist-filter .category { margin-bottom: 32px; display: flex; } .all-playlist-filter .category .category-title { font-size: 24px; font-weight: 700; opacity: 0.68; margin-left: 24px; min-width: 54px; height: 26px; margin-top: 8px; /* color: var(--icon-default-color); */ } .all-playlist-filter .category .category-filters { margin-left: 24px; display: flex; flex-wrap: wrap; /* color: var(--icon-default-color); */ } .all-playlist-filter .category .category-filters .filter-item { min-width: 98px; margin-top: 4px; display: flex; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; align-items: center; font-weight: 500; font-size: 16px; transition: 0.2s; } .all-playlist-filter .category .category-filters .filter-item span { display: flex; justify-content: center; align-items: center; cursor: pointer; padding: 6px 10px; height: 26px; border-radius: 12px; opacity: 0.88; transition: 0.2s; } .all-playlist-filter .category .category-filters .filter-item span:hover { opacity: 1; background-color: var(--theme-color-hover); border-radius: var(--default-border-radius); transition: 0.2s; color: var(--theme-color); } .all-playlist-filter .category .category-filters .filter-item span.active { color: var(--theme-color); } /* widget all-playlist-filter end */ ================================================ FILE: css/cover.css ================================================ /* * Globals */ /* Links */ a, a:focus, a:hover { color: #fff; } /* Custom default button */ .btn-default, .btn-default:hover, .btn-default:focus { color: #333; text-shadow: none; /* Prevent inheritence from `body` */ background-color: #fff; border: 1px solid #fff; } /* * Base structure */ html, body { height: 100%; background-color: #333; } body { color: #fff; overflow: hidden !important; /* text-align: center;*/ /*text-shadow: 0 1px 3px rgba(0,0,0,.5);*/ } /* Extra markup and styles for table-esque vertical and horizontal centering */ .site-wrapper { display: table; width: 100%; height: 100%; /* For at least Firefox */ min-height: 100%; /* -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); box-shadow: inset 0 0 100px rgba(0,0,0,.5);*/ } .site-wrapper-inner { display: table-cell; vertical-align: top; } .cover-container { margin-right: auto; margin-left: auto; } /* Padding for spacing */ .inner { padding: 30px; } /* * Header */ .masthead-brand { margin-top: 10px; margin-bottom: 10px; } .masthead-nav > li { display: inline-block; } .masthead-nav > li + li { margin-left: 20px; } .masthead-nav > li > a { padding-right: 0; padding-left: 0; font-size: 16px; font-weight: bold; color: #fff; /* IE8 proofing */ color: rgba(255, 255, 255, 0.75); border-bottom: 2px solid transparent; } .masthead-nav > li > a:hover, .masthead-nav > li > a:focus { background-color: transparent; border-bottom-color: #a9a9a9; border-bottom-color: rgba(255, 255, 255, 0.25); } .masthead-nav > .active > a, .masthead-nav > .active > a:hover, .masthead-nav > .active > a:focus { color: #fff; border-bottom-color: #fff; } @media (min-width: 768px) { .masthead-brand { float: left; } .masthead-nav { float: right; } } /* * Cover */ .cover { padding: 0 20px; } .cover .btn-lg { padding: 10px 20px; font-weight: bold; } /* * Footer */ .mastfoot { color: #999; /* IE8 proofing */ color: rgba(255, 255, 255, 0.5); } /* * Affix and center */ @media (min-width: 768px) { /* Pull out the header and footer */ .masthead { position: fixed; top: 0; } .mastfoot { position: fixed; bottom: 0; } /* Start the vertical centering */ .site-wrapper-inner { vertical-align: middle; } /* Handle the widths */ .masthead, .mastfoot, .cover-container { width: 100%; /* Must be percentage or pixels for horizontal alignment */ } } @media (min-width: 992px) { .masthead, .mastfoot, .cover-container { width: 880px; } } /* * Scroll bar * */ :root { --scrollWidth: 10px; } ::-webkit-scrollbar { width: var(--scrollWidth); height: var(--scrollWidth); } ::-webkit-scrollbar-button { width: 0; height: 0; } ::-webkit-scrollbar-button:start:increment, ::-webkit-scrollbar-button:end:decrement { display: none; } ::-webkit-scrollbar-corner { display: block; } ::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb { border-radius: 8px; border-right: 1px solid transparent; border-left: 1px solid transparent; } ::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.2); } ::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.3); } ::-webkit-scrollbar-track:hover { background-color: rgba(255, 255, 255, 0.08); } ================================================ FILE: css/hotkeys.css ================================================ /*! * angular-hotkeys v1.7.0 * https://chieffancypants.github.io/angular-hotkeys * Copyright (c) 2016 Wes Cruver * License: MIT */ .cfp-hotkeys-container { display: table !important; position: fixed; width: 100%; height: 100%; top: 0; left: 0; color: #333; font-size: 1em; background-color: rgba(255, 255, 255, 0.9); } .cfp-hotkeys-container.fade { z-index: -1024; visibility: hidden; opacity: 0; -webkit-transition: opacity 0.15s linear; -moz-transition: opacity 0.15s linear; -o-transition: opacity 0.15s linear; transition: opacity 0.15s linear; } .cfp-hotkeys-container.fade.in { z-index: 10002; visibility: visible; opacity: 1; } .cfp-hotkeys-title { font-weight: bold; text-align: center; font-size: 1.2em; } .cfp-hotkeys { width: 100%; height: 100%; display: table-cell; vertical-align: middle; } .cfp-hotkeys table { margin: auto; color: #333; } .cfp-content { display: table-cell; vertical-align: middle; } .cfp-hotkeys-keys { padding: 5px; text-align: right; } .cfp-hotkeys-key { display: inline-block; color: #fff; background-color: #333; border: 1px solid #333; border-radius: 5px; text-align: center; margin-right: 5px; box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb; padding: 5px 9px; font-size: 1em; } .cfp-hotkeys-text { padding-left: 10px; font-size: 1em; } .cfp-hotkeys-close { position: fixed; top: 20px; right: 20px; font-size: 2em; font-weight: bold; padding: 5px 10px; border: 1px solid #ddd; border-radius: 5px; min-height: 45px; min-width: 45px; text-align: center; } .cfp-hotkeys-close:hover { background-color: #fff; cursor: pointer; } @media all and (max-width: 500px) { .cfp-hotkeys { font-size: 0.8em; } } @media all and (min-width: 750px) { .cfp-hotkeys { font-size: 1.2em; } } ================================================ FILE: css/icon.css ================================================ @font-face { font-family: 'listen1-icon'; src: url('../fonts/listen1-icon.eot?4ftssm'); src: url('../fonts/listen1-icon.eot?4ftssm#iefix') format('embedded-opentype'), url('../fonts/listen1-icon.ttf?4ftssm') format('truetype'), url('../fonts/listen1-icon.woff?4ftssm') format('woff'), url('../fonts/listen1-icon.svg?4ftssm#listen1-icon') format('svg'); font-weight: normal; font-style: normal; } [class^='li-'], [class*=' li-'] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'listen1-icon' !important; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .li-play:before { content: '\e900'; } .li-previous:before { content: '\e901'; } .li-next:before { content: '\e902'; } .li-pause:before { content: '\e903'; } .li-random-loop:before { content: '\e904'; } .li-single-cycle:before { content: '\e905'; } .li-mute:before { content: '\e906'; } .li-volume:before { content: '\e907'; } .li-list:before { content: '\e908'; } .li-loop:before { content: '\e909'; } .li-del:before { content: '\e90a'; } .li-close:before { content: '\e90b'; } .li-back:before { content: '\e90c'; } .li-play-s:before { content: '\e90d'; } .li-collapse:before { content: '\e90e'; } .li-add:before { content: '\e90f'; } .li-advance:before { content: '\e910'; } .li-link:before { content: '\e911'; } .li-setting:before { content: '\e912'; } .li-songlist:before { content: '\e913'; } .li-featured-list:before { content: '\e914'; } ================================================ FILE: css/iparanoid.css ================================================ /* global settings (theme related) */ :root { --icon-default-color: #666666; --icon-highlight-color: #111111; --text-default-color: #111111; --text-subtitle-color: #666666; --text-disable-color: #999999; --lyric-default-color: #666666; --lyric-on-cover-color: #333333; --lyric-important-color: #ff4444; --lyric-important-on-cover-color: #ffffff; --link-default-color: #999999; --link-highlight-color: #111111; --link-active-color: #323232; --link-inactive-color: #a9a9a9; --line-default-color: #e5e5e5; --sidebar-background-color: rgba(245, 245, 245, 0.7); --sidebar-highlight-background-color: #4d4d4d; --sidebar-highlight-text-color: #ffffff; --sidebar-hover-background-color: #5f5f5f; --sidebar-hover-text-color: #ffffff; --content-background-color: #ffffff; --foot-background-color: #ffffff; --foot-border-color: #e5e5e5; --footer-main-background-color: #f2f2f2; --footer-player-bar-background-color: #e0e0e0; --footer-player-bar-cur-background-color: #666666; --footer-header-background-color: #e7e7e7; --footer-menu-even-background-color: #f7f7f7; --footer-menu-hover-background-color: #e5e5e5; --search-input-background-color: #f2f2f2; --footer-player-bar-cur-button-color: #111111; --window-control-border-color: #dddddd; --important-color: #ff4444; --button-background-color: #eeeeee; --button-border-color: #bebebe; --button-hover-background-color: #dddddd; --now-playing-page-background-color: #ffffff; --now-playing-close-icon-color: #666666; --disable-song-title-color: #b7b7b7; --windows-border-color: #dddddd; --default-border-radius: 2px; --text-default-size: 13px; --h2-title-font-size: 24px; --badge-font-size: 12px; --badge-font-color: #c75449; --badge-border-color: #d98e87; --songlist-odd-background-color: #f5f5f5; --songlist-border-color: #f4f4f4; --songlist-hover-background-color: #eeeeee; --player-left-icon-color: #b1b1b1; --player-icon-color: #666666; --player-icon-hover-color: #666666; --player-right-icon-color: #333333; --player-right-icon-hover-color: #000000; --dialog-highlight-color: #e3e3e5; --dialog-background-color: #fafafa; --dialog-text-color: #565656; --volume-icon-color: #333333; --volume-bar-background-color: #e0e0e0; --volume-bar-current-background-color: #333333; --scroll-color: #c2c2c2; --lyric-icon-background-color: #ffffff; --lyric-font-size: 15px; --lyric-line-margin: 20px; } ================================================ FILE: css/iparanoid2.css ================================================ /* global settings (theme related) */ :root { --icon-default-color: #000; --text-default-color: #000; --text-subtitle-color: #7a7a7b; /* --text-disable-color: #999999; */ --lyric-default-color: #666666; --lyric-on-cover-color: #333333; --lyric-important-on-cover-color: #ffffff; --link-default-color: #999999; --link-active-color: #323232; --line-default-color: #e5e5e5; --sidebar-highlight-background-color: #f2f2f3; --sidebar-hover-background-color: #f2f2f3; --sidebar-hover-text-color: #262626; --content-background-color: #ffffff; --footer-player-bar-cur-background-color: #666666; --search-input-background-color: #f2f2f3; --footer-player-bar-cur-button-color: #111111; --footer-player-bar-background-color: #e0e0e0; --window-control-border-color: #dddddd; --important-color: #017afe; --button-background-color: #f5f5f7; --button-border-color: #bebebe; --button-hover-background-color: #dddddd; --now-playing-close-icon-color: #666666; --disable-song-title-color: #b7b7b7; --windows-border-color: #dddddd; --default-border-radius: 10px; --text-default-size: 13px; --h2-title-font-size: 24px; --badge-font-size: 12px; --songlist-odd-background-color: #f5f5f5; --songlist-hover-background-color: #f2f2f7; --player-icon-color: #666666; --player-icon-hover-color: #666666; --player-right-icon-color: #333333; --player-right-icon-hover-color: #000000; --dialog-highlight-color: #e3e3e5; --dialog-background-color: #fafafa; --dialog-text-color: #565656; --volume-icon-color: #333333; --scroll-color: #c2c2c2; --lyric-icon-background-color: #ffffff; --footer-background-color: hsla(0, 0%, 100%, 0.86); --nav-background-color: hsla(0, 0%, 100%, 0.86); --color-body-bg: #fff; --white--black: #fff; --black--white: #222; --white--black-background: #222; --theme-color: #017afe; --theme-color-ope: rgba(1, 122, 254, 0.4); --theme-color-hover: #f0f7ff; --sidebar-background: #f2f2f7; --sidebar-button-background: #fff; --sidebar-splitter: #1d1d1f; --shadow-mask: rgba(30, 30, 30, 0.5); } ================================================ FILE: css/notyf_custom.css ================================================ .notyf__toast { border-radius: 10px; } .notyf__dismiss-btn { outline: 0; } ================================================ FILE: css/origin.css ================================================ /* global settings (theme related) */ :root { --icon-default-color: #666666; --icon-highlight-color: #111111; --text-default-color: #ffffff; --text-subtitle-color: #666666; --text-disable-color: #999999; --lyric-default-color: #666666; --lyric-on-cover-color: #bbbbbb; --lyric-important-color: #ffffff; --lyric-important-on-cover-color: #ffffff; --link-default-color: #999999; --link-highlight-color: #ffffff; --link-active-color: #ffffff; --link-inactive-color: rgba(255, 255, 255, 0.75); --line-default-color: #333333; --sidebar-background-color: #2e2e2e; --sidebar-highlight-background-color: #4d4d4d; --sidebar-highlight-text-color: #ffffff; --sidebar-hover-background-color: #3c3c3c; --sidebar-hover-text-color: #ffffff; --content-background-color: #333333; --foot-background-color: #222222; --foot-border-color: #222222; --footer-main-background-color: #222222; --footer-player-bar-background-color: #666666; --footer-player-bar-cur-background-color: #e0e0e0; --footer-header-background-color: #333333; --footer-menu-even-background-color: #2d2d2d; --footer-menu-hover-background-color: #333333; --search-input-background-color: #222222; --footer-player-bar-cur-button-color: #e0e0e0; --window-control-border-color: #dddddd; --important-color: #fff; --button-background-color: #222222; --button-border-color: #222222; --button-hover-background-color: #444444; --now-playing-page-background-color: #333333; --now-playing-close-icon-color: #b3b3b3; --disable-song-title-color: #b7b7b7; --windows-border-color: #333333; --default-border-radius: 2px; --text-default-size: 13px; --h2-title-font-size: 24px; --badge-font-size: 12px; --badge-font-color: #c3473a; --badge-border-color: #843932; --songlist-odd-background-color: #2d2d2d; --songlist-border-color: transparent; --songlist-hover-background-color: #3e3e3e; --player-left-icon-color: #b3b3b3; --player-icon-color: #b3b3b3; --player-icon-hover-color: #eeeeee; --player-right-icon-color: #b3b3b3; --player-right-icon-hover-color: #eeeeee; --player-icon-hightlight-color: #eeeeee; --dialog-highlight-color: #444444; --dialog-background-color: #333; --dialog-text-color: #ffffff; --volume-icon-color: #b3b3b3; --volume-bar-background-color: #333333; --volume-bar-current-background-color: #e0e0e0; --scroll-color: #444444; --lyric-icon-background-color: #222222; --lyric-font-size: 15px; --lyric-line-margin: 20px; } ================================================ FILE: css/origin2.css ================================================ /* global settings (theme related) */ :root { --icon-default-color: #666666; --text-default-color: #ffffff; --text-subtitle-color: #7a7a7b; --lyric-default-color: #666666; --lyric-on-cover-color: #bbbbbb; --lyric-important-on-cover-color: #ffffff; --link-default-color: #999999; --link-active-color: #ffffff; --line-default-color: rgba(255, 255, 255, 0.08); --sidebar-highlight-background-color: #4d4d4d; --sidebar-hover-background-color: #3c3c3c; --sidebar-hover-text-color: #ffffff; --content-background-color: #222; --footer-player-bar-background-color: #666666; --footer-player-bar-cur-background-color: #e0e0e0; --search-input-background-color: #323232; --footer-player-bar-cur-button-color: #e0e0e0; --window-control-border-color: #dddddd; --important-color: #017afe; --button-background-color: #323232; --button-border-color: #323232; --button-hover-background-color: #444444; --now-playing-close-icon-color: #b3b3b3; --disable-song-title-color: #b7b7b7; --windows-border-color: #222; --default-border-radius: 10px; --text-default-size: 13px; --h2-title-font-size: 24px; --badge-font-size: 12px; --songlist-odd-background-color: #2d2d2d; --songlist-hover-background-color: #3e3e3e; --player-icon-color: #b3b3b3; --player-icon-hover-color: #eeeeee; --player-right-icon-color: #b3b3b3; --player-right-icon-hover-color: #eeeeee; --dialog-highlight-color: #444444; --dialog-background-color: #222; --dialog-text-color: #ffffff; --volume-icon-color: #b3b3b3; --scroll-color: #444444; --lyric-icon-background-color: #323232; --nav-background-color: rgba(34, 34, 34, 0.86); --color-body-bg: #222; --white--black: #222; --black--white: #fff; --white--black-background: #fff; --theme-color: #017afe; --theme-color-ope: rgba(1, 122, 254, 0.4); --theme-color-hover: #bbcdff; --sidebar-background: #1D1D1F; --sidebar-button-background: #323232; --sidebar-splitter: #ebebec; --shadow-mask: rgba(0, 0, 0, 0.5); } ================================================ FILE: css/player.css ================================================ a { cursor: pointer; } .shadow { position: fixed; background: rgba(30, 30, 30, 0.5); _position: absolute; z-index: 9999; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; background-image: url(data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==); } .dialog { position: absolute; top: 120px; width: 480px; height: 420px; z-index: 10000; overflow: hidden; border-radius: 4px 4px 4px 4px; padding: 20px; background-color: #333; } .dialog-header { width: 100%; height: 30px; font-family: Arial, Helvetica, sans-serif; font-size: 15px; font-weight: bold; text-align: left; border-bottom: 1px solid; margin-bottom: 20px; } .dialog-header .dialog-close { float: right; font-size: 33px; cursor: pointer; margin-top: -15px; } .dialog-body { width: 100%; height: 320px; overflow-y: auto; background-color: #333; } /*.masthead { z-index: 999; }*/ .masthead, .mastfoot { margin: 0 auto; left: 0; right: 0; z-index: 999; background-color: #222222; } .masthead { background-color: #333; height: 90px; } .masthead .logo { float: left; height: 50px; width: 50px; margin-right: 20px; cursor: pointer; } .masthead .masthead-brand { color: rgba(255, 255, 255, 1); cursor: pointer; } .cover-container { position: relative; } .container-placeholder { margin-top: 90px; } .site-wrapper { width: 100%; overflow: hidden; position: absolute; padding-left: 17px; } .site-wrapper-innerd { overflow-y: scroll; margin-top: 90px; /* uncomment the line below will hide the scroll bar */ /*padding-right: 17px;*/ box-sizing: content-box; width: 100%; background-color: #333; } .searchbox { /* margin-top: 100px;*/ } .searchbox .nav { margin-top: 12px; } .searchbox .nav .searchspinner { margin-top: 8px; height: 25px; } .searchitem { height: 92px; } .searchitem img { float: left; height: 90px; width: 90px; } .searchitem div { float: left; margin-left: 48px; margin-top: 38px; width: 400px; } .playlist-covers { margin-bottom: 0px; } .playlist-covers li { float: left; display: inline-block; width: 140px; height: 188px; margin-right: 22px; } .playlist-covers .desc { text-align: left; } .playlist-covers .u-cover { position: relative; display: block; width: 140px; height: 140px; } .playlist-covers .u-cover .bottom { position: absolute; bottom: 0; left: 0; width: 100%; height: 27px; color: #ccc; } .playlist-covers .loading_bottom { clear: both; text-align: center; } .playlist-covers .loading_bottom img { height: 35px; } .u-cover .mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .u-cover img { display: block; width: 100%; height: 100%; } .u-cover .icon-play { position: absolute; right: 10px; bottom: 5px; width: 16px; height: 17px; background: url(../images/player_directplay.png); } .playlist-covers .u-cover .bottom .nb { float: left; margin: 7px 0 0 0; } .m-playbar { position: absolute; zoom: 1; top: -90px; left: 0; width: 100%; height: 90px; margin: 0 auto; background-color: #222222; } .m-playbar .btns { width: 157px; padding: 27px 0 0 19px; } .m-playbar .btns, .m-playbar .head, .m-playbar .play, .m-playbar .volum, .m-playbar .oper { float: left; } .m-pbar .btn i { visibility: hidden; position: absolute; left: 5px; top: 5px; width: 12px; height: 12px; background: url(../images/loading.gif); } .m-pbar .barbg, .m-pbar .cur, .m-pbar .left { background: url(../images/statbar.png) no-repeat 0 9999px; _background-image: url(../images/statbar.png); } .m-playbar .btns a { background: url(../images/player_large.png) no-repeat 0 9999px; _background-image: url(../images/player_large.png); cursor: pointer; } .m-playbar .btns a { display: block; float: left; width: 36px; height: 36px; margin-right: 8px; margin-top: 0px; text-indent: -9999px; } .m-playbar .btns .previous { background-position: -72px 0px; } .m-playbar .btns .previous:hover { background-position: -72px -36px; } .m-playbar .btns .play { width: 36px; height: 36px; margin-top: 0; background-position: 0px 0px; } .m-playbar .btns .play:hover { background-position: 0px -36px; } .m-playbar .btns .pas { background-position: -108px 0px; } .m-playbar .btns .pas:hover { background-position: -108px -36px; } .m-playbar .btns .next { /* pause icon distance adjust from 36 to 38 */ background-position: -38px 0px; } .m-playbar .btns .next:hover { background-position: -38px -36px; } .m-playbar .head { position: relative; margin: 10px 15px 0 0; } .m-playbar .head, .m-playbar .head img { width: 70px; height: 70px; } .m-playbar .head .mask { position: absolute; top: 0px; left: 0px; display: block; width: 70px; height: 70px; } .m-playbar .maininfo { float: none; margin-left: 245px; margin-right: 120px; } .m-playbar .words .notextdeco { text-decoration: none; } .m-playbar .words { margin-top: 14px; height: 28px; overflow: hidden; color: #e8e8e8; text-shadow: 0 1px 0 #171717; line-height: 28px; } .m-playbar .words .name { max-width: 300px; } .m-playbar .words .by { max-width: 220px; margin-left: 15px; color: #9b9b9b; } .m-playbar .words .by a { color: #9b9b9b; } .m-playbar .words .src { cursor: pointer; float: left; width: 25px; height: 25px; margin: 2px 0 0 13px; background: url(../images/player_small.png) no-repeat 0 9999px; background-position: -100px 0px; } .m-playbar .words .src:hover { background-position: -100px -25px; } .m-playbar .words .fc1 { color: #e8e8e8; margin-left: 3px; } .overflowhide { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .floatleft { float: left; } .m-pbar { position: relative; } .m-pbar.play { width: 80%; margin-top: 14px; } .m-pbar.volume { position: absolute; right: 13px; bottom: 11px; float: right; width: 56%; margin-top: 0px; } .m-pbar .barbg, .m-pbar .cur, .m-pbar .rdy { height: 7px; } .m-pbar .barbg { background-position: right 0; } .m-pbar .cur { position: absolute; top: 0; left: 0; width: 1%; background-position: left -9px; } .m-pbar .btn { position: absolute; top: -8px; right: -13px; width: 22px; height: 24px; margin-left: -11px; background: url(../images/progress_indicator.png) no-repeat; } .m-playbar .time { position: absolute; right: -122px; top: -6px; } .m-playbar .time { float: right; margin-right: 20px; color: #797979; text-shadow: 0 1px 0 #121212; } .m-playbar .time em { color: #a1a1a1; } em, i { font-style: normal; text-align: left; font-size: inherit; } .m-playbar a { background: url(../images/player_small.png) no-repeat 0 9999px; } .m-playbar .ctrl { position: absolute; right: 0px; bottom: 48px; z-index: 10; width: 103px; padding-left: 13px; float: none; } .m-playbar .icn-add { background-position: -25px 0px; } .m-playbar .icn-add:hover { background-position: -25px -25px; } .m-playbar .icn-list { background-position: -125px 0px; } .m-playbar .icn-vol { background-position: -175px 0px; } .m-playbar .icn-vol-mute { background-position: -200px 0px; } .m-playbar .icn-list:hover { background-position: -125px -25px; } .m-playbar .icn-loop { background-position: -50px 0px; } .m-playbar .icn-loop:hover { background-position: -50px -25px; } .m-playbar .icn-vol:hover { background-position: -175px -25px; } .m-playbar .icn-vol-mute:hover { background-position: -200px -25px; } .m-playbar .icn-shuffle { background-position: -150px 0px; } .m-playbar .icn-shuffle:hover { background-position: -150px -25px; } .m-playbar .icn-repeatone { background-position: -225px 0px; } .m-playbar .icn-repeatone:hover { background-position: -225px -25px; } .m-playbar .icn { float: left; width: 25px; height: 25px; margin: 11px 2px 0 0; text-indent: -9999px; } .m-playbar .icn-add { margin-right: 5px; } .m-playbar .menu { position: absolute; bottom: 90px; _bottom: 90px; right: 0px; _right: 0px; height: 349px; width: 60%; background-color: #121212; } .m-playbar .menu ul { display: inline-block; padding-left: 0px; height: 308px; overflow-y: scroll; margin-bottom: 0px; } .m-playbar .menu li { float: left; width: 100%; display: block; } .m-playbar .menu .lyric { text-align: center; width: 39%; display: inline-block; height: 308px; overflow-y: scroll; position: relative; } .m-playbar .menu .lyric p { min-height: 20px; } .m-playbar .menu .lyric .placeholder { height: 50px; } .m-playbar .menu .lyric .highlight { font-size: 15px; color: #ffffff; } .m-playbar .menu .playing { background-color: #555555; } .m-playbar .menu li:hover, .m-playbar .menu li:focus { background-color: #999999; } .m-playbar .menu .icn-remove { height: 20px; width: 20px; background-position: -75px -25px; display: inline-block; } .m-playbar .menu .icn-remove:hover { background-position: -75px -25px; } .volume-ctrl { position: absolute; right: 0px; bottom: 16px; width: 110px; } li { list-style: none; } .menu-header { height: 40px; background-color: #222222; padding-top: 4px; text-align: center; } .menu-header span { position: absolute; left: 19px; top: 7px; font-size: 18px; color: #ffffff; } .menu-header small { background-color: #333333; color: #ffffff; cursor: pointer; vertical-align: middle; display: inline-block; width: 60px; line-height: 20px; } .menu-header a:hover small { background-color: #ffffff; color: #333333; } .menu .add-all { display: inline-block; position: absolute; left: 335px; top: 9px; } .menu .remove-all { display: inline-block; position: absolute; left: 410px; top: 9px; } .menu .close-popup { float: right; margin-right: 14px; font-size: 20px; text-decoration: none; color: #aaaaaa; } .menu .close-popup:hover { color: #ffffff; } .menu .title { width: 300px; float: left; height: 28px; padding-top: 3px; text-align: left; padding-left: 20px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer; } .menu .singer { width: 180px; float: right; height: 28px; padding-top: 3px; text-align: left; padding-left: 20px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer; } .dbimport { /*margin-top: 100px;*/ } .form-signin { width: 300px; margin-left: auto; margin-right: auto; text-align: center; } .form-signin .form-control, .form-signin .valid-img, .form-signin .btn { margin-top: 10px; } .form-signin .valid-img { height: 40px; width: 220px; } .form-signin .security-notice { margin-top: 10px; } .playlist-detail { position: absolute; text-align: left; background-color: #333; width: 100%; } .playlist-detail .detail-head { width: 200px; position: fixed; margin-bottom: 20px; } .playlist-detail .detail-head-cover { height: 180px; /* width: 225px;*/ float: left; margin: 10px; } .playlist-detail .detail-head-cover img { max-width: 100%; max-height: 100%; } .detail-head-title { float: left; width: 100%; text-align: center; } .detail-head-title a { display: inline-block; text-indent: -9999px; width: 36px; height: 36px; margin-top: 0; background: url(../images/player_large.png) no-repeat 0 9999px; } .detail-head-title .play { background-position: 0px 0px; } .detail-head-title .play:hover { background-position: 0px -36px; } .detail-head-title .add { background-position: -216px 0px; } .detail-head-title .add:hover { background-position: -216px -36px; } .detail-head-title .link { background-position: -250px 0px; } .detail-head-title .link:hover { background-position: -250px -36px; } .detail-head-title .edit { background-position: -288px 0px; } .detail-head-title .edit:hover { background-position: -288px -36px; } .detail-head-title .clone { background-position: -144px 0px; } .detail-head-title .clone:hover { background-position: -144px -36px; } .detail-head-title .merge { background-position: -324px 0px; } .detail-head-title .merge:hover { background-position: -324px -36px; } .detail-head-title .ply:hover { background-position: -40px -204px; } .playlist-detail .detail-head-title h2 { font-size: 17px; margin-bottom: 35px; } .playlist-detail .detail-songlist { margin-left: 220px; margin-top: 6px; margin-right: 14px; } .playsong-detail .detail-head { width: 390px; position: fixed; margin-bottom: 20px; } .playsong-detail .detail-songinfo { padding-left: 440px; padding-right: 55px; } .playsong-detail .detail-head .detail-head-cover { margin: 0 auto; width: 200px; } .playsong-detail .detail-head .detail-head-cover img { width: 240px; height: 240px; } .playsong-detail .detail-songinfo h2 { font-size: 22px; } .playsong-detail .detail-songinfo .info { border-bottom: solid #444 1px; margin-bottom: 5px; padding-bottom: 10px; } .playsong-detail .detail-songinfo .info span { color: #9b9b9b; margin-right: 10px; } .playsong-detail .detail-songinfo .info span.album { margin-left: 30px; } .playsong-detail .lyric { color: #999; /* IE8 proofing */ color: rgba(255, 255, 255, 0.5); text-align: left; width: 100%; display: inline-block; height: 410px; overflow-y: scroll; position: relative; font-size: 15px; } .playsong-detail .lyric p { min-height: 20px; } .playsong-detail .lyric .placeholder { height: 50px; } .playsong-detail .lyric .highlight { color: #ffffff; } .detail-songlist { padding-left: 0px; text-align: left; } .detail-songlist li { float: left; width: 100%; display: block; padding: 10px; } .detail-songlist .col2 { float: left; width: 28%; margin-left: 2%; font-size: 15px; } .detail-songlist .col1 { float: left; width: 19%; margin-left: 2%; } .detail-songlist .disabled { color: #777777; } .detail-songlist .col-add { float: left; width: 75px; margin-left: 2%; } .detail-songlist .detail-tools { float: right; height: 21px; position: relative; width: 118px; } .detail-songlist .detail-tools a { background: url(../images/player_small.png) no-repeat 0 9999px; height: 25px; width: 25px; cursor: pointer; } .detail-songlist .detail-tools .detail-add-button { background-position: 0px 0px; } .detail-songlist .detail-tools .detail-fav-button { background-position: -25px 0px; } .detail-songlist .detail-tools .detail-delete-button { background-position: -75px 0px; } .detail-songlist .detail-tools .source-button { background-position: -100px 0px; } .detail-songlist .detail-tools .detail-add-button:hover { background-position: 0px -25px; } .detail-songlist .detail-tools .detail-fav-button:hover { background-position: -25px -25px; } .detail-songlist .detail-tools .detail-delete-button:hover { background-position: -75px -25px; } .detail-songlist .detail-tools .source-button:hover { background-position: -100px -25px; } .detail-songlist .detail-tools a { text-decoration: none; display: inline-block; } .detail-songlist .detail-artist a { color: #777777; } .detail-songlist .odd { background-color: #333; } .detail-songlist .even, .detail-songlist .detail-add { background-color: #2d2d2d; } .dialog .detail-songlist li:hover { background-color: #999999; cursor: pointer; } /*.playlist-detail .detail-songlist li:hover { background-color: #999999; }*/ .playlist-detail .btn { width: 88px; margin-top: 0; float: left; } .cover-container .detail-close { position: absolute; right: -32px; top: 0px; } .cover-container .detail-close span { font-size: 34px; cursor: pointer; color: #aaaaaa; } .cover-container .detail-close span:hover { color: #ffffff; } .dialog-playlist { padding-left: 0px; text-align: left; } .dialog-playlist li { cursor: pointer; height: 112px; padding: 6px; } .dialog-playlist li:hover { background-color: #555555; } .dialog-playlist li img { float: left; height: 100px; width: 100px; } .dialog-playlist li h2 { margin-left: 125px; font-size: 17px; } .dialog-backuplist { padding-left: 0px; text-align: left; } .dialog-backuplist li { cursor: pointer; height: 112px; padding: 6px; } .dialog-backuplist li:hover { background-color: #555555; } .dialog-backuplist li img { float: left; height: 100px; width: 100px; } .dialog-backuplist li h2 { margin-top: 10px; margin-left: 125px; font-size: 15px; } .dialog-merge-playlist { padding-left: 0px; text-align: left; } .dialog-merge-playlist li { cursor: pointer; height: 112px; padding: 6px; } .dialog-merge-playlist li:hover { background-color: #555555; } .dialog-merge-playlist li img { float: left; height: 100px; width: 100px; } .dialog-merge-playlist li h2 { margin-left: 125px; font-size: 17px; } .dialog-newplaylist input { margin-bottom: 22px; } .dialog-newplaylist .confirm-button { margin-left: 76px; margin-right: 96px; } .dialog-newbackup { text-align: center; } .dialog-newbackup .confirm-button { margin-right: 12px; } .dialog-editplaylist .dialog-footer { position: absolute; bottom: 20px; } .dialog-editplaylist .confirm-button, .dialog-open-url .confirm-button { margin-right: 82px; margin-left: 93px; } .dialog-connect-lastfm .buttons { margin-top: 30px; } .dialog-connect-lastfm .confirm-button { margin-left: 40px; margin-right: 48px; } .source-list { position: absolute; right: -32px; top: 0px; z-index: 10; text-align: center; } .source-list div { background-color: #333333; color: #ffffff; height: 35px; border: 1px solid #ffffff; width: 75px; cursor: pointer; vertical-align: middle; padding-top: 6px; } .source-list div:first-child:not(:last-child) { border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .source-list div:last-child:not(:first-child) { border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } .source-list .active { background-color: #e6e6e6; color: #333333; } .source-list div:hover { background-color: #ffffff; color: #333333; } .source-list .open-url-button { border-radius: 4px; } .settings-title { font-size: 20px; padding: 20px; border-bottom: 2px solid #aaaaaa; } .settings-content { padding: 20px; } .settings-content .btn { margin-right: 10px; } .btn-group button, .btn-pagination, .btn-pagination:focus { background-color: #333333; color: #ffffff; border-color: #333333; } .btn-group button:hover, .btn-pagination:hover { background-color: #ffffff; color: #333333; } .searchbox li > a:hover { color: #333333; } .search-pagination { text-align: center; display: block; vertical-align: middle; line-height: 45px; } .search-pagination button:focus { outline: 0; } .search-pagination label { margin: 0 15px; } ================================================ FILE: css/reset.css ================================================ html, body, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend { margin: 0; padding: 0; border: 0; outline: 0; font-weight: normal; font-style: normal; font-size: 100%; vertical-align: baseline; } :focus { outline: 0; } body { line-height: 1.2; color: black; background: white; } h1, h2, h3, h4, h5, h6 { font-size: 100%; font-weight: normal; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ''; } blockquote, q { quotes: '' ''; } img { border: none; } ================================================ FILE: i18n/en-US.json ================================================ { "HELLO" : "hi", "_ALL_MUSIC": "All Music", "_NETEASE_MUSIC": "Netease", "_QQ_MUSIC": "QQ", "_XIAMI_MUSIC": "Xiami", "_KUGOU_MUSIC": "Kugou", "_KUWO_MUSIC": "Kuwo", "_BILIBILI_MUSIC": "Bilibili", "_MIGU_MUSIC": "Migu", "_TAIHE_MUSIC": "Qianqian", "_PLATFORM_UNION": "Platforms", "_PLAYLISTS": "PLAYLISTS", "_MY_MUSIC": "My Music", "_CREATED_PLAYLIST": "My Playlist", "_FAVORITED_PLAYLIST": "Favorite Playlist", "_REFRESH_PLAYLIST": "Refresh Playlist", "_FAVORITED": "Favorited", "_FAVORITE": "Favorite", "_PLAY_ALL": "Play All", "_ADD_TO_PLAYLIST": "Add to Playlist", "_EDIT": "Edit", "_IMPORT": "Import", "_IMPORT_PLAYLIST": "Import Playlist", "_ORIGIN_LINK": "Origin link", "_SONGS": "Songs", "_ARTISTS": "Artists", "_ALBUMS": "Albums", "_OPERATION": "Tools", "_CREATE_PLAYLIST": "Create Playlist", "_CANCEL": "Cancel", "_CREATE_AND_ADD_PLAYLIST": "Create and Add", "_REMOVE_FROM_PLAYLIST": "Remove", "_ADD_TO_QUEUE": "Add to Queue", "_ARTIST": "Artist", "_ALBUM": "Album", "_PLAYLIST_TITLE": "Playlist Title", "_PLAYLIST_AUTHOR": "Playlist Author", "_PLAYLIST_SONG_COUNT": "Playlist Song Count", "_PLAYLIST_COVER_IMAGE_URL": "Cover Image URL", "_INPUT_PLAYLIST_TITLE": "Input Playlist Title", "_INPUT_PLAYLIST_COVER_IMAGE_URL": "Input Cover Image URL", "_EDIT_PLAYLIST": "Edit Playlist", "_REMOVE_PLAYLIST": "Remove Playlist", "_OPENING_LASTFM_PAGE":"Opening Last.fm页面...", "_CONFIRM_NOTICE_LASTFM": "Please click \"Yes, all access\" in new page, allow Listen 1 access your account.", "_AUTHORIZED_FINISHED": "Authorized Finished", "_AUTHORIZED_REOPEN": "I Have Problem, try again", "_PLAYLIST_LINK": "Playlist Link", "_OPEN_PLAYLIST": "Open Playlist", "_OPENING_GITHUB_PAGE": "Opening Github.com页面...", "_CONFIRM_NOTICE_GITHUB": "Please click \"Authencate\" in new page, allow Listen 1 access your account", "_CREATE_PLAYLIST_BACKUP": "Create Playlist Backup", "_CREATE_PUBLIC_BACKUP": "Create Public Backup", "_CREATE_PRIVATE_BACKUP": "Create Private Backup", "_BACKUP_PLAYLIST": "Backup Playlists", "_BACKUP_WARNING": "Reinstall or clear cache will lost all your playlists, backup is STRONG RECOMMENDED.", "_EXPORT_TO_LOCAL_FILE": "Export to Local File", "_EXPORT_TO_GITHUB_GIST": "Export to Github Gist", "_RECOVER_PLAYLIST": "Recover Playlists", "_RECOVER_WARNING": "Choose Backup File. Notice: It will overwrite current playlists.", "_RECOVER_FROM_LOCAL_FILE": "Recover from Local File", "_RECOVER_FROM_GITHUB_GIST": "Recover from Github Gist", "_CONNECT_TO_GITHUB": "Connect to Github.com", "_STATUS": "Status", "_RECONNECT": "Reconnect", "_CANCEL_CONNECT": "Cancel Connect", "_SHORTCUTS": "Shortcuts", "_VIEW_SHORTCUTS_LIST": "View Shortcuts List", "_GLOBAL_SHORTCUTS_NOTICE": "Enable Global Shortcuts", "_LYRIC_DISPLAY": "Lyric Display", "_SHOW_DESKTOP_LYRIC": "Show desktop lyric", "_SHOW_LYRIC_TRANSLATION": "Show lyric translation", "_SHOW_DESKTOP_LYRIC_TRANSLATION": "Show lyric translation with desktop lyric", "_CONNECT_TO_LASTFM": "Connect to Last.fm", "_ABOUT": "About", "_HOMEPAGE": "Homepage", "_EMAIL": "Email", "_FEEDBACK": "Feedback", "_DESIGNER": "Designer", "_VERSION": "Version", "_LATEST_VERSION": "Latest Version", "_LICENSE_NOTICE": "(This software is free and open source under MIT license)", "_TOTAL_SONG_PREFIX": "Total ", "_TOTAL_SONG_POSTFIX": " Songs", "_CLEAR_ALL": "Clear All", "_SEARCH_PLACEHOLDER": "Search", "_LANGUAGE": "Language", "_ADD_TO_PLAYLIST_SUCCESS": "Success: Add to My Playlist", "_FAVORITE_PLAYLIST_SUCCESS": "Success: Favorite Playlist", "_EDIT_PLAYLIST_SUCCESS": "Success: Edit Playlist", "_IMPORTING_PLAYLIST": "Importing playlists...", "_IMPORTING_PLAYLIST_SUCCESS": "Success: Import Playlists", "_REMOVE_PLAYLIST_SUCCESS": "Success: Remove Playlist", "_UNFAVORITE_PLAYLIST_SUCCESS": "Success: UnFavorite Playlist", "_REMOVE_SONG_FROM_PLAYLIST_SUCCESS": "Success: Remove Song from Playlist", "_ADD_TO_QUEUE_SUCCESS": "Success: Add to Queue", "_COPYRIGHT_ISSUE": "Fail because copyright issue, Search other platform instead", "_INPUT_NEW_PLAYLIST_TITLE": "Input New Playlist Title", "_FAIL_OPEN_PLAYLIST_URL": "Fail to Open Playlist url", "_EXAMPLE": "Example:", "_CONFIRM": "Confirm", "_THEME": "Theme", "_THEME_WHITE": "White Theme", "_THEME_BLACK": "Black Theme", "_THEME_MODERN_WHITE": "Modern White Theme", "_THEME_MODERN_BLACK": "Modern Black Theme", "_AUTO_CHOOSE_SOURCE": "Auto Choose Source", "_AUTO_CHOOSE_SOURCE_NOTICE": "Enable choose source from other music platform after fail.", "_AUTO_CHOOSE_SOURCE_LIST": "Music Platform to try after fail", "_NOWPLAYING_DISPLAY": "Now Playing Display", "_NOWPLAYING_COVER_BACKGROUND_NOTICE": "Show Cover Image for Now Playing", "_NOWPLAYING_BITRATE_NOTICE": "Show Bitrate", "_NOWPLAYING_PLATFORM_NOTICE": "Show Music Platform", "_LOCAL_MUSIC": "Local Music", "_ADD_LOCAL_SONGS": "Add Local Songs", "netease": "netease", "bilibili": "bili", "kugou": "kugou", "kuwo": "kuwo", "migu": "migu", "qq": "qq", "xiami": "xiami", "taihe": "qianqian", "localmusic": "local music", "_CLOSE_TAB_ACTION": "Close tab action", "_VALID_AFTER_RESTART": "Valid after restart", "_QUIT_APPLICATION": "Quit Application", "_MINIMIZE_TO_BACKGROUND": "Minimize to background", "_MY_CREATED_PLAYLIST": "My Created Playlist", "_MY_FAVORITE_PLAYLIST": "My Favorite Playlist", "_RECOMMEND_PLAYLIST": "Recommend Playlist", "_LISTEN1_LOGIN_NOTICE": "Listen1 WILL NOT transfer your account data to any server other than music platform server.", "_PASSWORD": "Password", "_LOGIN": "Login", "_LOGOUT": "Logout", "_LOGIN_ERROR": "Login error, please check user name and password", "_LOGIN_EMAIL_ERROR": "Please enter correct email address", "_LOGIN_COUNTRYCODE_ERROR": "Please enter correct country code", "_LOGIN_PHONE_ERROR": "Please enter correct phone number", "_LOGIN_PASSWORD_ERROR": "Password can't be empty", "_LOGIN_NETEASE": "Login Netease", "_LOGIN_BY_MOBILE_PHONE": "Login by mobile phone", "_LOGIN_BY_EMAIL": "Login by email", "_MOBILE_PHONE": "Mobile Phone", "_MY_NETEASE": "My Netease", "_MY_QQ": "My QQ Music", "_FAIL_ALL_NOTICE": "No available track in current playlist", "_SHORTCUTS_FUNCTION": "Shortcuts Function", "_GLOBAL_SHORTCUTS": "Global Shortcuts", "_PLAY_OR_PAUSE": "Play/Pause", "_PREVIOUS_TRACK": "Previous Track", "_NEXT_TRACK": "Next Track", "_VOLUME_UP": "Volume Up", "_VOLUME_DOWN": "Volume Down", "_SHORTCUTS_NOT_SET": "N/A", "_QUICK_SEARCH": "Quick Search", "_SEARCH_PLAYLIST": "Search Playlist", "_KEYBOARD_SPACE": "Space", "_NOT_LOGIN_NICKNAME": "Anonymous", "_LOGIN_DIALOG_NOTICE": "Opening Login page, please login in that page and click finish login", "_LOGIN_SUCCESS": "Finish Login", "_LOGIN_FAIL_RETRY": "Something wrong. Reopen Login page", "_PROXY_SYSTEM": "Use System Proxy", "_PROXY_DIRECT": "No Proxy", "_PROXY_CUSTOM": "Custom Proxy", "_PROXY_CONFIG": "Proxy Config", "_PROXY_NOT_SET": "Proxy Not Set", "_MODIFY": "Modify", "_PROTOCOL": "Protocol", "_HOST": "Host", "_PORT": "Port", "ZOOM_IN_OUT": "Zoom In/Out" } ================================================ FILE: i18n/fr-FR.json ================================================ { "HELLO" : "Bonjour", "_ALL_MUSIC": "All Music", "_NETEASE_MUSIC": "Netease", "_QQ_MUSIC": "QQ", "_XIAMI_MUSIC": "Xiami", "_KUGOU_MUSIC": "Kugou", "_KUWO_MUSIC": "Kuwo", "_BILIBILI_MUSIC": "Bilibili", "_MIGU_MUSIC": "Migu", "_TAIHE_MUSIC": "Qianqian", "_PLATFORM_UNION": "Platforms", "_PLAYLISTS": "PLAYLISTS", "_MY_MUSIC": "Ma Musique", "_CREATED_PLAYLIST": "Ma Playlist", "_FAVORITED_PLAYLIST": "Favorite Playlist", "_REFRESH_PLAYLIST": "Refresh Playlist", "_FAVORITED": "Favorited", "_FAVORITE": "Favorite", "_PLAY_ALL": "Lire tout", "_ADD_TO_PLAYLIST": "Ajouter à la Playlist", "_EDIT": "Editer", "_IMPORT": "Importer", "_IMPORT_PLAYLIST": "Importer la Playlist", "_ORIGIN_LINK": "Lien original", "_SONGS": "Musiques", "_ARTISTS": "Artistes", "_ALBUMS": "Albums", "_OPERATION": "Outils", "_CREATE_PLAYLIST": "Créer une Playlist", "_CANCEL": "Annuler", "_CREATE_AND_ADD_PLAYLIST": "Créer et ajouter", "_REMOVE_FROM_PLAYLIST": "Retirer", "_ADD_TO_QUEUE": "Ajouter à la liste de lecture", "_ARTIST": "Artiste", "_ALBUM": "Album", "_PLAYLIST_TITLE": "Titre de la Playlist", "_PLAYLIST_AUTHOR": "Playlist Author", "_PLAYLIST_SONG_COUNT": "Playlist Song Count", "_PLAYLIST_COVER_IMAGE_URL": "URL de la pochette", "_INPUT_PLAYLIST_TITLE": "Inserez le titre de la playlist", "_INPUT_PLAYLIST_COVER_IMAGE_URL": "Entrez l'URL de la pochette", "_EDIT_PLAYLIST": "Editer la playlist", "_REMOVE_PLAYLIST": "Supprimer la Playlist", "_OPENING_LASTFM_PAGE":"Ouverture de Last.fm页面...", "_CONFIRM_NOTICE_LASTFM": "Cliquez sur \"Oui, tout autoriser\" dans la nouvelle page, autoriser l'accès à votre compte.", "_AUTHORIZED_FINISHED": "Autorisation finie", "_AUTHORIZED_REOPEN": "J'ai un problème, veuillez réessayer", "_PLAYLIST_LINK": "Lien de la playslit", "_OPEN_PLAYLIST": "Ouvrir la laylist", "_OPENING_GITHUB_PAGE": "Ouverture de Github.com页面...", "_CONFIRM_NOTICE_GITHUB": "Cliquez sur \"S'identifier\" dans la nouvelle page, autoriser l'accès à votre compte.", "_CREATE_PLAYLIST_BACKUP": "Créer une sauvegarde de playslist", "_CREATE_PUBLIC_BACKUP": "Créer une sauvegarde public de playslist", "_CREATE_PRIVATE_BACKUP": "Créer une sauvegarde privée de playslist", "_BACKUP_PLAYLIST": "Sauvegardes de playslite", "_BACKUP_WARNING": "Réinstallez ou effacez votre cache vous fera perdre toutes vos playlists, les sauvegardes sont recommandés.", "_EXPORT_TO_LOCAL_FILE": "Exporter vers un fichier local", "_EXPORT_TO_GITHUB_GIST": "Exporter vers un Github Gist", "_RECOVER_PLAYLIST": "Récuperer une playlist", "_RECOVER_WARNING": "Séléctionner un fichier de sauvegarde. Notice: Elle remplacera la playliste actuelle.", "_RECOVER_FROM_LOCAL_FILE": "Récuperer par un fichier local", "_RECOVER_FROM_GITHUB_GIST": "Récuperer par un Github Gist", "_CONNECT_TO_GITHUB": "Connecter à Github.com", "_STATUS": "Statut", "_RECONNECT": "Reconnecter", "_CANCEL_CONNECT": "Annuler la connexion", "_SHORTCUTS": "Raccourcis", "_VIEW_SHORTCUTS_LIST": "Voir la liste des raccourcis", "_GLOBAL_SHORTCUTS_NOTICE": "Activer les raccourcis généraux", "_LYRIC_DISPLAY": "Affichage des parole", "_SHOW_DESKTOP_LYRIC": "Afficher les parole du bureau", "_SHOW_LYRIC_TRANSLATION": "Affiche la traduction des paroles", "_SHOW_DESKTOP_LYRIC_TRANSLATION": "Afficher les traductions de paroles du bureau", "_CONNECT_TO_LASTFM": "Ouverture de Last.fm", "_ABOUT": "A Propos", "_HOMEPAGE": "Accueil", "_EMAIL": "Email", "_FEEDBACK": "Feedback", "_DESIGNER": "Designer", "_VERSION": "Version", "_LATEST_VERSION": "Latest Version", "_LICENSE_NOTICE": "(Ce programme est gratuit et tout license MIT)", "_TOTAL_SONG_PREFIX": "Total ", "_TOTAL_SONG_POSTFIX": " Morceaux", "_CLEAR_ALL": "Tout effacer", "_SEARCH_PLACEHOLDER": "Recherche", "_LANGUAGE": "Langage", "_ADD_TO_PLAYLIST_SUCCESS": "Succès: Ajouter à la Playlist", "_FAVORITE_PLAYLIST_SUCCESS": "Success: Favorite Playlist", "_EDIT_PLAYLIST_SUCCESS": "Succès: Editer la Playlist", "_IMPORTING_PLAYLIST": "Importation des playlists...", "_IMPORTING_PLAYLIST_SUCCESS": "Succès: Importer les Playlists", "_REMOVE_PLAYLIST_SUCCESS": "Succès: Effacer Playlist", "_UNFAVORITE_PLAYLIST_SUCCESS": "Succès: UnFavorite Playlist", "_REMOVE_SONG_FROM_PLAYLIST_SUCCESS": "Succès: Retirer la morceau de la Playlist", "_ADD_TO_QUEUE_SUCCESS": "Succès: Ajouter à la file de lecture", "_COPYRIGHT_ISSUE": "Errur due au copyright, Cherchez sur une autre plateforme", "_INPUT_NEW_PLAYLIST_TITLE": "Entrez un nouveau titre de Playlist", "_FAIL_OPEN_PLAYLIST_URL": "Erreur ouverture de l'url", "_EXAMPLE": "Exemple:", "_CONFIRM": "Confirmer", "_THEME": "Thème", "_THEME_WHITE": "Thème blanc", "_THEME_BLACK": "Thème foncé", "_THEME_MODERN_WHITE": "Modern White Theme", "_THEME_MODERN_BLACK": "Modern Black Theme", "_AUTO_CHOOSE_SOURCE": "Auto Choose Source", "_AUTO_CHOOSE_SOURCE_NOTICE": "If play fail, auto choose source from other music platform.", "_AUTO_CHOOSE_SOURCE_LIST": "Music Platform to try after fail", "_NOWPLAYING_DISPLAY": "Now Playing Display", "_NOWPLAYING_COVER_BACKGROUND_NOTICE": "Show Cover Image for Now Playing", "_NOWPLAYING_BITRATE_NOTICE": "Show Bitrate", "_NOWPLAYING_PLATFORM_NOTICE": "Show Music Platform", "_LOCAL_MUSIC": "Local Music", "_ADD_LOCAL_SONGS": "Add Local Songs", "netease": "netease", "bilibili": "bili", "kugou": "kugou", "kuwo": "kuwo", "migu": "migu", "qq": "qq", "xiami": "xiami", "taihe": "qianqian", "localmusic": "local music", "_CLOSE_TAB_ACTION": "Close tab action", "_VALID_AFTER_RESTART": "Valid after restart", "_QUIT_APPLICATION": "Quit Application", "_MINIMIZE_TO_BACKGROUND": "Minimize to background", "_MY_CREATED_PLAYLIST": "My Created Playlist", "_MY_FAVORITE_PLAYLIST": "My Favorite Playlist", "_RECOMMEND_PLAYLIST": "Recommend Playlist", "_LISTEN1_LOGIN_NOTICE": "Listen1 NE transférera PAS les données de votre compte vers un serveur autre que le serveur de la plate-forme musicale.", "_PASSWORD": "Password", "_LOGIN": "Login", "_LOGOUT": "Logout", "_LOGIN_ERROR": "Login error, please check user name and password", "_LOGIN_EMAIL_ERROR": "Please enter correct email address", "_LOGIN_COUNTRYCODE_ERROR": "Please enter correct country code", "_LOGIN_PHONE_ERROR": "Please enter correct phone number", "_LOGIN_PASSWORD_ERROR": "Password can't be empty", "_LOGIN_NETEASE": "Login Netease", "_LOGIN_BY_MOBILE_PHONE": "Login by mobile phone", "_LOGIN_BY_EMAIL": "Login by email", "_MOBILE_PHONE": "Mobile Phone", "_MY_NETEASE": "My Netease", "_MY_QQ": "My QQ Music", "_FAIL_ALL_NOTICE": "No available track in current playlist", "_SHORTCUTS_FUNCTION": "Shortcuts Function", "_GLOBAL_SHORTCUTS": "Global Raccourcis", "_PLAY_OR_PAUSE": "Play/Pause", "_PREVIOUS_TRACK": "Previous Track", "_NEXT_TRACK": "Next Track", "_VOLUME_UP": "Volume Up", "_VOLUME_DOWN": "Volume Down", "_SHORTCUTS_NOT_SET": "N/A", "_QUICK_SEARCH": "Quick Search", "_SEARCH_PLAYLIST": "Search Playlist", "_KEYBOARD_SPACE": "Space", "_NOT_LOGIN_NICKNAME": "Anonymous", "_LOGIN_DIALOG_NOTICE": "Opening Login page, please login in that page and click finish login", "_LOGIN_SUCCESS": "Finish Login", "_LOGIN_FAIL_RETRY": "Something wrong. Reopen Login page", "_PROXY_SYSTEM": "Use System Proxy", "_PROXY_DIRECT": "No Proxy", "_PROXY_CUSTOM": "Custom Proxy", "_PROXY_CONFIG": "Proxy Config", "_PROXY_NOT_SET": "Proxy Not Set", "_MODIFY": "Modify", "_PROTOCOL": "Protocol", "_HOST": "Host", "_PORT": "Port", "ZOOM_IN_OUT": "Zoom In/Out" } ================================================ FILE: i18n/ko-KR.json ================================================ { "HELLO": "안녕하세요", "_ALL_MUSIC": "모든 음악", "_NETEASE_MUSIC": "Netease", "_QQ_MUSIC": "QQ", "_XIAMI_MUSIC": "Xiami", "_KUGOU_MUSIC": "Kugou", "_KUWO_MUSIC": "Kuwo", "_BILIBILI_MUSIC": "Bilibili", "_MIGU_MUSIC": "Migu", "_TAIHE_MUSIC": "Qianqian", "_PLATFORM_UNION": "전체 사이트", "_PLAYLISTS": "재생목록", "_MY_MUSIC": "내 음악", "_CREATED_PLAYLIST": "내 재생목록", "_FAVORITED_PLAYLIST": "좋아하는 재생목록", "_REFRESH_PLAYLIST": "갱신 재생목록", "_FAVORITED": "즐겨찾음", "_FAVORITE": "즐겨찾기", "_PLAY_ALL": "전체 재생", "_ADD_TO_PLAYLIST": "재생목록 추가", "_EDIT": "수정", "_IMPORT": "추가", "_IMPORT_PLAYLIST": "추가 재생목록", "_ORIGIN_LINK": "원본 링크", "_SONGS": "음악", "_ARTISTS": "가수", "_ALBUMS": "앨범", "_OPERATION": "도구", "_CREATE_PLAYLIST": "재생목록 생성", "_CANCEL": "취소", "_CREATE_AND_ADD_PLAYLIST": "생성 및 추가", "_REMOVE_FROM_PLAYLIST": "제거", "_ADD_TO_QUEUE": "목록 추가", "_ARTIST": "가수", "_ALBUM": "앨범", "_PLAYLIST_TITLE": "재생목록 이름", "_PLAYLIST_AUTHOR": "재생목록 생성자", "_PLAYLIST_SONG_COUNT": "재생목록 음악 수", "_PLAYLIST_COVER_IMAGE_URL": "커버 사진 URL", "_INPUT_PLAYLIST_TITLE": "재생목록 이름 입력", "_INPUT_PLAYLIST_COVER_IMAGE_URL": "커버 사진 URL 입력", "_EDIT_PLAYLIST": "재생목록 수정", "_REMOVE_PLAYLIST": "재생목록 제거", "_OPENING_LASTFM_PAGE": "Last.fm 여는중...", "_CONFIRM_NOTICE_LASTFM": "\"Yes, all access\" 을 클릭하여 Listen 1 이 계정에 접근 할 수 있도록 허용 해주세요.", "_AUTHORIZED_FINISHED": "인증 완료", "_AUTHORIZED_REOPEN": "문제 발생, 다시 시도 하세요", "_PLAYLIST_LINK": "재생목록 링크", "_OPEN_PLAYLIST": "재생목록 열기", "_OPENING_GITHUB_PAGE": "Github.com 여는중...", "_CONFIRM_NOTICE_GITHUB": "\"Authencate\" 을 클릭하여 Listen 1 이 계정에 접근 할 수 있도록 허용 해주세요", "_CREATE_PLAYLIST_BACKUP": "재생목록 백업 생성", "_CREATE_PUBLIC_BACKUP": "공개 백업 생성", "_CREATE_PRIVATE_BACKUP": "비공개 백업 생성", "_BACKUP_PLAYLIST": "백업 재생목록", "_BACKUP_WARNING": "캐시를 다시 설치하거나 지우면 모든 재생 목록이 손실됩니다. 백업을 권장 합니다", "_EXPORT_TO_LOCAL_FILE": "로컬 파일 내보내기", "_EXPORT_TO_GITHUB_GIST": "Github Gist 내보내기", "_RECOVER_PLAYLIST": "재생 목록 복구", "_RECOVER_WARNING": "백업 파일을 선택합니다. 알림: 현재 재생 목록을 덮어씁니다.", "_RECOVER_FROM_LOCAL_FILE": "로컬 파일 복구", "_RECOVER_FROM_GITHUB_GIST": "Github Gist 복구", "_CONNECT_TO_GITHUB": "Github.com 연결", "_STATUS": "상태", "_RECONNECT": "재접속", "_CANCEL_CONNECT": "접속 취소", "_SHORTCUTS": "단축키", "_VIEW_SHORTCUTS_LIST": "단축키 리스트 보기", "_GLOBAL_SHORTCUTS_NOTICE": "전역 단축키 활성", "_LYRIC_DISPLAY": "가사 표시", "_SHOW_DESKTOP_LYRIC": "바탕화면 가사 표시", "_SHOW_LYRIC_TRANSLATION": "외국어 가사 번역 표시", "_SHOW_DESKTOP_LYRIC_TRANSLATION": "외국어 가사 번역 및 바탕화면 표시", "_CONNECT_TO_LASTFM": "Last.fm 연결", "_ABOUT": "소개", "_HOMEPAGE": "홈페이지", "_EMAIL": "Email", "_FEEDBACK": "Feedback", "_DESIGNER": "디자이너", "_VERSION": "버전", "_LATEST_VERSION": "마지막 버전", "_LICENSE_NOTICE": "(This software is free and open source under MIT license)", "_TOTAL_SONG_PREFIX": "전체 ", "_TOTAL_SONG_POSTFIX": " 음악", "_CLEAR_ALL": "모두 지우기", "_SEARCH_PLACEHOLDER": "검색", "_LANGUAGE": "언어", "_ADD_TO_PLAYLIST_SUCCESS": "성공: 내 재생 목록에 추가", "_FAVORITE_PLAYLIST_SUCCESS": "성공: 즐겨찾기 재생 목록", "_EDIT_PLAYLIST_SUCCESS": "성공: 재생 목록 편집", "_IMPORTING_PLAYLIST": "재생 목록 가져오기...", "_IMPORTING_PLAYLIST_SUCCESS": "성공: 재생 목록 가져오기", "_REMOVE_PLAYLIST_SUCCESS": "성공: 재생 목록 제거", "_UNFAVORITE_PLAYLIST_SUCCESS": "성공: 재생 목록 즐겨찾기 취소", "_REMOVE_SONG_FROM_PLAYLIST_SUCCESS": "성공: 재생 목록에서 음악 제거", "_ADD_TO_QUEUE_SUCCESS": "성공: 재생 목록에 추가", "_COPYRIGHT_ISSUE": "저작권 문제로 인해 실패, 대신 다른 플랫폼 검색", "_INPUT_NEW_PLAYLIST_TITLE": "새 재생 목록 제목 입력", "_FAIL_OPEN_PLAYLIST_URL": "재생 목록 URL을 열지 못함", "_EXAMPLE": "예제:", "_CONFIRM": "확인", "_THEME": "테마", "_THEME_WHITE": "하얀색 테마", "_THEME_BLACK": "검색은 테마", "_THEME_MODERN_WHITE": "Modern White Theme", "_THEME_MODERN_BLACK": "Modern Black Theme", "_AUTO_CHOOSE_SOURCE": "소스 자동 선택", "_AUTO_CHOOSE_SOURCE_NOTICE": "재생 소스 자동 전환 여부 (음악 재생 실패 후에만 전환)", "_AUTO_CHOOSE_SOURCE_LIST": "실패 후 시도할 음악 플랫폼", "_NOWPLAYING_DISPLAY": "재생 중 표시", "_NOWPLAYING_COVER_BACKGROUND_NOTICE": "지금 재생 중인 커버 이미지 표시", "_NOWPLAYING_BITRATE_NOTICE": "비트 레이트 보이기", "_NOWPLAYING_PLATFORM_NOTICE": "음악 플랫폼 표시", "_LOCAL_MUSIC": "로컬 음악", "_ADD_LOCAL_SONGS": "로컬 음악 추가", "netease": "netease", "bilibili": "bili", "kugou": "kugou", "kuwo": "kuwo", "migu": "migu", "qq": "qq", "xiami": "xiami", "taihe": "qianqian", "localmusic": "로컬 음악", "_CLOSE_TAB_ACTION": "탭 닫기 동작", "_VALID_AFTER_RESTART": "다시 시작 후에 동작", "_QUIT_APPLICATION": "응용 프로그램 종료", "_MINIMIZE_TO_BACKGROUND": "배경으로 최소화", "_MY_CREATED_PLAYLIST": "내가 만든 재생 목록", "_MY_FAVORITE_PLAYLIST": "내가 소장 재생 목록", "_RECOMMEND_PLAYLIST": "추천 재생 목록", "_LISTEN1_LOGIN_NOTICE": "Listen1은 음악 플랫폼 서버 이외의 서버로 계정 데이터를 전송하지 않습니다.", "_PASSWORD": "패스워드", "_LOGIN": "로그인", "_LOGOUT": "로그아웃", "_LOGIN_ERROR": "로그인 오류. 아이디, 패스워드를 확인 하세요.", "_LOGIN_EMAIL_ERROR": "올바른 이메일 주소를 입력하십시오.", "_LOGIN_COUNTRYCODE_ERROR": "올바른 국가 코드를 입력하십시오.", "_LOGIN_PHONE_ERROR": "올바른 전화번호를 입력하십시오.", "_LOGIN_PASSWORD_ERROR": "암호는 비워 둘 수 없습니다.", "_LOGIN_NETEASE": "Netease 로그인", "_LOGIN_BY_MOBILE_PHONE": "휴대폰으로 로그인", "_LOGIN_BY_EMAIL": "이메일로 로그인", "_MOBILE_PHONE": "휴대폰", "_MY_NETEASE": "내 Netease", "_MY_QQ": "내 QQ 음악", "_FAIL_ALL_NOTICE": "현재 재생 목록에 재생 가능한 곡이 없습니다.", "_SHORTCUTS_FUNCTION": "단축키 기능", "_GLOBAL_SHORTCUTS": "전역 단축키", "_PLAY_OR_PAUSE": "재생/일시 중지", "_PREVIOUS_TRACK": "이전 음악", "_NEXT_TRACK": "다음 음악", "_VOLUME_UP": "사운드 크게", "_VOLUME_DOWN": "사운드 작게", "_SHORTCUTS_NOT_SET": "N/A", "_QUICK_SEARCH": "빠른 검색", "_SEARCH_PLAYLIST": "재생 목록 검색", "_KEYBOARD_SPACE": "스페이스", "_NOT_LOGIN_NICKNAME": "익명", "_LOGIN_DIALOG_NOTICE": "그인 페이지를 여는 중입니다. 해당 페이지에 로그인하고 로그인 완료를 클릭하십시오.", "_LOGIN_SUCCESS": "로그인 완료", "_LOGIN_FAIL_RETRY": "로그인에 문제가 생기면 로그인 페이지를 다시 엽니다.", "_PROXY_SYSTEM": "시스템 Proxy 사용", "_PROXY_DIRECT": "Proxy 미사용", "_PROXY_CUSTOM": "사용자 Proxy", "_PROXY_CONFIG": "Proxy 설정", "_PROXY_NOT_SET": "Proxy가 설정되지 않음", "_MODIFY": "수정", "_PROTOCOL": "Protocol", "_HOST": "Host", "_PORT": "Port", "ZOOM_IN_OUT": "확대/축소" } ================================================ FILE: i18n/pt-BR.json ================================================ { "HELLO" : "Olá", "_ALL_MUSIC": "Todas as músicas", "_NETEASE_MUSIC": "Netease", "_QQ_MUSIC": "QQ", "_XIAMI_MUSIC": "Xiami", "_KUGOU_MUSIC": "Kugou", "_KUWO_MUSIC": "Kuwo", "_BILIBILI_MUSIC": "Bilibili", "_MIGU_MUSIC": "Migu", "_TAIHE_MUSIC": "Qianqian", "_PLATFORM_UNION": "Plataformas", "_PLAYLISTS": "PLAYLISTS", "_MY_MUSIC": "Minha música", "_CREATED_PLAYLIST": "Minha Playlist", "_FAVORITED_PLAYLIST": "Playlist Favorita", "_REFRESH_PLAYLIST": "Playlist Recarregar", "_FAVORITED": "Favoritado", "_FAVORITE": "Favorito", "_PLAY_ALL": "Tocar todas", "_ADD_TO_PLAYLIST": "Adicionar pra Playlist", "_EDIT": "Editar", "_IMPORT": "Importar", "_IMPORT_PLAYLIST": "Importar Playlist", "_ORIGIN_LINK": "Link de Origem", "_SONGS": "Músicas", "_ARTISTS": "Artistas", "_ALBUMS": "Álbuns", "_OPERATION": "Ferramentas", "_CREATE_PLAYLIST": "Criar Playlist", "_CANCEL": "Cancelar", "_CREATE_AND_ADD_PLAYLIST": "Criar e adicionar", "_REMOVE_FROM_PLAYLIST": "Remover", "_ADD_TO_QUEUE": "Adicionar à fila", "_ARTIST": "Artista", "_ALBUM": "Álbum", "_PLAYLIST_TITLE": "Título da Playlist", "_PLAYLIST_AUTHOR": "Autor da Playlist", "_PLAYLIST_SONG_COUNT": "Quantidade de Músicas na Playlist", "_PLAYLIST_COVER_IMAGE_URL": "URL da Imagem da Capa", "_INPUT_PLAYLIST_TITLE": "Insira o Título da Lista de Reprodução", "_INPUT_PLAYLIST_COVER_IMAGE_URL": "Insira a URL da Imagem da Capa", "_EDIT_PLAYLIST": "Editar Playlist", "_REMOVE_PLAYLIST": "Remover Playlist", "_OPENING_LASTFM_PAGE":"Abrindo Last.fm...", "_CONFIRM_NOTICE_LASTFM": "Por favor, clique em \"Sim, todo acesso\" na nova página, permite que o Listen 1 acesse sua conta.", "_AUTHORIZED_FINISHED": "Autorização finalizada", "_AUTHORIZED_REOPEN": "Estou com um problema, tente novamente", "_PLAYLIST_LINK": "Link da Playlist", "_OPEN_PLAYLIST": "Abrir Playlist", "_OPENING_GITHUB_PAGE": "Abrindo Github.com...", "_CONFIRM_NOTICE_GITHUB": "Por favor, clique em \"Autenticar\" na nova página, permite que o Listen 1 acesse sua conta.", "_CREATE_PLAYLIST_BACKUP": "Criar Backup da Playlist ", "_CREATE_PUBLIC_BACKUP": "Criar Backup Público", "_CREATE_PRIVATE_BACKUP": "Criar Backup Privado", "_BACKUP_PLAYLIST": "Backup Playlists", "_BACKUP_WARNING": "Reinstalar ou limpar o cache perderá todas as suas listas de reprodução, o backup é FORTEMENTE RECOMENDADO.", "_EXPORT_TO_LOCAL_FILE": "Exportar para um arquivo local", "_EXPORT_TO_GITHUB_GIST": "Exportar para Github Gist", "_RECOVER_PLAYLIST": "Recuperar Playlists", "_RECOVER_WARNING": "Escolha o arquivo de backup. Aviso: Ele substituirá as listas de reprodução atuais.", "_RECOVER_FROM_LOCAL_FILE": "Recuperar de um arquivo local", "_RECOVER_FROM_GITHUB_GIST": "Recuperar do Github Gist", "_CONNECT_TO_GITHUB": "Conectar com Github.com", "_STATUS": "Status", "_RECONNECT": "Reconectar", "_CANCEL_CONNECT": "Cancelar conexão", "_SHORTCUTS": "Atalhos", "_VIEW_SHORTCUTS_LIST": "Ver Lista de Atalhos", "_GLOBAL_SHORTCUTS_NOTICE": "Habilitar Atalhos Globais", "_LYRIC_DISPLAY": "Exibição de letra", "_SHOW_DESKTOP_LYRIC": "Mostrar letra da área de trabalho", "_SHOW_LYRIC_TRANSLATION": "Mostrar tradução da letra", "_SHOW_DESKTOP_LYRIC_TRANSLATION": "Mostrar a tradução da letra com a letra da área de trabalho", "_CONNECT_TO_LASTFM": "Conectar à Last.fm", "_ABOUT": "Sobre", "_HOMEPAGE": "Página Inicial", "_EMAIL": "Email", "_FEEDBACK": "Feedback", "_DESIGNER": "Designer", "_VERSION": "Versão", "_LATEST_VERSION": "Última versão", "_LICENSE_NOTICE": "(Este software é gratuito e de código aberto sob licença do MIT)", "_TOTAL_SONG_PREFIX": "Total ", "_TOTAL_SONG_POSTFIX": " Músicas", "_CLEAR_ALL": "Limpar Tudo", "_SEARCH_PLACEHOLDER": "Pesquisar", "_LANGUAGE": "Língua", "_ADD_TO_PLAYLIST_SUCCESS": "Sucesso: Adicionar em minha Playlist", "_FAVORITE_PLAYLIST_SUCCESS": "Sucesso: Favoritar Playlist", "_EDIT_PLAYLIST_SUCCESS": "Sucesso: Editar Playlist", "_IMPORTING_PLAYLIST": "Importando playlists...", "_IMPORTING_PLAYLIST_SUCCESS": "Sucesso: Importar Playlists", "_REMOVE_PLAYLIST_SUCCESS": "Sucesso: Remover playlist", "_UNFAVORITE_PLAYLIST_SUCCESS": "Sucesso: Desfavoritar playlist", "_REMOVE_SONG_FROM_PLAYLIST_SUCCESS": "Sucesso: Remover música da playlist", "_ADD_TO_QUEUE_SUCCESS": "Sucesso: Adicionar à fila", "_COPYRIGHT_ISSUE": "Falha devido ao problema de direitos autorais. Pesquise em outra plataforma", "_INPUT_NEW_PLAYLIST_TITLE": "Insira o novo título Playlist", "_FAIL_OPEN_PLAYLIST_URL": "Falha em abrir o link da playlist", "_EXAMPLE": "Exemplo:", "_CONFIRM": "Confirmar", "_THEME": "Tema", "_THEME_WHITE": "Tema Claro", "_THEME_BLACK": "Tema Escuro", "_THEME_MODERN_WHITE": "Tema Modern White", "_THEME_MODERN_BLACK": "Tema Modern Black", "_AUTO_CHOOSE_SOURCE": "Escolha automática da fonte", "_AUTO_CHOOSE_SOURCE_NOTICE": "Ative a escolha da fonte de outra plataforma de música após a falha.", "_AUTO_CHOOSE_SOURCE_LIST": "Plataforma de música para tentar depois de falhar", "_NOWPLAYING_DISPLAY": "Tocando agora", "_NOWPLAYING_COVER_BACKGROUND_NOTICE": "Mostrar a imagem da capa para Tocando agora", "_NOWPLAYING_BITRATE_NOTICE": "Mostrar Bitrate", "_NOWPLAYING_PLATFORM_NOTICE": "Mostrar Plataforma da Música ", "_LOCAL_MUSIC": "Música Locas", "_ADD_LOCAL_SONGS": "Adicionar músicas locais", "netease": "netease", "bilibili": "bili", "kugou": "kugou", "kuwo": "kuwo", "migu": "migu", "qq": "qq", "xiami": "xiami", "taihe": "qianqian", "localmusic": "música local", "_CLOSE_TAB_ACTION": "Fechar guia", "_VALID_AFTER_RESTART": "Válidar após reiniciar", "_QUIT_APPLICATION": "Sair da aplicação", "_MINIMIZE_TO_BACKGROUND": "Minimizar para segundo plano", "_MY_CREATED_PLAYLIST": "Minhas Playlist Criadas", "_MY_FAVORITE_PLAYLIST": "Minhas Playlist Favoritas", "_RECOMMEND_PLAYLIST": "Playlists Recomendadas", "_LISTEN1_LOGIN_NOTICE": "Listen1 NÃO transferirá os dados da sua conta para nenhum servidor que não seja o servidor da plataforma de música.", "_PASSWORD": "Senha", "_LOGIN": "Login", "_LOGOUT": "Sair", "_LOGIN_ERROR": "Erro no login, por favor confira se as informações estão corretas", "_LOGIN_EMAIL_ERROR": "Por favor insira um email válido", "_LOGIN_COUNTRYCODE_ERROR": "Por favor insira o código de país correto", "_LOGIN_PHONE_ERROR": "Por favor insira um número de celular válido", "_LOGIN_PASSWORD_ERROR": "A senha não pode estar vazia", "_LOGIN_NETEASE": "Login Netease", "_LOGIN_BY_MOBILE_PHONE": "Login por Celular", "_LOGIN_BY_EMAIL": "Login por Email", "_MOBILE_PHONE": "Celular", "_MY_NETEASE": "Meu Netease", "_MY_QQ": "Meu QQ Music", "_FAIL_ALL_NOTICE": "Nenhuma faixa disponível na playlist atual", "_SHORTCUTS_FUNCTION": "Função dos atalhos", "_GLOBAL_SHORTCUTS": "Atalhos globais", "_PLAY_OR_PAUSE": "Play/Pause", "_PREVIOUS_TRACK": "Faixa anterior", "_NEXT_TRACK": "Próxima faixa", "_VOLUME_UP": "Volume Up", "_VOLUME_DOWN": "Volume Down", "_SHORTCUTS_NOT_SET": "N/A", "_QUICK_SEARCH": "Pesquisa Rápida", "_SEARCH_PLAYLIST": "Procurar Playlist", "_KEYBOARD_SPACE": "Espaço", "_NOT_LOGIN_NICKNAME": "Anônimo", "_LOGIN_DIALOG_NOTICE": "Abrindo a página de login, faça o login nessa página e clique em concluir o login", "_LOGIN_SUCCESS": "Concluir o login", "_LOGIN_FAIL_RETRY": "Algo deu errado. Reabra a página de login", "_PROXY_SYSTEM": "Usar Proxy do Sistema", "_PROXY_DIRECT": "Sem proxy", "_PROXY_CUSTOM": "Proxy personalizado", "_PROXY_CONFIG": "Configuração de proxy", "_PROXY_NOT_SET": "Proxy não definido", "_MODIFY": "Modificar", "_PROTOCOL": "Protocolo", "_HOST": "Host", "_PORT": "Porta", "ZOOM_IN_OUT": "Ampliar/Reduzir" } ================================================ FILE: i18n/zh-CN.json ================================================ { "HELLO" : "你好", "_ALL_MUSIC": "所有音乐", "_NETEASE_MUSIC": "网易云音乐", "_QQ_MUSIC": "QQ音乐", "_XIAMI_MUSIC": "虾米音乐", "_KUGOU_MUSIC": "酷狗音乐", "_KUWO_MUSIC": "酷我音乐", "_BILIBILI_MUSIC": "哔哩哔哩", "_MIGU_MUSIC": "咪咕音乐", "_TAIHE_MUSIC": "千千音乐", "_PLATFORM_UNION": "平台聚合", "_PLAYLISTS": "精选歌单", "_MY_MUSIC": "我的音乐", "_CREATED_PLAYLIST": "创建的歌单", "_FAVORITED_PLAYLIST": "收藏的歌单", "_REFRESH_PLAYLIST": "刷新", "_FAVORITED": "已收藏", "_FAVORITE": "收藏", "_PLAY_ALL": "播放全部", "_ADD_TO_PLAYLIST": "添加到我的歌单", "_EDIT": "编辑", "_IMPORT": "导入", "_IMPORT_PLAYLIST": "导入歌单", "_ORIGIN_LINK": "链接", "_SONGS": "歌曲名", "_ARTISTS": "歌手", "_ALBUMS": "专辑名", "_OPERATION": "操作", "_CREATE_PLAYLIST": "新建歌单", "_CANCEL": "取消", "_CREATE_AND_ADD_PLAYLIST": "创建并添加", "_REMOVE_FROM_PLAYLIST": "从歌单删除", "_ADD_TO_QUEUE": "添加到当前播放", "_ARTIST": "歌手", "_ALBUM": "专辑名", "_PLAYLIST_TITLE": "歌单名称", "_PLAYLIST_AUTHOR": "歌单作者", "_PLAYLIST_SONG_COUNT": "歌曲数", "_PLAYLIST_COVER_IMAGE_URL": "封面图片url", "_INPUT_PLAYLIST_TITLE": "输入歌单名称", "_INPUT_PLAYLIST_COVER_IMAGE_URL": "输入封面URL", "_EDIT_PLAYLIST": "编辑歌单", "_REMOVE_PLAYLIST": "删除歌单", "_OPENING_LASTFM_PAGE":"正在打开Last.fm页面...", "_CONFIRM_NOTICE_LASTFM": "请在打开的页面点击\"Yes, all access\", 允许Listen 1访问你的账户。", "_AUTHORIZED_FINISHED": "已经完成授权", "_AUTHORIZED_REOPEN": "遇到问题,再次打开授权页", "_PLAYLIST_LINK": "歌单链接", "_OPEN_PLAYLIST": "打开歌单", "_OPENING_GITHUB_PAGE": "正在打开Github.com页面...", "_CONFIRM_NOTICE_GITHUB": "请在打开的页面点击\"Authencate\", 允许Listen 1访问你的账户。", "_CREATE_PLAYLIST_BACKUP": "创建歌单备份", "_CREATE_PUBLIC_BACKUP": "创建公开备份", "_CREATE_PRIVATE_BACKUP": "创建私有备份", "_BACKUP_PLAYLIST": "备份歌单", "_BACKUP_WARNING": "重装插件或清除缓存数据会导致我的歌单数据丢失,强烈建议在这些操作前,备份我的歌单。", "_EXPORT_TO_LOCAL_FILE": "导出到本地文件", "_EXPORT_TO_GITHUB_GIST": "导出到Github Gist", "_RECOVER_PLAYLIST": "从备份恢复", "_RECOVER_WARNING": "选择备份文件,恢复我的歌单。注意:恢复我的歌单会覆盖现有的歌单。", "_RECOVER_FROM_LOCAL_FILE": "从本地文件导入", "_RECOVER_FROM_GITHUB_GIST": "从Github Gist导入", "_CONNECT_TO_GITHUB": "连接到Github.com", "_STATUS": "状态", "_RECONNECT": "重新连接", "_CANCEL_CONNECT": "取消连接", "_SHORTCUTS": "快捷键", "_VIEW_SHORTCUTS_LIST": "查看快捷键列表", "_GLOBAL_SHORTCUTS_NOTICE": "启用全局快捷键", "_LYRIC_DISPLAY": "歌词显示", "_SHOW_DESKTOP_LYRIC": "启用桌面歌词", "_SHOW_LYRIC_TRANSLATION": "外文歌词显示翻译(播放页)", "_SHOW_DESKTOP_LYRIC_TRANSLATION": "外文歌词显示翻译(桌面歌词)", "_CONNECT_TO_LASTFM": "连接到last.fm", "_ABOUT": "关于", "_HOMEPAGE": "主页", "_EMAIL": "邮箱", "_FEEDBACK": "反馈问题", "_DESIGNER": "主题设计", "_VERSION": "当前版本", "_LATEST_VERSION": "最新版本", "_LICENSE_NOTICE": "(本软件基于MIT协议开源免费)", "_TOTAL_SONG_PREFIX": "共", "_TOTAL_SONG_POSTFIX": "首", "_CLEAR_ALL": "清空", "_SEARCH_PLACEHOLDER": "搜索", "_LANGUAGE": "语言", "_ADD_TO_PLAYLIST_SUCCESS": "成功添加到我创建的歌单", "_FAVORITE_PLAYLIST_SUCCESS": "收藏成功", "_EDIT_PLAYLIST_SUCCESS": "编辑歌单成功", "_IMPORTING_PLAYLIST": "正在导入歌单...", "_IMPORTING_PLAYLIST_SUCCESS": "导入歌单成功", "_REMOVE_PLAYLIST_SUCCESS": "删除歌单成功", "_UNFAVORITE_PLAYLIST_SUCCESS": "取消收藏成功", "_REMOVE_SONG_FROM_PLAYLIST_SUCCESS": "删除歌曲成功", "_ADD_TO_QUEUE_SUCCESS": "添加到当前播放成功", "_COPYRIGHT_ISSUE": "版权原因无法播放,请搜索其他平台", "_INPUT_NEW_PLAYLIST_TITLE": "输入新歌单名称", "_FAIL_OPEN_PLAYLIST_URL": "未能打开输入的歌单地址", "_EXAMPLE": "例如", "_CONFIRM": "确认", "_THEME": "主题", "_THEME_WHITE": "简约白", "_THEME_BLACK": "深空灰", "_THEME_MODERN_WHITE": "现代白", "_THEME_MODERN_BLACK": "现代黑", "_AUTO_CHOOSE_SOURCE": "自动切换源", "_AUTO_CHOOSE_SOURCE_NOTICE": "是否自动切换播放源(仅在播放音乐失败后切换)", "_AUTO_CHOOSE_SOURCE_LIST": "从以下平台搜索可用源", "_NOWPLAYING_DISPLAY": "正在播放显示", "_NOWPLAYING_COVER_BACKGROUND_NOTICE": "显示专辑封面作为背景", "_NOWPLAYING_BITRATE_NOTICE": "显示比特率", "_NOWPLAYING_PLATFORM_NOTICE": "显示音乐平台", "_LOCAL_MUSIC": "本地音乐", "_ADD_LOCAL_SONGS": "添加歌曲", "netease": "网易", "bilibili": "哔哩", "kugou": "酷狗", "kuwo": "酷我", "migu": "咪咕", "qq": "QQ", "xiami": "虾米", "taihe": "千千", "localmusic": "本地", "_CLOSE_TAB_ACTION": "关闭标签页时行为", "_VALID_AFTER_RESTART": "需重启生效", "_QUIT_APPLICATION": "退出应用", "_MINIMIZE_TO_BACKGROUND": "最小化到后台", "_MY_CREATED_PLAYLIST": "我创建的歌单", "_MY_FAVORITE_PLAYLIST": "我收藏的歌单", "_RECOMMEND_PLAYLIST": "推荐歌单", "_LISTEN1_LOGIN_NOTICE": "Listen1不会传输你的账号数据到任何第三方服务器。", "_PASSWORD": "密码", "_LOGIN": "登录", "_LOGOUT": "退出登录", "_LOGIN_ERROR": "登录失败,请检查用户名和密码", "_LOGIN_EMAIL_ERROR": "请输入正确的邮箱地址", "_LOGIN_COUNTRYCODE_ERROR": "请输入正确的国家或地区代码", "_LOGIN_PHONE_ERROR": "请输入正确的手机号", "_LOGIN_PASSWORD_ERROR": "密码不能为空", "_LOGIN_NETEASE": "登录网易云音乐", "_LOGIN_BY_MOBILE_PHONE": "手机号登录", "_LOGIN_BY_EMAIL": "邮箱登录", "_MOBILE_PHONE": "手机号", "_MY_NETEASE": "我的网易云音乐", "_MY_QQ": "我的QQ音乐", "_FAIL_ALL_NOTICE": "当前播放列表没有可播放的歌曲", "_SHORTCUTS_FUNCTION": "功能说明", "_GLOBAL_SHORTCUTS": "全局快捷键", "_PLAY_OR_PAUSE": "播放/暂停", "_PREVIOUS_TRACK": "上一首", "_NEXT_TRACK": "下一首", "_VOLUME_UP": "音量加", "_VOLUME_DOWN": "音量减", "_SHORTCUTS_NOT_SET": "空", "_QUICK_SEARCH": "快速搜索", "_SEARCH_PLAYLIST": "搜索歌单", "_KEYBOARD_SPACE": "空格", "_NOT_LOGIN_NICKNAME": "未登录", "_LOGIN_DIALOG_NOTICE": "正在打开音乐平台的登录页,请在打开网页中完成登录流程,然后点击登录完成", "_LOGIN_SUCCESS": "登录完成", "_LOGIN_FAIL_RETRY": "登录遇到问题,再打开登录页面", "_PROXY_SYSTEM": "使用系统代理", "_PROXY_DIRECT": "不使用代理", "_PROXY_CUSTOM": "自定义代理", "_PROXY_CONFIG": "代理设置", "_PROXY_NOT_SET": "未设置", "_MODIFY": "修改", "_PROTOCOL": "代理协议", "_HOST": "主机地址", "_PORT": "端口", "ZOOM_IN_OUT": "放大/缩小" } ================================================ FILE: i18n/zh-TC.json ================================================ { "HELLO" : "你好", "_ALL_MUSIC": "所有音樂", "_NETEASE_MUSIC": "網易雲音樂", "_QQ_MUSIC": "QQ音樂", "_XIAMI_MUSIC": "蝦米音樂", "_KUGOU_MUSIC": "酷狗音樂", "_KUWO_MUSIC": "酷我音樂", "_BILIBILI_MUSIC": "嗶哩嗶哩", "_MIGU_MUSIC": "咪咕音樂", "_TAIHE_MUSIC": "千千音樂", "_PLATFORM_UNION": "平臺聚合", "_PLAYLISTS": "精選歌單", "_MY_MUSIC": "我的音樂", "_CREATED_PLAYLIST": "創建的歌單", "_FAVORITED_PLAYLIST": "收藏的歌單", "_REFRESH_PLAYLIST": "刷新", "_FAVORITED": "已收藏", "_FAVORITE": "收藏", "_PLAY_ALL": "播放全部", "_ADD_TO_PLAYLIST": "添加到我的歌單", "_EDIT": "編輯", "_IMPORT": "導入", "_IMPORT_PLAYLIST": "導入歌單", "_ORIGIN_LINK": "連結", "_SONGS": "曲目", "_ARTISTS": "歌手", "_ALBUMS": "專輯名", "_OPERATION": "操作", "_CREATE_PLAYLIST": "創建歌單", "_CANCEL": "取消", "_CREATE_AND_ADD_PLAYLIST": "創建並添加", "_REMOVE_FROM_PLAYLIST": "從歌單裡移除", "_ADD_TO_QUEUE": "加入到當前播放", "_ARTIST": "歌手", "_ALBUM": "專輯名", "_PLAYLIST_TITLE": "歌單名稱", "_PLAYLIST_AUTHOR": "歌單作者", "_PLAYLIST_SONG_COUNT": "曲目數", "_PLAYLIST_COVER_IMAGE_URL": "封面圖片url", "_INPUT_PLAYLIST_TITLE": "鍵入歌單名稱", "_INPUT_PLAYLIST_COVER_IMAGE_URL": "鍵入封面圖片URL", "_EDIT_PLAYLIST": "編輯歌單", "_REMOVE_PLAYLIST": "移除歌單", "_OPENING_LASTFM_PAGE":"正在打開Last.fm頁面...", "_CONFIRM_NOTICE_LASTFM": "請在打開的頁面點擊\"Yes, all access\", 允許Listen 1訪問你的帳戶。", "_AUTHORIZED_FINISHED": "已經完成授權", "_AUTHORIZED_REOPEN": "遇到問題,再次打開授權頁", "_PLAYLIST_LINK": "歌單連結", "_OPEN_PLAYLIST": "打開歌單", "_OPENING_GITHUB_PAGE": "正在打開Github.com頁面...", "_CONFIRM_NOTICE_GITHUB": "請在打開的頁面點擊\"Authencate\", 允許Listen 1訪問你的帳戶。", "_CREATE_PLAYLIST_BACKUP": "創建歌單備份", "_CREATE_PUBLIC_BACKUP": "創建公開備份", "_CREATE_PRIVATE_BACKUP": "創建私有備份", "_BACKUP_PLAYLIST": "備份歌單", "_BACKUP_WARNING": "重建應用程式或清除緩存檔案會導致我的歌單數據丟失,強烈建議在做這件事,備份我的歌單。", "_EXPORT_TO_LOCAL_FILE": "匯出到本地檔案", "_EXPORT_TO_GITHUB_GIST": "匯出到Github Gist", "_RECOVER_PLAYLIST": "從備份恢復", "_RECOVER_WARNING": "選擇備份檔案,恢復我的歌單。危險:恢復我的歌單會覆蓋現有的歌單。", "_RECOVER_FROM_LOCAL_FILE": "從本地檔案導入", "_RECOVER_FROM_GITHUB_GIST": "從Github Gist導入", "_CONNECT_TO_GITHUB": "連結到Github.com", "_STATUS": "狀態", "_RECONNECT": "重新連結", "_CANCEL_CONNECT": "取消連結", "_SHORTCUTS": "快速鍵", "_VIEW_SHORTCUTS_LIST": "查看快速鍵列表", "_GLOBAL_SHORTCUTS_NOTICE": "啟用全域快速鍵", "_LYRIC_DISPLAY": "歌詞顯示", "_SHOW_DESKTOP_LYRIC": "啟用桌面歌詞", "_SHOW_LYRIC_TRANSLATION": "外文歌詞顯示翻譯(播放頁)", "_SHOW_DESKTOP_LYRIC_TRANSLATION": "外文歌詞顯示翻譯(桌面歌詞)", "_CONNECT_TO_LASTFM": "連結到last.fm", "_ABOUT": "關於", "_HOMEPAGE": "主頁", "_EMAIL": "電郵", "_FEEDBACK": "回饋問題", "_DESIGNER": "主題設計", "_VERSION": "當前版本", "_LATEST_VERSION": "最新版本", "_LICENSE_NOTICE": "(本軟體基於MIT協定開源免費)", "_TOTAL_SONG_PREFIX": "總 ", "_TOTAL_SONG_POSTFIX": " 首", "_CLEAR_ALL": "清空", "_SEARCH_PLACEHOLDER": "搜索", "_LANGUAGE": "語言", "_ADD_TO_PLAYLIST_SUCCESS": "成功添加到我創建的歌單", "_FAVORITE_PLAYLIST_SUCCESS": "收藏成功", "_EDIT_PLAYLIST_SUCCESS": "編輯歌單成功", "_IMPORTING_PLAYLIST": "正在導入歌單...", "_IMPORTING_PLAYLIST_SUCCESS": "導入歌單成功", "_REMOVE_PLAYLIST_SUCCESS": "移除歌單成功", "_UNFAVORITE_PLAYLIST_SUCCESS": "移除收藏成功", "_REMOVE_SONG_FROM_PLAYLIST_SUCCESS": "移除曲目成功", "_ADD_TO_QUEUE_SUCCESS": "添加到當前播放成功", "_COPYRIGHT_ISSUE": "版權限定無法播放,請搜索其他平臺", "_INPUT_NEW_PLAYLIST_TITLE": "鍵入新歌單名稱", "_FAIL_OPEN_PLAYLIST_URL": "未能打開鍵入的歌單位址", "_EXAMPLE": "例如 ", "_CONFIRM": "確認", "_THEME": "樣式", "_THEME_WHITE": "簡約白", "_THEME_BLACK": "深空灰", "_THEME_MODERN_WHITE": "现代白", "_THEME_MODERN_BLACK": "现代黑", "_AUTO_CHOOSE_SOURCE": "自動切換源", "_AUTO_CHOOSE_SOURCE_NOTICE": "是否自動切換播放源(僅在播放音樂失敗後切換)", "_AUTO_CHOOSE_SOURCE_LIST": "從以下平臺搜索可用源", "_NOWPLAYING_DISPLAY": "正在播放顯示", "_NOWPLAYING_COVER_BACKGROUND_NOTICE": "顯示專輯封面作為背景", "_NOWPLAYING_BITRATE_NOTICE": "顯示比特率", "_NOWPLAYING_PLATFORM_NOTICE": "顯示音樂平臺", "_LOCAL_MUSIC": "本機音樂", "_ADD_LOCAL_SONGS": "添加本機音樂", "netease": "網易", "bilibili": "嗶哩嗶哩", "kugou": "酷狗", "kuwo": "酷我", "migu": "咪咕", "qq": "QQ", "xiami": "蝦米", "taihe": "千千", "localmusic": "本地", "_CLOSE_TAB_ACTION": "關閉標籤頁時行為", "_VALID_AFTER_RESTART": "需重載生效", "_QUIT_APPLICATION": "退出軟體", "_MINIMIZE_TO_BACKGROUND": "最小化到後臺", "_MY_CREATED_PLAYLIST": "我創建的歌單", "_MY_FAVORITE_PLAYLIST": "我收藏的歌單", "_RECOMMEND_PLAYLIST": "推薦歌單", "_LISTEN1_LOGIN_NOTICE": "Listen1不會傳輸你的賬號數據到任何第三方伺服器。", "_PASSWORD": "密碼", "_LOGIN": "登錄", "_LOGOUT": "退出登錄", "_LOGIN_ERROR": "登錄失敗,請檢查用戶名和密碼", "_LOGIN_EMAIL_ERROR": "請輸入正確的郵箱地址", "_LOGIN_COUNTRYCODE_ERROR": "請輸入正確的國家或地區代碼", "_LOGIN_PHONE_ERROR": "請輸入正確的手機號", "_LOGIN_PASSWORD_ERROR": "密碼不能為空", "_LOGIN_NETEASE": "登錄網易雲音樂", "_LOGIN_BY_MOBILE_PHONE": "手機號登錄", "_LOGIN_BY_EMAIL": "郵箱登錄", "_MOBILE_PHONE": "手機號", "_MY_NETEASE": "我的網易雲音樂", "_MY_QQ": "我的QQ音樂", "_FAIL_ALL_NOTICE": "當前播放列表沒有可播放的歌曲", "_SHORTCUTS_FUNCTION": "功能說明", "_GLOBAL_SHORTCUTS": "全局快捷鍵", "_PLAY_OR_PAUSE": "播放/暫停", "_PREVIOUS_TRACK": "上一首", "_NEXT_TRACK": "下一首", "_VOLUME_UP": "音量加", "_VOLUME_DOWN": "音量減", "_SHORTCUTS_NOT_SET": "空", "_QUICK_SEARCH": "快速搜索", "_SEARCH_PLAYLIST": "搜索歌單", "_KEYBOARD_SPACE": "空格", "_NOT_LOGIN_NICKNAME": "未登錄", "_LOGIN_DIALOG_NOTICE": "正在打開音樂平臺的登錄頁,請在打開網頁中完成登錄流程,然後點擊登錄完成", "_LOGIN_SUCCESS": "登錄完成", "_LOGIN_FAIL_RETRY": "登錄遇到問題,再打開登錄頁面", "_PROXY_SYSTEM": "使用系統代理", "_PROXY_DIRECT": "不使用代理", "_PROXY_CUSTOM": "自定義代理", "_PROXY_CONFIG": "代理設置", "_PROXY_NOT_SET": "未設置", "_MODIFY": "修改", "_PROTOCOL": "代理協議", "_HOST": "主機地址", "_PORT": "端口", "ZOOM_IN_OUT": "放大/縮小" } ================================================ FILE: js/app.js ================================================ /* eslint-disable no-shadow */ /* global l1Player require */ /* global angular isElectron i18next i18nextHttpBackend Notyf notyf */ /* global setPrototypeOfLocalStorage */ /* eslint-disable global-require */ /* eslint-disable no-unused-vars */ /* eslint-disable no-param-reassign */ /* eslint-disable import/no-unresolved */ const sourceList = [ { name: 'netease', displayId: '_NETEASE_MUSIC', }, { name: 'qq', displayId: '_QQ_MUSIC', }, { name: 'kugou', displayId: '_KUGOU_MUSIC', }, { name: 'kuwo', displayId: '_KUWO_MUSIC', }, { name: 'bilibili', displayId: '_BILIBILI_MUSIC', }, { name: 'migu', displayId: '_MIGU_MUSIC', }, { name: 'taihe', displayId: '_TAIHE_MUSIC', }, ]; const main = () => { const app = angular.module('listenone', []); setPrototypeOfLocalStorage(); app.config([ '$compileProvider', ($compileProvider) => { $compileProvider.imgSrcSanitizationWhitelist( /^\s*(https?|ftp|mailto|chrome-extension|moz-extension|file):/ ); }, ]); app.run([ '$q', ($q) => { axios.Axios.prototype.request_original = axios.Axios.prototype.request; axios.Axios.prototype.request = function new_req(config) { return $q.when(this.request_original(config)); }; window.notyf = new Notyf({ duration: 5000, ripple: true, position: { x: 'center', y: 'top' }, types: [ { type: 'warning', background: 'darkorange', icon: false, }, { type: 'info', background: 'deepskyblue', icon: false, }, { type: 'success', className: 'notyf__toast--success', backgroundColor: '#335eea', icon: { className: 'notyf__icon--success', tagName: 'i' }, }, { type: 'error', className: 'notyf__toast--error', backgroundColor: '#b90b2c', icon: { className: 'notyf__icon--error', tagName: 'i' }, }, ], }); window.notyf.warning = (msg, replace) => { if (replace) { notyf.dismissAll(); } window.notyf.open({ type: 'warning', message: msg, }); }; window.notyf.info = (msg, replace) => { if (replace) { notyf.dismissAll(); } window.notyf.open({ type: 'info', message: msg, }); }; }, ]); l1Player.injectDirectives(app); app.filter('playmode_title', () => (input) => { switch (input) { case 0: return '顺序'; case 1: return '随机'; case 2: return '单曲循环'; default: return ''; } }); app.directive('customOnChange', () => { const ret = { restrict: 'A', link: (scope, element, attrs) => { const onChangeHandler = scope.$eval(attrs.customOnChange); element.bind('change', onChangeHandler); }, }; return ret; }); app.directive('volumeWheel', () => (scope, element, attrs) => { element.bind('mousewheel', () => { l1Player.adjustVolume(window.event.wheelDelta > 0); }); }); app.directive('pagination', () => ({ restrict: 'EA', replace: false, template: ` `, })); app.directive('errSrc', () => ({ // https://stackoverflow.com/questions/16310298/if-a-ngsrc-path-resolves-to-a-404-is-there-a-way-to-fallback-to-a-default link: (scope, element, attrs) => { element.bind('error', () => { if (attrs.src !== attrs.errSrc) { attrs.$set('src', attrs.errSrc); } }); attrs.$observe('ngSrc', (value) => { if (!value && attrs.errSrc) { attrs.$set('src', attrs.errSrc); } }); }, })); app.directive('resize', ($window) => (scope, element) => { const w = angular.element($window); const changeHeight = () => { const headerHeight = 90; const footerHeight = 90; element.css('height', `${w.height() - headerHeight - footerHeight}px`); }; w.bind('resize', () => { changeHeight(); // when window size gets changed }); changeHeight(); // when page loads }); app.directive('addAndPlay', [ () => ({ restrict: 'EA', scope: { song: '=addAndPlay', }, link(scope, element, attrs) { element.bind('click', (event) => { l1Player.addTrack(scope.song); l1Player.playById(scope.song.id); }); }, }), ]); app.directive('addWithoutPlay', [ () => ({ restrict: 'EA', scope: { song: '=addWithoutPlay', }, link(scope, element, attrs) { element.bind('click', (event) => { l1Player.addTrack(scope.song); notyf.success(i18next.t('_ADD_TO_QUEUE_SUCCESS')); }); }, }), ]); app.directive('openUrl', [ '$window', ($window) => ({ restrict: 'EA', scope: { url: '=openUrl', }, link(scope, element, attrs) { element.bind('click', (event) => { if (isElectron()) { const { shell } = require('electron'); shell.openExternal(scope.url); } else { $window.open(scope.url, '_blank'); } }); }, }), ]); app.directive('windowControl', [ '$window', ($window) => ({ restrict: 'EA', scope: { action: '@windowControl', }, link(scope, element, attrs) { element.bind('click', (event) => { if (isElectron()) { const { ipcRenderer } = require('electron'); ipcRenderer.send('control', scope.action); } }); }, }), ]); app.directive('infiniteScroll', [ '$window', '$rootScope', ($window, $rootScope) => ({ restrict: 'EA', scope: { infiniteScroll: '&', contentSelector: '=contentSelector', }, link(scope, elements, attrs) { elements.bind('scroll', (event) => { if (scope.loading) { return; } const containerElement = elements[0]; const contentElement = document.querySelector(scope.contentSelector); const baseTop = containerElement.getBoundingClientRect().top; const currentTop = contentElement.getBoundingClientRect().top; const baseHeight = containerElement.offsetHeight; const offset = baseTop - currentTop; const bottom = offset + baseHeight; const height = contentElement.offsetHeight; const remain = height - bottom; if (remain < 0) { // page not shown return; } const offsetToload = 10; if (remain <= offsetToload) { $rootScope.$broadcast('infinite_scroll:hit_bottom', ''); } }); }, }), ]); /* drag drop support */ app.directive('dragDropZone', [ '$window', ($window) => ({ restrict: 'A', scope: { dragobject: '=dragZoneObject', dragtitle: '=dragZoneTitle', dragtype: '=dragZoneType', ondrop: '&dropZoneOndrop', ondragleave: '&dropZoneOndragleave', sortable: '=', }, link(scope, element, attrs) { // https://stackoverflow.com/questions/34200023/drag-drop-set-custom-html-as-drag-image element.on('dragstart', (ev) => { if (scope.dragobject === undefined) { return; } if (scope.dragtype === undefined) { return; } ev.dataTransfer.setData( scope.dragtype, JSON.stringify(scope.dragobject) ); const elem = document.createElement('div'); elem.id = 'drag-ghost'; elem.innerHTML = scope.dragtitle; elem.style.position = 'absolute'; elem.style.top = '-1000px'; elem.style.padding = '3px'; elem.style.background = '#eeeeee'; elem.style.color = '#333'; elem.style['border-radius'] = '3px'; document.body.appendChild(elem); ev.dataTransfer.setDragImage(elem, 0, 40); }); element.on('dragend', () => { const ghost = document.getElementById('drag-ghost'); if (ghost.parentNode) { ghost.parentNode.removeChild(ghost); } }); element.on('dragenter', (event) => { let dragType = ''; if (event.dataTransfer.types.length > 0) { [dragType] = event.dataTransfer.types; } if ( scope.dragtype === 'application/listen1-myplaylist' && dragType === 'application/listen1-song' ) { element[0].classList.add('dragover'); } }); element.on('dragleave', (event) => { element[0].classList.remove('dragover'); if (scope.ondragleave !== undefined) { scope.ondragleave(); } if (scope.sortable) { const target = element[0]; target.style['z-index'] = '0'; target.style['border-bottom'] = 'solid 2px transparent'; target.style['border-top'] = 'solid 2px transparent'; } }); element.on('dragover', (event) => { event.preventDefault(); const dragLineColor = '#FF4444'; let dragType = ''; if (event.dataTransfer.types.length > 0) { [dragType] = event.dataTransfer.types; } if (scope.dragtype === dragType && scope.sortable) { event.dataTransfer.dropEffect = 'move'; const bounding = event.target.getBoundingClientRect(); const offset = bounding.y + bounding.height / 2; const direction = event.clientY - offset > 0 ? 'bottom' : 'top'; const target = element[0]; if (direction === 'bottom') { target.style['border-bottom'] = `solid 2px ${dragLineColor}`; target.style['border-top'] = 'solid 2px transparent'; target.style['z-index'] = '9'; } else if (direction === 'top') { target.style['border-top'] = `solid 2px ${dragLineColor}`; target.style['border-bottom'] = 'solid 2px transparent'; target.style['z-index'] = '9'; } } else if ( scope.dragtype === 'application/listen1-myplaylist' && dragType === 'application/listen1-song' ) { event.dataTransfer.dropEffect = 'copy'; } }); element.on('drop', (event) => { if (scope.ondrop === undefined) { return; } const [dragType] = event.dataTransfer.types; const jsonString = event.dataTransfer.getData(dragType); const data = JSON.parse(jsonString); let direction = ''; const bounding = event.target.getBoundingClientRect(); const offset = bounding.y + bounding.height / 2; direction = event.clientY - offset > 0 ? 'bottom' : 'top'; // https://stackoverflow.com/questions/19889615/can-an-angular-directive-pass-arguments-to-functions-in-expressions-specified-in scope.ondrop({ arg1: data, arg2: dragType, arg3: direction }); element[0].classList.remove('dragover'); if (scope.sortable) { const target = element[0]; target.style['border-top'] = 'solid 2px transparent'; target.style['border-bottom'] = 'solid 2px transparent'; } }); }, }), ]); app.directive('draggableBar', [ '$document', '$rootScope', ($document, $rootScope) => (scope, element, attrs) => { let x; let container; const { mode } = attrs; function onMyMousedown() { if (mode === 'play') { $rootScope.$broadcast('dragbar:changing_progress', true); } } function onMyMouseup() { if (mode === 'play') { $rootScope.$broadcast('dragbar:changing_progress', false); } } function onMyUpdateProgress(progress) { if (mode === 'play') { $rootScope.$broadcast('dragbar:myprogress', progress * 100); } if (mode === 'volume') { l1Player.setVolume(progress * 100); l1Player.unmute(); } } function onMyCommitProgress(progress) { if (mode === 'play') { l1Player.seek(progress); } if (mode === 'volume') { const current = localStorage.getObject('player-settings'); current.volume = progress * 100; localStorage.setObject('player-settings', current); } } function commitProgress(progress) { onMyCommitProgress(progress); } function updateProgress() { if (container) { if (x < 0) { x = 0; } else if (x > container.right - container.left) { x = container.right - container.left; } } const progress = x / (container.right - container.left); onMyUpdateProgress(progress); } function mousemove(event) { x = event.clientX - container.left; updateProgress(); } function mouseup() { const progress = x / (container.right - container.left); commitProgress(progress); $document.off('mousemove', mousemove); $document.off('mouseup', mouseup); onMyMouseup(); } element.on('mousedown', (event) => { onMyMousedown(); container = document.getElementById(attrs.id).getBoundingClientRect(); // Prevent default dragging of selected content event.preventDefault(); x = event.clientX - container.left; updateProgress(); $document.on('mousemove', mousemove); $document.on('mouseup', mouseup); }); }, ]); }; i18next.use(i18nextHttpBackend).init({ lng: 'zh-CN', fallbackLng: 'zh-CN', supportedLngs: ['zh-CN', 'zh-TC', 'en-US', 'fr-FR', 'ko-KR', 'pt-BR'], preload: ['zh-CN', 'zh-TC', 'en-US', 'fr-FR', 'ko-KR', 'pt-BR'], debug: false, backend: { loadPath: 'i18n/{{lng}}.json', }, }); main(); ================================================ FILE: js/background.js ================================================ /* eslint-disable no-unused-vars */ /* global GithubClient */ chrome.action.onClicked.addListener((tab) => { chrome.tabs.create( { url: chrome.runtime.getURL('listen1.html'), }, (new_tab) => { // Tab opened. } ); }); // const MOBILE_UA = // 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'; // function hack_referer_header(details) { // const replace_referer = true; // let replace_origin = true; // let add_referer = true; // let add_origin = true; // let referer_value = ''; // let origin_value = ''; // let ua_value = ''; // if (details.url.includes('://music.163.com/')) { // referer_value = 'https://music.163.com/'; // } // if (details.url.includes('://interface3.music.163.com/')) { // referer_value = 'https://music.163.com/'; // } // if (details.url.includes('://gist.githubusercontent.com/')) { // referer_value = 'https://gist.githubusercontent.com/'; // } // if (details.url.includes('.xiami.com/')) { // add_origin = false; // add_referer = false; // // referer_value = "https://www.xiami.com"; // } // if (details.url.includes('c.y.qq.com/')) { // referer_value = 'https://y.qq.com/'; // origin_value = 'https://y.qq.com'; // } // if ( // details.url.includes('i.y.qq.com/') || // details.url.includes('qqmusic.qq.com/') || // details.url.includes('music.qq.com/') || // details.url.includes('imgcache.qq.com/') // ) { // referer_value = 'https://y.qq.com/'; // } // if (details.url.includes('.kugou.com/')) { // referer_value = 'https://www.kugou.com/'; // ua_value = MOBILE_UA; // } // if (details.url.includes('m.kugou.com/')) { // ua_value = MOBILE_UA; // } // if (details.url.includes('.kuwo.cn/')) { // referer_value = 'https://www.kuwo.cn/'; // } // if ( // details.url.includes('.bilibili.com/') || // details.url.includes('.bilivideo.com/') // ) { // referer_value = 'https://www.bilibili.com/'; // replace_origin = false; // add_origin = false; // } // if (details.url.includes('.bilivideo.cn')) { // referer_value = 'https://www.bilibili.com/'; // origin_value = 'https://www.bilibili.com/'; // add_referer = true; // add_origin = true; // } // if ( // details.url.includes('.taihe.com/') || // details.url.includes('music.91q.com') // ) { // referer_value = 'https://music.taihe.com/'; // } // if (details.url.includes('.migu.cn')) { // referer_value = 'https://music.migu.cn/v3/music/player/audio?from=migu'; // } // if (details.url.includes('m.music.migu.cn')) { // referer_value = 'https://m.music.migu.cn/'; // } // if ( // details.url.includes('app.c.nf.migu.cn') || // details.url.includes('d.musicapp.migu.cn') // ) { // ua_value = MOBILE_UA; // add_origin = false; // add_referer = false; // } // if (details.url.includes('jadeite.migu.cn')) { // ua_value = 'okhttp/3.12.12'; // add_origin = false; // add_referer = false; // } // if (origin_value === '') { // origin_value = referer_value; // } // let isRefererSet = false; // let isOriginSet = false; // let isUASet = false; // const headers = details.requestHeaders; // const blockingResponse = {}; // for (let i = 0, l = headers.length; i < l; i += 1) { // if ( // replace_referer && // headers[i].name === 'Referer' && // referer_value !== '' // ) { // headers[i].value = referer_value; // isRefererSet = true; // } // if (replace_origin && headers[i].name === 'Origin' && origin_value !== '') { // headers[i].value = origin_value; // isOriginSet = true; // } // if (headers[i].name === 'User-Agent' && ua_value !== '') { // headers[i].value = ua_value; // isUASet = true; // } // } // if (add_referer && !isRefererSet && referer_value !== '') { // headers.push({ // name: 'Referer', // value: referer_value, // }); // } // if (add_origin && !isOriginSet && origin_value !== '') { // headers.push({ // name: 'Origin', // value: origin_value, // }); // } // if (!isUASet && ua_value !== '') { // headers.push({ // name: 'User-Agent', // value: ua_value, // }); // } // blockingResponse.requestHeaders = headers; // return blockingResponse; // } // const urls = [ // '*://*.music.163.com/*', // '*://music.163.com/*', // '*://*.xiami.com/*', // '*://i.y.qq.com/*', // '*://c.y.qq.com/*', // '*://*.kugou.com/*', // '*://*.kuwo.cn/*', // '*://*.bilibili.com/*', // '*://*.bilivideo.com/*', // '*://*.bilivideo.cn/*', // '*://*.migu.cn/*', // '*://*.githubusercontent.com/*', // ]; // try { // chrome.webRequest.onBeforeSendHeaders.addListener( // hack_referer_header, // { // urls, // }, // ['requestHeaders', 'blocking', 'extraHeaders'] // ); // } catch (err) { // // before chrome v72, extraHeader is not supported // chrome.webRequest.onBeforeSendHeaders.addListener( // hack_referer_header, // { // urls, // }, // ['requestHeaders', 'blocking'] // ); // } /** * Get tokens. */ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type !== 'code') { return; } GithubClient.github.handleCallback(request.code); sendResponse(); }); ================================================ FILE: js/bridge.js ================================================ /* eslint-disable no-unused-vars */ /* build a bridge between UI and audio player audio player has 2 modes, but share same protocol: front and background. * front: audio player and UI are in same environment * background: audio player is in background page. */ function getFrontPlayer() { return window.threadPlayer; } function getBackgroundPlayer() { return chrome.extension.getBackgroundPage().threadPlayer; } function getBackgroundPlayerAsync(callback) { (chrome || browser).runtime.getBackgroundPage((w) => { callback(w.threadPlayer); }); } function getPlayer(mode) { if (mode === 'front') { return getFrontPlayer(); } if (mode === 'background') { return getBackgroundPlayer(); } return undefined; } function getPlayerAsync(mode, callback) { if (mode === 'front') { const player = getFrontPlayer(); return callback(player); } if (mode === 'background') { return getBackgroundPlayerAsync(callback); } return undefined; } const frontPlayerListener = []; function addFrontPlayerListener(listener) { frontPlayerListener.push(listener); } function addBackgroundPlayerListener(listener) { return (chrome || browser).runtime.onMessage.addListener( (msg, sender, res) => { if (!msg.type.startsWith('BG_PLAYER:')) { return null; } return listener(msg, sender, res); } ); } function addPlayerListener(mode, listener) { if (mode === 'front') { return addFrontPlayerListener(listener); } if (mode === 'background') { return addBackgroundPlayerListener(listener); } return null; } function frontPlayerSendMessage(message) { if (frontPlayerListener !== []) { frontPlayerListener.forEach((listener) => { listener(message); }); } } function backgroundPlayerSendMessage(message) { (chrome || browser).runtime.sendMessage(message); } function playerSendMessage(mode, message) { if (mode === 'front') { frontPlayerSendMessage(message); } if (mode === 'background') { backgroundPlayerSendMessage(message); } } ================================================ FILE: js/controller/auth.js ================================================ /* eslint-disable import/no-unresolved */ /* eslint-disable global-require */ /* global angular MediaService isElectron require */ angular.module('listenone').controller('AuthController', [ '$scope', ($scope) => { $scope.loginProgress = false; $scope.loginType = 'email'; $scope.loginSourceList = MediaService.getLoginProviders().map( (i) => i.name ); $scope.refreshAuthStatus = () => { $scope.loginSourceList.map((source) => MediaService.getUser(source).success((data) => { if (data.status === 'success') { $scope.setMusicAuth(source, data.data); } else { $scope.setMusicAuth(source, {}); } }) ); }; $scope.logout = (source) => { $scope.setMusicAuth(source, {}); MediaService.logout(source); }; $scope.is_login = (source) => $scope.musicAuth[source] && $scope.musicAuth[source].is_login; $scope.musicAuth = {}; $scope.setMusicAuth = (source, data) => { $scope.musicAuth[source] = data; }; $scope.getLoginUrl = (source) => MediaService.getLoginUrl(source); $scope.openLogin = (source) => { const url = $scope.getLoginUrl(source); if (isElectron()) { const { ipcRenderer } = require('electron'); return ipcRenderer.send('openUrl', url); } return window.open(url, '_blank'); }; }, ]); ================================================ FILE: js/controller/instant_search.js ================================================ /* eslint-disable no-param-reassign */ /* global angular i18next MediaService sourceList */ angular.module('listenone').controller('InstantSearchController', [ '$scope', '$timeout', '$rootScope', ($scope, $timeout, $rootScope) => { $scope.originpagelog = { allmusic: 1 }; sourceList.forEach((i) => { $scope.originpagelog[i.name] = 1; }); $scope.sourceList = sourceList.filter((i) => i.searchable !== false); $scope.tab = sourceList[0].name; $scope.keywords = ''; $scope.loading = false; $scope.curpagelog = { ...$scope.originpagelog }; $scope.totalpagelog = { ...$scope.originpagelog }; $scope.curpage = 1; $scope.totalpage = 1; $scope.searchType = 0; function updateCurrentPage(cp) { if (cp === -1) { // when search words changes,pagenums should be reset. $scope.curpagelog = { ...$scope.originpagelog }; $scope.curpage = 1; } else if (cp >= 0) { $scope.curpagelog[$scope.tab] = cp; $scope.curpage = $scope.curpagelog[$scope.tab]; } else { // only tab changed $scope.curpage = $scope.curpagelog[$scope.tab]; } } function updateTotalPage(totalItem) { if (totalItem === -1) { $scope.totalpagelog = { ...$scope.originpagelog }; $scope.totalpage = 1; } else if (totalItem >= 0) { $scope.totalpage = Math.ceil(totalItem / 20); $scope.totalpagelog[$scope.tab] = $scope.totalpage; } else { // just switch tab $scope.totalpage = $scope.totalpagelog[$scope.tab]; } } function performSearch() { $rootScope.$broadcast('search:keyword_change', $scope.keywords); MediaService.search($scope.tab, { keywords: $scope.keywords, curpage: $scope.curpage, type: $scope.searchType, }).success((data) => { // update the textarea data.result.forEach((r) => { r.sourceName = i18next.t(r.source); }); $scope.result = data.result; updateTotalPage(data.total); $scope.loading = false; // scroll back to top when finish searching document.querySelector('.site-wrapper-innerd').scrollTo({ top: 0 }); }); } $scope.changeSourceTab = (newTab) => { $scope.loading = true; $scope.tab = newTab; $scope.result = []; updateCurrentPage(); updateTotalPage(); if ($scope.keywords === '') { $scope.loading = false; } else { performSearch(); } }; $scope.changeSearchType = (newSearchType) => { $scope.loading = true; $scope.searchType = newSearchType; $scope.result = []; updateCurrentPage(); updateTotalPage(); if ($scope.keywords === '') { $scope.loading = false; } else { performSearch(); } }; $scope.isActiveTab = (tab) => $scope.tab === tab; $scope.isSearchType = (searchType) => $scope.searchType === searchType; // eslint-disable-next-line consistent-return function renderSearchPage() { updateCurrentPage(-1); updateTotalPage(-1); if (!$scope.keywords || $scope.keywords.length === 0) { $scope.result = []; return 0; } performSearch(); } $scope.$watch('keywords', (tmpStr) => { if (tmpStr === $scope.keywords) { // if searchStr is still the same.. // go ahead and retrieve the data renderSearchPage(); } }); $scope.enterEvent = (e) => { const keycode = window.event ? e.keyCode : e.which; if (keycode === 13) { // enter key renderSearchPage(); } }; $scope.nextPage = () => { $scope.curpagelog[$scope.tab] += 1; $scope.curpage = $scope.curpagelog[$scope.tab]; performSearch(); }; $scope.previousPage = () => { $scope.curpagelog[$scope.tab] -= 1; $scope.curpage = $scope.curpagelog[$scope.tab]; performSearch(); }; }, ]); ================================================ FILE: js/controller/my_playlist.js ================================================ /* eslint-disable no-unused-vars */ /* global angular MediaService */ angular.module('listenone').controller('MyPlayListController', [ '$scope', '$timeout', ($scope, $timeout) => { $scope.myplaylists = []; $scope.favoriteplaylists = []; $scope.loadMyPlaylist = () => { MediaService.showMyPlaylist().success((data) => { $scope.$evalAsync(() => { $scope.myplaylists = data.result; }); }); }; $scope.loadFavoritePlaylist = () => { MediaService.showFavPlaylist().success((data) => { $scope.$evalAsync(() => { $scope.favoriteplaylists = data.result; }); }); }; $scope.$watch('current_tag', (newValue, oldValue) => { if (newValue !== oldValue) { if (newValue === '1') { $scope.myplaylists = []; $scope.loadMyPlaylist(); } } }); $scope.$on('myplaylist:update', (event, data) => { $scope.loadMyPlaylist(); }); $scope.$on('favoriteplaylist:update', (event, data) => { $scope.loadFavoritePlaylist(); }); }, ]); ================================================ FILE: js/controller/navigation.js ================================================ /* eslint-disable import/no-unresolved */ /* eslint-disable global-require */ /* eslint-disable no-shadow */ /* eslint-disable no-unused-vars */ /* eslint-disable no-param-reassign */ /* global angular notyf i18next MediaService l1Player hotkeys isElectron require GithubClient lastfm */ // control main view of page, it can be called any place angular.module('listenone').controller('NavigationController', [ '$scope', '$timeout', '$rootScope', ($scope, $timeout, $rootScope) => { $rootScope.page_title = { title: 'Listen 1', artist: '', status: '' }; // eslint-disable-line no-param-reassign $scope.window_url_stack = []; $scope.window_poped_url_stack = []; $scope.current_tag = 2; $scope.is_window_hidden = 1; $scope.is_dialog_hidden = 1; $scope.tag_params = {}; $scope.songs = []; $scope.current_list_id = -1; $scope.dialog_song = ''; $scope.dialog_type = 0; $scope.dialog_title = ''; $scope.isDoubanLogin = false; $scope.lastfm = lastfm; $scope.isOpenSidebar = true; $scope.$on('isdoubanlogin:update', (event, data) => { $scope.isDoubanLogin = data; }); // isOpenSidebar if (localStorage.getObject('openSidebar') !== null) { $scope.isOpenSidebar = localStorage.getObject('openSidebar'); } $scope.openSidebar = () => { $scope.isOpenSidebar = !$scope.isOpenSidebar; localStorage.setObject('openSidebar', $scope.isOpenSidebar); }; // tag $scope.showTag = (tag_id, tag_params) => { $scope.current_tag = tag_id; $scope.is_window_hidden = 1; $scope.window_url_stack = []; $scope.window_poped_url_stack = []; $scope.tag_params = tag_params; if (tag_id === 6) { $rootScope.$broadcast('myplatform:update', tag_params.user); } $scope.closeWindow(); }; $scope.$on('search:keyword_change', (event, data) => { $scope.showTag(3); }); // playlist window $scope.resetWindow = (offset) => { if (offset === undefined) { offset = 0; } $scope.cover_img_url = 'images/loading.svg'; $scope.playlist_title = ''; $scope.playlist_source_url = ''; $scope.songs = []; $scope.window_type = 'list'; $timeout(() => { document.getElementsByClassName('browser')[0].scrollTop = offset; }, 0); }; $scope.closeWindow = (offset) => { if (offset === undefined) { offset = 0; } $scope.is_window_hidden = 1; $scope.resetWindow(offset); $scope.window_url_stack = []; $scope.window_poped_url_stack = []; }; function refreshWindow(url, offset = 0) { if (url === '/now_playing') { $scope.window_type = 'track'; return; } const listId = new URL(url, window.location).searchParams.get('list_id'); MediaService.getPlaylist(listId).success((data) => { $scope.songs = data.tracks; $scope.list_id = data.info.id; $scope.cover_img_url = data.info.cover_img_url; $scope.playlist_title = data.info.title; $scope.playlist_source_url = data.info.source_url; $scope.is_mine = data.info.id.slice(0, 2) === 'my'; $scope.is_local = data.info.id.slice(0, 2) === 'lm'; $timeout(() => { document.getElementsByClassName('browser')[0].scrollTop = offset; }, 0); }); } $scope.popWindow = () => { if ($scope.window_url_stack.length === 0) { return; } let poped = $scope.window_url_stack.pop(); if ($scope.getCurrentUrl() === '/now_playing') { poped = $scope.window_url_stack.pop(); } $scope.window_poped_url_stack.push(poped.url); if ($scope.window_url_stack.length === 0) { $scope.closeWindow(poped.offset); } else { $scope.resetWindow(poped.offset); const lastWindow = $scope.window_url_stack.slice(-1)[0]; refreshWindow(lastWindow.url, poped.offset); } }; $scope.toggleNowPlaying = () => { if ($scope.getCurrentUrl() === '/now_playing') { $scope.popWindow(); return; } if (!$scope.menuHidden) { $scope.togglePlaylist(); } // save current scrolltop $scope.is_window_hidden = 0; $scope.resetWindow(); $scope.window_url_stack.push({ url: '/now_playing', offset: document.getElementsByClassName('browser')[0].scrollTop, }); $scope.window_poped_url_stack = []; $scope.window_type = 'track'; }; $scope.forwardWindow = () => { if ($scope.window_poped_url_stack.length === 0) { return; } $scope.resetWindow(); const url = $scope.window_poped_url_stack.pop(); $scope.window_url_stack.push({ url, offset: 0, }); refreshWindow(url); }; $scope.getCurrentUrl = () => ($scope.window_url_stack.slice(-1)[0] || {}).url; $scope.showPlaylist = (list_id, useCache) => { $scope.clearFilter(); const url = `/playlist?list_id=${list_id}`; // save current scrolltop const offset = document.getElementsByClassName('browser')[0].scrollTop; if ($scope.getCurrentUrl() === url) { return; } $scope.is_window_hidden = 0; $scope.resetWindow(); if ($scope.getCurrentUrl() === '/now_playing') { // if now playing is top, pop it $scope.window_url_stack.pop(); } $scope.window_url_stack.push({ url, offset }); $scope.window_poped_url_stack = []; const listId = new URL(url, window.location).searchParams.get('list_id'); MediaService.getPlaylist(listId, useCache).success((data) => { if (data.status === '0') { notyf.info(data.reason); $scope.popWindow(); return; } $scope.songs = data.tracks; $scope.cover_img_url = data.info.cover_img_url; $scope.playlist_title = data.info.title; $scope.playlist_source_url = data.info.source_url; $scope.list_id = data.info.id; $scope.is_mine = data.info.id.slice(0, 2) === 'my'; $scope.is_local = data.info.id.slice(0, 2) === 'lm'; MediaService.queryPlaylist(data.info.id, 'favorite').success((res) => { // success 函数可能在异步回调中执行,需要手动触发脏检查 $timeout(() => { $scope.is_favorite = res.result; }, 0); }); $scope.window_type = 'list'; }); }; $scope.directplaylist = (list_id) => { MediaService.getPlaylist(list_id).success((data) => { $scope.songs = data.tracks; $scope.current_list_id = list_id; l1Player.setNewPlaylist($scope.songs); l1Player.play(); }); }; $scope.showDialog = (dialog_type, data) => { $scope.is_dialog_hidden = 0; $scope.dialog_data = data; const dialogWidth = 400; const dialogHeight = 430; const left = window.innerWidth / 2 - dialogWidth / 2; const top = window.innerHeight / 2 - dialogHeight / 2; $scope.myStyle = { left: `${left}px`, top: `${top}px`, }; $scope.dialog_type = dialog_type; if (dialog_type === 0) { $scope.dialog_title = i18next.t('_ADD_TO_PLAYLIST'); $scope.dialog_song = data; MediaService.showMyPlaylist().success((res) => { $scope.myplaylist = res.result; }); } // if (dialog_type === 2) { // $scope.dialog_title = '登录豆瓣'; // $scope.dialog_type = 2; // } if (dialog_type === 3) { $scope.dialog_title = i18next.t('_EDIT_PLAYLIST'); $scope.dialog_cover_img_url = data.cover_img_url; $scope.dialog_playlist_title = data.playlist_title; } if (dialog_type === 4) { $scope.dialog_title = i18next.t('_CONNECT_TO_LASTFM'); } if (dialog_type === 5) { $scope.dialog_title = i18next.t('_OPEN_PLAYLIST'); } if (dialog_type === 6) { $scope.dialog_title = i18next.t('_IMPORT_PLAYLIST'); MediaService.showMyPlaylist().success((res) => { $scope.myplaylist = res.result; }); } if (dialog_type === 7) { $scope.dialog_title = i18next.t('_CONNECT_TO_GITHUB'); } if (dialog_type === 8) { $scope.dialog_title = i18next.t('_EXPORT_TO_GITHUB_GIST'); GithubClient.gist.listExistBackup().then( (res) => { $scope.myBackup = res; }, (err) => { $scope.myBackup = []; } ); } if (dialog_type === 10) { $scope.dialog_title = i18next.t('_RECOVER_FROM_GITHUB_GIST'); GithubClient.gist.listExistBackup().then( (res) => { $scope.myBackup = res; }, (err) => { $scope.myBackup = []; } ); } if (dialog_type === 11) { $scope.dialog_title = i18next.t('_LOGIN'); } if (dialog_type === 12) { $scope.dialog_title = i18next.t('_PROXY_CONFIG'); } }; $scope.onSidebarPlaylistDrop = ( playlistType, list_id, data, dataType, direction ) => { if (playlistType === 'my' && dataType === 'application/listen1-song') { $scope.addMyPlaylist(list_id, data); } else if ( (playlistType === 'my' && dataType === 'application/listen1-myplaylist') || (playlistType === 'favorite' && dataType === 'application/listen1-favoriteplaylist') ) { MediaService.insertMyplaylistToMyplaylists( playlistType, data.info.id, list_id, direction ).success(() => { if (playlistType === 'my') { $rootScope.$broadcast('myplaylist:update'); } if (playlistType === 'favorite') { $rootScope.$broadcast('favoriteplaylist:update'); } }); } }; $scope.playlistFilter = { key: '' }; $scope.clearFilter = () => { $scope.playlistFilter.key = ''; }; $scope.fieldFilter = (song) => { if ($scope.playlistFilter.key === '') { return true; } return ( song.title.includes($scope.playlistFilter.key) || song.artist.includes($scope.playlistFilter.key) || (song.album && song.album.includes($scope.playlistFilter.key)) ); }; $scope.onPlaylistSongDrop = (list_id, song, data, dataType, direction) => { if (dataType === 'application/listen1-song') { // insert song MediaService.insertTrackToMyPlaylist( list_id, data, song, direction ).success((playlist) => { $scope.closeDialog(); if (list_id === $scope.list_id) { $scope.$evalAsync(() => { $scope.songs = playlist.tracks; }); } }); } }; $scope.onCurrentPlayingSongDrop = (song, data, dataType, direction) => { if (dataType === 'application/listen1-song') { l1Player.insertTrack(data, song, direction); } }; $scope.playById = (id) => { l1Player.playById(id); }; $scope.addAndPlay = (song) => { l1Player.addTrack(song); l1Player.playById(song.id); }; $scope.addMyPlaylist = (option_id, song) => { MediaService.addMyPlaylist(option_id, song).success((playlist) => { notyf.success(i18next.t('_ADD_TO_PLAYLIST_SUCCESS')); $scope.closeDialog(); // add to current playing list if (option_id === $scope.current_list_id) { l1Player.addTrack($scope.dialog_song); } if (option_id === $scope.list_id) { $scope.songs = playlist.tracks; } }); }; $scope.chooseDialogOption = (option_id) => { $scope.addMyPlaylist(option_id, $scope.dialog_song); }; $scope.newDialogOption = (option) => { $scope.dialog_type = option; }; $scope.cancelNewDialog = (option) => { $scope.dialog_type = option; }; $scope.createAndAddPlaylist = () => { MediaService.createMyPlaylist( $scope.newlist_title, $scope.dialog_song ).success(() => { $rootScope.$broadcast('myplaylist:update'); notyf.success(i18next.t('_ADD_TO_PLAYLIST_SUCCESS')); $scope.closeDialog(); }); }; $scope.editMyPlaylist = () => { MediaService.editMyPlaylist( $scope.list_id, $scope.dialog_playlist_title, $scope.dialog_cover_img_url ).success(() => { $rootScope.$broadcast('myplaylist:update'); $scope.playlist_title = $scope.dialog_playlist_title; $scope.cover_img_url = $scope.dialog_cover_img_url; notyf.success(i18next.t('_EDIT_PLAYLIST_SUCCESS')); $scope.closeDialog(); }); }; $scope.mergePlaylist = (target_list_id) => { notyf.info(i18next.t('_IMPORTING_PLAYLIST')); MediaService.mergePlaylist($scope.list_id, target_list_id).success(() => { notyf.success(i18next.t('_IMPORTING_PLAYLIST_SUCCESS')); $scope.closeDialog(); $scope.popWindow(); $scope.showPlaylist($scope.list_id); }); }; $scope.removeSongFromPlaylist = (song, list_id) => { let removeFunc = null; if (list_id.slice(0, 2) === 'my') { removeFunc = MediaService.removeTrackFromMyPlaylist; } else if (list_id.slice(0, 2) === 'lm') { removeFunc = MediaService.removeTrackFromPlaylist; } removeFunc(list_id, song.id).success(() => { // remove song from songs const index = $scope.songs.indexOf(song); if (index > -1) { $scope.songs.splice(index, 1); } notyf.success(i18next.t('_REMOVE_SONG_FROM_PLAYLIST_SUCCESS')); }); }; $scope.closeDialog = () => { $scope.is_dialog_hidden = 1; $scope.dialog_type = 0; // update lastfm status if not authorized if (lastfm.isAuthRequested()) { lastfm.updateStatus(); } }; $scope.setCurrentList = (list_id) => { $scope.current_list_id = list_id; }; $scope.playMylist = (list_id) => { l1Player.setNewPlaylist($scope.songs); l1Player.play(); $scope.setCurrentList(list_id); }; $scope.addMylist = (list_id) => { $timeout(() => { // add songs to playlist l1Player.addTracks($scope.songs); notyf.success(i18next.t('_ADD_TO_QUEUE_SUCCESS')); }, 0); }; $scope.clonePlaylist = (list_id) => { MediaService.clonePlaylist(list_id, 'my').success(() => { $rootScope.$broadcast('myplaylist:update'); $scope.closeWindow(); notyf.success(i18next.t('_ADD_TO_PLAYLIST_SUCCESS')); }); }; $scope.removeMyPlaylist = (list_id) => { MediaService.removeMyPlaylist(list_id, 'my').success(() => { $rootScope.$broadcast('myplaylist:update'); $scope.closeDialog(); $scope.closeWindow(); notyf.success(i18next.t('_REMOVE_PLAYLIST_SUCCESS')); }); }; $scope.downloadFile = (fileName, fileType, content) => { window.URL = window.URL || window.webkitURL; const blob = new Blob([content], { type: fileType, }); const link = document.createElement('a'); link.download = fileName; link.href = window.URL.createObjectURL(blob); link.style.display = 'none'; document.body.appendChild(link); link.click(); link.remove(); }; $scope.backupMySettings = () => { const items = {}; Object.keys(localStorage).forEach((key) => { items[key] = localStorage.getObject(key); }); const content = JSON.stringify(items); $scope.downloadFile('listen1_backup.json', 'application/json', content); }; $scope.importMySettings = (event) => { const fileObject = event.target.files[0]; if (fileObject === null) { notyf.warning('请选择备份文件'); return; } const reader = new FileReader(); reader.onloadend = (readerEvent) => { if (readerEvent.target.readyState === FileReader.DONE) { const data_json = readerEvent.target.result; // parse json let data = null; try { data = JSON.parse(data_json); } catch (e) { notyf.warning('备份文件格式错误,请重新选择'); return; } Object.keys(data).forEach((item) => localStorage.setObject(item, data[item]) ); $rootScope.$broadcast('myplaylist:update'); notyf.success('成功导入我的歌单'); } }; reader.readAsText(fileObject); }; $scope.gistBackupLoading = false; $scope.backupMySettings2Gist = (gistId, isPublic) => { const items = {}; Object.keys(localStorage).forEach((key) => { if (key !== 'gistid' && key !== 'githubOauthAccessKey') { // avoid token leak items[key] = localStorage.getObject(key); } }); const gistFiles = GithubClient.gist.json2gist(items); $scope.gistBackupLoading = true; GithubClient.gist.backupMySettings2Gist(gistFiles, gistId, isPublic).then( () => { notyf.dismissAll(); notyf.success('成功导出我的歌单到Gist'); $scope.gistBackupLoading = false; }, (err) => { notyf.dismissAll(); notyf.warning('导出我的歌单失败,检查后重试'); $scope.gistBackupLoading = false; } ); notyf.info('正在导出我的歌单到Gist...'); }; $scope.gistRestoreLoading = false; $scope.importMySettingsFromGist = (gistId) => { $scope.gistRestoreLoading = true; GithubClient.gist.importMySettingsFromGist(gistId).then( (raw) => { GithubClient.gist.gist2json(raw, (data) => { Object.keys(data).forEach((item) => localStorage.setObject(item, data[item]) ); notyf.dismissAll(); notyf.success('导入我的歌单成功'); $scope.gistRestoreLoading = false; $rootScope.$broadcast('myplaylist:update'); }); }, (err) => { notyf.dismissAll(); if (err === 404) { notyf.warning('未找到备份歌单,请先备份'); } else { notyf.warning('导入我的歌单失败,检查后重试'); } $scope.gistRestoreLoading = false; } ); notyf.info('正在从Gist导入我的歌单...'); }; $scope.showShortcuts = () => {}; // description: '快速搜索', hotkeys('f', () => { $scope.showTag(3); $timeout(() => { document.getElementById('search-input').focus(); }, 0); }); $scope.openUrl = (url) => { MediaService.parseURL(url).success((data) => { const { result } = data; if (result !== undefined) { $scope.showPlaylist(result.id); } else { notyf.info(i18next.t('_FAIL_OPEN_PLAYLIST_URL')); } }); }; $scope.favoritePlaylist = (list_id) => { if ($scope.is_favorite) { $scope.removeFavoritePlaylist(list_id); $scope.is_favorite = 0; } else { $scope.addFavoritePlaylist(list_id); $scope.is_favorite = 1; } }; $scope.addFavoritePlaylist = (list_id) => { MediaService.clonePlaylist(list_id, 'favorite').success((addResult) => { $rootScope.$broadcast('favoriteplaylist:update'); notyf.success(i18next.t('_FAVORITE_PLAYLIST_SUCCESS')); }); }; $scope.removeFavoritePlaylist = (list_id) => { MediaService.removeMyPlaylist(list_id, 'favorite').success(() => { $rootScope.$broadcast('favoriteplaylist:update'); // $scope.closeWindow(); notyf.success(i18next.t('_UNFAVORITE_PLAYLIST_SUCCESS')); }); }; $scope.addLocalMusic = (list_id) => { if (isElectron()) { const remote = require('@electron/remote'); const remoteFunctions = remote.require('./functions.js'); remote.dialog .showOpenDialog({ title: '添加歌曲', properties: ['openFile', 'multiSelections'], filters: [ { name: 'Music Files', extensions: ['flac', 'mp3', 'mp4', 'ogg', 'wav', 'webm'], }, ], }) .then((result) => { if (result.canceled) { return; } result.filePaths.forEach((fp) => { remoteFunctions.readAudioTags(fp).then((md) => { const track = { id: `lmtrack_${fp}`, title: md.common.title, artist: md.common.artist, artist_id: `lmartist_${md.common.artist}`, album: md.common.album, album_id: `lmalbum_${md.common.album}`, source: 'localmusic', source_url: '', img_url: '', lyrics: md.common.lyrics, // url: "lmtrack_"+fp, sound_url: `file://${fp}`, }; const list_id = 'lmplaylist_reserve'; MediaService.addPlaylist(list_id, [track]).success((res) => { const { playlist } = res; $scope.songs = playlist.tracks; $scope.list_id = playlist.info.id; $scope.cover_img_url = playlist.info.cover_img_url; $scope.playlist_title = playlist.info.title; $scope.playlist_source_url = playlist.info.source_url; $scope.is_mine = playlist.info.id.slice(0, 2) === 'my'; $scope.is_local = playlist.info.id.slice(0, 2) === 'lm'; $scope.$evalAsync(); }); }); }); }) .catch((err) => { // console.log(err); }); } }; }, ]); ================================================ FILE: js/controller/platform.js ================================================ /* global angular MediaService */ const platformSourceList = [ { name: 'my_created_playlist', displayId: '_MY_CREATED_PLAYLIST', }, { name: 'my_favorite_playlist', displayId: '_MY_FAVORITE_PLAYLIST', }, { name: 'recommend_playlist', displayId: '_RECOMMEND_PLAYLIST', }, ]; angular.module('listenone').controller('PlatformController', [ '$scope', ($scope) => { $scope.myPlatformPlaylists = []; $scope.myPlatformUser = {}; $scope.platformSourceList = platformSourceList; $scope.tab = platformSourceList[0].name; $scope.loadPlatformPlaylists = () => { if ($scope.myPlatformUser.platform === undefined) { return; } let getPlaylistFn = MediaService.getUserCreatedPlaylist; if ($scope.tab === 'recommend_playlist') { getPlaylistFn = MediaService.getRecommendPlaylist; } else if ($scope.tab === 'my_favorite_playlist') { getPlaylistFn = MediaService.getUserFavoritePlaylist; } const user = $scope.myPlatformUser; getPlaylistFn(user.platform, { user_id: user.user_id, }).success((response) => { const { data } = response; $scope.myPlatformPlaylists = data.playlists; }); }; $scope.initPlatformController = (user) => { $scope.tab = platformSourceList[0].name; $scope.myPlatformUser = user; $scope.loadPlatformPlaylists(); }; $scope.$on('myplatform:update', (event, user) => { $scope.initPlatformController(user); }); $scope.changeTab = (name) => { $scope.tab = name; $scope.loadPlatformPlaylists(); }; }, ]); ================================================ FILE: js/controller/play.js ================================================ /* eslint-disable no-param-reassign */ /* eslint-disable no-shadow */ /* eslint-disable import/no-unresolved */ /* eslint-disable global-require */ /* global angular notyf i18next MediaService l1Player hotkeys GithubClient isElectron require getLocalStorageValue getPlayer getPlayerAsync addPlayerListener smoothScrollTo lastfm */ function getCSSStringFromSetting(setting) { let { backgroundAlpha } = setting; if (backgroundAlpha === 0) { // NOTE: background alpha 0 results total transparent // which will cause mouse leave event not trigger // correct in windows platform for lyic window if disable // hardware accelerate backgroundAlpha = 0.01; } return `div.content.lyric-content{ font-size: ${setting.fontSize}px; color: ${setting.color}; background: rgba(36, 36, 36, ${backgroundAlpha}); } div.content.lyric-content span.contentTrans { font-size: ${setting.fontSize - 4}px; } `; } function useModernTheme() { const defaultTheme = localStorage.getObject('theme'); return defaultTheme === 'white2' || defaultTheme === 'black2'; } function getSafeIndex(index, length) { if (index < 0) { const r = index % length; if (r < 0) { return length + (index % length); } return r; } if (index > length - 1) { return index % length; } return index; } function formatSecond(posSec) { return `${Math.floor(posSec / 60)}:${`0${posSec % 60}`.slice(-2)}`; } angular.module('listenone').controller('PlayController', [ '$scope', '$timeout', '$log', '$anchorScroll', '$location', '$rootScope', ($scope, $timeout, $log, $anchorScroll, $location, $rootScope) => { $scope.menuHidden = true; $scope.volume = l1Player.status.volume; $scope.mute = l1Player.status.muted; $scope.settings = { playmode: 0, nowplaying_track_id: -1, }; $scope.lyricArray = []; $scope.lyricLineNumber = -1; $scope.lastTrackId = null; $scope.enableGloablShortcut = false; $scope.isChrome = !isElectron(); $scope.isMac = false; $scope.currentDuration = '0:00'; $scope.currentDurationSeconds = 0; $scope.currentPosition = '0:00'; $scope.currentIndex = 0; $scope.staged_playlist = []; $scope.getSongIdByIndex = (index) => { const songId = $scope.playlist[getSafeIndex(index, $scope.playlist.length)].id; return `${songId}_${index}`; }; $scope.refreshStage = () => { if ($scope.playlist === undefined) { return; } const STAGED_LENGTH = 5; let i = $scope.currentIndex - 2; $scope.staged_playlist = []; while ($scope.staged_playlist.length < STAGED_LENGTH) { const song = $scope.playlist[getSafeIndex(i, $scope.playlist.length)]; if (!song) { break; } $scope.staged_playlist.push({ ...song, stageId: `${song.id}_${i}` }); i += 1; } }; if (!$scope.isChrome) { // eslint-disable-next-line no-undef $scope.isMac = process.platform === 'darwin'; } function switchMode(mode) { // playmode 0:loop 1:shuffle 2:repeat one switch (mode) { case 0: l1Player.setLoopMode('all'); break; case 1: l1Player.setLoopMode('shuffle'); break; case 2: l1Player.setLoopMode('one'); break; default: } } $scope.loadLocalSettings = () => { const defaultSettings = { playmode: 0, nowplaying_track_id: -1, volume: 90, }; const localSettings = localStorage.getObject('player-settings'); if (localSettings === null) { $scope.settings = defaultSettings; $scope.saveLocalSettings(); } else { $scope.settings = localSettings; } // apply settings switchMode($scope.settings.playmode); $scope.volume = $scope.settings.volume; if ($scope.volume === null) { $scope.volume = 90; $scope.saveLocalSettings(); } else { l1Player.setVolume($scope.volume); } $scope.enableGlobalShortCut = localStorage.getObject( 'enable_global_shortcut' ); $scope.enableLyricFloatingWindow = localStorage.getObject( 'enable_lyric_floating_window' ); $scope.enableLyricTranslation = localStorage.getObject( 'enable_lyric_translation' ); $scope.enableLyricFloatingWindowTranslation = localStorage.getObject( 'enable_lyric_floating_window_translation' ); $scope.enableAutoChooseSource = getLocalStorageValue( 'enable_auto_choose_source', true ); $scope.autoChooseSourceList = getLocalStorageValue( 'auto_choose_source_list', ['kuwo', 'qq', 'migu'] ); $scope.enableStopWhenClose = isElectron() || getLocalStorageValue('enable_stop_when_close', true); $scope.enableNowplayingCoverBackground = getLocalStorageValue( 'enable_nowplaying_cover_background', false ); $scope.enableNowplayingBitrate = getLocalStorageValue( 'enable_nowplaying_bitrate', false ); $scope.enableNowplayingPlatform = getLocalStorageValue( 'enable_nowplaying_platform', false ); const defaultFloatWindowSetting = { fontSize: 20, color: '#ffffff', backgroundAlpha: 0.2, }; $scope.floatWindowSetting = getLocalStorageValue( 'float_window_setting', defaultFloatWindowSetting ); $scope.applyGlobalShortcut(); $scope.openLyricFloatingWindow(); }; // electron global shortcuts $scope.applyGlobalShortcut = (toggle) => { if (!isElectron()) { return; } let message = ''; if (toggle === true) { $scope.enableGlobalShortCut = !$scope.enableGlobalShortCut; } if ($scope.enableGlobalShortCut === true) { message = 'enable_global_shortcut'; } else { message = 'disable_global_shortcut'; } // check if globalShortcuts is allowed localStorage.setObject( 'enable_global_shortcut', $scope.enableGlobalShortCut ); const { ipcRenderer } = require('electron'); ipcRenderer.send('control', message); }; $scope.openLyricFloatingWindow = (toggle) => { if (!isElectron()) { return; } let message = ''; if (toggle === true) { $scope.enableLyricFloatingWindow = !$scope.enableLyricFloatingWindow; } if ($scope.enableLyricFloatingWindow === true) { message = 'enable_lyric_floating_window'; } else { message = 'disable_lyric_floating_window'; } localStorage.setObject( 'enable_lyric_floating_window', $scope.enableLyricFloatingWindow ); const { ipcRenderer } = require('electron'); ipcRenderer.send( 'control', message, getCSSStringFromSetting($scope.floatWindowSetting) ); }; if (isElectron()) { const { webFrame, ipcRenderer } = require('electron'); // webFrame.setVisualZoomLevelLimits(1, 3); ipcRenderer.on('setZoomLevel', (event, level) => { webFrame.setZoomLevel(level); }); ipcRenderer.on('lyricWindow', (event, arg) => { if (arg === 'float_window_close') { $scope.openLyricFloatingWindow(true); } else if ( arg === 'float_window_font_small' || arg === 'float_window_font_large' ) { const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 50; const offset = arg === 'float_window_font_small' ? -1 : 1; $scope.floatWindowSetting.fontSize += offset; if ($scope.floatWindowSetting.fontSize < MIN_FONT_SIZE) { $scope.floatWindowSetting.fontSize = MIN_FONT_SIZE; } else if ($scope.floatWindowSetting.fontSize > MAX_FONT_SIZE) { $scope.floatWindowSetting.fontSize = MAX_FONT_SIZE; } } else if ( arg === 'float_window_background_light' || arg === 'float_window_background_dark' ) { const MIN_BACKGROUND_ALPHA = 0; const MAX_BACKGROUND_ALPHA = 1; const offset = arg === 'float_window_background_light' ? -0.1 : 0.1; $scope.floatWindowSetting.backgroundAlpha += offset; if ( $scope.floatWindowSetting.backgroundAlpha < MIN_BACKGROUND_ALPHA ) { $scope.floatWindowSetting.backgroundAlpha = MIN_BACKGROUND_ALPHA; } else if ( $scope.floatWindowSetting.backgroundAlpha > MAX_BACKGROUND_ALPHA ) { $scope.floatWindowSetting.backgroundAlpha = MAX_BACKGROUND_ALPHA; } } else if (arg === 'float_window_font_change_color') { const floatWindowlyricColors = [ '#ffffff', '#65d29f', '#3c87eb', '#ec63af', '#4f5455', '#eb605b', ]; const currentIndex = floatWindowlyricColors.indexOf( $scope.floatWindowSetting.color ); const nextIndex = (currentIndex + 1) % floatWindowlyricColors.length; $scope.floatWindowSetting.color = floatWindowlyricColors[nextIndex]; } localStorage.setObject( 'float_window_setting', $scope.floatWindowSetting ); const { ipcRenderer } = require('electron'); const message = 'update_lyric_floating_window_css'; ipcRenderer.send( 'control', message, getCSSStringFromSetting($scope.floatWindowSetting) ); }); } $scope.saveLocalSettings = () => { localStorage.setObject('player-settings', $scope.settings); }; $scope.changePlaymode = () => { const playmodeCount = 3; $scope.settings.playmode = ($scope.settings.playmode + 1) % playmodeCount; switchMode($scope.settings.playmode); $scope.saveLocalSettings(); }; $rootScope.openGithubAuth = GithubClient.github.openAuthUrl; $rootScope.GithubLogout = () => { GithubClient.github.logout(); $scope.$evalAsync(() => { $scope.githubStatus = 0; $scope.githubStatusText = GithubClient.github.getStatusText(); }); }; $rootScope.updateGithubStatus = () => { GithubClient.github.updateStatus((data) => { $scope.$evalAsync(() => { $scope.githubStatus = data; $scope.githubStatusText = GithubClient.github.getStatusText(); }); }); }; $scope.togglePlaylist = () => { const anchor = `song${l1Player.status.playing.id}`; $scope.menuHidden = !$scope.menuHidden; if (!$scope.menuHidden) { $anchorScroll(anchor); } }; $scope.toggleMuteStatus = () => { // mute function is indeed toggle mute status. l1Player.toggleMute(); }; $scope.myProgress = 0; $scope.changingProgress = false; $scope.copyrightNotice = () => { notyf.info(i18next.t('_COPYRIGHT_ISSUE'), true); }; $scope.failAllNotice = () => { notyf.warning(i18next.t('_FAIL_ALL_NOTICE'), true); }; $rootScope.$on('dragbar:myprogress', (event, data) => { $scope.$evalAsync(() => { // should use apply to force refresh ui $scope.myProgress = data; const posSec = Math.floor( ($scope.currentDurationSeconds * $scope.myProgress) / 100 ); const posStr = formatSecond(posSec); $scope.currentPosition = posStr; }); }); $rootScope.$on('dragbar:changing_progress', (event, data) => { $scope.$evalAsync(() => { // should use apply to force refresh ui $scope.changingProgress = data; }); }); function parseLyric(lyric, tlyric) { const lines = lyric.split('\n'); let result = []; const timeResult = []; if (typeof tlyric !== 'string') { tlyric = ''; } const linesTrans = tlyric.split('\n'); const resultTrans = []; const timeResultTrans = []; if (tlyric === '') { linesTrans.splice(0); } function rightPadding(str, length, padChar) { const newstr = str + new Array(length - str.length + 1).join(padChar); return newstr; } const process = (result, timeResult, translationFlag) => (line, index) => { const tagReg = /\[\D*:([^\]]+)\]/g; const tagRegResult = tagReg.exec(line); if (tagRegResult) { const lyricObject = {}; lyricObject.seconds = 0; [lyricObject.content] = tagRegResult; result.push(lyricObject); return; } const timeReg = /\[(\d{2,})\:(\d{2})(?:\.(\d{1,3}))?\]/g; // eslint-disable-line no-useless-escape let timeRegResult = null; // eslint-disable-next-line no-cond-assign while ((timeRegResult = timeReg.exec(line)) !== null) { const htmlUnescapes = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ''': "'", }; timeResult.push({ content: line .replace(/\[(\d{2,}):(\d{2})(?:\.(\d{1,3}))?\]/g, '') .replace( /&(?:amp|lt|gt|quot|#39|apos);/g, (match) => htmlUnescapes[match] ), seconds: parseInt(timeRegResult[1], 10) * 60 * 1000 + // min parseInt(timeRegResult[2], 10) * 1000 + // sec (timeRegResult[3] ? parseInt(rightPadding(timeRegResult[3], 3, '0'), 10) : 0), // microsec translationFlag, index, }); } }; lines.forEach(process(result, timeResult, false)); linesTrans.forEach(process(resultTrans, timeResultTrans, true)); // sort time line result = timeResult.concat(timeResultTrans).sort((a, b) => { const keyA = a.seconds; const keyB = b.seconds; // Compare the 2 dates if (keyA < keyB) return -1; if (keyA > keyB) return 1; if (a.translationFlag !== b.translationFlag) { if (a.translationFlag === false) { return -1; } return 1; } if (a.index < b.index) return -1; if (a.index > b.index) return 1; return 0; }); // disable tag info, because music provider always write // tag info in lyric timeline. // result.push.apply(result, timeResult); // result = timeResult; // executed up there for (let i = 0; i < result.length; i += 1) { result[i].lineNumber = i; } return result; } const mode = isElectron() || getLocalStorageValue('enable_stop_when_close', true) ? 'front' : 'background'; getPlayer(mode).setMode(mode); if (mode === 'front') { if (!isElectron()) { // avoid background keep playing when change to front mode getPlayerAsync('background', (player) => { player.pause(); }); } } addPlayerListener(mode, (msg, sender, sendResponse) => { if ( typeof msg.type === 'string' && msg.type.split(':')[0] === 'BG_PLAYER' ) { switch (msg.type.split(':').slice(1).join('')) { case 'READY': { break; } case 'PLAY_FAILED': { notyf.info(i18next.t('_COPYRIGHT_ISSUE'), true); break; } case 'VOLUME': { $scope.$evalAsync(() => { $scope.volume = msg.data; }); break; } case 'FRAME_UPDATE': { // 'currentTrack:position' // update lyric position if (!l1Player.status.playing.id) break; const currentSeconds = msg.data.pos; let lastObject = null; let lastObjectTrans = null; $scope.lyricArray.forEach((lyric) => { if (currentSeconds >= lyric.seconds / 1000) { if (lyric.translationFlag !== true) { lastObject = lyric; } else { lastObjectTrans = lyric; } } }); if ( lastObject && lastObject.lineNumber !== $scope.lyricLineNumber ) { const lineElement = document.querySelector( `.playsong-detail .detail-songinfo .lyric p[data-line="${lastObject.lineNumber}"]` ); let windowHeight = document.querySelector( '.playsong-detail .detail-songinfo .lyric' ).offsetHeight; if (useModernTheme()) { windowHeight = document.querySelector('body').offsetHeight - 100; } const adjustOffset = 30; const offset = lineElement.offsetTop - windowHeight / 2 + adjustOffset; smoothScrollTo(document.querySelector('.lyric'), offset, 500); $scope.lyricLineNumber = lastObject.lineNumber; if ( lastObjectTrans && lastObjectTrans.lineNumber !== $scope.lyricLineNumberTrans ) { $scope.lyricLineNumberTrans = lastObjectTrans.lineNumber; } if (isElectron()) { const { ipcRenderer } = require('electron'); const currentLyric = $scope.lyricArray[lastObject.lineNumber].content; let currentLyricTrans = ''; if ( $scope.enableLyricFloatingWindowTranslation === true && lastObjectTrans ) { currentLyricTrans = $scope.lyricArray[lastObjectTrans.lineNumber].content; } ipcRenderer.send('currentLyric', { lyric: currentLyric, tlyric: currentLyricTrans, }); } } // 'currentTrack:duration' (() => { const durationSec = Math.floor(msg.data.duration); const durationStr = `${Math.floor(durationSec / 60)}:${`0${ durationSec % 60 }`.substr(-2)}`; if ( msg.data.duration === 0 || $scope.currentDuration === durationStr ) { return; } $scope.currentDuration = durationStr; $scope.currentDurationSeconds = msg.data.duration; })(); // 'track:progress' if ($scope.changingProgress === false) { $scope.$evalAsync(() => { if (msg.data.duration === 0) { $scope.myProgress = 0; } else { $scope.myProgress = (msg.data.pos / msg.data.duration) * 100; } const posSec = Math.floor(msg.data.pos); const posStr = formatSecond(posSec); $scope.currentPosition = posStr; }); } break; } case 'LOAD': { $scope.currentPlaying = msg.data.currentPlaying; const { length, index } = msg.data.playlist; if (useModernTheme()) { $scope.currentIndex = index; $scope.refreshStage(index, length); } if (useModernTheme()) { const rotatemark = document.getElementById('rotatemark'); const circlmark = document.getElementById('circlmark'); if (rotatemark !== null && circlmark !== null) { circlmark.classList.add('circlmark'); rotatemark.classList.add('rotatemark'); circlmark.addEventListener('animationend', () => { circlmark.classList.remove('circlmark'); }); rotatemark.addEventListener('animationend', () => { rotatemark.classList.remove('rotatemark'); }); } } if (msg.data.currentPlaying.id === undefined) { break; } $scope.currentPlaying.platformText = i18next.t( $scope.currentPlaying.platform ); $scope.myProgress = 0; if ($scope.lastTrackId === msg.data.currentPlaying.id) { break; } const current = localStorage.getObject('player-settings') || {}; current.nowplaying_track_id = msg.data.currentPlaying.id; localStorage.setObject('player-settings', current); // update lyric $scope.lyricArray = []; $scope.lyricLineNumber = -1; $scope.lyricLineNumberTrans = -1; smoothScrollTo(document.querySelector('.lyric'), 0, 300); const track = msg.data.currentPlaying; $rootScope.page_title = { title: track.title, artist: track.artist, status: 'playing', }; if (lastfm.isAuthorized()) { lastfm.sendNowPlaying(track.title, track.artist, () => {}); } MediaService.getLyric( msg.data.currentPlaying.id, msg.data.currentPlaying.album_id, track.lyric_url, track.tlyric_url ).success((res) => { const { lyric, tlyric } = res; if (!lyric) { return; } $scope.lyricArray = parseLyric(lyric, tlyric); }); $scope.lastTrackId = msg.data.currentPlaying.id; if (isElectron()) { const { ipcRenderer } = require('electron'); ipcRenderer.send('currentLyric', track.title); ipcRenderer.send('trackPlayingNow', track); } break; } case 'MUTE': { // 'music:mute' $scope.$evalAsync(() => { $scope.mute = msg.data; }); break; } case 'PLAYLIST': { // 'player:playlist' $scope.$evalAsync(() => { $scope.playlist = msg.data; $scope.refreshStage(); localStorage.setObject('current-playing', msg.data); }); break; } case 'PLAY_STATE': { // 'music:isPlaying' $scope.$evalAsync(() => { $scope.isPlaying = !!msg.data.isPlaying; }); let title = 'Listen 1'; if ($rootScope.page_title !== undefined) { title = ''; if (msg.data.isPlaying) { $rootScope.page_title.status = 'playing'; } else { $rootScope.page_title.status = 'paused'; } if ($rootScope.page_title.status !== '') { if ($rootScope.page_title.status === 'playing') { title += '▶ '; } else if ($rootScope.page_title.status === 'paused') { title += '❚❚ '; } } title += $rootScope.page_title.title; if ($rootScope.page_title.artist !== '') { title += ` - ${$rootScope.page_title.artist}`; } } $rootScope.document_title = title; if (isElectron()) { const { ipcRenderer } = require('electron'); if (msg.data.isPlaying) { ipcRenderer.send('isPlaying', true); } else { ipcRenderer.send('isPlaying', false); } } if (msg.data.reason === 'Ended') { if (!lastfm.isAuthorized()) { break; } // send lastfm scrobble const track = l1Player.getTrackById(l1Player.status.playing.id); lastfm.scrobble( l1Player.status.playing.playedFrom, track.title, track.artist, track.album, () => {} ); } break; } case 'RETRIEVE_URL_SUCCESS': { $scope.currentPlaying = msg.data; // update translate whenever set value $scope.currentPlaying.platformText = i18next.t( $scope.currentPlaying.platform ); break; } case 'RETRIEVE_URL_FAIL': { $scope.copyrightNotice(); break; } case 'RETRIEVE_URL_FAIL_ALL': { $scope.failAllNotice(); break; } default: break; } } if (sendResponse !== undefined) { sendResponse(); } }); // connect player should run after all addListener function finished l1Player.connectPlayer(); // define keybind // description: '播放/暂停', hotkeys('p', l1Player.togglePlayPause); // description: '上一首', hotkeys('[', l1Player.prev); // description: '下一首', hotkeys(']', l1Player.next); // description: '静音/取消静音', hotkeys('m', l1Player.toggleMute); // description: '打开/关闭播放列表', hotkeys('l', $scope.togglePlaylist); // description: '切换播放模式(顺序/随机/单曲循环)', hotkeys('s', $scope.changePlaymode); // description: '音量增加', hotkeys('u', () => { $timeout(() => { l1Player.adjustVolume(true); }); }); // description: '音量减少', hotkeys('d', () => { $timeout(() => { l1Player.adjustVolume(false); }); }); $scope.toggleLyricTranslation = () => { $scope.enableLyricTranslation = !$scope.enableLyricTranslation; localStorage.setObject( 'enable_lyric_translation', $scope.enableLyricTranslation ); }; $scope.toggleLyricFloatingWindowTranslation = () => { $scope.enableLyricFloatingWindowTranslation = !$scope.enableLyricFloatingWindowTranslation; localStorage.setObject( 'enable_lyric_floating_window_translation', $scope.enableLyricFloatingWindowTranslation ); }; if (isElectron()) { require('electron').ipcRenderer.on('globalShortcut', (event, message) => { if (message === 'right') { l1Player.next(); } else if (message === 'left') { l1Player.prev(); } else if (message === 'space') { l1Player.togglePlayPause(); } }); } $scope.setAutoChooseSource = (toggle) => { if (toggle === true) { $scope.enableAutoChooseSource = !$scope.enableAutoChooseSource; } localStorage.setObject( 'enable_auto_choose_source', $scope.enableAutoChooseSource ); }; $scope.enableSource = (source) => { if ($scope.autoChooseSourceList.indexOf(source) > -1) { return; } $scope.autoChooseSourceList = [...$scope.autoChooseSourceList, source]; localStorage.setObject( 'auto_choose_source_list', $scope.autoChooseSourceList ); }; $scope.disableSource = (source) => { if ($scope.autoChooseSourceList.indexOf(source) === -1) { return; } $scope.autoChooseSourceList = $scope.autoChooseSourceList.filter( (i) => i !== source ); localStorage.setObject( 'auto_choose_source_list', $scope.autoChooseSourceList ); }; $scope.setStopWhenClose = (status) => { $scope.enableStopWhenClose = status; localStorage.setObject( 'enable_stop_when_close', $scope.enableStopWhenClose ); }; $scope.setNowplayingCoverBackground = (toggle) => { if (toggle === true) { $scope.enableNowplayingCoverBackground = !$scope.enableNowplayingCoverBackground; } localStorage.setObject( 'enable_nowplaying_cover_background', $scope.enableNowplayingCoverBackground ); }; $scope.setNowplayingBitrate = (toggle) => { if (toggle === true) { $scope.enableNowplayingBitrate = !$scope.enableNowplayingBitrate; } localStorage.setObject( 'enable_nowplaying_bitrate', $scope.enableNowplayingBitrate ); }; $scope.setNowplayingPlatform = (toggle) => { if (toggle === true) { $scope.enableNowplayingPlatform = !$scope.enableNowplayingPlatform; } localStorage.setObject( 'enable_nowplaying_platform', $scope.enableNowplayingPlatform ); }; }, ]); ================================================ FILE: js/controller/playlist.js ================================================ /* eslint-disable no-unused-vars */ /* global angular MediaService sourceList */ angular.module('listenone').controller('PlayListController', [ '$scope', '$timeout', ($scope) => { $scope.result = []; $scope.tab = sourceList[0].name; $scope.sourceList = sourceList; $scope.playlistFilters = {}; $scope.allPlaylistFilters = {}; $scope.currentFilterId = ''; $scope.loading = true; $scope.showMore = false; $scope.$on('infinite_scroll:hit_bottom', (event, data) => { if ($scope.loading === true) { return; } $scope.loading = true; const offset = $scope.result.length; MediaService.showPlaylistArray( $scope.tab, offset, $scope.currentFilterId ).success((res) => { $scope.result = $scope.result.concat(res.result); $scope.loading = false; }); }); $scope.loadPlaylist = () => { const offset = 0; $scope.showMore = false; MediaService.showPlaylistArray( $scope.tab, offset, $scope.currentFilterId ).success((res) => { $scope.result = res.result; $scope.loading = false; }); if ( $scope.playlistFilters[$scope.tab] === undefined && $scope.allPlaylistFilters[$scope.tab] === undefined ) { MediaService.getPlaylistFilters($scope.tab).success((res) => { $scope.playlistFilters[$scope.tab] = res.recommend; $scope.allPlaylistFilters[$scope.tab] = res.all; }); } }; $scope.changeTab = (newTab) => { $scope.tab = newTab; $scope.result = []; $scope.currentFilterId = ''; $scope.loadPlaylist(); }; $scope.changeFilter = (filterId) => { $scope.result = []; $scope.currentFilterId = filterId; $scope.loadPlaylist(); }; $scope.toggleMorePlaylists = () => { $scope.showMore = !$scope.showMore; }; }, ]); ================================================ FILE: js/controller/profile.js ================================================ /* eslint-disable import/no-unresolved */ /* eslint-disable global-require */ /* eslint-disable no-undef */ /* eslint-disable no-param-reassign */ /* global angular i18next sourceList platformSourceList */ angular.module('listenone').controller('ProfileController', [ '$scope', ($scope) => { let defaultLang = 'zh-CN'; const supportLangs = ['zh-CN', 'en-US']; if (supportLangs.indexOf(navigator.language) !== -1) { defaultLang = navigator.language; } if (supportLangs.indexOf(localStorage.getObject('language')) !== -1) { defaultLang = localStorage.getObject('language'); } $scope.lastestVersion = ''; $scope.theme = ''; $scope.proxyModes = [ { name: 'system', displayId: '_PROXY_SYSTEM' }, { name: 'direct', displayId: '_PROXY_DIRECT' }, { name: 'custom', displayId: '_PROXY_CUSTOM' }, ]; [$scope.proxyModeInput] = $scope.proxyModes; [$scope.proxyMode] = $scope.proxyModes; $scope.proxyProtocols = ['http', 'https', 'quic', 'socks4', 'socks5']; $scope.proxyProtocol = 'http'; $scope.proxyRules = ''; $scope.changeProxyProtocol = (newProtocol) => { $scope.proxyProtocol = newProtocol; }; $scope.changeProxyMode = (newMode) => { $scope.proxyModeInput = newMode; }; $scope.setProxyConfig = () => { const mode = $scope.proxyModeInput.name; $scope.proxyMode = $scope.proxyModeInput; const host = document.getElementById('proxy-rules-host').value; const port = document.getElementById('proxy-rules-port').value; $scope.proxyRules = `${$scope.proxyProtocol}://${host}:${port}`; if (isElectron()) { const message = 'update_proxy_config'; const { ipcRenderer } = require('electron'); if (mode === 'system' || mode === 'direct') { ipcRenderer.send('control', message, { mode }); } else { ipcRenderer.send('control', message, { proxyRules: $scope.proxyRules, }); } } }; $scope.getProxyConfig = () => { if (isElectron()) { // get proxy config from main process const message = 'get_proxy_config'; const { ipcRenderer } = require('electron'); ipcRenderer.send('control', message); } }; $scope.initProfile = () => { const url = `https://api.github.com/repos/listen1/listen1_chrome_extension/releases/latest`; axios.get(url).then((response) => { $scope.lastestVersion = response.data.tag_name; }); $scope.getProxyConfig(); }; if (isElectron()) { const { ipcRenderer } = require('electron'); ipcRenderer.on('proxyConfig', (event, config) => { // parse config if (config.mode === 'system' || config.mode === 'direct') { [$scope.proxyMode] = $scope.proxyModes.filter( (i) => i.name === config.mode ); $scope.proxyModeInput = $scope.proxyMode; $scope.proxyRules = ''; } else { [$scope.proxyMode] = $scope.proxyModes.filter( (i) => i.name === 'custom' ); $scope.proxyModeInput = $scope.proxyMode; $scope.proxyRules = config.proxyRules; // rules = 'socks5://127.0.0.1:1080' const match = /(\w+):\/\/([\d.]+):(\d+)/.exec(config.proxyRules); const [, protocol, host, port] = match; $scope.proxyProtocol = protocol; document.getElementById('proxy-rules-host').value = host; document.getElementById('proxy-rules-port').value = port; } }); } $scope.setLang = (langKey) => { // You can change the language during runtime i18next.changeLanguage(langKey).then((t) => { axios.get('i18n/zh-CN.json').then((res) => { Object.keys(res.data).forEach((key) => { $scope[key] = t(key); }); sourceList.forEach((item) => { item.displayText = t(item.displayId); }); platformSourceList.forEach((item) => { item.displayText = t(item.displayId); }); $scope.proxyModes.forEach((item) => { item.displayText = t(item.displayId); }); }); localStorage.setObject('language', langKey); }); }; $scope.setLang(defaultLang); let defaultTheme = 'white'; if (localStorage.getObject('theme') !== null) { defaultTheme = localStorage.getObject('theme'); } $scope.setTheme = (theme) => { $scope.theme = theme; const themeFiles = { white: ['css/iparanoid.css', 'css/common.css'], black: ['css/origin.css', 'css/common.css'], white2: ['css/iparanoid2.css', 'css/common2.css'], black2: ['css/origin2.css', 'css/common2.css'], }; // You can change the language during runtime if (themeFiles[theme] !== undefined) { const keys = ['theme-css', 'common-css']; for (let i = 0; i < themeFiles[theme].length; i += 1) { document.getElementById(keys[i]).href = themeFiles[theme][i]; } localStorage.setObject('theme', theme); } axios.get('images/feather-sprite.svg').then((res) => { document.getElementById('feather-container').innerHTML = res.data; }); }; $scope.setTheme(defaultTheme); }, ]); ================================================ FILE: js/github.js ================================================ /* global isElectron require */ /* eslint-disable global-require */ function github() { const OAUTH_URL = 'https://github.com/login/oauth'; const API_URL = 'https://api.github.com'; const client_id = 'e099a4803bb1e2e773a3'; const client_secret = '81fbfc45c65af8c0fbf2b4dae6f23f22e656cfb8'; const GithubAPI = axios.create({ baseURL: API_URL, headers: { accept: 'application/json' }, }); GithubAPI.interceptors.request.use((config) => { const accessToken = localStorage.getObject('githubOauthAccessKey'); // eslint-disable-next-line no-param-reassign config.headers.Authorization = `token ${accessToken}`; return config; }); const Github = { status: 0, username: '', }; window.GithubClient = { github: { handleCallback: (code, cb) => { const url = `${OAUTH_URL}/access_token`; const params = { client_id, client_secret, code, }; axios .post(url, '', { params, headers: { accept: 'application/json' }, }) .then((res) => { const ak = res.data.access_token; if (ak) localStorage.setItem('githubOauthAccessKey', JSON.stringify(ak)); if (cb !== undefined) { cb(ak); } }); }, openAuthUrl: () => { Github.status = 1; const url = `${OAUTH_URL}/authorize?client_id=${client_id}&scope=gist`; if (isElectron()) { // normal window for link const { BrowserWindow } = require('@electron/remote'); // eslint-disable-line import/no-unresolved let win = new BrowserWindow({ width: 1000, height: 670, }); win.on('closed', () => { win = null; }); win.loadURL(url); return; } window.open(url, '_blank'); }, getStatus: () => Github.status, getStatusText: () => { switch (Github.status) { case 0: return '未连接'; case 1: return '连接中'; case 2: return `${Github.username}已登录`; default: return '???'; } }, updateStatus: async (callback) => { const access_token = localStorage.getObject('githubOauthAccessKey'); if (access_token == null) { Github.status = 0; } else { const { data } = await GithubAPI.get('/user'); if (data.login === undefined) { Github.status = 1; } else { Github.status = 2; Github.username = data.login; } } if (callback != null) { callback(Github.status); } }, logout: () => { localStorage.removeItem('githubOauthAccessKey'); Github.status = 0; }, }, gist: { json2gist(jsonObject) { const result = {}; result['listen1_backup.json'] = { content: JSON.stringify(jsonObject), }; // const markdown = '# My Listen1 Playlists\n'; const playlistIds = jsonObject.playerlists; const songsCount = playlistIds.reduce((count, playlistId) => { const playlist = jsonObject[playlistId]; const cover = `
`; const { title } = playlist.info; let tableHeader = '\n| 音乐标题 | 歌手 | 专辑 |\n'; tableHeader += '| --- | --- | --- |\n'; const tableBody = playlist.tracks.reduce( (r, track) => `${r} | ${track.title} | ${track.artist} | ${track.album} | \n`, '' ); const content = `
\n ${cover} ${title}

\n${tableHeader}${tableBody}

`; const filename = `listen1_${playlistId}.md`; result[filename] = { content, }; return count + playlist.tracks.length; }, 0); const summary = `本歌单由[Listen1](https://listen1.github.io/listen1/)创建, 歌曲数:${songsCount},歌单数:${playlistIds.length},点击查看更多`; result['listen1_aha_playlist.md'] = { content: summary, }; return result; }, gist2json(gistFiles, callback) { if (!gistFiles['listen1_backup.json'].truncated) { const jsonString = gistFiles['listen1_backup.json'].content; return callback(JSON.parse(jsonString)); } const url = gistFiles['listen1_backup.json'].raw_url; // const { size } = gistFiles['listen1_backup.json']; GithubAPI.get(url).then((res) => callback(res.data)); return null; }, listExistBackup() { return GithubAPI.get('/gists').then((res) => { const result = res.data; return result.filter((backupObject) => backupObject.description.startsWith('updated by Listen1') ); }); }, backupMySettings2Gist(files, gistId, isPublic) { let method = ''; let url = ''; if (gistId != null) { method = 'patch'; url = `/gists/${gistId}`; } else { method = 'post'; url = '/gists'; } return GithubAPI.request({ method, url, data: { description: `updated by Listen1(https://listen1.github.io/listen1/) at ${new Date().toLocaleString()}`, public: isPublic, files, }, }); }, importMySettingsFromGist(gistId) { return GithubAPI.get(`/gists/${gistId}`).then((res) => res.data.files); }, }, }; } github(); ================================================ FILE: js/l1_player.js ================================================ /* eslint-disable no-param-reassign */ /* global isElectron getPlayer getPlayerAsync addPlayerListener getLocalStorageValue */ { const mode = isElectron() || getLocalStorageValue('enable_stop_when_close', true) ? 'front' : 'background'; const myPlayer = getPlayer(mode); const l1Player = { status: { muted: myPlayer.muted, volume: myPlayer.volume * 100, loop_mode: myPlayer.loop_mode, playing: myPlayer.playing, }, play() { getPlayerAsync(mode, (player) => { player.play(); }); }, pause() { getPlayerAsync(mode, (player) => { player.pause(); }); }, togglePlayPause() { getPlayerAsync(mode, (player) => { if (player.playing) { player.pause(); } else { player.play(); } }); }, playById(id) { getPlayerAsync(mode, (player) => { player.playById(id); }); }, loadById(idx) { getPlayerAsync(mode, (player) => { player.loadById(idx); }); }, seek(per) { getPlayerAsync(mode, (player) => { player.seek(per); }); }, next() { getPlayerAsync(mode, (player) => { player.skip('next'); }); }, prev() { getPlayerAsync(mode, (player) => { player.skip('prev'); }); }, random() { getPlayerAsync(mode, (player) => { player.skip('random'); }); }, setLoopMode(input) { getPlayerAsync(mode, (player) => { // eslint-disable-next-line no-param-reassign player.loop_mode = input; }); }, mute() { getPlayerAsync(mode, (player) => { player.mute(); }); }, unmute() { getPlayerAsync(mode, (player) => { player.unmute(); }); }, toggleMute() { getPlayerAsync(mode, (player) => { if (player.muted) player.unmute(); else player.mute(); }); }, setVolume(per) { getPlayerAsync(mode, (player) => { // eslint-disable-next-line no-param-reassign player.volume = per / 100; }); }, adjustVolume(increase) { getPlayerAsync(mode, (player) => { player.adjustVolume(increase); }); }, addTrack(track) { getPlayerAsync(mode, (player) => { player.insertAudio(track); }); }, insertTrack(track, to_track, direction) { getPlayerAsync(mode, (player) => { player.insertAudioByDirection(track, to_track, direction); }); }, removeTrack(index) { getPlayerAsync(mode, (player) => { player.removeAudio(index); }); }, addTracks(list) { getPlayerAsync(mode, (player) => { player.appendAudioList(list); }); }, clearPlaylist() { getPlayerAsync(mode, (player) => { player.clearPlaylist(); }); }, setNewPlaylist(list) { getPlayerAsync(mode, (player) => { player.setNewPlaylist(list); }); }, getTrackById(id) { if (!l1Player.status.playlist) return null; return l1Player.status.playlist.find((track) => track.id === id); }, connectPlayer() { getPlayerAsync(mode, (player) => { if (!player.playing) { // load local storage settings if (!player.playlist.length) { const localCurrentPlaying = localStorage.getObject('current-playing'); if (localCurrentPlaying !== null) { localCurrentPlaying.forEach((i) => { i.disabled = false; }); player.setNewPlaylist(localCurrentPlaying); } } const localPlayerSettings = localStorage.getObject('player-settings'); if (localPlayerSettings !== null) { player.loadById(localPlayerSettings.nowplaying_track_id); } } player.sendPlaylistEvent(); player.sendPlayingEvent(); player.sendLoadEvent(); }); }, }; l1Player.injectDirectives = (ngApp) => { ngApp.directive('playFromPlaylist', () => ({ restrict: 'EA', scope: { song: '=playFromPlaylist', }, link(scope, element) { element.bind('click', () => { l1Player.playById(scope.song.id); }); }, })); ngApp.directive('nextTrack', () => ({ restrict: 'EA', link(scope, element) { element.bind('click', () => { l1Player.next(); }); }, })); ngApp.directive('prevTrack', () => ({ restrict: 'EA', link(scope, element) { element.bind('click', () => { l1Player.prev(); }); }, })); ngApp.directive('clearPlaylist', () => ({ restrict: 'EA', link(scope, element) { element.bind('click', () => { l1Player.clearPlaylist(); }); }, })); ngApp.directive('removeFromPlaylist', () => ({ restrict: 'EA', scope: { song: '=removeFromPlaylist', }, link(scope, element, attrs) { element.bind('click', () => { l1Player.removeTrack(attrs.index); }); }, })); ngApp.directive('playPauseToggle', () => ({ restrict: 'EA', link(scope, element) { element.bind('click', () => { l1Player.togglePlayPause(); }); }, })); }; addPlayerListener(mode, (msg, sender, res) => { if (msg.type === 'BG_PLAYER:FRAME_UPDATE') { l1Player.status.playing = { ...l1Player.status.playing, ...msg.data, }; } else if (msg.type === 'BG_PLAYER:PLAYLIST') { l1Player.status.playlist = msg.data || []; } if (res !== undefined) { res(); } }); window.l1Player = l1Player; } ================================================ FILE: js/lastfm.js ================================================ /* global forge */ // eslint-disable-next-line no-unused-vars { const options = { apiKey: '6790c00a181128dc7c4ce06cd99d17c8', apiSecret: 'd68f1dfc6ff43044c96a79ae7dfb5c27', }; const apiUrl = 'https://ws.audioscrobbler.com/2.0/'; let status = 0; // const publicApi = { // getAuth, // cancelAuth, // getSession, // sendNowPlaying, // scrobble, // getUserInfo, // getStatusText, // updateStatus, // isAuthorized, // isAuthRequested, // }; /** * Computes string for signing request * * See https://www.last.fm/api/authspec#8 */ const generateSign = (params) => { const keys = Object.keys(params).filter( (key) => key !== 'format' || key !== 'callback' ); // params has to be ordered alphabetically keys.sort(); const o = keys.reduce((r, key) => r + key + params[key], ''); // append secret return forge.md5 .create() .update(forge.util.encodeUtf8(o + options.apiSecret)) .digest() .toHex(); }; // eslint-disable-next-line no-underscore-dangle const _isAuthRequested = () => { const token = localStorage.getObject('lastfmtoken'); return token != null; }; // eslint-disable-next-line no-unused-vars class lastfm { static getSession(callback) { // load session info from localStorage let mySession = localStorage.getObject('lastfmsession'); if (mySession != null) { return callback(mySession); } // trade session with token const token = localStorage.getObject('lastfmtoken'); if (token == null) { return callback(null); } // token exists const params = { method: 'auth.getsession', api_key: options.apiKey, token, }; params.api_sig = generateSign(params); params.format = 'json'; axios .get(apiUrl, { params, }) .then((response) => { const { data } = response; mySession = data.session; localStorage.setObject('lastfmsession', mySession); callback(mySession); }) .catch((error) => { if (error.response.status === 403) { callback(null); } }); return null; } static getUserInfo(callback) { this.getSession((session) => { if (session == null) { callback(null); return; } const params = { method: 'user.getinfo', api_key: options.apiKey, sk: session.key, }; params.api_sig = generateSign(params); params.format = 'json'; axios .post(apiUrl, '', { params, }) .then((response) => { const { data } = response; if (callback != null) { callback(data); } }); }); } static updateStatus() { // auth status // 0: never request for auth // 1: request but fail to success // 2: success auth if (!_isAuthRequested()) { status = 0; return; } this.getUserInfo((data) => { if (data === null) { status = 1; } else { status = 2; } }); } static getAuth(callback) { axios .get(apiUrl, { params: { method: 'auth.gettoken', api_key: options.apiKey, format: 'json', }, }) .then((response) => { const { data } = response; const { token } = data; localStorage.setObject('lastfmtoken', token); const grant_url = `https://www.last.fm/api/auth/?api_key=${options.apiKey}&token=${token}`; window.open(grant_url, '_blank'); status = 1; if (callback != null) { callback(); } }); } static cancelAuth() { localStorage.removeItem('lastfmsession'); localStorage.removeItem('lastfmtoken'); this.updateStatus(); } static sendNowPlaying(track, artist, callback) { this.getSession((session) => { const params = { method: 'track.updatenowplaying', track, artist, api_key: options.apiKey, sk: session.key, }; params.api_sig = generateSign(params); params.format = 'json'; axios .post(apiUrl, '', { params, }) .then((response) => { const { data } = response; if (callback != null) { callback(data); } }); }); } static scrobble(timestamp, track, artist, album, callback) { this.getSession((session) => { const params = { method: 'track.scrobble', 'timestamp[0]': timestamp, 'track[0]': track, 'artist[0]': artist, api_key: options.apiKey, sk: session.key, }; if (album !== '' && album != null) { params['album[0]'] = album; } params.api_sig = generateSign(params); params.format = 'json'; axios .post(apiUrl, '', { params, }) .then((response) => { const { data } = response; if (callback != null) { callback(data); } }); }); } static isAuthorized() { return status === 2; } static isAuthRequested() { return !(status === 0); } static getStatusText() { switch (status) { case 0: return '未连接'; case 1: return '连接中'; case 2: return '已连接'; default: return ''; } } } window.lastfm = lastfm; } ================================================ FILE: js/loweb.js ================================================ /* global async LRUCache setPrototypeOfLocalStorage getLocalStorageValue */ /* global netease xiami qq kugou kuwo bilibili migu taihe localmusic myplaylist */ const PROVIDERS = [ { name: 'netease', instance: netease, searchable: true, support_login: true, id: 'ne', }, { name: 'xiami', instance: xiami, searchable: false, hidden: true, support_login: false, id: 'xm', }, { name: 'qq', instance: qq, searchable: true, support_login: true, id: 'qq', }, { name: 'kugou', instance: kugou, searchable: true, support_login: false, id: 'kg', }, { name: 'kuwo', instance: kuwo, searchable: true, support_login: false, id: 'kw', }, { name: 'bilibili', instance: bilibili, searchable: true, support_login: false, id: 'bi', }, { name: 'migu', instance: migu, searchable: true, support_login: true, id: 'mg', }, { name: 'taihe', instance: taihe, searchable: true, support_login: false, id: 'th', }, { name: 'localmusic', instance: localmusic, searchable: false, hidden: true, support_login: false, id: 'lm', }, { name: 'myplaylist', instance: myplaylist, searchable: false, hidden: true, support_login: false, id: 'my', }, ]; function getProviderByName(sourceName) { return (PROVIDERS.find((i) => i.name === sourceName) || {}).instance; } function getAllProviders() { return PROVIDERS.filter((i) => !i.hidden).map((i) => i.instance); } function getAllSearchProviders() { return PROVIDERS.filter((i) => i.searchable).map((i) => i.instance); } function getProviderNameByItemId(itemId) { const prefix = itemId.slice(0, 2); return (PROVIDERS.find((i) => i.id === prefix) || {}).name; } function getProviderByItemId(itemId) { const prefix = itemId.slice(0, 2); return (PROVIDERS.find((i) => i.id === prefix) || {}).instance; } /* cache for all playlist request except myplaylist and localmusic */ const playlistCache = new LRUCache({ max: 100, maxAge: 60 * 60 * 1000, // 1 hour cache expire }); function queryStringify(options) { const query = JSON.parse(JSON.stringify(options)); return new URLSearchParams(query).toString(); } setPrototypeOfLocalStorage(); // eslint-disable-next-line no-unused-vars const MediaService = { getLoginProviders() { return PROVIDERS.filter((i) => !i.hidden && i.support_login); }, search(source, options) { const url = `/search?${queryStringify(options)}`; if (source === 'allmusic') { // search all platform and merge result const callbackArray = getAllSearchProviders().map((p) => (fn) => { p.search(url).success((r) => { fn(null, r); }); }); return { success: (fn) => async.parallel(callbackArray, (err, platformResultArray) => { // TODO: nicer pager, playlist support const result = { result: [], total: 1000, type: platformResultArray[0].type, }; const maxLength = Math.max( ...platformResultArray.map((elem) => elem.result.length) ); for (let i = 0; i < maxLength; i += 1) { platformResultArray.forEach((elem) => { if (i < elem.result.length) { result.result.push(elem.result[i]); } }); } return fn(result); }), }; } const provider = getProviderByName(source); return provider.search(url); }, showMyPlaylist() { return myplaylist.show_myplaylist('my'); }, showPlaylistArray(source, offset, filter_id) { const provider = getProviderByName(source); const url = `/show_playlist?${queryStringify({ offset, filter_id })}`; return provider.show_playlist(url); }, getPlaylistFilters(source) { const provider = getProviderByName(source); return provider.get_playlist_filters(); }, getLyric(track_id, album_id, lyric_url, tlyric_url) { const provider = getProviderByItemId(track_id); const url = `/lyric?${queryStringify({ track_id, album_id, lyric_url, tlyric_url, })}`; return provider.lyric(url); }, showFavPlaylist() { return myplaylist.show_myplaylist('favorite'); }, queryPlaylist(listId, type) { const result = myplaylist.myplaylist_containers(type, listId); return { success: (fn) => fn({ result }), }; }, getPlaylist(listId, useCache = true) { const provider = getProviderByItemId(listId); const url = `/playlist?list_id=${listId}`; let hit = null; if (useCache) { hit = playlistCache.get(listId); } if (hit) { return { success: (fn) => fn(hit), }; } return { success: (fn) => provider.get_playlist(url).success((playlist) => { if (provider !== myplaylist && provider !== localmusic) { playlistCache.set(listId, playlist); } fn(playlist); }), }; }, clonePlaylist(id, type) { const provider = getProviderByItemId(id); const url = `/playlist?list_id=${id}`; return { success: (fn) => { provider.get_playlist(url).success((data) => { myplaylist.save_myplaylist(type, data); fn(); }); }, }; }, removeMyPlaylist(id, type) { myplaylist.remove_myplaylist(type, id); return { success: (fn) => fn(), }; }, addMyPlaylist(id, track) { const newPlaylist = myplaylist.add_track_to_myplaylist(id, track); return { success: (fn) => fn(newPlaylist), }; }, insertTrackToMyPlaylist(id, track, to_track, direction) { const newPlaylist = myplaylist.insert_track_to_myplaylist( id, track, to_track, direction ); return { success: (fn) => fn(newPlaylist), }; }, addPlaylist(id, tracks) { const provider = getProviderByItemId(id); return provider.add_playlist(id, tracks); }, removeTrackFromMyPlaylist(id, track) { myplaylist.remove_track_from_myplaylist(id, track); return { success: (fn) => fn(), }; }, removeTrackFromPlaylist(id, track) { const provider = getProviderByItemId(id); return provider.remove_from_playlist(id, track); }, createMyPlaylist(title, track) { myplaylist.create_myplaylist(title, track); return { success: (fn) => { fn(); }, }; }, insertMyplaylistToMyplaylists( playlistType, playlistId, toPlaylistId, direction ) { const newPlaylists = myplaylist.insert_myplaylist_to_myplaylists( playlistType, playlistId, toPlaylistId, direction ); return { success: (fn) => fn(newPlaylists), }; }, editMyPlaylist(id, title, coverImgUrl) { myplaylist.edit_myplaylist(id, title, coverImgUrl); return { success: (fn) => fn(), }; }, parseURL(url) { return { success: (fn) => { const providers = getAllProviders(); Promise.all( providers.map( (provider) => new Promise((res, rej) => provider.parse_url(url).success((r) => { if (r !== undefined) { return rej(r); } return res(r); }) ) ) ) .then(() => fn({})) .catch((result) => fn({ result })); }, }; }, mergePlaylist(source, target) { const tarData = localStorage.getObject(target).tracks; const srcData = localStorage.getObject(source).tracks; tarData.forEach((tarTrack) => { if (!srcData.find((srcTrack) => srcTrack.id === tarTrack.id)) { myplaylist.add_track_to_myplaylist(source, tarTrack); } }); return { success: (fn) => fn(), }; }, bootstrapTrack(track, playerSuccessCallback, playerFailCallback) { const successCallback = playerSuccessCallback; const sound = {}; function failureCallback() { if (localStorage.getObject('enable_auto_choose_source') === false) { playerFailCallback(); return; } const trackPlatform = getProviderNameByItemId(track.id); const failover_source_list = getLocalStorageValue( 'auto_choose_source_list', ['kuwo', 'qq', 'migu'] ).filter((i) => i !== trackPlatform); const getUrlPromises = failover_source_list.map( (source) => new Promise((resolve, reject) => { if (track.source === source) { // come from same source, no need to check resolve(); return; } // TODO: better query method const keyword = `${track.title} ${track.artist}`; const curpage = 1; const url = `/search?keywords=${keyword}&curpage=${curpage}&type=0`; const provider = getProviderByName(source); provider.search(url).success((data) => { for (let i = 0; i < data.result.length; i += 1) { const searchTrack = data.result[i]; // compare search track and track to check if they are same // TODO: better similar compare method (duration, md5) if ( !searchTrack.disable && searchTrack.title === track.title && searchTrack.artist === track.artist ) { provider.bootstrap_track( searchTrack, (response) => { sound.url = response.url; sound.bitrate = response.bitrate; sound.platform = response.platform; reject(sound); // Use Reject to return immediately }, resolve ); return; } } resolve(sound); }); }) ); // TODO: Use Promise.any() in ES2021 replace the tricky workaround Promise.all(getUrlPromises) .then(playerFailCallback) .catch((response) => { playerSuccessCallback(response); }); } const provider = getProviderByName(track.source); provider.bootstrap_track(track, successCallback, failureCallback); }, login(source, options) { const url = `/login?${queryStringify(options)}`; const provider = getProviderByName(source); return provider.login(url); }, getUser(source) { const provider = getProviderByName(source); return provider.get_user(); }, getLoginUrl(source) { const provider = getProviderByName(source); return provider.get_login_url(); }, getUserCreatedPlaylist(source, options) { const provider = getProviderByName(source); const url = `/get_user_create_playlist?${queryStringify(options)}`; return provider.get_user_created_playlist(url); }, getUserFavoritePlaylist(source, options) { const provider = getProviderByName(source); const url = `/get_user_favorite_playlist?${queryStringify(options)}`; return provider.get_user_favorite_playlist(url); }, getRecommendPlaylist(source) { const provider = getProviderByName(source); return provider.get_recommend_playlist(); }, logout(source) { const provider = getProviderByName(source); return provider.logout(); }, }; // eslint-disable-next-line no-unused-vars const loWeb = MediaService; ================================================ FILE: js/lowebutil.js ================================================ /* eslint-disable consistent-return */ /* eslint-disable no-param-reassign */ /* eslint-disable no-unused-vars */ function getParameterByName(name, url) { if (!url) url = window.location.href; name = name.replace(/[[\]]/g, '\\$&'); const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); const results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, ' ')); } function isElectron() { return window && window.process && window.process.type; } function cookieGet(cookieRequest, callback) { if (!isElectron()) { return chrome.cookies.get(cookieRequest, (cookie) => { callback(cookie); }); } const remote = require('@electron/remote'); // eslint-disable-line remote.session.defaultSession.cookies .get(cookieRequest) .then((cookieArray) => { let cookie = null; if (cookieArray.length > 0) { [cookie] = cookieArray; } callback(cookie); }); } function cookieSet(cookie, callback) { if (!isElectron()) { return chrome.cookies.set(cookie, (arg1, arg2) => { callback(arg1, arg2); }); } const remote = require('@electron/remote'); // eslint-disable-line remote.session.defaultSession.cookies.set(cookie).then((arg1, arg2) => { callback(null, arg1, arg2); }); } function cookieRemove(cookie, callback) { if (!isElectron()) { return chrome.cookies.remove(cookie, (arg1, arg2) => { callback(arg1, arg2); }); } const remote = require('@electron/remote'); // eslint-disable-line remote.session.defaultSession.cookies .remove(cookie.url, cookie.name) .then((arg1, arg2) => { callback(null, arg1, arg2); }); } function setPrototypeOfLocalStorage() { const proto = Object.getPrototypeOf(localStorage); proto.getObject = function getObject(key) { const value = this.getItem(key); try { return value && JSON.parse(value); } catch (error) { return {}; } }; proto.setObject = function setObject(key, value) { this.setItem(key, JSON.stringify(value)); }; Object.setPrototypeOf(localStorage, proto); } function getLocalStorageValue(key, defaultValue) { const keyString = localStorage.getItem(key); let result = keyString && JSON.parse(keyString); if (result === null) { result = defaultValue; } return result; } function easeInOutQuad(t, b, c, d) { // t = current time // b = start value // c = change in value // d = duration t /= d / 2; if (t < 1) return (c / 2) * t * t + b; t -= 1; return (-c / 2) * (t * (t - 2) - 1) + b; } function smoothScrollTo(element, to, duration) { const start = element.scrollTop; const change = to - start; const startTime = performance.now(); const animateScroll = (currentTime) => { const timeElapsed = currentTime - startTime; const val = easeInOutQuad(timeElapsed, start, change, duration); element.scrollTop = val; if (timeElapsed < duration) { requestAnimationFrame(animateScroll); } else { element.scrollTop = to; // Ensure it ends exactly at 'to' } }; requestAnimationFrame(animateScroll); } ================================================ FILE: js/myplaylist.js ================================================ /* eslint-disable no-unused-vars */ /* global getParameterByName */ const myplaylistFactory = () => { function array_move(arr, old_index, new_index) { // https://stackoverflow.com/questions/5306680/move-an-array-element-from-one-array-position-to-another if (new_index >= arr.length) { let k = new_index - arr.length + 1; while (k > 0) { k -= 1; arr.push(undefined); } } arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); return arr; // for testing } function getPlaylistObjectKey(playlist_type) { let key = ''; if (playlist_type === 'my') { key = 'playerlists'; } else if (playlist_type === 'favorite') { key = 'favoriteplayerlists'; } return key; } function show_myplaylist(playlist_type) { return { success(fn) { const key = getPlaylistObjectKey(playlist_type); if (key === '') { return fn({ result: [] }); } let playlists = localStorage.getObject(key); if (playlists == null) { playlists = []; } const result = playlists.reduce((res, id) => { const playlist = localStorage.getObject(id); if (playlist !== null && playlist.tracks !== undefined) { // clear url field when load old playlist playlist.tracks.forEach((e) => { delete e.url; }); } res.push(playlist); return res; }, []); return fn({ result }); }, }; } function get_myplaylist(url) { const list_id = getParameterByName('list_id', url); return { success(fn) { const playlist = localStorage.getObject(list_id); // clear url field when load old playlist if (playlist !== null && playlist.tracks !== undefined) { playlist.tracks.forEach((e) => { delete e.url; e.disabled = false; }); } fn(playlist); }, }; } function guid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } return `${s4() + s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; } function insert_myplaylist_to_myplaylists( playlist_type, playlist_id, to_playlist_id, direction ) { const key = getPlaylistObjectKey(playlist_type); if (key === '') { return []; } const playlists = localStorage.getObject(key); const index = playlists.findIndex((i) => i === playlist_id); let insertIndex = playlists.findIndex((i) => i === to_playlist_id); if (index === insertIndex) { return playlists; } if (insertIndex > index) { insertIndex -= 1; } const offset = direction === 'top' ? 0 : 1; array_move(playlists, index, insertIndex + offset); localStorage.setObject(key, playlists); return playlists; } const save_myplaylist = (playlist_type, playlistObj) => { const playlist = playlistObj; const key = getPlaylistObjectKey(playlist_type); if (key === '') { return; } let playlists = localStorage.getObject(key); if (playlists == null) { playlists = []; } // update listid let playlist_id = ''; if (playlist_type === 'my') { playlist_id = `myplaylist_${guid()}`; playlist.info.id = playlist_id; playlist.is_mine = 1; // eslint-disable-line no-param-reassign } else if (playlist_type === 'favorite') { playlist_id = playlist.info.id; playlist.is_fav = 1; // remove all tracks info, cause favorite playlist always load latest delete playlist.tracks; } playlists.push(playlist_id); localStorage.setObject(key, playlists); localStorage.setObject(playlist_id, playlist); }; const remove_myplaylist = (playlist_type, playlist_id) => { const key = getPlaylistObjectKey(playlist_type); if (key === '') { return; } const playlists = localStorage.getObject(key); if (playlists == null) { return; } const newplaylists = playlists.filter((item) => item !== playlist_id); localStorage.removeItem(playlist_id); localStorage.setObject(key, newplaylists); }; function add_track_to_myplaylist(playlist_id, track) { const playlist = localStorage.getObject(playlist_id); if (playlist == null) { return null; } // new track will always insert in beginning of playlist if (Array.isArray(track)) { playlist.tracks = track.concat(playlist.tracks); } else { playlist.tracks.unshift(track); } // dedupe const newTracks = []; const trackIds = []; playlist.tracks.forEach((tracki) => { if (trackIds.indexOf(tracki.id) === -1) { newTracks.push(tracki); trackIds.push(tracki.id); } }); playlist.tracks = newTracks; localStorage.setObject(playlist_id, playlist); return playlist; } function insert_track_to_myplaylist(playlist_id, track, to_track, direction) { const playlist = localStorage.getObject(playlist_id); if (playlist == null) { return null; } const index = playlist.tracks.findIndex((i) => i.id === track.id); let insertIndex = playlist.tracks.findIndex((i) => i.id === to_track.id); if (index === insertIndex) { return playlist; } if (insertIndex > index) { insertIndex -= 1; } const offset = direction === 'top' ? 0 : 1; array_move(playlist.tracks, index, insertIndex + offset); localStorage.setObject(playlist_id, playlist); return playlist; } function remove_track_from_myplaylist(playlist_id, track_id) { const playlist = localStorage.getObject(playlist_id); if (playlist == null) { return; } const newtracks = playlist.tracks.filter((item) => item.id !== track_id); playlist.tracks = newtracks; localStorage.setObject(playlist_id, playlist); } function create_myplaylist(playlist_title, track) { const playlist = {}; const info = { cover_img_url: 'images/mycover.jpg', title: playlist_title, id: '', source_url: '', }; playlist.is_mine = 1; playlist.info = info; if (Array.isArray(track)) { playlist.tracks = track; } else { playlist.tracks = [track]; } // notice: create only used by my playlist, favorite created by clone interface save_myplaylist('my', playlist); } function edit_myplaylist(playlist_id, title, cover_img_url) { const playlist = localStorage.getObject(playlist_id); if (playlist == null) { return; } playlist.info.title = title; playlist.info.cover_img_url = cover_img_url; localStorage.setObject(playlist_id, playlist); } function myplaylist_containers(playlist_type, list_id) { const key = getPlaylistObjectKey(playlist_type); if (key === '') { return false; } const playlist = localStorage.getObject(list_id); return playlist !== null && playlist.is_fav; } return { show_myplaylist, save_myplaylist, get_playlist: get_myplaylist, remove_myplaylist, add_track_to_myplaylist, remove_track_from_myplaylist, create_myplaylist, edit_myplaylist, myplaylist_containers, insert_track_to_myplaylist, insert_myplaylist_to_myplaylists, }; }; const myplaylist = myplaylistFactory(); // eslint-disable-line no-unused-vars ================================================ FILE: js/oauth_callback.js ================================================ /** * Get and send oauth tokens from query string. */ chrome.runtime.sendMessage( { type: 'code', code: new URLSearchParams(window.location.search).get('code'), }, // eslint-disable-next-line no-unused-vars (response) => { // window.open('', '_self', ''); // window.close(); } ); ================================================ FILE: js/player_thread.js ================================================ /* eslint-disable no-underscore-dangle */ /* global MediaMetadata playerSendMessage MediaService */ /* global Howl Howler */ { /** * Player class containing the state of our playlist and where we are in it. * Includes all methods for playing, skipping, updating the display, etc. * @param {Array} playlist Array of objects with playlist song details ({title, file, howl}). */ class Player { constructor() { this.playlist = []; this._random_playlist = []; this.index = -1; this._loop_mode = 0; this._media_uri_list = {}; this.playedFrom = 0; this.mode = 'background'; this.skipTime = 15; } setMode(newMode) { this.mode = newMode; } setRefreshRate(rate = 10) { clearInterval(this.refreshTimer); this.refreshTimer = setInterval(() => { if (this.playing) { this.sendFrameUpdate(); } }, 1000 / rate); } get currentAudio() { return this.playlist[this.index]; } get currentHowl() { return this.currentAudio && this.currentAudio.howl; } get playing() { return this.currentHowl ? this.currentHowl.playing() : false; } // eslint-disable-next-line class-methods-use-this get muted() { return !!Howler._muted; } insertAudio(audio, idx) { if (this.playlist.find((i) => audio.id === i.id)) return; const audioData = { ...audio, disabled: false, // avoid first time load block howl: null, }; if (idx) { this.playlist.splice(idx, 0, [audio]); } else { this.playlist.push(audioData); } this.sendPlaylistEvent(); this.sendLoadEvent(); } static array_move(arr, old_index, new_index) { // https://stackoverflow.com/questions/5306680/move-an-array-element-from-one-array-position-to-another if (new_index >= arr.length) { let k = new_index - arr.length + 1; while (k > 0) { k -= 1; arr.push(undefined); } } arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); return arr; // for testing } insertAudioByDirection(audio, to_audio, direction) { const originTrack = this.playlist[this.index]; const index = this.playlist.findIndex((i) => i.id === audio.id); let insertIndex = this.playlist.findIndex((i) => i.id === to_audio.id); if (index === insertIndex) { return; } if (insertIndex > index) { insertIndex -= 1; } const offset = direction === 'top' ? 0 : 1; this.playlist = Player.array_move( this.playlist, index, insertIndex + offset ); const foundOriginTrackIndex = this.playlist.findIndex( (i) => i.id === originTrack.id ); if (foundOriginTrackIndex >= 0) { this.index = foundOriginTrackIndex; } this.sendPlaylistEvent(); this.sendLoadEvent(); } removeAudio(idx) { if (!this.playlist[idx]) { return; } // restore playing status before change const isPlaying = this.playing; const { id: trackId } = this.currentAudio; if (isPlaying && this.playlist[idx].id === trackId) { this.pause(); } this.playlist.splice(idx, 1); const newIndex = this.playlist.findIndex((i) => i.id === trackId); if (newIndex >= 0) { this.index = newIndex; } else { // current playing is deleted if (idx >= this.playlist.length) { this.index = this.playlist.length - 1; } else { this.index = idx; } if (isPlaying) { this.play(); } } this.sendPlaylistEvent(); this.sendLoadEvent(); } appendAudioList(list) { if (!Array.isArray(list)) { return; } list.forEach((audio) => { this.insertAudio(audio); }); } clearPlaylist() { this.stopAll(); // stop the loadded track before remove list this.playlist = []; Howler.unload(); this.sendPlaylistEvent(); this.sendLoadEvent(); } stopAll() { this.playlist.forEach((i) => { if (i.howl) { i.howl.stop(); } }); } setNewPlaylist(list) { if (list.length) { // stop current this.stopAll(); Howler.unload(); this.playlist = list.map((audio) => ({ ...audio, howl: null, })); // TODO: random mode need random choose first song to load this.index = 0; this.load(0); } this.sendPlaylistEvent(); } playById(id) { const idx = this.playlist.findIndex((audio) => audio.id === id); this.play(idx); } loadById(id) { const idx = this.playlist.findIndex((audio) => audio.id === id); this.load(idx); } /** * Play a song in the playlist. * @param {Number} index Index of the song in the playlist * (leave empty to play the first or current). */ play(idx) { this.load(idx); const data = this.playlist[this.index]; if (!data.howl || !this._media_uri_list[data.id]) { this.retrieveMediaUrl(this.index, true); } else { this.finishLoad(this.index, true); } } retrieveMediaUrl(index, playNow) { const msg = { type: 'BG_PLAYER:RETRIEVE_URL', data: { ...this.playlist[index], howl: undefined, index, playNow, }, }; MediaService.bootstrapTrack( msg.data, (bootinfo) => { msg.type = 'BG_PLAYER:RETRIEVE_URL_SUCCESS'; msg.data = { ...msg.data, ...bootinfo }; this.playlist[index].bitrate = bootinfo.bitrate; this.playlist[index].platform = bootinfo.platform; this.setMediaURI(msg.data.url, msg.data.id); this.setAudioDisabled(false, msg.data.index); this.finishLoad(msg.data.index, playNow); playerSendMessage(this.mode, msg); }, () => { msg.type = 'BG_PLAYER:RETRIEVE_URL_FAIL'; this.setAudioDisabled(true, msg.data.index); playerSendMessage(this.mode, msg); this.skip('next'); } ); } /** * Load a song from the playlist. * @param {Number} index Index of the song in the playlist * (leave empty to load the first or current). */ load(idx) { let index = typeof idx === 'number' ? idx : this.index; if (index < 0) return; if (!this.playlist[index]) { index = 0; } // stop when load new track to avoid multiple songs play in same time if (index !== this.index) { Howler.unload(); } this.index = index; this.sendLoadEvent(); } finishLoad(index, playNow) { const data = this.playlist[index]; // If we already loaded this track, use the current one. // Otherwise, setup and load a new Howl. const self = this; if (!data.howl) { data.howl = new Howl({ src: [self._media_uri_list[data.url || data.id]], format: 'mp3', // bypass Howl checking url extension, issue #1200 volume: 1, mute: self.muted, html5: true, // Force to HTML5 so that the audio can stream in (best for large files). onplay() { if ('mediaSession' in navigator) { const { mediaSession } = navigator; mediaSession.playbackState = 'playing'; mediaSession.metadata = new MediaMetadata({ title: self.currentAudio.title, artist: self.currentAudio.artist, album: `Listen 1 • ${( self.currentAudio.album || '' ).padEnd(100)}`, artwork: [ { src: self.currentAudio.img_url, sizes: '500x500', }, ], }); } self.currentAudio.disabled = false; // Date.now() returns a millisecond timestamp that needs to be converted to a second timestamp self.playedFrom = Math.round(Date.now() / 1000); self.sendPlayingEvent('Playing'); }, onload() { self.currentAudio.disabled = false; self.sendPlayingEvent('Loaded'); }, onend() { switch (self.loop_mode) { case 2: self.skip('random'); break; case 1: self.play(); break; case 0: default: self.skip('next'); break; } self.sendPlayingEvent('Ended'); }, onpause() { navigator.mediaSession.playbackState = 'paused'; self.sendPlayingEvent('Paused'); }, onstop() { self.sendPlayingEvent('Stopped'); }, onseek() {}, onvolume() {}, onloaderror(id, err) { playerSendMessage(this.mode, { type: 'BG_PLAYER:PLAY_FAILED', data: err, }); self.currentAudio.disabled = true; self.sendPlayingEvent('err'); self.currentHowl.unload(); data.howl = null; delete self._media_uri_list[data.id]; }, onplayerror(id, err) { playerSendMessage(this.mode, { type: 'BG_PLAYER:PLAY_FAILED', data: err, }); self.currentAudio.disabled = true; self.sendPlayingEvent('err'); }, }); } if (playNow) { if (this.playing && index === this.index) { return; } this.playlist.forEach((i) => { if (i.howl && i.howl !== this.currentHowl) { i.howl.stop(); } }); this.currentHowl.play(); } } /** * Pause the currently playing track. */ pause() { if (!this.currentHowl) return; // Puase the sound. this.currentHowl.pause(); } /** * Skip to the next or previous track. * @param {String} direction 'next' or 'prev'. */ skip(direction) { Howler.unload(); // Get the next track based on the direction of the track. const nextIndexFn = (idx) => { const l = this.playlist.length; const random_mode = this._loop_mode === 2 || direction === 'random'; let rdx = idx; if (random_mode) { if (this._random_playlist.length / 2 !== l) { // construction random playlist const a = Array.from({ length: l }, (_v, i) => i); for (let i = 0; i < l; i += 1) { const e = l - i - 1; const s = Math.floor(Math.random() * e); const t = a[s]; a[s] = a[e]; a[e] = t; // lookup table a[t + l] = e; } this._random_playlist = a; } rdx = this._random_playlist[idx + l]; } else if (this._random_playlist.length !== 0) { // clear random playlist this._random_playlist = []; } if (direction === 'prev') { if (rdx === 0) rdx = l; rdx -= 1; } else { rdx += 1; } const result = random_mode ? this._random_playlist[rdx % l] : rdx % l; return result; }; this.index = nextIndexFn(this.index); let tryCount = 0; while (tryCount < this.playlist.length) { if (!this.playlist[this.index].disabled) { this.play(this.index); return; } this.index = nextIndexFn(this.index); tryCount += 1; } playerSendMessage(this.mode, { type: 'BG_PLAYER:RETRIEVE_URL_FAIL_ALL', }); this.sendLoadEvent(); } set loop_mode(input) { const LOOP_MODE = { all: 0, one: 1, shuffle: 2, }; let myMode = 0; if (typeof input === 'string') { myMode = LOOP_MODE[input]; } else { myMode = input; } if (!Object.values(LOOP_MODE).includes(myMode)) { return; } this._loop_mode = myMode; } get loop_mode() { return this._loop_mode; } /** * Set the volume and update the volume slider display. * @param {Number} val Volume between 0 and 1. */ set volume(val) { // Update the global volume (affecting all Howls). if (typeof val === 'number') { Howler.volume(val); this.sendVolumeEvent(); this.sendFrameUpdate(); } } // eslint-disable-next-line class-methods-use-this get volume() { return Howler.volume(); } adjustVolume(inc) { this.volume = inc ? Math.min(this.volume + 0.1, 1) : Math.max(this.volume - 0.1, 0); this.sendVolumeEvent(); this.sendFrameUpdate(); } mute() { Howler.mute(true); playerSendMessage(this.mode, { type: 'BG_PLAYER:MUTE', data: true, }); } unmute() { Howler.mute(false); playerSendMessage(this.mode, { type: 'BG_PLAYER:MUTE', data: false, }); } /** * Seek to a new position in the currently playing track. * @param {Number} per Percentage through the song to skip. */ seek(per) { if (!this.currentHowl) return; // Get the Howl we want to manipulate. const audio = this.currentHowl; // Convert the percent into a seek position. // if (audio.playing()) { // } audio.seek(audio.duration() * per); } /** * Seek to a new position in the currently playing track. * @param {Number} seconds Seconds through the song to skip. */ seekTime(seconds) { if (!this.currentHowl) return; const audio = this.currentHowl; audio.seek(seconds); } /** * Format the time from seconds to M:SS. * @param {Number} secs Seconds to format. * @return {String} Formatted time. */ static formatTime(secs) { const minutes = Math.floor(secs / 60) || 0; const seconds = secs - minutes * 60 || 0; return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; } setMediaURI(uri, url) { if (url) { this._media_uri_list[url] = uri; } } setAudioDisabled(disabled, idx) { if (this.playlist[idx]) { this.playlist[idx].disabled = disabled; } } async sendFrameUpdate() { const data = { id: this.currentAudio ? this.currentAudio.id : 0, duration: this.currentHowl ? this.currentHowl.duration() : 0, pos: this.currentHowl ? this.currentHowl.seek() : 0, playedFrom: this.playedFrom, playing: this.playing, }; if ('setPositionState' in navigator.mediaSession) { navigator.mediaSession.setPositionState({ duration: this.currentHowl ? this.currentHowl.duration() : 0, playbackRate: this.currentHowl ? this.currentHowl.rate() : 1, position: this.currentHowl ? this.currentHowl.seek() : 0, }); } playerSendMessage(this.mode, { type: 'BG_PLAYER:FRAME_UPDATE', data, }); } async sendPlayingEvent(reason = 'UNKNOWN') { playerSendMessage(this.mode, { type: 'BG_PLAYER:PLAY_STATE', data: { isPlaying: this.playing, reason, }, }); } async sendLoadEvent() { playerSendMessage(this.mode, { type: 'BG_PLAYER:LOAD', data: { currentPlaying: { ...this.currentAudio, howl: undefined, }, playlist: { index: this.index, length: this.playlist.length, }, }, }); } async sendVolumeEvent() { playerSendMessage(this.mode, { type: 'BG_PLAYER:VOLUME', data: this.volume * 100, }); } async sendPlaylistEvent() { playerSendMessage(this.mode, { type: 'BG_PLAYER:PLAYLIST', data: this.playlist.map((audio) => ({ ...audio, howl: undefined })), }); } } // Setup our new audio player class and pass it the playlist. const threadPlayer = new Player(); threadPlayer.setRefreshRate(); window.threadPlayer = threadPlayer; if ('mediaSession' in navigator) { const { mediaSession } = navigator; mediaSession.setActionHandler('play', () => { threadPlayer.play(); }); mediaSession.setActionHandler('pause', () => { threadPlayer.pause(); }); mediaSession.setActionHandler('seekforward', (details) => { // User clicked "Seek Forward" media notification icon. const { currentHowl } = threadPlayer; const skipTime = details.seekOffset || threadPlayer.skipTime; const newTime = Math.min( currentHowl.seek() + skipTime, currentHowl.duration() ); threadPlayer.seekTime(newTime); threadPlayer.sendFrameUpdate(); }); mediaSession.setActionHandler('seekbackward', (details) => { // User clicked "Seek Backward" media notification icon. const { currentHowl } = threadPlayer; const skipTime = details.seekOffset || threadPlayer.skipTime; const newTime = Math.max(currentHowl.seek() - skipTime, 0); threadPlayer.seekTime(newTime); threadPlayer.sendFrameUpdate(); }); mediaSession.setActionHandler('seekto', (details) => { const { seekTime } = details; threadPlayer.seekTime(seekTime); threadPlayer.sendFrameUpdate(); }); mediaSession.setActionHandler('nexttrack', () => { threadPlayer.skip('next'); threadPlayer.sendFrameUpdate(); }); mediaSession.setActionHandler('previoustrack', () => { threadPlayer.skip('prev'); threadPlayer.sendFrameUpdate(); }); } playerSendMessage(this.mode, { type: 'BG_PLAYER:READY', }); } ================================================ FILE: js/provider/bilibili.js ================================================ let wbi_key = null; /* global getParameterByName */ // eslint-disable-next-line no-unused-vars /* global cookieSet cookieGet */ // eslint-disable-next-line no-unused-vars class bilibili { static htmlDecode(value) { const parser = new DOMParser(); return parser.parseFromString(value, 'text/html').body.textContent; } static fetch_wbi_key() { return axios({ url: 'https://api.bilibili.com/x/web-interface/nav', method: 'get', responseType: 'json', }).then((resp) => { const json_content = resp.data; const { img_url } = json_content.data.wbi_img; const { sub_url } = json_content.data.wbi_img; return { img_key: img_url.slice( img_url.lastIndexOf('/') + 1, img_url.lastIndexOf('.') ), sub_key: sub_url.slice( sub_url.lastIndexOf('/') + 1, sub_url.lastIndexOf('.') ), }; }); } static clear_wbi_key() { wbi_key = null; } static get_wbi_key() { if (wbi_key) { return Promise.resolve(wbi_key); } return bilibili.fetch_wbi_key().then((key) => { wbi_key = key; return key; }); } static enc_wbi(params) { return bilibili.get_wbi_key().then(({ img_key, sub_key }) => { const mixinKeyEncTab = [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52, ]; // 对 imgKey 和 subKey 进行字符顺序打乱编码 function get_mixin_key(original) { let temp = ''; mixinKeyEncTab.forEach((n) => { temp += original[n]; }); return temp.slice(0, 32); } const mixin_key = get_mixin_key(img_key + sub_key); const curr_time = Math.round(Date.now() / 1000); const chr_filter = /[!'()*]/g; const query = []; Object.assign(params, { wts: curr_time }); // 添加 wts 字段 // 按照 key 重排参数 Object.keys(params) .sort() .forEach((key) => { query.push( `${encodeURIComponent(key)}=${encodeURIComponent( // 过滤 value 中的 "!'()*" 字符 params[key].toString().replace(chr_filter, '') )}` ); }); const query_string = query.join('&'); const wbi_sign = window.forge.md5 .create() .update(window.forge.util.encodeUtf8(query_string + mixin_key)) .digest() .toHex(); return `${query_string}&w_rid=${wbi_sign}`; }); } static wrap_wbi_request(url, params) { return bilibili .enc_wbi(params) .then((query_string) => { const target_url = `${url}?${query_string}`; return axios.get(target_url); }) .catch(() => { // 失败时进行一次清空 wbi_key 后的重试,避免因为 wbi_key 过期导致的错误 bilibili.clear_wbi_key(); return bilibili .enc_wbi(params) .then((query_string) => { const target_url = `${url}?${query_string}`; return axios.get(target_url); }) .catch(() => undefined); }); } static bi_convert_song(song_info) { const track = { id: `bitrack_${song_info.id}`, title: song_info.title, artist: song_info.uname, artist_id: `biartist_${song_info.uid}`, source: 'bilibili', source_url: `https://www.bilibili.com/audio/au${song_info.id}`, img_url: song_info.cover, // url: song_info.id, lyric_url: song_info.lyric, }; return track; } static bi_convert_song2(song_info) { let imgUrl = song_info.pic; if (imgUrl.startsWith('//')) { imgUrl = `https:${imgUrl}`; } const track = { id: `bitrack_v_${song_info.bvid}`, title: this.htmlDecode(song_info.title), artist: this.htmlDecode(song_info.author), artist_id: `biartist_v_${song_info.mid}`, source: 'bilibili', source_url: `https://www.bilibili.com/${song_info.bvid}`, img_url: imgUrl, }; return track; } static show_playlist(url) { let offset = getParameterByName('offset', url); if (offset === undefined) { offset = 0; } const page = offset / 20 + 1; const target_url = `https://www.bilibili.com/audio/music-service-c/web/menu/hit?ps=20&pn=${page}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response.data.data; const result = data.map((item) => ({ cover_img_url: item.cover, title: item.title, id: `biplaylist_${item.menuId}`, source_url: `https://www.bilibili.com/audio/am${item.menuId}`, })); return fn({ result, }); }); }, }; } static bi_get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_').pop(); const target_url = `https://www.bilibili.com/audio/music-service-c/web/menu/info?sid=${list_id}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response.data; const info = { cover_img_url: data.cover, title: data.title, id: `biplaylist_${list_id}`, source_url: `https://www.bilibili.com/audio/am${list_id}`, }; const target = `https://www.bilibili.com/audio/music-service-c/web/song/of-menu?pn=1&ps=100&sid=${list_id}`; axios.get(target).then((res) => { const tracks = res.data.data.data.map((item) => this.bi_convert_song(item) ); return fn({ info, tracks, }); }); }); }, }; } // eslint-disable-next-line no-unused-vars static bi_album(url) { return { success: (fn) => fn({ tracks: [], info: {}, }), // bilibili havn't album // const album_id = getParameterByName('list_id', url).split('_').pop(); // const target_url = ''; // axios.get(target_url).then((response) => { // const data = response.data; // const info = {}; // const tracks = []; // return fn({ // tracks, // info, // }); // }); }; } static bi_track(url) { const track_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const target_url = `https://api.bilibili.com/x/web-interface/view?bvid=${track_id}`; axios.get(target_url).then((response) => { const info = { cover_img_url: response.data.data.pic, title: response.data.data.title, id: `bitrack_v_${track_id}`, source_url: `https://www.bilibili.com/${track_id}`, }; const author = response.data.data.owner; const default_img = response.data.data.pic; const tracks = response.data.data.pages.map((item) => this.bi_convert_song3(item, track_id, author, default_img) ); return fn({ tracks, info, }); }); }, }; } static bi_convert_song3(song_info, bvid, author, default_img) { let imgUrl = song_info.first_frame; if (imgUrl === undefined) { imgUrl = default_img; } else if (imgUrl.startsWith('//')) { imgUrl = `https:${imgUrl}`; } const track = { id: `bitrack_v_${bvid}-${song_info.cid}`, title: this.htmlDecode(song_info.part), artist: this.htmlDecode(author.name), artist_id: `biartist_v_${author.mid}`, source: 'bilibili', source_url: `https://www.bilibili.com/${bvid}/?p=${song_info.page}`, img_url: imgUrl, }; return track; } static bi_artist(url) { const artist_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { let target_url; bilibili .wrap_wbi_request('https://api.bilibili.com/x/space/wbi/acc/info', { mid: artist_id, }) .then((response) => { const info = { cover_img_url: response.data.data.face, title: response.data.data.name, id: `biartist_${artist_id}`, source_url: `https://space.bilibili.com/${artist_id}/#/audio`, }; if (getParameterByName('list_id', url).split('_').length === 3) { return bilibili .wrap_wbi_request( 'https://api.bilibili.com/x/space/wbi/arc/search', { mid: artist_id, pn: 1, ps: 25, order: 'click', index: 1, } ) .then((res) => { const tracks = res.data.data.list.vlist.map((item) => this.bi_convert_song2(item) ); return fn({ tracks, info, }); }); } target_url = `https://api.bilibili.com/audio/music-service-c/web/song/upper?pn=1&ps=0&order=2&uid=${artist_id}`; return axios.get(target_url).then((res) => { const tracks = res.data.data.data.map((item) => this.bi_convert_song(item) ); return fn({ tracks, info, }); }); }); }, }; } static parse_url(url) { let result; const match = /\/\/www.bilibili.com\/audio\/am([0-9]+)/.exec(url); if (match != null) { const playlist_id = match[1]; result = { type: 'playlist', id: `biplaylist_${playlist_id}`, }; } return { success: (fn) => { fn(result); }, }; } static bootstrap_track(track, success, failure) { const trackId = track.id; if (trackId.startsWith('bitrack_v_')) { const sound = {}; let bvid = track.id.slice('bitrack_v_'.length); const trackIdCheck = trackId.split('-'); if (trackIdCheck.length > 1) { bvid = trackIdCheck[0].slice('bitrack_v_'.length); } const target_url = `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`; return axios.get(target_url).then((response) => { let { cid } = response.data.data.pages[0]; if (trackIdCheck.length > 1) { [, cid] = trackIdCheck; } const target_url2 = `http://api.bilibili.com/x/player/playurl?fnval=16&bvid=${bvid}&cid=${cid}`; axios.get(target_url2).then((response2) => { if (response2.data.data.dash.audio.length > 0) { const url = response2.data.data.dash.audio[0].baseUrl; sound.url = url; sound.platform = 'bilibili'; success(sound); } else { failure(sound); } }); }); } const sound = {}; const song_id = track.id.slice('bitrack_'.length); const target_url = `https://www.bilibili.com/audio/music-service-c/web/url?sid=${song_id}`; return axios.get(target_url).then((response) => { const { data } = response; if (data.code === 0) { [sound.url] = data.data.cdns; sound.platform = 'bilibili'; success(sound); } else { failure(sound); } }); } static search(url) { return { success: (fn) => { const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const target_url = `https://api.bilibili.com/x/web-interface/search/type?__refresh__=true&_extra=&context=&page=${curpage}&page_size=42&platform=pc&highlight=1&single_column=0&keyword=${encodeURIComponent( keyword )}&category_id=&search_type=video&dynamic_offset=0&preload=true&com2co=true`; const domain = `https://api.bilibili.com`; const cookieName = 'buvid3'; const expire = (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; cookieSet( { url: domain, name: cookieName, value: '0', expirationDate: expire, sameSite: 'no_restriction', }, () => { axios .get(target_url, { withCredentials: true }) .then((response) => { const result = response.data.data.result.map((song) => this.bi_convert_song2(song) ); const total = response.data.data.numResults; return fn({ result, total, }); }); } ); }, }; } static lyric() { return { success: (fn) => { fn({ lyric: '', }); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'biplaylist': return this.bi_get_playlist(url); case 'bialbum': return this.bi_album(url); case 'biartist': return this.bi_artist(url); case 'bitrack': return this.bi_track(url); default: return null; } } static get_playlist_filters() { return { success: (fn) => fn({ recommend: [], all: [] }), }; } static get_user() { return { success: (fn) => fn({ status: 'fail', data: {} }), }; } static get_login_url() { return `https://www.bilibili.com`; } static logout() {} // return { // show_playlist: bi_show_playlist, // get_playlist_filters, // get_playlist, // parse_url: bi_parse_url, // bootstrap_track: bi_bootstrap_track, // search: bi_search, // lyric: bi_lyric, // get_user: bi_get_user, // get_login_url: bi_get_login_url, // logout: bi_logout, // }; } ================================================ FILE: js/provider/kugou.js ================================================ /* eslint-disable no-unused-vars */ /* global async getParameterByName */ class kugou { static kg_convert_song(song) { const track = { id: `kgtrack_${song.FileHash}`, title: song.SongName, artist: '', artist_id: '', album: song.AlbumName, album_id: `kgalbum_${song.AlbumID}`, source: 'kugou', source_url: `https://www.kugou.com/song/#hash=${song.FileHash}&album_id=${song.AlbumID}`, img_url: '', // url: `kgtrack_${song.FileHash}`, lyric_url: song.FileHash, }; let singer_id = song.SingerId; let singer_name = song.SingerName; if (song.SingerId instanceof Array) { [singer_id] = singer_id; [singer_name] = singer_name.split('、'); } track.artist = singer_name; track.artist_id = `kgartist_${singer_id}`; return track; } static async_process_list( data_list, handler, handler_extra_param_list, callback ) { const fnDict = {}; data_list.forEach((item, index) => { fnDict[index] = (cb) => handler(index, item, handler_extra_param_list, cb); }); async.parallel(fnDict, (err, results) => callback( null, data_list.map((item, index) => results[index]) ) ); } static kg_render_search_result_item(index, item, params, callback) { const track = kugou.kg_convert_song(item); // Add singer img const url = `${'https://www.kugou.com/yy/index.php?r=play/getdata&hash='}${ track.lyric_url }`; axios.get(url).then((response) => { const { data } = response; track.img_url = data.data.img; callback(null, track); }); } static search(url) { const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const searchType = getParameterByName('type', url); if (searchType === '1') { return { success: (fn) => { const target_url = `${'http://mobilecdnbj.kugou.com/api/v3/search/special?keyword='}${keyword}&pagesize=20&filter=0&page=${curpage}`; axios .get(target_url) .then((response) => { const result = response.data.data.info.map((item) => ({ id: `kgplaylist_${item.specialid}`, title: item.specialname, source: 'kugou', source_url: 'https://www.kugou.com/yy/special/single/{size}.html'.replace( '{size}', item.specialid ), img_url: item.imgurl ? item.imgurl.replace('{size}', '400') : '', url: `kgplaylist_${item.specialid}`, author: item.nickname, count: item.songcount, })); const { total } = response.data.data; return fn({ result, total, type: searchType, }); }) .catch(() => { fn({ result: [], total: 0, type: searchType, }); }); }, }; } return { success: (fn) => { const target_url = `${'https://songsearch.kugou.com/song_search_v2?keyword='}${keyword}&page=${curpage}`; axios .get(target_url) .then((response) => { const { data } = response; this.async_process_list( data.data.lists, this.kg_render_search_result_item, [], (err, tracks) => fn({ result: tracks, total: data.data.total, type: searchType, }) ); }) .catch(() => fn({ result: [], total: 0, type: searchType, }) ); }, }; } static kg_render_playlist_result_item(index, item, params, callback) { const { hash } = item; let target_url = `${'https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash='}${hash}`; const track = { id: `kgtrack_${hash}`, title: '', artist: '', artist_id: '', album: '', album_id: `kgalbum_${item.album_id}`, source: 'kugou', source_url: `https://www.kugou.com/song/#hash=${hash}&album_id=${item.album_id}`, img_url: '', lyric_url: hash, }; // Fix song info axios.get(target_url).then((response) => { const { data } = response; track.title = data.songName; track.artist = data.singerId === 0 ? '未知' : data.singerName; track.artist_id = `kgartist_${data.singerId}`; if (data.album_img !== undefined) { track.img_url = data.album_img.replace('{size}', '400'); } else { // track['img_url'] = data.imgUrl.replace('{size}', '400'); } // Fix album target_url = `http://mobilecdnbj.kugou.com/api/v3/album/info?albumid=${item.album_id}`; axios.get(target_url).then((res) => { const { data: res_data } = res; if ( res_data.status && res_data.data !== undefined && res_data.data !== null ) { track.album = res_data.data.albumname; } else { track.album = ''; } return callback(null, track); }); }); } static kg_get_playlist(url) { return { success: (fn) => { const list_id = getParameterByName('list_id', url).split('_').pop(); const target_url = `https://m.kugou.com/plist/list/${list_id}?json=true`; axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.info.list.imgurl ? data.info.list.imgurl.replace('{size}', '400') : '', title: data.info.list.specialname, id: `kgplaylist_${data.info.list.specialid}`, source_url: 'https://www.kugou.com/yy/special/single/{size}.html'.replace( '{size}', data.info.list.specialid ), }; this.async_process_list( data.list.list.info, this.kg_render_playlist_result_item, [], (err, tracks) => fn({ tracks, info, }) ); }); }, }; } static kg_render_artist_result_item(index, item, params, callback) { const info = params[0]; const track = { id: `kgtrack_${item.hash}`, title: '', artist: '', artist_id: info.id, album: '', album_id: `kgalbum_${item.album_id}`, source: 'kugou', source_url: `https://www.kugou.com/song/#hash=${item.hash}&album_id=${item.album_id}`, img_url: '', // url: `kgtrack_${item.hash}`, lyric_url: item.hash, }; const one = item.filename.split('-'); track.title = one[1].trim(); track.artist = one[0].trim(); // Fix album name and img const target_url = `${'https://www.kugou.com/yy/index.php?r=play/getdata&hash='}${ item.hash }`; axios .get( `http://mobilecdnbj.kugou.com/api/v3/album/info?albumid=${item.album_id}` ) .then((response) => { const { data } = response; if (data.status && data.data !== undefined) { track.album = data.data.albumname; } else { track.album = ''; } axios.get(target_url).then((res) => { track.img_url = res.data.data.img; callback(null, track); }); }); } static kg_artist(url) { return { success: (fn) => { const artist_id = getParameterByName('list_id', url).split('_').pop(); let target_url = `http://mobilecdnbj.kugou.com/api/v3/singer/info?singerid=${artist_id}`; axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.data.imgurl.replace('{size}', '400'), title: data.data.singername, id: `kgartist_${artist_id}`, source_url: 'https://www.kugou.com/singer/{id}.html'.replace( '{id}', artist_id ), }; target_url = `http://mobilecdnbj.kugou.com/api/v3/singer/song?singerid=${artist_id}&page=1&pagesize=30`; axios.get(target_url).then((res) => { this.async_process_list( res.data.data.info, this.kg_render_artist_result_item, [info], (err, tracks) => fn({ tracks, info, }) ); }); }); }, }; } static getTimestampString() { return new Date().getTime().toString(); } static getRandomIntString() { return (Math.random() * 100).toString().replace(/\D/g, ''); } static getRandomHexString() { let result = ''; const letters = '0123456789abcdef'; for (let i = 0; i < 16; i += 1) { result += letters[Math.floor(Math.random() * 16)]; } return result; } static bootstrap_track(track, success, failure) { const track_id = track.id.slice('kgtrack_'.length); const target_url = `https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=${track_id}`; axios.get(target_url).then((response) => { const { data: info } = response; const { url } = info; if (url === '') { return failure({}); } return success({ url, bitrate: `${info.bitRate}kbps`, platform: 'kugou', }); }); } static lyric(url) { const track_id = getParameterByName('track_id', url).split('_').pop(); const album_id = getParameterByName('album_id', url).split('_').pop(); let lyric_url = `https://wwwapi.kugou.com/yy/index.php?r=play/getdata&callback=jQuery&mid=1&hash=${track_id}&platid=4&album_id=${album_id}`; const timstamp = +new Date(); lyric_url += `&_=${timstamp}`; return { success: (fn) => { axios.get(lyric_url).then((response) => { const { data } = response; const jsonString = data.slice('jQuery('.length, data.length - 1 - 1); const info = JSON.parse(jsonString); return fn({ lyric: info.data.lyrics, }); }); }, }; } static kg_render_album_result_item(index, item, params, callback) { const info = params[0]; const album_id = params[1]; const track = { id: `kgtrack_${item.hash}`, title: '', artist: '', artist_id: '', album: info.title, album_id: `kgalbum_${album_id}`, source: 'kugou', source_url: `https://www.kugou.com/song/#hash=${item.hash}&album_id=${album_id}`, img_url: '', // url: `xmtrack_${item.hash}`, lyric_url: item.hash, }; // Fix other data const target_url = `${'https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash='}${ item.hash }`; axios.get(target_url).then((response) => { const { data } = response; track.title = data.songName; track.artist = data.singerId === 0 ? '未知' : data.singerName; track.artist_id = `kgartist_${data.singerId}`; track.img_url = data.imgUrl.replace('{size}', '400'); callback(null, track); }); } static kg_album(url) { return { success: (fn) => { const album_id = getParameterByName('list_id', url).split('_').pop(); let target_url = `${'http://mobilecdnbj.kugou.com/api/v3/album/info?albumid='}${album_id}`; let info; // info axios.get(target_url).then((response) => { const { data } = response; info = { cover_img_url: data.data.imgurl.replace('{size}', '400'), title: data.data.albumname, id: `kgalbum_${data.data.albumid}`, source_url: 'https://www.kugou.com/album/{id}.html'.replace( '{id}', data.data.albumid ), }; target_url = `${'http://mobilecdnbj.kugou.com/api/v3/album/song?albumid='}${album_id}&page=1&pagesize=-1`; axios.get(target_url).then((res) => { this.async_process_list( res.data.data.info, this.kg_render_album_result_item, [info, album_id], (err, tracks) => fn({ tracks, info, }) ); }); }); }, }; } static show_playlist(url) { let offset = getParameterByName('offset', url); if (offset === undefined) { offset = 0; } const page = offset / 30 + 1; const target_url = `${'https://m.kugou.com/plist/index&json=true&page='}${page}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; // const total = data.plist.total; const result = data.plist.list.info.map((item) => ({ cover_img_url: item.imgurl ? item.imgurl.replace('{size}', '400') : '', title: item.specialname, id: `kgplaylist_${item.specialid}`, source_url: 'https://www.kugou.com/yy/special/single/{size}.html'.replace( '{size}', item.specialid ), })); return fn({ result, }); }); }, }; } static parse_url(url) { let result; const match = /\/\/www.kugou.com\/yy\/special\/single\/([0-9]+).html/.exec( url ); if (match != null) { const playlist_id = match[1]; result = { type: 'playlist', id: `kgplaylist_${playlist_id}`, }; } return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { // eslint-disable-line no-unused-vars const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'kgplaylist': return this.kg_get_playlist(url); case 'kgalbum': return this.kg_album(url); case 'kgartist': return this.kg_artist(url); default: return null; } } static get_playlist_filters() { return { success: (fn) => fn({ recommend: [], all: [] }), }; } static get_user() { return { success: (fn) => fn({ status: 'fail', data: {} }), }; } static get_login_url() { return `https://www.kugou.com`; } static logout() {} // return { // show_playlist: kg_show_playlist, // get_playlist_filters, // get_playlist, // parse_url: kg_parse_url, // bootstrap_track: kg_bootstrap_track, // search: kg_search, // lyric: kg_lyric, // get_user: kg_get_user, // get_login_url: kg_get_login_url, // logout: kg_logout, // }; } ================================================ FILE: js/provider/kuwo.js ================================================ /* eslint-disable no-undef */ /* eslint-disable no-unused-vars */ /* global async getParameterByName isElectron */ function h(t, e) { // NOTICE: this function is from kuwo website, so eslint is skipped. /* eslint-disable */ if (null == e || e.length <= 0) return ( console.log('Please enter a password with which to encrypt the message.'), null ); for (var n = '', i = 0; i < e.length; i++) n += e.charCodeAt(i).toString(); var r = Math.floor(n.length / 5), o = parseInt( n.charAt(r) + n.charAt(2 * r) + n.charAt(3 * r) + n.charAt(4 * r) + n.charAt(5 * r) ), l = Math.ceil(e.length / 2), c = Math.pow(2, 31) - 1; if (o < 2) return ( console.log( 'Algorithm cannot find a suitable hash. Please choose a different password. \nPossible considerations are to choose a more complex or longer password.' ), null ); var d = Math.round(1e9 * Math.random()) % 1e8; for (n += d; n.length > 10; ) n = ( parseInt(n.substring(0, 10)) + parseInt(n.substring(10, n.length)) ).toString(); n = (o * n + l) % c; var h = '', f = ''; for (i = 0; i < t.length; i++) (f += (h = parseInt(t.charCodeAt(i) ^ Math.floor((n / c) * 255))) < 16 ? '0' + h.toString(16) : h.toString(16)), (n = (o * n + l) % c); for (d = d.toString(16); d.length < 8; ) d = '0' + d; return (f += d); } class kuwo { static forgeMD5(message) { const md = forge.md.sha1.create(); md.update(message); const sig1 = md.digest().toHex(); const sig2 = forge.md5 .create() .update(forge.util.encodeUtf8(sig1)) .digest() .toHex(); return sig2; } // Convert html code static html_decode(str) { let text = str; const entities = [ ['amp', '&'], ['apos', "'"], ['#x27', "'"], ['#x2F', '/'], ['#39', "'"], ['#47', '/'], ['lt', '<'], ['gt', '>'], ['nbsp', ' '], ['quot', '"'], ]; for (let i = 0, max = entities.length; i < max; i += 1) { text = text.replace( new RegExp(`&${entities[i][0]};`, 'g'), entities[i][1] ); } return text; } // Fix single quote in json static fix_json(data) { return data.replace(/(')/g, '"'); } static num2str(num) { // const t = parseInt(num, 10); return parseInt(num / 10, 10).toString() + (num % 10).toString(); } /* static kw_convert_song(item) { const song_id = item.MUSICRID.split('_').pop(); const track = { id: `kwtrack_${song_id}`, title: html_decode(item.SONGNAME), artist: html_decode(item.ARTIST), artist_id: `kwartist_${item.ARTISTID}`, album: html_decode(item.ALBUM), album_id: `kwalbum_${item.ALBUMID}`, source: 'kuwo', source_url: `https://www.kuwo.cn/play_detail/${song_id}`, img_url: '', // url: `kwtrack_${song_id}`, lyric_url: song_id, }; return track; } */ static kw_convert_song2(item) { return { id: `kwtrack_${item.rid}`, title: this.html_decode(item.name), artist: this.html_decode(item.artist), artist_id: `kwartist_${item.artistid}`, album: this.html_decode(item.album), album_id: `kwalbum_${item.albumid}`, source: 'kuwo', source_url: `https://www.kuwo.cn/play_detail/${item.rid}`, img_url: item.pic, // url: `kwtrack_${musicrid}`, lyric_url: item.rid, }; } static kw_convert_song3(item) { return { id: `kwtrack_${item.DC_TARGETID}`, title: this.html_decode(item.NAME), artist: this.html_decode(item.ARTIST), artist_id: `kwartist_${item.ARTISTID}`, album: this.html_decode(item.ALBUM), album_id: `kwalbum_${item.ALBUMID}`, source: 'kuwo', source_url: `https://www.kuwo.cn/play_detail/${item.DC_TARGETID}`, img_url: `https://img2.kuwo.cn/star/albumcover/${item.web_albumpic_short}`, // url: `kwtrack_${musicrid}`, lyric_url: item.DC_TARGETID, }; } /* function async_process_list(data_list, handler, handler_extra_param_list, callback) { const fnDict = {}; data_list.forEach((item, index) => { fnDict[index] = (cb) => handler(index, item, handler_extra_param_list, cb); }); async.parallel(fnDict, (err, results) => { callback(null, data_list.map((item, index) => results[index])); }); } function kw_add_song_pic_in_track(track, params, callback) { // Add song picture image const target_url = 'https://artistpicserver.kuwo.cn/pic.web?' + `type=rid_pic&pictype=url&size=240&rid=${track.lyric_url}`; axios.get(target_url) .then((response) => { const { data } = response; track.img_url = data; // eslint-disable-line no-param-reassign callback(null, track); }); } function kw_render_search_result_item(index, item, params, callback) { const track = kw_convert_song(item); kw_add_song_pic_in_track(track, params, callback); } function kw_render_artist_result_item(index, item, params, callback) { const track = { id: `kwtrack_${item.musicrid}`, title: html_decode(item.name), artist: item.artist, artist_id: `kwartist_${item.artistid}`, album: html_decode(item.album), album_id: `kwalbum_${item.albumid}`, source: 'kuwo', source_url: `https://www.kuwo.cn/play_detail/${item.musicrid}`, img_url: '', //url: `kwtrack_${item.musicrid}`, lyric_url: item.musicrid, }; kw_add_song_pic_in_track(track, params, callback); } function kw_render_album_result_item(index, item, params, callback) { const info = params[0]; const track = { id: `kwtrack_${item.id}`, title: html_decode(item.name), artist: item.artist, artist_id: `kwartist_${item.artistid}`, album: info.title, album_id: `kwalbum_${info.id}`, source: 'kuwo', source_url: `https://www.kuwo.cn/play_detail/${item.id}`, img_url: '', //url: `kwtrack_${item.id}`, lyric_url: item.id, }; kw_add_song_pic_in_track(track, params, callback); } function kw_render_playlist_result_item(index, item, params, callback) { const track = { id: `kwtrack_${item.id}`, title: html_decode(item.name), artist: item.artist, artist_id: `kwartist_${item.artistid}`, album: html_decode(item.album), album_id: `kwalbum_${item.albumid}`, source: 'kuwo', source_url: `https://www.kuwo.cn/play_detail/${item.id}`, img_url: '', //url: `kwtrack_${item.id}`, lyric_url: item.id, }; kw_add_song_pic_in_track(track, params, callback); } */ static kw_get_token(callback, isRetry) { let isRetryValue = true; if (isRetry === undefined) { isRetryValue = false; } else { isRetryValue = isRetry; } const domain = 'https://www.kuwo.cn'; const name = 'Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324'; cookieGet( { url: domain, name, }, (cookie) => { if (cookie == null) { if (isRetryValue) { return callback(''); } return axios.get('https://www.kuwo.cn/').then(() => { this.kw_get_token(callback, true); }); } return callback(cookie.value); } ); } static kw_cookie_get(url, callback) { const name = 'Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324'; this.kw_get_token((token) => { axios .get(url, { headers: { Secret: h(token, name), }, }) .then((response) => { if (response.data.success === false) { // token expire, refetch token and start get url this.kw_get_token((token2) => { axios .get(url, { headers: { Secret: h(token2, name), }, }) .then((res) => { callback(res); }); }); } else { callback(response); } }) .catch(() => { callback(); }); }); } static kw_render_tracks(url, page, callback) { const list_id = getParameterByName('list_id', url).split('_').pop(); const playlist_type = getParameterByName('list_id', url).split('_')[0]; let tracks_url = ''; switch (playlist_type) { case 'kwplaylist': // tracks_url = `https://m.kuwo.cn/newh5app/api/mobile/v1/music/playlist/${list_id}?pn=${page}&rn=1000` tracks_url = `https://www.kuwo.cn/api/www/playlist/playListInfo?pid=${list_id}&pn=${page}&rn=100&httpsStatus=1`; break; case 'kwalbum': // tracks_url = `https://m.kuwo.cn/newh5app/api/mobile/v1/music/album/${list_id}?rn=1000` tracks_url = `https://www.kuwo.cn/api/www/album/albumInfo?albumId=${list_id}&pn=${page}&rn=100&httpsStatus=1`; break; default: break; } // axios.get(tracks_url).then((response) => { this.kw_cookie_get(tracks_url, (response) => { const tracks = response.data.data.musicList.map((item) => this.kw_convert_song2(item) ); return callback(null, tracks); }); } static search(url) { // eslint-disable-line no-unused-vars const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const pn = parseInt(curpage) - 1; const searchType = getParameterByName('type', url); let api = ''; let target_url = ''; switch (searchType) { case '0': api = 'searchMusicBykeyWord'; target_url = `https://www.kuwo.cn/search/${api}?vipver=1&client=kt&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&mobi=1&issubtitle=1&show_copyright_off=1&pn=${pn}&rn=20&all=${keyword}`; break; case '1': api = 'searchPlayListBykeyWord'; target_url = `https://www.kuwo.cn/api/www/search/${api}?key=${keyword}&pn=${curpage}&rn=30`; break; default: break; } return { success: (fn) => { this.kw_cookie_get(target_url, (response) => { let result = []; let total = 0; if (response === undefined) { return fn({ result, total, type: searchType, }); } if (searchType === '0' && response.data.abslist !== undefined) { result = response.data.abslist.map((item) => this.kw_convert_song3(item) ); total = parseInt(response.data.HIT); } else if (searchType === '1' && response.data.data !== undefined) { result = response.data.data.list.map((item) => ({ id: `kwplaylist_${item.id}`, title: this.html_decode(item.name), source: 'kuwo', source_url: `https://www.kuwo.cn/playlist_detail/${item.id}`, img_url: item.img, url: `kwplaylist_${item.id}`, author: this.html_decode(item.uname), count: item.total, })); total = response.data.data.total; } return fn({ result, total, type: searchType, }); }); }, }; } // eslint-disable-next-line no-unused-vars static bootstrap_track(track, success, failure) { const sound = {}; const song_id = track.id.slice('kwtrack_'.length); const target_url = `https://www.kuwo.cn/api/v1/www/music/playUrl?mid=${song_id}&type=music&httpsStatus=1&reqId=&plat=web_www&from=`; this.kw_cookie_get(target_url, (response) => { const { data } = response; if (data && data.data && data.data.url) { sound.url = data.data.url; sound.platform = 'kuwo'; success(sound); } else { failure(sound); } }); } static kw_get_lrc(arr) { const lyric = arr.reduce((str, item) => { const t = parseFloat(item.time); const m = parseInt(t / 60, 10); const s = parseInt(t - m * 60, 10); const ms = parseInt((t - m * 60 - s) * 100, 10); return `${str}[${this.num2str(m)}:${this.num2str( parseInt(s, 10) )}.${this.num2str(ms)}]${item.lineLyric}\n`; }, ''); return lyric; } static kw_generate_translation(lrclist) { if (lrclist) { lrclist.filter((e) => e && e.lineLyric !== '//'); // 暂时原歌词和翻译都在原歌词显示 // 酷我的歌词格式中没区分,查看了几个歌词文件发现,翻译歌词也存在和原来歌词的时间轴不一致的情况 // 如果按照时间区分可能造成错行问题。 // 将歌词和翻译分成两个数组,并将对应歌词和翻译的时间调整为相等,数组最后一个数据无法做判断,故传给翻译数组做后续处理 // const lrc_arr = []; // const tlrc_arr = []; // let lrc_arr = lrclist.filter( // (item, index, self) => { // if (index < self.length - 1) { // if (Number(item.time) === 0) { // return item; // } // return item.time !== self[index + 1].time; // } // return null; // }, // ); // let tlrc_arr = lrclist.filter( // (item, index, self) => { // if (index < self.length - 1 && Number(item.time) !== 0 // && item.time === self[index + 1].time) { // return item.time === self[index - 1].time; // } // return (index === self.length - 1 ? item : null); // }, // ); // // tlrc_arr如只有一个即为没有翻译歌词 // if (tlrc_arr.length === 1) { // lrc_arr = [...lrc_arr, ...tlrc_arr]; // tlrc_arr = []; // } else { // tlrc_arr[tlrc_arr.length - 1].time = lrc_arr[lrc_arr.length - 1].time; // } return { lrc: kuwo.kw_get_lrc(lrclist), tlrc: kuwo.kw_get_lrc([]), }; } return { lrc: '', tlrc: '', }; } static lyric(url) { // eslint-disable-line no-unused-vars const track_id = getParameterByName('lyric_url', url); const target_url = `https://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${track_id}`; return { success: (fn) => { axios.get(target_url).then((response) => { let { data } = response; data = data.status === 200 ? this.kw_generate_translation(data.data.lrclist) : {}; return fn({ lyric: data.lrc || '', tlyric: data.tlrc || '', }); }); }, }; } static kw_artist(url) { // eslint-disable-line no-unused-vars const artist_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { let target_url = `https://www.kuwo.cn/api/www/artist/artist?artistid=${artist_id}`; this.kw_cookie_get(target_url, (response) => { const { data } = response.data; // data = JSON.parse(fix_json(data)); const info = { cover_img_url: data.pic300, title: this.html_decode(data.name), id: `kwartist_${data.id}`, source_url: `https://www.kuwo.cn/singer_detail/${data.id}`, }; // Get songs target_url = `https://www.kuwo.cn/api/www/artist/artistMusic?artistid=${artist_id}&pn=1&rn=50`; this.kw_cookie_get(target_url, (res) => { const tracks = res.data.data.list.map((item) => this.kw_convert_song2(item) ); return fn({ tracks, info, }); }); /* target_url = 'https://search.kuwo.cn/r.s?stype=artist2music' + '&sortby=0&alflac=1&pcmp4=1&encoding=utf8' + `&artistid=${artist_id}&pn=0&rn=100`; axios.get(target_url).then((response) => { let { data } = response; // TODO: Check JSON Schema is correct data = JSON.parse(fix_json(data)); async_process_list(data.musiclist, kw_render_artist_result_item, [], (err, tracks) => fn({ tracks, info, })); */ }); }, }; } static kw_album(url) { // eslint-disable-line no-unused-vars const album_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const target_url = 'https://search.kuwo.cn/r.s?pn=0&rn=0&stype=albuminfo' + `&albumid=${album_id}&alflac=1&pcmp4=1&encoding=utf8` + '&vipver=MUSIC_8.7.7.0_W4'; axios.get(target_url).then((response) => { let { data } = response; data = JSON.parse(this.fix_json(data)); const info = { cover_img_url: data.hts_img.replace('/120/', '/400/'), title: this.html_decode(data.name), id: `kwalbum_${data.albumid}`, source_url: `https://www.kuwo.cn/album_detail/${data.albumid}`, }; // Get songs const total = data.songnum; const page = Math.ceil(total / 100); const page_array = Array.from({ length: page }, (v, k) => k + 1); async.concat( page_array, (item, callback) => this.kw_render_tracks(url, item, callback), (err, tracks) => { fn({ tracks, info, }); } ); /* async_process_list(data.musiclist, kw_render_album_result_item, [info], (err, tracks) => fn({ tracks, info, })); */ }); }, }; } static show_playlist(url) { const offset = Number(getParameterByName('offset', url)); /* const id_available = { 1265: '经典', 577: '纯音乐', 621: '网络', 155: '怀旧', 1879: '网红', 220: '佛乐', 180: '影视', 578: '器乐', 1877: '游戏', 181: '二次元', 882: 'KTV', 216: '喊麦', 1366: '3D', 146: '伤感', 62: '放松', 58: '励志', 143: '开心', 137: '甜蜜', 139: '兴奋', 67: '安静', 66: '治愈', 147: '寂寞', 160: '四年', 366: '运动', 354: '睡前', 378: '跳舞', 1876: '学习', 353: '清晨', 359: '夜店', 382: '校园', 544: '亲热', 363: '咖啡店', 375: '旅行', 371: '散步', 386: '工作', 336: '婚礼', 637: '70后', 638: '80后', 639: '90后', 640: '00后', 268: '10后', 393: '流行', 391: '电子', 389: '摇滚', 1921: '民歌', 392: '民谣', 399: '乡村', 35: '欧洲', 37: '华语', }; */ // const target_url = 'https://www.kuwo.cn/www/categoryNew/getPlayListInfoUnderCategory?' // + `type=taglist&digest=10000&id=${37}&start=${offset}&count=50`; const target_url = `https://www.kuwo.cn/api/pc/classify/playlist/getRcmPlayList?pn=${ offset / 25 + 1 }&rn=25&order=hot&httpsStatus=1`; /* 精选歌单:roder=最热:hot, 最新:new tag歌单地址 https://www.kuwo.cn/api/pc/classify/playlist/getTagPlayList?pn=${offset / 25 + 1}&rn=25&id=37&httpsStatus=1 id =华语:37, */ return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response.data; if (!data) { return fn([]); } const result = data.data.map((item) => ({ cover_img_url: item.img, title: item.name, id: `kwplaylist_${item.id}`, source_url: `https://www.kuwo.cn/playlist_detail/${item.id}`, })); return fn({ result, }); }); }, }; } static kw_get_playlist(url) { // eslint-disable-line no-unused-vars const list_id = getParameterByName('list_id', url).split('_').pop(); const target_url = 'https://nplserver.kuwo.cn/pl.svc?' + 'op=getlistinfo&pn=0&rn=0&encode=utf-8&keyset=pl2012&pcmp4=1' + `&pid=${list_id}&vipver=MUSIC_9.0.2.0_W1&newver=1`; // https://www.kuwo.cn/api/www/playlist/playListInfo?pid=3134372426&pn=1&rn=30 return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.pic.replace('_150.jpg', '_400.jpg'), title: data.title, id: `kwplaylist_${data.id}`, source_url: `https://www.kuwo.cn/playlist_detail/${data.id}`, }; const { total } = data; const page = Math.ceil(total / 100); const page_array = Array.from({ length: page }, (v, k) => k + 1); async.concat( page_array, (item, callback) => this.kw_render_tracks(url, item, callback), (err, tracks) => { fn({ tracks, info, }); } ); /* async_process_list(data.musiclist, kw_render_playlist_result_item, [], (err, tracks) => fn({ tracks, info, })); */ }); }, }; } static parse_url(myurl) { let result; let id; let url = myurl; url = url.replace(/kuwo.cn\/(h5app|newh5(?:app){0,1})\//, 'kuwo.cn/'); url = url.replace(/kuwo.cn\/(album\/|\?albumid=)/, 'kuwo.cn/album_detail/'); url = url.replace(/kuwo.cn\/(artist|singers)\//, 'kuwo.cn/singer_detail/'); url = url.replace(/kuwo.cn\/playlist\//, 'kuwo.cn/playlist_detail/'); if (url.search('kuwo.cn/playlist_detail') !== -1) { const match = /kuwo.cn\/playlist_detail\/([0-9]+)/.exec(url); id = match ? match[1] : getParameterByName('pid', url); result = { type: 'playlist', id: `kwplaylist_${id}`, }; } else if (url.search('kuwo.cn/singer_detail') !== -1) { const match = /kuwo.cn\/singer_detail\/([0-9]+)/.exec(url); id = match ? match[1] : getParameterByName('id', url); result = { type: 'playlist', id: `kwartist_${id}`, }; } else if (url.search('kuwo.cn/album_detail') !== -1) { const match = /kuwo.cn\/album_detail\/([0-9]+)/.exec(url); if (match) { // eslint-disable-next-line prefer-destructuring id = match[1]; result = { type: 'playlist', id: `kwalbum_${id}`, }; } } return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'kwplaylist': return this.kw_get_playlist(url); case 'kwalbum': return this.kw_album(url); case 'kwartist': return this.kw_artist(url); default: return null; } } static get_playlist_filters() { return { success: (fn) => fn({ recommend: [], all: [] }), }; } static get_user() { return { success: (fn) => { fn({ status: 'fail', data: {} }); }, }; } static get_login_url() { return `https://www.kuwo.com`; } static logout() {} // return { // show_playlist: kw_show_playlist, // get_playlist_filters, // get_playlist, // parse_url: kw_parse_url, // bootstrap_track: kw_bootstrap_track, // search: kw_search, // lyric: kw_lyric, // get_user: kw_get_user, // get_login_url: kw_get_login_url, // logout: kw_logout, // }; } ================================================ FILE: js/provider/localmusic.js ================================================ /* eslint-disable no-param-reassign */ /* eslint-disable no-unused-vars */ /* global getParameterByName */ const defaultLocalMusicPlaylist = { tracks: [], info: { id: 'lmplaylist_reserve', cover_img_url: 'images/mycover.jpg', title: '本地音乐', source_url: '', }, }; class localmusic { static show_playlist(url, hm) { return { success: (fn) => fn({ result: [], }), }; } static lm_get_playlist(url) { const list_id = getParameterByName('list_id', url); return { success: (fn) => { let playlist = localStorage.getObject(list_id); if (playlist === null || playlist === undefined) { playlist = defaultLocalMusicPlaylist; } fn(playlist); }, }; } static lm_album(url) { const album = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const list_id = 'lmplaylist_reserve'; let playlist = localStorage.getObject(list_id); if (playlist === null || playlist === undefined) { playlist = JSON.parse(JSON.stringify(defaultLocalMusicPlaylist)); playlist.info.title = album; } else { playlist.info.title = album; playlist.tracks = playlist.tracks.filter((tr) => tr.album === album); } fn(playlist); }, }; } static lm_artist(url) { const artist = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const list_id = 'lmplaylist_reserve'; let playlist = localStorage.getObject(list_id); if (playlist === null || playlist === undefined) { playlist = JSON.parse(JSON.stringify(defaultLocalMusicPlaylist)); playlist.info.title = artist; } else { playlist.info.title = artist; playlist.tracks = playlist.tracks.filter( (tr) => tr.artist === artist ); } fn(playlist); }, }; } static bootstrap_track(track, success, failure) { const sound = {}; sound.url = track.sound_url; sound.platform = 'localmusic'; success(sound); } static lyric(url) { const track_id = getParameterByName('track_id', url); const playlist = localStorage.getObject('lmplaylist_reserve'); const track = playlist.tracks.find((item) => item.id === track_id); let lyric = ''; if (track.lyrics !== undefined) { [lyric] = track.lyrics; } return { success: (fn) => fn({ lyric, tlyric: '', }), }; } static add_playlist(list_id, tracks) { if (typeof tracks === 'string') { tracks = JSON.parse(tracks); } let playlist = localStorage.getObject(list_id); if (playlist === null) { playlist = JSON.parse(JSON.stringify(defaultLocalMusicPlaylist)); } const tracksIdSet = {}; tracks.forEach((tr) => { tracksIdSet[tr.id] = true; }); playlist.tracks = tracks.concat( playlist.tracks.filter((tr) => tracksIdSet[tr.id] !== true) ); localStorage.setObject(list_id, playlist); return { success: (fn) => fn({ list_id, playlist }), }; } static parse_url(url) { let result; return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'lmplaylist': return this.lm_get_playlist(url); case 'lmartist': return this.lm_artist(url); case 'lmalbum': return this.lm_album(url); default: return null; } } static remove_from_playlist(list_id, track_id) { const playlist = localStorage.getObject(list_id); if (playlist == null) { return; } const newtracks = playlist.tracks.filter((item) => item.id !== track_id); playlist.tracks = newtracks; localStorage.setObject(list_id, playlist); // eslint-disable-next-line consistent-return return { success: (fn) => fn(), }; } static get_playlist_filters() { return { success: (fn) => fn({ recommend: [], all: [] }), }; } // return { // show_playlist: lm_show_playlist, // get_playlist_filters, // get_playlist, // parse_url: lm_parse_url, // bootstrap_track: lm_bootstrap_track, // search: lm_search, // lyric: lm_lyric, // add_playlist: lm_add_playlist, // remove_from_playlist: lm_remove_from_playlist, // }; } ================================================ FILE: js/provider/migu.js ================================================ /* eslint-disable consistent-return */ /* eslint-disable no-unused-vars */ /* eslint-disable no-use-before-define */ /* global getParameterByName cookieRemove async forge */ class migu { static mg_convert_song(song) { return { id: `mgtrack_${song.copyrightId}`, title: song.songName, artist: song.artists ? song.artists[0].name : song.singer, artist_id: `mgartist_${ song.artists ? song.artists[0].id : song.singerId }`, album: song.albumId !== 1 ? song.album : '', album_id: song.albumId !== 1 ? `mgalbum_${song.albumId}` : 'mgalbum_', source: 'migu', source_url: `https://music.migu.cn/v3/music/song/${song.copyrightId}`, img_url: song.albumImgs[1].img, // url: `mgtrack_${song.copyrightId}`, lyric_url: song.lrcUrl || '', tlyric_url: song.trcUrl || '', quality: song.toneControl, url: song.copyright === 0 ? '' : undefined, song_id: song.songId, content_id: song.contentId, }; } static mg_convert_song2(song) { return { id: `mgtrack_${song.copyrightId}`, title: song.songName, artist: song.singerList ? song.singerList[0].name : song.singer, artist_id: `mgartist_${ song.singerList ? song.singerList[0].id : song.singerId }`, album: song.albumId !== 1 ? song.album : '', album_id: song.albumId !== 1 ? `mgalbum_${song.albumId}` : 'mgalbum_', source: 'migu', source_url: `https://music.migu.cn/v3/music/song/${song.copyrightId}`, img_url: song.img1, // url: `mgtrack_${song.copyrightId}`, lyric_url: song.ext ? song.ext.lrcUrl : '', tlyric_url: song.ext ? song.ext.trcUrl : '', quality: song.toneControl, url: song.copyright === 0 ? '' : undefined, song_id: song.songId, content_id: song.contentId, }; } static mg_render_tracks(url, page, callback) { const list_id = getParameterByName('list_id', url).split('_').pop(); const playlist_type = getParameterByName('list_id', url).split('_')[0]; let tracks_url = ''; switch (playlist_type) { case 'mgplaylist': tracks_url = `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${list_id}&pageNo=${page}&pageSize=50`; break; case 'mgalbum': tracks_url = `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/queryAlbumSong?albumId=${list_id}&pageNo=${page}&pageSize=50`; break; default: break; } axios.get(tracks_url).then((response) => { const data = playlist_type === 'mgplaylist' ? response.data.list : response.data.data.songList; const tracks = data.map((item) => this.mg_convert_song(item)); return callback(null, tracks); }); } static mg_show_toplist(offset) { if (offset !== undefined && offset > 0) { return { success: (fn) => fn({ result: [] }), }; } const url = 'https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-list/release?dataVersion=1616469593718&templateVersion=9'; return { success: (fn) => { axios.get(url).then((response) => { const migu_board = response.data.data.contentItemList[4].itemList.map( (item) => ({ cover_img_url: item.imageUrl, title: item.displayLogId.param.rankName, id: `mgtoplist_${item.displayLogId.param.rankId}`, source_url: '', }) ); migu_board.splice(0, 2); const global_board = response.data.data.contentItemList[7].itemList.map((item) => ({ cover_img_url: item.imageUrl, title: item.displayLogId.param.rankName, id: `mgtoplist_${item.displayLogId.param.rankId}`, source_url: '', })); const chart_board = [ { cover_img_url: 'https://cdnmusic.migu.cn/tycms_picture/20/02/36/20020512065402_360x360_2997.png', title: '尖叫新歌榜', id: 'mgtoplist_27553319', source: '', }, { cover_img_url: 'https://cdnmusic.migu.cn/tycms_picture/20/04/99/200408163640868_360x360_6587.png', title: '尖叫热歌榜', id: 'mgtoplist_27186466', source: '', }, { cover_img_url: 'https://cdnmusic.migu.cn/tycms_picture/20/04/99/200408163702795_360x360_1614.png', title: '尖叫原创榜', id: 'mgtoplist_27553408', source: '', }, ]; const result = chart_board.concat(migu_board, global_board); return fn({ result }); }); }, }; } static show_playlist(url) { const offset = Number(getParameterByName('offset', url)); const filterId = getParameterByName('filter_id', url); if (filterId === 'toplist') { return this.mg_show_toplist(offset); } const pageSize = 30; let target_url = ''; if (!filterId) { target_url = `https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=${pageSize}&start=${ offset / pageSize + 1 }&templateVersion=5&type=1`; } else { target_url = `https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/musiclistplaza-listbytag?pageNumber=${ offset / pageSize + 1 }&tagId=${filterId}&templateVersion=1`; // const target_url = `https://m.music.migu.cn/migu/remoting/playlist_bycolumnid_tag?playListType=2&type=1&columnId=15127315&tagId=&startIndex=${offset}`; // columnId=15127315为推荐,15127272为最新 } return { success: (fn) => { axios.get(target_url).then((response) => { const data = !filterId ? response.data.data.contentItemList[0].itemList : response.data.data.contentItemList.itemList; const result = data.map((item) => { const match = /id=([0-9]+)&/.exec(item.actionUrl); const id = match ? match[1] : ''; return { cover_img_url: item.imageUrl, title: item.title, id: `mgplaylist_${id}`, source_url: `https://music.migu.cn/v3/music/playlist/${id}`, }; }); fn({ result }); }); }, }; } static mg_toplist(url) { const list_id = Number(getParameterByName('list_id', url).split('_').pop()); return { success: (fn) => { const board_list = { 27553319: { name: '尖叫新歌榜', url: 'jianjiao_newsong', img: '/20/02/36/20020512065402_360x360_2997.png', }, 27186466: { name: '尖叫热歌榜', url: 'jianjiao_hotsong', img: '/20/04/99/200408163640868_360x360_6587.png', }, 27553408: { name: '尖叫原创榜', url: 'jianjiao_original', img: '/20/04/99/200408163702795_360x360_1614.png', }, 1: { name: '音乐榜', url: 'migumusic', img: '/20/05/136/200515161733982_360x360_1523.png', }, 2: { name: '影视榜', url: 'movies', img: '/20/05/136/200515161848938_360x360_673.png', }, 23189399: { name: '内地榜', url: 'mainland', img: '/20/08/231/200818095104122_327x327_4971.png', }, 23189800: { name: '港台榜', url: 'hktw', img: '/20/08/231/200818095125191_327x327_2382.png', }, 19190036: { name: '欧美榜', url: 'eur_usa', img: '/20/08/231/200818095229556_327x327_1383.png', }, 23189813: { name: '日韩榜', url: 'jpn_kor', img: '/20/08/231/200818095259569_327x327_4628.png', }, 23190126: { name: '彩铃榜', url: 'coloring', img: '/20/08/231/200818095356693_327x327_7955.png', }, 15140045: { name: 'KTV榜', url: 'ktv', img: '/20/08/231/200818095414420_327x327_4992.png', }, 15140034: { name: '网络榜', url: 'network', img: '/20/08/231/200818095442606_327x327_1298.png', }, 23218151: { name: '新专辑榜', url: 'newalbum', img: '/20/08/231/200818095603246_327x327_7480.png', }, 33683712: { name: '数字专辑畅销榜', url: '', img: 'https://d.musicapp.migu.cn/prod/file-service/file-down/bcb5ddaf77828caee4eddc172edaa105/2297b53efa678bbc8a5b83064622c4c8/ebfe5bff9fd9981b5ae1c043f743bfb3', }, 23217754: { name: 'MV榜', url: 'mv', img: '/20/08/231/200818095656365_327x327_8344.png', }, 21958042: { name: '美国iTunes榜', url: 'itunes', img: '/20/08/231/200818095755771_327x327_9250.png', }, 21975570: { name: '美国billboard榜', url: 'billboard', img: '/20/08/231/20081809581365_327x327_4636.png', }, 22272815: { name: 'Hito中文榜', url: 'hito', img: '/20/08/231/200818095834912_327x327_5042.png', }, 22272943: { name: '韩国Melon榜', url: 'mnet', img: '/20/08/231/200818095926828_327x327_3277.png', }, 22273437: { name: '英国UK榜', url: 'uk', img: '/20/08/231/200818095950791_327x327_8293.png', }, }; const target_url = `https://music.migu.cn/v3/music/top/${board_list[list_id].url}`; axios.get(target_url).then((response) => { const { data } = response; const info = { id: `mgtoplist_${list_id}`, cover_img_url: list_id === 33683712 ? board_list[list_id].img : `https://cdnmusic.migu.cn/tycms_picture${board_list[list_id].img}`, title: data.data ? data.data.columnInfo.title : board_list[list_id].name, source_url: `https://music.migu.cn/v3/music/top/${board_list[list_id].url}`, }; let tracks = []; const list_elements = new DOMParser() .parseFromString(data, 'text/html') .getElementsByTagName('script'); const result = JSON.parse( list_elements[1].innerText.split('=').slice(1).join('=') ); if (result.songs && result.songs.items) { tracks = result.songs.items.map((song) => { let songAlbum = ''; let songAlbumId = 'mgalbum_'; if (song.album && song.album.albumId !== 1) { songAlbum = song.album.albumName; songAlbumId = `mgalbum_${song.album.albumId}`; } const track = { id: `mgtrack_${song.copyrightId}`, title: song.name, artist: song.singers[0].name, artist_id: `mgartist_${song.singers[0].id}`, album: songAlbum, album_id: songAlbumId, source: 'migu', source_url: `https://music.migu.cn/v3/music/song/${song.copyrightId}`, img_url: `https:${song.mediumPic}`, // url: `mgtrack_${song.copyrightId}`, lyric_url: 'null', tlyric_url: '', song_id: song.id, url: undefined, }; if (song.bit24) { track.quality = '111111'; } else if (song.sq) { track.quality = '111100'; } else { track.quality = '110000'; } return track; }); } return fn({ tracks, info, }); }); }, }; } static mg_get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const info_url = `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?needSimple=00&resourceType=2021&resourceId=${list_id}`; axios.get(info_url).then((response) => { const info = { id: `mgplaylist_${list_id}`, cover_img_url: response.data.resource[0].imgItem.img, title: response.data.resource[0].title, source_url: `https://music.migu.cn/v3/music/playlist/${list_id}`, }; const total = response.data.resource[0].musicNum; const page = Math.ceil(total / 50); const page_array = Array.from({ length: page }, (v, k) => k + 1); async.concat( page_array, (item, callback) => this.mg_render_tracks(url, item, callback), (err, tracks) => { fn({ tracks, info, }); } ); }); }, }; } static mg_album(url) { const album_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const info_url = `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?needSimple=00&resourceType=2003&resourceId=${album_id}`; axios.get(info_url).then((response) => { const { data } = response; const info = { id: `mgalbum_${album_id}`, cover_img_url: data.resource[0].imgItems[1].img, title: data.resource[0].title, source_url: `https://music.migu.cn/v3/music/album/${album_id}`, }; const total = data.resource[0].totalCount; const page = Math.ceil(total / 50); const page_array = Array.from({ length: page }, (v, k) => k + 1); async.concat( page_array, (item, callback) => this.mg_render_tracks(url, item, callback), (err, tracks) => { fn({ tracks, info, }); } ); }); }, }; } static mg_artist(url) { const artist_id = getParameterByName('list_id', url).split('_').pop(); const offset = Number(getParameterByName('offset', url)); const pageSize = 50; const page = offset / pageSize + 1; const target_url = `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/singer_songs.do?pageNo=${page}&pageSize=${pageSize}&resourceType=2&singerId=${artist_id}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const info = { id: `mgartist_${artist_id}`, cover_img_url: data.singer.imgs[1].img, title: data.singer.singer, source_url: `https://music.migu.cn/v3/music/artist/${artist_id}/song`, }; const tracks = data.songlist.map((item) => this.mg_convert_song(item) ); return fn({ tracks, info, }); }); }, }; } static bootstrap_track(track, success, failure) { const sound = {}; const contentId = track.content_id; const copyrightId = track.id.slice('mgtrack_'.length); /* const copyrightId = track.id.slice('mgtrack_'.length); const type = 1; // NOTICE:howler flac support is not ready for production. // Sometimes network keep pending forever and block later music. // So use normal quality. // switch (track.quality) { // case '110000': // type = 2; // break; // case '111100': // type = 3; // break; // case '111111': // type = 4; // break; // default: // type = 1; // } const k = '4ea5c508a6566e76240543f8feb06fd457777be39549c4016436afda65d2330e'; // type parameter for music quality: 1: normal, 2: hq, 3: sq, 4: zq, 5: z3d const plain = forge.util.createBuffer( `{"copyrightId":"${copyrightId}","type":${type},"auditionsFlag":0}` ); const salt = forge.random.getBytesSync(8); const derivedBytes = forge.pbe.opensslDeriveBytes(k, salt, 48); const buffer = forge.util.createBuffer(derivedBytes); const key = buffer.getBytes(32); const iv = buffer.getBytes(16); const cipher = forge.cipher.createCipher('AES-CBC', key); cipher.start({ iv }); cipher.update(plain); cipher.finish(); const output = forge.util.createBuffer(); output.putBytes('Salted__'); output.putBytes(salt); output.putBuffer(cipher.output); const aesResult = forge.util.encode64(output.bytes()); const publicKey = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKW\nVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin\n2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exRe\nZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----'; const secKey = forge.util.encode64( forge.pki.publicKeyFromPem(publicKey).encrypt(k) ); const target_url = `https://music.migu.cn/v3/api/music/audioPlayer/getPlayInfo?dataType=2&data=${encodeURIComponent( aesResult )}&secKey=${encodeURIComponent(secKey)}`; */ let toneFlag; switch (track.quality) { case '110000': toneFlag = 'HQ'; break; case '111100': toneFlag = 'SQ'; break; case '111111': toneFlag = 'ZQ'; break; default: toneFlag = 'PQ'; } // const target_url = `https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.2?netType=01&resourceType=E&songId=${songId}&toneFlag=${toneFlag}`; const target_url = `https://app.c.nf.migu.cn/MIGUM3.0/strategy/pc/listen/v1.0?scene=&netType=01&resourceType=2©rightId=${copyrightId}&contentId=${contentId}&toneFlag=${toneFlag}`; axios .get(target_url, { headers: { channel: '0146951', uid: 1234, }, }) .then((response) => { // const { data } = response.data; // let playUrl = response.data.data ? response.data.data.playUrl : null; let playUrl = response.data.data ? response.data.data.url : null; if (playUrl) { if (playUrl.startsWith('//')) { playUrl = `https:${playUrl}`; } sound.url = playUrl.replace(/\+/g, '%2B'); // eslint-disable-line no-param-reassign sound.platform = 'migu'; switch (toneFlag) { case 'HQ': sound.bitrate = '320kbps'; break; case 'SQ': sound.bitrate = '999kbps'; break; case 'ZQ': sound.bitrate = '999kbps'; break; default: sound.bitrate = '128kbps'; } success(sound); } else { failure(sound); } }); } static search(url) { const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const searchType = getParameterByName('type', url); // const sid = (this.uuid() + this.uuid()).replace(/-/g, ''); // // let type =''; // let searchSwitch = ''; // let target_url = // 'https://jadeite.migu.cn/music_search/v2/search/searchAll?'; // switch (searchType) { // case '0': // searchSwitch = '{"song":1}'; // {"song":1,"album":0,"singer":0,"tagSong":1,"mvSong":0,"bestShow":1,"songlist":0,"lyricSong":0} // // type = 2; // target_url = // `${target_url}sid=${sid}&isCorrect=1&isCopyright=1` + // `&searchSwitch=${encodeURIComponent(searchSwitch)}&pageSize=20` + // `&text=${encodeURIComponent(keyword)}&pageNo=${curpage}` + // '&feature=1000000000&sort=1'; // break; // case '1': // searchSwitch = '{"songlist":1}'; // // type = 6; // target_url = // `${target_url}sid=${sid}&isCorrect=1&isCopyright=1` + // `&searchSwitch=${encodeURIComponent(searchSwitch)}` + // '&userFilter=%7B%22songlisttag%22%3A%5B%5D%7D&pageSize=20' + // `&text=${encodeURIComponent(keyword)}&pageNo=${curpage}` + // // + `&sort=1&userSort=%7B%22songlist%22%3A%22default%22%7D`; // '&feature=0000000010&sort=1'; // break; // default: // break; // } // const target_url = `https://pd.musicapp.migu.cn/MIGUM3.0/v1.0/content/search_all.do?&isCopyright=0&isCorrect=0&text=${keyword}&pageNo=${curpage}&searchSwitch=${searchSwitch}`; // const target_url = `https://m.music.migu.cn/migu/remoting/scr_search_tag?rows=20&type=${type}&keyword=${keyword}'&pgc=${curpage}`; let target_url = `https://app.u.nf.migu.cn/pc/resource/song/item/search/v1.0?text=${keyword}&pageNo=${curpage}&pageSize=20`; if (searchType === '1') { target_url = `https://app.u.nf.migu.cn/pc/v1.0/content/search_all.do?text=${keyword}&pageNo=${curpage}&pageSize=20&searchSwitch={%22songlist%22:+1}`; } // const deviceId = forge.md5 // .create() // .update(this.uuid().replace(/-/g, '')) // .digest() // .toHex() // .toLocaleUpperCase(); // 设备的UUID // const timestamp = new Date().getTime(); // const signature_md5 = '6cdc72a439cef99a3418d2a78aa28c73'; // app签名证书的md5 // const text = `${ // keyword + signature_md5 // }yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${timestamp}`; // const sign = forge.md5 // .create(text) // .update(forge.util.encodeUtf8(text)) // .digest() // .toHex(); // const headers = { // // android_id: 'db2cd8c4cdc1345f', // appId: 'yyapp2', // // brand: 'google', // // channel: '0147151', // deviceId, // // HWID: '', // // IMEI: '', // // IMSI: '', // // ip: '192.168.1.101', // // mac: '02:00:00:00:00:00', // // 'mgm-Network-standard': '01', // // 'mgm-Network-type': '04', // // mode: 'android', // // msisdn: '', // // OAID: '', // // os: 'android 7.0', // // osVersion: 'android 7.0', // // platform: 'G011C', // sign, // timestamp, // // ua: 'Android_migu', // // uid: '', // uiVersion: 'A_music_3.3.0', // version: '7.0.4', // }; return { success: (fn) => { axios .get(target_url, { // headers, }) .then((response) => { const { data } = response; let result = []; let total = 0; if (searchType === '0') { if (data) { result = data.map((item) => this.mg_convert_song2(item)); // no total available total = 1000; } } else if (searchType === '1') { if (data.songListResultData.result) { result = data.songListResultData.result.map((item) => ({ // result = data.songLists.map(item => ({ id: `mgplaylist_${item.id}`, title: item.name, source: 'migu', source_url: `https://music.migu.cn/v3/music/playlist/${item.id}`, // img_url: item.img, img_url: item.imgItems[0].img, url: `mgplaylist_${item.id}`, author: item.userName, count: item.musicNum, })); total = data.songListResultData.totalCount; } } return fn({ result, total, type: searchType, }); }); }, }; } // https://abhishekdutta.org/blog/standalone_uuid_generator_in_javascript.html static uuid() { const temp_url = URL.createObjectURL(new Blob()); const strTemp = temp_url.toString(); URL.revokeObjectURL(temp_url); return strTemp.substr(strTemp.lastIndexOf('/') + 1); // remove prefix (e.g. blob:null/, blob:www.test.com/, ...) } static lyric(url) { const lyric_url = getParameterByName('lyric_url', url); const tlyric_url = getParameterByName('tlyric_url', url); return { success: (fn) => { if (lyric_url !== 'null') { async.parallel( [ (callback) => { if (lyric_url) { axios .get(lyric_url) .then((response) => callback(null, response.data)); } else { return callback(null, '[00:00.00]暂无歌词\r\n[00:02.00]\r\n'); } }, (callback) => { if (tlyric_url) { axios .get(tlyric_url) .then((response) => callback(null, response.data)); } else { return callback(null, ''); } }, ], (err, results) => { const data = this.mg_generate_translation(results[0], results[1]); return fn({ lyric: data.lrc, tlyric: data.tlrc, }); } ); } else { const song_id = getParameterByName('track_id', url).split('_').pop(); const target_url = `https://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=${song_id}`; axios.get(target_url).then((response) => { const data = this.mg_generate_translation( response.data.lyric, response.data.translatedLyric ); return fn({ lyric: data.lrc, tlyric: data.tlrc, }); }); } }, }; } static mg_generate_translation(plain, translation) { if (!translation) { return { lrc: plain, tlrc: '', }; } const arr_plain = plain.split('\n'); let arr_translation = translation.split('\n'); // 歌词和翻译顶部信息不一定都有,会导致行列对不齐,所以删掉 const reg_head = /\[(ti|ar|al|by|offset|kana|high):/; let plain_head_line = 0; let trans_head_line = 0; for (let i = 0; i < 7; i += 1) { if (reg_head.test(arr_plain[i])) { plain_head_line += 1; } if (reg_head.test(arr_translation[i])) { trans_head_line += 1; } } arr_plain.splice(0, plain_head_line); arr_translation.splice(0, trans_head_line); // 删除翻译与原歌词重复的歌曲名,歌手、作曲、作词等信息 const reg_info = /(\u4f5c|\u7f16)(\u8bcd|\u66f2)|\u6b4c(\u624b|\u66f2)\u540d|Written by/; let trans_info_line = 0; for (let i = 0; i < 6; i += 1) { if (reg_info.test(arr_translation[i])) { trans_info_line += 1; } } arr_translation = arr_translation.splice(trans_info_line); const tlrc = arr_translation.join('\r\n'); return { lrc: plain, tlrc, }; } static parse_url(url) { let result; // eslint-disable-next-line no-param-reassign url = url.replace( 'music.migu.cn/v3/my/playlist/', 'music.migu.cn/v3/music/playlist/' ); const regex = /\/\/music.migu.cn\/v3\/music\/playlist\/([0-9]+)/g; const regex_result = regex.exec(url); if (regex_result) { result = { type: 'playlist', id: `mgplaylist_${regex_result[1]}`, }; } return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'mgplaylist': return this.mg_get_playlist(url); case 'mgalbum': return this.mg_album(url); case 'mgartist': return this.mg_artist(url); case 'mgtoplist': return this.mg_toplist(url); default: return null; } } static get_playlist_filters() { return { success: (fn) => { let target_url = 'https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/musiclistplaza-hottaglist/release'; axios.get(target_url).then((response) => { const recommend = response.data.data.contentItemList.map((item) => ({ id: item.tagId, name: item.tagName, })); recommend.unshift( { id: '', name: '推荐' }, { id: 'toplist', name: '排行榜' } ); target_url = 'https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/musiclistplaza-taglist/release?templateVersion=1'; axios.get(target_url).then((res) => { const all = res.data.data.map((cate) => { const result = { category: cate.header.title }; result.filters = cate.content.map((item) => ({ id: item.texts[1], name: item.texts[0], })); return result; }); return fn({ recommend, all, }); }); }); }, }; } static get_user() { const ts = +new Date(); const url = `https://music.migu.cn/v3/api/user/getUserInfo?_=${ts}`; return { success: (fn) => { axios.get(url).then((res) => { let result = { is_login: false }; let status = 'fail'; if (res.data.success) { status = 'success'; const { data } = res; result = { is_login: true, user_id: data.user.uid, user_name: data.user.mobile, nickname: data.user.nickname, avatar: data.user.avatar.midAvatar, platform: 'migu', data, }; } return fn({ status, data: result, }); }); }, }; } static get_login_url() { return `https://music.migu.cn`; } static logout() { const removeFn = (url, name) => cookieRemove( { url, name, }, () => {} ); const musicCookieList = [ 'migu_music_sid', 'migu_music_platinum', 'migu_music_level', 'migu_music_nickname', 'migu_music_avatar', 'migu_music_uid', 'migu_music_credit_level', 'migu_music_passid', 'migu_music_email', 'migu_music_msisdn', 'migu_music_status', ]; const passportCookieList = ['USessionID', 'LTToken']; musicCookieList.map((name) => removeFn('https://music.migu.cn', name)); passportCookieList.map((name) => removeFn('https://passport.migu.cn', name) ); } // return { // show_playlist: mg_show_playlist, // get_playlist_filters, // get_playlist, // parse_url: mg_parse_url, // bootstrap_track: mg_bootstrap_track, // search: mg_search, // lyric: mg_lyric, // get_user: migu_get_user, // get_login_url: migu_get_login_url, // logout: mg_logout, // }; } ================================================ FILE: js/provider/netease.js ================================================ /* eslint-disable no-underscore-dangle */ /* eslint-disable no-unused-vars */ /* global getParameterByName forge */ /* global isElectron cookieSet cookieGet cookieRemove async */ class netease { static _create_secret_key(size) { const result = []; const choice = '012345679abcdef'.split(''); for (let i = 0; i < size; i += 1) { const index = Math.floor(Math.random() * choice.length); result.push(choice[index]); } return result.join(''); } static _aes_encrypt(text, sec_key, algo) { const cipher = forge.cipher.createCipher(algo, sec_key); cipher.start({ iv: '0102030405060708' }); cipher.update(forge.util.createBuffer(text)); cipher.finish(); return cipher.output; } static _rsa_encrypt(text, pubKey, modulus) { text = text.split('').reverse().join(''); // eslint-disable-line no-param-reassign const n = new forge.jsbn.BigInteger(modulus, 16); const e = new forge.jsbn.BigInteger(pubKey, 16); const b = new forge.jsbn.BigInteger(forge.util.bytesToHex(text), 16); const enc = b.modPow(e, n).toString(16).padStart(256, '0'); return enc; } static weapi(text) { const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b72' + '5152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbd' + 'a92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe48' + '75d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'; const nonce = '0CoJUm6Qyw8W8jud'; const pubKey = '010001'; text = JSON.stringify(text); // eslint-disable-line no-param-reassign const sec_key = this._create_secret_key(16); const enc_text = btoa( this._aes_encrypt( btoa(this._aes_encrypt(text, nonce, 'AES-CBC').data), sec_key, 'AES-CBC' ).data ); const enc_sec_key = this._rsa_encrypt(sec_key, pubKey, modulus); const data = { params: enc_text, encSecKey: enc_sec_key, }; return data; } // refer to https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js static eapi(url, object) { const eapiKey = 'e82ckenh8dichen8'; const text = typeof object === 'object' ? JSON.stringify(object) : object; const message = `nobody${url}use${text}md5forencrypt`; const digest = forge.md5 .create() .update(forge.util.encodeUtf8(message)) .digest() .toHex(); const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`; return { params: this._aes_encrypt(data, eapiKey, 'AES-ECB').toHex().toUpperCase(), }; } static ne_show_toplist(offset) { if (offset !== undefined && offset > 0) { return { success: (fn) => fn({ result: [] }), }; } const url = 'https://music.163.com/weapi/toplist/detail'; const data = this.weapi({}); return { success: (fn) => { axios.post(url, new URLSearchParams(data)).then((response) => { const result = []; response.data.list.forEach((item) => { const playlist = { cover_img_url: item.coverImgUrl, id: `neplaylist_${item.id}`, source_url: `https://music.163.com/#/playlist?id=${item.id}`, title: item.name, }; result.push(playlist); }); return fn({ result }); }); }, }; } static show_playlist(url) { const order = 'hot'; const offset = getParameterByName('offset', url); const filterId = getParameterByName('filter_id', url); if (filterId === 'toplist') { return this.ne_show_toplist(offset); } let filter = ''; if (filterId !== '') { filter = `&cat=${filterId}`; } let target_url = ''; if (offset != null) { target_url = `https://music.163.com/discover/playlist/?order=${order}${filter}&limit=35&offset=${offset}`; } else { target_url = `https://music.163.com/discover/playlist/?order=${order}${filter}`; } return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const list_elements = Array.from( new DOMParser() .parseFromString(data, 'text/html') .getElementsByClassName('m-cvrlst')[0].children ); const result = list_elements.map((item) => ({ cover_img_url: item .getElementsByTagName('img')[0] .src.replace('140y140', '512y512'), title: item .getElementsByTagName('div')[0] .getElementsByTagName('a')[0].title, id: `neplaylist_${getParameterByName( 'id', item.getElementsByTagName('div')[0].getElementsByTagName('a')[0] .href )}`, source_url: `https://music.163.com/#/playlist?id=${getParameterByName( 'id', item.getElementsByTagName('div')[0].getElementsByTagName('a')[0] .href )}`, })); return fn({ result, }); }); }, }; } static ne_ensure_cookie(callback) { const domain = 'https://music.163.com'; const nuidName = '_ntes_nuid'; const nnidName = '_ntes_nnid3'; const nuidValue = this._create_secret_key(32); const nnidValue = `${nuidValue},${new Date().getTime()}`; const nmtidName = 'NMTID'; const nmtidValue = '0'; // netease default cookie expire time: 100 years const expire = (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; async.concat( [ { url: domain, name: nuidName }, { url: domain, name: nnidName }, { url: domain, name: nmtidName }, ], (item, cb) => { cookieGet(item, (result) => cb(null, result)); }, (_, results) => { if (results.filter((i) => i === null).length > 0) { async.concat( [ { url: domain, name: nuidName, value: nuidValue, expirationDate: expire, sameSite: 'no_restriction', }, { url: domain, name: nnidName, value: nnidValue, expirationDate: expire, sameSite: 'no_restriction', }, { url: domain, name: nmtidName, value: nmtidValue, expirationDate: expire, sameSite: 'no_restriction', }, ], cookieSet, () => { callback(null); } ); } } ); } static async_process_list( data_list, handler, handler_extra_param_list, callback ) { const fnDict = {}; data_list.forEach((item, index) => { fnDict[index] = (cb) => handler(index, item, handler_extra_param_list, cb); }); async.parallel(fnDict, (err, results) => callback( null, data_list.map((item, index) => results[index]) ) ); } static ng_render_playlist_result_item(index, item, callback) { const target_url = 'https://music.163.com/weapi/v3/song/detail'; const queryIds = [item.id]; const d = { c: `[${queryIds.map((id) => `{"id":${id}}`).join(',')}]`, ids: `[${queryIds.join(',')}]`, }; const data = this.weapi(d); axios .post(target_url, new URLSearchParams(data).toString()) .then((response) => { const track_json = response.data.songs[0]; const track = { id: `netrack_${track_json.id}`, title: track_json.name, artist: track_json.ar[0].name, artist_id: `neartist_${track_json.ar[0].id}`, album: track_json.al.name, album_id: `nealbum_${track_json.al.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${track_json.id}`, img_url: track_json.al.picUrl, // url: `netrack_${track_json.id}`, }; return callback(null, track); }); } static ng_parse_playlist_tracks(playlist_tracks, callback) { const target_url = 'https://music.163.com/weapi/v3/song/detail'; const track_ids = playlist_tracks.map((i) => i.id); const d = { c: `[${track_ids.map((id) => `{"id":${id}}`).join(',')}]`, ids: `[${track_ids.join(',')}]`, }; const data = this.weapi(d); axios.post(target_url, new URLSearchParams(data)).then((response) => { const tracks = response.data.songs.map((track_json) => ({ id: `netrack_${track_json.id}`, title: track_json.name, artist: track_json.ar[0].name, artist_id: `neartist_${track_json.ar[0].id}`, album: track_json.al.name, album_id: `nealbum_${track_json.al.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${track_json.id}`, img_url: track_json.al.picUrl, // url: `netrack_${track_json.id}`, })); return callback(null, tracks); }); } static split_array(myarray, size) { const count = Math.ceil(myarray.length / size); const result = []; for (let i = 0; i < count; i += 1) { result.push(myarray.slice(i * size, (i + 1) * size)); } return result; } static ne_get_playlist(url) { // special thanks for @Binaryify // https://github.com/Binaryify/NeteaseCloudMusicApi return { success: (fn) => { const list_id = getParameterByName('list_id', url).split('_').pop(); const target_url = 'https://music.163.com/weapi/v3/playlist/detail'; const d = { id: list_id, offset: 0, total: true, limit: 1000, n: 1000, csrf_token: '', }; const data = this.weapi(d); this.ne_ensure_cookie(() => { axios.post(target_url, new URLSearchParams(data)).then((response) => { const { data: res_data } = response; const info = { id: `neplaylist_${list_id}`, cover_img_url: res_data.playlist.coverImgUrl, title: res_data.playlist.name, source_url: `https://music.163.com/#/playlist?id=${list_id}`, }; const max_allow_size = 1000; const trackIdsArray = this.split_array( res_data.playlist.trackIds, max_allow_size ); function ng_parse_playlist_tracks_wrapper(trackIds, callback) { return netease.ng_parse_playlist_tracks(trackIds, callback); } async.concat( trackIdsArray, ng_parse_playlist_tracks_wrapper, (err, tracks) => { fn({ tracks, info }); } ); // request every tracks to fetch song info // async_process_list(res_data.playlist.trackIds, ng_render_playlist_result_item, // (err, tracks) => fn({ // tracks, // info, // })); }); }); }, }; } static bootstrap_track(track, success, failure) { const sound = {}; const target_url = `https://interface3.music.163.com/eapi/song/enhance/player/url`; let song_id = track.id; const eapiUrl = '/api/song/enhance/player/url'; song_id = song_id.slice('netrack_'.length); const d = { ids: `[${song_id}]`, br: 999000, }; const data = this.eapi(eapiUrl, d); const expire = (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; cookieSet( { url: 'https://interface3.music.163.com', name: 'os', value: 'pc', expirationDate: expire, }, (cookie) => { axios.post(target_url, new URLSearchParams(data)).then((response) => { const { data: res_data } = response; const { url, br } = res_data.data[0]; if (url != null) { sound.url = url; const bitrate = `${(br / 1000).toFixed(0)}kbps`; sound.bitrate = bitrate; sound.platform = 'netease'; success(sound); } else { failure(sound); } }); } ); } static is_playable(song) { return song.fee !== 4 && song.fee !== 1; } static search(url) { // use chrome extension to modify referer. const target_url = 'https://music.163.com/api/search/pc'; const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const searchType = getParameterByName('type', url); let ne_search_type = '1'; if (searchType === '1') { ne_search_type = '1000'; } const req_data = { s: keyword, offset: 20 * (curpage - 1), limit: 20, type: ne_search_type, }; return { success: (fn) => { this.ne_ensure_cookie(() => { axios .post(target_url, new URLSearchParams(req_data)) .then((response) => { const { data } = response; let result = []; let total = 0; if (searchType === '0') { result = data.result.songs.map((song_info) => ({ id: `netrack_${song_info.id}`, title: song_info.name, artist: song_info.artists[0].name, artist_id: `neartist_${song_info.artists[0].id}`, album: song_info.album.name, album_id: `nealbum_${song_info.album.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${song_info.id}`, img_url: song_info.album.picUrl, // url: `netrack_${song_info.id}`, url: !this.is_playable(song_info) ? '' : undefined, })); total = data.result.songCount; } else if (searchType === '1') { result = data.result.playlists.map((info) => ({ id: `neplaylist_${info.id}`, title: info.name, source: 'netease', source_url: `https://music.163.com/#/playlist?id=${info.id}`, img_url: info.coverImgUrl, url: `neplaylist_${info.id}`, author: info.creator.nickname, count: info.trackCount, })); total = data.result.playlistCount; } return fn({ result, total, type: searchType, }); }) .catch(() => fn({ result: [], total: 0, type: searchType, }) ); }); }, }; } static ne_album(url) { const album_id = getParameterByName('list_id', url).split('_').pop(); // use chrome extension to modify referer. const target_url = `https://music.163.com/api/album/${album_id}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.album.picUrl, title: data.album.name, id: `nealbum_${data.album.id}`, source_url: `https://music.163.com/#/album?id=${data.album.id}`, }; const tracks = data.album.songs.map((song_info) => ({ id: `netrack_${song_info.id}`, title: song_info.name, artist: song_info.artists[0].name, artist_id: `neartist_${song_info.artists[0].id}`, album: song_info.album.name, album_id: `nealbum_${song_info.album.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${song_info.id}`, img_url: song_info.album.picUrl, url: !this.is_playable(song_info) ? '' : undefined, })); return fn({ tracks, info, }); }); }, }; } static ne_artist(url) { const artist_id = getParameterByName('list_id', url).split('_').pop(); // use chrome extension to modify referer. const target_url = `https://music.163.com/api/artist/${artist_id}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.artist.picUrl, title: data.artist.name, id: `neartist_${data.artist.id}`, source_url: `https://music.163.com/#/artist?id=${data.artist.id}`, }; const tracks = data.hotSongs.map((song_info) => ({ id: `netrack_${song_info.id}`, title: song_info.name, artist: song_info.artists[0].name, artist_id: `neartist_${song_info.artists[0].id}`, album: song_info.album.name, album_id: `nealbum_${song_info.album.id}`, source: 'netease', source_url: `https://music.163.com/#/song?id=${song_info.id}`, img_url: song_info.album.picUrl, // url: `netrack_${song_info.id}`, url: !this.is_playable(song_info) ? '' : undefined, })); return fn({ tracks, info, }); }); }, }; } static lyric(url) { const track_id = getParameterByName('track_id', url).split('_').pop(); // use chrome extension to modify referer. const target_url = 'https://music.163.com/weapi/song/lyric?csrf_token='; const csrf = ''; const d = { id: track_id, lv: -1, tv: -1, csrf_token: csrf, }; const data = this.weapi(d); return { success: (fn) => { axios.post(target_url, new URLSearchParams(data)).then((response) => { const { data: res_data } = response; let lrc = ''; let tlrc = ''; if (res_data.lrc != null) { lrc = res_data.lrc.lyric; } if (res_data.tlyric != null && res_data.tlyric.lyric != null) { // eslint-disable-next-line no-control-regex tlrc = res_data.tlyric.lyric.replace(/(|\\)/g, ''); tlrc = tlrc.replace(/[\u2005]+/g, ' '); } return fn({ lyric: lrc, tlyric: tlrc, }); }); }, }; } static parse_url(url) { let result; let id = ''; // eslint-disable-next-line no-param-reassign url = url.replace( 'music.163.com/#/discover/toplist?', 'music.163.com/#/playlist?' ); // eslint-disable-line no-param-reassign url = url.replace('music.163.com/#/my/m/music/', 'music.163.com/'); // eslint-disable-line no-param-reassign url = url.replace('music.163.com/#/m/', 'music.163.com/'); // eslint-disable-line no-param-reassign url = url.replace('music.163.com/#/', 'music.163.com/'); // eslint-disable-line no-param-reassign if (url.search('//music.163.com/playlist') !== -1) { const match = /\/\/music.163.com\/playlist\/([0-9]+)/.exec(url); id = match ? match[1] : getParameterByName('id', url); result = { type: 'playlist', id: `neplaylist_${id}`, }; } else if (url.search('//music.163.com/artist') !== -1) { result = { type: 'playlist', id: `neartist_${getParameterByName('id', url)}`, }; } else if (url.search('//music.163.com/album') !== -1) { const match = /\/\/music.163.com\/album\/([0-9]+)/.exec(url); id = match ? match[1] : getParameterByName('id', url); result = { type: 'playlist', id: `nealbum_${id}`, }; } return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'neplaylist': return this.ne_get_playlist(url); case 'nealbum': return this.ne_album(url); case 'neartist': return this.ne_artist(url); default: return null; } } static get_playlist_filters() { const recommend = [ { id: '', name: '全部' }, { id: 'toplist', name: '排行榜' }, { id: '流行', name: '流行' }, { id: '民谣', name: '民谣' }, { id: '电子', name: '电子' }, { id: '舞曲', name: '舞曲' }, { id: '说唱', name: '说唱' }, { id: '轻音乐', name: '轻音乐' }, { id: '爵士', name: '爵士' }, { id: '乡村', name: '乡村' }, ]; const all = [ { category: '语种', filters: [ { id: '华语', name: '华语' }, { id: '欧美', name: '欧美' }, { id: '日语', name: '日语' }, { id: '韩语', name: '韩语' }, { id: '粤语', name: '粤语' }, ], }, { category: '风格', filters: [ { id: '流行', name: '流行' }, { id: '民谣', name: '民谣' }, { id: '电子', name: '电子' }, { id: '舞曲', name: '舞曲' }, { id: '说唱', name: '说唱' }, { id: '轻音乐', name: '轻音乐' }, { id: '爵士', name: '爵士' }, { id: '乡村', name: '乡村' }, { id: 'R%26B%2FSoul', name: 'R&B/Soul' }, { id: '古典', name: '古典' }, { id: '民族', name: '民族' }, { id: '英伦', name: '英伦' }, { id: '金属', name: '金属' }, { id: '朋克', name: '朋克' }, { id: '蓝调', name: '蓝调' }, { id: '雷鬼', name: '雷鬼' }, { id: '世界音乐', name: '世界音乐' }, { id: '拉丁', name: '拉丁' }, { id: 'New Age', name: 'New Age' }, { id: '古风', name: '古风' }, { id: '后摇', name: '后摇' }, { id: 'Bossa Nova', name: 'Bossa Nova' }, ], }, { category: '场景', filters: [ { id: '清晨', name: '清晨' }, { id: '夜晚', name: '夜晚' }, { id: '学习', name: '学习' }, { id: '工作', name: '工作' }, { id: '午休', name: '午休' }, { id: '下午茶', name: '下午茶' }, { id: '地铁', name: '地铁' }, { id: '驾车', name: '驾车' }, { id: '运动', name: '运动' }, { id: '旅行', name: '旅行' }, { id: '散步', name: '散步' }, { id: '酒吧', name: '酒吧' }, ], }, { category: '情感', filters: [ { id: '怀旧', name: '怀旧' }, { id: '清新', name: '清新' }, { id: '浪漫', name: '浪漫' }, { id: '伤感', name: '伤感' }, { id: '治愈', name: '治愈' }, { id: '放松', name: '放松' }, { id: '孤独', name: '孤独' }, { id: '感动', name: '感动' }, { id: '兴奋', name: '兴奋' }, { id: '快乐', name: '快乐' }, { id: '安静', name: '安静' }, { id: '思念', name: '思念' }, ], }, { category: '主题', filters: [ { id: '综艺', name: '综艺' }, { id: '影视原声', name: '影视原声' }, { id: 'ACG', name: 'ACG' }, { id: '儿童', name: '儿童' }, { id: '校园', name: '校园' }, { id: '游戏', name: '游戏' }, { id: '70后', name: '70后' }, { id: '80后', name: '80后' }, { id: '90后', name: '90后' }, { id: '网络歌曲', name: '网络歌曲' }, { id: 'KTV', name: 'KTV' }, { id: '经典', name: '经典' }, { id: '翻唱', name: '翻唱' }, { id: '吉他', name: '吉他' }, { id: '钢琴', name: '钢琴' }, { id: '器乐', name: '器乐' }, { id: '榜单', name: '榜单' }, { id: '00后', name: '00后' }, ], }, ]; return { success: (fn) => fn({ recommend, all }), }; } static login(url) { // use chrome extension to modify referer. let target_url = 'https://music.163.com/weapi/login'; const loginType = getParameterByName('type', url); const password = getParameterByName('password', url); let req_data = {}; if (loginType === 'email') { const email = getParameterByName('email', url); req_data = { username: email, password: forge.md5 .create() .update(forge.util.encodeUtf8(password)) .digest() .toHex(), rememberLogin: 'true', }; } else if (loginType === 'phone') { target_url = `https://music.163.com/weapi/login/cellphone`; const countrycode = getParameterByName('countrycode', url); const phone = getParameterByName('phone', url); req_data = { phone, countrycode, password: forge.md5 .create() .update(forge.util.encodeUtf8(password)) .digest() .toHex(), rememberLogin: 'true', }; } const encrypt_req_data = this.weapi(req_data); const expire = (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; cookieSet( { url: 'https://music.163.com', name: 'os', value: 'pc', expirationDate: expire, }, (cookie) => {} ); return { success: (fn) => { axios .post(target_url, new URLSearchParams(encrypt_req_data)) .then((response) => { const { data } = response; const result = { is_login: true, user_id: data.account.id, user_name: data.account.userName, nickname: data.profile.nickname, avatar: data.profile.avatarUrl, platform: 'netease', data, }; return fn({ status: 'success', data: result, }); }) .catch(() => fn({ status: 'fail', data: {}, }) ); }, }; } static get_user_playlist(url, playlistType) { const user_id = getParameterByName('user_id', url); const target_url = 'https://music.163.com/api/user/playlist'; const req_data = { uid: user_id, limit: 1000, offset: 0, includeVideo: true, }; return { success: (fn) => { axios .post(target_url, new URLSearchParams(req_data)) .then((response) => { const playlists = []; response.data.playlist.forEach((item) => { if (playlistType === 'created' && item.subscribed !== false) { return; } if (playlistType === 'favorite' && item.subscribed !== true) { return; } const playlist = { cover_img_url: item.coverImgUrl, id: `neplaylist_${item.id}`, source_url: `https://music.163.com/#/playlist?id=${item.id}`, title: item.name, }; playlists.push(playlist); }); return fn({ status: 'success', data: { playlists, }, }); }); }, }; } static get_user_created_playlist(url) { return this.get_user_playlist(url, 'created'); } static get_user_favorite_playlist(url) { return this.get_user_playlist(url, 'favorite'); } static get_recommend_playlist() { const target_url = 'https://music.163.com/weapi/personalized/playlist'; const req_data = { limit: 30, total: true, n: 1000, }; const encrypt_req_data = this.weapi(req_data); return { success: (fn) => { axios .post(target_url, new URLSearchParams(encrypt_req_data)) .then((response) => { const playlists = []; response.data.result.forEach((item) => { const playlist = { cover_img_url: item.picUrl, id: `neplaylist_${item.id}`, source_url: `https://music.163.com/#/playlist?id=${item.id}`, title: item.name, }; playlists.push(playlist); }); return fn({ status: 'success', data: { playlists, }, }); }); }, }; } static get_user() { const url = `https://music.163.com/api/nuser/account/get`; const encrypt_req_data = this.weapi({}); return { success: (fn) => { axios.post(url, new URLSearchParams(encrypt_req_data)).then((res) => { let result = { is_login: false }; let status = 'fail'; if (res.data.account !== null) { status = 'success'; const { data } = res; result = { is_login: true, user_id: data.account.id, user_name: data.account.userName, nickname: data.profile.nickname, avatar: data.profile.avatarUrl, platform: 'netease', data, }; } return fn({ status, data: result, }); }); }, }; } static get_login_url() { return `https://music.163.com/#/login`; } static logout() { cookieRemove( { url: 'https://music.163.com', name: 'MUSIC_U', }, (cookie) => {} ); } } ================================================ FILE: js/provider/qq.js ================================================ /* eslint-disable no-use-before-define */ /* global getParameterByName cookieGet cookieRemove */ // eslint-disable-next-line no-unused-vars class qq { static htmlDecode(value) { const parser = new DOMParser(); return parser.parseFromString(value, 'text/html').body.textContent; } static qq_show_toplist(offset) { if (offset !== undefined && offset > 0) { return { success: (fn) => fn({ result: [] }), }; } const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg?g_tk=5381&inCharset=utf-8&outCharset=utf-8¬ice=0&format=json&uin=0&needNewCode=1&platform=h5'; return { success: (fn) => { axios.get(url).then((response) => { const result = []; response.data.data.topList.forEach((item) => { const playlist = { cover_img_url: item.picUrl, id: `qqtoplist_${item.id}`, source_url: `https://y.qq.com/n/yqq/toplist/${item.id}.html`, title: item.topTitle, }; result.push(playlist); }); return fn({ result }); }); }, }; } static show_playlist(url) { const offset = Number(getParameterByName('offset', url)) || 0; let filterId = getParameterByName('filter_id', url) || ''; if (filterId === 'toplist') { return this.qq_show_toplist(offset); } if (filterId === '') { filterId = '10000000'; } const target_url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg' + `?picmid=1&rnd=${Math.random()}&g_tk=732560869` + '&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8' + '¬ice=0&platform=yqq.json&needNewCode=0' + `&categoryId=${filterId}&sortId=5&sin=${offset}&ein=${29 + offset}`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const playlists = data.data.list.map((item) => ({ cover_img_url: item.imgurl, title: this.htmlDecode(item.dissname), id: `qqplaylist_${item.dissid}`, source_url: `https://y.qq.com/n/ryqq/playlist/${item.dissid}`, })); return fn({ result: playlists, }); }); }, }; } static qq_get_image_url(qqimgid, img_type) { if (qqimgid == null) { return ''; } let category = ''; if (img_type === 'artist') { category = 'T001R300x300M000'; } if (img_type === 'album') { category = 'T002R300x300M000'; } const s = category + qqimgid; const url = `https://y.gtimg.cn/music/photo_new/${s}.jpg`; return url; } static qq_is_playable(song) { const switch_flag = song.switch.toString(2).split(''); switch_flag.pop(); switch_flag.reverse(); // flag switch table meaning: // ["play_lq", "play_hq", "play_sq", "down_lq", "down_hq", "down_sq", "soso", // "fav", "share", "bgm", "ring", "sing", "radio", "try", "give"] const play_flag = switch_flag[0]; const try_flag = switch_flag[13]; return play_flag === '1' || (play_flag === '1' && try_flag === '1'); } static qq_convert_song(song) { const d = { id: `qqtrack_${song.songmid}`, title: this.htmlDecode(song.songname), artist: this.htmlDecode(song.singer[0].name), artist_id: `qqartist_${song.singer[0].mid}`, album: this.htmlDecode(song.albumname), album_id: `qqalbum_${song.albummid}`, img_url: this.qq_get_image_url(song.albummid, 'album'), source: 'qq', source_url: `https://y.qq.com/#type=song&mid=${song.songmid}&tpl=yqq_song_detail`, // url: `qqtrack_${song.songmid}`, url: !qq.qq_is_playable(song) ? '' : undefined, }; return d; } static qq_convert_song2(song) { const d = { id: `qqtrack_${song.mid}`, title: this.htmlDecode(song.name), artist: this.htmlDecode(song.singer[0].name), artist_id: `qqartist_${song.singer[0].mid}`, album: this.htmlDecode(song.album.name), album_id: `qqalbum_${song.album.mid}`, img_url: this.qq_get_image_url(song.album.mid, 'album'), source: 'qq', source_url: `https://y.qq.com/#type=song&mid=${song.mid}&tpl=yqq_song_detail`, url: '', }; return d; } static get_toplist_url(id, period, limit) { return `https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&platform=yqq.json&needNewCode=0&data=${encodeURIComponent( JSON.stringify({ comm: { cv: 1602, ct: 20, }, toplist: { module: 'musicToplist.ToplistInfoServer', method: 'GetDetail', param: { topid: id, num: limit, period, }, }, }) )}`; } static get_periods(topid) { const periodUrl = 'https://c.y.qq.com/node/pc/wk_v15/top.html'; const regExps = { periodList: //g, period: /data-listname="(.+?)" data-tid=".*?\/(.+?)" data-date="(.+?)" .+?<\/i>/, }; const periods = {}; return axios.get(periodUrl).then((response) => { const html = response.data; const pl = html.match(regExps.periodList); if (!pl) return Promise.reject(); pl.forEach((p) => { const pr = p.match(regExps.period); if (!pr) return; periods[pr[2]] = { name: pr[1], id: pr[2], period: pr[3], }; }); const info = periods[topid]; return info && info.period; }); } static qq_toplist(url) { // special thanks to lx-music-desktop solution // https://github.com/lyswhut/lx-music-desktop/blob/24521bf50d80512a44048596639052e3194b2bf1/src/renderer/utils/music/tx/leaderboard.js const list_id = Number(getParameterByName('list_id', url).split('_').pop()); return { success: (fn) => { this.get_periods(list_id).then((listPeriod) => { const limit = 100; // TODO: visit all pages of toplist const target_url = this.get_toplist_url(list_id, listPeriod, limit); axios.get(target_url).then((response) => { const { data } = response; const tracks = data.toplist.data.songInfoList.map((song) => { const d = { id: `qqtrack_${song.mid}`, title: this.htmlDecode(song.name), artist: this.htmlDecode(song.singer[0].name), artist_id: `qqartist_${song.singer[0].mid}`, album: this.htmlDecode(song.album.name), album_id: `qqalbum_${song.album.mid}`, img_url: this.qq_get_image_url(song.album.mid, 'album'), source: 'qq', source_url: `https://y.qq.com/#type=song&mid=${song.mid}&tpl=yqq_song_detail`, }; return d; }); const info = { cover_img_url: data.toplist.data.data.frontPicUrl, title: data.toplist.data.data.title, id: `qqtoplist_${list_id}`, source_url: `https://y.qq.com/n/yqq/toplist/${list_id}.html`, }; return fn({ tracks, info, }); }); }); }, }; } static qq_get_playlist(url) { // eslint-disable-line no-unused-vars const list_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const target_url = 'https://i.y.qq.com/qzone-music/fcg-bin/fcg_ucc_getcdinfo_' + 'byids_cp.fcg?type=1&json=1&utf8=1&onlysong=0' + `&nosign=1&disstid=${list_id}&g_tk=5381&loginUin=0&hostUin=0` + '&format=json&inCharset=GB2312&outCharset=utf-8¬ice=0' + '&platform=yqq&needNewCode=0'; axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: data.cdlist[0].logo, title: data.cdlist[0].dissname, id: `qqplaylist_${list_id}`, source_url: `https://y.qq.com/n/ryqq/playlist/${list_id}`, }; const tracks = data.cdlist[0].songlist.map((item) => this.qq_convert_song(item) ); return fn({ tracks, info, }); }); }, }; } static qq_album(url) { const album_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const target_url = 'https://i.y.qq.com/v8/fcg-bin/fcg_v8_album_info_cp.fcg' + `?platform=h5page&albummid=${album_id}&g_tk=938407465` + '&uin=0&format=json&inCharset=utf-8&outCharset=utf-8' + '¬ice=0&platform=h5&needNewCode=1&_=1459961045571'; axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: this.qq_get_image_url(album_id, 'album'), title: data.data.name, id: `qqalbum_${album_id}`, source_url: `https://y.qq.com/#type=album&mid=${album_id}`, }; const tracks = data.data.list.map((item) => this.qq_convert_song(item) ); return fn({ tracks, info, }); }); }, }; } static qq_artist(url) { const artist_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { const target_url = `https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&loginUin=0&hostUin=0inCharset=utf8&outCharset=utf-8&platform=yqq.json&needNewCode=0&data=${encodeURIComponent( JSON.stringify({ comm: { ct: 24, cv: 0, }, singer: { method: 'get_singer_detail_info', param: { sort: 5, singermid: artist_id, sin: 0, num: 50, }, module: 'music.web_singer_info_svr', }, }) )}`; axios.get(target_url).then((response) => { const { data } = response; const info = { cover_img_url: this.qq_get_image_url(artist_id, 'artist'), title: data.singer.data.singer_info.name, id: `qqartist_${artist_id}`, source_url: `https://y.qq.com/#type=singer&mid=${artist_id}`, }; const tracks = data.singer.data.songlist.map((item) => this.qq_convert_song2(item) ); return fn({ tracks, info, }); }); }, }; } static search(url) { // eslint-disable-line no-unused-vars const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const searchType = getParameterByName('type', url); // API solution from lx-music-desktop // https://github.com/lyswhut/lx-music-desktop/blob/master/src/renderer/utils/music/tx/musicSearch.js const target_url = 'https://u.y.qq.com/cgi-bin/musicu.fcg'; const searchTypeMapping = { 0: 0, 1: 3, }; return { success: (fn) => { const limit = 50; const page = curpage; const query = { comm: { ct: '19', cv: '1859', uin: '0', }, req: { method: 'DoSearchForQQMusicDesktop', module: 'music.search.SearchCgiService', param: { grp: 1, num_per_page: limit, page_num: parseInt(page, 10), query: keyword, search_type: searchTypeMapping[searchType], }, }, }; axios.post(target_url, query).then((response) => { const { data } = response; let result = []; let total = 0; if (searchType === '0') { result = data.req.data.body.song.list.map((item) => this.qq_convert_song2(item) ); total = data.req.data.meta.sum; } else if (searchType === '1') { result = data.req.data.body.songlist.list.map((info) => ({ id: `qqplaylist_${info.dissid}`, title: this.htmlDecode(info.dissname), source: 'qq', source_url: `https://y.qq.com/n/ryqq/playlist/${info.dissid}`, img_url: info.imgurl, url: `qqplaylist_${info.dissid}`, author: this.UnicodeToAscii(info.creator.name), count: info.song_count, })); total = data.req.data.meta.sum; } return fn({ result, total, type: searchType, }); }); }, }; } static UnicodeToAscii(str) { const result = str.replace(/&#(\d+);/g, () => // eslint-disable-next-line prefer-rest-params String.fromCharCode(arguments[1]) ); return result; } // eslint-disable-next-line no-unused-vars static bootstrap_track(track, success, failure) { const sound = {}; const songId = track.id.slice('qqtrack_'.length); const target_url = 'https://u.y.qq.com/cgi-bin/musicu.fcg'; // thanks to https://github.com/Rain120/qq-music-api/blob/2b9cb811934888a532545fbd0bf4e4ab2aea5dbe/routers/context/getMusicPlay.js const guid = '10000'; const songmidList = [songId]; const uin = '0'; // server won't response with 320kbps request, downgrade to 128kbps const fileType = '128'; const fileConfig = { m4a: { s: 'C400', e: '.m4a', bitrate: 'M4A', }, 128: { s: 'M500', e: '.mp3', bitrate: '128kbps', }, 320: { s: 'M800', e: '.mp3', bitrate: '320kbps', }, ape: { s: 'A000', e: '.ape', bitrate: 'APE', }, flac: { s: 'F000', e: '.flac', bitrate: 'FLAC', }, }; const fileInfo = fileConfig[fileType]; const file = songmidList.length === 1 && `${fileInfo.s}${songId}${songId}${fileInfo.e}`; const reqData = { req_1: { module: 'vkey.GetVkeyServer', method: 'CgiGetVkey', param: { filename: file ? [file] : [], guid, songmid: songmidList, songtype: [0], uin, loginflag: 1, platform: '20', }, }, loginUin: uin, comm: { uin, format: 'json', ct: 24, cv: 0, }, }; axios.post(target_url, reqData).then((response) => { const { data } = response; const { purl } = data.req_1.data.midurlinfo[0]; if (purl === '') { // vip failure(sound); return; } const url = data.req_1.data.sip[0] + purl; sound.url = url; const prefix = purl.slice(0, 4); const found = Object.values(fileConfig).filter((i) => i.s === prefix); sound.bitrate = found.length > 0 ? found[0].bitrate : ''; sound.platform = 'qq'; success(sound); }); } // eslint-disable-next-line no-unused-vars static str2ab(str) { // string to array buffer. const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i += 1) { bufView[i] = str.charCodeAt(i); } return buf; } static lyric(url) { // eslint-disable-line no-unused-vars const track_id = getParameterByName('track_id', url).split('_').pop(); // use chrome extension to modify referer. const target_url = 'https://i.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?' + `songmid=${track_id}&g_tk=5381&format=json&inCharset=utf8&outCharset=utf-8&nobase64=1`; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const lrc = data.lyric || ''; const tlrc = data.trans.replace(/\/\//g, '') || ''; return fn({ lyric: lrc, tlyric: tlrc, }); }); }, }; } static parse_url(url) { return { success: (fn) => { let result; let match = /\/\/y.qq.com\/n\/yqq\/playlist\/([0-9]+)/.exec(url); if (match != null) { const playlist_id = match[1]; result = { type: 'playlist', id: `qqplaylist_${playlist_id}`, }; } match = /\/\/y.qq.com\/n\/yqq\/playsquare\/([0-9]+)/.exec(url); if (match != null) { const playlist_id = match[1]; result = { type: 'playlist', id: `qqplaylist_${playlist_id}`, }; } match = /\/\/y.qq.com\/n\/m\/detail\/taoge\/index.html\?id=([0-9]+)/.exec( url ); if (match != null) { const playlist_id = match[1]; result = { type: 'playlist', id: `qqplaylist_${playlist_id}`, }; } // c.y.qq.com/base/fcgi-bin/u?__=1MsbSLu match = /\/\/c.y.qq.com\/base\/fcgi-bin\/u\?__=([0-9a-zA-Z]+)/.exec( url ); if (match != null) { return axios .get(url) .then((response) => { const { responseURL } = response.request; const playlist_id = getParameterByName('id', responseURL); result = { type: 'playlist', id: `qqplaylist_${playlist_id}`, }; return fn(result); }) .catch(() => fn(undefined)); } return fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'qqplaylist': return this.qq_get_playlist(url); case 'qqalbum': return this.qq_album(url); case 'qqartist': return this.qq_artist(url); case 'qqtoplist': return this.qq_toplist(url); default: return null; } } static get_playlist_filters() { const target_url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_tag_conf.fcg' + `?picmid=1&rnd=${Math.random()}&g_tk=732560869` + '&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8' + '¬ice=0&platform=yqq.json&needNewCode=0'; return { success: (fn) => { axios.get(target_url).then((response) => { const { data } = response; const all = []; data.data.categories.forEach((cate) => { const result = { category: cate.categoryGroupName, filters: [] }; if (cate.usable === 1) { cate.items.forEach((item) => { result.filters.push({ id: item.categoryId, name: this.htmlDecode(item.categoryName), }); }); all.push(result); } }); const recommendLimit = 8; const recommend = [ { id: '', name: '全部' }, { id: 'toplist', name: '排行榜' }, ...all[1].filters.slice(0, recommendLimit), ]; return fn({ recommend, all, }); }); }, }; } static get_user_by_uin(uin, callback) { const infoUrl = `https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&&loginUin=${uin}&hostUin=0inCharset=utf8&outCharset=utf-8&platform=yqq.json&needNewCode=0&data=${encodeURIComponent( JSON.stringify({ comm: { ct: 24, cv: 0 }, vip: { module: 'userInfo.VipQueryServer', method: 'SRFVipQuery_V2', param: { uin_list: [uin] }, }, base: { module: 'userInfo.BaseUserInfoServer', method: 'get_user_baseinfo_v2', param: { vec_uin: [uin] }, }, }) )}`; return axios.get(infoUrl).then((response) => { const { data } = response; const info = data.base.data.map_userinfo[uin]; const result = { is_login: true, user_id: uin, user_name: uin, nickname: info.nick, avatar: info.headurl, platform: 'qq', data, }; return callback({ status: 'success', data: result }); }); } static get_user_created_playlist(url) { const user_id = getParameterByName('user_id', url); // TODO: load more than size const size = 100; const target_url = `https://c.y.qq.com/rsc/fcgi-bin/fcg_user_created_diss?cv=4747474&ct=24&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=1&uin=${user_id}&hostuin=${user_id}&sin=0&size=${size}`; return { success: (fn) => { axios.get(target_url).then((response) => { const playlists = []; response.data.data.disslist.forEach((item) => { let playlist = {}; if (item.dir_show === 0) { if (item.tid === 0) { return; } if (item.diss_name === '我喜欢') { playlist = { cover_img_url: 'https://y.gtimg.cn/mediastyle/y/img/cover_love_300.jpg', id: `qqplaylist_${item.tid}`, source_url: `https://y.qq.com/n/ryqq/playlist/${item.tid}`, title: item.diss_name, }; playlists.push(playlist); } } else { playlist = { cover_img_url: item.diss_cover, id: `qqplaylist_${item.tid}`, source_url: `https://y.qq.com/n/ryqq/playlist/${item.tid}`, title: item.diss_name, }; playlists.push(playlist); } }); return fn({ status: 'success', data: { playlists, }, }); }); }, }; } static get_user_favorite_playlist(url) { const user_id = getParameterByName('user_id', url); // TODO: load more than size const size = 100; // https://github.com/jsososo/QQMusicApi/blob/master/routes/user.js const target_url = `https://c.y.qq.com/fav/fcgi-bin/fcg_get_profile_order_asset.fcg`; const data = { ct: 20, cid: 205360956, userid: user_id, reqtype: 3, sin: 0, ein: size, }; return { success: (fn) => { axios.get(target_url, { params: data }).then((response) => { const playlists = []; response.data.data.cdlist.forEach((item) => { let playlist = {}; if (item.dir_show === 0) { return; } playlist = { cover_img_url: item.logo, id: `qqplaylist_${item.dissid}`, source_url: `https://y.qq.com/n/ryqq/playlist/${item.dissid}`, title: item.dissname, }; playlists.push(playlist); }); return fn({ status: 'success', data: { playlists, }, }); }); }, }; } static get_recommend_playlist() { const target_url = `https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&&loginUin=0&hostUin=0inCharset=utf8&outCharset=utf-8&platform=yqq.json&needNewCode=0&data=${encodeURIComponent( JSON.stringify({ comm: { ct: 24, }, recomPlaylist: { method: 'get_hot_recommend', param: { async: 1, cmd: 2, }, module: 'playlist.HotRecommendServer', }, }) )}`; return { success: (fn) => { axios.get(target_url).then((response) => { const playlists = []; response.data.recomPlaylist.data.v_hot.forEach((item) => { const playlist = { cover_img_url: item.cover, id: `qqplaylist_${item.content_id}`, source_url: `https://y.qq.com/n/ryqq/playlist/${item.content_id}`, title: item.title, }; playlists.push(playlist); }); return fn({ status: 'success', data: { playlists, }, }); }); }, }; } static get_user() { return { success: (fn) => { const domain = 'https://y.qq.com'; cookieGet( { url: domain, name: 'uin', }, (qqCookie) => { if (qqCookie === null) { return cookieGet( { url: domain, name: 'wxuin', }, (wxCookie) => { if (wxCookie == null) { return fn({ status: 'fail', data: {} }); } let { value: uin } = wxCookie; uin = `1${uin.slice('o'.length)}`; // replace prefix o with 1 return this.get_user_by_uin(uin, fn); } ); } const { value: uin } = qqCookie; return this.get_user_by_uin(uin, fn); } ); }, }; } static get_login_url() { return `https://y.qq.com/portal/profile.html`; } static logout() { cookieRemove( { url: 'https://y.qq.com', name: 'uin', }, () => { cookieRemove( { url: 'https://y.qq.com', name: 'wxuin', }, () => {} ); } ); } } ================================================ FILE: js/provider/taihe.js ================================================ /* eslint-disable no-unused-vars */ /* global async getParameterByName forge */ const axiosTH = axios.create({ baseURL: 'https://music.taihe.com/v1', }); axiosTH.interceptors.request.use( (config) => { const params = { ...config.params }; params.timestamp = Math.round(Date.now() / 1000); params.appid = '16073360'; const q = new URLSearchParams(params); q.sort(); const signStr = decodeURIComponent( `${q.toString()}0b50b02fd0d73a9c4c8c3a781c30845f` ); params.sign = forge.md5 .create() .update(forge.util.encodeUtf8(signStr)) .digest() .toHex(); return { ...config, params }; }, null, { synchronous: true } ); class taihe { static th_convert_song(song) { const track = { id: `thtrack_${song.id}`, title: song.title, album: song.albumTitle, album_id: `thalbum_${song.albumAssetCode}`, source: 'taihe', source_url: `https://music.taihe.com/song/${song.id}`, img_url: song.pic, lyric_url: song.lyric || '', }; if (song.artist && song.artist.length) { track.artist = song.artist[0].name; track.artist_id = `thartist_${song.artist[0].artistCode}`; } return track; } static th_render_tracks(url, page, callback) { const list_id = getParameterByName('list_id', url).split('_').pop(); axiosTH .get('/tracklist/info', { params: { id: list_id, pageNo: page, pageSize: 100, }, }) .then((response) => { const data = response.data.data.trackList; const tracks = data.map(this.th_convert_song); return callback(null, tracks); }); } static search(url) { const keyword = getParameterByName('keywords', url); const curpage = getParameterByName('curpage', url); const searchType = getParameterByName('type', url); if (searchType === '1') { return { success: (fn) => fn({ result: [], total: 0, type: searchType, }), }; } return { success: (fn) => { axiosTH .get('/search', { params: { word: keyword, pageNo: curpage || 1, type: 1, }, }) .then((res) => { const { data } = res; const tracks = data.data.typeTrack.map(this.th_convert_song); return fn({ result: tracks, total: data.data.total, type: searchType, }); }) .catch(() => fn({ result: [], total: 0, type: searchType, }) ); }, }; } static th_get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => { axiosTH .get('/tracklist/info', { params: { id: list_id, }, }) .then((response) => { const { data } = response.data; const info = { cover_img_url: data.pic, title: data.title, id: `thplaylist_${list_id}`, source_url: `https://music.taihe.com/songlist/${list_id}`, }; const total = data.trackCount; const page = Math.ceil(total / 100); const page_array = Array.from({ length: page }, (v, k) => k + 1); async.concat( page_array, (item, callback) => this.th_render_tracks(url, item, callback), (err, tracks) => { fn({ tracks, info, }); } ); }); }, }; } static th_artist(url) { return { success: (fn) => { const artist_id = getParameterByName('list_id', url).split('_').pop(); axiosTH .get('/artist/info', { params: { artistCode: artist_id, }, }) .then((response) => { const info = { cover_img_url: response.data.data.pic, title: response.data.data.name, id: `thartist_${artist_id}`, source_url: `https://music.taihe.com/artist/${artist_id}`, }; axiosTH .get('/artist/song', { params: { artistCode: artist_id, pageNo: 1, pageSize: 50, }, }) .then((res) => { const tracks = res.data.data.result.map(this.th_convert_song); return fn({ tracks, info, }); }); }); }, }; } static bootstrap_track(track, success, failure) { const sound = {}; const song_id = track.id.slice('thtrack_'.length); axiosTH .get('/song/tracklink', { params: { TSID: song_id, }, }) .then((response) => { const { data } = response; if (data.data && data.data.path) { sound.url = data.data.path; sound.platform = 'taihe'; sound.bitrate = `${data.data.rate}kbps`; success(sound); } else { failure(sound); } }); } static lyric(url) { // eslint-disable-line no-unused-vars const lyric_url = getParameterByName('lyric_url', url); return { success: (fn) => { if (lyric_url) { axios.get(lyric_url).then((response) => fn({ lyric: response.data, }) ); } else { const track_id = getParameterByName('track_id', url).split('_').pop(); axiosTH .get('/song/tracklink', { params: { TSID: track_id, }, }) .then((response) => { axios.get(response.data.data.lyric).then((res) => fn({ lyric: res.data, }) ); }); } }, }; } static th_album(url) { return { success: (fn) => { const album_id = getParameterByName('list_id', url).split('_').pop(); axiosTH .get('/album/info', { params: { albumAssetCode: album_id, }, }) .then((response) => { const { data } = response.data; const info = { cover_img_url: data.pic, title: data.title, id: `thalbum_${album_id}`, source_url: `https://music.taihe.com/album/${album_id}`, }; const tracks = data.trackList.map((song) => ({ id: `thtrack_${song.assetId}`, title: song.title, artist: song.artist ? song.artist[0].name : '', artist_id: song.artist ? `thartist_${song.artist[0].artistCode}` : 'thartist_', album: info.title, album_id: `thalbum_${album_id}`, source: 'taihe', source_url: `https://music.taihe.com/song/${song.assetId}`, img_url: info.cover_img_url, lyric_url: '', })); return fn({ tracks, info, }); }); }, }; } static show_playlist(url) { const offset = Number(getParameterByName('offset', url)); const subCate = getParameterByName('filter_id', url); return { success: (fn) => { axiosTH .get('/tracklist/list', { params: { pageNo: offset / 25 + 1, pageSize: 25, subCateId: subCate, }, }) .then((response) => { const { data } = response.data; const result = data.result.map((item) => ({ cover_img_url: item.pic, title: item.title, id: `thplaylist_${item.id}`, source_url: `https://music.taihe.com/songlist/${item.id}`, })); return fn({ result, }); }); }, }; } static parse_url(url) { let result; let id = ''; let match = /\/\/music.taihe.com\/([a-z]+)\//.exec(url); if (match) { switch (match[1]) { case 'songlist': match = /\/\/music.taihe.com\/songlist\/([0-9]+)/.exec(url); id = match ? `thplaylist_${match[1]}` : ''; break; case 'artist': match = /\/\/music.taihe.com\/artist\/(A[0-9]+)/.exec(url); id = match ? `thartist_${match[1]}` : ''; break; case 'album': match = /\/\/music.taihe.com\/album\/(P[0-9]+)/.exec(url); id = match ? `thalbum_${match[1]}` : ''; break; default: break; } result = { type: 'playlist', id, }; } return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'thplaylist': return this.th_get_playlist(url); case 'thalbum': return this.th_album(url); case 'thartist': return this.th_artist(url); default: return null; } } static get_playlist_filters() { return { success: (fn) => { axiosTH.get('/tracklist/category').then((res) => fn({ recommend: [{ id: '', name: '推荐歌单' }], all: res.data.data.map((sub) => ({ category: sub.categoryName, filters: sub.subCate.map((i) => ({ id: i.id, name: i.categoryName, })), })), }) ); }, }; } static get_user() { return { success: (fn) => { fn({ status: 'fail', data: {} }); }, }; } static get_login_url() { return `https://music.taihe.com`; } static logout() {} } ================================================ FILE: js/provider/xiami.js ================================================ /* eslint-disable radix */ /* eslint-disable no-use-before-define */ /* global getParameterByName */ /* eslint-disable no-param-reassign */ // eslint-disable-next-line no-unused-vars class xiami { static show_playlist() { return { success: (fn) => fn({ result: [], }), }; } // eslint-disable-next-line no-unused-vars static bootstrap_track(track, success, failure) { const sound = {}; failure(sound); } static xm_get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_').pop(); return { success: (fn) => fn({ tracks: [], info: { cover_img_url: '', title: '', id: `xmplaylist_${list_id}`, source_url: `https://www.xiami.com/collect/${list_id}`, }, }), }; } static xm_search(url) { const searchType = getParameterByName('type', url); return { success: (fn) => fn({ result: [], total: 0, type: searchType, }), }; } static xm_album(url) { return { success: (fn) => { const album_id = getParameterByName('list_id', url).split('_').pop(); return fn({ tracks: [], info: { cover_img_url: '', title: album_id, id: `xmalbum_${album_id}`, source_url: `https://www.xiami.com/album/${album_id}`, }, }); }, }; } static xm_artist(url) { return { success: (fn) => { const artist_id = getParameterByName('list_id', url).split('_').pop(); return fn({ tracks: [], info: { cover_img_url: '', title: artist_id, id: `xmartist_${artist_id}`, source_url: `https://www.xiami.com/artist/${artist_id}`, }, }); }, }; } static lyric() { return { success: (fn) => fn({ lyric: '', tlyric: '', }), }; } static parse_url() { let result; return { success: (fn) => { fn(result); }, }; } static get_playlist(url) { const list_id = getParameterByName('list_id', url).split('_')[0]; switch (list_id) { case 'xmplaylist': return this.xm_get_playlist(url); case 'xmalbum': return this.xm_album(url); case 'xmartist': return this.xm_artist(url); default: return null; } } static get_playlist_filters() { return { success: (fn) => fn({ recommend: [], all: [] }), }; } static get_user() { return { success: (fn) => { fn({ status: 'fail', data: {} }); }, }; } static get_login_url() { return `https://www.xiami.com`; } static logout() {} // return { // show_playlist: xm_show_playlist, // get_playlist_filters, // get_playlist, // parse_url: xm_parse_url, // bootstrap_track: xm_bootstrap_track, // search: xm_search, // lyric: xm_lyric, // get_user: xm_get_user, // get_login_url: xm_get_login_url, // logout: xm_logout, // }; } ================================================ FILE: listen1.html ================================================ Listen 1
{{ dialog_title }}×
  • {{_CREATE_PLAYLIST}}

  • {{ playlist.info.title }}

{{_OPENING_LASTFM_PAGE}}

{{_CONFIRM_NOTICE_LASTFM}}

  • {{ playlist.info.title }}

{{_OPENING_GITHUB_PAGE}}

{{_CONFIRM_NOTICE_GITHUB}}

  • {{_CREATE_PLAYLIST_BACKUP}}

  • {{ backup.id }}
    {{backup.description}}

  • {{ backup.id }} {{backup.description}}

{{_PROTOCOL}}
{{_HOST}}
{{_PORT}}
{{source.displayText}}
{{filter.name}}
更多...
{{category.category}}
{{filter.name}}
  • {{i.title}}
{{source.displayText}}
  • {{i.title}}
{{_LANGUAGE}}
{{_THEME}}
{{_AUTO_CHOOSE_SOURCE}}
{{_AUTO_CHOOSE_SOURCE_NOTICE}}
{{_AUTO_CHOOSE_SOURCE_LIST}}
{{item.displayText}}
{{_CLOSE_TAB_ACTION}}({{_VALID_AFTER_RESTART}})
{{_QUIT_APPLICATION}} {{_MINIMIZE_TO_BACKGROUND}}
{{_NOWPLAYING_DISPLAY}}
{{_NOWPLAYING_COVER_BACKGROUND_NOTICE}}
{{_NOWPLAYING_BITRATE_NOTICE}}
{{_NOWPLAYING_PLATFORM_NOTICE}}
{{_LYRIC_DISPLAY}}
{{_SHOW_DESKTOP_LYRIC}}
{{_SHOW_LYRIC_TRANSLATION}}
{{_SHOW_DESKTOP_LYRIC_TRANSLATION}}
{{_BACKUP_PLAYLIST}}

{{_BACKUP_WARNING}}

{{_RECOVER_PLAYLIST}}

{{_RECOVER_WARNING}}

{{_CONNECT_TO_GITHUB}}

{{_STATUS}}:{{ githubStatusText }}

{{_CONNECT_TO_LASTFM}}

{{_STATUS}}:{{ lastfm.getStatusText() }}

{{_SHORTCUTS}}
{{_SHORTCUTS_FUNCTION}}
{{_SHORTCUTS}}
{{_GLOBAL_SHORTCUTS}}
{{_PLAY_OR_PAUSE}}
p
Ctrl(Cmd) + Alt + {{_KEYBOARD_SPACE}}
{{_PREVIOUS_TRACK}}
[
Ctrl(Cmd) + Alt + ←
{{_NEXT_TRACK}}
]
Ctrl(Cmd) + Alt + →
{{_VOLUME_UP}}
u
{{_SHORTCUTS_NOT_SET}}
{{_VOLUME_DOWN}}
d
{{_SHORTCUTS_NOT_SET}}
{{_QUICK_SEARCH}}
f
{{_SHORTCUTS_NOT_SET}}
{{ZOOM_IN_OUT}}
Ctrl(Cmd) + +/-
{{_SHORTCUTS_NOT_SET}}
{{_GLOBAL_SHORTCUTS_NOTICE}}
{{_PROXY_CONFIG}}
{{_PROXY_CONFIG}}: {{proxyMode.displayText}} {{proxyRules}}
{{_ABOUT}}

Listen 1 {{_HOMEPAGE}}: https://listen1.github.io/listen1/

Listen 1 {{_EMAIL}}: githublisten1@gmail.com

{{_FEEDBACK}}: https://github.com/listen1/listen1_chrome_extension/issues https://github.com/listen1/listen1_desktop/issues

{{_DESIGNER}} ({{_THEME_WHITE}}): iparanoid

{{_DESIGNER}} ({{_THEME_MODERN_BLACK}}, {{_THEME_MODERN_WHITE}}): 814959822, Antion

{{_VERSION}}: v2.33.0 {{_LICENSE_NOTICE}}

{{_LATEST_VERSION}}: {{lastestVersion}}

{{ playlist_title }}

{{_PLAY_ALL}}
{{_ADD_LOCAL_SONGS}}
{{_ADD_TO_PLAYLIST}}
{{_EDIT}}
{{is_favorite?_FAVORITED:_FAVORITE}}
{{_REFRESH_PLAYLIST}}
{{_ORIGIN_LINK}}
{{_IMPORT}}

{{ currentPlaying.title }}

{{ currentPlaying.bitrate }} {{ currentPlaying.platformText }}

{{ line.content }}

{{ dialog_title }}×
  • {{_CREATE_PLAYLIST}}

  • {{ playlist.info.title }}

{{_OPENING_LASTFM_PAGE}}

{{_CONFIRM_NOTICE_LASTFM}}

  • {{ playlist.info.title }}

{{_OPENING_GITHUB_PAGE}}

{{_CONFIRM_NOTICE_GITHUB}}

  • {{_CREATE_PLAYLIST_BACKUP}}

  • {{ backup.id }}
    {{backup.description}}

  • {{ backup.id }} {{backup.description}}

{{_PROTOCOL}}
{{_HOST}}
{{_PORT}}
{{source.displayText}}
{{filter.name}}
{{category.category}}
{{filter.name}}
{{source.displayText}}
  • {{i.title}}
{{_LANGUAGE}}
{{_THEME}}
{{_AUTO_CHOOSE_SOURCE}}
{{_AUTO_CHOOSE_SOURCE_NOTICE}}
{{_AUTO_CHOOSE_SOURCE_LIST}}
{{item.displayText}}
{{_CLOSE_TAB_ACTION}}({{_VALID_AFTER_RESTART}})
{{_QUIT_APPLICATION}} {{_MINIMIZE_TO_BACKGROUND}}
{{_NOWPLAYING_DISPLAY}}
{{_NOWPLAYING_COVER_BACKGROUND_NOTICE}}
{{_NOWPLAYING_BITRATE_NOTICE}}
{{_NOWPLAYING_PLATFORM_NOTICE}}
{{_LYRIC_DISPLAY}}
{{_SHOW_DESKTOP_LYRIC}}
{{_SHOW_LYRIC_TRANSLATION}}
{{_SHOW_DESKTOP_LYRIC_TRANSLATION}}
{{_BACKUP_PLAYLIST}}

{{_BACKUP_WARNING}}

{{_RECOVER_PLAYLIST}}

{{_RECOVER_WARNING}}

{{_CONNECT_TO_GITHUB}}

{{_STATUS}}:{{ githubStatusText }}

{{_CONNECT_TO_LASTFM}}

{{_STATUS}}:{{ lastfm.getStatusText() }}

{{_SHORTCUTS}}
{{_SHORTCUTS_FUNCTION}}
{{_SHORTCUTS}}
{{_GLOBAL_SHORTCUTS}}
{{_PLAY_OR_PAUSE}}
p
Ctrl(Cmd) + Alt + {{_KEYBOARD_SPACE}}
{{_PREVIOUS_TRACK}}
[
Ctrl(Cmd) + Alt + ←
{{_NEXT_TRACK}}
]
Ctrl(Cmd) + Alt + →
{{_VOLUME_UP}}
u
{{_SHORTCUTS_NOT_SET}}
{{_VOLUME_DOWN}}
d
{{_SHORTCUTS_NOT_SET}}
{{_QUICK_SEARCH}}
f
{{_SHORTCUTS_NOT_SET}}
{{ZOOM_IN_OUT}}
Ctrl(Cmd) + +/-
{{_SHORTCUTS_NOT_SET}}
{{_GLOBAL_SHORTCUTS_NOTICE}}
{{_PROXY_CONFIG}}
{{_PROXY_CONFIG}}: {{proxyMode.displayText}} {{proxyRules}}
{{_ABOUT}}

Listen 1 {{_HOMEPAGE}}: https://listen1.github.io/listen1/

Listen 1 {{_EMAIL}}: githublisten1@gmail.com

{{_FEEDBACK}}: https://github.com/listen1/listen1_chrome_extension/issues https://github.com/listen1/listen1_desktop/issues

{{_DESIGNER}} ({{_THEME_WHITE}}): iparanoid

{{_DESIGNER}} ({{_THEME_MODERN_BLACK}}, {{_THEME_MODERN_WHITE}}): 814959822, Antion

{{_VERSION}}: v2.33.0 {{_LICENSE_NOTICE}}

{{_LATEST_VERSION}}: {{lastestVersion}}

{{ playlist_title }}

{{_PLAY_ALL}}
{{_ADD_LOCAL_SONGS}}
{{_ADD_TO_PLAYLIST}}
{{_EDIT}}
{{is_favorite?_FAVORITED:_FAVORITE}}
{{_REFRESH_PLAYLIST}}
{{_ORIGIN_LINK}}
{{_IMPORT}}
================================================ FILE: manifest.json ================================================ { "background": { "service_worker": "js/background.js" }, "action": { "default_icon": "images/logo.png", "default_title": "Listen 1" }, "description": "One for all free music in China", "icons": { "128": "images/logo.png", "16": "images/logo_16.png", "48": "images/logo_48.png" }, "manifest_version": 3, "name": "Listen 1", "permissions": [ "notifications", "unlimitedStorage", "cookies", "declarativeNetRequest" ], "declarative_net_request": { "rule_resources": [ { "id": "ruleset_1", "enabled": true, "path": "rules_1.json" } ] }, "host_permissions": [ "*://music.163.com/*", "*://*.music.163.com/*", "*://*.xiami.com/*", "*://*.qq.com/*", "*://*.kugou.com/", "*://*.kuwo.cn/", "*://*.bilibili.com/*", "*://*.bilivideo.com/*", "*://*.bilivideo.cn/*", "*://*.migu.cn/*", "*://*.taihe.com/*", "*://music.91q.com/*", "*://api.github.com/*", "*://github.com/*", "*://gist.githubusercontent.com/*" ], "version": "2.33.0", "web_accessible_resources": [ { "resources": ["images/*"], "matches": ["*://*/*"] } ], "content_scripts": [ { "matches": ["https://listen1.github.io/listen1/*"], "js": ["js/oauth_callback.js"] } ] } ================================================ FILE: manifest_firefox.json ================================================ { "applications": { "gecko": { "id": "githublisten1@gmail.com", "strict_min_version": "45.0" } }, "background": { "scripts": [ "js/vendor/axios.min.js", "js/github.js", "js/background.js", "js/vendor/howler.core.min.js", "js/bridge.js", "js/player_thread.js" ], "persistent": true }, "browser_action": { "default_icon": "images/logo.png", "default_title": "Listen 1" }, "description": "One for all free music in China", "icons": { "128": "images/logo.png", "16": "images/logo_16.png", "48": "images/logo_48.png" }, "manifest_version": 2, "name": "Listen 1", "permissions": [ "notifications", "unlimitedStorage", "downloads", "storage", "contextMenus", "tabs", "cookies", "*://music.163.com/*", "*://*.music.163.com/*", "*://*.xiami.com/*", "*://*.qq.com/*", "*://*.kugou.com/", "*://*.kuwo.cn/", "*://*.bilibili.com/*", "*://*.bilivideo.com/*", "*://*.migu.cn/*", "*://*.taihe.com/*", "*://music.91q.com/*", "*://api.github.com/*", "*://github.com/*", "*://gist.githubusercontent.com/*", "webRequest", "webRequestBlocking" ], "version": "2.33.0", "web_accessible_resources": ["images/*"], "content_scripts": [ { "matches": ["https://listen1.github.io/listen1/*"], "js": ["js/oauth_callback.js"] } ] } ================================================ FILE: package.json ================================================ { "name": "listen1_chrome_extension", "version": "2.33.0", "description": "one for all free music in china", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "https://github.com/listen1/listen1_chrome_extension.git" }, "author": "", "license": "MIT", "bugs": { "url": "https://github.com/listen1/listen1_chrome_extension/issues" }, "homepage": "https://github.com/listen1/listen1_chrome_extension#readme", "devDependencies": { "eslint": "^7.30.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.23.4", "husky": "^4.3.8", "lint-staged": "^10.5.4", "prettier": "^2.3.2" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "!(**/vendor/*.js)*.js": "eslint --cache --fix", "!(**/vendor/*.js)*.{js,css,md}": "prettier --write" } } ================================================ FILE: rules_1.json ================================================ [ { "id": 1, "priority": 1, "action": { "type": "modifyHeaders", "requestHeaders": [ { "header": "referer", "operation": "set", "value": "https://y.qq.com/" }, { "header": "origin", "operation": "set", "value": "https://y.qq.com/" } ] }, "condition": { "urlFilter": "|*.y.qq.com", "resourceTypes": ["main_frame", "xmlhttprequest"] } }, { "id": 2, "priority": 1, "action": { "type": "modifyHeaders", "requestHeaders": [ { "header": "user-agent", "operation": "set", "value": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30" } ] }, "condition": { "urlFilter": "|*.kugou.com", "resourceTypes": ["main_frame", "xmlhttprequest"] } }, { "id": 3, "priority": 1, "action": { "type": "modifyHeaders", "requestHeaders": [ { "header": "referer", "operation": "set", "value": "https://www.bilibili.com/" } ] }, "condition": { "urlFilter": "|*.bilivideo.com/", "resourceTypes": ["media"] } } ]