From aba7cb302ff0aecbe0a1917b404da391edd3af79 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sat, 3 Feb 2024 22:47:57 -0800 Subject: [PATCH 01/17] add navidrome version check for smart playlists --- package-lock.json | 264 ++++++------------ package.json | 1 + src/renderer/api/controller.ts | 2 +- .../api/navidrome/navidrome-controller.ts | 81 +++++- src/renderer/api/navidrome/navidrome-types.ts | 4 + src/renderer/api/types.ts | 3 + src/renderer/api/utils.ts | 17 ++ .../features/lyrics/queries/lyric-query.ts | 3 +- .../components/create-playlist-form.tsx | 15 +- src/renderer/hooks/use-server-version.ts | 3 +- src/renderer/types.ts | 20 +- 11 files changed, 201 insertions(+), 212 deletions(-) diff --git a/package-lock.json b/package-lock.json index 662bb006..c1a9bd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "react-virtualized-auto-sizer": "^1.0.17", "react-window": "^1.8.9", "react-window-infinite-loader": "^1.0.9", + "semver": "^7.5.4", "styled-components": "^6.0.8", "swiper": "^9.3.1", "zod": "^3.22.3", @@ -5023,7 +5024,7 @@ "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "devOptional": true + "dev": true }, "node_modules/@types/qs": { "version": "6.9.7", @@ -5041,7 +5042,7 @@ "version": "18.0.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", - "devOptional": true, + "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5113,7 +5114,7 @@ "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "devOptional": true + "dev": true }, "node_modules/@types/semver": { "version": "7.3.13", @@ -6445,30 +6446,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-styled-components": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", - "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", - "optional": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.0", - "@babel/helper-module-imports": "^7.16.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11", - "picomatch": "^2.3.0" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", - "optional": true, - "peer": true - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -9979,6 +9956,21 @@ "@mdn/browser-compat-data": "^5.2.34" } }, + "node_modules/eslint-plugin-compat/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-import": { "version": "2.27.5", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", @@ -17952,9 +17944,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -18251,21 +18243,6 @@ "node": ">=10" } }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sirv": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", @@ -21972,8 +21949,7 @@ "@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "requires": {} + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==" }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -22847,8 +22823,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz", "integrity": "sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA==", - "dev": true, - "requires": {} + "dev": true }, "@csstools/css-tokenizer": { "version": "2.2.0", @@ -22860,15 +22835,13 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.4.tgz", "integrity": "sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw==", - "dev": true, - "requires": {} + "dev": true }, "@csstools/selector-specificity": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz", "integrity": "sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==", - "dev": true, - "requires": {} + "dev": true }, "@develar/schema-utils": { "version": "2.6.5", @@ -23453,8 +23426,7 @@ "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", - "requires": {} + "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==" }, "@emotion/utils": { "version": "1.2.0", @@ -24007,8 +23979,7 @@ "@mantine/hooks": { "version": "6.0.17", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-6.0.17.tgz", - "integrity": "sha512-7vf2w1NlzKlUynSuyI2DAIKoEOYKYC8k+tlSsk3BRdbzhbJAiWxcYzJy5seg5dFW1WIpKAZ0wiVdHXf/WRlRgg==", - "requires": {} + "integrity": "sha512-7vf2w1NlzKlUynSuyI2DAIKoEOYKYC8k+tlSsk3BRdbzhbJAiWxcYzJy5seg5dFW1WIpKAZ0wiVdHXf/WRlRgg==" }, "@mantine/modals": { "version": "6.0.17", @@ -24051,8 +24022,7 @@ "@mantine/utils": { "version": "6.0.17", "resolved": "https://registry.npmjs.org/@mantine/utils/-/utils-6.0.17.tgz", - "integrity": "sha512-U6SWV/asYE6NhiHx4ltmVZdQR3HwGVqJxVulhOylMcV1tX/P1LMQUCbGV2Oe4O9jbX4/YW5B/CBb4BbEhENQFQ==", - "requires": {} + "integrity": "sha512-U6SWV/asYE6NhiHx4ltmVZdQR3HwGVqJxVulhOylMcV1tX/P1LMQUCbGV2Oe4O9jbX4/YW5B/CBb4BbEhENQFQ==" }, "@mdn/browser-compat-data": { "version": "5.3.5", @@ -24609,8 +24579,7 @@ "@ts-rest/core": { "version": "3.23.0", "resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.23.0.tgz", - "integrity": "sha512-2vJwa682m9yS/xQPvPxZBluJfIZwNkt2HY9ER3UtGnu8Dijw+8iymSyIyjRLpFFWUyRnVp9IqrEi/d84bkNFIw==", - "requires": {} + "integrity": "sha512-2vJwa682m9yS/xQPvPxZBluJfIZwNkt2HY9ER3UtGnu8Dijw+8iymSyIyjRLpFFWUyRnVp9IqrEi/d84bkNFIw==" }, "@tsconfig/node10": { "version": "1.0.8", @@ -24978,7 +24947,7 @@ "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "devOptional": true + "dev": true }, "@types/qs": { "version": "6.9.7", @@ -24996,7 +24965,7 @@ "version": "18.0.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", - "devOptional": true, + "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -25068,7 +25037,7 @@ "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "devOptional": true + "dev": true }, "@types/semver": { "version": "7.3.13", @@ -25447,8 +25416,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", - "dev": true, - "requires": {} + "dev": true }, "@webpack-cli/info": { "version": "1.4.1", @@ -25463,8 +25431,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", - "dev": true, - "requires": {} + "dev": true }, "@xhayper/discord-rpc": { "version": "1.0.24", @@ -25478,8 +25445,7 @@ "ws": { "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "requires": {} + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==" } } }, @@ -25557,15 +25523,13 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "requires": {} + "dev": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "7.2.0", @@ -25651,8 +25615,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "ansi-escapes": { "version": "4.3.2", @@ -26095,27 +26058,6 @@ "@babel/helper-define-polyfill-provider": "^0.4.2" } }, - "babel-plugin-styled-components": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", - "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", - "optional": true, - "peer": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.0", - "@babel/helper-module-imports": "^7.16.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11", - "picomatch": "^2.3.0" - } - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", - "optional": true, - "peer": true - }, "babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -26374,8 +26316,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/browserslist-config-erb/-/browserslist-config-erb-0.0.3.tgz", "integrity": "sha512-y47DryCY92lxkKyRVMlaZvXAolIY7U33q9e4CS0MdWeJkoAht7OzsrkfdZFCBOP3H5q1EVUxS0L7VVsKM6gZCQ==", - "dev": true, - "requires": {} + "dev": true }, "bs-logger": { "version": "0.2.6", @@ -27215,8 +27156,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz", "integrity": "sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg==", - "dev": true, - "requires": {} + "dev": true }, "css-functions-list": { "version": "3.2.0", @@ -27412,8 +27352,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -28685,8 +28624,7 @@ "version": "8.8.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.7", @@ -28803,6 +28741,15 @@ "requires": { "@mdn/browser-compat-data": "^5.2.34" } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } } } }, @@ -28918,8 +28865,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-react": { "version": "7.33.0", @@ -28976,8 +28922,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-sort-keys-fix": { "version": "1.1.2", @@ -30572,8 +30517,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "idb-keyval": { "version": "6.2.1", @@ -31404,8 +31348,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "27.5.1", @@ -32935,8 +32878,7 @@ "overlayscrollbars-react": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.1.tgz", - "integrity": "sha512-0xw9J1CT/cQ+ELYy3hudG6nY1H5dgJ1DdVW3d8aZwqx6wyHNZV4nsBQXUxoHmPo3dmlJ5MvOLzpKWA4X6nL4QA==", - "requires": {} + "integrity": "sha512-0xw9J1CT/cQ+ELYy3hudG6nY1H5dgJ1DdVW3d8aZwqx6wyHNZV4nsBQXUxoHmPo3dmlJ5MvOLzpKWA4X6nL4QA==" }, "p-cancelable": { "version": "2.1.1", @@ -33317,29 +33259,25 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-media-query-parser": { "version": "0.2.3", @@ -33413,8 +33351,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -33449,8 +33386,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -33565,15 +33501,13 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-scss": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-selector-parser": { "version": "6.0.13", @@ -33618,8 +33552,7 @@ "version": "0.36.2", "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true, - "requires": {} + "dev": true }, "postcss-unique-selectors": { "version": "5.1.1", @@ -33964,8 +33897,7 @@ "react-icons": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", - "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", - "requires": {} + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==" }, "react-is": { "version": "17.0.2", @@ -34002,8 +33934,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-refresh-typescript/-/react-refresh-typescript-2.0.4.tgz", "integrity": "sha512-ySsBExEFik5Jjf7NoXtFbzUk2rYWM4gF5gg+wRTNmp9p7B2uMpAAa339FHWqmB8EAr0e6mzzskAXxc0Jd04fBw==", - "dev": true, - "requires": {} + "dev": true }, "react-remove-scroll": { "version": "2.5.5", @@ -34056,8 +33987,7 @@ "react-simple-img": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-simple-img/-/react-simple-img-3.0.0.tgz", - "integrity": "sha512-I0sG/GgY9c+04BgWf1YRlipWBQxR3oG2s/bagU8EO7zals3/Vkfk1PJMeYh/wHfjxJtUmal+y7HWEBm4MzXVsQ==", - "requires": {} + "integrity": "sha512-I0sG/GgY9c+04BgWf1YRlipWBQxR3oG2s/bagU8EO7zals3/Vkfk1PJMeYh/wHfjxJtUmal+y7HWEBm4MzXVsQ==" }, "react-style-singleton": { "version": "2.2.1", @@ -34112,8 +34042,7 @@ "react-virtualized-auto-sizer": { "version": "1.0.20", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz", - "integrity": "sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==", - "requires": {} + "integrity": "sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==" }, "react-window": { "version": "1.8.9", @@ -34134,8 +34063,7 @@ "react-window-infinite-loader": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-1.0.9.tgz", - "integrity": "sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw==", - "requires": {} + "integrity": "sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw==" }, "read-config-file": { "version": "6.3.2", @@ -34650,9 +34578,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -34895,17 +34823,6 @@ "dev": true, "requires": { "semver": "^7.5.3" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "sirv": { @@ -35293,8 +35210,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true, - "requires": {} + "dev": true }, "style-search": { "version": "0.1.0", @@ -35503,8 +35419,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-8.0.2.tgz", "integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==", - "dev": true, - "requires": {} + "dev": true }, "stylelint-order": { "version": "6.0.3", @@ -35522,8 +35437,7 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz", "integrity": "sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==", - "dev": true, - "requires": {} + "dev": true }, "stylelint-config-standard": { "version": "34.0.0", @@ -35559,8 +35473,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz", "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -35577,8 +35490,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz", "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==", - "dev": true, - "requires": {} + "dev": true } } } @@ -36297,8 +36209,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/typescript-plugin-styled-components/-/typescript-plugin-styled-components-3.0.0.tgz", "integrity": "sha512-QWlhTl6NqsFxtJyxn7pJjm3RhgzXSByUftZ3AoQClrMMpa4yAaHuJKTN1gFpH3Ti+Rwm56fNUfG9pXSBU+WW3A==", - "dev": true, - "requires": {} + "dev": true }, "unbox-primitive": { "version": "1.0.2", @@ -36454,14 +36365,12 @@ "use-composed-ref": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", - "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", - "requires": {} + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==" }, "use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", - "requires": {} + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==" }, "use-latest": { "version": "1.2.1", @@ -36483,8 +36392,7 @@ "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "requires": {} + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" }, "utf8-byte-length": { "version": "1.0.4", @@ -37012,8 +36920,7 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -37162,8 +37069,7 @@ "version": "7.5.7", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", - "dev": true, - "requires": {} + "dev": true }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index adb41546..8b2e6854 100644 --- a/package.json +++ b/package.json @@ -345,6 +345,7 @@ "react-virtualized-auto-sizer": "^1.0.17", "react-window": "^1.8.9", "react-window-infinite-loader": "^1.0.9", + "semver": "^7.5.4", "styled-components": "^6.0.8", "swiper": "^9.3.1", "zod": "^3.22.3", diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 6cd4d623..6d203a98 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -173,7 +173,7 @@ const endpoints: ApiController = { getPlaylistList: ndController.getPlaylistList, getPlaylistSongList: ndController.getPlaylistSongList, getRandomSongList: ssController.getRandomSongList, - getServerInfo: ssController.getServerInfo, + getServerInfo: ndController.getServerInfo, getSongDetail: ndController.getSongDetail, getSongList: ndController.getSongList, getStructuredLyrics: ssController.getStructuredLyrics, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 486cb1bc..162be26a 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -1,3 +1,9 @@ +import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api'; +import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize'; +import { NavidromeExtensions, ndType } from '/@/renderer/api/navidrome/navidrome-types'; +import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; +import semverCoerce from 'semver/functions/coerce'; +import semverGte from 'semver/functions/gte'; import { AlbumArtistDetailArgs, AlbumArtistDetailResponse, @@ -39,11 +45,10 @@ import { RemoveFromPlaylistResponse, RemoveFromPlaylistArgs, genreListSortMap, + ServerInfo, + ServerInfoArgs, } from '../types'; -import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api'; -import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize'; -import { ndType } from '/@/renderer/api/navidrome/navidrome-types'; -import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; +import { hasFeature } from '/@/renderer/api/utils'; const authenticate = async ( url: string, @@ -355,6 +360,16 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise => { const { query, apiClientProps } = args; + const customQuery = query._custom?.navidrome; + + // Smart playlists only became available in 0.48.0. Do not filter for previous versions + if ( + customQuery && + customQuery.smart !== undefined && + !hasFeature(apiClientProps.server, NavidromeExtensions.SMART_PLAYLISTS) + ) { + customQuery.smart = undefined; + } const res = await ndApiClient(apiClientProps).getPlaylistList({ query: { @@ -363,7 +378,7 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise]> = [ + ['0.48.0', { [NavidromeExtensions.SMART_PLAYLISTS]: [1] }], +]; + +const getFeatures = (version: string): Record => { + const cleanVersion = semverCoerce(version); + const features: Record = {}; + let matched = cleanVersion === null; + + for (const [version, supportedFeatures] of VERSION_INFO) { + if (!matched) { + matched = semverGte(cleanVersion!, version); + } + + if (matched) { + for (const [feature, feat] of Object.entries(supportedFeatures)) { + if (feature in features) { + features[feature].push(...feat); + } else { + features[feature] = feat; + } + } + } + } + + return features; +}; + +const getServerInfo = async (args: ServerInfoArgs): Promise => { + const { apiClientProps } = args; + + // Navidrome will always populate serverVersion + const ping = await ssApiClient(apiClientProps).ping(); + + if (ping.status !== 200) { + throw new Error('Failed to ping server'); + } + + const features: Record = getFeatures(ping.body.serverVersion!); + + if (ping.body.openSubsonic) { + const res = await ssApiClient(apiClientProps).getServerInfo(); + + if (res.status !== 200) { + throw new Error('Failed to get server extensions'); + } + + for (const extension of res.body.openSubsonicExtensions) { + features[extension.name] = extension.versions; + } + } + + return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! }; +}; + export const ndController = { addToPlaylist, authenticate, @@ -478,6 +548,7 @@ export const ndController = { getPlaylistDetail, getPlaylistList, getPlaylistSongList, + getServerInfo, getSongDetail, getSongList, getUserList, diff --git a/src/renderer/api/navidrome/navidrome-types.ts b/src/renderer/api/navidrome/navidrome-types.ts index d01174f6..9e8f4e43 100644 --- a/src/renderer/api/navidrome/navidrome-types.ts +++ b/src/renderer/api/navidrome/navidrome-types.ts @@ -342,6 +342,10 @@ const removeFromPlaylistParameters = z.object({ id: z.array(z.string()), }); +export enum NavidromeExtensions { + SMART_PLAYLISTS = 'smartPlaylists', +} + export const ndType = { _enum: { albumArtistList: ndAlbumArtistListSort, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 7241d82e..9a8fb815 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -57,13 +57,16 @@ export type User = { export type ServerListItem = { credential: string; + features?: Record; id: string; name: string; ndCredential?: string; + savePassword?: boolean; type: ServerType; url: string; userId: string | null; username: string; + version?: string; }; export enum ServerType { diff --git a/src/renderer/api/utils.ts b/src/renderer/api/utils.ts index 0063fae9..5c676bb5 100644 --- a/src/renderer/api/utils.ts +++ b/src/renderer/api/utils.ts @@ -38,3 +38,20 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => { useAuthStore.getState().actions.setCurrentServer(null); } }; + +export const hasFeature = ( + server: ServerListItem | null, + feature: string, + version = 1, +): boolean => { + if (!server || !server.features) { + return false; + } + + const versions = server.features[feature]; + if (!versions || versions.length === 0) { + return false; + } + + return versions.includes(version); +}; diff --git a/src/renderer/features/lyrics/queries/lyric-query.ts b/src/renderer/features/lyrics/queries/lyric-query.ts index a02da327..66e5c74d 100644 --- a/src/renderer/features/lyrics/queries/lyric-query.ts +++ b/src/renderer/features/lyrics/queries/lyric-query.ts @@ -15,6 +15,7 @@ import { queryKeys } from '/@/renderer/api/query-keys'; import { ServerType } from '/@/renderer/types'; import { api } from '/@/renderer/api'; import isElectron from 'is-electron'; +import { hasFeature } from '/@/renderer/api/utils'; const lyricsIpc = isElectron() ? window.electron.lyrics : null; @@ -112,7 +113,7 @@ export const useSongLyricsBySong = ( source: server?.name ?? 'music server', }; } - } else if (server.features && SubsonicExtensions.SONG_LYRICS in server.features) { + } else if (hasFeature(server, SubsonicExtensions.SONG_LYRICS)) { const subsonicLyrics = await api.controller .getStructuredLyrics({ apiClientProps: { server, signal }, diff --git a/src/renderer/features/playlists/components/create-playlist-form.tsx b/src/renderer/features/playlists/components/create-playlist-form.tsx index 63c9fc47..e597292b 100644 --- a/src/renderer/features/playlists/components/create-playlist-form.tsx +++ b/src/renderer/features/playlists/components/create-playlist-form.tsx @@ -11,6 +11,8 @@ import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/crea import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils'; import { useCurrentServer } from '/@/renderer/store'; import { useTranslation } from 'react-i18next'; +import { hasFeature } from '/@/renderer/api/utils'; +import { NavidromeExtensions } from '/@/renderer/api/navidrome/navidrome-types'; interface CreatePlaylistFormProps { onCancel: () => void; @@ -120,12 +122,13 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => { })} /> )} - {server?.type === ServerType.NAVIDROME && ( - setIsSmartPlaylist(e.currentTarget.checked)} - /> - )} + {server?.type === ServerType.NAVIDROME && + hasFeature(server, NavidromeExtensions.SMART_PLAYLISTS) && ( + setIsSmartPlaylist(e.currentTarget.checked)} + /> + )} {server?.type === ServerType.NAVIDROME && isSmartPlaylist && ( diff --git a/src/renderer/hooks/use-server-version.ts b/src/renderer/hooks/use-server-version.ts index 6ca1327d..5565457e 100644 --- a/src/renderer/hooks/use-server-version.ts +++ b/src/renderer/hooks/use-server-version.ts @@ -3,6 +3,7 @@ import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store'; import { useQuery } from '@tanstack/react-query'; import { queryKeys } from '/@/renderer/api/query-keys'; import { controller } from '/@/renderer/api/controller'; +import isEqual from 'lodash/isEqual'; export const useServerVersion = () => { const { updateServer } = useAuthStoreActions(); @@ -24,7 +25,7 @@ export const useServerVersion = () => { useEffect(() => { if (server && server.id === serverInfo.data?.id) { const { version, features } = serverInfo.data; - if (version !== server.version) { + if (version !== server.version || !isEqual(features, server.features)) { updateServer(server.id, { features, version, diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 5df65673..0f4e400a 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -54,25 +54,7 @@ export enum Platform { WINDOWS = 'windows', } -export enum ServerType { - JELLYFIN = 'jellyfin', - NAVIDROME = 'navidrome', - SUBSONIC = 'subsonic', -} - -export type ServerListItem = { - credential: string; - features?: Record; - id: string; - name: string; - ndCredential?: string; - savePassword?: boolean; - type: ServerType; - url: string; - userId: string | null; - username: string; - version?: string; -}; +export { ServerType, ServerListItem } from '/@/renderer/api/types'; export enum PlayerStatus { PAUSED = 'paused', From b2fce071a9b61594ce13c376ea18b708a6d8ff9a Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:37:49 -0800 Subject: [PATCH 02/17] [bugfix]: Check for Navidrome authentication on startup Resolves #403. This PR introduces a startup check for Navidrome that tries a simple API request (/songs) before loading homepage. If the check fails, Navidrome API will fallback to trying saved password (if available). Notes: - It might also be worthwhile to do a periodic poll? --- .../hooks/use-server-authenticated.ts | 54 +++++++++++++++++++ src/renderer/router/app-outlet.tsx | 11 +++- src/renderer/types.ts | 6 +++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/renderer/hooks/use-server-authenticated.ts diff --git a/src/renderer/hooks/use-server-authenticated.ts b/src/renderer/hooks/use-server-authenticated.ts new file mode 100644 index 00000000..3696595c --- /dev/null +++ b/src/renderer/hooks/use-server-authenticated.ts @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCurrentServer } from '/@/renderer/store'; +import { AuthState, ServerListItem, ServerType } from '/@/renderer/types'; +import { api } from '/@/renderer/api'; +import { SongListSort, SortOrder } from '/@/renderer/api/types'; +import { debounce } from 'lodash'; + +export const useServerAuthenticated = () => { + const priorServerId = useRef(); + const server = useCurrentServer(); + const [ready, setReady] = useState( + server?.type === ServerType.NAVIDROME ? AuthState.LOADING : AuthState.VALID, + ); + + const authenticateNavidrome = useCallback(async (server: ServerListItem) => { + // This trick works because navidrome-api.ts will internally check for authentication + // failures and try to log in again (where available). So, all that's necessary is + // making one request first + try { + await api.controller.getSongList({ + apiClientProps: { server }, + query: { + limit: 1, + sortBy: SongListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, + }, + }); + + setReady(AuthState.VALID); + } catch (error) { + setReady(AuthState.INVALID); + } + }, []); + + const debouncedAuth = debounce((server: ServerListItem) => { + authenticateNavidrome(server).catch(console.error); + }, 300); + + useEffect(() => { + if (priorServerId.current !== server?.id) { + priorServerId.current = server?.id || ''; + + if (server?.type === ServerType.NAVIDROME) { + setReady(AuthState.LOADING); + debouncedAuth(server); + } else { + setReady(AuthState.VALID); + } + } + }, [debouncedAuth, server]); + + return ready; +}; diff --git a/src/renderer/router/app-outlet.tsx b/src/renderer/router/app-outlet.tsx index d77a8981..8e1c52a0 100644 --- a/src/renderer/router/app-outlet.tsx +++ b/src/renderer/router/app-outlet.tsx @@ -3,7 +3,9 @@ import isElectron from 'is-electron'; import { Navigate, Outlet } from 'react-router-dom'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store'; -import { toast } from '/@/renderer/components'; +import { Spinner, toast } from '/@/renderer/components'; +import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated'; +import { AuthState } from '/@/renderer/types'; const ipc = isElectron() ? window.electron.ipc : null; const utils = isElectron() ? window.electron.utils : null; @@ -12,6 +14,7 @@ const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : nul export const AppOutlet = () => { const currentServer = useCurrentServer(); const setFallback = useSetPlayerFallback(); + const authState = useServerAuthenticated(); const isActionsRequired = useMemo(() => { const isServerRequired = !currentServer; @@ -37,7 +40,11 @@ export const AppOutlet = () => { }; }, [setFallback]); - if (isActionsRequired) { + if (authState === AuthState.LOADING) { + return ; + } + + if (isActionsRequired || authState === AuthState.INVALID) { return ( Date: Mon, 19 Feb 2024 13:55:11 -0800 Subject: [PATCH 03/17] [bugfix]: carousel fixes --- .../components/grid-carousel/index.tsx | 65 +++++++++++++------ 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/src/renderer/components/grid-carousel/index.tsx b/src/renderer/components/grid-carousel/index.tsx index 2f31aae5..7e0d51cc 100644 --- a/src/renderer/components/grid-carousel/index.tsx +++ b/src/renderer/components/grid-carousel/index.tsx @@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect, + useLayoutEffect, useMemo, useRef, useState, @@ -117,6 +118,7 @@ export const SwiperGridCarousel = ({ const swiperRef = useRef(null); const playButtonBehavior = usePlayButtonBehavior(); const handlePlayQueueAdd = usePlayQueueAdd(); + const [slideCount, setSlideCount] = useState(4); useEffect(() => { swiperRef.current?.slideTo(0, 0); @@ -191,23 +193,24 @@ export const SwiperGridCarousel = ({ const handleNext = useCallback(() => { const activeIndex = swiperRef?.current?.activeIndex || 0; - const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4)); + const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount)); swiperRef?.current?.slideTo(activeIndex + slidesPerView); - }, [swiperProps?.slidesPerView]); + }, [slideCount, swiperProps?.slidesPerView]); const handlePrev = useCallback(() => { const activeIndex = swiperRef?.current?.activeIndex || 0; - const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4)); + const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount)); swiperRef?.current?.slideTo(activeIndex - slidesPerView); - }, [swiperProps?.slidesPerView]); + }, [slideCount, swiperProps?.slidesPerView]); const handleOnSlideChange = useCallback((e: SwiperCore) => { const { slides, isEnd, isBeginning, params } = e; if (isEnd || isBeginning) return; + const slideCount = (params.slidesPerView as number | undefined) || 4; setPagination({ - hasNextPage: (params?.slidesPerView || 4) < slides.length, - hasPreviousPage: (params?.slidesPerView || 4) < slides.length, + hasNextPage: slideCount < slides.length, + hasPreviousPage: slideCount < slides.length, }); }, []); @@ -215,39 +218,61 @@ export const SwiperGridCarousel = ({ const { slides, isEnd, isBeginning, params } = e; if (isEnd || isBeginning) return; + const slideCount = (params.slidesPerView as number | undefined) || 4; setPagination({ - hasNextPage: (params.slidesPerView || 4) < slides.length, - hasPreviousPage: (params.slidesPerView || 4) < slides.length, + hasNextPage: slideCount < slides.length, + hasPreviousPage: slideCount < slides.length, }); }, []); const handleOnReachEnd = useCallback((e: SwiperCore) => { const { slides, params } = e; + const slideCount = (params.slidesPerView as number | undefined) || 4; setPagination({ hasNextPage: false, - hasPreviousPage: (params.slidesPerView || 4) < slides.length, + hasPreviousPage: slideCount < slides.length, }); }, []); const handleOnReachBeginning = useCallback((e: SwiperCore) => { const { slides, params } = e; + const slideCount = (params.slidesPerView as number | undefined) || 4; setPagination({ - hasNextPage: (params.slidesPerView || 4) < slides.length, + hasNextPage: slideCount < slides.length, hasPreviousPage: false, }); }, []); - const handleOnResize = useCallback((e: SwiperCore) => { - if (!e) return; - const { width } = e; - const slidesPerView = getSlidesPerView(width); - if (!e.params) return; - e.params.slidesPerView = slidesPerView; - }, []); + useLayoutEffect(() => { + const handleResize = () => { + const { activeIndex, params, slides, width } = + (swiperRef.current as SwiperCore | undefined) ?? {}; - const throttledOnResize = throttle(handleOnResize, 200); + if (width) { + const slidesPerView = getSlidesPerView(width); + setSlideCount(slidesPerView); + } + + if (activeIndex !== undefined && slides && params?.slidesPerView) { + const slideCount = (params.slidesPerView as number | undefined) || 4; + setPagination({ + hasNextPage: activeIndex + slideCount < slides.length, + hasPreviousPage: activeIndex > 0, + }); + } + }; + + handleResize(); + + const throttledResize = throttle(handleResize, 200); + window.addEventListener('resize', throttledResize); + + return () => { + window.removeEventListener('resize', throttledResize); + }; + }, []); return ( { swiperRef.current = swiper; }} - onBeforeResize={handleOnResize} onReachBeginning={handleOnReachBeginning} onReachEnd={handleOnReachEnd} - onResize={throttledOnResize} onSlideChange={handleOnSlideChange} onZoomChange={handleOnZoomChange} {...swiperProps} From 860dd8b499f8e4acc07a987fb8f3bfcfea18739e Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:34:36 -0800 Subject: [PATCH 04/17] [enhancement]: add codec column for tracks --- src/i18n/locales/en.json | 3 +++ src/renderer/components/virtual-table/index.tsx | 8 ++++++++ .../components/virtual-table/table-config-dropdown.tsx | 4 ++++ src/renderer/types.ts | 1 + 4 files changed, 16 insertions(+) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 34b77a0b..8c6951cf 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -38,6 +38,7 @@ "channel_other": "channels", "clear": "clear", "close": "close", + "codec": "codec", "collapse": "collapse", "comingSoon": "coming soon…", "configure": "configure", @@ -592,6 +593,7 @@ "bitrate": "bitrate", "bpm": "bpm", "channels": "$t(common.channel_other)", + "codec": "$t(common.codec)", "comment": "comment", "dateAdded": "date added", "discNumber": "disc", @@ -626,6 +628,7 @@ "bitrate": "$t(common.bitrate)", "bpm": "$t(common.bpm)", "channels": "$t(common.channel_other)", + "codec": "$t(common.codec)", "dateAdded": "date added", "discNumber": "disc number", "duration": "$t(common.duration)", diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index 1f1039b6..42dc722a 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -158,6 +158,14 @@ const tableColumns: { [key: string]: ColDef } = { params.data ? params.data.channels : undefined, width: 100, }, + codec: { + cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }), + colId: TableColumn.CODEC, + headerName: i18n.t('table.column.codec'), + valueGetter: (params: ValueGetterParams) => + params.data ? params.data.container : undefined, + width: 60, + }, comment: { cellRenderer: NoteCell, colId: TableColumn.COMMENT, diff --git a/src/renderer/components/virtual-table/table-config-dropdown.tsx b/src/renderer/components/virtual-table/table-config-dropdown.tsx index 199534a5..6f09ff13 100644 --- a/src/renderer/components/virtual-table/table-config-dropdown.tsx +++ b/src/renderer/components/virtual-table/table-config-dropdown.tsx @@ -60,6 +60,10 @@ export const SONG_TABLE_COLUMNS = [ label: i18n.t('table.config.label.bitrate', { postProcess: 'titleCase' }), value: TableColumn.BIT_RATE, }, + { + label: i18n.t('table.config.label.codec', { postProcess: 'titleCase' }), + value: TableColumn.CODEC, + }, { label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }), value: TableColumn.LAST_PLAYED, diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 5df65673..4845d754 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -144,6 +144,7 @@ export enum TableColumn { BIT_RATE = 'bitRate', BPM = 'bpm', CHANNELS = 'channels', + CODEC = 'codec', COMMENT = 'comment', DATE_ADDED = 'dateAdded', DISC_NUMBER = 'discNumber', From 77e220c873cb304b16c730ecdace27e013e08ac5 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 19 Feb 2024 23:04:41 -0800 Subject: [PATCH 05/17] [enhancement]: add codec column for tracks --- .../components/grid-carousel/index.tsx | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/renderer/components/grid-carousel/index.tsx b/src/renderer/components/grid-carousel/index.tsx index 7e0d51cc..8ae02e66 100644 --- a/src/renderer/components/grid-carousel/index.tsx +++ b/src/renderer/components/grid-carousel/index.tsx @@ -115,6 +115,7 @@ export const SwiperGridCarousel = ({ isLoading, uniqueId, }: SwiperGridCarouselProps) => { + const containerRef = useRef(null); const swiperRef = useRef(null); const playButtonBehavior = usePlayButtonBehavior(); const handlePlayQueueAdd = usePlayQueueAdd(); @@ -247,7 +248,9 @@ export const SwiperGridCarousel = ({ useLayoutEffect(() => { const handleResize = () => { - const { activeIndex, params, slides, width } = + // Use the container div ref and not swiper width, as this value is more accurate + const width = containerRef.current?.clientWidth; + const { activeIndex, params, slides } = (swiperRef.current as SwiperCore | undefined) ?? {}; if (width) { @@ -279,41 +282,43 @@ export const SwiperGridCarousel = ({ className="grid-carousel" spacing="md" > - {title ? ( - - ) : null} - <Swiper - ref={swiperRef} - resizeObserver - modules={[Virtual]} - slidesPerView={slideCount} - spaceBetween={20} - style={{ height: '100%', width: '100%' }} - onBeforeInit={(swiper) => { - swiperRef.current = swiper; - }} - onReachBeginning={handleOnReachBeginning} - onReachEnd={handleOnReachEnd} - onSlideChange={handleOnSlideChange} - onZoomChange={handleOnZoomChange} - {...swiperProps} - > - {slides.map((slideContent, index) => { - return ( - <SwiperSlide - key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`} - virtualIndex={index} - > - {slideContent} - </SwiperSlide> - ); - })} - </Swiper> + <div ref={containerRef}> + {title ? ( + <Title + {...title} + handleNext={handleNext} + handlePrev={handlePrev} + pagination={pagination} + /> + ) : null} + <Swiper + ref={swiperRef} + resizeObserver + modules={[Virtual]} + slidesPerView={slideCount} + spaceBetween={20} + style={{ height: '100%', width: '100%' }} + onBeforeInit={(swiper) => { + swiperRef.current = swiper; + }} + onReachBeginning={handleOnReachBeginning} + onReachEnd={handleOnReachEnd} + onSlideChange={handleOnSlideChange} + onZoomChange={handleOnZoomChange} + {...swiperProps} + > + {slides.map((slideContent, index) => { + return ( + <SwiperSlide + key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`} + virtualIndex={index} + > + {slideContent} + </SwiperSlide> + ); + })} + </Swiper> + </div> </CarouselContainer> ); }; From 77fa723cf8807611cd6d93cc53b85e768e98228b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 03:49:44 +0000 Subject: [PATCH 06/17] Bump the npm_and_yarn group across 2 directories with 1 update (#521) Bumps the npm_and_yarn group with 1 update in the /. directory: [ip](https://github.com/indutny/node-ip). Updates `ip` from 1.1.5 to 1.1.9 - [Commits](https://github.com/indutny/node-ip/compare/v1.1.5...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect dependency-group: npm_and_yarn-security-group ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5689d9db..fca89bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12555,9 +12555,9 @@ } }, "node_modules/ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, "node_modules/ipaddr.js": { @@ -30704,9 +30704,9 @@ } }, "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, "ipaddr.js": { From 5caf0d439f1d42e607d2a57d234ce7ff5d64a84d Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:31:17 +0000 Subject: [PATCH 07/17] [enhancement]: Start minimized (#522) * [enhancement]: support starting minimized * show window when dock clicked macos --- src/i18n/locales/en.json | 2 ++ src/main/main.ts | 15 +++++++---- .../components/window/window-settings.tsx | 25 +++++++++++++++++++ src/renderer/store/settings.store.ts | 2 ++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8c6951cf..438e44a2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -568,6 +568,8 @@ "skipDuration_description": "sets the duration to skip when using the skip buttons on the player bar", "skipPlaylistPage": "skip playlist page", "skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page", + "startMinimized": "start minimized", + "startMinimized_description": "start the application in system tray", "theme": "theme", "theme_description": "sets the theme to use for the application", "themeDark": "theme (dark)", diff --git a/src/main/main.ts b/src/main/main.ts index d87aaf36..107c6562 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -206,7 +206,7 @@ const createTray = () => { tray.setContextMenu(contextMenu); }; -const createWindow = async () => { +const createWindow = async (first = true) => { if (isDevelopment) { await installExtensions(); } @@ -350,13 +350,14 @@ const createWindow = async () => { mainWindow.loadURL(resolveHtmlPath('index.html')); + const startWindowMinimized = store.get('window_start_minimized', false) as boolean; + mainWindow.on('ready-to-show', () => { if (!mainWindow) { throw new Error('"mainWindow" is not defined'); } - if (process.env.START_MINIMIZED) { - mainWindow.minimize(); - } else { + + if (!first || !startWindowMinimized) { mainWindow.show(); createWinThumbarButtons(); } @@ -608,7 +609,11 @@ if (!singleInstance) { app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. - if (mainWindow === null) createWindow(); + if (mainWindow === null) createWindow(false); + else if (!mainWindow.isVisible()) { + mainWindow.show(); + createWinThumbarButtons(); + } }); }) .catch(console.log); diff --git a/src/renderer/features/settings/components/window/window-settings.tsx b/src/renderer/features/settings/components/window/window-settings.tsx index 899b328b..2b60be69 100644 --- a/src/renderer/features/settings/components/window/window-settings.tsx +++ b/src/renderer/features/settings/components/window/window-settings.tsx @@ -131,6 +131,31 @@ export const WindowSettings = () => { isHidden: !isElectron(), title: t('setting.exitToTray', { postProcess: 'sentenceCase' }), }, + { + control: ( + <Switch + aria-label="Toggle start in tray" + defaultChecked={settings.startMinimized} + disabled={!isElectron()} + onChange={(e) => { + if (!e) return; + localSettings?.set('window_start_minimized', e.currentTarget.checked); + setSettings({ + window: { + ...settings, + startMinimized: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.startMinimized', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.startMinimized', { postProcess: 'sentenceCase' }), + }, ]; return <SettingsSection options={windowOptions} />; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index bf475b19..1d5aed8c 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -267,6 +267,7 @@ export interface SettingsState { disableAutoUpdate: boolean; exitToTray: boolean; minimizeToTray: boolean; + startMinimized: boolean; windowBarStyle: Platform; }; } @@ -575,6 +576,7 @@ const initialState: SettingsState = { disableAutoUpdate: false, exitToTray: false, minimizeToTray: false, + startMinimized: false, windowBarStyle: platformDefaultWindowBarStyle, }, }; From 28bb6990242b87b963bbf9c1968a8e5e04629716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Rodr=C3=ADguez?= <alberto_kakolukylla@hotmail.com> Date: Sat, 24 Feb 2024 07:55:23 +0100 Subject: [PATCH 08/17] Add a pre-defined server for the docker version (#413) * Moved build to docker stage. * Do not copy node_modules to the docker image * Optimize Docker builds * Lock a predefined server with enviroment variables * Added a example docker compose file * Removed useless layer * Fix error with empty server type * pass process via preload, use file, strict server check * remove duplicate content-type * update readme, docker compose * bugfix: server lock false, not jellyfin * fix preload type definition * fix docker, web server lock check --------- Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com> --- .dockerignore | 3 + .erb/configs/webpack.config.renderer.dev.ts | 313 +++++++++--------- .erb/configs/webpack.config.renderer.prod.ts | 197 +++++------ .erb/configs/webpack.config.renderer.web.ts | 3 + .erb/configs/webpack.config.web.prod.ts | 3 + Dockerfile | 6 +- README.md | 2 + docker-compose.yaml | 13 + ng.conf.template | 8 + package.json | 2 +- settings.js.template | 1 + src/main/preload/local-settings.ts | 13 +- .../servers/components/add-server-form.tsx | 27 +- src/renderer/index.ejs | 3 + src/renderer/preload.d.ts | 4 + src/renderer/types.ts | 11 + 16 files changed, 348 insertions(+), 261 deletions(-) create mode 100644 .dockerignore create mode 100644 docker-compose.yaml create mode 100644 settings.js.template diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8a678147 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +Dockerfile +docker-compose.* diff --git a/.erb/configs/webpack.config.renderer.dev.ts b/.erb/configs/webpack.config.renderer.dev.ts index f6ca25d8..46e51530 100644 --- a/.erb/configs/webpack.config.renderer.dev.ts +++ b/.erb/configs/webpack.config.renderer.dev.ts @@ -16,7 +16,7 @@ import webpackPaths from './webpack.paths'; // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's // at the dev webpack config is not accidentally run in a production environment if (process.env.NODE_ENV === 'production') { - checkNodeEnv('development'); + checkNodeEnv('development'); } const port = process.env.PORT || 4343; @@ -28,171 +28,174 @@ const requiredByDLLConfig = module.parent!.filename.includes('webpack.config.ren * Warn if the DLL is not built */ if (!requiredByDLLConfig && !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))) { - console.log( - chalk.black.bgYellow.bold( - 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"', - ), - ); - execSync('npm run postinstall'); + console.log( + chalk.black.bgYellow.bold( + 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"', + ), + ); + execSync('npm run postinstall'); } const configuration: webpack.Configuration = { - devtool: 'inline-source-map', + devtool: 'inline-source-map', - mode: 'development', + mode: 'development', - target: ['web', 'electron-renderer'], + target: ['web', 'electron-renderer'], - entry: [ - `webpack-dev-server/client?http://localhost:${port}/dist`, - 'webpack/hot/only-dev-server', - path.join(webpackPaths.srcRendererPath, 'index.tsx'), - ], - - output: { - path: webpackPaths.distRendererPath, - publicPath: '/', - filename: 'renderer.dev.js', - library: { - type: 'umd', - }, - }, - - module: { - rules: [ - { - test: /\.s?css$/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - modules: { - localIdentName: '[name]__[local]--[hash:base64:5]', - exportLocalsConvention: 'camelCaseOnly', - }, - sourceMap: true, - importLoaders: 1, - }, - }, - 'sass-loader', - ], - include: /\.module\.s?(c|a)ss$/, - }, - { - test: /\.s?css$/, - use: ['style-loader', 'css-loader', 'sass-loader'], - exclude: /\.module\.s?(c|a)ss$/, - }, - // Fonts - { - test: /\.(woff|woff2|eot|ttf|otf)$/i, - type: 'asset/resource', - }, - // Images - { - test: /\.(png|svg|jpg|jpeg|gif)$/i, - type: 'asset/resource', - }, + entry: [ + `webpack-dev-server/client?http://localhost:${port}/dist`, + 'webpack/hot/only-dev-server', + path.join(webpackPaths.srcRendererPath, 'index.tsx'), ], - }, - plugins: [ - ...(requiredByDLLConfig - ? [] - : [ - new webpack.DllReferencePlugin({ - context: webpackPaths.dllPath, - manifest: require(manifest), - sourceType: 'var', - }), - ]), - new webpack.NoEmitOnErrorsPlugin(), - - /** - * Create global constants which can be configured at compile time. - * - * Useful for allowing different behaviour between development builds and - * release builds - * - * NODE_ENV should be production so that modules do not perform certain - * development checks - * - * By default, use 'development' as NODE_ENV. This can be overriden with - * 'staging', for example, by changing the ENV variables in the npm scripts - */ - new webpack.EnvironmentPlugin({ - NODE_ENV: 'development', - }), - - new webpack.LoaderOptionsPlugin({ - debug: true, - }), - - new ReactRefreshWebpackPlugin(), - - new HtmlWebpackPlugin({ - filename: path.join('index.html'), - template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: false, - env: process.env.NODE_ENV, - isDevelopment: process.env.NODE_ENV !== 'production', - nodeModules: webpackPaths.appNodeModulesPath, - }), - ], - - node: { - __dirname: false, - __filename: false, - }, - - devServer: { - port, - compress: true, - hot: true, - headers: { 'Access-Control-Allow-Origin': '*' }, - static: { - publicPath: '/', + output: { + path: webpackPaths.distRendererPath, + publicPath: '/', + filename: 'renderer.dev.js', + library: { + type: 'umd', + }, }, - historyApiFallback: { - verbose: true, - }, - setupMiddlewares(middlewares) { - console.log('Starting preload.js builder...'); - const preloadProcess = spawn('npm', ['run', 'start:preload'], { - shell: true, - stdio: 'inherit', - }) - .on('close', (code: number) => process.exit(code!)) - .on('error', (spawnError) => console.error(spawnError)); - console.log('Starting remote.js builder...'); - const remoteProcess = spawn('npm', ['run', 'start:remote'], { - shell: true, - stdio: 'inherit', - }) - .on('close', (code: number) => process.exit(code!)) - .on('error', (spawnError) => console.error(spawnError)); - - console.log('Starting Main Process...'); - spawn('npm', ['run', 'start:main'], { - shell: true, - stdio: 'inherit', - }) - .on('close', (code: number) => { - preloadProcess.kill(); - remoteProcess.kill(); - process.exit(code!); - }) - .on('error', (spawnError) => console.error(spawnError)); - return middlewares; + module: { + rules: [ + { + test: /\.s?css$/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + modules: { + localIdentName: '[name]__[local]--[hash:base64:5]', + exportLocalsConvention: 'camelCaseOnly', + }, + sourceMap: true, + importLoaders: 1, + }, + }, + 'sass-loader', + ], + include: /\.module\.s?(c|a)ss$/, + }, + { + test: /\.s?css$/, + use: ['style-loader', 'css-loader', 'sass-loader'], + exclude: /\.module\.s?(c|a)ss$/, + }, + // Fonts + { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: 'asset/resource', + }, + // Images + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', + }, + ], + }, + plugins: [ + ...(requiredByDLLConfig + ? [] + : [ + new webpack.DllReferencePlugin({ + context: webpackPaths.dllPath, + manifest: require(manifest), + sourceType: 'var', + }), + ]), + + new webpack.NoEmitOnErrorsPlugin(), + + /** + * Create global constants which can be configured at compile time. + * + * Useful for allowing different behaviour between development builds and + * release builds + * + * NODE_ENV should be production so that modules do not perform certain + * development checks + * + * By default, use 'development' as NODE_ENV. This can be overriden with + * 'staging', for example, by changing the ENV variables in the npm scripts + */ + new webpack.EnvironmentPlugin({ + NODE_ENV: 'development', + }), + + new webpack.LoaderOptionsPlugin({ + debug: true, + }), + + new ReactRefreshWebpackPlugin(), + + new HtmlWebpackPlugin({ + filename: path.join('index.html'), + template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: false, + env: process.env.NODE_ENV, + isDevelopment: process.env.NODE_ENV !== 'production', + nodeModules: webpackPaths.appNodeModulesPath, + templateParameters: { + web: false, + }, + }), + ], + + node: { + __dirname: false, + __filename: false, + }, + + devServer: { + port, + compress: true, + hot: true, + headers: { 'Access-Control-Allow-Origin': '*' }, + static: { + publicPath: '/', + }, + historyApiFallback: { + verbose: true, + }, + setupMiddlewares(middlewares) { + console.log('Starting preload.js builder...'); + const preloadProcess = spawn('npm', ['run', 'start:preload'], { + shell: true, + stdio: 'inherit', + }) + .on('close', (code: number) => process.exit(code!)) + .on('error', (spawnError) => console.error(spawnError)); + + console.log('Starting remote.js builder...'); + const remoteProcess = spawn('npm', ['run', 'start:remote'], { + shell: true, + stdio: 'inherit', + }) + .on('close', (code: number) => process.exit(code!)) + .on('error', (spawnError) => console.error(spawnError)); + + console.log('Starting Main Process...'); + spawn('npm', ['run', 'start:main'], { + shell: true, + stdio: 'inherit', + }) + .on('close', (code: number) => { + preloadProcess.kill(); + remoteProcess.kill(); + process.exit(code!); + }) + .on('error', (spawnError) => console.error(spawnError)); + return middlewares; + }, }, - }, }; export default merge(baseConfig, configuration); diff --git a/.erb/configs/webpack.config.renderer.prod.ts b/.erb/configs/webpack.config.renderer.prod.ts index 324630a7..379bb2d0 100644 --- a/.erb/configs/webpack.config.renderer.prod.ts +++ b/.erb/configs/webpack.config.renderer.prod.ts @@ -21,114 +21,117 @@ checkNodeEnv('production'); deleteSourceMaps(); const devtoolsConfig = - process.env.DEBUG_PROD === 'true' - ? { - devtool: 'source-map', - } - : {}; + process.env.DEBUG_PROD === 'true' + ? { + devtool: 'source-map', + } + : {}; const configuration: webpack.Configuration = { - ...devtoolsConfig, + ...devtoolsConfig, - mode: 'production', + mode: 'production', - target: ['web', 'electron-renderer'], + target: ['web', 'electron-renderer'], - entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], + entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], - output: { - path: webpackPaths.distRendererPath, - publicPath: './', - filename: 'renderer.js', - library: { - type: 'umd', + output: { + path: webpackPaths.distRendererPath, + publicPath: './', + filename: 'renderer.js', + library: { + type: 'umd', + }, }, - }, - module: { - rules: [ - { - test: /\.s?(a|c)ss$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - modules: { - localIdentName: '[name]__[local]--[hash:base64:5]', - exportLocalsConvention: 'camelCaseOnly', - }, - sourceMap: true, - importLoaders: 1, + module: { + rules: [ + { + test: /\.s?(a|c)ss$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + modules: { + localIdentName: '[name]__[local]--[hash:base64:5]', + exportLocalsConvention: 'camelCaseOnly', + }, + sourceMap: true, + importLoaders: 1, + }, + }, + 'sass-loader', + ], + include: /\.module\.s?(c|a)ss$/, + }, + { + test: /\.s?(a|c)ss$/, + use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], + exclude: /\.module\.s?(c|a)ss$/, + }, + // Fonts + { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: 'asset/resource', + }, + // Images + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', }, - }, - 'sass-loader', ], - include: /\.module\.s?(c|a)ss$/, - }, - { - test: /\.s?(a|c)ss$/, - use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], - exclude: /\.module\.s?(c|a)ss$/, - }, - // Fonts - { - test: /\.(woff|woff2|eot|ttf|otf)$/i, - type: 'asset/resource', - }, - // Images - { - test: /\.(png|svg|jpg|jpeg|gif)$/i, - type: 'asset/resource', - }, + }, + + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + }), + new CssMinimizerPlugin(), + ], + }, + + plugins: [ + /** + * Create global constants which can be configured at compile time. + * + * Useful for allowing different behaviour between development builds and + * release builds + * + * NODE_ENV should be production so that modules do not perform certain + * development checks + */ + new webpack.EnvironmentPlugin({ + NODE_ENV: 'production', + DEBUG_PROD: false, + }), + + new MiniCssExtractPlugin({ + filename: 'style.css', + }), + + new BundleAnalyzerPlugin({ + analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', + }), + + new HtmlWebpackPlugin({ + filename: 'index.html', + template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: false, + isDevelopment: process.env.NODE_ENV !== 'production', + templateParameters: { + web: false, + }, + }), ], - }, - - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin({ - parallel: true, - }), - new CssMinimizerPlugin(), - ], - }, - - plugins: [ - /** - * Create global constants which can be configured at compile time. - * - * Useful for allowing different behaviour between development builds and - * release builds - * - * NODE_ENV should be production so that modules do not perform certain - * development checks - */ - new webpack.EnvironmentPlugin({ - NODE_ENV: 'production', - DEBUG_PROD: false, - }), - - new MiniCssExtractPlugin({ - filename: 'style.css', - }), - - new BundleAnalyzerPlugin({ - analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', - }), - - new HtmlWebpackPlugin({ - filename: 'index.html', - template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: false, - isDevelopment: process.env.NODE_ENV !== 'production', - }), - ], }; export default merge(baseConfig, configuration); diff --git a/.erb/configs/webpack.config.renderer.web.ts b/.erb/configs/webpack.config.renderer.web.ts index 39fca511..82b5f79a 100644 --- a/.erb/configs/webpack.config.renderer.web.ts +++ b/.erb/configs/webpack.config.renderer.web.ts @@ -116,6 +116,9 @@ const configuration: webpack.Configuration = { env: process.env.NODE_ENV, isDevelopment: process.env.NODE_ENV !== 'production', nodeModules: webpackPaths.appNodeModulesPath, + templateParameters: { + web: false, // with hot reload, we don't have NGINX injecting variables + }, }), ], diff --git a/.erb/configs/webpack.config.web.prod.ts b/.erb/configs/webpack.config.web.prod.ts index 73cc32fc..ed935fd6 100644 --- a/.erb/configs/webpack.config.web.prod.ts +++ b/.erb/configs/webpack.config.web.prod.ts @@ -128,6 +128,9 @@ const configuration: webpack.Configuration = { }, isBrowser: false, isDevelopment: process.env.NODE_ENV !== 'production', + templateParameters: { + web: true, + }, }), ], }; diff --git a/Dockerfile b/Dockerfile index 856af4d2..fdf2c6ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,20 @@ # --- Builder stage FROM node:18-alpine as builder WORKDIR /app -COPY . /app +#Copy package.json first to cache node_modules +COPY package.json package-lock.json . # Scripts include electron-specific dependencies, which we don't need RUN npm install --legacy-peer-deps --ignore-scripts +#Copy code and build with cached modules +COPY . . RUN npm run build:web # --- Production stage FROM nginx:alpine-slim COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html +COPY ./settings.js.template /etc/nginx/templates/settings.js.template COPY ng.conf.template /etc/nginx/templates/default.conf.template ENV PUBLIC_PATH="/" diff --git a/README.md b/README.md index faa5513b..a4a28652 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ docker run --name feishin -p 9180:9180 feishin 3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`. +4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set. + ## FAQ ### MPV is either not working or is rapidly switching between pause/play states diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..44308381 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3.5' +services: + feishin: + container_name: feishin + image: jeffvli/feishin + restart: unless-stopped + ports: + - 9180:9180 + environment: + - SERVER_NAME=jellyfin # pre defined server name + - SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled + - SERVER_TYPE=jellyfin # navidrome also works + - SERVER_URL= # http://address:port diff --git a/ng.conf.template b/ng.conf.template index 3fed35b8..eb46ff72 100644 --- a/ng.conf.template +++ b/ng.conf.template @@ -16,4 +16,12 @@ server { alias /usr/share/nginx/html/; try_files $uri $uri/ /index.html =404; } + + location ${PUBLIC_PATH}settings.js { + alias /etc/nginx/conf.d/settings.js; + } + + location ${PUBLIC_PATH}/settings.js { + alias /etc/nginx/conf.d/settings.js; + } } \ No newline at end of file diff --git a/package.json b/package.json index 7be6527b..93e71ab1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build:remote": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.prod.ts", "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", "build:web": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.web.prod.ts", - "build:docker": "npm run build:web && docker build -t jeffvli/feishin .", + "build:docker": "docker build -t jeffvli/feishin .", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", "lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"", "lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix", diff --git a/settings.js.template b/settings.js.template new file mode 100644 index 00000000..782f0d7e --- /dev/null +++ b/settings.js.template @@ -0,0 +1 @@ +"use strict";window.SERVER_URL="${SERVER_URL}";window.SERVER_NAME="${SERVER_NAME}";window.SERVER_TYPE="${SERVER_TYPE}";window.SERVER_LOCK=${SERVER_LOCK}; diff --git a/src/main/preload/local-settings.ts b/src/main/preload/local-settings.ts index b8aafba0..c6269411 100644 --- a/src/main/preload/local-settings.ts +++ b/src/main/preload/local-settings.ts @@ -1,6 +1,6 @@ import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron'; import Store from 'electron-store'; -import type { TitleTheme } from '/@/renderer/types'; +import { toServerType, type TitleTheme } from '/@/renderer/types'; const store = new Store(); @@ -56,9 +56,20 @@ const themeSet = (theme: TitleTheme): void => { ipcRenderer.send('theme-set', theme); }; +const SERVER_TYPE = toServerType(process.env.SERVER_TYPE); + +const env = { + SERVER_LOCK: + SERVER_TYPE !== null ? process.env.SERVER_LOCK?.toLocaleLowerCase() === 'true' : false, + SERVER_NAME: process.env.SERVER_NAME ?? '', + SERVER_TYPE, + SERVER_URL: process.env.SERVER_URL ?? 'http://', +}; + export const localSettings = { disableMediaKeys, enableMediaKeys, + env, fontError, get, passwordGet, diff --git a/src/renderer/features/servers/components/add-server-form.tsx b/src/renderer/features/servers/components/add-server-form.tsx index 16fd21b8..d55a34bc 100644 --- a/src/renderer/features/servers/components/add-server-form.tsx +++ b/src/renderer/features/servers/components/add-server-form.tsx @@ -8,7 +8,7 @@ import isElectron from 'is-electron'; import { nanoid } from 'nanoid/non-secure'; import { AuthenticationResponse } from '/@/renderer/api/types'; import { useAuthStoreActions } from '/@/renderer/store'; -import { ServerType } from '/@/renderer/types'; +import { ServerType, toServerType } from '/@/renderer/types'; import { api } from '/@/renderer/api'; import { useTranslation } from 'react-i18next'; @@ -33,15 +33,27 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { const form = useForm({ initialValues: { legacyAuth: false, - name: '', + name: (localSettings ? localSettings.env.SERVER_NAME : window.SERVER_NAME) ?? '', password: '', savePassword: false, - type: ServerType.JELLYFIN, - url: 'http://', + type: + (localSettings + ? localSettings.env.SERVER_TYPE + : toServerType(window.SERVER_TYPE)) ?? ServerType.JELLYFIN, + url: (localSettings ? localSettings.env.SERVER_URL : window.SERVER_URL) ?? 'https://', username: '', }, }); + // server lock for web is only true if lock is true *and* all other properties are set + const serverLock = + (localSettings + ? !!localSettings.env.SERVER_LOCK + : !!window.SERVER_LOCK && + window.SERVER_TYPE && + window.SERVER_NAME && + window.SERVER_URL) || false; + const isSubmitDisabled = !form.values.name || !form.values.url || !form.values.username; const handleSubmit = form.onSubmit(async (values) => { @@ -62,7 +74,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { password: values.password, username: values.username, }, - values.type, + values.type as ServerType, ); if (!data) { @@ -76,7 +88,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { id: nanoid(), name: values.name, ndCredential: data.ndCredential, - type: values.type, + type: values.type as ServerType, url: values.url.replace(/\/$/, ''), userId: data.userId, username: data.username, @@ -117,11 +129,13 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { > <SegmentedControl data={SERVER_TYPES} + disabled={serverLock} {...form.getInputProps('type')} /> <Group grow> <TextInput data-autofocus + disabled={serverLock} label={t('form.addServer.input', { context: 'name', postProcess: 'titleCase', @@ -129,6 +143,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { {...form.getInputProps('name')} /> <TextInput + disabled={serverLock} label={t('form.addServer.input', { context: 'url', postProcess: 'titleCase', diff --git a/src/renderer/index.ejs b/src/renderer/index.ejs index ca220526..4d5ecd15 100644 --- a/src/renderer/index.ejs +++ b/src/renderer/index.ejs @@ -6,6 +6,9 @@ <meta http-equiv="Content-Security-Policy" /> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Feishin + <% if (web) { %> + + <% } %> diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index db69c9ba..7d8c9258 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -13,6 +13,10 @@ import { Browser } from '/@/main/preload/browser'; declare global { interface Window { + SERVER_LOCK?: boolean; + SERVER_NAME?: string; + SERVER_TYPE?: string; + SERVER_URL?: string; electron: { browser: Browser; discordRpc: DiscordRpc; diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 4845d754..fe683f30 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -60,6 +60,17 @@ export enum ServerType { SUBSONIC = 'subsonic', } +export const toServerType = (value?: string): ServerType | null => { + switch (value?.toLowerCase()) { + case ServerType.JELLYFIN: + return ServerType.JELLYFIN; + case ServerType.NAVIDROME: + return ServerType.NAVIDROME; + default: + return null; + } +}; + export type ServerListItem = { credential: string; features?: Record; From 94b649fefef2e067581697bd372fd94e96d85090 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:47:17 -0800 Subject: [PATCH 09/17] don't transform artist/album for fullscreen player --- .../features/player/components/full-screen-player-image.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/renderer/features/player/components/full-screen-player-image.tsx b/src/renderer/features/player/components/full-screen-player-image.tsx index d3053a90..c776aa0b 100644 --- a/src/renderer/features/player/components/full-screen-player-image.tsx +++ b/src/renderer/features/player/components/full-screen-player-image.tsx @@ -278,7 +278,6 @@ export const FullScreenPlayerImage = () => { to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentSong?.albumId || '', })} - transform="uppercase" w="100%" weight={600} > @@ -292,7 +291,6 @@ export const FullScreenPlayerImage = () => { style={{ textShadow: 'var(--fullscreen-player-text-shadow)', }} - transform="uppercase" > {index > 0 && ( { to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId: artist.id, })} - transform="uppercase" weight={600} > {artist.name} From 960c126283145ef7af90ed719534a19319b27f0e Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 28 Feb 2024 21:07:50 -0800 Subject: [PATCH 10/17] Fix incorrect album artist sidebar icon --- src/renderer/features/sidebar/components/sidebar-icon.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/features/sidebar/components/sidebar-icon.tsx b/src/renderer/features/sidebar/components/sidebar-icon.tsx index 7d48e723..ef87e702 100644 --- a/src/renderer/features/sidebar/components/sidebar-icon.tsx +++ b/src/renderer/features/sidebar/components/sidebar-icon.tsx @@ -38,6 +38,9 @@ export const SidebarIcon = ({ active, route, size }: SidebarIconProps) => { case AppRoute.LIBRARY_ALBUMS: if (active) return ; return ; + case AppRoute.LIBRARY_ALBUM_ARTISTS: + if (active) return ; + return ; case AppRoute.LIBRARY_ARTISTS: if (active) return ; return ; From 753ca01d41f0720947104afbe4f6b48c8092c4b4 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:28:46 -0800 Subject: [PATCH 11/17] [bugfix]: fix update rating for items with no data node --- src/renderer/features/context-menu/context-menu-provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 83cce029..34ebc325 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -522,7 +522,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { const handleUpdateRating = useCallback( (rating: number) => { - if (!ctx.dataNodes || !ctx.data) return; + if (!ctx.dataNodes && !ctx.data) return; let uniqueServerIds: string[] = []; let items: AnyLibraryItems = []; From f50d1e0a8cfc22ac51e50d3c93511e66914f1208 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Fri, 1 Mar 2024 19:54:30 -0800 Subject: [PATCH 12/17] support opacity 0, spellcheck --- .../features/player/components/full-screen-player-image.tsx | 3 +-- .../features/player/components/full-screen-player-queue.tsx | 6 ++---- .../features/player/components/full-screen-player.tsx | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/renderer/features/player/components/full-screen-player-image.tsx b/src/renderer/features/player/components/full-screen-player-image.tsx index c776aa0b..d3c51564 100644 --- a/src/renderer/features/player/components/full-screen-player-image.tsx +++ b/src/renderer/features/player/components/full-screen-player-image.tsx @@ -134,7 +134,7 @@ export const FullScreenPlayerImage = () => { const albumArtRes = useSettingsStore((store) => store.general.albumArtRes); const { queue } = usePlayerData(); - const { opacity, useImageAspectRatio } = useFullScreenPlayerStore(); + const { useImageAspectRatio } = useFullScreenPlayerStore(); const currentSong = queue.current; const { color: background } = useFastAverageColor({ algorithm: 'dominant', @@ -250,7 +250,6 @@ export const FullScreenPlayerImage = () => { ` +const GridContainer = styled.div` display: grid; grid-template-rows: auto minmax(0, 1fr); grid-template-columns: 1fr; @@ -82,8 +82,6 @@ export const FullScreenPlayerQueue = () => { }, ]; - console.log('opacity', opacity); - return ( { defaultValue={opacity} label={(e) => `${e} %`} max={100} - min={1} + min={0} w="100%" onChangeEnd={(e) => setStore({ opacity: Number(e) })} /> From f1f6ccfd02a973bef95540841027e381dd602afe Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 3 Mar 2024 22:15:49 -0800 Subject: [PATCH 13/17] Normalize server feature set --- src/renderer/api/features.types.ts | 6 ++++++ .../api/jellyfin/jellyfin-controller.ts | 12 +++++++++++- src/renderer/api/jellyfin/jellyfin-types.ts | 4 ++++ .../api/navidrome/navidrome-controller.ts | 18 ++++++++++++++---- .../api/subsonic/subsonic-controller.ts | 18 ++++++++++++++---- src/renderer/api/subsonic/subsonic-types.ts | 6 ++++++ src/renderer/api/types.ts | 11 +++-------- src/renderer/api/utils.ts | 14 +++----------- .../features/lyrics/queries/lyric-query.ts | 4 ++-- 9 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 src/renderer/api/features.types.ts diff --git a/src/renderer/api/features.types.ts b/src/renderer/api/features.types.ts new file mode 100644 index 00000000..be5606a5 --- /dev/null +++ b/src/renderer/api/features.types.ts @@ -0,0 +1,6 @@ +export enum ServerFeature { + SMART_PLAYLISTS = 'smartPlaylists', + SONG_LYRICS = 'songLyrics', +} + +export type ServerFeatures = Record, boolean>; diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 1d1339ce..385c6ef3 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -59,6 +59,7 @@ import packageJson from '../../../../package.json'; import { z } from 'zod'; import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; import isElectron from 'is-electron'; +import { ServerFeatures } from '/@/renderer/api/features.types'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); @@ -957,7 +958,16 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to get server info'); } - return { id: apiClientProps.server?.id, version: res.body.Version }; + const features: ServerFeatures = { + smartPlaylists: false, + songLyrics: true, + }; + + return { + features, + id: apiClientProps.server?.id, + version: res.body.Version, + }; }; export const jfController = { diff --git a/src/renderer/api/jellyfin/jellyfin-types.ts b/src/renderer/api/jellyfin/jellyfin-types.ts index 3ce22e37..8ed18b8c 100644 --- a/src/renderer/api/jellyfin/jellyfin-types.ts +++ b/src/renderer/api/jellyfin/jellyfin-types.ts @@ -665,6 +665,10 @@ const serverInfo = z.object({ Version: z.string(), }); +export enum JellyfinExtensions { + SONG_LYRICS = 'songLyrics', +} + export const jfType = { _enum: { albumArtistList: albumArtistListSort, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 162be26a..a3fd3a8d 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -49,6 +49,7 @@ import { ServerInfoArgs, } from '../types'; import { hasFeature } from '/@/renderer/api/utils'; +import { ServerFeature, ServerFeatures } from '/@/renderer/api/features.types'; const authenticate = async ( url: string, @@ -366,7 +367,7 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise]> = [ - ['0.48.0', { [NavidromeExtensions.SMART_PLAYLISTS]: [1] }], + ['0.48.0', { [ServerFeature.SMART_PLAYLISTS]: [1] }], ]; const getFeatures = (version: string): Record => { @@ -518,7 +519,7 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to ping server'); } - const features: Record = getFeatures(ping.body.serverVersion!); + const navidromeFeatures: Record = getFeatures(ping.body.serverVersion!); if (ping.body.openSubsonic) { const res = await ssApiClient(apiClientProps).getServerInfo(); @@ -528,10 +529,19 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { } for (const extension of res.body.openSubsonicExtensions) { - features[extension.name] = extension.versions; + navidromeFeatures[extension.name] = extension.versions; } } + const features: ServerFeatures = { + smartPlaylists: false, + songLyrics: true, + }; + + if (navidromeFeatures[NavidromeExtensions.SMART_PLAYLISTS]) { + features[ServerFeature.SMART_PLAYLISTS] = true; + } + return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! }; }; diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index b67200e8..457672de 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -2,7 +2,7 @@ import md5 from 'md5'; import { z } from 'zod'; import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize'; -import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; +import { SubsonicExtensions, ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { ArtistInfoArgs, AuthenticationResponse, @@ -27,6 +27,7 @@ import { StructuredLyric, } from '/@/renderer/api/types'; import { randomString } from '/@/renderer/utils'; +import { ServerFeatures } from '/@/renderer/api/features.types'; const authenticate = async ( url: string, @@ -381,8 +382,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to ping server'); } + const features: ServerFeatures = { + smartPlaylists: false, + songLyrics: false, + }; + if (!ping.body.openSubsonic || !ping.body.serverVersion) { - return { version: ping.body.version }; + return { features, version: ping.body.version }; } const res = await ssApiClient(apiClientProps).getServerInfo(); @@ -391,9 +397,13 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to get server extensions'); } - const features: Record = {}; + const subsonicFeatures: Record = {}; for (const extension of res.body.openSubsonicExtensions) { - features[extension.name] = extension.versions; + subsonicFeatures[extension.name] = extension.versions; + } + + if (subsonicFeatures[SubsonicExtensions.SONG_LYRICS]) { + features.songLyrics = true; } return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion }; diff --git a/src/renderer/api/subsonic/subsonic-types.ts b/src/renderer/api/subsonic/subsonic-types.ts index 9005fe8c..b113446f 100644 --- a/src/renderer/api/subsonic/subsonic-types.ts +++ b/src/renderer/api/subsonic/subsonic-types.ts @@ -247,6 +247,12 @@ const structuredLyrics = z.object({ .optional(), }); +export enum SubsonicExtensions { + FORM_POST = 'formPost', + SONG_LYRICS = 'songLyrics', + TRANSCODE_OFFSET = 'transcodeOffset', +} + export const ssType = { _parameters: { albumList: albumListParameters, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index c8abd9e1..7001d179 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -20,6 +20,7 @@ import { NDUserListSort, NDGenreListSort, } from './navidrome.types'; +import { ServerFeatures } from '/@/renderer/api/features.types'; export enum LibraryItem { ALBUM = 'album', @@ -57,7 +58,7 @@ export type User = { export type ServerListItem = { credential: string; - features?: Record; + features?: ServerFeatures; id: string; name: string; ndCredential?: string; @@ -1144,14 +1145,8 @@ export type FontData = { export type ServerInfoArgs = BaseEndpointArgs; -export enum SubsonicExtensions { - FORM_POST = 'formPost', - SONG_LYRICS = 'songLyrics', - TRANSCODE_OFFSET = 'transcodeOffset', -} - export type ServerInfo = { - features?: Record; + features: ServerFeatures; id?: string; version: string; }; diff --git a/src/renderer/api/utils.ts b/src/renderer/api/utils.ts index a24748c6..9e209591 100644 --- a/src/renderer/api/utils.ts +++ b/src/renderer/api/utils.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { toast } from '/@/renderer/components'; import { useAuthStore } from '/@/renderer/store'; import { ServerListItem } from '/@/renderer/api/types'; +import { ServerFeature } from '/@/renderer/api/features.types'; // Since ts-rest client returns a strict response type, we need to add the headers to the body object export const resultWithHeaders = (itemSchema: ItemType) => { @@ -39,19 +40,10 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => { } }; -export const hasFeature = ( - server: ServerListItem | null, - feature: string, - version = 1, -): boolean => { +export const hasFeature = (server: ServerListItem | null, feature: ServerFeature): boolean => { if (!server || !server.features) { return false; } - const versions = server.features[feature]; - if (!versions || versions.length === 0) { - return false; - } - - return versions.includes(version); + return server.features[feature]; }; diff --git a/src/renderer/features/lyrics/queries/lyric-query.ts b/src/renderer/features/lyrics/queries/lyric-query.ts index fd6aa5e5..6a862e07 100644 --- a/src/renderer/features/lyrics/queries/lyric-query.ts +++ b/src/renderer/features/lyrics/queries/lyric-query.ts @@ -6,7 +6,6 @@ import { InternetProviderLyricResponse, FullLyricsMetadata, LyricGetQuery, - SubsonicExtensions, StructuredLyric, ServerType, } from '/@/renderer/api/types'; @@ -16,6 +15,7 @@ import { queryKeys } from '/@/renderer/api/query-keys'; import { api } from '/@/renderer/api'; import isElectron from 'is-electron'; import { hasFeature } from '/@/renderer/api/utils'; +import { ServerFeature } from '/@/renderer/api/features.types'; const lyricsIpc = isElectron() ? window.electron.lyrics : null; @@ -113,7 +113,7 @@ export const useSongLyricsBySong = ( source: server?.name ?? 'music server', }; } - } else if (hasFeature(server, SubsonicExtensions.SONG_LYRICS)) { + } else if (hasFeature(server, ServerFeature.SONG_LYRICS)) { const subsonicLyrics = await api.controller .getStructuredLyrics({ apiClientProps: { server, signal }, From 84837a6887526385b703be2acc8b30022dbbe3d4 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 3 Mar 2024 22:16:25 -0800 Subject: [PATCH 14/17] Prevent version check from running on every query in Navidrome --- src/renderer/hooks/use-server-version.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/renderer/hooks/use-server-version.ts b/src/renderer/hooks/use-server-version.ts index 5565457e..96245e76 100644 --- a/src/renderer/hooks/use-server-version.ts +++ b/src/renderer/hooks/use-server-version.ts @@ -23,14 +23,18 @@ export const useServerVersion = () => { }); useEffect(() => { - if (server && server.id === serverInfo.data?.id) { - const { version, features } = serverInfo.data; - if (version !== server.version || !isEqual(features, server.features)) { + if (!server?.id) { + return; + } + + if (server?.id === serverInfo.data?.id) { + const { version, features } = serverInfo.data || {}; + if (version !== server?.version || !isEqual(features, server?.features)) { updateServer(server.id, { features, version, }); } } - }, [server, serverInfo.data, updateServer]); + }, [serverInfo?.data, server?.features, server?.id, server?.version, updateServer]); }; From c8b1e2312a07b1c220f71e388ebe31cd0d62826d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 4 Mar 2024 01:44:11 -0800 Subject: [PATCH 15/17] Change smart playlist feature check --- .../features/playlists/components/create-playlist-form.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/features/playlists/components/create-playlist-form.tsx b/src/renderer/features/playlists/components/create-playlist-form.tsx index e597292b..6e720a6b 100644 --- a/src/renderer/features/playlists/components/create-playlist-form.tsx +++ b/src/renderer/features/playlists/components/create-playlist-form.tsx @@ -12,7 +12,7 @@ import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils import { useCurrentServer } from '/@/renderer/store'; import { useTranslation } from 'react-i18next'; import { hasFeature } from '/@/renderer/api/utils'; -import { NavidromeExtensions } from '/@/renderer/api/navidrome/navidrome-types'; +import { ServerFeature } from '/@/renderer/api/features.types'; interface CreatePlaylistFormProps { onCancel: () => void; @@ -123,7 +123,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => { /> )} {server?.type === ServerType.NAVIDROME && - hasFeature(server, NavidromeExtensions.SMART_PLAYLISTS) && ( + hasFeature(server, ServerFeature.SMART_PLAYLISTS) && ( setIsSmartPlaylist(e.currentTarget.checked)} From 237fb91a60caa7d70424f9a2ad91d77c1ccc91de Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:22:38 -0800 Subject: [PATCH 16/17] [enhancement]: Differentiate shared and owner playlists for Navidrome Resolves #368. If Navidrome, post-process the playlist list and separate into owned and shared (in that order). --- src/i18n/locales/en.json | 1 + .../components/sidebar-playlist-list.tsx | 55 +++++++++++++++---- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 438e44a2..fbede376 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -364,6 +364,7 @@ "playlists": "$t(entity.playlist_other)", "search": "$t(common.search)", "settings": "$t(common.setting_other)", + "shared": "shared $t(entity.playlist_other)", "tracks": "$t(entity.track_other)" }, "trackList": { diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx index 8078043c..3a1cf83c 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx @@ -1,20 +1,20 @@ import { useCallback, useMemo, useState } from 'react'; -import { Flex, Group } from '@mantine/core'; +import { Box, Flex, Group } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; import { useTranslation } from 'react-i18next'; import { RiAddBoxFill, RiAddCircleFill, RiPlayFill } from 'react-icons/ri'; import { generatePath } from 'react-router'; import { Link } from 'react-router-dom'; -import { LibraryItem } from '/@/renderer/api/types'; +import { LibraryItem, Playlist } from '/@/renderer/api/types'; import { Button, Text } from '/@/renderer/components'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlaylistList } from '/@/renderer/features/playlists'; import { AppRoute } from '/@/renderer/router/routes'; -import { Play } from '/@/renderer/types'; +import { Play, ServerType } from '/@/renderer/types'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { useHideScrollbar } from '/@/renderer/hooks'; -import { useGeneralSettings } from '/@/renderer/store'; +import { useCurrentServer, useGeneralSettings } from '/@/renderer/store'; interface SidebarPlaylistListProps { data: ReturnType['data']; @@ -22,6 +22,20 @@ interface SidebarPlaylistListProps { const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => { const { t } = useTranslation(); + + if (data?.items[index] === null) { + return ( +
+ + {t('page.sidebar.shared', { postProcess: 'titleCase' })} + +
+ ); + } + const path = data?.items[index].id ? data.defaultFullPlaylist ? generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data.items[index].id }) @@ -125,6 +139,7 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => { const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0); const handlePlayQueueAdd = usePlayQueueAdd(); const { defaultFullPlaylist } = useGeneralSettings(); + const { type, username } = useCurrentServer() || {}; const [rect, setRect] = useState({ height: 0, @@ -147,12 +162,30 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => { ); const memoizedItemData = useMemo(() => { - return { - defaultFullPlaylist, - handlePlay: handlePlayPlaylist, - items: data?.items, - }; - }, [data?.items, defaultFullPlaylist, handlePlayPlaylist]); + const base = { defaultFullPlaylist, handlePlay: handlePlayPlaylist }; + + if (!type || !username || type === ServerType.JELLYFIN || !data?.items) { + return { ...base, items: data?.items }; + } + + const owned: Array = []; + const shared: Playlist[] = []; + + for (const playlist of data.items) { + if (playlist.owner !== username) { + shared.push(playlist); + } else { + owned.push(playlist); + } + } + + if (shared.length > 0) { + // Use `null` as a separator between owned and shared playlists + owned.push(null); + } + + return { ...base, items: owned.concat(shared) }; + }, [data?.items, defaultFullPlaylist, handlePlayPlaylist, type, username]); return ( { : 'overlay-scrollbar' } height={debounced.height} - itemCount={data?.items?.length || 0} + itemCount={memoizedItemData?.items?.length || 0} itemData={memoizedItemData} itemSize={25} overscanCount={20} From 83b5afb1879c0ce33dec9fa8bc02a8e00764aad1 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 4 Mar 2024 02:37:37 -0800 Subject: [PATCH 17/17] Remove ServerType check on shared playlist display --- .../features/sidebar/components/sidebar-playlist-list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx index 3a1cf83c..2592e191 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx @@ -10,7 +10,7 @@ import { Button, Text } from '/@/renderer/components'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlaylistList } from '/@/renderer/features/playlists'; import { AppRoute } from '/@/renderer/router/routes'; -import { Play, ServerType } from '/@/renderer/types'; +import { Play } from '/@/renderer/types'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { useHideScrollbar } from '/@/renderer/hooks'; @@ -164,7 +164,7 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => { const memoizedItemData = useMemo(() => { const base = { defaultFullPlaylist, handlePlay: handlePlayPlaylist }; - if (!type || !username || type === ServerType.JELLYFIN || !data?.items) { + if (!type || !username || !data?.items) { return { ...base, items: data?.items }; }