diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..bcd4285cf08b0903077ba74f87bf84fd91179bf5 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "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" + ] +} \ No newline at end of file diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 0000000000000000000000000000000000000000..87178e782cbf4a0e6b145906a8586699ff839509 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,23 @@ +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 . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..18e7c004606b225d3552e4cb11ac7edaffe548cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Folder view configuration files +.DS_Store +.vscode +.eslintcache + +node_modules +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..627d6c57c8da98601cf2e2c71941ede306b0fa11 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..c2312c148eebfec7c98a7c009185a94b2f943859 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +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. \ No newline at end of file diff --git a/README.md b/README.md index ce848dcc4274dd66293109ba706622b8478fbe87..045d05aca9fbbcb590e568bbf765e472bb58f9f0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,510 @@ ---- -title: ListenOne -emoji: 👀 -colorFrom: blue -colorTo: blue -sdk: static -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# Listen 1 (Chrome Extension) V2.21.7 + +(最后更新于 2022 年 01 月 23 日) + +[![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/hneiglcmpeedblkmbndhfbeahcpjojjg) + +感谢 [@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,即使可能它即将成为历史。 + +## 更新日志 + +`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 diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000000000000000000000000000000000000..635a4c9e73947164c8c1ebcb829bfc1e02170f4c --- /dev/null +++ b/README_EN.md @@ -0,0 +1,358 @@ +# Listen 1 (Chrome Extension) V2.21.7 + +(Last Update Jan 23rd, 2022) + +[![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 + +`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) +- emove 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 scorll 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 functino: 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 playling 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 permession 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 diff --git a/css/common.css b/css/common.css new file mode 100644 index 0000000000000000000000000000000000000000..b1651e5fb155f7d6ffc4c6e3bf81334e50f9f911 --- /dev/null +++ b/css/common.css @@ -0,0 +1,1802 @@ +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 */ diff --git a/css/cover.css b/css/cover.css new file mode 100644 index 0000000000000000000000000000000000000000..7b8a6ffbe4a6979cf3c5f9203f504126267e32b1 --- /dev/null +++ b/css/cover.css @@ -0,0 +1,200 @@ +/* + * 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) !important; + height: var(--scrollWidth) !important; +} +::-webkit-scrollbar-button { + width: 0 !important; + height: 0 !important; +} +::-webkit-scrollbar-button:start:increment, +::-webkit-scrollbar-button:end:decrement { + display: none !important; +} +::-webkit-scrollbar-corner { + display: block !important; +} + +::-webkit-scrollbar-track, +::-webkit-scrollbar-thumb { + border-radius: 8px !important; + border-right: 1px solid transparent !important; + border-left: 1px solid transparent !important; +} + +::-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) !important; +} diff --git a/css/hotkeys.css b/css/hotkeys.css new file mode 100644 index 0000000000000000000000000000000000000000..55380ab603f6808816d69db257a596d25019cfc3 --- /dev/null +++ b/css/hotkeys.css @@ -0,0 +1,110 @@ +/*! + * 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; + } +} diff --git a/css/icon.css b/css/icon.css new file mode 100644 index 0000000000000000000000000000000000000000..569dee125db5db87e44315b954ca963f05a50ff2 --- /dev/null +++ b/css/icon.css @@ -0,0 +1,90 @@ +@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'; +} diff --git a/css/iparanoid.css b/css/iparanoid.css new file mode 100644 index 0000000000000000000000000000000000000000..33fb3cc3de1bbe04f5cad16ab5543584e16a6e62 --- /dev/null +++ b/css/iparanoid.css @@ -0,0 +1,84 @@ +/* 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; +} diff --git a/css/notyf.min.css b/css/notyf.min.css new file mode 100644 index 0000000000000000000000000000000000000000..dbb5a16487aa1e89b558a492fb32d00973a071ea --- /dev/null +++ b/css/notyf.min.css @@ -0,0 +1 @@ +@-webkit-keyframes notyf-fadeinup{0%{opacity:0;transform:translateY(25%)}to{opacity:1;transform:translateY(0)}}@keyframes notyf-fadeinup{0%{opacity:0;transform:translateY(25%)}to{opacity:1;transform:translateY(0)}}@-webkit-keyframes notyf-fadeinleft{0%{opacity:0;transform:translateX(25%)}to{opacity:1;transform:translateX(0)}}@keyframes notyf-fadeinleft{0%{opacity:0;transform:translateX(25%)}to{opacity:1;transform:translateX(0)}}@-webkit-keyframes notyf-fadeoutright{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(25%)}}@keyframes notyf-fadeoutright{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(25%)}}@-webkit-keyframes notyf-fadeoutdown{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(25%)}}@keyframes notyf-fadeoutdown{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(25%)}}@-webkit-keyframes ripple{0%{transform:scale(0) translateY(-45%) translateX(13%)}to{transform:scale(1) translateY(-45%) translateX(13%)}}@keyframes ripple{0%{transform:scale(0) translateY(-45%) translateX(13%)}to{transform:scale(1) translateY(-45%) translateX(13%)}}.notyf{position:fixed;top:0;left:0;height:100%;width:100%;color:#fff;z-index:9999;display:flex;flex-direction:column;align-items:flex-end;justify-content:flex-end;pointer-events:none;box-sizing:border-box;padding:20px}.notyf__icon--error,.notyf__icon--success{height:21px;width:21px;background:#fff;border-radius:50%;display:block;margin:0 auto;position:relative}.notyf__icon--error:after,.notyf__icon--error:before{content:"";background:currentColor;display:block;position:absolute;width:3px;border-radius:3px;left:9px;height:12px;top:5px}.notyf__icon--error:after{transform:rotate(-45deg)}.notyf__icon--error:before{transform:rotate(45deg)}.notyf__icon--success:after,.notyf__icon--success:before{content:"";background:currentColor;display:block;position:absolute;width:3px;border-radius:3px}.notyf__icon--success:after{height:6px;transform:rotate(-45deg);top:9px;left:6px}.notyf__icon--success:before{height:11px;transform:rotate(45deg);top:5px;left:10px}.notyf__toast{display:block;overflow:hidden;pointer-events:auto;-webkit-animation:notyf-fadeinup .3s ease-in forwards;animation:notyf-fadeinup .3s ease-in forwards;box-shadow:0 3px 7px 0 rgba(0,0,0,.25);position:relative;padding:0 15px;border-radius:2px;max-width:300px;transform:translateY(25%);box-sizing:border-box;flex-shrink:0}.notyf__toast--disappear{transform:translateY(0);-webkit-animation:notyf-fadeoutdown .3s forwards;animation:notyf-fadeoutdown .3s forwards;-webkit-animation-delay:.25s;animation-delay:.25s}.notyf__toast--disappear .notyf__icon,.notyf__toast--disappear .notyf__message{-webkit-animation:notyf-fadeoutdown .3s forwards;animation:notyf-fadeoutdown .3s forwards;opacity:1;transform:translateY(0)}.notyf__toast--disappear .notyf__dismiss{-webkit-animation:notyf-fadeoutright .3s forwards;animation:notyf-fadeoutright .3s forwards;opacity:1;transform:translateX(0)}.notyf__toast--disappear .notyf__message{-webkit-animation-delay:.05s;animation-delay:.05s}.notyf__toast--upper{margin-bottom:20px}.notyf__toast--lower{margin-top:20px}.notyf__toast--dismissible .notyf__wrapper{padding-right:30px}.notyf__ripple{height:400px;width:400px;position:absolute;transform-origin:bottom right;right:0;top:0;border-radius:50%;transform:scale(0) translateY(-51%) translateX(13%);z-index:5;-webkit-animation:ripple .4s ease-out forwards;animation:ripple .4s ease-out forwards}.notyf__wrapper{display:flex;align-items:center;padding-top:17px;padding-bottom:17px;padding-right:15px;border-radius:3px;position:relative;z-index:10}.notyf__icon{width:22px;text-align:center;font-size:1.3em;opacity:0;-webkit-animation:notyf-fadeinup .3s forwards;animation:notyf-fadeinup .3s forwards;-webkit-animation-delay:.3s;animation-delay:.3s;margin-right:13px}.notyf__dismiss{position:absolute;top:0;right:0;height:100%;width:26px;margin-right:-15px;-webkit-animation:notyf-fadeinleft .3s forwards;animation:notyf-fadeinleft .3s forwards;-webkit-animation-delay:.35s;animation-delay:.35s;opacity:0}.notyf__dismiss-btn{background-color:rgba(0,0,0,.25);border:none;cursor:pointer;transition:opacity .2s ease,background-color .2s ease;outline:none;opacity:.35;height:100%;width:100%}.notyf__dismiss-btn:after,.notyf__dismiss-btn:before{content:"";background:#fff;height:12px;width:2px;border-radius:3px;position:absolute;left:calc(50% - 1px);top:calc(50% - 5px)}.notyf__dismiss-btn:after{transform:rotate(-45deg)}.notyf__dismiss-btn:before{transform:rotate(45deg)}.notyf__dismiss-btn:hover{opacity:.7;background-color:rgba(0,0,0,.15)}.notyf__dismiss-btn:active{opacity:.8}.notyf__message{vertical-align:middle;position:relative;opacity:0;-webkit-animation:notyf-fadeinup .3s forwards;animation:notyf-fadeinup .3s forwards;-webkit-animation-delay:.25s;animation-delay:.25s;line-height:1.5em}@media only screen and (max-width:480px){.notyf{padding:0}.notyf__ripple{height:600px;width:600px;-webkit-animation-duration:.5s;animation-duration:.5s}.notyf__toast{max-width:none;border-radius:0;box-shadow:0 -2px 7px 0 rgba(0,0,0,.13);width:100%}.notyf__dismiss{width:56px}} \ No newline at end of file diff --git a/css/origin.css b/css/origin.css new file mode 100644 index 0000000000000000000000000000000000000000..b875533214f7d2134ed5f32b1fac3b6d62420992 --- /dev/null +++ b/css/origin.css @@ -0,0 +1,85 @@ +/* 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; +} diff --git a/css/player.css b/css/player.css new file mode 100644 index 0000000000000000000000000000000000000000..42498ec1e8201b28a50b41ede1d85a29cc7d9388 --- /dev/null +++ b/css/player.css @@ -0,0 +1,1225 @@ +a { + cursor: pointer; +} + +.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: 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; +} diff --git a/css/reset.css b/css/reset.css new file mode 100644 index 0000000000000000000000000000000000000000..521dd751b037a3a1aea94b9b7bc1233207f66680 --- /dev/null +++ b/css/reset.css @@ -0,0 +1,111 @@ +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; +} diff --git a/fonts/listen1-icon.eot b/fonts/listen1-icon.eot new file mode 100644 index 0000000000000000000000000000000000000000..cf4b5a41ab3db1c8c1a640da70b41d0031356e42 Binary files /dev/null and b/fonts/listen1-icon.eot differ diff --git a/fonts/listen1-icon.svg b/fonts/listen1-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..88bb53eff1e7df02e9be4ba3c0754b326530f82d --- /dev/null +++ b/fonts/listen1-icon.svg @@ -0,0 +1,31 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/listen1-icon.ttf b/fonts/listen1-icon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..80e4eb8e414a308e9d80ed964dbebe9625e434d3 Binary files /dev/null and b/fonts/listen1-icon.ttf differ diff --git a/fonts/listen1-icon.woff b/fonts/listen1-icon.woff new file mode 100644 index 0000000000000000000000000000000000000000..617652eb082c31f8013bed5a26396582431636da Binary files /dev/null and b/fonts/listen1-icon.woff differ diff --git a/i18n/en-US.json b/i18n/en-US.json new file mode 100644 index 0000000000000000000000000000000000000000..e43313bc449987b6168e0c2fdbfebce93a68915a --- /dev/null +++ b/i18n/en-US.json @@ -0,0 +1,172 @@ +{ + "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 STORNG 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 for song, artist or album", + "_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", + "_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" +} diff --git a/i18n/fr-FR.json b/i18n/fr-FR.json new file mode 100644 index 0000000000000000000000000000000000000000..14a4617741df50b3eb32fae7f241f01cde36cfd4 --- /dev/null +++ b/i18n/fr-FR.json @@ -0,0 +1,172 @@ +{ + "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": "Rechercher un morceau, un artiste ou un album", + "_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é", + "_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" +} diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json new file mode 100644 index 0000000000000000000000000000000000000000..24fd208c7e8fe713ac93567c33fc82c2cfcbbb3b --- /dev/null +++ b/i18n/zh-CN.json @@ -0,0 +1,172 @@ +{ + "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": "深空灰", + "_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": "放大/缩小" +} diff --git a/i18n/zh-TC.json b/i18n/zh-TC.json new file mode 100644 index 0000000000000000000000000000000000000000..b60bc31482b8574a5f8db18d4a2cc64c3f375484 --- /dev/null +++ b/i18n/zh-TC.json @@ -0,0 +1,172 @@ +{ + "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": "深空灰", + "_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": "放大/縮小" +} diff --git a/images/favicon.ico b/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0995a87051e9a592b5373d24590a9ac45bda9955 Binary files /dev/null and b/images/favicon.ico differ diff --git a/images/feather-sprite.svg b/images/feather-sprite.svg new file mode 100644 index 0000000000000000000000000000000000000000..6e20a2c140ce32ecef4d6b30ef2f608c239baea2 --- /dev/null +++ b/images/feather-sprite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/loading-1.gif b/images/loading-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..a2239ba488de6d67f64b3efddec4e09f6e398de6 Binary files /dev/null and b/images/loading-1.gif differ diff --git a/images/loading.gif b/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..6a2a3a931d634717d69c4f0a828bc9f5c592e0cd Binary files /dev/null and b/images/loading.gif differ diff --git a/images/loading.svg b/images/loading.svg new file mode 100644 index 0000000000000000000000000000000000000000..6996bcbe6138fd6fc32ffd319e1f409bca5463dd --- /dev/null +++ b/images/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..11fd9ace90869646b3ac58869bdcfa9386f1b23c Binary files /dev/null and b/images/logo.png differ diff --git a/images/logo_16.png b/images/logo_16.png new file mode 100644 index 0000000000000000000000000000000000000000..885187d14cd740cab01a35123860c9e4e3a05f83 Binary files /dev/null and b/images/logo_16.png differ diff --git a/images/logo_48.png b/images/logo_48.png new file mode 100644 index 0000000000000000000000000000000000000000..1e0b0fd0b7d738660197cd88dc624e06e37580dc Binary files /dev/null and b/images/logo_48.png differ diff --git a/images/mycover.jpg b/images/mycover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5a36abeb63c81ca9066ee560bf405553a2514733 Binary files /dev/null and b/images/mycover.jpg differ diff --git a/images/netease-logo.png b/images/netease-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b14f263db1221992ce909ad711566f8856eeb6f8 Binary files /dev/null and b/images/netease-logo.png differ diff --git a/images/placeholder.png b/images/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..0ac0d9098497b874e3bc2a3a2a49dfe766460127 Binary files /dev/null and b/images/placeholder.png differ diff --git a/images/player_directplay.png b/images/player_directplay.png new file mode 100644 index 0000000000000000000000000000000000000000..b55a10b711458d2db93d4109b08636f6b95be1a8 Binary files /dev/null and b/images/player_directplay.png differ diff --git a/images/player_large.png b/images/player_large.png new file mode 100644 index 0000000000000000000000000000000000000000..b670fcee10eebc5783d2c05ae034a68699b280b0 Binary files /dev/null and b/images/player_large.png differ diff --git a/images/player_small.png b/images/player_small.png new file mode 100644 index 0000000000000000000000000000000000000000..c426d3895983f623c972fc47f1ee91616ed5d785 Binary files /dev/null and b/images/player_small.png differ diff --git a/images/progress_indicator.png b/images/progress_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..b086d0a7ad014a6f304d4efb484e623fbb3a74d5 Binary files /dev/null and b/images/progress_indicator.png differ diff --git a/images/statbar.png b/images/statbar.png new file mode 100644 index 0000000000000000000000000000000000000000..3bd291583475fb49876f238399c7024bf1dd7ec9 Binary files /dev/null and b/images/statbar.png differ diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000000000000000000000000000000000000..94c5c6d32ee8bbf369f9d79445baed1f89c377ff --- /dev/null +++ b/js/app.js @@ -0,0 +1,500 @@ +/* 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', + searchable: false, + }, + { + 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, + }, + ], + }); + 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, + }); + }; + axios.get('images/feather-sprite.svg').then((res) => { + document.getElementById('feather-container').innerHTML = res.data; + }); + }, + ]); + + 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') { + scope.changingProgress = true; + } + } + + function onMyMouseup() { + if (mode === 'play') { + scope.changingProgress = false; + } + } + + function onMyUpdateProgress(progress) { + if (mode === 'play') { + $rootScope.$broadcast('track: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'], + preload: ['zh-CN', 'zh-TC', 'en-US', 'fr-FR'], + debug: false, + backend: { + loadPath: 'i18n/{{lng}}.json', + }, +}); + +main(); diff --git a/js/background.js b/js/background.js new file mode 100644 index 0000000000000000000000000000000000000000..89057dac940a4d0e2cf31396771459a1854b7730 --- /dev/null +++ b/js/background.js @@ -0,0 +1,213 @@ +/* eslint-disable no-unused-vars */ +/* global GithubClient */ +chrome.browserAction.onClicked.addListener((tab) => { + chrome.tabs.create( + { + url: chrome.extension.getURL('listen1.html'), + }, + (new_tab) => { + // Tab opened. + } + ); +}); + +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/'; + } + + 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('.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 = + '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'; + 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/*', + '*://*.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(); +}); + +// at end of background.js +chrome.commands.onCommand.addListener((command) => { + const [viewWindow] = chrome.extension + .getViews() + .filter((p) => p.location.href.endsWith('listen1.html')); + + switch (command) { + case 'play_next': + viewWindow.document.querySelector('.li-next').click(); + break; + case 'play_prev': + viewWindow.document.querySelector('.li-previous').click(); + break; + case 'play_pause': + viewWindow.document.querySelector('.play').click(); + break; + default: + // console.log('不支持的快捷键') + } +}); diff --git a/js/bridge.js b/js/bridge.js new file mode 100644 index 0000000000000000000000000000000000000000..6f4935de53fae0844b83077da8c39901dcb979de --- /dev/null +++ b/js/bridge.js @@ -0,0 +1,91 @@ +/* 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); + } +} diff --git a/js/controller/auth.js b/js/controller/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..a3ce9e60ebcdad59fa293615b9bb27336b9694f9 --- /dev/null +++ b/js/controller/auth.js @@ -0,0 +1,49 @@ +/* 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'); + }; + }, +]); diff --git a/js/controller/instant_search.js b/js/controller/instant_search.js new file mode 100644 index 0000000000000000000000000000000000000000..1623d29d271828d143e78c94cfe06c9b078ba17a --- /dev/null +++ b/js/controller/instant_search.js @@ -0,0 +1,139 @@ +/* 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(); + }; + }, +]); diff --git a/js/controller/my_playlist.js b/js/controller/my_playlist.js new file mode 100644 index 0000000000000000000000000000000000000000..5febae55668851b37dc5009210e5bedc61286085 --- /dev/null +++ b/js/controller/my_playlist.js @@ -0,0 +1,43 @@ +/* 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(); + }); + }, +]); diff --git a/js/controller/navigation.js b/js/controller/navigation.js new file mode 100644 index 0000000000000000000000000000000000000000..8a7fd6950ea7b7cb442db39e5e1942f1b82bc49b --- /dev/null +++ b/js/controller/navigation.js @@ -0,0 +1,680 @@ +/* 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.$on('isdoubanlogin:update', (event, data) => { + $scope.isDoubanLogin = data; + }); + + // 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; + } + // 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) => { + $scope.is_favorite = res.result; + }); + + $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.indexOf($scope.playlistFilter.key) > -1 || + song.artist.indexOf($scope.playlistFilter.key) > -1 || + song.album.indexOf($scope.playlistFilter.key) > -1 + ); + }; + $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.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'); + const remoteFunctions = remote.require('./functions.js'); + remote.dialog + .showOpenDialog({ + title: '添加歌曲', + properties: ['openFile', 'multiSelections'], + filters: [ + { + name: 'Music Files', + extensions: ['mp3', 'flac', 'ape'], + }, + ], + }) + .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); + }); + } + }; + }, +]); diff --git a/js/controller/platform.js b/js/controller/platform.js new file mode 100644 index 0000000000000000000000000000000000000000..06988ebd007d0b5aac80f27ffd92fbb041bf3b11 --- /dev/null +++ b/js/controller/platform.js @@ -0,0 +1,58 @@ +/* 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(); + }; + }, +]); diff --git a/js/controller/play.js b/js/controller/play.js new file mode 100644 index 0000000000000000000000000000000000000000..fcdf3b3e937dc5d54e9a7b739db4a3bc35e5d902 --- /dev/null +++ b/js/controller/play.js @@ -0,0 +1,827 @@ +/* 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; + } + `; +} + +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.currentPosition = '0:00'; + + 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('track:myprogress', (event, data) => { + $scope.$evalAsync(() => { + // should use apply to force refresh ui + $scope.myProgress = 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(timeRegResult[0], '') + .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( + `.page .playsong-detail .detail-songinfo .lyric p[data-line="${lastObject.lineNumber}"]` + ); + const windowHeight = document.querySelector( + '.page .playsong-detail .detail-songinfo .lyric' + ).offsetHeight; + + 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; + })(); + + // '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 = `${Math.floor(posSec / 60)}:${`0${ + posSec % 60 + }`.substr(-2)}`; + $scope.currentPosition = posStr; + }); + } + break; + } + + case 'LOAD': { + $scope.currentPlaying = msg.data; + if (msg.data.id === undefined) { + break; + } + $scope.currentPlaying.platformText = i18next.t( + $scope.currentPlaying.platform + ); + $scope.myProgress = 0; + if ($scope.lastTrackId === msg.data.id) { + break; + } + const current = localStorage.getObject('player-settings') || {}; + current.nowplaying_track_id = msg.data.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; + $rootScope.page_title = { + title: track.title, + artist: track.artist, + status: 'playing', + }; + if (lastfm.isAuthorized()) { + lastfm.sendNowPlaying(track.title, track.artist, () => {}); + } + + MediaService.getLyric( + msg.data.id, + msg.data.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.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; + 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 + ); + }; + }, +]); diff --git a/js/controller/playlist.js b/js/controller/playlist.js new file mode 100644 index 0000000000000000000000000000000000000000..9f07639eadc37aff310b583fc534e7111aa49fc8 --- /dev/null +++ b/js/controller/playlist.js @@ -0,0 +1,73 @@ +/* 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; + }; + }, +]); diff --git a/js/controller/profile.js b/js/controller/profile.js new file mode 100644 index 0000000000000000000000000000000000000000..cc690ecd408fa81f2d1891621f56e8d02832c00c --- /dev/null +++ b/js/controller/profile.js @@ -0,0 +1,143 @@ +/* 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.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) => { + const themeFiles = { + white: 'css/iparanoid.css', + black: 'css/origin.css', + }; + // You can change the language during runtime + if (themeFiles[theme] !== undefined) { + document.getElementById('theme').href = themeFiles[theme]; + localStorage.setObject('theme', theme); + } + }; + $scope.setTheme(defaultTheme); + }, +]); diff --git a/js/github.js b/js/github.js new file mode 100644 index 0000000000000000000000000000000000000000..2c798da4781f1d646fbed32f0f845340d13861a1 --- /dev/null +++ b/js/github.js @@ -0,0 +1,187 @@ +/* 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(); diff --git a/js/l1_player.js b/js/l1_player.js new file mode 100644 index 0000000000000000000000000000000000000000..08aad1f0b3622a45bf6d1e9c7b9cc6384dc51147 --- /dev/null +++ b/js/l1_player.js @@ -0,0 +1,238 @@ +/* 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; +} diff --git a/js/lastfm.js b/js/lastfm.js new file mode 100644 index 0000000000000000000000000000000000000000..127ec50b9e5c1fd2e9b935d5d89f4c2423175e26 --- /dev/null +++ b/js/lastfm.js @@ -0,0 +1,243 @@ +/* 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; +} + diff --git a/js/loweb.js b/js/loweb.js new file mode 100644 index 0000000000000000000000000000000000000000..72df95108426220e7e0ce3faca12f2001b84de77 --- /dev/null +++ b/js/loweb.js @@ -0,0 +1,444 @@ +/* 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: false, + 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; diff --git a/js/lowebutil.js b/js/lowebutil.js new file mode 100644 index 0000000000000000000000000000000000000000..baf2bbd517973a9b7d654a3f527f59e95e9d86b8 --- /dev/null +++ b/js/lowebutil.js @@ -0,0 +1,111 @@ +/* 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, '\\$&'); // eslint-disable-line no-useless-escape + 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); + return value && JSON.parse(value); + }; + 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) { + /* https://gist.github.com/andjosh/6764939 */ + const start = element.scrollTop; + const change = to - start; + let currentTime = 0; + const increment = 20; + + const animateScroll = () => { + currentTime += increment; + const val = easeInOutQuad(currentTime, start, change, duration); + element.scrollTop = val; + if (currentTime < duration) { + setTimeout(animateScroll, increment); + } + }; + animateScroll(); +} diff --git a/js/myplaylist.js b/js/myplaylist.js new file mode 100644 index 0000000000000000000000000000000000000000..8be372c2ad020eb0e0a1d3cc6289de080d134243 --- /dev/null +++ b/js/myplaylist.js @@ -0,0 +1,262 @@ +/* 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 diff --git a/js/oauth_callback.js b/js/oauth_callback.js new file mode 100644 index 0000000000000000000000000000000000000000..e8edf1f8d18eed36b7b9bc2bd8f46ae9cf5caba5 --- /dev/null +++ b/js/oauth_callback.js @@ -0,0 +1,15 @@ +/** + * 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(); + } +); diff --git a/js/player_thread.js b/js/player_thread.js new file mode 100644 index 0000000000000000000000000000000000000000..3235c42b9b00a976236972ef40e052721cfcbfef --- /dev/null +++ b/js/player_thread.js @@ -0,0 +1,627 @@ +/* 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.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.playlist = []; + this.stopAll(); + 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]], + 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: `Listen1 • ${( + self.currentAudio.album || '' + ).padEnd(100)}`, + artwork: [ + { + src: self.currentAudio.img_url, + sizes: '300x300', + }, + ], + }); + } + self.currentAudio.disabled = false; + self.playedFrom = Date.now(); + 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(); + 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. + let nextIndexFn = null; + if (this._loop_mode === 2 || direction === 'random') { + // TODO: shuffle algorithm instead of random + nextIndexFn = () => Math.floor(Math.random() * this.playlist.length); + } else if (direction === 'prev') { + nextIndexFn = (idx) => (idx - 1) % this.playlist.length; + } else if (direction === 'next') { + nextIndexFn = (idx) => (idx + 1) % this.playlist.length; + } + 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: { + ...this.currentAudio, + howl: undefined, + }, + }); + } + + 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', + }); +} diff --git a/js/provider/bilibili.js b/js/provider/bilibili.js new file mode 100644 index 0000000000000000000000000000000000000000..c0ac53067e0f7937eb0aa2ad52b34c9f536a7f08 --- /dev/null +++ b/js/provider/bilibili.js @@ -0,0 +1,252 @@ +/* global getParameterByName */ +// eslint-disable-next-line no-unused-vars +class bilibili { + 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 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_artist(url) { + return { + success: (fn) => { + const artist_id = getParameterByName('list_id', url).split('_').pop(); + let target_url = `https://api.bilibili.com/x/space/acc/info?mid=${artist_id}&jsonp=jsonp`; + axios.get(target_url).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`, + }; + target_url = `https://api.bilibili.com/audio/music-service-c/web/song/upper?pn=1&ps=0&order=2&uid=${artist_id}`; + 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 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}`; + 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 au = /\d+$/.exec(keyword); + if (au != null) { + const target_url = `https://www.bilibili.com/audio/music-service-c/web/song/info?sid=${au}`; + axios.get(target_url).then((response) => { + const { data } = response.data; + const tracks = [this.bi_convert_song(data)]; + return fn({ + result: tracks, + total: 1, + }); + }); + } else { + return fn({ + result: [], + total: 0, + }); + } + // inferred, not implemented yet + const target_url = `https://api.bilibili.com/x/web-interface/search/type?search_type=audio&keyword=${keyword}&page=${curpage}`; + axios.get(target_url).then((response) => { + const { data } = response.data; + const tracks = data.result.map((item) => this.bi_convert_song(item)); + return fn({ + result: tracks, + total: data.numResults, + }); + }); + return null; + }, + }; + } + + static lyric(url) { + // const track_id = getParameterByName('track_id', url).split('_').pop(); + const lyric_url = getParameterByName('lyric_url', url); + return { + success: (fn) => { + axios.get(lyric_url).then((response) => { + const { data } = response; + return fn({ + lyric: data, + }); + }); + }, + }; + } + + 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); + 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, + // }; +} diff --git a/js/provider/kugou.js b/js/provider/kugou.js new file mode 100644 index 0000000000000000000000000000000000000000..24204c04861159cf06dff98d28ca73a8f3aa765a --- /dev/null +++ b/js/provider/kugou.js @@ -0,0 +1,485 @@ +/* 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) => + 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 = `${'http://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 = `http://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 album_id = track.album_id.slice('kgalbum_'.length); + let target_url = `https://wwwapi.kugou.com/yy/index.php?r=play/getdata&callback=jQuery&hash=${track_id}&dfid=dfid&mid=mid&platid=4`; + if (album_id !== '') { + target_url += `&album_id=${album_id}`; + } + axios.get(target_url).then((response) => { + const { data } = response; + const jsonString = data.slice('jQuery('.length, data.length - 1 - 1); + const info = JSON.parse(jsonString); + const { play_url } = info.data; + + if (play_url === '') { + return failure({}); + } + + return success({ + url: play_url, + bitrate: `${info.data.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(); + const lyric_url = `https://wwwapi.kugou.com/yy/index.php?r=play/getdata&callback=jQuery&hash=${track_id}&dfid=dfid&mid=mid&platid=4&album_id=${album_id}`; + + 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 = `${'http://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 = `${'http://m.kugou.com/plist/index&json=true&page='}${offset}`; + 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, + // }; +} diff --git a/js/provider/kuwo.js b/js/provider/kuwo.js new file mode 100644 index 0000000000000000000000000000000000000000..69e184ea4150ec0885d9e494e14c552a8ea16abb --- /dev/null +++ b/js/provider/kuwo.js @@ -0,0 +1,717 @@ +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +/* global async getParameterByName isElectron */ +class kuwo { + // 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, + }; + } + + /* + 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 = 'kw_token'; + + 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) { + this.kw_get_token((token) => { + axios + .get(url, { + headers: { + csrf: token, + }, + }) + .then((response) => { + if (response.data.success === false) { + // token expire, refetch token and start get url + this.kw_get_token((token2) => { + axios + .get(url, { + headers: { + csrf: token2, + }, + }) + .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 searchType = getParameterByName('type', url); + let api = ''; + switch (searchType) { + case '0': + api = 'searchMusicBykeyWord'; + break; + case '1': + api = 'searchPlayListBykeyWord'; + break; + default: + break; + } + const target_url = `https://www.kuwo.cn/api/www/search/${api}?key=${keyword}&pn=${curpage}&rn=20`; + 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.data !== undefined) { + result = response.data.data.list.map((item) => + this.kw_convert_song2(item) + ); + total = response.data.data.total; + } 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://antiserver.kuwo.cn/anti.s?' + + `type=convert_url&format=mp3&response=url&rid=${song_id}`; + /* const target_url = 'https://www.kuwo.cn/url?' + + `format=mp3&rid=${song_id}&response=url&type=convert_url3&br=128kmp3&from=web`; + https://m.kuwo.cn/newh5app/api/mobile/v1/music/src/${song_id} */ + + axios.get(target_url).then((response) => { + const { data } = response; + if (data.length > 0) { + sound.url = data; + 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, + // }; +} diff --git a/js/provider/localmusic.js b/js/provider/localmusic.js new file mode 100644 index 0000000000000000000000000000000000000000..3e467998055b3d98e791fc5eebf4d9702ee7c243 --- /dev/null +++ b/js/provider/localmusic.js @@ -0,0 +1,180 @@ +/* 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, + // }; +} diff --git a/js/provider/migu.js b/js/provider/migu.js new file mode 100644 index 0000000000000000000000000000000000000000..ba1219cd7b81c1b68418f1c07a2d24d74ba94c89 --- /dev/null +++ b/js/provider/migu.js @@ -0,0 +1,952 @@ +/* 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, + }; + } + + 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.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: '', + }, + { + cover_img_url: + 'https://cdnmusic.migu.cn/tycms_picture/20/05/136/200515161733982_360x360_1523.png', + title: '音乐榜', + id: 'mgtoplist_1', + source: '', + }, + { + cover_img_url: + 'https://cdnmusic.migu.cn/tycms_picture/20/05/136/200515161848938_360x360_673.png', + title: '影视榜', + id: 'mgtoplist_2', + 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', + }, + }; + let target_url = ''; + if (list_id === 1 || list_id === 2) { + target_url = `https://music.migu.cn/v3/music/top/${board_list[list_id].url}`; + } else { + target_url = `https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-detail/release?columnId=${list_id}&needAll=0&resourceType=2009`; + } + + 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 = {}; + if (list_id === 1 || list_id === 2) { + // 音乐榜及影视榜 + const list_elements = new DOMParser() + .parseFromString(data, 'text/html') + .getElementsByTagName('script'); + const result = JSON.parse( + list_elements[1].innerText.split('=').pop() + ); + tracks = result.songs.items.map((song) => { + const track = { + id: `mgtrack_${song.copyrightId}`, + title: song.name, + artist: song.singers[0].name, + artist_id: `mgartist_${song.singers[0].id}`, + album: song.album.albumId !== 1 ? song.album.albumName : '', + album_id: + song.album.albumId !== 1 + ? `mgalbum_${song.album.albumId}` + : 'mgalbum_', + 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; + }); + } else if (list_id === 23217754) { + // MV榜 + tracks = data.data.columnInfo.dataList.map((song) => ({ + id: `mgtrack_${song.copyrightId}`, + title: song.songName, + artist: song.singer, + artist_id: `mgartist_${song.singerId}`, + album: '', + album_id: 'mgalbum_', + source: 'migu', + source_url: `https://music.migu.cn/v3/music/song/${song.copyrightId}`, + img_url: song.imgs[1].img, + // url: `mgtrack_${song.copyrightId}`, + lyric_url: null, + tlyric_url: '', + song_id: song.songId, + url: song.copyright === 0 ? '' : undefined, + })); + } else if (list_id === 23218151 || list_id === 33683712) { + // 新专辑榜及数字专辑畅销榜 + tracks = data.data.columnInfo.dataList.map((item) => ({ + id: `mgtrack_`, + title: '', + artist: item.singer, + artist_id: `mgartist_${item.singerId}`, + album: item.title, + album_id: item.albumId ? `mgalbum_${item.albumId}` : 'mgalbum_', + source: 'migu', + source_url: `https://music.migu.cn/v3/music/album/${ + item.albumId || '' + }`, + img_url: item.imgItems[1].img, + // url: `mgtrack_${song.copyrightId}`, + lyric_url: '', + tlyric_url: '', + url: '', + })); + } else { + tracks = data.data.columnInfo.dataList.map((item) => + this.mg_convert_song(item) + ); + } + 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 songId = track.song_id; + /* + 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}`; + 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}`; + + 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.songResultData.result) { + result = data.songResultData.result.map((item) => + this.mg_convert_song(item) + ); + total = data.songResultData.totalCount; + } + } 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.musicListPicUrl, + 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, + // }; +} diff --git a/js/provider/netease.js b/js/provider/netease.js new file mode 100644 index 0000000000000000000000000000000000000000..06f9014f2f6c5e77910710aa9bb194394c58a112 --- /dev/null +++ b/js/provider/netease.js @@ -0,0 +1,946 @@ +/* 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, + 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_nnid'; + + cookieGet( + { + url: domain, + name: nuidName, + }, + (cookie) => { + if (cookie == null) { + const nuidValue = this._create_secret_key(32); + const nnidValue = `${nuidValue},${new Date().getTime()}`; + // netease default cookie expire time: 100 years + const expire = + (new Date().getTime() + 1e3 * 60 * 60 * 24 * 365 * 100) / 1000; + + cookieSet( + { + url: domain, + name: nuidName, + value: nuidValue, + expirationDate: expire, + }, + () => { + cookieSet( + { + url: domain, + name: nnidName, + value: nnidValue, + expirationDate: expire, + }, + () => { + callback(null); + } + ); + } + ); + } else { + 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) => { + 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) => {} + ); + } +} diff --git a/js/provider/qq.js b/js/provider/qq.js new file mode 100644 index 0000000000000000000000000000000000000000..0ec277235791ee113ab7c8bd9c61a07045c5159d --- /dev/null +++ b/js/provider/qq.js @@ -0,0 +1,830 @@ +/* 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 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://i.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg' + + `?platform=h5page&order=listen&begin=0&num=50&singermid=${artist_id}` + + '&g_tk=938407465&uin=0&format=json&' + + 'inCharset=utf-8&outCharset=utf-8¬ice=0&platform=' + + 'h5&needNewCode=1&from=h5&_=1459960621777'; + axios.get(target_url).then((response) => { + const { data } = response; + + const info = { + cover_img_url: this.qq_get_image_url(artist_id, 'artist'), + title: data.data.singer_name, + id: `qqartist_${artist_id}`, + source_url: `https://y.qq.com/#type=singer&mid=${artist_id}`, + }; + + const tracks = data.data.list.map((item) => + this.qq_convert_song(item.musicData) + ); + 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); + let target_url = ''; + switch (searchType) { + case '0': + target_url = + 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' + + 'g_tk=938407465&uin=0&format=json&inCharset=utf-8' + + '&outCharset=utf-8¬ice=0&platform=h5&needNewCode=1' + + `&w=${keyword}&zhidaqu=1&catZhida=1` + + `&t=0&flag=1&ie=utf-8&sem=1&aggr=0&perpage=20&n=20&p=${curpage}&remoteplace=txt.mqq.all&_=1459991037831`; + break; + case '1': + target_url = + 'https://c.y.qq.com/soso/fcgi-bin/client_music_search_songlist?' + + 'remoteplace=txt.yqq.playlist&&outCharset=utf-8&format=json' + + `&page_no=${curpage - 1}&num_per_page=20&query=${keyword}`; + break; + default: + break; + } + return { + success: (fn) => { + axios.get(target_url).then((response) => { + const { data } = response; + let result = []; + let total = 0; + if (searchType === '0') { + result = data.data.song.list.map((item) => + this.qq_convert_song(item) + ); + total = data.data.song.totalnum; + } else if (searchType === '1') { + result = data.data.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.data.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'; + + 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_0: { + 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, + }, + }; + const params = { + format: 'json', + data: JSON.stringify(reqData), + }; + axios.get(target_url, { params }).then((response) => { + const { data } = response; + const { purl } = data.req_0.data.midurlinfo[0]; + + if (purl === '') { + // vip + failure(sound); + return; + } + const url = data.req_0.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', + }, + () => {} + ); + } + ); + } +} diff --git a/js/provider/taihe.js b/js/provider/taihe.js new file mode 100644 index 0000000000000000000000000000000000000000..460efb48acc20def4a69e9b6346505c4e3deaa93 --- /dev/null +++ b/js/provider/taihe.js @@ -0,0 +1,391 @@ +/* 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() {} +} diff --git a/js/provider/xiami.js b/js/provider/xiami.js new file mode 100644 index 0000000000000000000000000000000000000000..6d8f359eab3afe0741c3ce1b6e68de837caae2a4 --- /dev/null +++ b/js/provider/xiami.js @@ -0,0 +1,152 @@ +/* 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, + // }; +} diff --git a/js/vendor/angular.min.js b/js/vendor/angular.min.js new file mode 100644 index 0000000000000000000000000000000000000000..87108f9aed6a90b190f8474d386971b2425b2524 --- /dev/null +++ b/js/vendor/angular.min.js @@ -0,0 +1,352 @@ +/* + AngularJS v1.8.2 + (c) 2010-2020 Google LLC. http://angularjs.org + License: MIT +*/ +(function(z){'use strict';function ve(a){if(D(a))w(a.objectMaxDepth)&&(Xb.objectMaxDepth=Yb(a.objectMaxDepth)?a.objectMaxDepth:NaN),w(a.urlErrorParamsEnabled)&&Ga(a.urlErrorParamsEnabled)&&(Xb.urlErrorParamsEnabled=a.urlErrorParamsEnabled);else return Xb}function Yb(a){return X(a)&&0c)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f").append(a).html();try{return a[0].nodeType===Pa?K(b):b.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+K(b)})}catch(d){return K(b)}}function Vc(a){try{return decodeURIComponent(a)}catch(b){}}function hc(a){var b={};r((a||"").split("&"), +function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Vc(e),w(e)&&(f=w(f)?Vc(f):!0,ta.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Ce(a){var b=[];r(a,function(a,c){H(a)?r(a,function(a){b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))}):b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))});return b.length?b.join("&"):""}function ic(a){return ba(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ba(a, +b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function De(a,b){var d,c,e=Qa.length;for(c=0;c protocol indicates an extension, document.location.href does not match."))}function Wc(a,b,d){D(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=x(a);if(a.injector()){var c=a[0]===z.document?"document":Aa(a);throw oa("btstrpd",c.replace(//,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider", +function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=fb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;z&&e.test(z.name)&&(d.debugInfoEnabled=!0,z.name=z.name.replace(e,""));if(z&&!f.test(z.name))return c();z.name=z.name.replace(f,"");ca.resumeBootstrap=function(a){r(a,function(a){b.push(a)});return c()};B(ca.resumeDeferredBootstrap)&& +ca.resumeDeferredBootstrap()}function Ge(){z.name="NG_ENABLE_DEBUG_INFO!"+z.name;z.location.reload()}function He(a){a=ca.element(a).injector();if(!a)throw oa("test");return a.get("$$testability")}function Xc(a,b){b=b||"_";return a.replace(Ie,function(a,c){return(c?b:"")+a.toLowerCase()})}function Je(){var a;if(!Yc){var b=rb();(sb=A(b)?z.jQuery:b?z[b]:void 0)&&sb.fn.on?(x=sb,S(sb.fn,{scope:Wa.scope,isolateScope:Wa.isolateScope,controller:Wa.controller,injector:Wa.injector,inheritedData:Wa.inheritedData})): +x=U;a=x.cleanData;x.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=(x._data(f)||{}).events)&&c.$destroy&&x(f).triggerHandler("$destroy");a(b)};ca.element=x;Yc=!0}}function Ke(){U.legacyXHTMLReplacement=!0}function gb(a,b,d){if(!a)throw oa("areq",b||"?",d||"required");return a}function tb(a,b,d){d&&H(a)&&(a=a[a.length-1]);gb(B(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ja(a,b){if("hasOwnProperty"===a)throw oa("badname", +b);}function Le(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g"):a;if(10>wa)for(c=hb[c]||hb._default,d.innerHTML=c[1]+e+c[2],k=c[0];k--;)d=d.firstChild;else{c=qa[c]||[];for(k=c.length;-1<--k;)d.appendChild(z.document.createElement(c[k])),d=d.firstChild;d.innerHTML=e}g=db(g,d.childNodes);d=f.firstChild;d.textContent=""}else g.push(b.createTextNode(a)); +f.textContent="";f.innerHTML="";r(g,function(a){f.appendChild(a)});return f}function U(a){if(a instanceof U)return a;var b;C(a)&&(a=V(a),b=!0);if(!(this instanceof U)){if(b&&"<"!==a.charAt(0))throw oc("nosel");return new U(a)}if(b){b=z.document;var d;a=(d=tg.exec(a))?[b.createElement(d[1])]:(d=gd(a,b))?d.childNodes:[];pc(this,a)}else B(a)?hd(a):pc(this,a)}function qc(a){return a.cloneNode(!0)}function zb(a,b){!b&&mc(a)&&x.cleanData([a]);a.querySelectorAll&&x.cleanData(a.querySelectorAll("*"))}function id(a){for(var b in a)return!1; +return!0}function jd(a){var b=a.ng339,d=b&&Ka[b],c=d&&d.events,d=d&&d.data;d&&!id(d)||c&&!id(c)||(delete Ka[b],a.ng339=void 0)}function kd(a,b,d,c){if(w(c))throw oc("offargs");var e=(c=Ab(a))&&c.events,f=c&&c.handle;if(f){if(b){var g=function(b){var c=e[b];w(d)&&cb(c||[],d);w(d)&&c&&0l&&this.remove(n.key);return b}},get:function(a){if(l";b=Fa.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function sa(a,b){try{a.addClass(b)}catch(c){}}function da(a,b,c,d,e){a instanceof x||(a=x(a));var f=Xa(a,b,a,c,d,e);da.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw $("multilink");gb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement; +h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ua(d)&&la.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?x(ja(g,x("
").append(a).html())):c?Wa.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);da.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Xa(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,p,I,t;if(n)for(t=Array(c.length),m=0;mu.priority)break;if(O=u.scope)u.templateUrl||(D(O)?(ba("new/isolated scope",s||t,u,y),s=u):ba("new/isolated scope",s,u,y)),t=t||u;Q=u.name;if(!ma&&(u.replace&&(u.templateUrl||u.template)||u.transclude&& +!u.$$tlb)){for(O=sa+1;ma=a[O++];)if(ma.transclude&&!ma.$$tlb||ma.replace&&(ma.templateUrl||ma.template)){Jb=!0;break}ma=!0}!u.templateUrl&&u.controller&&(J=J||T(),ba("'"+Q+"' controller",J[Q],u,y),J[Q]=u);if(O=u.transclude)if(G=!0,u.$$tlb||(ba("transclusion",L,u,y),L=u),"element"===O)N=!0,n=u.priority,M=y,y=d.$$element=x(da.$$createComment(Q,d[Q])),b=y[0],oa(f,Ha.call(M,0),b),R=Z(Jb,M,e,n,g&&g.name,{nonTlbTranscludeDirective:L});else{var ka=T();if(D(O)){M=z.document.createDocumentFragment();var Xa= +T(),F=T();r(O,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Xa[a]=b;ka[b]=null;F[b]=c});r(y.contents(),function(a){var b=Xa[xa(ua(a))];b?(F[b]=!0,ka[b]=ka[b]||z.document.createDocumentFragment(),ka[b].appendChild(a)):M.appendChild(a)});r(F,function(a,b){if(!a)throw $("reqslot",b);});for(var K in ka)ka[K]&&(R=x(ka[K].childNodes),ka[K]=Z(Jb,R,e));M=x(M.childNodes)}else M=x(qc(b)).contents();y.empty();R=Z(Jb,M,e,void 0,void 0,{needsNewScope:u.$$isolateScope||u.$$newScope});R.$$slots=ka}if(u.template)if(P= +!0,ba("template",v,u,y),v=u,O=B(u.template)?u.template(y,d):u.template,O=Na(O),u.replace){g=u;M=nc.test(O)?td(ja(u.templateNamespace,V(O))):[];b=M[0];if(1!==M.length||1!==b.nodeType)throw $("tplrt",Q,"");oa(f,y,b);C={$attr:{}};O=tc(b,[],C);var Ig=a.splice(sa+1,a.length-(sa+1));(s||t)&&fa(O,s,t);a=a.concat(O).concat(Ig);ga(d,C);C=a.length}else y.html(O);if(u.templateUrl)P=!0,ba("template",v,u,y),v=u,u.replace&&(g=u),p=ha(a.splice(sa,a.length-sa),y,d,f,G&&R,h,k,{controllerDirectives:J,newScopeDirective:t!== +u&&t,newIsolateScopeDirective:s,templateDirective:v,nonTlbTranscludeDirective:L}),C=a.length;else if(u.compile)try{q=u.compile(y,d,R);var Y=u.$$originalDirective||u;B(q)?m(null,Va(Y,q),E,jb):q&&m(Va(Y,q.pre),Va(Y,q.post),E,jb)}catch(ca){c(ca,Aa(y))}u.terminal&&(p.terminal=!0,n=Math.max(n,u.priority))}p.scope=t&&!0===t.scope;p.transcludeOnThisElement=G;p.templateOnThisElement=P;p.transclude=R;l.hasElementTranscludeDirective=N;return p}function X(a,b,c,d){var e;if(C(b)){var f=b.match(l);b=b.substring(f[0].length); +var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e="^^"===g&&c[0]&&9===c[0].nodeType?null:g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw $("ctreq",b,a);}else if(H(b))for(e=[],g=0,f=b.length;gc.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=bc(c,{$$start:k,$$end:l}));if(!c.$$bindings){var I=m=c,t=c.name,u={isolateScope:null,bindToController:null}; +D(I.scope)&&(!0===I.bindToController?(u.bindToController=d(I.scope,t,!0),u.isolateScope={}):u.isolateScope=d(I.scope,t,!1));D(I.bindToController)&&(u.bindToController=d(I.bindToController,t,!0));if(u.bindToController&&!I.controller)throw $("noctrl",t);m=m.$$bindings=u;D(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function ca(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d"+b+"";return c.childNodes[0].childNodes;default:return b}}function qa(a,b){if("srcdoc"=== +b)return u.HTML;if("src"===b||"ngSrc"===b)return-1===["img","video","audio","source","track"].indexOf(a)?u.RESOURCE_URL:u.MEDIA_URL;if("xlinkHref"===b)return"image"===a?u.MEDIA_URL:"a"===a?u.URL:u.RESOURCE_URL;if("form"===a&&"action"===b||"base"===a&&"href"===b||"link"===a&&"href"===b)return u.RESOURCE_URL;if("a"===a&&("href"===b||"ngHref"===b))return u.URL}function ya(a,b){var c=b.toLowerCase();return v[a+"|"+c]||v["*|"+c]}function za(a){return ma(u.valueOf(a),"ng-prop-srcset")}function Ea(a,b,c, +d){if(m.test(d))throw $("nodomevents");a=ua(a);var e=ya(a,d),f=Ta;"srcset"!==d||"img"!==a&&"source"!==a?e&&(f=u.getTrusted.bind(u,e)):f=za;b.push({priority:100,compile:function(a,b){var e=p(b[c]),g=p(b[c],function(a){return u.valueOf(a)});return{pre:function(a,b){function c(){var g=e(a);b[0][d]=f(g)}c();a.$watch(g,c)}}}})}function Ia(a,c,d,e,f){var g=ua(a),k=qa(g,e),l=h[e]||f,p=b(d,!f,k,l);if(p){if("multiple"===e&&"select"===g)throw $("selmulti",Aa(a));if(m.test(e))throw $("nodomevents");c.push({priority:100, +compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=T());var g=f[e];g!==d&&(p=g&&b(g,!0,k,l),d=g);p&&(f[e]=p(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(p,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function oa(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Pa&&""===d.nodeValue.trim())&&Kg.call(a,b,1)}return a} +function Gg(a,b){if(b&&C(b))return b;if(C(a)){var d=wd.exec(a);if(d)return d[3]}}function Kf(){var a={};this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,d){Ja(b,"controller");D(b)?S(a,b):a[b]=d};this.$get=["$injector",function(b){function d(a,b,d,g){if(!a||!D(a.$scope))throw F("$controller")("noscp",g,b);a.$scope[b]=d}return function(c,e,f,g){var k,h,l;f=!0===f;g&&C(g)&&(l=g);if(C(c)){g=c.match(wd);if(!g)throw xd("ctrlfmt",c);h=g[1];l=l||g[3];c=a.hasOwnProperty(h)?a[h]:Le(e.$scope, +h,!0);if(!c)throw xd("ctrlreg",h);tb(c,h,!0)}if(f)return f=(H(c)?c[c.length-1]:c).prototype,k=Object.create(f||null),l&&d(e,l,k,h||c.name),S(function(){var a=b.invoke(c,k,e,h);a!==k&&(D(a)||B(a))&&(k=a,l&&d(e,l,k,h||c.name));return k},{instance:k,identifier:l});k=b.instantiate(c,e,h);l&&d(e,l,k,h||c.name);return k}}]}function Lf(){this.$get=["$window",function(a){return x(a.document)}]}function Mf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden; +a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function Nf(){this.$get=["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function vc(a){return D(a)?ha(a)?a.toISOString():eb(a):a}function Tf(){this.$get=function(){return function(a){if(!a)return"";var b=[];Qc(a,function(a,c){null===a||A(a)||B(a)||(H(a)?r(a,function(a){b.push(ba(c)+"="+ba(vc(a)))}):b.push(ba(c)+"="+ba(vc(a))))});return b.join("&")}}}function Uf(){this.$get= +function(){return function(a){function b(a,e,f){H(a)?r(a,function(a,c){b(a,e+"["+(D(a)?c:"")+"]")}):D(a)&&!ha(a)?Qc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):(B(a)&&(a=a()),d.push(ba(e)+"="+(null==a?"":ba(vc(a)))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function wc(a,b){if(C(a)){var d=a.replace(Lg,"").trim();if(d){var c=b("Content-Type"),c=c&&0===c.indexOf(yd),e;(e=c)||(e=(e=d.match(Mg))&&Ng[e[0]].test(d));if(e)try{a=Tc(d)}catch(f){if(!c)return a;throw Lb("baddata",a,f);}}}return a} +function zd(a){var b=T(),d;C(a)?r(a.split("\n"),function(a){d=a.indexOf(":");var e=K(V(a.substr(0,d)));a=V(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):D(a)&&r(a,function(a,d){var f=K(d),g=V(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function Ad(a){var b;return function(d){b||(b=zd(a));return d?(d=b[K(d)],void 0===d&&(d=null),d):b}}function Bd(a,b,d,c){if(B(c))return c(a,b,d);r(c,function(c){a=c(a,b,d)});return a}function Sf(){var a=this.defaults={transformResponse:[wc],transformRequest:[function(a){return D(a)&& +"[object File]"!==la.call(a)&&"[object Blob]"!==la.call(a)&&"[object FormData]"!==la.call(a)?eb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ja(xc),put:ja(xc),patch:ja(xc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return w(a)?(b=!!a,this):b};var d=this.interceptors=[],c=this.xsrfTrustedOrigins=[];Object.defineProperty(this,"xsrfWhitelistedOrigins", +{get:function(){return this.xsrfTrustedOrigins},set:function(a){this.xsrfTrustedOrigins=a}});this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(e,f,g,k,h,l,m,p){function n(b){function c(a,b){for(var d=0,e=b.length;da?b:l.reject(b)}if(!D(b))throw F("$http")("badreq",b);if(!C(p.valueOf(b.url)))throw F("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers=function(b){var c=a.headers,e=S({},b.headers),f,g,h,c=S({},c.common,c[K(b.method)]);a:for(f in c){g=K(f);for(h in e)if(K(h)===g)continue a;e[f]=c[f]}return d(e,ja(b))}(b);g.method= +vb(g.method);g.paramSerializer=C(g.paramSerializer)?m.get(g.paramSerializer):g.paramSerializer;e.$$incOutstandingRequestCount("$http");var h=[],k=[];b=l.resolve(g);r(v,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&k.push(a.response,a.responseError)});b=c(b,h);b=b.then(function(b){var c=b.headers,d=Bd(b.data,Ad(c),void 0,b.transformRequest);A(d)&&r(c,function(a,b){"content-type"===K(b)&&delete c[b]});A(b.withCredentials)&&!A(a.withCredentials)&& +(b.withCredentials=a.withCredentials);return s(b,d).then(f,f)});b=c(b,k);return b=b.finally(function(){e.$$completeOutstandingRequest(E,"$http")})}function s(c,d){function e(a){if(a){var c={};r(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function k(a,c,d,e,f){function g(){m(c,a,d,e,f)}R&&(200<=a&&300>a?R.put(O,[a,c,zd(d),e,f]):R.remove(O));b?h.$applyAsync(g):(g(),h.$$phase||h.$apply())}function m(a,b,d,e,f){b=-1<=b?b:0;(200<=b&&300> +b?L.resolve:L.reject)({data:a,status:b,headers:Ad(d),config:c,statusText:e,xhrStatus:f})}function s(a){m(a.data,a.status,ja(a.headers()),a.statusText,a.xhrStatus)}function v(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var L=l.defer(),u=L.promise,R,q,ma=c.headers,x="jsonp"===K(c.method),O=c.url;x?O=p.getTrustedResourceUrl(O):C(O)||(O=p.valueOf(O));O=G(O,c.paramSerializer(c.params));x&&(O=t(O,c.jsonpCallbackParam));n.pendingRequests.push(c);u.then(v,v);!c.cache&&!a.cache|| +!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(R=D(c.cache)?c.cache:D(a.cache)?a.cache:N);R&&(q=R.get(O),w(q)?q&&B(q.then)?q.then(s,s):H(q)?m(q[1],q[0],ja(q[2]),q[3],q[4]):m(q,200,{},"OK","complete"):R.put(O,u));A(q)&&((q=kc(c.url)?g()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(ma[c.xsrfHeaderName||a.xsrfHeaderName]=q),f(c.method,O,d,k,ma,c.timeout,c.withCredentials,c.responseType,e(c.eventHandlers),e(c.uploadEventHandlers)));return u}function G(a,b){0=h&&(t.resolve(s),f(r.$$intervalId));G||c.$apply()},k,t,G);return r}}}]}function Cd(a,b){var d=ga(a);b.$$protocol=d.protocol;b.$$host= +d.hostname;b.$$port=fa(d.port)||Rg[d.protocol]||null}function Dd(a,b,d){if(Sg.test(a))throw kb("badpath",a);var c="/"!==a.charAt(0);c&&(a="/"+a);a=ga(a);for(var c=(c&&"/"===a.pathname.charAt(0)?a.pathname.substring(1):a.pathname).split("/"),e=c.length;e--;)c[e]=decodeURIComponent(c[e]),d&&(c[e]=c[e].replace(/\//g,"%2F"));d=c.join("/");b.$$path=d;b.$$search=hc(a.search);b.$$hash=decodeURIComponent(a.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function yc(a,b){return a.slice(0, +b.length)===b}function ya(a,b){if(yc(b,a))return b.substr(a.length)}function Da(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function zc(a,b,d){this.$$html5=!0;d=d||"";Cd(a,this);this.$$parse=function(a){var d=ya(b,a);if(!C(d))throw kb("ipthprfx",a,b);Dd(d,this,!0);this.$$path||(this.$$path="/");this.$$compose()};this.$$normalizeUrl=function(a){return b+a.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;w(f=ya(a,c))?(g=f,g=d&&w(f=ya(d,f))? +b+(ya("/",f)||f):a+g):w(f=ya(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function Ac(a,b,d){Cd(a,this);this.$$parse=function(c){var e=ya(a,c)||ya(b,c),f;A(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",A(e)&&(a=c,this.replace())):(f=ya(d,e),A(f)&&(f=e));Dd(f,this,!1);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;yc(f,e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$normalizeUrl=function(b){return a+(b?d+b:"")};this.$$parseLinkUrl=function(b, +d){return Da(a)===Da(b)?(this.$$parse(b),!0):!1}}function Ed(a,b,d){this.$$html5=!0;Ac.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a===Da(c)?f=c:(g=ya(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$normalizeUrl=function(b){return a+d+b}}function Mb(a){return function(){return this[a]}}function Fd(a,b){return function(d){if(A(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Yf(){var a="!", +b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return w(b)?(a=b,this):a};this.html5Mode=function(a){if(Ga(a))return b.enabled=a,this;if(D(a)){Ga(a.enabled)&&(b.enabled=a.enabled);Ga(a.requireBase)&&(b.requireBase=a.requireBase);if(Ga(a.rewriteLinks)||C(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function k(a,b){return a===b||ga(a).href===ga(b).href}function h(a, +b,d){var e=m.url(),f=m.$$state;try{c.url(a,b,d),m.$$state=c.state()}catch(g){throw m.url(e),m.$$state=f,g;}}function l(a,b){d.$broadcast("$locationChangeSuccess",m.absUrl(),a,m.$$state,b)}var m,p;p=c.baseHref();var n=c.url(),s;if(b.enabled){if(!p&&b.requireBase)throw kb("nobase");s=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(p||"/");p=e.history?zc:Ed}else s=Da(n),p=Ac;var r=s.substr(0,Da(s).lastIndexOf("/")+1);m=new p(s,r,"#"+a);m.$$parseLinkUrl(n,n);m.$$state=c.state();var t=/^\s*(javascript|mailto):/i; +f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!==a.which&&2!==a.button){for(var g=x(a.target);"a"!==ua(g[0]);)if(g[0]===f[0]||!(g=g.parent())[0])return;if(!C(e)||!A(g.attr(e))){var e=g.prop("href"),h=g.attr("href")||g.attr("xlink:href");D(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=ga(e.animVal).href);t.test(e)||!e||g.attr("target")||a.isDefaultPrevented()||!m.$$parseLinkUrl(e,h)||(a.preventDefault(),m.absUrl()!==c.url()&&d.$apply())}}});m.absUrl()!== +n&&c.url(m.absUrl(),!0);var N=!0;c.onUrlChange(function(a,b){yc(a,r)?(d.$evalAsync(function(){var c=m.absUrl(),e=m.$$state,f;m.$$parse(a);m.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;m.absUrl()===a&&(f?(m.$$parse(c),m.$$state=e,h(c,!1,e)):(N=!1,l(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(N||m.$$urlUpdatedByLocation){m.$$urlUpdatedByLocation=!1;var a=c.url(),b=m.absUrl(),f=c.state(),g=m.$$replace,n=!k(a,b)||m.$$html5&&e.history&&f!== +m.$$state;if(N||n)N=!1,d.$evalAsync(function(){var b=m.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,m.$$state,f).defaultPrevented;m.absUrl()===b&&(c?(m.$$parse(a),m.$$state=f):(n&&h(b,g,f===m.$$state?null:m.$$state),l(a,f)))})}m.$$replace=!1});return m}]}function Zf(){var a=!0,b=this;this.debugEnabled=function(b){return w(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){dc(a)&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&& +(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||E;return function(){var a=[];r(arguments,function(b){a.push(c(b))});return Function.prototype.apply.call(e,b,a)}}var f=wa||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Tg(a){return a+""}function Ug(a,b){return"undefined"!==typeof a?a: +b}function Gd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function Vg(a,b){switch(a.type){case q.MemberExpression:if(a.computed)return!1;break;case q.UnaryExpression:return 1;case q.BinaryExpression:return"+"!==a.operator?1:!1;case q.CallExpression:return!1}return void 0===b?Hd:b}function Z(a,b,d){var c,e,f=a.isPure=Vg(a,d);switch(a.type){case q.Program:c=!0;r(a.body,function(a){Z(a.expression,b,f);c=c&&a.expression.constant});a.constant=c;break;case q.Literal:a.constant=!0;a.toWatch= +[];break;case q.UnaryExpression:Z(a.argument,b,f);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case q.BinaryExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case q.LogicalExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case q.ConditionalExpression:Z(a.test,b,f);Z(a.alternate,b,f);Z(a.consequent,b,f);a.constant=a.test.constant&& +a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case q.Identifier:a.constant=!1;a.toWatch=[a];break;case q.MemberExpression:Z(a.object,b,f);a.computed&&Z(a.property,b,f);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=a.constant?[]:[a];break;case q.CallExpression:c=d=a.filter?!b(a.callee.name).$stateful:!1;e=[];r(a.arguments,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=d?e:[a];break;case q.AssignmentExpression:Z(a.left, +b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case q.ArrayExpression:c=!0;e=[];r(a.elements,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=e;break;case q.ObjectExpression:c=!0;e=[];r(a.properties,function(a){Z(a.value,b,f);c=c&&a.value.constant;e.push.apply(e,a.value.toWatch);a.computed&&(Z(a.key,b,!1),c=c&&a.key.constant,e.push.apply(e,a.key.toWatch))});a.constant=c;a.toWatch=e;break;case q.ThisExpression:a.constant= +!1;a.toWatch=[];break;case q.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Id(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Jd(a){return a.type===q.Identifier||a.type===q.MemberExpression}function Kd(a){if(1===a.body.length&&Jd(a.body[0].expression))return{type:q.AssignmentExpression,left:a.body[0].expression,right:{type:q.NGValueParameter},operator:"="}}function Ld(a){this.$filter=a}function Md(a){this.$filter=a}function Nb(a,b,d){this.ast= +new q(a,d);this.astCompiler=d.csp?new Md(b):new Ld(b)}function Bc(a){return B(a.valueOf)?a.valueOf():Wg.call(a)}function $f(){var a=T(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(b,c){var d,f;switch(typeof b){case "string":return f=b=b.trim(),d=a[f],d||(d=new Ob(G),d=(new Nb(d,e,G)).parse(b),a[f]=p(d)),s(d,c);case "function":return s(b,c);default:return s(E, +c)}}function g(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=Bc(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function k(a,b,c,d,e){var f=d.inputs,h;if(1===f.length){var k=g,f=f[0];return a.$watch(function(a){var b=f(a);g(b,k,f.isPure)||(h=d(a,void 0,void 0,[b]),k=b&&Bc(b));return h},b,c,e)}for(var l=[],m=[],n=0,p=f.length;n=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;fa)for(b in l++, +f)ta.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$$pure=g(a).literal;c.$stateful=!c.$$pure;var d=this,e,f,h,k=1r&&(A=4-r,N[A]||(N[A]=[]),N[A].push({msg:B(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:h}));else if(a===c){s= +!1;break a}}catch(E){f(E)}if(!(n=!q.$$suspended&&q.$$watchersCount&&q.$$childHead||q!==y&&q.$$nextSibling))for(;q!==y&&!(n=q.$$nextSibling);)q=q.$parent}while(q=n);if((s||w.length)&&!r--)throw v.$$phase=null,d("infdig",b,N);}while(s||w.length);for(v.$$phase=null;Jwa)throw Ea("iequirks");var c=ja(W);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ta);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;r(W, +function(a,b){var d=K(b);c[("parse_as_"+d).replace(Dc,xb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(Dc,xb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(Dc,xb)]=function(b){return g(a,b)}});return c}]}function fg(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=fa((/android (\d+)/.exec(K((a.navigator||{}).userAgent))|| +[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},k=g.body&&g.body.style,h=!1,l=!1;k&&(h=!!("transition"in k||"webkitTransition"in k),l=!!("animation"in k||"webkitAnimation"in k));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&wa)return!1;if(A(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ba(),transitions:h,animations:l,android:e}}]}function gg(){this.$get=ia(function(a){return new Yg(a)})}function Yg(a){function b(){var a=e.pop();return a&& +a.cb}function d(a){for(var b=e.length-1;0<=b;--b){var c=e[b];if(c.type===a)return e.splice(b,1),c.cb}}var c={},e=[],f=this.ALL_TASKS_TYPE="$$all$$",g=this.DEFAULT_TASK_TYPE="$$default$$";this.completeTask=function(e,h){h=h||g;try{e()}finally{var l;l=h||g;c[l]&&(c[l]--,c[f]--);l=c[h];var m=c[f];if(!m||!l)for(l=m?d:b;m=l(h);)try{m()}catch(p){a.error(p)}}};this.incTaskCount=function(a){a=a||g;c[a]=(c[a]||0)+1;c[f]=(c[f]||0)+1};this.notifyWhenNoPendingTasks=function(a,b){b=b||f;c[b]?e.push({type:b,cb:a}): +a()}}function ig(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler","$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(k,h){g.totalPendingRequests++;if(!C(k)||A(d.get(k)))k=f.getTrustedResourceUrl(k);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==wc}):l===wc&&(l=null);return c.get(k,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){return d.put(k,a.data)}, +function(a){h||(a=Zg("tpload",k,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=0;return g}]}function jg(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var c=ca.element(a).data("$binding");c&&r(c,function(c){d?(new RegExp("(^|\\s)"+Od(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-", +"data-ng-","ng\\:"],k=0;kc&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===Fc;e++);if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===Fc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Yd&&(d=d.splice(0,Yd-1),b=c-1,c=1);return{d:d,e:b,i:c}}function ih(a, +b,d,c){var e=a.d,f=e.length-a.i;b=A(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;fk;)h.unshift(0),k++;0=b.lgSize&&k.unshift(h.splice(-b.lgSize,h.length).join(""));h.length>b.gSize;)k.unshift(h.splice(-b.gSize,h.length).join(""));h.length&&k.unshift(h.join(""));h=k.join(d);f.length&&(h+=c+f.join(""));e&&(h+="e+"+e)}return 0>a&&!g?b.negPre+h+b.negSuf:b.posPre+ +h+b.posSuf}function Pb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length-d)f+=d;0===f&&-12===d&&(f=12);return Pb(f,b,c,e)}}function lb(a,b,d){return function(c,e){var f=c["get"+a](),g=vb((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Zd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function $d(a){return function(b){var d= +Zd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Pb(b,a)}}function Gc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Td(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,k=b[8]?a.setUTCFullYear:a.setFullYear,h=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=fa(b[9]+b[10]),g=fa(b[9]+b[11]));k.call(a,fa(b[1]),fa(b[2])-1,fa(b[3]));f=fa(b[4]||0)-f;g=fa(b[5]||0)-g;k=fa(b[6]||0);b=Math.round(1E3*parseFloat("0."+ +(b[7]||0)));h.call(a,f,g,k,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",k=[],h,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;C(c)&&(c=jh.test(c)?fa(c):b(c));X(c)&&(c=new Date(c));if(!ha(c)||!isFinite(c.getTime()))return c;for(;d;)(l=kh.exec(d))?(k=db(k,l,1),d=k.pop()):(k.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=fc(f,m),c=gc(c,f,!0));r(k,function(b){h=lh[b];g+=h?h(c,a.DATETIME_FORMATS, +m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ch(){return function(a,b){A(b)&&(b=2);return eb(a,b)}}function dh(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):fa(b);if(Y(b))return a;X(a)&&(a=a.toString());if(!za(a))return a;d=!d||isNaN(d)?0:fa(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?Hc(a,d,d+b):0===d?Hc(a,b,a.length):Hc(a,Math.max(0,d+b),d)}}function Hc(a,b,d){return C(a)?a.slice(b,d):Ha.call(a,b,d)}function Vd(a){function b(b){return b.map(function(b){var c= +1,d=Ta;if(B(b))d=b;else if(C(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function c(a,b){var c=0,d=a.type,h=b.type;if(d===h){var h=a.value,l=b.value;"string"===d?(h=h.toLowerCase(),l=l.toLowerCase()):"object"===d&&(D(h)&&(h=a.index),D(l)&&(l=b.index));h!==l&&(c= +hb||37<=b&&40>=b|| +m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut drop",m)}b.on("change",l);if(ee[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!h){var b=this.validity,c=b.badInput,d=b.typeMismatch;h=f.defer(function(){h=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Rb(a,b){return function(d,c){var e,f;if(ha(d))return d;if(C(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length- +1)&&(d=d.substring(1,d.length-1));if(mh.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},r(e,function(a,c){cf.yyyy&&e.setFullYear(f.yyyy),e}return NaN}}function ob(a,b,d,c){return function(e,f,g,k,h,l,m, +p){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function s(a){return w(a)&&!ha(a)?r(a)||void 0:a}function r(a,b){var c=k.$options.getOption("timezone");v&&v!==c&&(b=Uc(b,fc(v)));var e=d(a,b);!isNaN(e)&&c&&(e=gc(e,c));return e}Jc(e,f,g,k,a);Sa(e,f,g,k,h,l);var t="time"===a||"datetimelocal"===a,q,v;k.$parsers.push(function(c){if(k.$isEmpty(c))return null;if(b.test(c))return r(c,q);k.$$parserName=a});k.$formatters.push(function(a){if(a&&!ha(a))throw pb("datefmt",a);if(n(a)){q=a;var b= +k.$options.getOption("timezone");b&&(v=b,q=gc(q,b,!0));var d=c;t&&C(k.$options.getOption("timeSecondsFormat"))&&(d=c.replace("ss.sss",k.$options.getOption("timeSecondsFormat")).replace(/:$/,""));a=m("date")(a,d,b);t&&k.$options.getOption("timeStripZeroSeconds")&&(a=a.replace(/(?::00)?(?:\.000)?$/,""));return a}v=q=null;return""});if(w(g.min)||g.ngMin){var x=g.min||p(g.ngMin)(e),z=s(x);k.$validators.min=function(a){return!n(a)||A(z)||d(a)>=z};g.$observe("min",function(a){a!==x&&(z=s(a),x=a,k.$validate())})}if(w(g.max)|| +g.ngMax){var y=g.max||p(g.ngMax)(e),J=s(y);k.$validators.max=function(a){return!n(a)||A(J)||d(a)<=J};g.$observe("max",function(a){a!==y&&(J=s(a),y=a,k.$validate())})}}}function Jc(a,b,d,c,e){(c.$$hasNativeValidators=D(b[0].validity))&&c.$parsers.push(function(a){var d=b.prop("validity")||{};if(d.badInput||d.typeMismatch)c.$$parserName=e;else return a})}function fe(a){a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(nh.test(b))return parseFloat(b);a.$$parserName="number"});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!X(b))throw pb("numfmt", +b);b=b.toString()}return b})}function na(a){w(a)&&!X(a)&&(a=parseFloat(a));return Y(a)?void 0:a}function Kc(a){var b=a.toString(),d=b.indexOf(".");return-1===d?-1a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ge(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Kc(a):0,k=e?Kc(b):0,h=f?Kc(d):0,g=Math.max(g,k,h),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function he(a,b,d,c,e){if(w(c)){a= +a(c);if(!a.constant)throw pb("constexpr",d,c);return a(b)}return e}function Lc(a,b){function d(a,b){if(!a||!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d(?:<\/\1>|)$/,nc=/<|&#?\w+;/,rg=/<([\w:-]+)/,sg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,qa={thead:["table"],col:["colgroup","table"],tr:["tbody","table"],td:["tr", +"tbody","table"]};qa.tbody=qa.tfoot=qa.colgroup=qa.caption=qa.thead;qa.th=qa.td;var hb={option:[1,'"],_default:[0,"",""]},Nc;for(Nc in qa){var le=qa[Nc],me=le.slice().reverse();hb[Nc]=[me.length,"<"+me.join("><")+">",""]}hb.optgroup=hb.option;var zg=z.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Wa=U.prototype={ready:hd,toString:function(){var a=[];r(this,function(b){a.push(""+b)});return"["+a.join(", ")+ +"]"},eq:function(a){return 0<=a?x(this[a]):x(this[this.length+a])},length:0,push:ph,sort:[].sort,splice:[].splice},Hb={};r("multiple selected checked disabled readOnly required open".split(" "),function(a){Hb[K(a)]=a});var od={};r("input select option textarea button form details".split(" "),function(a){od[a]=!0});var vd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern",ngStep:"step"};r({data:sc,removeData:rc,hasData:function(a){for(var b in Ka[a.ng339])return!0; +return!1},cleanData:function(a){for(var b=0,d=a.length;b/,Cg=/^[^(]*\(\s*([^)]*)\)/m,sh=/,/,th=/^\s*(_?)(\S+?)\1\s*$/,Ag=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ca=F("$injector");fb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw C(d)&&d||(d=a.name||Dg(a)),Ca("strictdi",d);b=qd(a);r(b[1].split(sh),function(a){a.replace(th,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,tb(a[b],"fn"),c=a.slice(0,b)):tb(a,"fn",!0);return c};var ne=F("$animate"), +Ef=function(){this.$get=E},Ff=function(){var a=new Ib,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=C(b)?b.split(" "):H(b)?b:[],r(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){r(b,function(b){var c=a.get(b);if(c){var d=Eg(b.attr("class")),e="",f="";r(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});r(b,function(a){e&&Eb(a,e);f&&Db(a,f)});a.delete(b)}});b.length=0}return{enabled:E,on:E,off:E,pin:E,push:function(g, +k,h,l){l&&l();h=h||{};h.from&&g.css(h.from);h.to&&g.css(h.to);if(h.addClass||h.removeClass)if(k=h.addClass,l=h.removeClass,h=a.get(g)||{},k=e(h,k,!0),l=e(h,l,!1),k||l)a.set(g,h),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},Cf=["$provide",function(a){var b=this,d=null,c=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw ne("notcsel",c);var g=c+"-animation";b.$$registeredAnimations[c.substr(1)]=g;a.factory(g, +d)};this.customFilter=function(a){1===arguments.length&&(c=B(a)?a:null);return c};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,ne("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e <= >= && || ! = |".split(" "),function(a){Vb[a]= +!0});var wh={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Ob=function(a){this.options=a};Ob.prototype={constructor:Ob,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index=a&&"string"=== +typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)}, +isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b= +w(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ya("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index","<=",">=");)a={type:q.BinaryExpression,operator:b.text, +left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:q.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()}, +primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=Ia(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:q.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression", +this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:q.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:q.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:q.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:q.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression()); +return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:q.Identifier,name:a.text}},constant:function(){return{type:q.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]"); +return{type:q.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:q.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"), +b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");return{type:q.ObjectExpression,properties:a}},throwError:function(a,b){throw Ya("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ya("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ya("ueoe", +this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:q.ThisExpression},$locals:{type:q.LocalsExpression}}};var Hd=2;Ld.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[], +body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};Z(a,b.$filter);var d="",c;this.stage="assign";if(c=Kd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Id(a.body);b.stage="inputs";r(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var k=b.nextId();b.recurse(a,k);b.return_(k);b.state.inputs.push({name:d,isPure:a.isPure});a.watchId=c});this.state.computing="fn";this.stage= +"main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,Tg,Ug,Gd);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;r(b,function(b){a.push("var "+b.name+"="+d.generateFunction(b.name,"s"));b.isPure&&a.push(b.name,".isPure="+JSON.stringify(b.isPure)+ +";")});b.length&&a.push("fn.inputs=["+b.map(function(a){return a.name}).join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;r(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")}, +recurse:function(a,b,d,c,e,f){var g,k,h=this,l,m,p;c=c||E;if(!f&&w(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case q.Program:r(a.body,function(b,c){h.recurse(b.expression,void 0,void 0,function(a){k=a});c!==a.body.length-1?h.current().body.push(k,";"):h.return_(k)});break;case q.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case q.UnaryExpression:this.recurse(a.argument,void 0, +void 0,function(a){k=a});m=a.operator+"("+this.ifDefined(k,0)+")";this.assign(b,m);c(m);break;case q.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){k=a});m="+"===a.operator?this.plus(g,k):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(k,0):"("+g+")"+a.operator+"("+k+")";this.assign(b,m);c(m);break;case q.LogicalExpression:b=b||this.nextId();h.recurse(a.left,b);h.if_("&&"===a.operator?b:h.not(b),h.lazyRecurse(a.right, +b));c(b);break;case q.ConditionalExpression:b=b||this.nextId();h.recurse(a.test,b);h.if_(b,h.lazyRecurse(a.alternate,b),h.lazyRecurse(a.consequent,b));c(b);break;case q.Identifier:b=b||this.nextId();d&&(d.context="inputs"===h.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);h.if_("inputs"===h.stage||h.not(h.getHasOwnProperty("l",a.name)),function(){h.if_("inputs"===h.stage||"s",function(){e&&1!==e&&h.if_(h.isNull(h.nonComputedMember("s",a.name)), +h.lazyAssign(h.nonComputedMember("s",a.name),"{}"));h.assign(b,h.nonComputedMember("s",a.name))})},b&&h.lazyAssign(b,h.nonComputedMember("l",a.name)));c(b);break;case q.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();h.recurse(a.object,g,void 0,function(){h.if_(h.notNull(g),function(){a.computed?(k=h.nextId(),h.recurse(a.property,k),h.getStringValue(k),e&&1!==e&&h.if_(h.not(h.computedMember(g,k)),h.lazyAssign(h.computedMember(g,k),"{}")),m=h.computedMember(g,k),h.assign(b, +m),d&&(d.computed=!0,d.name=k)):(e&&1!==e&&h.if_(h.isNull(h.nonComputedMember(g,a.property.name)),h.lazyAssign(h.nonComputedMember(g,a.property.name),"{}")),m=h.nonComputedMember(g,a.property.name),h.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){h.assign(b,"undefined")});c(b)},!!e);break;case q.CallExpression:b=b||this.nextId();a.filter?(k=h.filter(a.callee.name),l=[],r(a.arguments,function(a){var b=h.nextId();h.recurse(a,b);l.push(b)}),m=k+"("+l.join(",")+")",h.assign(b,m),c(b)): +(k=h.nextId(),g={},l=[],h.recurse(a.callee,k,g,function(){h.if_(h.notNull(k),function(){r(a.arguments,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m=g.name?h.member(g.context,g.name,g.computed)+"("+l.join(",")+")":k+"("+l.join(",")+")";h.assign(b,m)},function(){h.assign(b,"undefined")});c(b)}));break;case q.AssignmentExpression:k=this.nextId();g={};this.recurse(a.left,void 0,g,function(){h.if_(h.notNull(g.context),function(){h.recurse(a.right,k);m=h.member(g.context, +g.name,g.computed)+a.operator+k;h.assign(b,m);c(b||m)})},1);break;case q.ArrayExpression:l=[];r(a.elements,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case q.ObjectExpression:l=[];p=!1;r(a.properties,function(a){a.computed&&(p=!0)});p?(b=b||this.nextId(),this.assign(b,"{}"),r(a.properties,function(a){a.computed?(g=h.nextId(),h.recurse(a.key,g)):g=a.key.type===q.Identifier?a.key.name:""+a.key.value;k=h.nextId(); +h.recurse(a.value,k);h.assign(h.member(b,g,a.computed),k)})):(r(a.properties,function(b){h.recurse(b.value,a.constant?void 0:h.nextId(),void 0,function(a){l.push(h.escape(b.key.type===q.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case q.ThisExpression:this.assign(b,"s");c(b||"s");break;case q.LocalsExpression:this.assign(b,"l");c(b||"l");break;case q.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+ +b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a, +b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a, +b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(C(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(X(a))return a.toString();if(!0===a)return"true";if(!1=== +a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ya("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Md.prototype={compile:function(a){var b=this;Z(a,b.$filter);var d,c;if(d=Kd(a))c=this.recurse(d);d=Id(a.body);var e;d&&(e=[],r(d,function(a,c){var d=b.recurse(a);d.isPure=a.isPure;a.input=d;e.push(d);a.watchId=c}));var f=[];r(a.body, +function(a){f.push(b.recurse(a.expression))});a=0===a.body.length?E:1===a.body.length?f[0]:function(a,b){var c;r(f,function(d){c=d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case q.Literal:return this.value(a.value,b);case q.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case q.BinaryExpression:return c=this.recurse(a.left), +e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case q.Identifier:return f.identifier(a.name,b,d);case q.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c, +e,b,d):this.nonComputedMember(c,e,b,d);case q.CallExpression:return g=[],r(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var p=[],n=0;n":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c= +a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k)?b(e,f,g,k):d(e,f,g,k);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c= +e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]={});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,k){var h=a(e,f,g,k),l,m;null!=h&&(l=b(e,f,g,k),l+="",c&&1!==c&&h&&!h[l]&&(h[l]={}),m=h[l]);return d?{context:h,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d, +c,e,f){return f?f[b]:a(d,c,e)}}};Nb.prototype={constructor:Nb,parse:function(a){a=this.getAst(a);var b=this.astCompiler.compile(a.ast),d=a.ast;b.literal=0===d.body.length||1===d.body.length&&(d.body[0].expression.type===q.Literal||d.body[0].expression.type===q.ArrayExpression||d.body[0].expression.type===q.ObjectExpression);b.constant=a.ast.constant;b.oneTime=a.oneTime;return b},getAst:function(a){var b=!1;a=a.trim();":"===a.charAt(0)&&":"===a.charAt(1)&&(b=!0,a=a.substring(2));return{ast:this.ast.ast(a), +oneTime:b}}};var Ea=F("$sce"),W={HTML:"html",CSS:"css",MEDIA_URL:"mediaUrl",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},Dc=/_([a-z])/g,Zg=F("$templateRequest"),$g=F("$timeout"),aa=z.document.createElement("a"),Qd=ga(z.location.href),Na;aa.href="http://[::1]";var ah="[::1]"===aa.hostname;Rd.$inject=["$document"];fd.$inject=["$provide"];var Yd=22,Xd=".",Fc="0";Sd.$inject=["$locale"];Ud.$inject=["$locale"];var lh={yyyy:ea("FullYear",4,0,!1,!0),yy:ea("FullYear",2,0,!0,!0),y:ea("FullYear",1,0,!1,!0), +MMMM:lb("Month"),MMM:lb("Month",!0),MM:ea("Month",2,1),M:ea("Month",1,1),LLLL:lb("Month",!1,!0),dd:ea("Date",2),d:ea("Date",1),HH:ea("Hours",2),H:ea("Hours",1),hh:ea("Hours",2,-12),h:ea("Hours",1,-12),mm:ea("Minutes",2),m:ea("Minutes",1),ss:ea("Seconds",2),s:ea("Seconds",1),sss:ea("Milliseconds",3),EEEE:lb("Day"),EEE:lb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Pb(Math[0=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},kh=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,jh=/^-?\d+$/;Td.$inject=["$locale"];var eh=ia(K),fh=ia(vb);Vd.$inject=["$parse"];var Re=ia({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===la.call(b.prop("href"))?"xlink:href":"href"; +b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),wb={};r(Hb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=xa("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});wb[c]=function(){return{restrict:"A",priority:100,link:e}}}});r(vd,function(a,b){wb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(ke))){e.$set("ngPattern",new RegExp(c[1], +c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});r(["src","srcset","href"],function(a){var b=xa("ng-"+a);wb[b]=["$sce",function(d){return{priority:99,link:function(c,e,f){var g=a,k=a;"href"===a&&"[object SVGAnimatedString]"===la.call(e.prop("href"))&&(k="xlinkHref",f.$attr[k]="xlink:href",g=null);f.$set(b,d.getTrustedMediaUrl(f[b]));f.$observe(b,function(b){b?(f.$set(k,b),wa&&g&&e.prop(g,f[k])):"href"===a&&f.$set(k,null)})}}}]});var mb={$addControl:E,$getControls:ia([]),$$renameControl:function(a, +b){a.$name=b},$removeControl:E,$setValidity:E,$setDirty:E,$setPristine:E,$setSubmitted:E,$$setSubmitted:E};Qb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Qb.prototype={$rollbackViewValue:function(){r(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){r(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ja(a.$name,"input");this.$$controls.push(a);a.$name&&(this[a.$name]=a);a.$$parentForm=this},$getControls:function(){return ja(this.$$controls)}, +$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];r(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);r(this.$error,function(b,d){this.$setValidity(d,null,a)},this);r(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);cb(this.$$controls,a);a.$$parentForm=mb},$setDirty:function(){this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element, +Wb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Za,Wb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;r(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){r(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){for(var a=this;a.$$parentForm&&a.$$parentForm!==mb;)a=a.$$parentForm;a.$$setSubmitted()},$$setSubmitted:function(){this.$$animate.addClass(this.$$element, +"ng-submitted");this.$submitted=!0;r(this.$$controls,function(a){a.$$setSubmitted&&a.$$setSubmitted()})}};ce({clazz:Qb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&(cb(c,d),0===c.length&&delete a[b])}});var oe=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||E}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Qb,compile:function(d,f){d.addClass(Za).addClass(nb); +var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var p=f[0];if(!("action"in e)){var n=function(b){a.$apply(function(){p.$commitViewValue();p.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",n);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",n)},0,!1)})}(f[1]||p.$$parentForm).$addControl(p);var s=g?c(p.$name):E;g&&(s(a,p),e.$observe(g,function(b){p.$name!==b&&(s(a,void 0),p.$$parentForm.$$renameControl(p,b),s=c(p.$name),s(a,p))})); +d.on("$destroy",function(){p.$$parentForm.$removeControl(p);s(a,void 0);S(p,mb)})}}}}}]},Se=oe(),df=oe(!0),mh=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,xh=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,yh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/, +nh=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,pe=/^(\d{4,})-(\d{2})-(\d{2})$/,qe=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Oc=/^(\d{4,})-W(\d\d)$/,re=/^(\d{4,})-(\d\d)$/,se=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ee=T();r(["date","datetime-local","month","time","week"],function(a){ee[a]=!0});var te={text:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c)},date:ob("date",pe,Rb(pe,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ob("datetimelocal",qe,Rb(qe,"yyyy MM dd HH mm ss sss".split(" ")), +"yyyy-MM-ddTHH:mm:ss.sss"),time:ob("time",se,Rb(se,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:ob("week",Oc,function(a,b){if(ha(a))return a;if(C(a)){Oc.lastIndex=0;var d=Oc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,k=0,h=Zd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),k=b.getMilliseconds());return new Date(c,0,h.getDate()+e,d,f,g,k)}}return NaN},"yyyy-Www"),month:ob("month",re,Rb(re,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f,g,k){Jc(a,b,d,c,"number");fe(c);Sa(a, +b,d,c,e,f);var h;if(w(d.min)||d.ngMin){var l=d.min||k(d.ngMin)(a);h=na(l);c.$validators.min=function(a,b){return c.$isEmpty(b)||A(h)||b>=h};d.$observe("min",function(a){a!==l&&(h=na(a),l=a,c.$validate())})}if(w(d.max)||d.ngMax){var m=d.max||k(d.ngMax)(a),p=na(m);c.$validators.max=function(a,b){return c.$isEmpty(b)||A(p)||b<=p};d.$observe("max",function(a){a!==m&&(p=na(a),m=a,c.$validate())})}if(w(d.step)||d.ngStep){var n=d.step||k(d.ngStep)(a),s=na(n);c.$validators.step=function(a,b){return c.$isEmpty(b)|| +A(s)||ge(b,h||0,s)};d.$observe("step",function(a){a!==n&&(s=na(a),n=a,c.$validate())})}},url:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||xh.test(d)}},email:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||yh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==V(d.ngTrim);A(d.name)&&b.attr("name",++qb);b.on("change",function(a){var g;b[0].checked&&(g=d.value,e&&(g= +V(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=V(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);var e=d[a];d.$observe(a,function(a){a!==e&&(e=a,c(a))})}function k(a){p=na(a);Y(c.$modelValue)||(m?(a=b.val(),p>a&&(a=p,b.val(a)),c.$setViewValue(a)):c.$validate())}function h(a){n=na(a);Y(c.$modelValue)||(m?(a=b.val(),n=p},g("min",k)); +e&&(n=na(d.max),c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||A(n)||b<=n},g("max",h));f&&(s=na(d.step),c.$validators.step=m?function(){return!r.stepMismatch}:function(a,b){return c.$isEmpty(b)||A(s)||ge(b,p||0,s)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,k){var h=he(k,a,"ngTrueValue",d.ngTrueValue,!0),l=he(k,a,"ngFalseValue",d.ngFalseValue,!1);b.on("change",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty= +function(a){return!1===a};c.$formatters.push(function(a){return va(a,h)});c.$parsers.push(function(a){return a?h:l})},hidden:E,button:E,submit:E,reset:E,file:E},$c=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,k){k[0]&&(te[K(g.type)]||te.text)(e,f,g,k[0],b,a,d,c)}}}}],Af=function(){var a={configurable:!0,enumerable:!1,get:function(){return this.getAttribute("value")||""},set:function(a){this.setAttribute("value",a)}}; +return{restrict:"E",priority:200,compile:function(b,d){if("hidden"===K(d.type))return{pre:function(b,d,f,g){b=d[0];b.parentNode&&b.parentNode.insertBefore(b,b.nextSibling);Object.defineProperty&&Object.defineProperty(b,"value",a)}}}}},zh=/^(true|false|\d+)$/,xf=function(){function a(a,d,c){var e=w(c)?c:9===wa?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return zh.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue, +function(b){a(d,f,b)})}}}},We=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=jc(a)})}}}}],Ye=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=A(a)?"":a})}}}}], +Xe=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],wf=ia({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ze=Lc("",!0),af=Lc("Odd",0),$e=Lc("Even",1),bf=Ra({compile:function(a, +b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),cf=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],ed={},Ah={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=xa("ng-"+a);ed[b]=["$parse","$rootScope","$exceptionHandler",function(d,c,e){return sd(d,c,e,b,a,Ah[a])}]});var ff=["$animate","$compile",function(a,b){return{multiElement:!0, +transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var k,h,l;d.$watch(e.ngIf,function(d){d?h||g(function(d,f){h=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);k={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),h&&(h.$destroy(),h=null),k&&(l=ub(k.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),k=null))})}}}],gf=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element", +controller:ca.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",k=e.autoscroll;return function(c,e,m,p,n){var r=0,q,t,x,v=function(){t&&(t.remove(),t=null);q&&(q.$destroy(),q=null);x&&(d.leave(x).done(function(a){!1!==a&&(t=null)}),t=x,x=null)};c.$watch(f,function(f){var m=function(a){!1===a||!w(k)||k&&!c.$eval(k)||b()},t=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===r){var b=c.$new();p.template=a;a=n(b,function(a){v();d.enter(a,null,e).done(m)});q=b;x=a;q.$emit("$includeContentLoaded", +f);c.$eval(g)}},function(){c.$$destroyed||t!==r||(v(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(v(),p.template=null)})}}}}],zf=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){la.call(d[0]).match(/SVG/)?(d.empty(),a(gd(e.template,z.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],hf=Ra({priority:450,compile:function(){return{pre:function(a, +b,d){a.$eval(d.ngInit)}}}}),vf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?V(e):e;c.$parsers.push(function(a){if(!A(a)){var b=[];a&&r(a.split(g),function(a){a&&b.push(f?V(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},nb="ng-valid",be="ng-invalid",Za="ng-pristine",Wb="ng-dirty",pb=F("ngModel");Sb.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" "); +Sb.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);B(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){B(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw pb("nonassign",this.$$attr.ngModel,Aa(this.$$element));},$render:E,$isEmpty:function(a){return A(a)|| +""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Wb);this.$$animate.addClass(this.$$element,Za)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element, +Za);this.$$animate.addClass(this.$$element,Wb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!Y(this.$modelValue)){var a= +this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;r(h.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(r(h.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;r(h.$asyncValidators,function(e, +g){var h=e(a,b);if(!h||!B(h.then))throw pb("nopromise",h);f(g,void 0);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?h.$$q.all(c).then(function(){g(d)},E):g(!0)}function f(a,b){k===h.$$currentValidationRunId&&h.$setValidity(a,b)}function g(a){k===h.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var k=this.$$currentValidationRunId,h=this;(function(){var a=h.$$parserName;if(A(h.$$parserValid))f(a,null);else return h.$$parserValid||(r(h.$validators,function(a, +b){f(b,null)}),r(h.$asyncValidators,function(a,b){f(b,null)})),f(a,h.$$parserValid),h.$$parserValid;return!0})()?c()?e():g(!1):g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;this.$$parserValid= +A(a)?void 0:!0;this.$setValidity(this.$$parserName,null);this.$$parserName="parse";if(this.$$parserValid)for(var d=0;dg||e.$isEmpty(b)||b.length<=g}}}}}],cd= +["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.minlength||a(c.ngMinlength)(b),g=Ub(f)||-1;c.$observe("minlength",function(a){f!==a&&(g=Ub(a)||-1,f=a,e.$validate())});e.$validators.minlength=function(a,b){return e.$isEmpty(b)||b.length>=g}}}}}];z.angular.bootstrap?z.console&&console.log("WARNING: Tried to load AngularJS more than once."):(Je(),Oe(ca),ca.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1== +b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "), +WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a, +c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),x(function(){Ee(z.document,Wc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(window.angular.element("