├── .github ├── ISSUE_TEMPLATE │ └── bug-report-----.md └── workflows │ └── build.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── NeteaseAPI2_Dockerfile ├── README.md ├── README_zh.md ├── config-simple.json ├── data ├── .preserve └── mongo-init.js ├── dataModule.js ├── dataModule ├── cryptoUtils.js ├── dsm.js ├── lyricUtils.js ├── netease2.js ├── poka.js ├── qqmusic.js └── youtube.js ├── db ├── db.js ├── log.js ├── lyric.js ├── pin.js ├── playlist.js ├── record.js └── user.js ├── docker-compose.yml ├── index.js ├── install └── index.html ├── log.js ├── package-lock.json ├── package.json ├── pokaplayer.js ├── public ├── assets │ ├── 404-9b37a669.js │ ├── Album-7e10ba23.css │ ├── Album-d71314ff.js │ ├── Albums-51ec468d.js │ ├── Artists-de6da841.js │ ├── Folders-941ca665.js │ ├── Index-77de2a6a.js │ ├── Library-6e5b1b31.js │ ├── Log-6cc3aa27.js │ ├── Log-c9db7119.css │ ├── Login-0c932826.js │ ├── Login-65f28d2c.css │ ├── Pins-0ce5b192.js │ ├── Playlists-35b3be64.js │ ├── Quality-316f457e.js │ ├── Search-1a5f5fd1.js │ ├── Search-f3c2d4ea.css │ ├── System-38da5b99.js │ ├── Theme-08e39d68.css │ ├── Theme-217bd9f0.js │ ├── User-abf1ad8a.js │ ├── Users-15669eb8.js │ ├── default-8fdf2100.js │ ├── default-ae053ae9.css │ ├── empty-4a05e738.js │ ├── index-55e28da3.js │ ├── index-d8bacfc1.css │ └── user-8fc11de1.js ├── favicon.ico ├── img │ ├── apple-touch-icon.png │ ├── cover.jpg │ ├── icon.png │ ├── icon.svg │ ├── playlist │ │ ├── cloud.jpg │ │ ├── cover.psd │ │ ├── dailyRecommendSongs.jpg │ │ └── listenedRecently.jpg │ ├── pwa-192x192.png │ └── pwa-512x512.png ├── index.html ├── manifest.webmanifest ├── registerSW.js ├── robots.txt ├── sw.js └── workbox-7369c0e1.js ├── router ├── config.js ├── index.js ├── info.js ├── log.js ├── pin.js ├── playlist.js ├── record.js ├── user.js └── users.js ├── test └── netease2.js └── update-database.js /.github/ISSUE_TEMPLATE/bug-report-----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 錯誤回報 3 | about: Create a report to help us improve 建立一個回報以幫助我們改進 4 | 5 | --- 6 | 7 | **Describe the bug 描述這個錯誤** 8 | A clear and concise description of what the bug is. 9 | 請描述這則 Bug。 10 | 11 | **To Reproduce 如何重現** 12 | Steps to reproduce the behavior 重現的步驟: 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | **Expected behavior 預期會發生的行為** 19 | A clear and concise description of what you expected to happen. 20 | 清楚描述您期望發生的事情。 21 | 22 | **Screenshots 截圖** 23 | If applicable, add screenshots to help explain your problem. 24 | 如果可以,請新增截圖以幫助解釋您的問題。 25 | 26 | **Desktop (please complete the following information):** 27 | - OS 作業系統: [e.g. macOS] 28 | - Browser 瀏覽器 [e.g. chrome, safari] 29 | - Version 版本 [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device 裝置: [e.g. iPhone6] 33 | - OS 作業系統: [e.g. iOS8.1] 34 | - Browser 瀏覽器 [e.g. stock browser, safari] 35 | - Version 版本 [e.g. 22] 36 | 37 | **Additional context 備註** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | release: 9 | types: 10 | - created 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Docker meta 19 | id: meta 20 | uses: docker/metadata-action@v3 21 | with: 22 | # list of Docker images to use as base name for tags 23 | images: | 24 | gnehs/pokaplayer 25 | ghcr.io/gnehs/pokaplayer 26 | # generate Docker tags based on the following events/attributes 27 | tags: | 28 | type=schedule 29 | type=ref,event=branch 30 | type=ref,event=pr 31 | type=semver,pattern={{version}} 32 | type=semver,pattern={{major}}.{{minor}} 33 | type=semver,pattern={{major}} 34 | type=sha 35 | - name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@v1 37 | - name: Cache Docker layers 38 | if: github.event_name != 'pull_request' 39 | uses: actions/cache@v2 40 | with: 41 | path: /tmp/.buildx-cache 42 | key: ${{ runner.os }}-buildx-${{ github.sha }} 43 | restore-keys: | 44 | ${{ runner.os }}-buildx- 45 | - name: Login to DockerHub 46 | if: github.event_name != 'pull_request' 47 | uses: docker/login-action@v1 48 | with: 49 | username: ${{ secrets.DOCKERHUB_USERNAME }} 50 | password: ${{ secrets.DOCKERHUB_TOKEN }} 51 | - name: Login to GHCR 52 | if: github.event_name != 'pull_request' 53 | uses: docker/login-action@v1 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.repository_owner }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | - name: Build and push 59 | id: docker_build 60 | uses: docker/build-push-action@v2 61 | with: 62 | push: ${{ github.event_name != 'pull_request' }} 63 | tags: ${{ steps.meta.outputs.tags }} 64 | labels: ${{ steps.meta.outputs.labels }} 65 | cache-from: type=local,src=/tmp/.buildx-cache 66 | cache-to: type=local,dest=/tmp/.buildx-cache-new 67 | - name: Image digest 68 | run: echo ${{ steps.docker_build.outputs.digest }} 69 | - 70 | # Temp fix 71 | # https://github.com/docker/build-push-action/issues/252 72 | # https://github.com/moby/buildkit/issues/1896 73 | name: Move cache 74 | run: | 75 | rm -rf /tmp/.buildx-cache 76 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | #PokaPlayer config 64 | config.json 65 | sessions/* 66 | playlist.json 67 | package-lock.json 68 | dataModule/netease2Pin.json 69 | tasks.json 70 | cache 71 | .DS_Store 72 | data/ 73 | cookie.json 74 | /temp 75 | /-.* 76 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:14-alpine3.15 3 | 4 | WORKDIR /app 5 | 6 | RUN apk add git && git clone https://github.com/gnehs/Pokaplayer . 7 | 8 | COPY . /app/ 9 | 10 | RUN apk update && \ 11 | apk add --no-cache --virtual build-pkg build-base python2 && \ 12 | npm install --production --silent && \ 13 | apk del build-pkg 14 | 15 | # 環境設定 16 | ENV NODE_ENV=production 17 | EXPOSE 3000 18 | # 啟動 19 | CMD ["npm", "start"] 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 棒棒勝 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NeteaseAPI2_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | RUN apk add --no-cache tini git 4 | RUN git clone https://github.com/Binaryify/NeteaseCloudMusicApi.git /app 5 | RUN chown -R node:node /app 6 | 7 | ENV NODE_ENV production 8 | USER node 9 | 10 | WORKDIR /app 11 | 12 | RUN npm i --omit=dev --ignore-scripts 13 | 14 | EXPOSE 3000 15 | 16 | CMD [ "/sbin/tini", "--", "node", "app.js" ] 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![works badge](https://cdn.rawgit.com/nikku/works-on-my-machine/v0.2.0/badge.svg?style=flat-square)](https://github.com/nikku/works-on-my-machine) 2 | [![GitHub issues](https://img.shields.io/github/issues/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/issues) 3 | [![GitHub forks](https://img.shields.io/github/forks/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/network) 4 | [![GitHub stars](https://img.shields.io/github/stars/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/stargazers) 5 | [![GitHub license](https://img.shields.io/github/license/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/blob/master/LICENSE) 6 | [![GitHub tag (latest Ver)](https://img.shields.io/github/package-json/v/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/releases/latest) 7 | [![GitHub repo size in bytes](https://img.shields.io/github/repo-size/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/archive/master.zip) 8 | 9 | [繁體中文](https://github.com/gnehs/PokaPlayer/blob/master/README_zh.md) 10 | 11 | # PokaPlayer 12 | PokaPlayer is a player that can unify and play from multiple sources like DSM and Netease. 13 | 14 | ![image](https://user-images.githubusercontent.com/16719720/139267172-3960a386-d858-4db3-a9d7-30df8f379fd2.png) 15 | 16 | ## Get Started 17 | - If you need to listen to your local music, you will need a Synology NAS with Audio Station installed, or you can try the [Open Audio Server](https://github.com/openaudioserver/open-audio-server) which is compatible with the Audio Station API. 18 | - Deploy [Mongo](https://hub.docker.com/_/mongo) containers 19 | - init database 20 | ```bash 21 | # docker exec 22 | $ docker exec -it bash 23 | # enter mongo 24 | $ mongo 25 | # create database and user 26 | $ db.createUser( 27 | { 28 | user: "", 29 | pwd: "", 30 | roles: [ 31 | { 32 | role: "readWrite", 33 | db: "" 34 | } 35 | ] 36 | } 37 | ); 38 | # exit mongo 39 | $ exit 40 | # exit docker 41 | $ exit 42 | ``` 43 | - Fill out the configuration file according to config-simple.json 44 | - Deploy [PokaPlayer](https://hub.docker.com/repository/docker/gnehs/pokaplayer) container(optional [neteasecloudmusicapi](https://hub.docker.com/repository/docker/gnehs/neteasecloudmusicapi-docker)) 45 | - Mount the configuration file to `/app/config.json` 46 | - Connect the mongo container 47 | - export port 3000 48 | - Done! 49 | 50 | ## Suggestions and Tips 51 | - Chrome is recommended 52 | - Chrome top right corner `...` Select "Add to Home" for a native APP-like experience. 53 | - **We strongly recommend open an new account that can only play music on DSM** 54 | 55 | ## Supported sources 56 | - [DSM Audio Station](https://www.synology.com/dsm/feature/audio_station) 57 | - [Netease Cloud Music](https://music.163.com/) 58 | - The module's lyric conversion function uses the API service of the [zhconvert](https://zhconvert.org/) 59 | 60 | ## Features 61 | - Pinned Items 62 | - Search 63 | - Albums 64 | - Recently added albums 65 | - Folder 66 | - Performers 67 | - Composer 68 | - Random Play 69 | - Password Protection 70 | - Night Mode 71 | - Multi-User 72 | - MediaSession 73 | 74 | mediasession 75 | 76 | ## Contributors 77 | ![](https://contributors.nn.ci/api?repo=gnehs/PokaPlayer) 78 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | [![works badge](https://cdn.rawgit.com/nikku/works-on-my-machine/v0.2.0/badge.svg?style=flat-square)](https://github.com/nikku/works-on-my-machine) 2 | [![GitHub issues](https://img.shields.io/github/issues/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/issues) 3 | [![GitHub forks](https://img.shields.io/github/forks/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/network) 4 | [![GitHub stars](https://img.shields.io/github/stars/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/stargazers) 5 | [![GitHub license](https://img.shields.io/github/license/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/blob/master/LICENSE) 6 | [![GitHub tag (latest Ver)](https://img.shields.io/github/package-json/v/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/releases/latest) 7 | [![GitHub repo size in bytes](https://img.shields.io/github/repo-size/gnehs/PokaPlayer.svg?style=flat-square)](https://github.com/gnehs/PokaPlayer/archive/master.zip) 8 | [![Docker Build Status](https://img.shields.io/docker/build/gnehs/pokaplayer.svg?style=flat-square)](https://hub.docker.com/r/gnehs/pokaplayer/) 9 | 10 | # PokaPlayer 11 | PokaPlayer 是個能統合多個來源並進行播放的播放器。 12 | 13 | ![image](https://user-images.githubusercontent.com/16719720/139267013-17ed31c5-8194-4498-b2b4-9bf149ac9860.png) 14 | 15 | ## 開始使用 16 | - 若您需要聆聽本地的音樂,您會需要已安裝 Audio Station 的 Synology NAS,或是你也可以試試看與 Audio Station API 相容的 [Open Audio Server](https://github.com/openaudioserver/open-audio-server) 17 | - 部署 [Mongo](https://hub.docker.com/_/mongo) 容器 18 | - 初始化資料庫 19 | ```bash 20 | # 進入 docker 容器 21 | $ docker exec -it bash 22 | # 進入 mongo 23 | $ mongo 24 | # 建立資料庫與使用者 25 | $ db.createUser( 26 | { 27 | user: "", 28 | pwd: "", 29 | roles: [ 30 | { 31 | role: "readWrite", 32 | db: "" 33 | } 34 | ] 35 | } 36 | ); 37 | # 退出 mongo 38 | $ exit 39 | # 退出 docker 40 | $ exit 41 | ``` 42 | - 按照 config-simple.json 填寫設定檔 43 | - 部署 [PokaPlayer](https://hub.docker.com/repository/docker/gnehs/pokaplayer) 容器 (可選用 [neteasecloudmusicapi](https://hub.docker.com/repository/docker/gnehs/neteasecloudmusicapi-docker)) 44 | - 將設定檔掛載到 `/app/config.json` 45 | - 連接 mongo 容器 46 | - export port 3000 47 | - 完成! 48 | 49 | ## 建議和提示 50 | 51 | - 手機建議使用 Chrome 52 | - Chrome 右上角 `...` 選「加到主畫面」可以有原生 APP 般的體驗 53 | - **強烈建議在 DSM 上開一個只能播音樂的帳號** 54 | 55 | ## 支援的來源 56 | - [DSM Audio Station](https://www.synology.com/dsm/feature/audio_station) 57 | - [Netease Cloud Music](https://music.163.com/) 58 | - 該模組之歌詞轉換功能使用了 [繁化姬](https://zhconvert.org/) 的 API 服務 59 | 60 | ## 功能 61 | - 釘選項目 62 | - 搜尋 63 | - 專輯 64 | - 最近加入的專輯 65 | - 資料夾 66 | - 演出者 67 | - 作曲者 68 | - 隨機播放 69 | - 密碼保護 70 | - 夜間模式 71 | - 多使用者 72 | - MediaSession 73 | 74 | mediasession 75 | 76 | 77 | ## 貢獻者 78 | ![](https://contributors.nn.ci/api?repo=gnehs/PokaPlayer) -------------------------------------------------------------------------------- /config-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "PokaPlayer": { 3 | "debug": false, 4 | "sc2tc": true, 5 | "fixPunctuation": true 6 | }, 7 | "mongodb": "mongodb://mongodb/poka", 8 | "DSM": { 9 | "enabled": true, 10 | "protocol": "https", 11 | "host": "192.168.x.x", 12 | "port": 5000, 13 | "account": "YOUR_DSM_ACCOUNT", 14 | "password": "YOUR_DSM_PASSWORD" 15 | }, 16 | "YouTube": { 17 | "enabled": true 18 | }, 19 | "QQMusic": { 20 | "//": "enabled QQMusic as lyric provider", 21 | "enabled": true 22 | }, 23 | "Netease2": { 24 | "enabled": false, 25 | "server": "http://neteaseapi2:3000/", 26 | "isPremium": true, 27 | "login": { 28 | "method": "email/phone", 29 | "account": "YOUR_NETEASE_ACCOUNT", 30 | "password": "YOUR_NETEASE_PASSWORD" 31 | }, 32 | "topPlaylist": { 33 | "enabled": true, 34 | "categories": [ 35 | "ACG", 36 | "日语", 37 | "欧美" 38 | ], 39 | "limit": 25, 40 | "order": "hot" 41 | }, 42 | "hqPlaylist": { 43 | "enabled": true, 44 | "categories": [ 45 | "ACG", 46 | "日语", 47 | "欧美" 48 | ], 49 | "limit": 25 50 | }, 51 | "dailyRecommendSongs": { 52 | "enabled": true 53 | }, 54 | "dailyRecommendPlaylists": { 55 | "enabled": true, 56 | "limit": 10 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /data/.preserve: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/data/.preserve -------------------------------------------------------------------------------- /data/mongo-init.js: -------------------------------------------------------------------------------- 1 | db.createUser( 2 | { 3 | user: "", 4 | pwd: "", 5 | roles: [ 6 | { 7 | role: "readWrite", 8 | db: "" 9 | } 10 | ] 11 | } 12 | ); -------------------------------------------------------------------------------- /dataModule/cryptoUtils.js: -------------------------------------------------------------------------------- 1 | 2 | const jsonfile = require('jsonfile') 3 | const crypto = require('crypto') 4 | const config = jsonfile.readFileSync("./config.json") 5 | const { sessionSecret } = config.PokaPlayer 6 | 7 | const decodeBase64 = x => Buffer.from(x, "base64url").toString("utf8"); 8 | const encodeBase64 = x => Buffer.from(x).toString("base64url"); 9 | function genHash(str) { 10 | const hash = crypto.createHash('sha256') 11 | hash.update(str.toString() + sessionSecret, 'utf8') 12 | return hash.digest('hex').slice(0, 8) 13 | } 14 | function encodeURL(url) { 15 | return encodeBase64(JSON.stringify([url, genHash(url)])) 16 | } 17 | function decodeURL(str) { 18 | let [url, hash] = JSON.parse(decodeBase64(str)) 19 | if (genHash(url) == hash) { 20 | return url 21 | } else { 22 | return false 23 | } 24 | } 25 | module.exports = { 26 | decodeBase64, 27 | encodeBase64, 28 | encodeURL, 29 | decodeURL 30 | } 31 | -------------------------------------------------------------------------------- /dataModule/dsm.js: -------------------------------------------------------------------------------- 1 | const config = require("../config.json"), // 很會設定ㄉ朋友 2 | schedule = require("node-schedule"), // 很會計時ㄉ朋友 3 | pokaLog = require("../log"), // 可愛控制台輸出 4 | dsmURL = `${config.DSM.protocol}://${config.DSM.host}:${config.DSM.port}`, 5 | lyricRegex = /\[([0-9.:]*)\]/i 6 | 7 | const axios = require('axios'); 8 | const { wrapper } = require('axios-cookiejar-support'); 9 | const { CookieJar } = require('tough-cookie'); 10 | const jar = new CookieJar(); 11 | const client = wrapper(axios.create({ jar, baseURL: dsmURL })); 12 | const transformRequest = (jsonData = {}) => Object.entries(jsonData).map(x => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`).join('&'); 13 | let SynoToken = ""; 14 | let sid = ""; 15 | 16 | const { decodeBase64, encodeBase64 } = require('./cryptoUtils') 17 | 18 | function parseSongs(songs) { 19 | return songs.map(x => { 20 | let albumInfo = [ 21 | x.additional.song_tag.album || "", 22 | "", 23 | x.additional.song_tag.album_artist || "" 24 | ]; 25 | let cover = 26 | `/pokaapi/cover/?moduleName=DSM&data=` + 27 | encodeURIComponent(encodeBase64( 28 | JSON.stringify({ 29 | type: "album", 30 | info: albumInfo 31 | }))); 32 | return { 33 | artist: x.additional.song_tag.artist, 34 | artistId: x.additional.song_tag.artist, 35 | album: x.additional.song_tag.album, 36 | albumId: encodeBase64(JSON.stringify(albumInfo)), 37 | bitrate: x.additional.song_audio.bitrate, 38 | cover, 39 | codec: x.additional.song_audio.codec, 40 | id: x.id, 41 | source: "DSM", 42 | name: x.title, 43 | track: x.additional.song_tag.track, 44 | url: "/pokaapi/song/?moduleName=DSM&songId=" + x.id, 45 | year: x.additional.song_tag.year, 46 | } 47 | }); 48 | } 49 | 50 | function parseAlbums(albums) { 51 | return albums.map(x => { 52 | let coverInfo = [ 53 | x.name || "", 54 | x.artist || "", 55 | x.album_artist || "" 56 | ]; 57 | let cover = 58 | `/pokaapi/cover/?moduleName=DSM&data=` + 59 | encodeURIComponent(encodeBase64( 60 | JSON.stringify({ 61 | type: "album", 62 | info: coverInfo 63 | }))); 64 | return { 65 | artist: x.display_artist, 66 | cover, 67 | id: encodeBase64(JSON.stringify(coverInfo)), 68 | name: x.name, 69 | source: "DSM", 70 | year: x.year, 71 | } 72 | }); 73 | } 74 | 75 | function parsePlaylists(playlists) { 76 | return playlists.map(x => ({ 77 | id: x.id, 78 | name: x.name, 79 | source: "DSM", 80 | })); 81 | } 82 | function parseArtists(data, type = "artist") { 83 | return data.map(x => ({ 84 | id: x.name == '' ? `DSM_unknown` : x.name, 85 | name: x.name == '' ? `Unknown` : x.name, 86 | cover: `/pokaapi/cover/?moduleName=DSM&data=${encodeURIComponent(encodeBase64(JSON.stringify({ type, info: x.name || "" })))}`, 87 | source: "DSM", 88 | })); 89 | } 90 | 91 | function parseComposers(data) { 92 | return parseArtists(data, type = "composer") 93 | } 94 | 95 | 96 | async function onLoaded() { 97 | //自動重新登入 98 | schedule.scheduleJob("0 0 * * *", function () { 99 | pokaLog.logDM('DSM', '正在重新登入...') 100 | login(); 101 | }); 102 | 103 | return await login(); 104 | } 105 | async function login() { 106 | if (!config.DSM.account && !config.DSM.password) { 107 | pokaLog.logDMErr('DSM', '登入失敗,未設定帳號密碼') 108 | return false; 109 | } 110 | let result = await requestAPI({ 111 | path: "entry.cgi", 112 | name: "SYNO.API.Auth", 113 | method: "login", 114 | params: { 115 | account: config.DSM.account, 116 | passwd: config.DSM.password, 117 | enable_syno_token: 'yes' 118 | }, 119 | version: 7 120 | }) 121 | if (result.success) { 122 | SynoToken = result.data.synotoken; 123 | sid = result.data.sid; 124 | pokaLog.logDM('DSM', `${config.DSM.account} 登入成功!(DSM 7.0)`) 125 | return true; 126 | } else { 127 | pokaLog.logDM('DSM', `正在嘗試以舊版 API 登入...`) 128 | // 嘗試舊版 API (6.0 以下) 129 | let oldApiResult = await requestAPI({ 130 | path: "auth.cgi", 131 | name: "SYNO.API.Auth", 132 | method: "Login", 133 | params: { 134 | account: config.DSM.account, 135 | passwd: config.DSM.password, 136 | session: "AudioStation", 137 | format: "cookie" 138 | }, 139 | version: 3 140 | }) 141 | if (oldApiResult.success) { 142 | pokaLog.logDM('DSM', `${config.DSM.account} 登入成功! (DSM 6.0)`) 143 | return true; 144 | } else { 145 | pokaLog.logDMErr('DSM', `${config.DSM.account} 登入失敗,請檢查您的設定檔是否正確`) 146 | return false; 147 | } 148 | } 149 | } 150 | 151 | async function requestAPI({ 152 | path, 153 | name, 154 | method, 155 | params = {}, 156 | version = 1, 157 | requestMethod = "POST" 158 | }) { 159 | params = { ...params, SynoToken } 160 | let form = Object.assign({ 161 | api: name, 162 | method, 163 | version, 164 | }, params); 165 | try { 166 | let { data } = await client.post(`/webapi/${path}?api=${name}`, transformRequest(form), { 167 | method: requestMethod, 168 | params, 169 | headers: { 170 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 171 | 'x-syno-token': SynoToken 172 | } 173 | }) 174 | // console.log(name, params, data) 175 | return data; 176 | } catch (e) { 177 | pokaLog.logDMErr('DSM', `${name} API request error:\n${e.message}`) 178 | return false; 179 | } 180 | } 181 | 182 | async function getHome() { 183 | let latestAlbum = await getAlbums(25, "time", "desc") 184 | latestAlbum.title = "home_recentAlbums" 185 | latestAlbum.source = "DSM" 186 | latestAlbum.icon = "album" 187 | return latestAlbum.albums.length ? [latestAlbum] : [] 188 | } 189 | async function getSong(req, songRes = "high", songId) { 190 | let url = dsmURL; 191 | switch (songRes) { 192 | case "high": 193 | url += `/webapi/AudioStation/stream.cgi/0.wav?api=SYNO.AudioStation.Stream&version=2&method=transcode&SynoToken=${SynoToken}&format=wav&id=`; 194 | break; 195 | case "low": //128K 196 | url += `/webapi/AudioStation/stream.cgi/0.mp3?api=SYNO.AudioStation.Stream&version=2&method=transcode&SynoToken=${SynoToken}&format=mp3&id=`; 197 | break; 198 | case "medium": //128K 199 | url += `/webapi/AudioStation/stream.cgi/0.mp3?api=SYNO.AudioStation.Stream&version=2&method=transcode&SynoToken=${SynoToken}&format=mp3&id=`; 200 | break; 201 | case "original": 202 | url += `/webapi/AudioStation/stream.cgi/0.mp3?api=SYNO.AudioStation.Stream&version=2&method=stream&SynoToken=${SynoToken}&id=`; 203 | break; 204 | default: 205 | url += `/webapi/AudioStation/stream.cgi/0.mp3?api=SYNO.AudioStation.Stream&version=2&method=stream&SynoToken=${SynoToken}&id=`; 206 | break; 207 | } 208 | url += songId; 209 | return (await client.get(url, { 210 | responseType: 'stream', 211 | headers: { 212 | ...req.headers, 213 | Host: config.DSM.host 214 | }, 215 | })) 216 | } 217 | 218 | async function getCover(data) { 219 | function deReq(x) { 220 | const b2a = x => Buffer.from(x, "base64").toString("utf8"); 221 | const decode = x => /(.{5})(.+)3C4C7CB3(.+)/.exec(x); 222 | let [_, rand, link, checkSum] = decode(x); 223 | [_, rand, link, checkSum] = [_, rand, b2a(link), b2a(checkSum)]; 224 | if (!Number.isInteger(Math.log10(rand.charCodeAt(0) + checkSum.charCodeAt(0)))) { 225 | return false; 226 | } 227 | return link; 228 | } 229 | let coverData; 230 | if (data.startsWith("Poka-")) { 231 | coverData = JSON.parse(deReq(data)); 232 | coverData.info = Object.values(coverData.info) 233 | } else { 234 | coverData = JSON.parse(decodeBase64(data)); 235 | } 236 | let url = `/webapi/AudioStation/cover.cgi?api=SYNO.AudioStation.Cover&output_default=true&is_hr=false&version=3&library=shared&method=getcover&view=default&SynoToken=${SynoToken}`; 237 | switch (coverData.type) { 238 | case "artist": //演出者 239 | url += coverData.info ? 240 | `&artist_name=${encodeURIComponent(coverData.info)}` : 241 | `&artist_name=`; 242 | break; 243 | case "composer": //作曲者 244 | url += coverData.info ? 245 | `&composer_name=${encodeURIComponent(coverData.info)}` : 246 | `&composer_name=`; 247 | break; 248 | case "genre": //類型 249 | url += coverData.info ? `&genre_name=${encodeURIComponent(coverData.info)}` : ``; 250 | break; 251 | case "song": //歌曲 252 | url = `/webapi/AudioStation/cover.cgi?api=SYNO.AudioStation.Cover&output_default=true&is_hr=false&version=3&library=shared&method=getsongcover&view=large&id=${coverData.info}`; 253 | break; 254 | case "folder": //資料夾 255 | url = `/webapi/AudioStation/cover.cgi?api=SYNO.AudioStation.Cover&output_default=true&is_hr=false&version=3&library=shared&method=getfoldercover&view=default&id=${coverData.info}`; 256 | break; 257 | case "album": //專輯 258 | url += coverData.info[0] ? `&album_name=${encodeURIComponent(coverData.info[0])}` : ``; 259 | url += coverData.info[1] ? `&artist_name=${encodeURIComponent(coverData.info[1])}` : ``; 260 | url += `&album_artist_name=${encodeURIComponent(coverData.info[2] || '')}`; 261 | break; 262 | } 263 | return (await client.get(url, { 264 | responseType: 'stream', 265 | })).data 266 | } 267 | 268 | async function search(keyword) { 269 | let result = await requestAPI({ 270 | path: "AudioStation/search.cgi", 271 | name: "SYNO.AudioStation.Search", 272 | method: "list", 273 | params: { 274 | additional: "song_tag,song_audio,song_rating", 275 | library: "shared", 276 | limit: 50, 277 | sort_by: "title", 278 | sort_direction: "ASC", 279 | keyword 280 | }, 281 | version: 1 282 | }) 283 | return { 284 | albums: parseAlbums(result.data.albums || []), 285 | songs: parseSongs(result.data.songs || []), 286 | artists: parseArtists(result.data.artists || []) 287 | }; 288 | } 289 | 290 | async function getAlbums(limit = 1000, sort_by = "name", sort_direction = "ASC") { 291 | let result = await requestAPI({ 292 | path: "AudioStation/album.cgi", 293 | name: "SYNO.AudioStation.Album", 294 | method: "list", 295 | params: { 296 | additional: "avg_rating", 297 | library: "shared", 298 | limit: limit, 299 | sort_by, 300 | sort_direction, 301 | }, 302 | version: 3 303 | }) 304 | return { 305 | albums: parseAlbums(result.data.albums) 306 | }; 307 | } 308 | async function getAlbum(id) { 309 | let [album, album_artist, artist] = JSON.parse(decodeBase64(id)); 310 | let params = { 311 | additional: "song_tag,song_audio,song_rating", 312 | library: "shared", 313 | limit: 100000, 314 | sort_by: "title", 315 | sort_direction: "ASC", 316 | } 317 | if (album != '') params["album"] = album 318 | if (album_artist != '') params["album_artist"] = album_artist 319 | if (artist != '') params["artist"] = artist 320 | let result = await requestAPI({ 321 | path: "AudioStation/song.cgi", 322 | name: "SYNO.AudioStation.Song", 323 | method: "list", 324 | params, 325 | version: 3 326 | }) 327 | let cover = `/pokaapi/cover/?moduleName=DSM&data=` + encodeURIComponent(encodeBase64(JSON.stringify({ type: "album", info: [album, album_artist, artist] }))) 328 | // sort by track 329 | result.data.songs.sort((a, b) => a.additional.song_tag.track - b.additional.song_tag.track) 330 | return { 331 | name: album, 332 | artist: artist || album_artist, 333 | artistId: artist, 334 | cover: cover, 335 | songs: parseSongs(result.data.songs) 336 | }; 337 | } 338 | 339 | async function getFolders() { 340 | return await getFolderFiles(); 341 | } 342 | 343 | async function getFolderFiles(id) { 344 | let result = await requestAPI({ 345 | path: "AudioStation/folder.cgi", 346 | name: "SYNO.AudioStation.Folder", 347 | method: "list", 348 | params: { 349 | additional: "song_tag,song_audio,song_rating", 350 | library: "shared", 351 | limit: 1000, 352 | method: "list", 353 | sort_by: "title", 354 | sort_direction: "ASC", 355 | id: id || "", 356 | }, 357 | version: 2 358 | }) 359 | let songs = parseSongs(result.data.items.filter(({ type }) => type === "file")) 360 | let folders = result.data.items.filter(({ type }) => type === "folder").map(x => ({ 361 | id: x.id, 362 | name: x.title, 363 | cover: `/pokaapi/cover/?moduleName=DSM&data=` + 364 | encodeURIComponent( 365 | encodeBase64( 366 | JSON.stringify({ 367 | type: "folder", 368 | info: x.id 369 | }) 370 | ) 371 | ), 372 | source: "DSM" 373 | })) 374 | return { 375 | songs: songs, 376 | folders: folders 377 | }; 378 | } 379 | 380 | async function getArtists() { 381 | let result = await requestAPI({ 382 | path: "AudioStation/artist.cgi", 383 | name: "SYNO.AudioStation.Artist", 384 | method: "list", 385 | params: { 386 | additional: "avg_rating", 387 | library: "shared", 388 | limit: 1000, 389 | sort_by: "name", 390 | sort_direction: "ASC" 391 | }, 392 | version: 4 393 | }) 394 | return { 395 | artists: parseArtists(result.data.artists) 396 | }; 397 | } 398 | 399 | async function getArtist(id) { 400 | let result = {} 401 | result.name = id; 402 | result.cover = `/pokaapi/cover/?moduleName=DSM&data=${encodeURIComponent(encodeBase64(JSON.stringify({ type: "artist", info: id })))}`; 403 | return result; 404 | } 405 | async function getArtistAlbums(id) { 406 | if (id == `DSM_unknown`) id = '' 407 | let result = await requestAPI({ 408 | path: "AudioStation/album.cgi", 409 | name: "SYNO.AudioStation.Album", 410 | method: "list", 411 | params: { 412 | additional: "avg_rating", 413 | library: "shared", 414 | limit: 1000, 415 | method: "list", 416 | sort_by: "display_artist", 417 | sort_direction: "ASC", 418 | artist: id 419 | }, 420 | version: 3 421 | }) 422 | return { 423 | albums: parseAlbums(result.data.albums) 424 | }; 425 | } 426 | 427 | async function getComposer(id) { 428 | let result = {} 429 | result.name = id; 430 | result.cover = `/pokaapi/cover/?moduleName=DSM&data=${encodeURIComponent(encodeBase64(JSON.stringify({ type: "composer", info: id })))}`; 431 | return result; 432 | } 433 | async function getComposers() { 434 | let result = await requestAPI({ 435 | path: "AudioStation/composer.cgi", 436 | name: "SYNO.AudioStation.Composer", 437 | method: "list", 438 | params: { 439 | additional: "avg_rating", 440 | library: "shared", 441 | limit: 1000, 442 | sort_by: "name", 443 | sort_direction: "ASC" 444 | }, 445 | version: 2 446 | }) 447 | return { 448 | composers: parseComposers(result.data.composers) 449 | }; 450 | } 451 | 452 | async function getComposerAlbums(id) { 453 | if (id == `DSM_unknown`) id = '' 454 | let result = await requestAPI({ 455 | path: "AudioStation/album.cgi", 456 | name: "SYNO.AudioStation.Album", 457 | method: "list", 458 | params: { 459 | additional: "avg_rating", 460 | library: "shared", 461 | limit: 1000, 462 | method: "list", 463 | sort_by: "display_artist", 464 | sort_direction: "ASC", 465 | composer: id 466 | }, 467 | version: 3 468 | }) 469 | return { 470 | albums: parseAlbums(result.data.albums) 471 | }; 472 | } 473 | 474 | async function getPlaylists() { 475 | let result = await requestAPI({ 476 | path: "AudioStation/playlist.cgi", 477 | name: "SYNO.AudioStation.Playlist", 478 | method: "list", 479 | params: { 480 | limit: 1000, 481 | library: "shared", 482 | sort_by: "", 483 | sort_direction: "ASC" 484 | }, 485 | version: 3 486 | }) 487 | return { 488 | playlists: parsePlaylists(result.data.playlists) 489 | }; 490 | } 491 | 492 | async function getPlaylistSongs(id) { 493 | let result = await requestAPI({ 494 | path: "AudioStation/playlist.cgi", 495 | name: "SYNO.AudioStation.Playlist", 496 | method: "getinfo", 497 | params: { 498 | additional: "songs_song_tag,songs_song_audio,songs_song_rating,sharing_info", 499 | limit: 1000, 500 | library: "shared", 501 | sort_by: "", 502 | sort_direction: "ASC", 503 | id 504 | }, 505 | version: 3 506 | }) 507 | result = result.data.playlists[0]; 508 | return { 509 | songs: parseSongs(result.additional.songs), 510 | playlists: [{ 511 | name: result.name, 512 | source: "DSM", 513 | id: result.id 514 | }] 515 | }; 516 | } 517 | async function getRandomSongs(id) { 518 | let result = await requestAPI({ 519 | path: "AudioStation/song.cgi", 520 | name: "SYNO.AudioStation.Song", 521 | method: "list", 522 | params: { 523 | additional: "song_tag,song_audio,song_rating", 524 | library: "shared", 525 | limit: 100, 526 | sort_by: "random", 527 | }, 528 | version: 1 529 | }) 530 | return { 531 | songs: parseSongs(result.data.songs) 532 | }; 533 | } 534 | 535 | async function getLyric(id) { 536 | let result = await requestAPI({ 537 | path: "AudioStation/lyrics.cgi", 538 | name: "SYNO.AudioStation.Lyrics", 539 | method: "getlyrics", 540 | params: { id }, 541 | version: 2 542 | }) 543 | result = result && result.lyrics ? result.lyrics : false; 544 | if (result && result.match(lyricRegex)) return result; 545 | else return false; 546 | } 547 | 548 | module.exports = { 549 | name: "DSM", 550 | enabled: config.DSM.enabled, 551 | onLoaded, 552 | getHome, 553 | getSong, 554 | getCover, 555 | search, 556 | getAlbums, 557 | getAlbum, 558 | //getAlbumSongs, 559 | getFolders, 560 | getFolderFiles, 561 | getArtist, 562 | getArtists, 563 | getArtistAlbums, 564 | getComposer, 565 | getComposers, 566 | getComposerAlbums, 567 | getPlaylists, 568 | getPlaylistSongs, 569 | getRandomSongs, 570 | getLyric 571 | }; -------------------------------------------------------------------------------- /dataModule/lyricUtils.js: -------------------------------------------------------------------------------- 1 | const OpenCC = require('opencc-js'); 2 | const converter_TW = OpenCC.ConverterFactory( 3 | OpenCC.Locale.from.cn, 4 | OpenCC.Locale.to.twp 5 | ); 6 | const converter_TC = OpenCC.ConverterFactory( 7 | OpenCC.Locale.from.cn, 8 | OpenCC.Locale.to.tw 9 | ); 10 | const jsonfile = require('jsonfile') 11 | const pangu = require('pangu'); 12 | const config = jsonfile.readFileSync("./config.json").PokaPlayer; 13 | function migrate(org, t, offset = 10 ** -3) { 14 | const isDigit = x => !isNaN(Number(x)); 15 | 16 | const plus = (num1, num2, ...others) => { 17 | // 精確加法 18 | if (others.length > 0) return plus(plus(num1, num2), others[0], ...others.slice(1)); 19 | const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2))); 20 | return (times(num1, baseNum) + times(num2, baseNum)) / baseNum; 21 | }; 22 | const digitLength = num => { 23 | // Get digit length of e 24 | const eSplit = num.toString().split(/[eE]/); 25 | const len = (eSplit[0].split(".")[1] || "").length - +(eSplit[1] || 0); 26 | return len > 0 ? len : 0; 27 | }; 28 | const times = (num1, num2, ...others) => { 29 | // 精確乘法 30 | function checkBoundary(num) { 31 | if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { 32 | console.warn(`${num} is beyond boundary when transfer to integer, the results may not be accurate`); 33 | } 34 | } 35 | 36 | function float2Fixed(num) { 37 | if (num.toString().indexOf("e") === -1) return Number(num.toString().replace(".", "")); 38 | const dLen = digitLength(num); 39 | return dLen > 0 ? num * Math.pow(10, dLen) : num; 40 | } 41 | 42 | if (others.length > 0) return times(times(num1, num2), others[0], ...others.slice(1)); 43 | const num1Changed = float2Fixed(num1); 44 | const num2Changed = float2Fixed(num2); 45 | const baseNum = digitLength(num1) + digitLength(num2); 46 | const leftValue = num1Changed * num2Changed; 47 | 48 | checkBoundary(leftValue); 49 | 50 | return leftValue / Math.pow(10, baseNum); 51 | }; 52 | const minus = (num1, num2, ...others) => { 53 | // 精確減法 54 | if (others.length > 0) return minus(minus(num1, num2), others[0], ...others.slice(1)); 55 | const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2))); 56 | return (times(num1, baseNum) - times(num2, baseNum)) / baseNum; 57 | }; 58 | const strip = (x, precision = 12) => +parseFloat(x.toPrecision(precision)); // 數字精確化 59 | 60 | const tagToTime = tag => 61 | isDigit(tag[0]) ? 62 | tag 63 | .split(":") 64 | .reverse() 65 | .reduce((acc, cur, index) => plus(acc, Number(cur) * 60 ** index), 0) : 66 | tag; 67 | const parse = (x, isTranslated = false) => { 68 | let pLyricLines = x 69 | .split("\n") 70 | .filter(x => x != "") 71 | .map(str => { 72 | const regex = /\[(\d+:\d+\.\d+)\]/gm; 73 | let m; 74 | 75 | let result = []; 76 | 77 | while ((m = regex.exec(str)) !== null) { 78 | if (m.index === regex.lastIndex) regex.lastIndex++; 79 | result.push(m[1]); 80 | } 81 | result.push(str.match(/.+\]((?:.|^$)*)/)[1]); 82 | return result; 83 | }); 84 | let result = []; 85 | for (let pLyricLine of pLyricLines) { 86 | let lyric = pLyricLine.pop(); 87 | for (let time of pLyricLine) { 88 | result.push([tagToTime(time), lyric, isTranslated]); 89 | } 90 | } 91 | return result; 92 | }; 93 | 94 | const timeToTag = seconds => { 95 | let minute = Math.floor(seconds / 60); 96 | let second = minus(seconds, minute * 60); 97 | return `${minute}:${second}`; 98 | }; 99 | 100 | // 開始切成 [(tag, lyric)] 101 | 102 | parsedLyrics = parse(org) 103 | .concat(parse(t, true)) 104 | .sort((a, b) => { 105 | if ((typeof a[0] == typeof b[0]) == "string") return 0; 106 | else if (typeof a[0] == "string") return -1; 107 | else if (typeof b[0] == "string") return 1; 108 | else { 109 | if (a[0] == b[0]) return a[2] ? 1 : -1; 110 | else return a[0] < b[0] ? -1 : 1; 111 | } 112 | }); 113 | 114 | // 整理成 [[time, [orgLyric, tLyric]]] 115 | let parsedLyricPairs = []; 116 | 117 | let i = 0; 118 | while (i < parsedLyrics.length) { 119 | if (typeof parsedLyrics[i][0] == "string") { 120 | parsedLyricPairs.push(parsedLyrics[i]); 121 | i += 1; 122 | } else if (i != parsedLyrics.length - 1) { 123 | if (parsedLyrics[i][0] == parsedLyrics[i + 1][0]) { 124 | parsedLyricPairs.push([parsedLyrics[i][0], 125 | [parsedLyrics[i][1], parsedLyrics[i + 1][1]] 126 | ]); 127 | i += 2; 128 | } else { 129 | parsedLyricPairs.push([parsedLyrics[i][0], 130 | [parsedLyrics[i][1], parsedLyrics[i][1]] 131 | ]); 132 | i += 1; 133 | } 134 | } else { 135 | parsedLyricPairs.push([parsedLyrics[i][0], 136 | [parsedLyrics[i][1], parsedLyrics[i][1]] 137 | ]); 138 | i += 1; 139 | } 140 | } 141 | 142 | // 壓回 LRC 143 | let result = ""; 144 | for (let i in parsedLyricPairs) { 145 | i = Number(i); 146 | if (typeof parsedLyricPairs[i][0] == "string") result += `[${parsedLyricPairs[i][0]}]\n`; 147 | else { 148 | if (i != parsedLyricPairs.length - 1) { 149 | result += `[${timeToTag(parsedLyricPairs[i][0])}]${parsedLyricPairs[i][1][0]}\n[${timeToTag( 150 | plus(parsedLyricPairs[i + 1][0], -offset) 151 | )}]${parsedLyricPairs[i][1][1]}\n`; 152 | } else { 153 | result += `[${timeToTag(parsedLyricPairs[i][0])}]${parsedLyricPairs[i][1][0]}\n[${timeToTag( 154 | parsedLyricPairs[i][0] 155 | )}]${parsedLyricPairs[i][1][1]}\n`; 156 | } 157 | } 158 | } 159 | 160 | return result; 161 | } 162 | async function parseLyric(originalLyric, translatedLyric) { 163 | let result = "" 164 | if (!translatedLyric) { 165 | originalLyric = fixPunctuation(originalLyric) 166 | originalLyric = pangu.spacing(originalLyric) 167 | originalLyric = await zhconvert(originalLyric, "TC") 168 | result = originalLyric 169 | } else { 170 | translatedLyric = await zhconvert(translatedLyric, "TC") 171 | result = migrate(originalLyric, translatedLyric) 172 | result = fixPunctuation(result) 173 | result = pangu.spacing(result) 174 | } 175 | return result 176 | } 177 | async function zhconvert(text, converter = "Taiwan") { 178 | if (!config.sc2tc) { 179 | return text; 180 | } 181 | 182 | if (converter == "Taiwan") { 183 | return converter_TW(text); 184 | } 185 | 186 | return converter_TC(text); 187 | } 188 | function fixPunctuation(text) { 189 | if (!config.fixPunctuation) { 190 | return text; 191 | } 192 | 193 | let punctuationList = { 194 | "“": "「", 195 | "”": "」", 196 | "‘": "『", 197 | "’": "』", 198 | "词": "詞", 199 | "编": "編", 200 | }; 201 | 202 | for (let key in punctuationList) { 203 | text = text.replace(new RegExp(key, "g"), punctuationList[key]); 204 | } 205 | 206 | return text 207 | .replace(/(\w)』(\w)/g, "$1’$2") 208 | .replace(/(\w)'(\w)/g, "$1’$2") 209 | .replace(/『(\w)/g, "‘$1") 210 | .replace(/"(\w)"/g, "“$1”") 211 | .replace(/\/\//g, ""); 212 | } 213 | module.exports = { 214 | zhconvert, parseLyric, 215 | chnToTw: converter_TW 216 | } -------------------------------------------------------------------------------- /dataModule/poka.js: -------------------------------------------------------------------------------- 1 | const playlistDB = require('../db/playlist') 2 | const pinDB = require('../db/pin') 3 | const recordDB = require('../db/record') 4 | const { encodeBase64, decodeURL, encodeURL } = require('./cryptoUtils') 5 | const axios = require('axios'); 6 | const lyricdb = require("../db/lyric.js"); 7 | async function onLoaded() { 8 | return true 9 | } 10 | async function searchLyrics(keyword) { 11 | let res = await lyricdb.searchLyric(keyword) 12 | res = res 13 | .map(x => ({ 14 | artist: x.artist, 15 | name: x.title, 16 | id: x.songId, 17 | source: "poka", 18 | lyric: x.lyric 19 | })) 20 | .filter(x => x.lyric != '[00:00.000]') 21 | return { lyrics: res }; 22 | } 23 | 24 | 25 | async function getCover(data) { 26 | let url = decodeURL(data) 27 | if (url) { 28 | return (await axios.get(url, { 29 | responseType: 'stream', 30 | })).data 31 | } else { 32 | return null 33 | } 34 | } 35 | async function getPlaylists(userId) { 36 | return ({ 37 | playlists: [ 38 | ...(await playlistDB.getParsedUserPlaylists(userId)), 39 | { name: "最近聽過", source: "poka", id: "listenedRecently", cover: `/img/playlist/listenedRecently.jpg` } 40 | ] 41 | }) 42 | } 43 | async function getPlaylistSongs(id, userId) { 44 | if (id == 'listenedRecently') { 45 | return ({ 46 | songs: (await recordDB.fetchListenedRecently(userId)), 47 | playlists: [{ 48 | name: "最近聽過", 49 | source: "poka", 50 | id: "listenedRecently", 51 | cover: `/img/playlist/listenedRecently.jpg` 52 | }] 53 | }) 54 | } 55 | else { 56 | return (await playlistDB.getParsedUserPlaylistById(id, userId)) 57 | } 58 | } 59 | async function getHome(userId) { 60 | let pins = { 61 | title: 'home_pins', 62 | source: "poka", 63 | icon: "push_pin", 64 | artists: [], 65 | composers: [], 66 | folders: [], 67 | playlists: [], 68 | albums: [] 69 | }; 70 | let pinsData = await pinDB.getPins(userId) 71 | pinsData.map(x => { 72 | try { 73 | pins[{ artist: 'artists', composer: 'composers', folder: 'folders', playlist: 'playlists', album: 'albums' }[x.type]].push(x) 74 | } 75 | catch (e) { 76 | throw new Error(`${e} ${JSON.stringify(x)}`); 77 | } 78 | }) 79 | pins.albums = pins.albums.map(x => { 80 | if (x.source == 'DSM') { 81 | if (!x.id.startsWith('Wy')) { 82 | x.id = encodeBase64(JSON.stringify(Object.values(JSON.parse(x.id)))) 83 | } 84 | } 85 | return x 86 | }) 87 | pins.playlists = pins.playlists.map(x => { 88 | if (x.source == 'poka') { 89 | let url = x.cover 90 | if (url.startsWith('http')) { 91 | x.cover = `/pokaapi/cover/?moduleName=poka&data=${encodeURL(url)}` 92 | } 93 | } 94 | return x 95 | }) 96 | return [pins] 97 | } 98 | module.exports = { 99 | name: "poka", 100 | enabled: true, 101 | onLoaded, 102 | searchLyrics, 103 | getPlaylists, 104 | getPlaylistSongs, 105 | getHome, 106 | getCover, 107 | }; -------------------------------------------------------------------------------- /dataModule/qqmusic.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const pangu = require('pangu'); 3 | const { decodeHTML } = require("entities") 4 | const { parseLyric } = require('./lyricUtils') 5 | const pokaLog = require("../log"); // 可愛控制台輸出 6 | const config = require(__dirname + "/../config.json").QQMusic; // 設定 7 | async function searchLyrics(keyword) { 8 | // ?w=${encodeURIComponent(keyword)}&format=json&cr=1&g_tk=5381&t=0&n=5&p=1 9 | let searchResult = await axios(`https://u.y.qq.com/cgi-bin/musicu.fcg`, { 10 | method: "POST", 11 | headers: { 12 | Referer: 'https://y.qq.com', 13 | }, 14 | data: { 15 | "music.search.SearchCgiService": { 16 | method: "DoSearchForQQMusicDesktop", 17 | module: "music.search.SearchCgiService", 18 | param: { 19 | num_per_page: 5, 20 | page_num: 1, 21 | query: keyword, 22 | search_type: 0, // 0:单曲,2:歌单,7:歌词,8:专辑,9:歌手,12:mv 23 | }, 24 | }, 25 | } 26 | }) 27 | .then(res => res.data['music.search.SearchCgiService'].data.body.song.list) 28 | searchResult = searchResult.map(async y => { 29 | if (!y.mid) return null 30 | let lyric = await getLyric(y.mid) 31 | if (!lyric) { 32 | return null 33 | } 34 | lyric = decodeHTML(lyric) 35 | return { 36 | name: y.title, 37 | artist: y.singer.map(x => x.name).join(`、`), 38 | source: "QQMusic", 39 | id: y.songmid, 40 | lyric 41 | } 42 | }) 43 | 44 | return { lyrics: (await Promise.all(searchResult)).filter(x => x) }; 45 | } 46 | function getLyric(id) { 47 | let url = `https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?songmid=${encodeURIComponent(id)}` 48 | url += `&pcachetime=${new Date().getTime()}` 49 | url += `&g_tk=5381` 50 | url += `&loginUin=0` 51 | url += `&hostUin=0` 52 | url += `&inCharset=utf8` 53 | url += `&outCharset=utf-8` 54 | url += `¬ice=0` 55 | url += `&platform=yqq` 56 | url += `&needNewCode=0` 57 | 58 | return axios(url, { 59 | method: "GET", 60 | headers: { 61 | Referer: 'https://y.qq.com', 62 | } 63 | }) 64 | .then(res => res.data) 65 | // decode jsonp 66 | .then(res => res.replace(/^.*?\(/, '').replace(/\);?$/, '')) 67 | .then(res => JSON.parse(res)) 68 | .then(async x => { 69 | if (x.lyric) { 70 | let lyric, tlyric 71 | lyric = Buffer.from(x.lyric, 'base64').toString() 72 | try { 73 | tlyric = Buffer.from(x.trans, 'base64').toString() 74 | } catch (e) { } 75 | let result = tlyric ? await parseLyric(lyric, tlyric) : await parseLyric(lyric) 76 | result = result 77 | .split('\n') 78 | .map(x => x.endsWith('//') ? x.replace(/\/\/$/, '') : x) 79 | .join('\n') 80 | return result 81 | } 82 | }) 83 | } 84 | async function onLoaded() { 85 | console.time("QQMusic Lyric Test"); 86 | try { 87 | let res = await searchLyrics(`世界で一番恋してる 喜多修平`) 88 | if (res.lyrics.length) { 89 | pokaLog.logDM('QQMusic', `Lyric loaded`) 90 | console.timeEnd("QQMusic Lyric Test"); 91 | } 92 | return res.lyrics.length 93 | } catch (e) { 94 | console.log(e) 95 | return false 96 | } 97 | } 98 | 99 | module.exports = { 100 | name: "QQMusic", 101 | enabled: config && config.enabled, 102 | onLoaded, 103 | searchLyrics, 104 | }; 105 | -------------------------------------------------------------------------------- /dataModule/youtube.js: -------------------------------------------------------------------------------- 1 | const yt = require('youtube-search-without-api-key'); 2 | const YTDlpWrap = require('yt-dlp-wrap').default; 3 | const config = require('../config.json').YouTube; 4 | const pokaLog = require("../log"); // 可愛控制台輸出 5 | const axios = require('axios'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const YTDLP_PATH = "./temp/yt-dlp"; 9 | fs.mkdirSync('./temp', { recursive: true }); 10 | // clear temp folder 11 | fs.readdirSync('./temp').forEach(file => { 12 | if (file == "yt-dlp") return; 13 | fs.unlink(`./temp/${file}`, err => { 14 | if (err) throw err; 15 | }); 16 | }); 17 | let ytDlpWrap; 18 | async function onLoaded() { 19 | try { 20 | await YTDlpWrap.downloadFromGithub(YTDLP_PATH) 21 | ytDlpWrap = new YTDlpWrap(YTDLP_PATH) 22 | pokaLog.logDM("YouTube", "YouTube Loaded"); 23 | return true 24 | } catch (e) { 25 | pokaLog.logDMErr("YouTube", "YouTube Load Failed"); 26 | return false 27 | } 28 | } 29 | async function parseSongs(songs) { 30 | let result = [] 31 | await Promise.all(songs.map(async x => { 32 | let id = x.id.videoId 33 | let metadata = await ytDlpWrap.getVideoInfo(`https://www.youtube.com/watch?v=${id}`); 34 | result.push({ 35 | id, 36 | name: metadata.fulltitle, 37 | artist: metadata.channel, 38 | // artistId: metadata.channel_id, 39 | album: metadata.channel, 40 | cover: x.snippet.thumbnails.url, 41 | url: `/pokaapi/song/?moduleName=YouTube&songId=${x.id.videoId}`, 42 | duration: x.duration_raw, 43 | source: "YouTube" 44 | }) 45 | })) 46 | return result 47 | } 48 | async function search(keyword) { 49 | let res = await yt.search(keyword) 50 | return { songs: await parseSongs(res) } 51 | } 52 | async function getSong(req, songRes = "high", id, res) { 53 | let file 54 | file = fs.readdirSync('./temp').find(x => x.startsWith(id)) 55 | if (!file) { 56 | await new Promise((resolve, reject) => { 57 | let readableStream = ytDlpWrap.execStream([ 58 | `https://www.youtube.com/watch?v=${id}`, 59 | '-f', 60 | 'bestaudio', 61 | ]); 62 | let writeStream = fs.createWriteStream(`./temp/${id}.m4a`); 63 | readableStream 64 | .pipe(writeStream) 65 | .on('close', () => { 66 | resolve() 67 | }) 68 | .on('error', (err) => { 69 | reject(err) 70 | }) 71 | }) 72 | } 73 | res.status(206); 74 | file = fs.readdirSync('./temp').find(x => x.startsWith(id)) 75 | 76 | res.sendFile(path.resolve(`./temp/${file}`)) 77 | return 78 | } 79 | async function getLyric(id) { 80 | let subtitlesDownloadOrder = ['zh-TW', 'zh-Hant', 'zh-Hans', 'zh-HK', 'zh-CN', 'zh-SG', 'zh-MO', 'zh', 'ja', 'en'] 81 | let metadata = await ytDlpWrap.getVideoInfo(`https://www.youtube.com/watch?v=${id}`); 82 | for (let lang of subtitlesDownloadOrder) { 83 | let subtitle = metadata.subtitles[lang] 84 | if (subtitle) { 85 | let json3Url = subtitle.find(x => x.ext == 'json3').url 86 | let json3 = (await axios.get(json3Url)).data 87 | let lrc = json3toLrc(json3) 88 | return lrc 89 | } 90 | } 91 | return 92 | } 93 | function json3toLrc(json3) { 94 | let events = json3.events 95 | let lrc = `` 96 | const timeToTag = seconds => { 97 | let minute = Math.floor(seconds / 60); 98 | let second = seconds - minute * 60 99 | return `${minute}:${second}`; 100 | }; 101 | for (let event of events) { 102 | let time = timeToTag(event.tStartMs / 1000) 103 | lrc += `[${time}]${event.segs[0].utf8}\n` 104 | } 105 | return lrc 106 | } 107 | module.exports = { 108 | name: "YouTube", 109 | enabled: config && config.enabled, 110 | onLoaded, 111 | getSong, 112 | search, 113 | getLyric, 114 | }; 115 | -------------------------------------------------------------------------------- /db/db.js: -------------------------------------------------------------------------------- 1 | /*=======================*/ 2 | /* mongoose */ 3 | /*=======================*/ 4 | const mongoose = require('mongoose'); 5 | let config 6 | try { 7 | config = require('../config.json') 8 | } catch (e) { 9 | config = false 10 | } 11 | const db = mongoose.connection; 12 | mongoose.Promise = global.Promise; 13 | mongoose.set('useCreateIndex', true) 14 | mongoose.connect(config.mongodb, { 15 | useNewUrlParser: true, 16 | useUnifiedTopology: true 17 | }); 18 | 19 | /*=======================*/ 20 | /* session */ 21 | /*=======================*/ 22 | const _session = require('express-session'); 23 | const MongoDBStore = require('connect-mongodb-session')(_session); 24 | const store = new MongoDBStore({ 25 | uri: config.mongodb, 26 | collection: 'Sessions' 27 | }); 28 | const session = _session({ 29 | secret: config ? config.PokaPlayer.sessionSecret : "no config.json", 30 | resave: true, 31 | saveUninitialized: true, 32 | store, 33 | cookie: { 34 | httpOnly: true 35 | } 36 | }) 37 | 38 | module.exports = { 39 | db, 40 | session 41 | } -------------------------------------------------------------------------------- /db/log.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const LogSchema = new mongoose.Schema({ 3 | level: String, 4 | type: String, 5 | event: String, 6 | user: String, 7 | description: String, 8 | time: { 9 | type: Date, 10 | default: Date.now 11 | }, 12 | }); 13 | const model = mongoose.model('Log', LogSchema) 14 | // type: 15 | // - user (login, logout) 16 | // - system 17 | 18 | // level: 19 | // - info 20 | // - warn 21 | // - error 22 | async function addLog({ level, type, event, user = "System", description }) { 23 | await (new model({ 24 | level, type, event, user, description 25 | })).save(err => err ? console.error(err) : null) 26 | } 27 | async function getLogs(limit = 100, page = 0) { 28 | return await model.find({}).limit(limit).sort('-time').skip(limit * page) 29 | } 30 | async function clearLogs() { 31 | return await model.deleteMany({}) 32 | } 33 | module.exports = { 34 | addLog, 35 | getLogs, 36 | clearLogs, 37 | model 38 | } -------------------------------------------------------------------------------- /db/lyric.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const LyricSchema = new mongoose.Schema({ 3 | title: String, 4 | artist: String, 5 | songId: String, 6 | source: String, 7 | lyric: String 8 | }); 9 | LyricSchema.index({ title: 'text', artist: 'text' }) 10 | const model = mongoose.model('Lyric', LyricSchema) 11 | async function saveLyric({ 12 | title, 13 | artist, 14 | songId, 15 | source, 16 | lyric 17 | }) { 18 | let lyricData 19 | lyricData = await model.findOne({ 20 | songId, 21 | source 22 | }) 23 | if (lyricData) { 24 | lyricData.title = title 25 | lyricData.artist = artist 26 | lyricData.lyric = lyric 27 | } else { 28 | lyricData = new model({ 29 | title, 30 | artist, 31 | songId, 32 | source, 33 | lyric 34 | }) 35 | } 36 | await lyricData.save(err => err ? console.error(err) : null) 37 | return ({ 38 | success: true, 39 | data: lyricData 40 | }) 41 | } 42 | async function getLyric(data) { 43 | let result = await model.findOne(data, err => err ? console.error(err) : null) 44 | if (result) 45 | return result.lyric 46 | else 47 | return null 48 | } 49 | async function searchLyric(keyword) { 50 | let result = await model.find({ $text: { $search: keyword } }, err => err ? console.error(err) : null).limit(10) 51 | return result 52 | } 53 | module.exports = { 54 | model, 55 | saveLyric, 56 | getLyric, 57 | searchLyric 58 | } -------------------------------------------------------------------------------- /db/pin.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const PinSchema = new mongoose.Schema({ 3 | name: String, 4 | artist: String, 5 | id: String, 6 | type: String, 7 | cover: String, 8 | source: String, 9 | owner: String 10 | }); 11 | const model = mongoose.model('Pin', PinSchema) 12 | async function addPin({ 13 | name, 14 | artist, 15 | id, 16 | type, 17 | cover, 18 | source, 19 | owner 20 | }) { 21 | let pin = new model({ 22 | name, 23 | artist, 24 | id, 25 | type, 26 | cover, 27 | source, 28 | owner 29 | }) 30 | await pin.save() 31 | return ({ 32 | success: true, 33 | pin 34 | }) 35 | } 36 | async function unPin({ 37 | id, 38 | type, 39 | source, 40 | owner 41 | }) { 42 | try { 43 | await model.deleteOne({ 44 | id, 45 | type, 46 | source, 47 | owner 48 | }) 49 | return ({ 50 | success: true 51 | }) 52 | } 53 | catch (e) { 54 | return ({ 55 | success: false, 56 | error: e 57 | }) 58 | } 59 | } 60 | async function isPinned({ 61 | id, 62 | type, 63 | source, 64 | owner 65 | }) { 66 | return await model.findOne({ 67 | id, 68 | type, 69 | source, 70 | owner 71 | }) 72 | } 73 | async function getPins(userId) { 74 | return (await model.find({ owner: userId })) 75 | } 76 | module.exports = { 77 | getPins, 78 | addPin, 79 | unPin, 80 | isPinned, 81 | model, 82 | } -------------------------------------------------------------------------------- /db/playlist.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const { db } = require('./db.js') 3 | const { encodeURL } = require('../dataModule/cryptoUtils') 4 | const songSchema = new mongoose.Schema({ 5 | name: String, 6 | artist: String, 7 | artistId: String, 8 | album: String, 9 | albumId: String, 10 | cover: String, 11 | url: String, 12 | source: String, 13 | id: String, 14 | }); 15 | const playlistSchema = new mongoose.Schema({ 16 | name: String, 17 | image: String, 18 | owner: String, 19 | pinned: { 20 | type: Boolean, 21 | default: false 22 | }, 23 | source: { 24 | type: String, 25 | default: 'poka' 26 | }, 27 | songs: [songSchema] 28 | }); 29 | 30 | const model = mongoose.model('Playlist', playlistSchema) 31 | 32 | function parsePlaylist(playlist) { 33 | return ({ 34 | songs: playlist.songs || [], 35 | playlists: [{ 36 | name: playlist.name, 37 | source: playlist.source, 38 | id: playlist._id, 39 | cover: `/pokaapi/cover/?moduleName=poka&data=${encodeURL(playlist.image)}` 40 | }] 41 | }) 42 | } 43 | 44 | function parsePlaylists(playlists) { 45 | return playlists.map(x => ({ 46 | name: x.name, 47 | source: x.source, 48 | id: x._id, 49 | cover: `/pokaapi/cover/?moduleName=poka&data=${encodeURL(x.image)}` 50 | })) 51 | } 52 | 53 | async function createPlaylist(name, userId) { 54 | let playlist = new model({ 55 | name, 56 | owner: userId 57 | }) 58 | await playlist.save(err => err ? console.error(err) : null) 59 | return ({ 60 | success: true, 61 | playlist 62 | }) 63 | } 64 | 65 | async function delPlaylist(id) { 66 | try { 67 | await model.deleteOne({ 68 | "_id": id 69 | }) 70 | return ({ 71 | success: true, 72 | error: null 73 | }) 74 | } catch (e) { 75 | return ({ 76 | success: false, 77 | error: e 78 | }) 79 | } 80 | } 81 | async function editPlaylist(id, data) { 82 | try { 83 | let playlist = await getPlaylistById(id) 84 | if (data.name) 85 | playlist.name = data.name 86 | if (data.image) 87 | playlist.image = data.cover 88 | playlist.save() 89 | return ({ 90 | success: true, 91 | error: null, 92 | playlist 93 | }) 94 | } catch (e) { 95 | return ({ 96 | success: false, 97 | error: e 98 | }) 99 | } 100 | } 101 | 102 | async function getPlaylistById(id) { 103 | return (await model.findById(id, err => err ? console.error(err) : null)) 104 | } 105 | async function getParsedUserPlaylistById(id, userId) { 106 | let playlistData = await getPlaylistById(id) 107 | 108 | return playlistData.owner == userId 109 | ? parsePlaylist(playlistData) 110 | : ({ success: false, error: 'Permission Denied' }) 111 | } 112 | async function getPlaylists(userId) { 113 | return (await model.find({ owner: userId })) 114 | } 115 | async function getParsedUserPlaylists(userId) { 116 | return parsePlaylists(await getPlaylists(userId)) 117 | } 118 | async function toggleSongOfPlaylist({ playlistId, song }) { 119 | try { 120 | let playlist = await getPlaylistById(playlistId) 121 | if (playlist.songs.filter(x => x.id == song.id && x.source == song.source).length > 0) { 122 | playlist.songs = playlist.songs.filter(x => x.id != song.id || x.source != song.source) 123 | } else { 124 | playlist.songs.push(song) 125 | } 126 | await playlist.save(err => err ? console.error(err) : null) 127 | return ({ 128 | success: true, 129 | }) 130 | } catch (e) { 131 | return ({ 132 | success: false, 133 | error: e 134 | }) 135 | } 136 | } 137 | async function getAllPlaylists() { 138 | return (await model.find({})) 139 | } 140 | module.exports = { 141 | model, 142 | createPlaylist, 143 | getAllPlaylists, 144 | delPlaylist, 145 | editPlaylist, 146 | getPlaylists, 147 | getParsedUserPlaylistById, 148 | getPlaylistById, 149 | getParsedUserPlaylists, 150 | //song 151 | toggleSongOfPlaylist, 152 | //parse 153 | parsePlaylist, 154 | parsePlaylists 155 | } -------------------------------------------------------------------------------- /db/record.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const RecordSchema = new mongoose.Schema({ 3 | name: String, 4 | cover: String, 5 | url: String, 6 | artist: String, 7 | artistId: String, 8 | album: String, 9 | albumId: String, 10 | songId: String, 11 | source: String, 12 | userId: String, 13 | playedTimes: { type: [Date], index: true }, 14 | }); 15 | const model = mongoose.model('Record', RecordSchema) 16 | async function addRecord({ 17 | name, 18 | cover, 19 | url, 20 | artist, 21 | artistId, 22 | album, 23 | albumId, 24 | songId, 25 | source, 26 | userId 27 | }) { 28 | let recordData 29 | recordData = await model.findOne({ 30 | songId, 31 | source, 32 | userId 33 | }) 34 | if (!recordData) { 35 | recordData = new model({ 36 | name, 37 | cover, 38 | url, 39 | artist, 40 | artistId, 41 | album, 42 | albumId, 43 | songId, 44 | source, 45 | userId 46 | }) 47 | } 48 | recordData.playedTimes.push(Date.now()) 49 | await recordData.save(err => err ? console.error(err) : null) 50 | return ({ 51 | success: true, 52 | data: recordData 53 | }) 54 | } 55 | async function clearUserRecords(userId) { 56 | return await model.deleteMany({ userId }) 57 | } 58 | async function countRecords() { 59 | return (await model.countDocuments({})) 60 | } 61 | async function countUserRecords(userId) { 62 | return (await model.countDocuments({ userId })) 63 | } 64 | async function getReview(userId, year = 2021) { 65 | let res = {} 66 | res.songs = await model.aggregate([ 67 | { 68 | $match: { 69 | userId: userId.toString(), playedTimes: { 70 | $gte: new Date(year, 0, 1), 71 | $lt: new Date(year + 1, 0, 1) 72 | } 73 | } 74 | }, 75 | { $addFields: { count: { $size: '$playedTimes' } } }, 76 | { $sort: { count: -1 } }, 77 | { $limit: 16 } 78 | ]) 79 | res.songs = res.songs.map(x => { 80 | if (!x.name) x.name = x.title // fixed name 81 | x.url = `/pokaapi/song/?moduleName=${x.source}&songId=${x.songId}` 82 | x.id = x.songId 83 | return x 84 | }) 85 | res.artists = await model.aggregate([ 86 | { 87 | $match: { 88 | userId: userId.toString(), playedTimes: { 89 | $gte: new Date(year, 0, 1), 90 | $lt: new Date(year + 1, 0, 1) 91 | } 92 | } 93 | }, 94 | { $addFields: { count: { $size: '$playedTimes' } } }, 95 | { 96 | $group: { 97 | _id: "$artistId", 98 | count: { $sum: "$count" }, 99 | name: { "$first": "$artist" }, 100 | id: { "$first": "$artistId" }, 101 | cover: { "$first": "$cover" }, 102 | source: { "$first": "$source" } 103 | } 104 | }, 105 | { $sort: { count: -1 } }, 106 | { $limit: 12 } 107 | ]) 108 | res.albums = await model.aggregate([ 109 | { 110 | $match: { 111 | userId: userId.toString(), 112 | playedTimes: { 113 | $gte: new Date(year, 0, 1), 114 | $lt: new Date(year + 1, 0, 1) 115 | } 116 | } 117 | }, 118 | { $addFields: { count: { $size: '$playedTimes' } } }, 119 | { 120 | $group: { 121 | _id: "$albumId", 122 | count: { $sum: "$count" }, 123 | name: { "$first": "$album" }, 124 | id: { "$first": "$albumId" }, 125 | artist: { "$first": "$artist" }, 126 | cover: { "$first": "$cover" }, 127 | source: { "$first": "$source" } 128 | } 129 | }, 130 | { $sort: { count: -1 } }, 131 | { $limit: 12 } 132 | ]) 133 | res.days = await model.aggregate([ 134 | { 135 | $match: { 136 | userId: userId.toString(), playedTimes: { 137 | $gte: new Date(year, 0, 1), 138 | $lt: new Date(year + 1, 0, 1) 139 | } 140 | } 141 | }, 142 | { $addFields: { count: { $size: '$playedTimes' } } }, 143 | { $unwind: "$playedTimes" }, 144 | { $addFields: { date: { $dateToString: { format: "%Y-%m-%d", date: "$playedTimes" } } } }, 145 | { $group: { _id: "$date", count: { $sum: 1 }, } }, 146 | { $sort: { count: -1 } }, 147 | { $limit: 12 } 148 | ]) 149 | res.total = await model.countDocuments({ 150 | userId, 151 | playedTimes: { 152 | $gte: new Date(year, 0, 1), 153 | $lt: new Date(year + 1, 0, 1) 154 | } 155 | }) 156 | return res 157 | } 158 | async function fetchListenedRecently(userId) { 159 | let res = (await model.find({ userId })) 160 | let deepcopy = x => JSON.parse(JSON.stringify(x)) 161 | return deepcopy(res) 162 | .map(x => { 163 | x.lastListened = x.playedTimes[x.playedTimes.length - 1] 164 | if (!x.name) x.name = x.title // fixed name 165 | x.url = `/pokaapi/song/?moduleName=${x.source}&songId=${x.songId}` 166 | x.id = x.songId 167 | return x 168 | }) 169 | .sort((a, b) => Date.parse(b.lastListened) - Date.parse(a.lastListened)) 170 | .filter((_, i) => i < 25) 171 | } 172 | async function getAllRecords() { 173 | return (await model.find({})) 174 | } 175 | module.exports = { 176 | model, 177 | addRecord, 178 | clearUserRecords, 179 | countRecords, 180 | countUserRecords, 181 | fetchListenedRecently, 182 | getReview, 183 | getAllRecords, 184 | } -------------------------------------------------------------------------------- /db/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const bcrypt = require('bcryptjs') 3 | const log = require('./log').model 4 | const userSchema = new mongoose.Schema({ 5 | name: String, 6 | username: String, 7 | password: String, 8 | createTime: { 9 | type: Date, 10 | default: Date.now 11 | }, 12 | role: { 13 | type: String, 14 | default: 'user' 15 | }, 16 | settings: { 17 | type: String, 18 | default: `{}` 19 | } 20 | }); 21 | const model = mongoose.model('User', userSchema) 22 | async function create({ 23 | name, 24 | username, 25 | password, 26 | role 27 | }) { 28 | password = bcrypt.hashSync(password, 10) 29 | if (await getUserByUsername(username)) 30 | return ({ 31 | success: false, 32 | error: 'username already taken' 33 | }) 34 | let user = new model({ 35 | name, 36 | username, 37 | password, 38 | role 39 | }) 40 | await user.save() 41 | return ({ 42 | success: true, 43 | user 44 | }) 45 | } 46 | async function changeName(_id, name) { 47 | let user = await getUserById(_id) 48 | if (!user) return { 49 | success: false, 50 | error: 'user not found' 51 | } 52 | user.name = name 53 | await user.save() 54 | return ({ 55 | success: true 56 | }) 57 | } 58 | async function changeUsername(_id, username) { 59 | // is username available 60 | let usernameCheck = await getUserByUsername(username) 61 | if (usernameCheck) return { 62 | success: false, 63 | error: 'username already taken' 64 | } 65 | // change username 66 | let user = await getUserById(_id) 67 | if (!user) return { 68 | success: false, 69 | error: 'user not found' 70 | } 71 | user.username = username 72 | await user.save() 73 | return ({ 74 | success: true 75 | }) 76 | } 77 | async function changePassword(_id, oldpassword, password) { 78 | let user = await getUserById(_id) 79 | if (!user) return { 80 | success: false, 81 | error: 'user not found' 82 | } 83 | 84 | if (comparePassword(oldpassword, user.password)) { 85 | user.password = bcrypt.hashSync(password, 10) 86 | await user.save(err => err ? console.error(err) : null) 87 | return { 88 | success: true, 89 | error: null, 90 | user: user._id 91 | } 92 | } else 93 | return { 94 | success: false, 95 | error: 'password invalid' 96 | } 97 | } 98 | async function changePasswordAdmin(_id, password) { 99 | let user = await getUserById(_id) 100 | if (!user) return { 101 | success: false, 102 | error: 'user not found' 103 | } 104 | user.password = bcrypt.hashSync(password, 10) 105 | await user.save(err => err ? console.error(err) : null) 106 | return { 107 | success: true, 108 | error: null, 109 | user: user._id 110 | } 111 | } 112 | async function getSetting(_id) { 113 | let user = await getUserById(_id) 114 | if (!user) return { 115 | success: false, 116 | error: 'user not found' 117 | } 118 | return ({ 119 | success: true, 120 | settings: JSON.parse(user.settings) 121 | }) 122 | } 123 | async function changeSetting(_id, settings) { 124 | let user = await getUserById(_id) 125 | if (!user) return { 126 | success: false, 127 | error: 'user not found' 128 | } 129 | let s = JSON.parse(user.settings) 130 | for (let i in settings) 131 | s[i] = settings[i] 132 | user.settings = JSON.stringify(s) 133 | await user.save() 134 | return ({ 135 | success: true, 136 | settings: s 137 | }) 138 | } 139 | async function login({ 140 | username, 141 | password 142 | }) { 143 | let user = await getUserByUsername(username) 144 | if (!user) 145 | return { 146 | success: false, 147 | error: 'user not found' 148 | } 149 | if (comparePassword(password, user.password)) 150 | return { 151 | success: true, 152 | error: null, 153 | user: user._id,// TODO: remove 154 | name: user.name, 155 | role: user.role, 156 | id: user._id 157 | } 158 | else 159 | return { 160 | success: false, 161 | error: 'password invalid' 162 | } 163 | } 164 | async function getUserByUsername(username) { 165 | return (await model.findOne({ 166 | username: username 167 | }, err => err ? console.error(err) : null)) 168 | } 169 | async function isUserAdmin(id) { 170 | let userData = await model.findById(id) 171 | return userData && userData.role == 'admin' 172 | } 173 | async function getAllUsers() { 174 | let res = (await model.find({}, err => err ? console.error(err) : null)) 175 | res = JSON.parse(JSON.stringify(res)) 176 | // get last login time 177 | if (res) { 178 | res = res.map(async user => { 179 | try { 180 | user.lastLoginTime = (await log.findOne({ user: user._id, event: 'Login' }).sort({ 'time': -1 }).limit(1)).time 181 | return user 182 | } catch (e) { 183 | user.lastLoginTime = null 184 | return user 185 | } 186 | }) 187 | res = await Promise.all(res) 188 | } 189 | return res 190 | } 191 | async function getUserById(id) { 192 | return (await model.findById(id, err => err ? console.error(err) : null)) 193 | } 194 | async function deleteUserById(_id) { 195 | return (await model.deleteOne({ _id }, err => err ? console.error(err) : null)) 196 | } 197 | 198 | function comparePassword(s, hash) { 199 | return bcrypt.compareSync(s, hash) 200 | } 201 | 202 | module.exports = { 203 | create, 204 | login, 205 | getAllUsers, 206 | getUserByUsername, 207 | getUserById, 208 | getSetting, 209 | changeName, 210 | changeSetting, 211 | changeUsername, 212 | changePassword, 213 | deleteUserById, 214 | changePasswordAdmin, 215 | isUserAdmin 216 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | pokaplayer: 5 | image: gnehs/pokaplayer:master 6 | ports: 7 | - "3000" 8 | networks: 9 | - pokaplayer 10 | depends_on: 11 | - neteaseapi2 12 | - mongodb 13 | volumes: 14 | - ./data/config.json:/app/config.json 15 | restart: always 16 | 17 | neteaseapi2: 18 | build: 19 | context: . 20 | dockerfile: NeteaseAPI2_Dockerfile 21 | networks: 22 | - pokaplayer 23 | 24 | mongodb: 25 | image: mongo:latest 26 | command: --smallfiles 27 | environment: 28 | MONGO_INITDB_ROOT_USERNAME: 29 | MONGO_INITDB_ROOT_PASSWORD: 30 | MONGO_INITDB_DATABASE: 31 | volumes: 32 | - ./data/db:/data/db 33 | - ./data/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro 34 | networks: 35 | - pokaplayer 36 | 37 | networks: 38 | - pokaplayer -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); //檔案系統 2 | const jsonfile = require('jsonfile') 3 | const packageData = require("./package.json"); // package 4 | const { session } = require("./db/db"); // DB 5 | const User = require("./db/user"); // userDB 6 | const pokaLog = require("./log"); // 可愛控制台輸出 7 | const path = require('path'); 8 | const git = require("simple-git")(__dirname); 9 | const child_process = require('child_process'); 10 | //express 11 | const express = require("express"); 12 | const helmet = require("helmet"); 13 | const compression = require('compression') 14 | const app = express(); 15 | const server = require("http").createServer(app) 16 | const io = require("socket.io")(server) 17 | 18 | const { addLog } = require("./db/log"); 19 | const updateDatabase = require("./update-database"); 20 | 21 | 22 | const delay = interval => new Promise(resolve => setTimeout(resolve, interval)); 23 | // 24 | // config init 25 | // 26 | let _c = false 27 | if (fs.existsSync("./config.json")) { 28 | let edited = false 29 | _c = jsonfile.readFileSync("./config.json") 30 | // sessionSecret 31 | if (!_c.PokaPlayer.sessionSecret) { 32 | _c.PokaPlayer.sessionSecret = Math.random().toString(36).substring(7) 33 | edited = true 34 | } 35 | if (!_c.PokaPlayer.sc2tc) { 36 | _c.PokaPlayer.sc2tc = true 37 | edited = true 38 | } 39 | if (!_c.PokaPlayer.fixPunctuation) { 40 | _c.PokaPlayer.fixPunctuation = true 41 | edited = true 42 | } 43 | if (_c.Netease2.login) { 44 | if (_c.Netease2.login.email) { 45 | _c.Netease2.login.method = "email" 46 | _c.Netease2.login.account = _c.Netease2.login.email 47 | delete _c.Netease2.login.email 48 | edited = true 49 | } 50 | if (_c.Netease2.login.phone) { 51 | _c.Netease2.login.method = "phone" 52 | _c.Netease2.login.account = _c.Netease2.login.phone 53 | delete _c.Netease2.login.phone 54 | edited = true 55 | } 56 | } 57 | jsonfile.writeFileSync("./config.json", _c, { 58 | spaces: 4, 59 | EOL: '\r\n' 60 | }) 61 | if (edited) { 62 | //exit 63 | pokaLog.logDB('config', `config changed, restarting`) 64 | process.exit() 65 | } 66 | } 67 | const config = _c; // 設定檔 68 | 69 | // 資料模組 70 | app.use("/pokaapi", require("./dataModule.js")); 71 | 72 | // cors for debug 73 | if (config.PokaPlayer.debug) { 74 | app.use(require('cors')({ 75 | credentials: true, 76 | origin: true 77 | })) 78 | } 79 | 80 | // 檢查 branch 81 | git.raw(["symbolic-ref", "--short", "HEAD"]).then(branch => { 82 | branch = branch.slice(0, -1); // 結果會多一個換行符 83 | if (branch != (config.PokaPlayer.debug ? "dev" : "master")) { 84 | git.fetch(["--all"]) 85 | .then(() => 86 | git.reset(["--hard", "origin/" + (config.PokaPlayer.debug ? "dev" : "master")]) 87 | ) 88 | .then(() => git.checkout(config.PokaPlayer.debug ? "dev" : "master")) 89 | .then(() => process.exit()) 90 | .catch(err => { 91 | console.error("failed: ", err); 92 | socket.emit("err", err.toString()); 93 | }); 94 | } 95 | }); 96 | 97 | // 98 | app.use(express.json()); 99 | app.use(express.static("public")) 100 | app.use(helmet({ contentSecurityPolicy: false })) 101 | app.use(compression()) 102 | // disable X-Powered-By 103 | app.set('x-powered-by', false); 104 | // session 105 | app.use(session) 106 | 107 | // convert a connect middleware to a Socket.IO middleware 108 | const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); 109 | io.use(wrap(session)); 110 | 111 | 112 | app.use(async (req, res, next) => { 113 | if (req.session.user && await User.isUserAdmin(req.session.user)) next() 114 | else res.sendFile(path.join(__dirname + '/public/index.html')) 115 | }); 116 | 117 | 118 | app.use((req, res, next) => { 119 | res.sendFile(path.join(__dirname + '/public/index.html')) 120 | }); 121 | 122 | io.on("connection", socket => { 123 | socket.emit("hello"); 124 | socket.on('send-nickname', nickname => { 125 | socket.nickname = nickname; 126 | }); 127 | // 更新 128 | socket.on("update", async () => { 129 | await git.raw(['config', '--global', 'user.email']).then(r => { 130 | if (r == '\n') { 131 | git.raw(['config', '--global', 'user.email', 'poka@pokaplayer.poka']) 132 | git.raw(['config', '--global', 'user.name', 'pokaUpdater']) 133 | } 134 | }) 135 | if (await User.isUserAdmin(socket.request.session.user)) { 136 | if (!config.PokaPlayer.debug) { 137 | addLog({ 138 | level: "info", 139 | type: "system", 140 | event: "Update", 141 | description: `PokaPlayer update.` 142 | }) 143 | socket.emit("init"); 144 | git.reset(["--hard", "HEAD"]) 145 | .then(() => socket.emit("git", "fetch")) 146 | .then(() => git.remote(["set-url", "origin", "https://github.com/gnehs/PokaPlayer.git"])) 147 | .then(() => git.fetch()) 148 | .then(() => git.pull()) 149 | .then(() => socket.emit("git", "reset")) 150 | .then(() => { 151 | if (process.env.NODE_ENV == 'production') { 152 | child_process.execSync('npm install --production', { stdio: [0, 1, 2], cwd: "/app/" }); 153 | } else { 154 | child_process.execSync('npm install --production', { stdio: [0, 1, 2] }); 155 | } 156 | }) 157 | .then(() => socket.emit("git", "package_updated")) 158 | .then(() => socket.emit("restart")) 159 | .then(async () => { 160 | await delay(3000) 161 | }) 162 | .then(() => process.exit()) 163 | .catch(err => { 164 | console.error("failed: ", err); 165 | socket.emit("err", err.toString()); 166 | }); 167 | } else if (config.PokaPlayer.debug) { 168 | // for ui test 169 | socket.emit("git", "fetch") 170 | await delay(1500) 171 | socket.emit("git", "reset") 172 | await delay(1500) 173 | socket.emit("git", "package_updated") 174 | await delay(1500) 175 | socket.emit("restart") 176 | await delay(3000) 177 | socket.emit("hello"); 178 | } 179 | } 180 | else { 181 | socket.emit("err", "Permission Denied Desu"); 182 | } 183 | }); 184 | socket.on("restart", async () => { 185 | if (await User.isUserAdmin(socket.request.session.user)) { 186 | addLog({ 187 | level: "info", 188 | type: "system", 189 | event: "Restart", 190 | description: `PokaPlayer restart.` 191 | }) 192 | socket.emit("restart") 193 | await delay(3000) 194 | process.exit() 195 | } else { 196 | socket.emit("err", "Permission Denied Desu"); 197 | } 198 | }); 199 | }); 200 | async function pokaStart() { 201 | // 如果資料庫裡沒有使用者自動建立一個 202 | async function autoCreateUser() { 203 | let userlist = await User.getAllUsers() 204 | if (!userlist || userlist.length <= 0) { 205 | let username = "poka", 206 | password = "poka" 207 | User.create({ 208 | name: "Admin", 209 | username, 210 | password, 211 | role: "admin" 212 | }) 213 | pokaLog.logDB('init', `已自動建立使用者`) 214 | pokaLog.logDB('init', `User has been created automatically`) 215 | pokaLog.logDB('init', `username: ${username}`) 216 | pokaLog.logDB('init', `password: ${password}`) 217 | } 218 | } 219 | await autoCreateUser() 220 | await updateDatabase() 221 | 222 | // 啟動囉 223 | server.listen(3000, () => { 224 | pokaLog.log('PokaPlayer', packageData.version) 225 | if (config.PokaPlayer.debug) 226 | pokaLog.log('INFO', 'Debug Mode') 227 | pokaLog.log('INFO', 'http://localhost:3000/') 228 | pokaLog.log('TIME', new Date().toLocaleString()) 229 | if (!config.PokaPlayer.debug) 230 | addLog({ 231 | level: "info", 232 | type: "system", 233 | event: "Start", 234 | description: `PokaPlayer started. version: ${packageData.version}` 235 | }) 236 | }); 237 | } 238 | module.exports = { 239 | pokaStart 240 | } -------------------------------------------------------------------------------- /install/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PokaPlayer 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 |
17 |
18 |
Can not read config.json
19 |

Please refer to the config-simple.json to create the config.json file

20 |
21 | config-simple.json 23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 |
無法讀取 config.json
32 |

請參考 config-simple.json 來建立設定檔

33 | 38 |
39 |
40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /log.js: -------------------------------------------------------------------------------- 1 | let Reset = "\x1b[0m", 2 | Bright = "\x1b[1m", 3 | Dim = "\x1b[2m", 4 | Underscore = "\x1b[4m", 5 | Blink = "\x1b[5m", 6 | Reverse = "\x1b[7m", 7 | Hidden = "\x1b[8m", 8 | FgBlack = "\x1b[30m", 9 | FgRed = "\x1b[31m", 10 | FgGreen = "\x1b[32m", 11 | FgYellow = "\x1b[33m", 12 | FgBlue = "\x1b[34m", 13 | FgMagenta = "\x1b[35m", 14 | FgCyan = "\x1b[36m", 15 | FgWhite = "\x1b[37m", 16 | BgBlack = "\x1b[40m", 17 | BgRed = "\x1b[41m", 18 | BgGreen = "\x1b[42m", 19 | BgYellow = "\x1b[43m", 20 | BgBlue = "\x1b[44m", 21 | BgMagenta = "\x1b[45m", 22 | BgCyan = "\x1b[46m", 23 | BgWhite = "\x1b[47m" 24 | 25 | const log = (a, b) => console.log(`${BgBlue}${FgBlue}[${FgWhite}%s${FgBlue}]${Reset} %s`, a, b) 26 | const logErr = (a, b) => console.log(`${BgBlue}${FgBlue}[${FgWhite}%s${FgBlue}]${Reset} ${FgRed}%s${Reset}`, a, b) 27 | const logDM = (a, b) => console.log(`${BgBlue}${FgBlue}[${FgWhite}DataModules${FgBlue}]${BgGreen}${FgGreen}[${FgWhite}%s${FgGreen}]${Reset} %s`, a, b) 28 | const logDB = (a, b) => console.log(`${BgBlue}${FgBlue}[${FgWhite}DB${FgBlue}]${BgCyan}${FgCyan}[${FgWhite}%s${FgCyan}]${Reset} %s`, a, b) 29 | const logDBErr = (a, b) => console.log(`${BgBlue}${FgBlue}[${FgWhite}DB${FgBlue}]${BgRed}${FgRed}[${FgWhite}%s${FgRed}]${Reset} %s`, a, b) 30 | const logDMErr = (a, b) => console.log(`${BgBlue}${FgBlue}[${FgWhite}DataModules${FgBlue}]${BgRed}${FgRed}[${FgWhite}%s${FgRed}]${Reset} %s`, a, b) 31 | 32 | module.exports = { 33 | log, 34 | logErr, 35 | logDM, logDMErr, 36 | logDB, logDBErr 37 | 38 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokaplayer", 3 | "version": "4.0.8", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon pokaplayer.js -e js --ignore public/ --ignore .git/", 8 | "start": "node pokaplayer.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/gnehs/PokaPlayer.git" 13 | }, 14 | "author": "gnehs, rextw.com", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/gnehs/PokaPlayer/issues" 18 | }, 19 | "homepage": "https://github.com/gnehs/PokaPlayer#readme", 20 | "dependencies": { 21 | "axios": "^0.24.0", 22 | "axios-cookiejar-support": "^2.0.3", 23 | "bcryptjs": "*", 24 | "compression": "^1.7.4", 25 | "connect-mongo": "^4.4.1", 26 | "connect-mongodb-session": "*", 27 | "cors": "*", 28 | "entities": "^3.0.1", 29 | "express": "^4.17.3", 30 | "express-session": "^1.17.1", 31 | "fs-extra": "^9.1.0", 32 | "helmet": "^4.4.1", 33 | "jsonfile": "^6.1.0", 34 | "mongoose": "^5.12.2", 35 | "node-schedule": "^2.0.0", 36 | "opencc-js": "^1.0.5", 37 | "pangu": "^4.0.7", 38 | "qrcode-terminal": "^0.12.0", 39 | "simple-git": "^3.15.0", 40 | "socket.io": "^4.0.0", 41 | "stream-transcoder": "0.0.5", 42 | "tough-cookie": "^4.0.0", 43 | "youtube-search-without-api-key": "^1.0.7", 44 | "yt-dlp-wrap": "^2.3.11" 45 | }, 46 | "devDependencies": { 47 | "chai": "*", 48 | "mocha": "^8.3.2" 49 | } 50 | } -------------------------------------------------------------------------------- /pokaplayer.js: -------------------------------------------------------------------------------- 1 | const pokaLog = require("./log"); // 可愛控制台輸出 2 | // start PokaPlayer 3 | const jsonfile = require('jsonfile') 4 | 5 | let config 6 | try { 7 | config = jsonfile.readFileSync("./config.json") 8 | } catch (e) { 9 | pokaLog.logErr('CONFIG', `config.json 讀取失敗`) 10 | } 11 | if (config) { 12 | const { pokaStart } = require('./index') 13 | pokaStart() 14 | } else { 15 | const express = require("express"); 16 | const app = express(); // Node.js Web 架構 17 | pokaLog.log('INSTALL', `The configuration file has not yet been created`) 18 | pokaLog.log('INSTALL', `Fill out the configuration file according to config-simple.json from the repo`) 19 | pokaLog.log('INSTALL', `config.json 設定教學: https://git.io/PokaConfigChinese`) 20 | app.use(express.static("install")) 21 | const server = require("http").createServer(app) 22 | server.listen(3000, () => { 23 | pokaLog.log('INFO', 'http://localhost:3000/') 24 | }); 25 | } -------------------------------------------------------------------------------- /public/assets/404-9b37a669.js: -------------------------------------------------------------------------------- 1 | import{_ as a,r,o as t,c as n,e as c,b as s,t as l,T as i,f as p,w as _,F as d}from"./index-55e28da3.js";const m={},f=s("i",{class:"bx bx-error"},null,-1);function u(e,h){const o=r("empty-state");return t(),n(d,null,[(t(),c(i,{to:"#header-center"},[s("p",null,l(e.$t("404.title")),1)])),p(o,{title:e.$t("404.title"),description:e.$t("404.description")},{default:_(()=>[f]),_:1},8,["title","description"])],64)}const b=a(m,[["render",u]]);export{b as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Album-7e10ba23.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.header[data-v-7c7a3cc7]{display:flex;gap:calc(var(--padding) * 2);padding-bottom:calc(var(--padding) * 2);margin-bottom:calc(var(--padding) * 2);border-bottom:1px solid var(--background-layer-2)}@media (max-width: 768px){.header[data-v-7c7a3cc7]{flex-direction:column;gap:calc(var(--padding));align-items:center;text-align:center}}.header .cover[data-v-7c7a3cc7]{width:200px;height:200px}.header .cover img[data-v-7c7a3cc7]{width:100%;height:100%;object-fit:cover;aspect-ratio:1/1;border-radius:var(--border-radius);border:1px solid var(--background-layer-2)}.header .album-info[data-v-7c7a3cc7]{display:flex;flex-direction:column;flex:1;width:100%}.header .album-info .title[data-v-7c7a3cc7]{font-size:1.75rem;font-weight:700;margin:calc(var(--padding) / 2) 0}.header .album-info .artist[data-v-7c7a3cc7]{font-size:1.5rem;color:var(--text-color);opacity:.75}.header .album-info .meta[data-v-7c7a3cc7]{display:flex;margin-top:calc(var(--padding))}@media (max-width: 768px){.header .album-info .meta[data-v-7c7a3cc7]{justify-content:center}}.header .album-info .meta span[data-v-7c7a3cc7]{font-size:.75rem;color:var(--text-color);opacity:.5}.header .album-info .meta span[data-v-7c7a3cc7]:not(:last-child):after{content:"\b7";margin:0 calc(var(--padding) / 2)}.header .album-info .actions[data-v-7c7a3cc7]{flex:1;display:flex;align-items:flex-end;gap:var(--padding);margin-top:calc(var(--padding))}@media (max-width: 768px){.header .album-info .actions[data-v-7c7a3cc7]{justify-content:flex-end;flex-wrap:wrap;flex-direction:row}}.header .album-info .actions .spacer[data-v-7c7a3cc7]{flex:1}.p-list-items[data-v-7c7a3cc7]{margin:0 calc(var(--padding) * -1);margin-top:calc(var(--padding) * 2)} 2 | -------------------------------------------------------------------------------- /public/assets/Album-d71314ff.js: -------------------------------------------------------------------------------- 1 | import{_ as P,j as b,u as S,i as w,k as x,r as c,o as _,c as B,e as g,b as e,m as i,t as o,q as l,T as C,f as u,w as d,p as M,g as N}from"./index-55e28da3.js";const p=r=>(M("data-v-7c7a3cc7"),r=r(),N(),r),T={key:0},V=p(()=>e("br",null,null,-1)),j={style:{opacity:"0.5"}},D={class:"header"},L={class:"cover"},q=["src"],E={class:"album-info"},O={class:"title"},R={class:"meta"},z={class:"actions"},F=p(()=>e("i",{class:"bx bx-play"},null,-1)),G=p(()=>e("i",{class:"bx bx-shuffle"},null,-1)),H=p(()=>e("div",{class:"spacer"},null,-1)),J={__name:"Album",setup(r){const m=b("PokaAPI"),v=b("Player"),a=S(),s=w(null);x(async()=>{a.meta.type=="album"&&(s.value=await m.getAlbum(a.params.source,a.params.id)),a.meta.type=="playlist"&&(s.value=await m.getPlaylist(a.params.source,a.params.id))});function y(t=!1){v.audioOrder=t?"random":"list";let n=t?Math.floor(Math.random()*s.value.songs.length):0;v.addSongs({songs:s.value.songs,index:n})}return(t,n)=>{const f=c("router-link"),h=c("p-btn"),$=c("pin-btn"),k=c("parse-songs"),A=c("Loader");return s.value?(_(),B("div",T,[(_(),g(C,{to:"#header-center"},[e("p",null,[i(o(s.value.name??s.value.playlists[0].name)+" ",1),V,e("small",j,o(t.$t(`nav.${l(a).meta.type}s`)),1)])])),e("div",D,[e("div",L,[e("img",{src:s.value.cover??s.value.playlists[0].cover},null,8,q)]),e("div",E,[e("div",O,o(s.value.name??s.value.playlists[0].name),1),u(f,{class:"artist",to:`/ artist / ${l(a).params.source} /${s.value.artistId}`},{default:d(()=>[i(o(s.value.artist),1)]),_:1},8,["to"]),e("div",R,[e("span",null,o(t.$t(`nav.${l(a).meta.type}s`)),1),e("span",null,o(t.$t("songs",s.value.songs.length,{count:s.value.songs.length})),1),e("span",null,o(t.$t(`source.${l(a).params.source}`)),1)]),e("div",z,[u(h,{onClick:n[0]||(n[0]=I=>y())},{default:d(()=>[F,i(" "+o(t.$t("album.playAll")),1)]),_:1}),u(h,{outline:"",onClick:n[1]||(n[1]=I=>y(!0))},{default:d(()=>[G,i(" "+o(t.$t("album.playAllShuffle")),1)]),_:1}),H,u($,{name:s.value.name??s.value.playlists[0].name,cover:s.value.cover??s.value.playlists[0].cover,artist:s.value.artist,id:l(a).params.id,source:l(a).params.source,type:l(a).meta.type},null,8,["name","cover","artist","id","source","type"])])])]),u(k,{items:s.value.songs},null,8,["items"])])):(_(),g(A,{key:1}))}}},Q=P(J,[["__scopeId","data-v-7c7a3cc7"]]);export{Q as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Albums-51ec468d.js: -------------------------------------------------------------------------------- 1 | import{j as k,u as f,i as p,k as h,l as A,r as l,o as s,c as m,e as c,m as w,t as u,b as d,q as r,T as _,f as g,s as I}from"./index-55e28da3.js";const $={key:0},B=d("br",null,null,-1),N={style:{opacity:"0.5"}},P={key:1},C={__name:"Albums",setup(V){const o=k("PokaAPI"),e=f(),n=p(null),t=p(null);h(async()=>{await i()}),A(()=>e.path,async()=>{await i()});async function i(){let a;switch(n.value=null,e.meta.type){case"artists":a=await o.getArtistAlbums(e.params.source,e.meta.type,e.params.id),t.value=await o.getArtistInfo(e.params.source,"artist",e.params.id);break;case"composers":a=await o.getArtistAlbums(e.params.source,e.meta.type,e.params.id),t.value=await o.getArtistInfo(e.params.source,"composer",e.params.id);break;case"albums":a=await o.getAlbums();break}n.value=a==null?void 0:a.albums}return(a,T)=>{const y=l("pin-btn"),v=l("Loader"),b=l("parse-albums");return s(),m("div",null,[(s(),c(_,{to:"#header-center"},[t.value?(s(),m("p",$,[w(u(t.value.name)+" ",1),B,d("small",N,u(a.$t(`nav.${r(e).meta.type}`)),1)])):(s(),m("p",P,u(a.$t(`nav.${r(e).meta.type}`)),1))])),t.value?(s(),c(_,{key:0,to:"#header-actions"},[g(y,{name:t.value.name,cover:t.value.cover,id:r(e).params.id,source:r(e).params.source,type:r(e).meta.type=="artists"?"artist":"composer"},null,8,["name","cover","id","source","type"])])):I("",!0),n.value?(s(),c(b,{key:2,items:n.value},null,8,["items"])):(s(),c(v,{key:1}))])}}};export{C as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Artists-de6da841.js: -------------------------------------------------------------------------------- 1 | import{u as i,j as m,i as _,k as y,l as d,r as n,o as a,c as k,e as s,b as f,t as v,q as r,T as g,F as w}from"./index-55e28da3.js";const h={__name:"Artists",setup(A){const e=i(),c=m("PokaAPI"),t=_(null);async function o(){t.value=null,t.value=await c[e.meta.type=="artists"?"getArtists":"getComposers"]()}return y(async()=>{await o()}),d(e,async()=>{await o()},{deep:!0}),(l,B)=>{const p=n("Loader"),u=n("parse-artists");return a(),k(w,null,[(a(),s(g,{to:"#header-center"},[f("p",null,v(l.$t(`nav.${r(e).meta.type}`)),1)])),t.value?(a(),s(u,{key:1,items:t.value,type:r(e).meta.type},null,8,["items","type"])):(a(),s(p,{key:0}))],64)}}};export{h as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Folders-941ca665.js: -------------------------------------------------------------------------------- 1 | import{u as b,j as B,i as F,k as P,l as C,r as s,o as e,c as n,e as _,b as i,t as d,T as L,f as c,s as v,w as p,F as h,d as N}from"./index-55e28da3.js";const V={key:0},j={key:0},x={key:1},A=i("i",{class:"bx bx-folder"},null,-1),T={__name:"Folders",setup(D){const u=b(),g=B("PokaAPI"),t=F(null);P(async()=>{await m()}),C(()=>u.path,async()=>{await m()});async function m(){var a,l;let o=(a=u.params)==null?void 0:a.source,f=(l=u.params)==null?void 0:l.id;t.value=null,t.value=await g.getFolders(o,f)}return(o,f)=>{const a=s("parse-songs"),l=s("p-list-item-icon-btn"),k=s("p-list-item-content"),y=s("p-list-item"),w=s("p-list-items"),$=s("Loader");return e(),n(h,null,[(e(),_(L,{to:"#header-center"},[i("p",null,d(o.$t("nav.folders")),1)])),t.value?(e(),n("div",V,[t.value.songs.length?(e(),n("div",j,[i("h4",null,d(o.$t("nav.songs")),1),c(a,{items:t.value.songs},null,8,["items"])])):v("",!0),t.value.folders.length?(e(),n("div",x,[i("h4",null,d(o.$t("nav.folders")),1),c(w,null,{default:p(()=>[(e(!0),n(h,null,N(t.value.folders,r=>(e(),_(y,{to:`/folder/${r.source}/${r.id}`},{default:p(()=>[c(l,null,{default:p(()=>[A]),_:1}),c(k,{title:r.name,description:r.source},null,8,["title","description"])]),_:2},1032,["to"]))),256))]),_:1})])):v("",!0)])):(e(),_($,{key:1}))],64)}}};export{T as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Index-77de2a6a.js: -------------------------------------------------------------------------------- 1 | import{y as V,z as k,r as a,o as u,c as r,e as q,b as i,t as _,T as B,f as t,w as s,q as c,A as L,F as m,d as S,s as C}from"./index-55e28da3.js";import{u as I}from"./user-8fc11de1.js";const N={class:"setting-item"},T={class:"content"},w={class:"title"},z=i("div",{class:"description"},null,-1),F={class:"control"},R=["value"],U=i("i",{class:"bx bx-music"},null,-1),A=i("i",{class:"bx bx-brush-alt"},null,-1),D=i("i",{class:"bx bx-pin"},null,-1),E=i("i",{class:"bx bx-user"},null,-1),M=i("i",{class:"bx bx-server"},null,-1),j=i("i",{class:"bx bx-group"},null,-1),G=i("i",{class:"bx bx-file"},null,-1),O={__name:"Index",setup(H){const{locale:p,availableLocales:b,getLocaleMessage:f}=V({inheritLocale:!0,useScope:"global"}),h=I(),{userInfo:$}=k(h);return(e,g)=>{const v=a("p-select"),n=a("p-list-item-icon-btn"),l=a("p-list-item-content"),o=a("p-list-item"),x=a("p-list-items");return u(),r(m,null,[(u(),q(B,{to:"#header-center"},[i("p",null,_(e.$t("nav.settings")),1)])),i("div",N,[i("div",T,[i("div",w,_(e.$t("language")),1),z]),i("div",F,[t(v,{modelValue:c(p),"onUpdate:modelValue":g[0]||(g[0]=d=>L(p)?p.value=d:null)},{default:s(()=>[(u(!0),r(m,null,S(c(b),d=>(u(),r("option",{value:d},_(c(f)(d).language_name({normalize:y=>y[0]})),9,R))),256))]),_:1},8,["modelValue"])])]),i("div",null,[t(x,null,{default:s(()=>[t(o,{to:"/settings/quality",tabindex:"0"},{default:s(()=>[t(n,null,{default:s(()=>[U]),_:1}),t(l,{title:e.$t("settings.quality.title"),description:e.$t("settings.quality.description")},null,8,["title","description"])]),_:1}),t(o,{to:"/settings/theme",tabindex:"0"},{default:s(()=>[t(n,null,{default:s(()=>[A]),_:1}),t(l,{title:e.$t("settings.theme.title"),description:e.$t("settings.theme.description")},null,8,["title","description"])]),_:1}),t(o,{to:"/settings/pins",tabindex:"0"},{default:s(()=>[t(n,null,{default:s(()=>[D]),_:1}),t(l,{title:e.$t("settings.pins.title"),description:e.$t("settings.pins.description")},null,8,["title","description"])]),_:1}),t(o,{to:"/settings/user",tabindex:"0"},{default:s(()=>[t(n,null,{default:s(()=>[E]),_:1}),t(l,{title:e.$t("settings.user.title"),description:e.$t("settings.user.description")},null,8,["title","description"])]),_:1}),c($).role==="admin"?(u(),r(m,{key:0},[t(o,{to:"/settings/system",tabindex:"0"},{default:s(()=>[t(n,null,{default:s(()=>[M]),_:1}),t(l,{title:e.$t("settings.system.title"),description:e.$t("settings.system.description")},null,8,["title","description"])]),_:1}),t(o,{to:"/settings/users",tabindex:"0"},{default:s(()=>[t(n,null,{default:s(()=>[j]),_:1}),t(l,{title:e.$t("settings.users.title"),description:e.$t("settings.users.description")},null,8,["title","description"])]),_:1}),t(o,{to:"/settings/log",tabindex:"0"},{default:s(()=>[t(n,null,{default:s(()=>[G]),_:1}),t(l,{title:e.$t("settings.log.title"),description:e.$t("settings.log.description")},null,8,["title","description"])]),_:1})],64)):C("",!0)]),_:1})])],64)}}};export{O as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Library-6e5b1b31.js: -------------------------------------------------------------------------------- 1 | import{j as l,i as m,k as p,r as t,o as e,c as i,e as o,b as u,t as _,T as k}from"./index-55e28da3.js";const h={__name:"Library",setup(d){const n=l("PokaAPI"),a=m(null);return p(async()=>{a.value=await n.getHome()}),(s,v)=>{const r=t("parse-home"),c=t("Loader");return e(),i("div",null,[(e(),o(k,{to:"#header-center"},[u("p",null,_(s.$t("nav.library")),1)])),a.value?(e(),o(r,{key:0,items:a.value},null,8,["items"])):(e(),o(c,{key:1}))])}}};export{h as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Log-6cc3aa27.js: -------------------------------------------------------------------------------- 1 | import{_ as h,j as B,i as o,J as x,k as I,r as P,o as i,c as b,e as f,b as m,t as y,T as C,s as E,F as j}from"./index-55e28da3.js";const A={__name:"Log",setup(D){const _=B("PokaAPI"),t=o(""),L=o(0),g=o(!1),a=o([]);async function p(){let e=await _.getUserList();a.value=e}async function v(e=0){var s;(!a.value||!a.value.length)&&await p();let n=await _.getLog(e);for(let{level:k,type:w,event:$,user:l,description:c,time:u}of n)l=((s=a.value.find(r=>r._id==l))==null?void 0:s.username)||l,u=new Date(u).toLocaleString(),a.value.map(r=>{c=c.replace(new RegExp(`{${r._id}}`,"g"),r.username)}),t.value+=`[${k}] ${w} / ${$} 2 | `,t.value+=` 📄 ${c} 3 | `,t.value+=` 👤 ${l} 4 | `,t.value+=` 🕒 ${u} 5 | `,t.value+=` 6 | `;n.length||(g.value=!0)}const d=o(null);return x(d,async([{isIntersecting:e}],n)=>{e&&await v(L.value++)},{threshold:.5,rootMargin:"0px"}),I(async()=>{await p(),await v()}),(e,n)=>{const s=P("Loader");return i(),b(j,null,[(i(),f(C,{to:"#header-center"},[m("p",null,y(e.$t("settings.log.title")),1)])),m("pre",{class:"log",ref:"logContainer"},y(t.value),513),g.value?E("",!0):(i(),f(s,{key:0,ref_key:"logBottom",ref:d},null,512))],64)}}},M=h(A,[["__scopeId","data-v-640354d8"]]);export{M as default}; 7 | -------------------------------------------------------------------------------- /public/assets/Log-c9db7119.css: -------------------------------------------------------------------------------- 1 | pre.log[data-v-640354d8]{overflow-x:auto;padding:var(--padding);white-space:pre-wrap;word-break:break-all} 2 | -------------------------------------------------------------------------------- /public/assets/Login-0c932826.js: -------------------------------------------------------------------------------- 1 | import{_ as c,j as u,r as i,o as m,c as _,b as a,x as g,f as n,w as f,m as w,t as b,p as h,g as v}from"./index-55e28da3.js";const y="/img/icon.svg";const V={name:"LoginDialog",setup(){return{socket:u("socket")}},data(){return{username:localStorage.getItem("username")||"",password:localStorage.getItem("password")||""}},methods:{async login(){(await this.$PokaAPI.login(this.username,this.password)).success&&this.$router.push("/")}}},p=e=>(h("data-v-1e5115e3"),e=e(),v(),e),k={class:"login-container"},I={class:"login-form"},S=p(()=>a("img",{class:"logo",src:y,alt:"logo"},null,-1)),x=p(()=>a("h1",null,"PokaPlayer",-1));function P(e,o,B,L,t,l){const r=i("p-input"),d=i("p-btn");return m(),_("div",k,[a("div",I,[S,x,a("form",{onSubmit:o[2]||(o[2]=g((...s)=>l.login&&l.login(...s),["prevent"]))},[n(r,{label:e.$t("username"),modelValue:t.username,"onUpdate:modelValue":o[0]||(o[0]=s=>t.username=s),required:""},null,8,["label","modelValue"]),n(r,{label:e.$t("password"),modelValue:t.password,"onUpdate:modelValue":o[1]||(o[1]=s=>t.password=s),type:"password",required:""},null,8,["label","modelValue"]),n(d,{type:"submit",block:"",style:{"margin-top":"calc(var(--padding) * 2)"},color:"primary"},{default:f(()=>[w(b(e.$t("login")),1)]),_:1})],32)])])}const $=c(V,[["render",P],["__scopeId","data-v-1e5115e3"]]);export{$ as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Login-65f28d2c.css: -------------------------------------------------------------------------------- 1 | .login-container[data-v-1e5115e3]{display:flex;justify-content:center;align-items:center;height:100vh;background-color:var(--background-layer-2)}.login-container .login-form[data-v-1e5115e3]{display:flex;flex-direction:column;align-items:center;background-color:var(--background-layer-1);padding:calc(var(--padding) * 2);border-radius:var(--border-radius)}.login-container .login-form .logo[data-v-1e5115e3]{--size: 72px;width:var(--size);height:var(--size);margin-top:calc(-1 * var(--size) / 2)}.login-container .login-form h1[data-v-1e5115e3]{font-size:24px;text-align:left;font-family:Product Sans,sans-serif;margin-bottom:calc(var(--padding) * 4)}.login-container .login-form form[data-v-1e5115e3]{display:flex;flex-direction:column;align-items:center;width:400px;max-width:calc(100vw - 80px)} 2 | -------------------------------------------------------------------------------- /public/assets/Pins-0ce5b192.js: -------------------------------------------------------------------------------- 1 | import{j as y,i as b,k as v,r as e,o as s,c as p,e as _,b as a,t as u,T as P,f as i,w as o,F as m,d as B,x as C}from"./index-55e28da3.js";const x={style:{"margin-bottom":"calc(var(--padding) * 2)"}},A=a("i",{class:"bx bx-trash"},null,-1),I={__name:"Pins",setup(j){const c=y("PokaAPI"),l=b(null);async function r(){let t=await c.getPins();l.value=t}async function d(t){window.confirm("Are you sure you want to unpin this item?")&&(await c.unpin(t),await r())}return v(async()=>{await r()}),(t,f)=>{const g=e("p-list-item-img"),w=e("p-list-item-content"),$=e("p-list-item-icon-btn"),h=e("p-list-item"),k=e("p-list-items");return s(),p(m,null,[(s(),_(P,{to:"#header-center"},[a("p",null,u(t.$t("settings.pins.title")),1)])),a("p",x,u(t.$t("settings.pins.intro")),1),i(k,null,{default:o(()=>[(s(!0),p(m,null,B(l.value,n=>(s(),_(h,{tabindex:"0"},{actions:o(()=>[i($,{onClick:C(D=>d(n),["stop"])},{default:o(()=>[A]),_:2},1032,["onClick"])]),default:o(()=>[i(g,{src:n.cover},null,8,["src"]),i(w,{title:n.name,description:`${t.$t(`nav.${n.type}s`)} | ${t.$t(`source.${n.source}`)}`},null,8,["title","description"])]),_:2},1024))),256))]),_:1})],64)}}};export{I as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Playlists-35b3be64.js: -------------------------------------------------------------------------------- 1 | import{j as B,u as C,i as L,k as N,l as V,r as a,o as s,c as l,e as _,b as o,t as d,T as j,f as n,s as f,w as u,F as g,d as A}from"./index-55e28da3.js";const D={key:0},I={key:0},T={style:{"margin-bottom":"calc(var(--padding) * 2)"}},x={key:1},E={style:{margin:"calc(var(--padding) * 2) 0"}},M=o("i",{class:"bx bx-folder"},null,-1),q={__name:"Playlists",setup(R){const k=B("PokaAPI"),i=C(),t=L(null);N(async()=>{await m()}),V(()=>i.path,async()=>{await m()});async function m(){var c;t.value=null;let e=await k.getPlaylists();(c=i.params)!=null&&c.id&&(e=e.playlistFolders.filter(r=>r.id==i.params.id)[0]),t.value=e}return(e,c)=>{var y,v,h;const r=a("parse-playlists"),$=a("p-list-item-icon-btn"),b=a("p-list-item-content"),w=a("p-list-item"),P=a("p-list-items"),F=a("Loader");return s(),l(g,null,[(s(),_(j,{to:"#header-center"},[o("p",null,d(((y=t.value)==null?void 0:y.name)||e.$t("nav.playlists")),1)])),t.value?(s(),l("div",D,[(v=t.value.playlists)!=null&&v.length?(s(),l("div",I,[o("h4",T,d(t.value.name||e.$t("nav.playlists")),1),n(r,{items:t.value.playlists},null,8,["items"])])):f("",!0),(h=t.value.playlistFolders)!=null&&h.length?(s(),l("div",x,[o("h4",E,d(e.$t("nav.folders")),1),n(P,null,{default:u(()=>[(s(!0),l(g,null,A(t.value.playlistFolders,p=>(s(),_(w,{to:`/playlists/folder/${p.id}`},{default:u(()=>[n($,null,{default:u(()=>[M]),_:1}),n(b,{title:p.name,description:e.$t(`source.${p.source}`)},null,8,["title","description"])]),_:2},1032,["to"]))),256))]),_:1})])):f("",!0)])):(s(),_(F,{key:1}))],64)}}};export{q as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Quality-316f457e.js: -------------------------------------------------------------------------------- 1 | import{B as m,r as n,o as t,c as s,e as d,b as k,t as b,T as g,f as i,w as l,F as c,d as h,q as y}from"./index-55e28da3.js";const f={key:0,class:"bx bx-checkbox-checked"},x={key:1,class:"bx bx-checkbox"},C={__name:"Quality",setup(q){const a=m("poka.quality","original");return(o,$)=>{const r=n("p-list-item-icon-btn"),_=n("p-list-item-content"),p=n("p-list-item"),u=n("p-list-items");return t(),s(c,null,[(t(),d(g,{to:"#header-center"},[k("p",null,b(o.$t("settings.quality.title")),1)])),i(u,{"single-row":""},{default:l(()=>[(t(),s(c,null,h(["low","medium","high","original"],e=>i(p,{tabindex:"0",onClick:B=>a.value=e},{default:l(()=>[i(r,null,{default:l(()=>[y(a)==e?(t(),s("i",f)):(t(),s("i",x))]),_:2},1024),i(_,{title:o.$t(`settings.quality.${e}.title`),description:o.$t(`settings.quality.${e}.description`)},null,8,["title","description"])]),_:2},1032,["onClick"])),64))]),_:1})],64)}}};export{C as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Search-1a5f5fd1.js: -------------------------------------------------------------------------------- 1 | import{_ as C,j as N,u as R,v as j,i as p,k as A,r as a,o as t,c as l,b as u,f as o,w as F,x as L,e as M,s as n,F as D,t as c,p as E,g as U}from"./index-55e28da3.js";const z=_=>(E("data-v-35d51b69"),_=_(),U(),_),G=["onSubmit"],H=z(()=>u("i",{class:"bx bx-search"},null,-1)),J={key:0,class:"search__result"},K={key:1,class:"search__result"},O={key:2,class:"search__result"},Q={key:3,class:"search__result"},T={key:4,class:"search__result"},W={__name:"Search",setup(_){const k=N("PokaAPI"),S=R(),w=j(),r=p(""),e=p(null),i=p(!1);A(async()=>{let s=S.query.q;s&&(r.value=s,await m())});async function m(){r.value!=""&&(i.value=!0,w.replace({path:"/search",query:{q:r.value}}),e.value=null,e.value=await k.search(r.value),i.value=!1)}return(s,v)=>{var h,y,b,g,f;const $=a("p-input"),I=a("p-btn"),V=a("Loader"),q=a("parse-songs"),B=a("parse-albums"),d=a("parse-artists"),P=a("parse-playlists");return t(),l("div",null,[u("form",{class:"search__input",onSubmit:L(m,["prevent"])},[o($,{modelValue:r.value,"onUpdate:modelValue":v[0]||(v[0]=x=>r.value=x)},null,8,["modelValue"]),o(I,{type:"submit"},{default:F(()=>[H]),_:1})],40,G),i.value?(t(),M(V,{key:0})):n("",!0),e.value?(t(),l(D,{key:1},[(h=e.value.songs)!=null&&h.length?(t(),l("div",J,[u("h3",null,c(s.$t("nav.songs")),1),o(q,{items:e.value.songs},null,8,["items"])])):n("",!0),(y=e.value.albums)!=null&&y.length?(t(),l("div",K,[u("h3",null,c(s.$t("nav.albums")),1),o(B,{items:e.value.albums},null,8,["items"])])):n("",!0),(b=e.value.artists)!=null&&b.length?(t(),l("div",O,[u("h3",null,c(s.$t("nav.artists")),1),o(d,{type:"artists",items:e.value.artists},null,8,["items"])])):n("",!0),(g=e.value.composers)!=null&&g.length?(t(),l("div",Q,[u("h3",null,c(s.$t("nav.composers")),1),o(d,{type:"composers",items:e.value.composers},null,8,["items"])])):n("",!0),(f=e.value.playlists)!=null&&f.length?(t(),l("div",T,[u("h3",null,c(s.$t("nav.playlists")),1),o(P,{items:e.value.playlists},null,8,["items"])])):n("",!0)],64)):n("",!0)])}}},Y=C(W,[["__scopeId","data-v-35d51b69"]]);export{Y as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Search-f3c2d4ea.css: -------------------------------------------------------------------------------- 1 | .search__input[data-v-35d51b69]{display:flex;gap:var(--padding);max-width:500px;margin:0 auto}.search__input .p-input[data-v-35d51b69]{flex:1}.search__result[data-v-35d51b69]{margin:calc(var(--padding) * 6) 0}.search__result h3[data-v-35d51b69]{margin-bottom:var(--padding)} 2 | -------------------------------------------------------------------------------- /public/assets/System-38da5b99.js: -------------------------------------------------------------------------------- 1 | import{j as w,i as f,I as T,k as N,r as c,o as x,c as R,e as j,b as i,t as v,T as B,f as e,w as a,F,q as P,m as b}from"./index-55e28da3.js";const $=i("i",{class:"bx bx-edit"},null,-1),E=i("i",{class:"bx bx-revision"},null,-1),q=i("i",{class:"bx bx-cloud-upload"},null,-1),z={style:{margin:"var(--padding) 0"}},M={style:{margin:"var(--padding) 0"}},G={style:{display:"flex","justify-content":"flex-end","margin-top":"var(--padding)",gap:"var(--padding)"}},H={style:{margin:"var(--padding) 0","text-align":"center"}},O={__name:"System",setup(J){const V=w("PokaAPI"),n=w("socket"),g=f(null),r=f(null),p=f(!1),u=f(!1),o=f("Updating..."),k=T(()=>g.value&&r.value?g.value.version!=r.value.tag_name:!1);async function D(){const s=await V.getSystemInfo();g.value=s}async function S(){p.value=!1,u.value=!0,n.emit("update"),n.on("Permission Denied Desu",()=>{u.value=!1,alert("Permission Denied")}),n.on("init",()=>{o.value="Initializing..."}),n.on("git",s=>{o.value={fetch:"Fetching...",reset:"Resetting...",package_updated:"Package updated..."}[s]}),n.on("restart",()=>{o.value="Restarting..."}),n.on("hello",()=>{o.value="System updated!",setTimeout(()=>{location.reload()},1e3)}),n.on("err",async s=>{const t=l=>new Promise(d=>{setTimeout(d,l)});o.value="An error occurred, please check console.",console.error(s),await t(1e3),u.value=!1})}async function U(){!window.confirm("Are you sure you want to restart the system?")||(n.emit("restart"),u.value=!0,o.value="Loading...",n.on("restart",()=>{o.value="Restarting..."}),n.on("hello",()=>{o.value="System restarted!",setTimeout(()=>{location.reload()},1e3)}),n.on("err",async t=>{const l=d=>new Promise(m=>{setTimeout(m,d)});o.value="An error occurred, please check console.",console.error(t),await l(1e3),u.value=!1}))}async function C(){await D();let{debug:s}=g.value,t=await fetch("https://api.github.com/repos/gnehs/PokaPlayer/releases").then(l=>l.json());r.value=t.filter(l=>s||!l.prerelease)[0]}function A(){alert("Not available yet")}return N(async()=>{await C()}),(s,t)=>{const l=c("p-list-item-icon-btn"),d=c("p-list-item-content"),m=c("p-list-item"),I=c("p-list-items"),y=c("p-btn"),h=c("Dialog"),L=c("Loader");return x(),R(F,null,[(x(),j(B,{to:"#header-center"},[i("p",null,v(s.$t("settings.system.title")),1)])),e(I,{"single-row":""},{default:a(()=>[e(m,{tabindex:"0",onClick:A},{default:a(()=>[e(l,null,{default:a(()=>[$]),_:1}),e(d,{title:"Edit config"})]),_:1}),e(m,{tabindex:"0",onClick:U},{default:a(()=>[e(l,null,{default:a(()=>[E]),_:1}),e(d,{title:"Restart"})]),_:1}),e(m,{tabindex:"0",onClick:t[0]||(t[0]=_=>P(k)&&(p.value=!0))},{default:a(()=>[e(l,null,{default:a(()=>[q]),_:1}),e(d,{title:"Update PokaPlayer",description:r.value?P(k)?"New update available":"Up to date":"Loading..."},null,8,["description"])]),_:1})]),_:1}),e(h,{modelValue:p.value,"onUpdate:modelValue":t[2]||(t[2]=_=>p.value=_)},{default:a(()=>[i("h2",null,"Update PokaPlayer to "+v(r.value.tag_name),1),i("p",z,v(r.value.body),1),i("p",M,"Release date: "+v(new Date(r.value.published_at).toLocaleString()),1),e(y,{href:r.value.html_url,target:"_blank",rel:"noopener noreferrer",outlined:""},{default:a(()=>[b("View release")]),_:1},8,["href"]),i("div",G,[e(y,{onClick:t[1]||(t[1]=_=>p.value=!1),outlined:""},{default:a(()=>[b("Cancel")]),_:1}),e(y,{onClick:S,color:"primary"},{default:a(()=>[b("Update")]),_:1})])]),_:1},8,["modelValue"]),e(h,{modelValue:u.value,"onUpdate:modelValue":t[3]||(t[3]=_=>u.value=_),closeable:!1},{default:a(()=>[e(L,{style:{margin:"calc(var(--padding) * 4) 0"}}),i("p",H,v(o.value),1)]),_:1},8,["modelValue"])],64)}}};export{O as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Theme-08e39d68.css: -------------------------------------------------------------------------------- 1 | input[type=color]{-webkit-appearance:none;border:none;width:32px;height:32px;border-radius:var(--border-radius);overflow:hidden;transition:all var(--transition);cursor:pointer;position:relative}input[type=color]:before{content:"\ea07";position:absolute;font-family:boxicons!important;font-size:16px;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;z-index:1;filter:drop-shadow(0 0 1px rgba(0,0,0,.1))}input[type=color]:hover{transform:scale(1.1)}input[type=color]:focus{outline:none;transform:scale(1.1);box-shadow:var(--box-shadow)}input[type=color]::-webkit-color-swatch-wrapper{padding:0;transform:scale(2)}input[type=color]::-webkit-color-swatch{border:none}.theme-preview-items{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--min-card-width),1fr));grid-gap:calc(var(--padding) * 2);margin:calc(var(--padding) * 2) 0}.theme-preview-items .theme-preview-item{display:grid;grid-template-columns:1fr 1fr;justify-content:center;border-radius:var(--border-radius);overflow:hidden;background-color:var(--background-layer-2);color:rgba(var(--text-color-value),1);border:1px solid var(--background-layer-2);transition:transform var(--transition);cursor:pointer}.theme-preview-items .theme-preview-item .layer-1,.theme-preview-items .theme-preview-item .layer-2{padding:var(--padding);height:64px;display:flex;align-items:center;justify-content:center;font-size:18px}.theme-preview-items .theme-preview-item .layer-1{background-color:var(--background-layer-1);font-size:24px}.theme-preview-items .theme-preview-item .layer-2{background-color:var(--background-layer-2)}.theme-preview-items .theme-preview-item:hover{transform:scale(1.05)}.theme-preview-items .theme-preview-item:active{transform:scale(1)} 2 | -------------------------------------------------------------------------------- /public/assets/Theme-217bd9f0.js: -------------------------------------------------------------------------------- 1 | import{B as T,i as b,l as k,r as g,o as d,c as r,e as S,b as e,t as n,T as U,F as m,d as V,q as u,s as $,f as c,w as p,C as _,D as y,E,G as B,H as j}from"./index-55e28da3.js";const P={style:{"margin-bottom":"var(--padding)"}},A={class:"theme-preview-items"},D=["onClick"],N={class:"layer-1"},O={class:"bx bx-check"},q=e("div",{class:"layer-2"}," Aa ",-1),F={class:"layer-1"},z={key:0,class:"bx bx-check"},G={class:"layer-2"},H={class:"setting-item"},L={class:"content"},M={class:"title"},R={class:"control"},W=e("optgroup",{label:"🌑"},null,-1),I=e("option",{value:"0,0,0"},"0 ",-1),J=e("option",{value:"25,25,25"},"25 ",-1),K={value:"51,51,51"},Q=e("option",{value:"200,200,200"},"200",-1),X=e("option",{value:"230,230,230"},"230 ",-1),Y=e("option",{value:"255,255,255"},"255 ",-1),Z=e("optgroup",{label:"☀️"},null,-1),ee={class:"setting-item"},te={class:"content"},oe={class:"title"},se={class:"control"},le={style:{"margin-bottom":"var(--padding)"}},ne={style:{"margin-bottom":"var(--padding)"}},ae={class:"setting-item"},ie={class:"content"},de={class:"title"},re={class:"control"},ce=e("option",{value:"72px"},"72px ",-1),ue=e("option",{value:"96px"},"96px ",-1),pe={value:"128px"},me=e("option",{value:"160px"},"160px ",-1),_e=e("option",{value:"192px"},"192px ",-1),ve={class:"setting-item"},he={class:"content"},ge={class:"title"},ye={class:"control"},fe=e("option",{value:"4px"},"4px ",-1),xe=e("option",{value:"8px"},"8px ",-1),be={value:"12px"},ke=e("option",{value:"16px"},"16px ",-1),Ve=e("option",{value:"24px"},"24px ",-1),$e={class:"setting-item"},we={class:"content"},Ce={class:"title"},Te={class:"control"},Se=e("option",{value:"4px"},"4px ",-1),Ue={value:"8px"},Ee=e("option",{value:"10px"},"10px ",-1),Be=e("option",{value:"12px"},"12px ",-1),je=e("option",{value:"16px"},"16px ",-1),De={__name:"Theme",setup(Pe){const a=T("poka.theme",{theme:"light",cssText:""}),f=b({light:{"--background-layer-1":"#ffffff","--background-layer-2":"#f2f2f2","--text-color-value":"51,51,51"},dark:{"--background-layer-1":"#1e1e1e","--background-layer-2":"#2e2e2e","--text-color-value":"255,255,255"},black:{"--background-layer-1":"#0b0b0b","--background-layer-2":"#000000","--text-color-value":"255,255,255"},ocean:{"--background-layer-1":"#393644","--background-layer-2":"#302e38","--text-color-value":"230,230,255"}}),s=b({"--border-radius":"12px","--padding":"8px","--min-card-width":"128px","--primary-color":"#007bff","--background-layer-1":"#ffffff","--background-layer-2":"#f8f9fa","--text-color-value":"51,51,51"});for(let t in s.value)s.value[t]=document.documentElement.style.getPropertyValue(t)||s.value[t];return k(a,(t,o)=>{let i=f.value[t.theme];if(i)for(let[v,h]of Object.entries(i))s.value[v]=h}),k(s,t=>{for(let o in t)document.documentElement.style.setProperty(o,t[o]);j(()=>{a.value.cssText=document.documentElement.style.cssText;let o=document.querySelector('meta[name="theme-color"]');o&&o.setAttribute("content",getComputedStyle(document.documentElement).getPropertyValue("--background-layer-1"))})},{deep:!0}),(t,o)=>{const i=g("p-select"),v=g("p-card"),h=g("p-cards");return d(),r(m,null,[(d(),S(U,{to:"#header-center"},[e("p",null,n(t.$t("settings.theme.title")),1)])),e("h4",P,n(t.$t("settings.theme.themeAndColor")),1),e("div",A,[(d(!0),r(m,null,V(Object.entries(f.value),([l,w])=>(d(),r("div",{class:"theme-preview-item",onClick:x=>u(a).theme=l,style:E(Object.entries(w).map(([x,C])=>`${x}:${C}`).join(";")),tabindex:"0"},[e("div",N,[_(e("i",O,null,512),[[B,u(a).theme==l]])]),q],12,D))),256)),e("div",{class:"theme-preview-item",onClick:o[0]||(o[0]=l=>u(a).theme="custom"),tabindex:"0"},[e("div",F,[u(a).theme=="custom"?(d(),r("i",z)):$("",!0)]),e("div",G,n(t.$t("settings.theme.custom")),1)])]),u(a).theme=="custom"?(d(),r(m,{key:0},[e("div",H,[e("div",L,[e("div",M,n(t.$t("settings.theme.textColor")),1)]),e("div",R,[c(i,{modelValue:s.value["--text-color-value"],"onUpdate:modelValue":o[1]||(o[1]=l=>s.value["--text-color-value"]=l)},{default:p(()=>[W,I,J,e("option",K,"51 ("+n(t.$t("settings.theme.default"))+") ",1),Q,X,Y,Z]),_:1},8,["modelValue"])])]),e("div",ee,[e("div",te,[e("div",oe,n(t.$t("settings.theme.color")),1)]),e("div",se,[_(e("input",{type:"color","onUpdate:modelValue":o[2]||(o[2]=l=>s.value["--primary-color"]=l)},null,512),[[y,s.value["--primary-color"]]]),_(e("input",{type:"color","onUpdate:modelValue":o[3]||(o[3]=l=>s.value["--background-layer-1"]=l)},null,512),[[y,s.value["--background-layer-1"]]]),_(e("input",{type:"color","onUpdate:modelValue":o[4]||(o[4]=l=>s.value["--background-layer-2"]=l)},null,512),[[y,s.value["--background-layer-2"]]])])])],64)):$("",!0),e("h4",le,n(t.$t("settings.theme.preview")),1),c(h,{style:{margin:"calc(var(--padding) * 2) 0"}},{default:p(()=>[(d(),r(m,null,V(4,l=>c(v,{imgSrc:"/img/pwa-512x512.png",title:t.$t("settings.theme.preview"),source:t.$t("settings.theme.preview")},null,8,["title","source"])),64))]),_:1}),e("h4",ne,n(t.$t("settings.theme.style")),1),e("div",ae,[e("div",ie,[e("div",de,n(t.$t("settings.theme.cardWidth")),1)]),e("div",re,[c(i,{modelValue:s.value["--min-card-width"],"onUpdate:modelValue":o[5]||(o[5]=l=>s.value["--min-card-width"]=l)},{default:p(()=>[ce,ue,e("option",pe,"128px ("+n(t.$t("settings.theme.default"))+") ",1),me,_e]),_:1},8,["modelValue"])])]),e("div",ve,[e("div",he,[e("div",ge,n(t.$t("settings.theme.borderRadius")),1)]),e("div",ye,[c(i,{modelValue:s.value["--border-radius"],"onUpdate:modelValue":o[6]||(o[6]=l=>s.value["--border-radius"]=l)},{default:p(()=>[fe,xe,e("option",be,"12px ("+n(t.$t("settings.theme.default"))+")",1),ke,Ve]),_:1},8,["modelValue"])])]),e("div",$e,[e("div",we,[e("div",Ce,n(t.$t("settings.theme.padding")),1)]),e("div",Te,[c(i,{modelValue:s.value["--padding"],"onUpdate:modelValue":o[7]||(o[7]=l=>s.value["--padding"]=l)},{default:p(()=>[Se,e("option",Ue,"8px ("+n(t.$t("settings.theme.default"))+")",1),Ee,Be,je]),_:1},8,["modelValue"])])])],64)}}};export{De as default}; 2 | -------------------------------------------------------------------------------- /public/assets/User-abf1ad8a.js: -------------------------------------------------------------------------------- 1 | import{z as j,j as F,i as u,r as P,o as k,c as b,e as I,b as e,t as l,T as B,q as v,f as o,w as n,s as S,F as A,m as d}from"./index-55e28da3.js";import{u as q}from"./user-8fc11de1.js";const z={key:0},E={class:"setting-item"},L={class:"content"},R={class:"title"},G={class:"description"},H={class:"control"},J={class:"setting-item"},K={class:"content"},M={class:"title"},O={class:"description"},Q={class:"control"},W={class:"setting-item"},X={class:"content"},Y={class:"title"},Z={class:"control"},x={class:"setting-item"},ee={class:"content"},se={class:"title"},te={class:"description"},le={class:"setting-item"},ae={class:"content"},oe={class:"title"},ne={class:"description"},ie={class:"setting-item"},de={class:"content"},ue={class:"title"},re={class:"description"},ve={style:{display:"flex","justify-content":"flex-end","margin-top":"var(--padding)",gap:"var(--padding)"}},me={style:{display:"flex","justify-content":"flex-end","margin-top":"var(--padding)",gap:"var(--padding)"}},ce={style:{display:"flex","justify-content":"flex-end","margin-top":"var(--padding)",gap:"var(--padding)"}},_e={__name:"User",setup(pe){const C=q(),{userInfo:r}=j(C),f=F("PokaAPI"),m=u(!1),$=u(""),c=u(!1),V=u(""),p=u(!1),w=u(""),_=u(""),U=u("");async function h(){r.value=await f.getUserInfo()}async function N(){await f.changeUserName($.value),m.value=!1,await h()}async function D(){await f.changeUserUsername(V.value),c.value=!1,await h()}async function T(){if(_.value!==U.value){window.alert("Password not match");return}await f.changeUserPassword(w.value,_.value),p.value=!1}return(t,s)=>{const i=P("p-btn"),g=P("p-input"),y=P("Dialog");return k(),b(A,null,[(k(),I(B,{to:"#header-center"},[e("p",null,l(t.$t("settings.user.title")),1)])),v(r)?(k(),b("div",z,[e("div",E,[e("div",L,[e("div",R,l(t.$t("settings.user.name")),1),e("div",G,l(v(r).name),1)]),e("div",H,[o(i,{onClick:s[0]||(s[0]=a=>m.value=!0)},{default:n(()=>[d(l(t.$t("settings.user.edit")),1)]),_:1})])]),e("div",J,[e("div",K,[e("div",M,l(t.$t("settings.user.username")),1),e("div",O,l(v(r).username),1)]),e("div",Q,[o(i,{onClick:s[1]||(s[1]=a=>c.value=!0)},{default:n(()=>[d(l(t.$t("settings.user.edit")),1)]),_:1})])]),e("div",W,[e("div",X,[e("div",Y,l(t.$t("settings.user.password")),1)]),e("div",Z,[o(i,{onClick:s[2]||(s[2]=a=>p.value=!0)},{default:n(()=>[d(l(t.$t("settings.user.changePassword")),1)]),_:1})])]),e("div",x,[e("div",ee,[e("div",se,l(t.$t("settings.user.id")),1),e("div",te,l(v(r)._id),1)])]),e("div",le,[e("div",ae,[e("div",oe,l(t.$t("settings.user.role")),1),e("div",ne,l(v(r).role),1)])]),e("div",ie,[e("div",de,[e("div",ue,l(t.$t("settings.user.createTime")),1),e("div",re,l(new Date(v(r).createTime).toLocaleString()),1)])])])):S("",!0),o(y,{modelValue:m.value,"onUpdate:modelValue":s[5]||(s[5]=a=>m.value=a)},{default:n(()=>[e("h3",null,l(t.$t("settings.user.name")),1),o(g,{label:t.$t("settings.user.name"),modelValue:$.value,"onUpdate:modelValue":s[3]||(s[3]=a=>$.value=a)},null,8,["label","modelValue"]),e("div",ve,[o(i,{onClick:s[4]||(s[4]=a=>m.value=!1)},{default:n(()=>[d(l(t.$t("cancel")),1)]),_:1}),o(i,{onClick:N,color:"primary"},{default:n(()=>[d(l(t.$t("save")),1)]),_:1})])]),_:1},8,["modelValue"]),o(y,{modelValue:c.value,"onUpdate:modelValue":s[8]||(s[8]=a=>c.value=a)},{default:n(()=>[e("h3",null,l(t.$t("settings.user.username")),1),o(g,{label:t.$t("settings.user.username"),modelValue:V.value,"onUpdate:modelValue":s[6]||(s[6]=a=>V.value=a)},null,8,["label","modelValue"]),e("div",me,[o(i,{onClick:s[7]||(s[7]=a=>c.value=!1)},{default:n(()=>[d(l(t.$t("cancel")),1)]),_:1}),o(i,{onClick:D,color:"primary"},{default:n(()=>[d(l(t.$t("save")),1)]),_:1})])]),_:1},8,["modelValue"]),o(y,{modelValue:p.value,"onUpdate:modelValue":s[13]||(s[13]=a=>p.value=a)},{default:n(()=>[e("h3",null,l(t.$t("settings.user.changePassword")),1),o(g,{label:t.$t("settings.user.oldPassword"),modelValue:w.value,"onUpdate:modelValue":s[9]||(s[9]=a=>w.value=a)},null,8,["label","modelValue"]),o(g,{label:t.$t("settings.user.newPassword"),modelValue:_.value,"onUpdate:modelValue":s[10]||(s[10]=a=>_.value=a)},null,8,["label","modelValue"]),o(g,{label:t.$t("settings.user.confirmPassword"),modelValue:U.value,"onUpdate:modelValue":s[11]||(s[11]=a=>U.value=a)},null,8,["label","modelValue"]),e("div",ce,[o(i,{onClick:s[12]||(s[12]=a=>p.value=!1)},{default:n(()=>[d(l(t.$t("cancel")),1)]),_:1}),o(i,{onClick:T,color:"primary"},{default:n(()=>[d(l(t.$t("save")),1)]),_:1})])]),_:1},8,["modelValue"])],64)}}};export{_e as default}; 2 | -------------------------------------------------------------------------------- /public/assets/Users-15669eb8.js: -------------------------------------------------------------------------------- 1 | import{j as I,i as g,I as j,k as E,r as d,o as p,c as P,e as c,b as s,t as U,T as D,f as e,w as l,s as L,q as R,A as S,F as $,m as w,d as h}from"./index-55e28da3.js";const F=s("i",{class:"bx bx-edit"},null,-1),q=s("h3",null,"Create user",-1),M=s("p",null,"Create a new user",-1),z=s("option",{value:"user"},"User",-1),G=s("option",{value:"admin"},"Admin",-1),H={style:{display:"flex","justify-content":"flex-end","margin-top":"var(--padding)",gap:"var(--padding)"}},J=s("i",{class:"bx bx-lock-alt"},null,-1),K=s("i",{class:"bx bx-trash-alt"},null,-1),O={style:{display:"flex","justify-content":"flex-end","margin-top":"var(--padding)"}},Y={__name:"Users",setup(Q){const _=I("PokaAPI"),y=g(null),n=g(null),i=g({name:"",username:"",password:"",role:"user"}),f=j({get:()=>!!n.value,set:()=>n.value=null}),m=g(!1);async function b(){let o=await _.getUserList();y.value=o}async function N(){let o=prompt("New password");if(o){let t=await _.changeUserPasswordById(n.value._id,o);t.success?alert("Password changed"):alert("Error: "+t.error)}}async function T(){prompt("Are you sure you want to delete this user? Type 'yes' to confirm")=="yes"&&((await _.deleteUser(n.value._id)).ok?await b():alert("Error"))}async function A(){let o=await _.createUser(i.value);o.success?(await b(),m.value=!1):alert("Error: "+o.error)}return E(async()=>{await b()}),(o,t)=>{const v=d("p-btn"),u=d("p-list-item-content"),V=d("p-list-item-icon-btn"),r=d("p-list-item"),k=d("p-list-items"),x=d("p-input"),B=d("p-select"),C=d("Dialog");return p(),P($,null,[(p(),c(D,{to:"#header-center"},[s("p",null,U(o.$t("settings.users.title")),1)])),(p(),c(D,{to:"#header-actions"},[e(v,{onClick:t[0]||(t[0]=a=>m.value=!0),outline:""},{default:l(()=>[w("Create")]),_:1})])),y.value?(p(),c(k,{key:0},{default:l(()=>[(p(!0),P($,null,h(y.value,a=>(p(),c(r,{tabindex:0},{actions:l(()=>[s("span",null,U(a.role),1),e(V,{onClick:W=>n.value=a},{default:l(()=>[F]),_:2},1032,["onClick"])]),default:l(()=>[e(u,{title:a.name,description:a.username},null,8,["title","description"])]),_:2},1024))),256))]),_:1})):L("",!0),e(C,{modelValue:m.value,"onUpdate:modelValue":t[6]||(t[6]=a=>m.value=a)},{default:l(()=>[q,M,e(x,{modelValue:i.value.name,"onUpdate:modelValue":t[1]||(t[1]=a=>i.value.name=a),label:"Name"},null,8,["modelValue"]),e(x,{modelValue:i.value.username,"onUpdate:modelValue":t[2]||(t[2]=a=>i.value.username=a),label:"Username"},null,8,["modelValue"]),e(x,{modelValue:i.value.password,"onUpdate:modelValue":t[3]||(t[3]=a=>i.value.password=a),label:"Password",type:"password"},null,8,["modelValue"]),e(B,{modelValue:i.value.role,"onUpdate:modelValue":t[4]||(t[4]=a=>i.value.role=a),label:"Role",style:{"margin-top":"var(--padding)"}},{default:l(()=>[z,G]),_:1},8,["modelValue"]),s("div",H,[e(v,{onClick:t[5]||(t[5]=a=>m.value=!1)},{default:l(()=>[w("Cancel")]),_:1}),e(v,{onClick:A,color:"primary"},{default:l(()=>[w("Create")]),_:1})])]),_:1},8,["modelValue"]),e(C,{modelValue:R(f),"onUpdate:modelValue":t[8]||(t[8]=a=>S(f)?f.value=a:null)},{default:l(()=>[n.value?(p(),c(k,{key:0,"single-row":""},{default:l(()=>[e(r,{tabindex:"0"},{default:l(()=>[e(u,{title:n.value.name,description:n.value.username},null,8,["title","description"])]),_:1}),e(r,{tabindex:"0"},{default:l(()=>[e(u,{title:n.value._id,description:"ID"},null,8,["title"])]),_:1}),e(r,{tabindex:"0"},{default:l(()=>[e(u,{title:n.value.role,description:"Role"},null,8,["title"])]),_:1}),e(r,{tabindex:"0"},{default:l(()=>[e(u,{title:new Date(n.value.createTime).toLocaleString(),description:"Create time"},null,8,["title"])]),_:1}),e(r,{tabindex:"0"},{default:l(()=>[e(u,{title:new Date(n.value.lastLoginTime).toLocaleString(),description:"Last login time"},null,8,["title"])]),_:1}),e(r,{tabindex:"0",onClick:N},{default:l(()=>[e(V,null,{default:l(()=>[J]),_:1}),e(u,{title:o.$t("settings.user.changePassword")},null,8,["title"])]),_:1}),e(r,{tabindex:"0",onClick:T},{default:l(()=>[e(V,null,{default:l(()=>[K]),_:1}),e(u,{title:"Delete user"})]),_:1})]),_:1})):L("",!0),s("div",O,[e(v,{onClick:t[7]||(t[7]=a=>f.value=!1),color:"primary"},{default:l(()=>[w(U(o.$t("close")),1)]),_:1})])]),_:1},8,["modelValue"])],64)}}};export{Y as default}; 2 | -------------------------------------------------------------------------------- /public/assets/default-8fdf2100.js: -------------------------------------------------------------------------------- 1 | import{u as f}from"./user-8fc11de1.js";import{_ as y,R as g,a as k,r as a,o as s,c as i,b as e,F as u,d as _,e as n,f as S,w as m,n as p,t as h,p as I,g as w}from"./index-55e28da3.js";const $={name:"DefaultLayout",setup(){return{userStore:f()}},components:{RouterView:g,RouterLink:k},data(){return{starting:!0,actions:[{icon:"bx-cog",text:"settings",to:"/settings"}],nav:[{icon:"bx-library",text:"library",to:"/"},{icon:"bx-search",text:"search",to:"/search"},{icon:"bx-album",text:"albums",to:"/albums"},{icon:"bx-folder",text:"folders",to:"/folders"},{icon:"bx-microphone",text:"artists",to:"/artists"},{icon:"bxs-piano",text:"composers",to:"/composers"},{icon:"bxs-playlist",text:"playlists",to:"/playlists"}]}},mounted(){this.start(),this.loadTheme()},methods:{async start(){try{let t=await this.$PokaAPI.getUserInfo();this.starting=!1,this.userStore.setUserInfo(t)}catch(t){console.log(t),this.$router.push("/login")}},loadTheme(){let t=localStorage.getItem("poka.theme");if(t){document.documentElement.style.cssText=JSON.parse(t).cssText;let c=document.querySelector('meta[name="theme-color"]');c&&c.setAttribute("content",getComputedStyle(document.documentElement).getPropertyValue("--background-layer-1"))}}}},l=t=>(I("data-v-9033cb87"),t=t(),w(),t),V={class:"default-layout-container"},C={class:"header"},L=l(()=>e("div",{class:"logo"}," PokaPlayer ",-1)),P=l(()=>e("div",{class:"header-center",id:"header-center"},null,-1)),R={class:"header-actions"},B=l(()=>e("div",{id:"header-actions"},null,-1)),T={class:"nav-item-text"},E={class:"nav"},N={class:"nav-item-text"},U={class:"main"},A={class:"player"};function D(t,c,F,q,r,z){const d=a("router-link"),v=a("Loader"),x=a("RouterView"),b=a("bottom-player");return s(),i("div",V,[e("div",C,[L,P,e("div",R,[B,(s(!0),i(u,null,_(r.actions,o=>(s(),n(d,{class:"nav-item",to:o.to},{default:m(()=>[e("i",{class:p(["nav-item-icon bx",o.icon])},null,2),e("div",T,h(t.$t(`nav.${o.text}`)),1)]),_:2},1032,["to"]))),256))])]),e("div",E,[(s(!0),i(u,null,_(r.nav,o=>(s(),n(d,{class:"nav-item",to:o.to},{default:m(()=>[e("i",{class:p(["nav-item-icon bx",o.icon])},null,2),e("div",N,h(t.$t(`nav.${o.text}`)),1)]),_:2},1032,["to"]))),256))]),e("div",U,[r.starting?(s(),n(v,{key:0})):(s(),n(x,{key:1}))]),e("div",A,[S(b)])])}const j=y($,[["render",D],["__scopeId","data-v-9033cb87"]]);export{j as default}; 2 | -------------------------------------------------------------------------------- /public/assets/default-ae053ae9.css: -------------------------------------------------------------------------------- 1 | .default-layout-container[data-v-9033cb87]{width:100svw;height:100svh;background-color:var(--background-layer-2);display:grid;grid-template-columns:calc(var(--padding) * 6 + 24px) 1fr;grid-template-rows:auto 1fr;grid-template-areas:"header header" "nav main" "player player"}@media (max-width: 768px){.default-layout-container[data-v-9033cb87]{grid-template-columns:calc(var(--padding) * 3.5 + 24px) 1fr}}.default-layout-container .header[data-v-9033cb87]{grid-area:header;display:grid;grid-template-columns:200px 1fr 200px}@media (max-width: 768px){.default-layout-container .header[data-v-9033cb87]{grid-template-columns:auto 1fr}}.default-layout-container .header .logo[data-v-9033cb87]{padding:calc(var(--padding) * 2);font-family:Product Sans,sans-serif;font-weight:700;font-size:24px}.default-layout-container .header .header-center[data-v-9033cb87]{display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px;text-align:center}@media (max-width: 768px){.default-layout-container .header .header-center[data-v-9033cb87]{display:none}}.default-layout-container .header .header-actions[data-v-9033cb87]{display:flex;align-items:center;justify-content:flex-end;gap:calc(var(--padding) / 2)}.default-layout-container .header .header-actions #header-actions[data-v-9033cb87]{display:flex;align-items:center;gap:calc(var(--padding) / 2)}.default-layout-container .header .header-actions .nav-item[data-v-9033cb87]{margin-top:0}.default-layout-container .header .header-actions .nav-item .nav-item-text[data-v-9033cb87]{left:initial;top:calc(var(--padding) * 8);right:0;transform:scale(.75) translateY(calc(var(--padding) * -5))}.default-layout-container .header .header-actions .nav-item:hover .nav-item-text[data-v-9033cb87]{transform:none}.default-layout-container .nav[data-v-9033cb87]{grid-area:nav;padding:0 var(--padding)}@media (max-width: 768px){.default-layout-container .nav[data-v-9033cb87]{padding:0 calc(var(--padding) * .25)}}.default-layout-container .nav-item[data-v-9033cb87]{padding:calc(var(--padding) * 2);border-radius:var(--border-radius);color:var(--text-color);text-decoration:none;display:flex;align-items:center;justify-content:center;position:relative;transition:all var(--transition);z-index:1}@media (max-height: 768px){.default-layout-container .nav-item[data-v-9033cb87]{padding:calc(var(--padding) * 1.5)}}.default-layout-container .nav-item[data-v-9033cb87]:not(:first-child){margin-top:calc(var(--padding) / 2)}.default-layout-container .nav-item .nav-item-icon[data-v-9033cb87]{font-size:24px}.default-layout-container .nav-item .nav-item-text[data-v-9033cb87]{opacity:0;position:absolute;left:calc(var(--padding) * 7.5);transition:all var(--transition);transform:scale(.75) translate(calc(var(--padding) * -5));background-color:var(--background-layer-1);border:var(--border-width) solid var(--border-color);padding:var(--padding) calc(var(--padding) * 2);box-shadow:var(--box-shadow);border-radius:var(--border-radius);pointer-events:none;white-space:nowrap}.default-layout-container .nav-item[data-v-9033cb87]:hover{background-color:rgba(var(--text-color-value),.075);outline:0}.default-layout-container .nav-item:hover .nav-item-text[data-v-9033cb87]{opacity:1;filter:blur(0);transform:none}.default-layout-container .nav-item:hover .nav-item-icon.bx-cog[data-v-9033cb87]{transform:rotate(60deg);transition:all var(--transition)}.default-layout-container .nav-item[data-v-9033cb87]:active{background-color:rgba(var(--text-color-value),.15);transform:scale(.95)}@media (max-width: 768px){.default-layout-container .nav-item[data-v-9033cb87]{padding:calc(var(--padding) * 1.5)}.default-layout-container .nav-item .nav-item-text[data-v-9033cb87]{left:calc(var(--padding) * 7)}.default-layout-container .nav-item .nav-item-icon[data-v-9033cb87]{margin-right:0}}.default-layout-container .nav-item.router-link-exact-active[data-v-9033cb87]{background-color:rgba(var(--text-color-value),.15);transform:none}.default-layout-container .nav-item.router-link-exact-active .nav-item-text[data-v-9033cb87]{transition-delay:.4s;opacity:0;transform:scale(.75) translate(calc(var(--padding) * -5))}.default-layout-container .main[data-v-9033cb87]{grid-area:main;background-color:var(--background-layer-1);border-top-left-radius:var(--border-radius);border-bottom-left-radius:var(--border-radius);padding:calc(var(--padding) * 4);overflow-y:scroll}.default-layout-container .main[data-v-9033cb87]::-webkit-scrollbar{width:4px}.default-layout-container .main[data-v-9033cb87]::-webkit-scrollbar-track{background:transparent}.default-layout-container .main[data-v-9033cb87]::-webkit-scrollbar-thumb{background:#888;border-radius:2px}.default-layout-container .main[data-v-9033cb87]::-webkit-scrollbar-thumb:hover{background:#777}.default-layout-container .main[data-v-9033cb87]::-webkit-scrollbar-thumb:active{background:#666}.default-layout-container .main[data-v-9033cb87]::-webkit-scrollbar-button{display:none}@media (max-width: 768px){.default-layout-container .main[data-v-9033cb87]{padding:calc(var(--padding) * 2)}}.default-layout-container .player[data-v-9033cb87]{grid-area:player;background-color:var(--background-layer-2)} 2 | -------------------------------------------------------------------------------- /public/assets/empty-4a05e738.js: -------------------------------------------------------------------------------- 1 | import{o as e,e as r,q as t,R as a}from"./index-55e28da3.js";const n={__name:"empty",setup(o){return(s,c)=>(e(),r(t(a)))}};export{n as default}; 2 | -------------------------------------------------------------------------------- /public/assets/index-d8bacfc1.css: -------------------------------------------------------------------------------- 1 | .bottom-player[data-v-62673e53]{padding:calc(var(--padding) / 2) var(--padding);display:grid;grid-template-columns:1fr 250px 1fr}@media screen and (max-width: 768px){.bottom-player[data-v-62673e53]{grid-template-columns:1fr auto}.bottom-player .player-control[data-v-62673e53]{display:none}}.bottom-player .track-info[data-v-62673e53]{display:flex;align-items:center;width:100%;cursor:pointer}.bottom-player .track-info .cover[data-v-62673e53]{align-items:center;--cover-size: calc(var(--padding) * 5 + 24px);width:var(--cover-size);height:var(--cover-size);display:flex;flex-direction:column;justify-content:center}@media (max-width: 768px){.bottom-player .track-info .cover[data-v-62673e53]{--cover-size: calc(var(--padding) * 3.5 + 24px)}}.bottom-player .track-info .cover img[data-v-62673e53]{width:var(--cover-size);height:var(--cover-size);border-radius:var(--border-radius);object-fit:cover;background-color:#fff}.bottom-player .track-info .track-info-text[data-v-62673e53]{margin-left:var(--padding);font-size:18px;width:100%;max-width:calc(50vw - 125px - 64px - var(--padding) * 2);overflow:hidden;flex:1}@media (max-width: 768px){.bottom-player .track-info .track-info-text[data-v-62673e53]{font-size:16px;max-width:calc(100vw - 134px - 52px - var(--padding) * 2)}}.bottom-player .track-info .track-info-text .track-artist[data-v-62673e53],.bottom-player .track-info .track-info-text .track-name[data-v-62673e53]{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%}.bottom-player .track-info .track-info-text .track-name[data-v-62673e53]{font-weight:700}.bottom-player .track-info .track-info-text .track-artist[data-v-62673e53]{color:var(--text-color);opacity:.75}.bottom-player .track-control[data-v-62673e53]{display:flex;align-items:center;justify-content:center;gap:calc(var(--padding) / 2)}@media (max-width: 768px){.bottom-player .track-control .time[data-v-62673e53]{display:none}}.bottom-player .player-control[data-v-62673e53]{display:flex;align-items:center;justify-content:flex-end;gap:calc(var(--padding) / 2)}@media (max-width: 768px){.bottom-player .player-control[data-v-62673e53]{display:none}}.modal-mask{position:fixed;z-index:9998;top:0;left:0;width:100%;height:100%;overflow:hidden;background-color:#000000bf;display:table;transition:opacity var(--transition);font-size:14px;cursor:pointer}.modal-wrapper{display:grid;place-content:center;height:100vh;height:100svh;line-break:anywhere}.modal-container{max-height:calc(100svh - 60px);overflow-y:auto;overflow-x:hidden;cursor:default;width:600px;max-width:var(--max-width);margin:0 auto;padding:calc(var(--padding) * 2);background-color:var(--background-layer-1);border-radius:var(--border-radius);box-shadow:var(--box-shadow);transition:all var(--transition);position:sticky;top:0;position:relative}.modal-container .close{position:sticky;cursor:pointer;--size: 36px;transition:all .2s ease;font-size:var(--size);z-index:1;height:var(--size);display:flex;justify-content:flex-end;align-items:center;margin-bottom:calc(var(--size) * -1)}.modal-container .close i{background:#333;color:#fff;border-radius:100em}@media screen and (max-width: 768px){.modal-container .close{--size: 24px}}.modal-container .close:hover{cursor:pointer;opacity:.8}.modal-container .close:active{opacity:.6}.modal-enter-from,.modal-leave-to{opacity:0}.modal-enter-active .modal-container,.modal-leave-active .modal-container{transform:translateY(100px);opacity:0}.empty-state[data-v-11011440]{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:var(--text-color)}.empty-state .empty-state__icon[data-v-11011440]{font-size:3rem;margin-bottom:var(--padding)}.empty-state .empty-state__title[data-v-11011440]{font-size:1.5rem;margin-bottom:var(--padding)}.empty-state .empty-state__description[data-v-11011440]{font-size:1rem;color:var(--text-color);opacity:.75}.fullscreen-player[data-v-17ca1e16]{position:fixed;top:0;left:0;width:100vw;height:100vh;height:100svh;background-color:var(--background-layer-2);z-index:1;transition:transform var(--transition),border-radius var(--transition);overflow:hidden}.fullscreen-player[data-v-17ca1e16]:not(.show){transform:translateY(100vh);border-radius:var(--border-radius)}.fullscreen-player .fullscreen-player__container[data-v-17ca1e16]{width:min(1200px,100vw - var(--padding) * 2);padding:var(--padding) calc(var(--padding) * 2);height:100vh;margin:0 auto}.fullscreen-player .fullscreen-player__container .header[data-v-17ca1e16]{display:flex;align-items:center}.fullscreen-player .fullscreen-player__container .header .left[data-v-17ca1e16]{flex:1}.fullscreen-player .fullscreen-player__container .header .left .logo[data-v-17ca1e16]{font-family:Product Sans,sans-serif;font-weight:700;font-size:24px}.fullscreen-player .fullscreen-player__container .content[data-v-17ca1e16]{display:grid;grid-template-columns:400px 1fr;align-items:center;gap:calc(var(--padding) * 4);margin:calc(var(--padding) * 1) 0;height:calc(100svh - var(--padding) * 4 - 84px)}@media screen and (max-width: 768px){.fullscreen-player .fullscreen-player__container .content[data-v-17ca1e16]{grid-template-columns:1fr}}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16],.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]{overflow:hidden;overflow-y:auto}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]::-webkit-scrollbar,.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]::-webkit-scrollbar{width:4px}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]::-webkit-scrollbar-track,.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]::-webkit-scrollbar-track{background:transparent}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]::-webkit-scrollbar-thumb,.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]::-webkit-scrollbar-thumb{background:#888;border-radius:2px}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]::-webkit-scrollbar-thumb:hover,.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]::-webkit-scrollbar-thumb:hover{background:#777}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]::-webkit-scrollbar-thumb:active,.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]::-webkit-scrollbar-thumb:active{background:#666}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]::-webkit-scrollbar-button,.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]::-webkit-scrollbar-button{display:none}.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]{max-height:100%}@media screen and (max-width: 768px){.fullscreen-player .fullscreen-player__container .content .left[data-v-17ca1e16]{display:none}}.fullscreen-player .fullscreen-player__container .content .right[data-v-17ca1e16]{padding:calc(var(--padding) * 2);background-color:var(--background-layer-1);border-radius:var(--border-radius);height:100%}.fullscreen-player .fullscreen-player__container .content .right.current-page-player[data-v-17ca1e16]{background-color:var(--background-layer-2);display:flex;align-items:center;justify-content:center}.fullscreen-player .fullscreen-player__container .footer[data-v-17ca1e16]{display:flex;gap:calc(var(--padding) / 2);justify-content:flex-end}@media screen and (max-width: 768px){.fullscreen-player .fullscreen-player__container .footer[data-v-17ca1e16]{justify-content:center}}@media screen and (min-width: 768.1px){.fullscreen-player .fullscreen-player__container .footer .p-btn[data-v-17ca1e16]:first-child{display:none}}.fullscreen-player__list[data-v-1d86eedc]{height:100%;overflow-y:auto}.fullscreen-player__lyric[data-v-7293c89c]{padding-bottom:25vh}.fullscreen-player__lyric .is-lyric-correct[data-v-7293c89c]{position:sticky;top:0;z-index:1;border-radius:var(--border-radius);border:1px solid rgba(var(--text-color-value),.1);background-color:var(--background-layer-1);display:flex;gap:var(--padding);box-shadow:var(--box-shadow);transition:transform .2s ease,opacity .2s ease;max-width:512px;margin:0 auto}.fullscreen-player__lyric .is-lyric-correct[data-v-7293c89c]:not(.show){transform:translateY(-100%) scale(.95);transform-origin:top center;opacity:0;pointer-events:none}.fullscreen-player__lyric .is-lyric-correct .icon[data-v-7293c89c]{font-size:36px;display:inline-flex;align-items:center;justify-content:center;padding:var(--padding) calc(var(--padding) * 2);border-radius:var(--border-radius) 0 0 var(--border-radius);background-color:var(--background-layer-2)}.fullscreen-player__lyric .is-lyric-correct .content[data-v-7293c89c]{flex:1;line-height:1.5;padding:var(--padding)}.fullscreen-player__lyric .is-lyric-correct .content .title[data-v-7293c89c]{font-size:18px;font-weight:700}.fullscreen-player__lyric .is-lyric-correct .content .description[data-v-7293c89c]{font-size:14px;color:rgba(var(--text-color-value),.5)}.fullscreen-player__lyric .is-lyric-correct .content .actions[data-v-7293c89c]{margin-top:var(--padding);display:flex;justify-content:flex-end;gap:calc(var(--padding) / 2)}.fullscreen-player__lyric .lyric-item[data-v-7293c89c]{line-height:2;font-size:24px;opacity:.2;margin:var(--padding);transition:all .3s ease}.fullscreen-player__lyric .lyric-item[data-lyric-set="0"][data-v-7293c89c]{opacity:1}.fullscreen-player__lyric .lyric-item[data-lyric-set="-1"][data-v-7293c89c],.fullscreen-player__lyric .lyric-item[data-lyric-set="1"][data-v-7293c89c]{opacity:.5}.fullscreen-player__lyric .lyric-item[data-lyric-set="-2"][data-v-7293c89c],.fullscreen-player__lyric .lyric-item[data-lyric-set="2"][data-v-7293c89c]{opacity:.4875}.fullscreen-player__lyric .lyric-item[data-lyric-set="-3"][data-v-7293c89c],.fullscreen-player__lyric .lyric-item[data-lyric-set="3"][data-v-7293c89c]{opacity:.325}.fullscreen-player__lyric .lyric-item[data-lyric-set="-4"][data-v-7293c89c],.fullscreen-player__lyric .lyric-item[data-lyric-set="4"][data-v-7293c89c]{opacity:.2}.fullscreen-player__lyric.with-translated .lyric-item[data-v-7293c89c]{margin-bottom:0}.fullscreen-player__lyric.with-translated .lyric-item.translated[data-v-7293c89c]{margin-top:0;margin-bottom:var(--padding);font-size:18px}.lyric-search__header[data-v-7293c89c]{display:flex;align-items:center;gap:var(--padding)}.lyric-search__content[data-v-7293c89c]{height:400px;overflow-y:auto}.fullscreen-player__player[data-v-92556033]{display:flex;flex-direction:column;gap:var(--padding)}.fullscreen-player__player .cover[data-v-92556033]{display:flex;justify-content:center}.fullscreen-player__player .cover img[data-v-92556033]{width:100%;height:100%;object-fit:cover;aspect-ratio:1/1;border-radius:var(--border-radius);border:1px solid var(--background-layer-2);max-width:var(--maxCoverSize);max-height:var(--maxCoverSize);background-color:#fff}.fullscreen-player__player .track-info-text .track-name[data-v-92556033],.fullscreen-player__player .track-info-text .track-artist[data-v-92556033]{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%}.fullscreen-player__player .track-info-text .track-name[data-v-92556033]{font-size:1.5rem;font-weight:700}.fullscreen-player__player .track-info-text .track-artist[data-v-92556033]{font-size:1.25rem;opacity:.75}.fullscreen-player__player .time-items[data-v-92556033]{display:flex;justify-content:space-between}.fullscreen-player__player .time-items .time[data-v-92556033]{opacity:.75}.fullscreen-player__player .track-control[data-v-92556033]{display:flex;align-items:center;justify-content:center;gap:calc(var(--padding) / 2)}.loader[data-v-f4517432]{display:flex;align-items:center;justify-content:center;margin-top:30vh}.loader .loader__spinner[data-v-f4517432]{width:40px;height:40px;border:4px solid transparent;border-top-color:var(--primary-color);border-radius:50%;animation:spin-f4517432 .5s linear infinite}@keyframes spin-f4517432{to{transform:rotate(360deg)}}.p-btn[data-v-6e7d6ed3]{color:var(--text-color);background-color:rgba(var(--text-color-value),.075);border:1px solid transparent;padding:var(--padding) calc(var(--padding) * 2);border-radius:var(--border-radius);cursor:pointer;transition:all var(--transition);font-size:14px;line-height:1.5;font-weight:700;display:flex;align-items:center;justify-content:center;gap:var(--padding);box-shadow:inset 0 0 0 100px transparent;text-decoration:none;display:inline-flex}.p-btn[data-v-6e7d6ed3]:hover{box-shadow:inset 0 0 0 100px rgba(var(--text-color-value),.1);border:1px solid rgba(var(--text-color-value),.1)}.p-btn[data-v-6e7d6ed3]:active{box-shadow:inset 0 0 0 100px rgba(var(--text-color-value),.2);border:1px solid rgba(var(--text-color-value),.2);transform:scale(.95)}.p-btn[color=primary][data-v-6e7d6ed3]{background-color:var(--primary-color);color:#fff}.p-btn.outline[data-v-6e7d6ed3]{background-color:transparent;border-color:rgba(var(--text-color-value),.05)}.p-btn.outline[data-v-6e7d6ed3]:hover{border-color:rgba(var(--text-color-value),.15)}.p-btn.outline[data-v-6e7d6ed3]:active{border-color:rgba(var(--text-color-value),.25)}.p-btn.text[data-v-6e7d6ed3]{background-color:transparent}.p-btn.icon[data-v-6e7d6ed3]{padding:var(--padding);font-size:24px}.p-btn.block[data-v-6e7d6ed3]{width:100%;display:block}.p-card[data-v-7ba3ec3e]{width:100%;transition:all var(--transition);border-radius:var(--border-radius);color:var(--text-color);text-decoration:none}.p-card[data-v-7ba3ec3e]:hover{background-color:var(--background-layer-2);box-shadow:0 0 0 var(--padding) var(--background-layer-2)}.p-card:hover .p-card__img[data-v-7ba3ec3e]{filter:brightness(1.05)}.p-card[data-v-7ba3ec3e]:active{transform:scale(.95)}.p-card .p-card__img-container[data-v-7ba3ec3e]{position:relative;width:100%}.p-card .p-card__img-container .p-card__img[data-v-7ba3ec3e]{border-radius:var(--border-radius);width:100%;aspect-ratio:1/1;object-fit:cover;border:1px solid var(--background-layer-2);transition:filter var(--transition)}.p-card .p-card__img-container .p-card__source[data-v-7ba3ec3e]{position:absolute;top:calc(var(--padding) / 2);right:calc(var(--padding) / 2);padding:calc(var(--padding) * .5) var(--padding);background-color:var(--background-layer-2);color:var(--text-color);font-size:.75rem;opacity:.75;border-radius:calc(var(--border-radius) - var(--padding) / 2)}.p-card .p-card__content[data-v-7ba3ec3e]{padding:calc(var(--padding) / 2) 0}.p-card .p-card__content .p-card__title[data-v-7ba3ec3e]{font-size:1em;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.p-card .p-card__content .p-card__description[data-v-7ba3ec3e]{color:var(--text-color);opacity:.75;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.p-cards[data-v-cd28673c]{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--min-card-width),1fr));gap:calc(var(--padding) * 2)}.p-input[data-v-dc46621b]{display:flex;flex-direction:column;width:100%}.p-input+.p-input[data-v-dc46621b]{margin-top:calc(var(--padding) * 2)}.p-input .p-input__label[data-v-dc46621b]{display:flex;align-items:center}.p-input .p-input__label .p-input__label-text[data-v-dc46621b]{font-size:14px;color:var(--text-color);transition:all var(--transition)}.p-input:not(.value):not(:has(input:focus)) .p-input__label[data-v-dc46621b]{cursor:text}.p-input:not(.value):not(:has(input:focus)) .p-input__label .p-input__label-text[data-v-dc46621b]{transform:translateY(calc(var(--padding) * 2 + 16px)) translate(var(--padding)) scale(1.1429);opacity:.5}.p-input:has(input:focus) .p-input__label .p-input__label-text[data-v-dc46621b]{color:var(--primary-color);font-weight:700}.p-input .p-input__input[data-v-dc46621b]{border:none;outline:none;background:none;width:100%;height:100%;padding:calc(var(--padding) * 1.5) var(--padding);font-size:16px;border-bottom:var(--border-width) solid var(--border-color);transition:border-bottom var(--transition),box-shadow var(--transition);box-shadow:0 1px transparent;border-radius:0;color:var(--text-color)}.p-input .p-input__input[data-v-dc46621b]:focus{border-color:var(--primary-color);box-shadow:0 1px var(--primary-color)}.p-list-item{padding:calc(var(--padding) * .75) var(--padding);border-radius:var(--border-radius);display:flex;transition:all var(--transition);width:100%;cursor:pointer;color:var(--text-color);text-decoration:none}.p-list-item:hover{background-color:rgba(var(--text-color-value),.05)}.p-list-item:active,.p-list-item.active{background-color:rgba(var(--text-color-value),.1)}.p-list-item .p-list-item__content,.p-list-item .p-list-item__actions{display:flex;gap:var(--padding);align-items:center}.p-list-item .p-list-item__content{flex:1}.p-list-item .p-list-item__img{--size: 42px;width:var(--size);height:var(--size);min-width:var(--size)}.p-list-item .p-list-item__img img{width:100%;height:100%;object-fit:cover;aspect-ratio:1/1;border-radius:var(--border-radius);border:1px solid var(--background-layer-2);background-color:#fff}.p-list-item .p-list-item__content-content .p-list-item__content-title{font-size:1rem;font-weight:700}.p-list-item .p-list-item__content-content .p-list-item__content-description{opacity:.5}.p-list-item .p-list-item__icon-btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:var(--border-radius);cursor:pointer;transition:all var(--transition);background-color:transparent;border:1px solid transparent}.p-list-item .p-list-item__icon-btn:hover{background-color:rgba(var(--text-color-value),.05)}.p-list-item .p-list-item__icon-btn:active{background-color:rgba(var(--text-color-value),.1)}.p-list-item .p-list-item__icon-btn i{font-size:24px;color:var(--text-color)}.p-list-items[data-v-6dc192e0]{display:grid;grid-template-columns:repeat(2,1fr)}.p-list-items.single-row[data-v-6dc192e0]{grid-template-columns:1fr}@media screen and (max-width: 768px){.p-list-items[data-v-6dc192e0]{grid-template-columns:1fr}}.p-select[data-v-396b2373]{width:100%;position:relative}.p-select[data-v-396b2373]:before{content:"\ea4a";font-family:boxicons!important;position:absolute;right:var(--padding);top:50%;transform:translateY(-50%);pointer-events:none;font-size:1.5em;color:rgba(var(--text-color-value),.5)}.p-select select[data-v-396b2373]{width:100%;height:100%;border:none;background:transparent;font-size:1rem;color:var(--text-color);appearance:none;color:rgba(var(--text-color-value),1);padding:var(--padding) calc(var(--padding) * 2);padding-right:calc(var(--padding) * 2 + 1.5em);border:1px solid rgba(var(--text-color-value),.1);border-radius:var(--border-radius)}.p-select select[data-v-396b2373]:focus{outline:none}input[data-v-4ad436c5]{-webkit-appearance:none;cursor:pointer;width:100%;height:4px;border-radius:2px;background:linear-gradient(to right,var(--primary-color) var(--value-in-percent),rgba(var(--text-color-value),.1) var(--value-in-percent)) no-repeat;outline:none;border:none;--thumb-outline-size: 5px}input[data-v-4ad436c5]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;border-radius:100em;background:var(--primary-color);cursor:pointer;transition:all var(--transition);outline:var(--thumb-outline-size) solid transparent;box-shadow:none}input[data-v-4ad436c5]::-webkit-slider-runnable-track{-webkit-appearance:none;box-shadow:none;border:none;background:transparent}input[data-v-4ad436c5]::-moz-range-thumb{width:12px;height:12px;border-radius:100em;background:var(--primary-color);cursor:pointer;transition:all var(--transition);outline:var(--thumb-outline-size) solid transparent}input[data-v-4ad436c5]:hover::-webkit-slider-thumb{outline:var(--thumb-outline-size) solid rgba(var(--text-color-value),.1)}input[data-v-4ad436c5]:hover::-moz-range-thumb{outline:var(--thumb-outline-size) solid rgba(var(--text-color-value),.1)}input[data-v-4ad436c5]:active{--thumb-outline-size: 10px}input[data-v-4ad436c5]:active::-webkit-slider-thumb{outline:var(--thumb-outline-size) solid rgba(var(--text-color-value),.2)}input[data-v-4ad436c5]:active::-moz-range-thumb{outline:var(--thumb-outline-size) solid rgba(var(--text-color-value),.2)}.home-item[data-v-25d19ae0]{margin-bottom:calc(var(--padding) * 4)}.home-item .home-item__header[data-v-25d19ae0]{margin-bottom:calc(var(--padding) * 1)}.song-info-dialog-content .cover[data-v-2a2e2f70]{width:100%;display:block;max-width:240px;margin:0 auto;aspect-ratio:1/1;object-fit:cover;border-radius:var(--border-radius);border:1px solid var(--background-layer-2)}.song-info-dialog-content .info[data-v-2a2e2f70]{text-align:center;font-size:16px;margin:var(--padding) 0}.song-info-dialog-content .info .name[data-v-2a2e2f70]{font-weight:700}.song-info-dialog-content .info .artist[data-v-2a2e2f70]{opacity:.5}*,:after,:before{box-sizing:inherit;padding:0;margin:0}html{line-height:1.15;box-sizing:border-box;font-family:sans-serif}main{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}button,input,optgroup,select,textarea{line-height:inherit;border:1px solid currentColor}button{overflow:visible;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;padding:1px 6px}input{overflow:visible}input,textarea{padding:1px}fieldset{border:1px solid currentColor;margin:0 2px}legend{color:inherit;display:table;max-width:100%;white-space:normal}progress{display:inline-block;vertical-align:baseline}select{text-transform:none}textarea{overflow:auto;vertical-align:top}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=color]{background:inherit}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:.5}::-webkit-file-upload-button,::-webkit-search-decoration{-webkit-appearance:button;font:inherit}::-moz-focus-inner{border:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}hr{box-sizing:content-box;height:0;color:inherit;overflow:visible}dl,ol,ul{margin:1em 0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{margin:0}b,strong{font-weight:bolder}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border:0}svg:not(:root){overflow:hidden}table{text-indent:0;border-color:inherit}details{display:block}dialog{background-color:inherit;border:solid;color:inherit;display:block;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}dialog:not([open]){display:none}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}:root{--background-layer-1: #fff;--background-layer-2: #f2f2f2;--text-color-value: 51,51,51;--text-color: rgba(var(--text-color-value), 1);--border-color: rgba(var(--text-color-value), .1);--border-focus-color: rgba(var(--text-color-value), .2);--border-width: 1px;--box-shadow: 0 4px 8px rgba(0, 0, 0, .05);--transition-duration: .15s;--transition-timing-function: ease;--transition: var(--transition-duration) var(--transition-timing-function);--primary-color: #007bff;--min-card-width: 128px;--border-radius: 12px;--padding: 8px}body,html{font-size:16px;line-height:1.25;font-family:Lato,Noto Sans TC,sans-serif;color:var(--text-color)}body h1,html h1{font-size:42px;font-weight:400;margin:.2em 0}.setting-item{padding:calc(var(--padding) * 2);margin-bottom:calc(var(--padding) * 2);border:1px solid rgba(var(--text-color-value),.1);box-shadow:var(--box-shadow);border-radius:var(--border-radius);display:flex;align-items:center;gap:calc(var(--padding) * 2)}.setting-item .content{flex:1}.setting-item .content .title{font-weight:700}.setting-item .content .description{opacity:.75}.setting-item .control{width:200px;display:flex;align-items:center;justify-content:flex-end;gap:var(--padding)}.fade-enter-active,.fade-leave-active{transition:opacity .15s linear}.fade-enter,.fade-leave-to{opacity:0} 2 | -------------------------------------------------------------------------------- /public/assets/user-8fc11de1.js: -------------------------------------------------------------------------------- 1 | import{h as o,i as t}from"./index-55e28da3.js";const u=o("user",()=>{const e=t({});function r(s){e.value=s}return{userInfo:e,setUserInfo:r}});export{u}; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/favicon.ico -------------------------------------------------------------------------------- /public/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/cover.jpg -------------------------------------------------------------------------------- /public/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/icon.png -------------------------------------------------------------------------------- /public/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | -------------------------------------------------------------------------------- /public/img/playlist/cloud.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/playlist/cloud.jpg -------------------------------------------------------------------------------- /public/img/playlist/cover.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/playlist/cover.psd -------------------------------------------------------------------------------- /public/img/playlist/dailyRecommendSongs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/playlist/dailyRecommendSongs.jpg -------------------------------------------------------------------------------- /public/img/playlist/listenedRecently.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/playlist/listenedRecently.jpg -------------------------------------------------------------------------------- /public/img/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/pwa-192x192.png -------------------------------------------------------------------------------- /public/img/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnehs/PokaPlayer/b1a4434454cccf21028a5742fba792cd6d00dfe8/public/img/pwa-512x512.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | PokaPlayer 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"PokaPlayer","short_name":"PokaPlayer","start_url":"/","display":"standalone","background_color":"#ffffff","lang":"en","scope":"/","description":"PokaPlayer is a player that can unify and play from multiple sources like DSM and Netease.","theme_color":"#f2f2f2","icons":[{"src":"/img/pwa-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/img/pwa-512x512.png","sizes":"512x512","type":"image/png"}]} 2 | -------------------------------------------------------------------------------- /public/registerSW.js: -------------------------------------------------------------------------------- 1 | if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/sw.js', { scope: '/' })})} -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | if(!self.define){let s,e={};const l=(l,i)=>(l=new URL(l+".js",i).href,e[l]||new Promise((e=>{if("document"in self){const s=document.createElement("script");s.src=l,s.onload=e,document.head.appendChild(s)}else s=l,importScripts(l),e()})).then((()=>{let s=e[l];if(!s)throw new Error(`Module ${l} didn’t register its module`);return s})));self.define=(i,r)=>{const n=s||("document"in self?document.currentScript.src:"")||location.href;if(e[n])return;let u={};const a=s=>l(s,n),o={module:{uri:n},exports:u,require:a};e[n]=Promise.all(i.map((s=>o[s]||a(s)))).then((s=>(r(...s),u)))}}define(["./workbox-7369c0e1"],(function(s){"use strict";self.addEventListener("message",(s=>{s.data&&"SKIP_WAITING"===s.data.type&&self.skipWaiting()})),s.precacheAndRoute([{url:"assets/404-9b37a669.js",revision:null},{url:"assets/Album-7e10ba23.css",revision:null},{url:"assets/Album-d71314ff.js",revision:null},{url:"assets/Albums-51ec468d.js",revision:null},{url:"assets/Artists-de6da841.js",revision:null},{url:"assets/default-8fdf2100.js",revision:null},{url:"assets/default-ae053ae9.css",revision:null},{url:"assets/empty-4a05e738.js",revision:null},{url:"assets/Folders-941ca665.js",revision:null},{url:"assets/index-55e28da3.js",revision:null},{url:"assets/Index-77de2a6a.js",revision:null},{url:"assets/index-d8bacfc1.css",revision:null},{url:"assets/Library-6e5b1b31.js",revision:null},{url:"assets/Log-6cc3aa27.js",revision:null},{url:"assets/Log-c9db7119.css",revision:null},{url:"assets/Login-0c932826.js",revision:null},{url:"assets/Login-65f28d2c.css",revision:null},{url:"assets/Pins-0ce5b192.js",revision:null},{url:"assets/Playlists-35b3be64.js",revision:null},{url:"assets/Quality-316f457e.js",revision:null},{url:"assets/Search-1a5f5fd1.js",revision:null},{url:"assets/Search-f3c2d4ea.css",revision:null},{url:"assets/System-38da5b99.js",revision:null},{url:"assets/Theme-08e39d68.css",revision:null},{url:"assets/Theme-217bd9f0.js",revision:null},{url:"assets/user-8fc11de1.js",revision:null},{url:"assets/User-abf1ad8a.js",revision:null},{url:"assets/Users-15669eb8.js",revision:null},{url:"index.html",revision:"839b0b1aca73c71147a0de5aff26d6f6"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"favicon.ico",revision:"de3528fc1966170b009743368e6fc4b3"},{url:"img/apple-touch-icon.png",revision:"bf1ff7241b39d4a945cff48130fd6af8"},{url:"img/icon.svg",revision:"4dde6544b72cded309d3a3b3dbd9c822"},{url:"img/pwa-192x192.png",revision:"1c154bf837876442174679d1ecfff61e"},{url:"img/pwa-512x512.png",revision:"8d5e56b6505094cae2e181f0d4fb889c"},{url:"manifest.webmanifest",revision:"7ed65e55f4121e9109d525fe95fcad44"}],{}),s.cleanupOutdatedCaches(),s.registerRoute(new s.NavigationRoute(s.createHandlerBoundToURL("index.html")))})); 2 | -------------------------------------------------------------------------------- /public/workbox-7369c0e1.js: -------------------------------------------------------------------------------- 1 | define(["exports"],(function(t){"use strict";try{self["workbox:core:6.5.3"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:6.5.3"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class o{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let o=r&&r.handler;const c=t.method;if(!o&&this.i.has(c)&&(o=this.i.get(c)),!o)return;let a;try{a=o.handle({url:s,request:t,event:e,params:i})}catch(t){a=Promise.reject(t)}const h=r&&r.catchHandler;return a instanceof Promise&&(this.o||h)&&(a=a.catch((async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n}))),a}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const o=r.match({url:t,sameOrigin:e,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&0===i.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let c;const a=()=>(c||(c=new o,c.addFetchListener(),c.addCacheListener()),c);function h(t,e,n){let o;if("string"==typeof t){const s=new URL(t,location.href);o=new i((({url:t})=>t.href===s.href),e,n)}else if(t instanceof RegExp)o=new r(t,e,n);else if("function"==typeof t)o=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}return a().registerRoute(o),o}const u={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},l=t=>[u.prefix,t,u.suffix].filter((t=>t&&t.length>0)).join("-"),f=t=>t||l(u.precache),w=t=>t||l(u.runtime);function d(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:6.5.3"]&&_()}catch(t){}function p(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),r=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:r.href}}class y{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class g{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.h.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.h=t}}let R;async function m(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=e?e(r):r,c=function(){if(void 0===R){const t=new Response("");if("body"in t)try{new Response(t.body),R=!0}catch(t){R=!1}R=!1}return R}()?i.body:await i.blob();return new Response(c,o)}function v(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class q{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}}const U=new Set;try{self["workbox:strategies:6.5.3"]&&_()}catch(t){}function L(t){return"string"==typeof t?new Request(t):t}class b{constructor(t,e){this.u={},Object.assign(this,e),this.event=e.event,this.l=t,this.p=new q,this.g=[],this.R=[...t.plugins],this.m=new Map;for(const t of this.R)this.m.set(t,{});this.event.waitUntil(this.p.promise)}async fetch(t){const{event:e}=this;let n=L(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.l.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=L(t);let s;const{cacheName:n,matchOptions:i}=this.l,r=await this.getCacheKey(e,"read"),o=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,o);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=L(t);var i;await(i=0,new Promise((t=>setTimeout(t,i))));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(o=r.url,new URL(String(o),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var o;const c=await this.v(e);if(!c)return!1;const{cacheName:a,matchOptions:h}=this.l,u=await self.caches.open(a),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=v(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),o=await t.keys(e,r);for(const e of o)if(i===v(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?c.clone():c)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of U)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:a,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.u[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=L(await t({mode:e,request:n,event:this.event,params:this.params}));this.u[s]=n}return this.u[s]}hasCallback(t){for(const e of this.l.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.l.plugins)if("function"==typeof e[t]){const s=this.m.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.g.push(t),t}async doneWaiting(){let t;for(;t=this.g.shift();)await t}destroy(){this.p.resolve(null)}async v(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class C{constructor(t={}){this.cacheName=w(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new b(this,{event:e,request:s,params:n}),r=this.q(i,s,e);return[r,this.U(r,i,s,e)]}async q(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this.L(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async U(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}class E extends C{constructor(t={}){t.cacheName=f(t.cacheName),super(t),this._=!1!==t.fallbackToNetwork,this.plugins.push(E.copyRedirectedCacheableResponsesPlugin)}async L(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.C(t,e):await this.O(t,e))}async O(t,e){let n;const i=e.params||{};if(!this._)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,o=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&o&&"no-cors"!==t.mode&&(this.N(),await e.cachePut(t,n.clone()))}return n}async C(t,e){this.N();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}N(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==E.copyRedirectedCacheableResponsesPlugin&&(n===E.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(E.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}E.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},E.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await m(t):t};class O{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.k=new Map,this.K=new Map,this.P=new Map,this.l=new E({cacheName:f(t),plugins:[...e,new g({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.l}precache(t){this.addToCacheList(t),this.T||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.T=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=p(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.k.has(i)&&this.k.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.k.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.P.has(t)&&this.P.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.P.set(t,n.integrity)}if(this.k.set(i,t),this.K.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return d(t,(async()=>{const e=new y;this.strategy.plugins.push(e);for(const[e,s]of this.k){const n=this.P.get(s),i=this.K.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}}))}activate(t){return d(t,(async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.k.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}}))}getURLsToCacheKeys(){return this.k}getCachedURLs(){return[...this.k.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.k.get(e.href)}getIntegrityForCacheKey(t){return this.P.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}let x;const N=()=>(x||(x=new O),x);class k extends i{constructor(t,e){super((({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const o=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some((t=>t.test(s)))&&t.searchParams.delete(s);return t}(r,e);if(yield o.href,s&&o.pathname.endsWith("/")){const t=new URL(o.href);t.pathname+=s,yield t.href}if(n){const t=new URL(o.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}}),t.strategy)}}t.NavigationRoute=class extends i{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super((t=>this.W(t)),t),this.j=e,this.M=s}W({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.M)if(t.test(s))return!1;return!!this.j.some((t=>t.test(s)))}},t.cleanupOutdatedCaches=function(){self.addEventListener("activate",(t=>{const e=f();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter((s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t));return await Promise.all(s.map((t=>self.caches.delete(t)))),s})(e).then((t=>{})))}))},t.createHandlerBoundToURL=function(t){return N().createHandlerBoundToURL(t)},t.precacheAndRoute=function(t,e){!function(t){N().precache(t)}(t),function(t){const e=N();h(new k(e,t))}(e)},t.registerRoute=h})); 2 | -------------------------------------------------------------------------------- /router/config.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const playlistDB = require("../db/playlist"); 3 | const recordDB = require("../db/record"); 4 | const dsm = require("../dataModule/dsm"); 5 | const fs = require("fs"); 6 | router.get("/", async (req, res) => { 7 | res.json(JSON.parse(fs.readFileSync("./config.json", "utf8"))); 8 | }); 9 | router.post("/save", async (req, res) => { 10 | let config = req.body 11 | try { 12 | fs.writeFileSync("./config.json", JSON.stringify(config, null, 2)) 13 | res.json({ success: true }) 14 | } catch (err) { 15 | res.json({ success: false, err }) 16 | } 17 | }); 18 | 19 | router.get("/dsm-music-id-fix", async (req, res) => { 20 | res.json({ success: true, }) 21 | console.log('fixing music id...') 22 | // fix record 23 | let records = await recordDB.getAllRecords(); 24 | for (let record of records) { 25 | if (record.source == "DSM") { 26 | 27 | if (!record.albumId) 28 | record.albumId = JSON.stringify({ 29 | album_name: record.album, 30 | artist_name: record.artist, 31 | }) 32 | try { 33 | let album = await dsm.getAlbum(record.albumId); 34 | if (album) { 35 | let filteredSong = album.songs.filter(x => x.name == record.name) 36 | if (filteredSong.length) { 37 | if (record.songId !== filteredSong[0].id) { 38 | console.log(`[Record] ${record.name} ${record.songId} => ${filteredSong[0].id}`) 39 | record.url = filteredSong[0].url 40 | record.songId = filteredSong[0].id 41 | await record.save() 42 | } 43 | } else { 44 | console.log(`[Record] ${record.name} not found in ${album.name}`) 45 | } 46 | } 47 | } catch (e) { 48 | console.log(e) 49 | } 50 | } 51 | } 52 | // fix playlist 53 | let playlists = await playlistDB.getAllPlaylists() 54 | for (let playlist of playlists) { 55 | for (let song of playlist.songs) { 56 | if (song.source == "DSM") { 57 | try { 58 | if (!song.albumId) 59 | song.albumId = JSON.stringify({ 60 | album_name: song.album, 61 | artist_name: song.artist, 62 | }) 63 | // get album data 64 | let album = await dsm.getAlbum(song.albumId); 65 | if (album) { 66 | let filteredSong = album.songs.filter(x => x.name == song.name) 67 | if (filteredSong.length) { 68 | if (song.id !== filteredSong[0].id) { 69 | console.log(`[Playlist] ${song.name} ${song.id} => ${filteredSong[0].id}`) 70 | song.url = filteredSong[0].url 71 | song.id = filteredSong[0].id 72 | } 73 | } else { 74 | console.log(`[Playlist] ${song.name} not found in ${album.name}`) 75 | } 76 | } 77 | } catch (e) { 78 | console.log(song, e) 79 | } 80 | } 81 | } 82 | await playlist.save() 83 | } 84 | console.log('done') 85 | }) 86 | module.exports = router; -------------------------------------------------------------------------------- /router/index.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const User = require("../db/user"); // userDB 3 | router.get("/", (_, res) => res.send("Poka API v2")) 4 | router.use("/playlist", require("./playlist")); 5 | router.use("/user", require("./user")); 6 | router.use("/pin", require("./pin")); 7 | router.use("/record", require("./record")); 8 | router.use("/info", require("./info")); 9 | // admin 10 | router.use(async (req, res, next) => { 11 | if (req.session.user && await User.isUserAdmin(req.session.user)) 12 | next() 13 | else 14 | res.status(403).send("Permission Denied Desu"); 15 | }); 16 | router.use("/users", require("./users")); 17 | router.use("/config", require("./config")); 18 | router.use("/log", require("./log")); 19 | module.exports = router; -------------------------------------------------------------------------------- /router/info.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const packageData = require("../package.json"); // package 3 | const jsonfile = require('jsonfile') 4 | const git = require("simple-git")(__dirname); 5 | const config = jsonfile.readFileSync("./config.json") 6 | router.get("/", async (req, res) => { 7 | let result = { 8 | uid: req.session.user, 9 | version: packageData.version, 10 | debug: config.PokaPlayer.debug 11 | } 12 | if (config.PokaPlayer.debug) { 13 | result['debugString'] = (await git.raw(["rev-parse", "--short", "HEAD"])).slice(0, -1) 14 | } 15 | res.json(result) 16 | }); 17 | module.exports = router; -------------------------------------------------------------------------------- /router/log.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const Log = require("../db/log"); // userDB 3 | 4 | router.get("/", async (req, res) => { 5 | let { page = 0, limit = 50 } = req.query; 6 | res.json(await Log.getLogs(limit, page)) 7 | }) 8 | router.post("/clear", async (req, res) => { 9 | try { 10 | await Log.clearLogs() 11 | res.json({ 12 | success: true, error: null 13 | }) 14 | } catch (e) { 15 | res.json({ 16 | success: false, error: e 17 | }) 18 | } 19 | 20 | }) 21 | 22 | module.exports = router; -------------------------------------------------------------------------------- /router/pin.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const Pin = require("../db/pin"); // pin db 3 | router.get("/", async (req, res) => { 4 | res.send("Poka API v2 Pin") 5 | }) 6 | router.post("/pins", async (req, res) => { 7 | let pinRes = await Pin.getPins(req.session.user) 8 | res.json(pinRes) 9 | }) 10 | router.post("/pin", async (req, res) => { 11 | let { 12 | name, 13 | artist, 14 | id, 15 | type, 16 | cover, 17 | source } = req.body 18 | let owner = req.session.user 19 | let pinRes = await Pin.addPin({ 20 | name, 21 | artist, 22 | id, 23 | type, 24 | cover, 25 | source, 26 | owner 27 | }) 28 | res.json(pinRes) 29 | }) 30 | router.post("/ispinned", async (req, res) => { 31 | let { id, type, source } = req.body 32 | let owner = req.session.user 33 | let pinRes = await Pin.isPinned({ 34 | id, 35 | type, 36 | source, 37 | owner 38 | }) 39 | res.json(!!pinRes) 40 | }) 41 | router.post("/unpin", async (req, res) => { 42 | let { id, type, source } = req.body 43 | let owner = req.session.user 44 | let pinRes = await Pin.unPin({ 45 | id, 46 | type, 47 | source, 48 | owner 49 | }) 50 | res.json(pinRes) 51 | }) 52 | module.exports = router; -------------------------------------------------------------------------------- /router/playlist.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const playlistDB = require('../db/playlist') 3 | const userDB = require('../db/user') 4 | const pokaLog = require("../log"); // 可愛控制台輸出 5 | router.get("/", async (req, res) => { 6 | res.send("Poka Playlist API") 7 | }) 8 | async function checkPlaylistOwner(playlistId, userId) { 9 | let playlist = await playlistDB.getPlaylistById(playlistId) 10 | return playlist.owner == userId 11 | } 12 | router.post("/create", async (req, res) => { 13 | let { name } = req.body 14 | if (!name) { 15 | res.status(400).send("name is required") 16 | return 17 | } 18 | res.json(await playlistDB.createPlaylist(name, req.session.user)) 19 | }) 20 | router.post("/del", async (req, res) => { 21 | let { id } = req.body 22 | if (await checkPlaylistOwner(id, req.session.user)) 23 | res.json(await playlistDB.delPlaylist(id)) 24 | else 25 | res.json({ 26 | success: false, 27 | error: 'Permission Denied' 28 | }) 29 | 30 | }) 31 | router.post("/edit", async (req, res) => { 32 | let { data, id } = req.body 33 | if (await checkPlaylistOwner(id, req.session.user)) 34 | res.json(await playlistDB.editPlaylist(id, data)) 35 | else 36 | res.json({ 37 | success: false, 38 | error: 'Permission Denied' 39 | }) 40 | }) 41 | router.post("/song/exist", async (req, res) => { 42 | let song = req.body 43 | let playlists = (await playlistDB.getPlaylists(req.session.user)).map(x => ({ id: x._id, ...JSON.parse(JSON.stringify(x)) })) 44 | let result = [] 45 | for (let playlist of playlists) { 46 | playlist.exist = playlist.songs && playlist.songs.filter(x => x.source == song.source && x.id == song.id).length > 0 47 | delete playlist.songs 48 | result.push(playlist) 49 | } 50 | res.json(result) 51 | }) 52 | router.post("/song", async (req, res) => { 53 | let { playlistId, song } = req.body 54 | if (await checkPlaylistOwner(playlistId, req.session.user)) 55 | res.json(await playlistDB.toggleSongOfPlaylist({ 56 | playlistId, 57 | song 58 | })) 59 | else 60 | res.json({ 61 | success: false, 62 | error: 'Permission Denied' 63 | }) 64 | }) 65 | module.exports = router; -------------------------------------------------------------------------------- /router/record.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const recordDB = require('../db/record') 3 | router.get("/", async (req, res) => { 4 | res.send("Poka API v2 Record") 5 | }) 6 | router.post("/add", async (req, res) => { 7 | let { 8 | name, 9 | artist, 10 | artistId, 11 | album, 12 | albumId, 13 | source, 14 | originalCover: cover, 15 | id: songId 16 | } = req.body 17 | res.json(await recordDB.addRecord({ 18 | name, 19 | artist, 20 | artistId, 21 | album, 22 | albumId, 23 | source, 24 | cover, 25 | songId, 26 | userId: req.session.user 27 | })) 28 | }) 29 | router.get("/count", async (req, res) => { 30 | res.json(await recordDB.countRecords()) 31 | }) 32 | router.get("/review", async (req, res) => { 33 | res.json(await recordDB.getReview(req.session.user, req.query.year)) 34 | }) 35 | router.get("/count/user", async (req, res) => { 36 | res.json(await recordDB.countUserRecords(req.session.user)) 37 | }) 38 | router.post("/clear", async (req, res) => { 39 | res.json(await recordDB.clearUserRecords(req.session.user)) 40 | }) 41 | module.exports = router; -------------------------------------------------------------------------------- /router/user.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const User = require("../db/user"); 3 | const { addLog } = require("../db/log"); 4 | router.get("/", async (req, res) => { 5 | let result = await User.getUserById(req.session.user) 6 | if (result) 7 | result.password = null 8 | res.json(result) 9 | }) 10 | router.post("/login/", async (req, res) => { 11 | let { username, password } = req.body 12 | let u = await User.login({ username, password }) 13 | if (u.success) { 14 | req.session.user = u.user 15 | addLog({ 16 | level: "info", 17 | type: "user", 18 | event: "Login", 19 | user: req.session.user, 20 | description: `User {${req.session.user}} login from ${req.headers['x-forwarded-for'] || req.socket.remoteAddress}` 21 | }) 22 | } else { 23 | addLog({ 24 | level: "warn", 25 | type: "user", 26 | event: "Login", 27 | description: `User ${username} login failed from ${req.headers['x-forwarded-for'] || req.socket.remoteAddress}` 28 | }) 29 | } 30 | res.json(u) 31 | }) 32 | router.get("/logout/", (req, res) => { 33 | // 登出 34 | if (req.session?.user) { 35 | addLog({ 36 | level: "info", 37 | type: "user", 38 | event: "Logout", 39 | user: req.session.user, 40 | description: `User {${req.session.user}} logout` 41 | }) 42 | } 43 | req.session.destroy(err => { 44 | if (err) { 45 | console.error(err); 46 | } 47 | res.clearCookie(); 48 | res.json({ success: true }) 49 | }); 50 | }) 51 | router.get("/setting/", async (req, res) => { 52 | res.json(await User.getSetting(req.session.user)) 53 | }) 54 | router.post("/setting/", async (req, res) => { 55 | res.json(await User.changeSetting(req.session.user, req.body.n)) 56 | }) 57 | router.post("/name/", async (req, res) => { 58 | res.json(await User.changeName(req.session.user, req.body.n)) 59 | addLog({ 60 | level: "info", 61 | type: "user", 62 | event: "Name changed", 63 | user: req.session.user, 64 | description: `User {${req.session.user}} changed name to "${req.body.n}".` 65 | }) 66 | }) 67 | router.post("/username/", async (req, res) => { 68 | let result = await User.changeUsername(req.session.user, req.body.n) 69 | res.json(result) 70 | if (result.success) { 71 | addLog({ 72 | level: "info", 73 | type: "user", 74 | event: "Username changed", 75 | user: req.session.user, 76 | description: `User {${req.session.user}} changed username to "${req.body.n}".` 77 | }) 78 | } 79 | }) 80 | router.post("/password/", async (req, res) => { 81 | res.json(await User.changePassword(req.session.user, req.body.oldpassword, req.body.password)) 82 | addLog({ 83 | level: "info", 84 | type: "user", 85 | event: "Password changed", 86 | user: req.session.user, 87 | description: `User {${req.session.user}} password changed.` 88 | }) 89 | }) 90 | module.exports = router; -------------------------------------------------------------------------------- /router/users.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const User = require("../db/user"); 3 | const { addLog } = require("../db/log"); 4 | router.get("/", async (req, res) => { 5 | res.send("Poka API v2 Users") 6 | }) 7 | router.get("/list", async (req, res) => { 8 | let userList = await User.getAllUsers() 9 | res.json(userList) 10 | }) 11 | router.post("/create", async (req, res) => { 12 | res.json(await User.create(req.body)) 13 | }) 14 | router.post("/change-password", async (req, res) => { 15 | res.json(await User.changePasswordAdmin(req.body._id, req.body.password)) 16 | addLog({ 17 | level: "info", 18 | type: "user", 19 | event: "Password changed", 20 | user: req.session.user, 21 | description: `Admin changed {${req.body._id}}'s password.` 22 | }) 23 | }) 24 | router.post("/delete", async (req, res) => { 25 | if (req.body._id != req.session.user) { 26 | res.json(await User.deleteUserById(req.body._id)) 27 | addLog({ 28 | level: "info", 29 | type: "user", 30 | event: "User deleted", 31 | user: req.session.user, 32 | description: `Admin deleted {${req.body._id}}.` 33 | }) 34 | } else { 35 | res.status(406).send('You CAN NOT delete yourself') 36 | } 37 | }) 38 | module.exports = router; -------------------------------------------------------------------------------- /test/netease2.js: -------------------------------------------------------------------------------- 1 | const expect = require("chai").expect; 2 | 3 | const Netease2 = require("../dataModule/netease2.js"); 4 | 5 | const songs = { 6 | ids: [435552097, 1293905031], 7 | names: ["RAGE OF DUST", "月の姫"] 8 | }; 9 | 10 | before(done => expect(Netease2.onLoaded()).to.eventually.be.true.notify(done)); 11 | 12 | describe("獲取歌曲", () => { 13 | it("getSongsUrl 回傳歌曲的 URL", async done => { 14 | try { 15 | expect(await Netease2.getSongsUrl(songs.ids)).to.satisfy(data => 16 | data.every(x => x.url.includes("http") && x.url.endsWith(".mp3")) 17 | ); 18 | } catch (e) { 19 | done(e); 20 | } 21 | }); 22 | 23 | it("getSongs 回傳歌曲資訊", async done => { 24 | try { 25 | expect(await getSongs(songs.ids)).to.satisfy(data => 26 | data.every(x => songs.names.includes(x.name)) 27 | ); 28 | } catch (e) { 29 | done(e); 30 | } 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /update-database.js: -------------------------------------------------------------------------------- 1 | const pokaLog = require("./log"); // 可愛控制台輸出 2 | const Pin = require('./db/pin') 3 | const Playlist = require('./db/playlist') 4 | const Record = require('./db/record') 5 | const encodeBase64 = require('./dataModule/cryptoUtils').encodeBase64 6 | 7 | 8 | function deReq(x) { 9 | const b2a = x => Buffer.from(x, "base64").toString("utf8"); 10 | const decode = x => /(.{5})(.+)3C4C7CB3(.+)/.exec(x); 11 | let [_, rand, link, checkSum] = decode(x); 12 | [_, rand, link, checkSum] = [_, rand, b2a(link), b2a(checkSum)]; 13 | if (!Number.isInteger(Math.log10(rand.charCodeAt(0) + checkSum.charCodeAt(0)))) { 14 | return false; 15 | } 16 | return link; 17 | } 18 | async function updateDatabase() { 19 | Pin.model.find({}).then(async pins => { 20 | let count = 0 21 | for (let pin of pins) { 22 | if (pin.type == 'album' && pin.source == 'DSM' && !pin.id.match(/^Wy/)) { 23 | pin.id = encodeBase64(JSON.stringify(Object.values(JSON.parse(pin.id)))) 24 | await pin.save() 25 | count++ 26 | } 27 | } 28 | if (count) 29 | pokaLog.logDB('UPDATE', `${count} pins updated`) 30 | }) 31 | Playlist.model.find({}).then(async playlists => { 32 | let count = 0 33 | for (let playlist of playlists) { 34 | for (let song of playlist.songs) { 35 | if (song.source == 'DSM' && song.albumId && !song.albumId.match(/^Wy/)) { 36 | song.albumId = encodeBase64(JSON.stringify(Object.values(JSON.parse(song.albumId)))) 37 | count++ 38 | } 39 | } 40 | await playlist.save() 41 | } 42 | if (count) 43 | pokaLog.logDB('UPDATE', `${count} playlist songs updated`) 44 | }) 45 | Record.model.find({}).then(async records => { 46 | let count = 0 47 | for (let record of records) { 48 | if (record.source == 'DSM' && record.albumId && !record.albumId.match(/^Wy/)) { 49 | record.albumId = encodeBase64(JSON.stringify(Object.values(JSON.parse(record.albumId)))) 50 | await record.save() 51 | count++ 52 | } 53 | } 54 | if (count) 55 | pokaLog.logDB('UPDATE', `${count} records updated`) 56 | }) 57 | } 58 | module.exports = updateDatabase --------------------------------------------------------------------------------