diff --git a/release/app/package-lock.json b/release/app/package-lock.json index 0bddb1a4..2489d161 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { + "cheerio": "^1.0.0-rc.12", "mpris-service": "^2.1.2" }, "devDependencies": { @@ -25,6 +26,7 @@ "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", + "global-agent": "^3.0.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", @@ -147,6 +149,11 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -207,6 +214,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -219,12 +262,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/dbus-next": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/dbus-next/-/dbus-next-0.9.2.tgz", "integrity": "sha512-tzQq/+wrTZ2yU+U5PoeXc97KABhX2v55C/T0finH3tSKYuI8H/SqppIFymBBrUHcK13LvEGY3vdj3ikPPenL5g==", "dependencies": { "@nornagon/put": "0.0.8", + "abstract-socket": "^2.0.0", "event-stream": "3.3.4", "hexy": "^0.2.10", "jsbi": "^2.0.5", @@ -327,6 +397,57 @@ "dev": true, "optional": true }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -359,6 +480,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -408,6 +540,7 @@ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "dependencies": { + "@types/yauzl": "^2.9.1", "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" @@ -633,6 +766,24 @@ "hexy": "bin/hexy_cmd.js" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -719,6 +870,9 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -820,6 +974,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", @@ -861,6 +1026,29 @@ "node": ">=8" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -1248,6 +1436,11 @@ "file-uri-to-path": "1.0.0" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -1296,6 +1489,33 @@ "get-intrinsic": "^1.0.2" } }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -1305,6 +1525,23 @@ "mimic-response": "^1.0.0" } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "dbus-next": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/dbus-next/-/dbus-next-0.9.2.tgz", @@ -1381,6 +1618,39 @@ "dev": true, "optional": true }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -1406,6 +1676,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -1608,6 +1883,17 @@ "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==" }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -1756,6 +2042,14 @@ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", @@ -1785,6 +2079,23 @@ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + }, "pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", diff --git a/release/app/package.json b/release/app/package.json index db905a2a..2f0b9c46 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -13,6 +13,7 @@ "postinstall": "npm run electron-rebuild && npm run link-modules" }, "dependencies": { + "cheerio": "^1.0.0-rc.12", "mpris-service": "^2.1.2" }, "devDependencies": { diff --git a/src/main/features/core/index.ts b/src/main/features/core/index.ts index c273d1a5..06a0a8da 100644 --- a/src/main/features/core/index.ts +++ b/src/main/features/core/index.ts @@ -1,2 +1,3 @@ +import './lyrics'; import './player'; import './settings'; diff --git a/src/main/features/core/lyrics/genius.ts b/src/main/features/core/lyrics/genius.ts new file mode 100644 index 00000000..41cf903e --- /dev/null +++ b/src/main/features/core/lyrics/genius.ts @@ -0,0 +1,59 @@ +import axios, { AxiosResponse } from 'axios'; +import { load } from 'cheerio'; +import type { QueueSong } from '/@/renderer/api/types'; + +const search_url = 'https://genius.com/api/search/song'; + +async function getSongURL(metadata: QueueSong) { + let result: AxiosResponse; + try { + result = await axios.get(search_url, { + params: { + per_page: '1', + q: `${metadata.artistName} ${metadata.name}`, + }, + }); + } catch (e) { + console.error('Genius search request got an error!', e); + return undefined; + } + + return result.data.response?.sections?.[0]?.hits?.[0]?.result?.url; +} + +async function getLyricsFromGenius(url: string): Promise { + let result: AxiosResponse; + try { + result = await axios.get(url, { responseType: 'text' }); + } catch (e) { + console.error('Genius lyrics request got an error!', e); + return null; + } + + const $ = load(result.data.split('
').join('\n')); + const lyricsDiv = $('div.lyrics'); + + if (lyricsDiv.length > 0) return lyricsDiv.text().trim(); + + const lyricSections = $('div[class^=Lyrics__Container]') + .map((_, e) => $(e).text()) + .toArray() + .join('\n'); + return lyricSections; +} + +export async function query(metadata: QueueSong): Promise { + const songId = await getSongURL(metadata); + if (!songId) { + console.error('Could not find the song on Genius!'); + return null; + } + + const lyrics = await getLyricsFromGenius(songId); + if (!lyrics) { + console.error('Could not get lyrics on Genius!'); + return null; + } + + return lyrics; +} diff --git a/src/main/features/core/lyrics/index.ts b/src/main/features/core/lyrics/index.ts new file mode 100644 index 00000000..02609bdc --- /dev/null +++ b/src/main/features/core/lyrics/index.ts @@ -0,0 +1,26 @@ +import { QueueSong } from '/@/renderer/api/types'; +import { query as queryGenius } from './genius'; +import { query as queryNetease } from './netease'; +import { LyricSource } from '../../../../renderer/types'; +import { ipcMain } from 'electron'; +import { getMainWindow } from '../../../main'; +import { store } from '../settings/index'; + +type SongFetcher = (song: QueueSong) => Promise; + +const FETCHERS: Record = { + [LyricSource.GENIUS]: queryGenius, + [LyricSource.NETEASE]: queryNetease, +}; + +ipcMain.on('lyric-fetch', async (_event, song: QueueSong) => { + const sources = store.get('lyrics', []) as LyricSource[]; + + for (const source of sources) { + const lyric = await FETCHERS[source](song); + if (lyric) { + getMainWindow()?.webContents.send('lyric-get', song.name, source, lyric); + break; + } + } +}); diff --git a/src/main/features/core/lyrics/netease.ts b/src/main/features/core/lyrics/netease.ts new file mode 100644 index 00000000..4faeeb43 --- /dev/null +++ b/src/main/features/core/lyrics/netease.ts @@ -0,0 +1,58 @@ +import axios, { AxiosResponse } from 'axios'; +import type { QueueSong } from '/@/renderer/api/types'; + +const SEARCH_URL = 'https://music.163.com/api/search/get'; +const LYRICS_URL = 'https://music.163.com/api/song/lyric'; + +async function getSongId(metadata: QueueSong) { + let result: AxiosResponse; + try { + result = await axios.get(SEARCH_URL, { + params: { + limit: 10, + offset: 0, + s: `${metadata.artistName} ${metadata.name}`, + type: '1', + }, + }); + } catch (e) { + console.error('NetEase search request got an error!', e); + return undefined; + } + + return result?.data.result?.songs?.[0].id; +} + +async function getLyricsFromSongId(songId: string) { + let result: AxiosResponse; + try { + result = await axios.get(LYRICS_URL, { + params: { + id: songId, + kv: '-1', + lv: '-1', + }, + }); + } catch (e) { + console.error('NetEase lyrics request got an error!', e); + return undefined; + } + + return result.data.klyric?.lyric || result.data.lrc?.lyric; +} + +export async function query(metadata: QueueSong): Promise { + const songId = await getSongId(metadata); + if (!songId) { + console.error('Could not find the song on NetEase!'); + return null; + } + + const lyrics = await getLyricsFromSongId(songId); + if (!lyrics) { + console.error('Could not get lyrics on NetEase!'); + return null; + } + + return lyrics; +} diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts index e1e97f85..3f18a778 100644 --- a/src/main/features/core/player/index.ts +++ b/src/main/features/core/player/index.ts @@ -128,3 +128,7 @@ ipcMain.on('player-volume', async (_event, value: number) => { ipcMain.on('player-mute', async () => { await getMpvInstance()?.mute(); }); + +ipcMain.handle('player-get-time', async (): Promise => { + return getMpvInstance()?.getTimePosition(); +}); diff --git a/src/main/preload.ts b/src/main/preload.ts index 996b0e52..7cf38577 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -2,6 +2,7 @@ import { contextBridge } from 'electron'; import { browser } from './preload/browser'; import { ipc } from './preload/ipc'; import { localSettings } from './preload/local-settings'; +import { lyrics } from './preload/lyrics'; import { mpris } from './preload/mpris'; import { mpvPlayer, mpvPlayerListener } from './preload/mpv-player'; import { utils } from './preload/utils'; @@ -10,6 +11,7 @@ contextBridge.exposeInMainWorld('electron', { browser, ipc, localSettings, + lyrics, mpris, mpvPlayer, mpvPlayerListener, diff --git a/src/main/preload/lyrics.ts b/src/main/preload/lyrics.ts new file mode 100644 index 00000000..398d2e38 --- /dev/null +++ b/src/main/preload/lyrics.ts @@ -0,0 +1,17 @@ +import { IpcRendererEvent, ipcRenderer } from 'electron'; +import { QueueSong } from '/@/renderer/api/types'; + +const fetchLyrics = (song: QueueSong) => { + ipcRenderer.send('lyric-fetch', song); +}; + +const getLyrics = ( + cb: (event: IpcRendererEvent, songName: string, source: string, lyric: string) => void, +) => { + ipcRenderer.on('lyric-get', cb); +}; + +export const lyrics = { + fetchLyrics, + getLyrics, +}; diff --git a/src/main/preload/mpv-player.ts b/src/main/preload/mpv-player.ts index d06cc2eb..7442be93 100644 --- a/src/main/preload/mpv-player.ts +++ b/src/main/preload/mpv-player.ts @@ -78,6 +78,10 @@ const quit = () => { ipcRenderer.send('player-quit'); }; +const getCurrentTime = async () => { + return ipcRenderer.invoke('player-get-time'); +}; + const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => { ipcRenderer.on('renderer-player-auto-next', cb); }; @@ -157,6 +161,7 @@ const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => { export const mpvPlayer = { autoNext, currentTime, + getCurrentTime, initialize, mute, next, diff --git a/src/renderer/components/select/index.tsx b/src/renderer/components/select/index.tsx index e477a9a1..d887326c 100644 --- a/src/renderer/components/select/index.tsx +++ b/src/renderer/components/select/index.tsx @@ -10,7 +10,7 @@ interface SelectProps extends MantineSelectProps { width?: number | string; } -interface MultiSelectProps extends MantineMultiSelectProps { +export interface MultiSelectProps extends MantineMultiSelectProps { maxWidth?: number | string; width?: number | string; } diff --git a/src/renderer/components/virtual-table/table-config-dropdown.tsx b/src/renderer/components/virtual-table/table-config-dropdown.tsx index 1a21f5a8..77954ad3 100644 --- a/src/renderer/components/virtual-table/table-config-dropdown.tsx +++ b/src/renderer/components/virtual-table/table-config-dropdown.tsx @@ -2,7 +2,11 @@ import type { ChangeEvent } from 'react'; import { MultiSelect } from '/@/renderer/components/select'; import { Slider } from '/@/renderer/components/slider'; import { Switch } from '/@/renderer/components/switch'; -import { useSettingsStoreActions, useSettingsStore } from '/@/renderer/store/settings.store'; +import { + useSettingsStoreActions, + useSettingsStore, + useLyricsSettings, +} from '/@/renderer/store/settings.store'; import { TableColumn, TableType } from '/@/renderer/types'; import { Option } from '/@/renderer/components/option'; @@ -82,6 +86,7 @@ interface TableConfigDropdownProps { export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => { const { setSettings } = useSettingsStoreActions(); const tableConfig = useSettingsStore((state) => state.tables); + const lyricConfig = useLyricsSettings(); const handleAddOrRemoveColumns = (values: TableColumn[]) => { const existingColumns = tableConfig[type].columns; @@ -166,6 +171,15 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => { }); }; + const handleLyricFollow = (e: ChangeEvent) => { + setSettings({ + lyrics: { + ...useSettingsStore.getState().lyrics, + follow: e.currentTarget.checked, + }, + }); + }; + return ( <> +