mostly complete base spec player

This commit is contained in:
2024-06-26 17:44:07 +02:00
parent 4ecf93d31b
commit 76f543eb2f
20 changed files with 2226 additions and 154 deletions

View File

@@ -7,6 +7,7 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
<!-- TODO: Update URL -->
<script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js"></script>
<script src="https://static.janishutz.com/libs/jquery/jquery.min.js"></script>
<title>MusicPlayer</title>
</head>
<body>

View File

@@ -8,8 +8,15 @@
"name": "musicplayerv2-gui",
"version": "0.0.0",
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@rollup/plugin-inject": "^5.0.5",
"buffer": "^6.0.3",
"music-metadata-browser": "^2.5.10",
"musickit-typescript": "^1.2.4",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.5",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
@@ -40,6 +47,28 @@
"node": ">=6.0.0"
}
},
"node_modules/@esbuild-plugins/node-globals-polyfill": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz",
"integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==",
"license": "ISC",
"peerDependencies": {
"esbuild": "*"
}
},
"node_modules/@esbuild-plugins/node-modules-polyfill": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz",
"integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==",
"license": "ISC",
"dependencies": {
"escape-string-regexp": "^4.0.0",
"rollup-plugin-node-polyfills": "^0.2.1"
},
"peerDependencies": {
"esbuild": "*"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -47,7 +76,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -64,7 +92,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -81,7 +108,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -98,7 +124,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -115,7 +140,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -132,7 +156,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -149,7 +172,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -166,7 +188,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -183,7 +204,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -200,7 +220,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -217,7 +236,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -234,7 +252,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -251,7 +268,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -268,7 +284,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -285,7 +300,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -302,7 +316,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -319,7 +332,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -336,7 +348,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -353,7 +364,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -370,7 +380,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -387,7 +396,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -404,7 +412,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -421,7 +428,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -619,6 +625,50 @@
"node": ">= 8"
}
},
"node_modules/@rollup/plugin-inject": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz",
"integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.3"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
@@ -850,6 +900,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT"
},
"node_modules/@tsconfig/node20": {
"version": "20.1.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz",
@@ -861,7 +923,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -1301,6 +1362,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -1391,6 +1464,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -1421,6 +1514,30 @@
"node": ">=8"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1482,6 +1599,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1527,7 +1653,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
@@ -1574,6 +1699,28 @@
"node": ">=6.0.0"
}
},
"node_modules/engine.io-client": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -1590,7 +1737,6 @@
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -1629,7 +1775,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -1841,6 +1986,24 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -1915,6 +2078,23 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-type": {
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
"license": "MIT",
"dependencies": {
"readable-web-to-node-stream": "^3.0.0",
"strtok3": "^6.2.4",
"token-types": "^4.1.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -2112,6 +2292,26 @@
"he": "bin/he"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -2165,7 +2365,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/is-extglob": {
@@ -2325,6 +2524,15 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@@ -2378,7 +2586,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true,
"license": "MIT"
},
"node_modules/muggle-string": {
@@ -2388,6 +2595,45 @@
"dev": true,
"license": "MIT"
},
"node_modules/music-metadata": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.14.0.tgz",
"integrity": "sha512-xrm3w7SV0Wk+OythZcSbaI8mcr/KHd0knJieu8bVpaPfMv/Agz5EooCAPz3OR5hbYMiUG6dgAPKZKnMzV+3amA==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"content-type": "^1.0.5",
"debug": "^4.3.4",
"file-type": "^16.5.4",
"media-typer": "^1.1.0",
"strtok3": "^6.3.0",
"token-types": "^4.2.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/music-metadata-browser": {
"version": "2.5.10",
"resolved": "https://registry.npmjs.org/music-metadata-browser/-/music-metadata-browser-2.5.10.tgz",
"integrity": "sha512-03UnAmsSJoZZ5kK2BnEnd2zpH8LXRWQ6xlc7akKudhc2d9FT+yAiqapnmOzjW3g4cxxvIsSK5MVBO2Gi+Ymjfw==",
"license": "MIT",
"dependencies": {
"buffer": "^6.0.3",
"debug": "^4.3.4",
"music-metadata": "^7.13.3",
"readable-stream": "^4.3.0",
"readable-web-to-node-stream": "^3.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/musickit-typescript": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/musickit-typescript/-/musickit-typescript-1.2.4.tgz",
@@ -2601,6 +2847,19 @@
"node": ">=8"
}
},
"node_modules/peek-readable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -2611,7 +2870,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -2737,6 +2995,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2782,6 +3049,52 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/readable-stream": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
"license": "MIT",
"dependencies": {
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/readable-web-to-node-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2824,7 +3137,7 @@
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
@@ -2856,6 +3169,57 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-inject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
"integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==",
"deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.",
"license": "MIT",
"dependencies": {
"estree-walker": "^0.6.1",
"magic-string": "^0.25.3",
"rollup-pluginutils": "^2.8.1"
}
},
"node_modules/rollup-plugin-inject/node_modules/estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"license": "MIT"
},
"node_modules/rollup-plugin-inject/node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
"license": "MIT",
"dependencies": {
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/rollup-plugin-node-polyfills": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz",
"integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==",
"license": "MIT",
"dependencies": {
"rollup-plugin-inject": "^3.0.0"
}
},
"node_modules/rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"license": "MIT",
"dependencies": {
"estree-walker": "^0.6.1"
}
},
"node_modules/rollup-pluginutils/node_modules/estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"license": "MIT"
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -2880,6 +3244,26 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
@@ -2936,6 +3320,34 @@
"node": ">=8"
}
},
"node_modules/socket.io-client": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
@@ -2945,6 +3357,22 @@
"node": ">=0.10.0"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -2971,6 +3399,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strtok3": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^4.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3004,6 +3449,23 @@
"node": ">=8.0"
}
},
"node_modules/token-types": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -3078,7 +3540,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
@@ -3260,6 +3721,27 @@
"dev": true,
"license": "ISC"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@@ -3270,6 +3752,14 @@
"node": ">=12"
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -12,8 +12,15 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@rollup/plugin-inject": "^5.0.5",
"buffer": "^6.0.3",
"music-metadata-browser": "^2.5.10",
"musickit-typescript": "^1.2.4",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.5",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},

View File

@@ -1,24 +1,35 @@
<template>
<div>
<h1>Library</h1>
<playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )"></playlistsView>
<playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )" :is-logged-in="$props.isLoggedIn"
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"></playlistsView>
</div>
</template>
<script setup lang="ts">
import playlistsView from '@/components/playlistsView.vue';
import type { ReadFile } from '@/scripts/song';
const emits = defineEmits( [ 'selected-playlist' ] );
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] );
const selectPlaylist = ( id: string ) => {
emits( 'selected-playlist', id );
}
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
emits( 'custom-playlist', playlist );
}
defineProps( {
'playlists': {
'default': [],
'type': Array<any>,
'required': true,
}
},
'isLoggedIn': {
'default': false,
'type': Boolean,
'required': true,
}
} );
</script>

View File

@@ -0,0 +1,383 @@
<!-- eslint-disable no-undef -->
<template>
<div id="notifications">
<div class="message-box" :class="[ location, size ]">
<div class="message-container" :class="messageType">
<button @click="handleNotifications();" class="close-notification"><span class="material-symbols-outlined close-notification-icon">close</span></button>
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
<p class="message" @click="notificationAction()">{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}</p>
<div :class="'countdown countdown-' + messageType" :style="'width: ' + ( 100 - ( currentTime - notificationDisplayStartTime ) / ( notifications[ currentDID ] ? notifications[ currentDID ].showDuration : 1 ) / 10 ) + '%'"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import router from '@/router';
import { onUnmounted, ref, type Ref } from 'vue';
defineProps( {
location: {
type: String,
'default': 'topleft',
},
size: {
type: String,
'default': 'default',
}
// Size options: small, default (default option), big, bigger, huge
} );
interface Notification {
message: string;
showDuration: number;
messageType: string;
priority: string;
id: number;
redirect?: string;
}
interface NotificationList {
[ key: string ]: Notification
}
const notifications: Ref<NotificationList> = ref( {} );
const queue: Ref<number[]> = ref( [] );
const currentDID: Ref<number> = ref( 0 );
const messageType: Ref<string> = ref( 'hide' );
const currentID = ref( { 'critical': 0, 'medium': 1000, 'low': 10000 } );
const notificationDisplayStartTime: Ref<number> = ref( 0 );
const currentTime: Ref<number> = ref( 0 );
let progressBar = 0;
let notificationTimeout = 0;
const notificationAction = () => {
if ( notifications.value[ currentDID.value ] ) {
if ( notifications.value[ currentDID.value ].redirect ) {
router.push( notifications.value[ currentDID.value ].redirect ?? '' );
}
}
};
/**
* Create a notification that will be displayed using the internal notification scheduler
* @param {string} message The message to show. Can only be plain text (no HTML)
* @param {number} showDuration The duration in seconds for which to show the notification
* @param {string} messageType Type of notification to show. Will dictate how it looks: 'ok', 'error', 'info', 'warn', 'progress'
* @param {string} priority The priority of the message: 'low', 'normal', 'critical'
* @returns {number}
*/
const createNotification = ( message: string, showDuration: number, messageType: string, priority: string, redirect?: string ): number => {
/*
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
*/
let id = 0;
if ( priority === 'critical' ) {
currentID.value[ 'critical' ] += 1;
id = currentID.value[ 'critical' ];
} else if ( priority === 'normal' ) {
currentID.value[ 'medium' ] += 1;
id = currentID.value[ 'medium' ];
} else if ( priority === 'low' ) {
currentID.value[ 'low' ] += 1;
id = currentID.value[ 'low' ];
}
notifications.value[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id, redirect: redirect };
queue.value.push( id );
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) ) {
handleNotifications();
}
return id;
}
/**
* Update a notification's message after creating it
* @param {number} id The notification ID returned by createNotification
* @param {string} message The new message
* @returns {void}
*/
const updateNotification = ( id: number, message: string ): void => {
if ( notifications.value[ id ] ) {
notifications.value[ id ].message = message;
}
}
/**
* Delete a previously created notification
* @param {string} id The notification ID returned by createNotification
* @returns {undefined}
*/
const cancelNotification = ( id: number ): undefined => {
try {
delete notifications.value[ id ];
} catch ( error ) {
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
}
try {
queue.value.splice( queue.value.indexOf( id ), 1 );
} catch {
console.debug( 'queue empty' );
}
if ( currentDID.value == id ) {
try {
clearTimeout( notificationTimeout );
} catch (err) { /* empty */ }
handleNotifications();
}
}
const handleNotifications = () => {
notificationDisplayStartTime.value = new Date().getTime();
queue.value.sort();
if ( queue.value.length > 0 ) {
if ( currentDID.value !== 0 ) {
delete notifications.value[ currentDID.value ];
}
currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ];
messageType.value = notifications.value[ queue.value[ 0 ] ].messageType;
queue.value.reverse();
queue.value.pop();
progressBar = setInterval( progressBarHandler, 25 );
notificationTimeout = setTimeout( () => {
handleNotifications();
}, notifications.value[ currentDID.value ].showDuration * 1000 );
// eslint-disable-next-line no-undef
$( '.message-box' ).css( 'z-index', 1000 );
} else {
try {
clearInterval( progressBar );
} catch (err) { /* empty */ }
messageType.value = 'hide';
// eslint-disable-next-line no-undef
$( '.message-box' ).css( 'z-index', -1 );
}
}
const progressBarHandler = () => {
currentTime.value = new Date().getTime();
}
onUnmounted( () => {
try {
clearInterval( progressBar );
} catch (err) { /* empty */ }
try {
clearInterval( notificationTimeout );
} catch (err) { /* empty */ }
} );
defineExpose( {
createNotification,
cancelNotification,
updateNotification
} );
</script>
<style scoped>
.message-box {
position: fixed;
z-index: -1;
color: white;
transition: all 0.5s;
width: 95vw;
right: 2.5vw;
top: 1vh;
height: 10vh;
}
.close-notification {
position: absolute;
top: 5px;
right: 5px;
background: none;
color: white;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
}
.close-notification-icon {
font-size: 1.75rem;
}
.countdown {
position: absolute;
bottom: 0;
left: 0;
height: 5px;
}
.message-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
opacity: 1;
transition: all 0.5s;
cursor: default;
}
.types {
color: white;
border-radius: 100%;
margin-right: auto;
margin-left: 5%;
padding: 1.5%;
font-size: 200%;
}
.message {
margin-right: calc( 5% + 30px );
text-align: end;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.ok {
background-color: rgb(1, 71, 1);
}
.countdown-ok {
background-color: green;
}
.error {
background-color: rgb(114, 1, 1);
}
.countdown-error {
background-color: red;
}
.info {
background-color: rgb(44, 112, 151);
}
.countdown-info {
background-color: blue;
}
.warning {
background-color: orange;
}
.countdown-warning {
background-color: orangered;
}
.hide {
opacity: 0;
}
.progress {
z-index: 100;
background-color: rgb(0, 0, 99);
}
.countdown-ok {
background: none;
}
.progress-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}
@media only screen and (min-width: 750px) {
.default {
height: 10vh;
width: 32vw;
}
.small {
height: 7vh;
width: 27vw;
}
.big {
height: 12vh;
width: 38vw;
}
.bigger {
height: 15vh;
width: 43vw;
}
.huge {
height: 20vh;
width: 50vw;
}
.topleft {
top: 3vh;
left: 0.5vw;
}
.topright {
top: 3vh;
right: 0.5vw;
}
.bottomright {
bottom: 3vh;
right: 0.5vw;
}
.bottomleft {
bottom: 3vh;
right: 0.5vw;
}
}
@media only screen and (min-width: 1500px) {
.default {
height: 10vh;
width: 15vw;
}
.small {
height: 7vh;
width: 11vw;
}
.big {
height: 12vh;
width: 17vw;
}
.bigger {
height: 15vh;
width: 20vw;
}
.huge {
height: 20vh;
width: 25vw;
}
}
</style>

View File

@@ -1,40 +1,54 @@
<template>
<div>
<div :class="'player' + ( isShowingFullScreenPlayer ? '' : ' player-hidden' )">
<div class="main-player">
<!-- TODO: Make cover art of song or otherwise MusicPlayer Logo -->
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" @click="controlUI( 'show' )" v-if="coverArt === ''">
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" @click="controlUI( 'show' )" v-else>
<div class="song-name-wrapper">
<p class="song-name" @click="controlUI( 'show' )">{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i></p>
<div :class="'playback' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
<sliderView :position="pos" :active="true" :duration="duration" name="main" @pos="( pos ) => player.goToPos( pos )"
v-if="isShowingFullScreenPlayer"></sliderView>
<div :class="'playback-pos-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
<p class="playback-pos">{{ nicePlaybackPos }}</p>
<p v-if="!isShowingFullScreenPlayer"> / </p>
<p class="playback-duration">{{ niceDuration }}</p>
<div class="player">
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
<div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )">
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" v-if="coverArt === ''">
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" v-else>
<div class="name-time">
<p class="song-name">{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i></p>
<div class="playback" v-if="!isShowingFullScreenPlayer">
<div class="playback-pos-wrapper">
<p class="playback-pos">{{ nicePlaybackPos }}</p>
<p> / </p>
<p class="playback-duration">{{ niceDuration }}</p>
</div>
</div>
</div>
</div>
<div class="controls-wrapper">
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous">skip_previous</span>
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'">replay_10</span>
<span class="material-symbols-outlined controls" v-if="isPlaying" @click="playPause()" id="play-pause">pause</span>
<span class="material-symbols-outlined controls" v-else @click="playPause()" id="play-pause">play_arrow</span>
<span class="material-symbols-outlined controls forward-back" @click="control( 'forward' )" :style="'rotate: ' + 360 * clickCountForward + 'deg;'">forward_10</span>
<span class="material-symbols-outlined controls next-previous" @click="control( 'next' )" id="next">skip_next</span>
<div :class="'controls-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
<div class="main-controls">
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous" v-if="isShowingFullScreenPlayer">skip_previous</span>
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'" v-if="isShowingFullScreenPlayer">replay_10</span>
<span class="material-symbols-outlined controls" v-if="isPlaying" @click="playPause()" id="play-pause">pause</span>
<span class="material-symbols-outlined controls" v-else @click="playPause()" id="play-pause">play_arrow</span>
<span class="material-symbols-outlined controls forward-back" @click="control( 'forward' )" :style="'rotate: ' + 360 * clickCountForward + 'deg;'" v-if="isShowingFullScreenPlayer">forward_10</span>
<span class="material-symbols-outlined controls next-previous" @click="control( 'next' )" id="next">skip_next</span>
</div>
<span class="material-symbols-outlined controls" @click="control( 'repeat' )" style="margin-left: 20px;">repeat{{ repeatMode }}</span>
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
<div class="slider-wrapper" v-if="isShowingFullScreenPlayer">
<div class="slider-pb-pos">
<p class="playback-pos">{{ nicePlaybackPos }}</p>
<p class="playback-duration" @click="toggleRemaining()">{{ niceDuration }}</p>
</div>
<sliderView :position="pos" :active="true" :duration="duration" name="main" @pos="( pos ) => player.goToPos( pos )"></sliderView>
</div>
<div class="shuffle-repeat" v-if="isShowingFullScreenPlayer">
<span class="material-symbols-outlined controls" @click="control( 'repeat' )" style="margin-right: auto;">repeat{{ repeatMode }}</span>
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
</div>
</div>
</div>
</div>
<div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )">
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying"
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"></playlistView>
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos"
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )"></playlistView>
</div>
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
<audio src="" id="local-audio" controls="false"></audio>
</div>
</template>
@@ -46,7 +60,9 @@
import playlistView from '@/components/playlistView.vue';
import MusicKitJSWrapper from '@/scripts/music-player';
import sliderView from './sliderView.vue';
import type { Song } from '@/scripts/song';
import type { ReadFile, Song, SongMove } from '@/scripts/song';
import { parseBlob } from 'music-metadata-browser';
import notificationsModule from './notificationsModule.vue';
const isPlaying = ref( false );
const repeatMode = ref( '' );
@@ -65,6 +81,7 @@
const currentlyPlayingSongArtist = ref( '' );
const pos = ref( 0 );
const duration = ref( 0 );
const notifications = ref( notificationsModule );
const emits = defineEmits( [ 'playerStateChange' ] );
@@ -79,6 +96,10 @@
}
}
const toggleRemaining = () => {
isShowingRemainingTime.value = !isShowingRemainingTime.value;
}
const control = ( action: string ) => {
if ( action === 'pause' ) {
isPlaying.value = false;
@@ -103,9 +124,11 @@
if ( shuffleMode.value === '' ) {
shuffleMode.value = '_on';
player.setShuffle( true );
getDetails();
} else {
shuffleMode.value = '';
player.setShuffle( false );
getDetails();
}
getDetails();
} else if ( action === 'forward' ) {
@@ -177,13 +200,71 @@
} );
}
const selectCustomPlaylist = async ( pl: ReadFile[] ) => {
let n = notifications.value.createNotification( 'Analyzing playlist', 200, 'progress', 'normal' );
playlist.value = [];
let plLoad: Song[] = [];
for ( let element in pl ) {
try {
plLoad.push( await fetchSongData( pl[ element ] ) );
} catch ( e ) {
console.error( e );
}
notifications.value.updateNotification( n, `Analyzing playlist (${element}/${pl.length})` );
}
playlist.value = plLoad;
player.setPlaylist( playlist.value );
player.prepare( 0 );
isPlaying.value = true;
setTimeout( () => {
startProgressTracker();
getDetails();
}, 2000 );
notifications.value.cancelNotification( n );
notifications.value.createNotification( 'Playlist loaded', 10, 'ok', 'normal' );
}
const fetchSongData = ( songDetails: ReadFile ): Promise<Song> => {
return new Promise( ( resolve, reject ) => {
fetch( songDetails.url ).then( res => {
if ( res.status === 200 ) {
res.blob().then( blob => {
parseBlob( blob ).then( data => {
player.findSongOnAppleMusic( data.common.title ?? songDetails.filename.split( '.' )[ 0 ] ).then( d => {
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
const song: Song = {
artist: data.common.artist ?? d.data.results.songs.data[ 0 ].attributes.artistName,
title: data.common.title ?? d.data.results.songs.data[ 0 ].attributes.name,
duration: data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
id: songDetails.url,
origin: 'disk',
cover: url
}
resolve( song );
} ).catch( e => {
reject( e );
} );
} ).catch( e => {
reject( e );
} );
} ).catch( e => {
reject( e );
} );
}
} ).catch( e => {
reject( e );
} );
} );
}
const getDetails = () => {
const details = player.getPlayingSong();
currentlyPlayingSongName.value = details.title;
coverArt.value = details.cover;
currentlyPlayingSongIndex.value = player.getPlayingSongID();
currentlyPlayingSongIndex.value = player.getQueueID();
playlist.value = player.getQueue();
console.log( playlist.value );
currentlyPlayingSongArtist.value = details.artist;
}
@@ -207,7 +288,10 @@
let progressTracker = 0;
let hasReachedEnd = false;
let hasStarted = false;
const startProgressTracker = () => {
hasReachedEnd = false;
isPlaying.value = true;
const playingSong = player.getPlayingSong();
duration.value = playingSong.duration;
@@ -224,9 +308,18 @@
}
progressTracker = setInterval( () => {
pos.value = player.getPlaybackPos();
if ( pos.value > playingSong.duration - 1 ) {
// TODO: repeat
control( 'next' );
if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) {
stopProgressTracker();
hasReachedEnd = true;
if ( repeatMode.value === '_one_on' ) {
player.goToPos( 0 );
} else {
control( 'next' );
}
}
if ( pos.value > 0 && !hasStarted ) {
hasStarted = true;
}
const minuteCount = Math.floor( pos.value / 60 );
@@ -264,6 +357,49 @@
isPlaying.value = false;
}
const moveSong = ( move: SongMove ) => {
player.moveSong( move );
getDetails();
}
const addNewSongs = async ( songs: ReadFile[] ) => {
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' );
playlist.value = player.getPlaylist();
for ( let element in songs ) {
try {
playlist.value.push( await fetchSongData( songs[ element ] ) );
} catch ( e ) {
console.error( e );
}
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
}
player.setPlaylist( playlist.value );
player.prepare( 0 );
isPlaying.value = true;
setTimeout( () => {
startProgressTracker();
getDetails();
}, 2000 );
notifications.value.cancelNotification( n );
notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' );
}
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
document.addEventListener( 'keydown', ( e ) => {
if ( e.key === ' ' ) {
// TODO: fix
e.preventDefault();
playPause();
} else if ( e.key === 'ArrowRight' ) {
e.preventDefault();
control( 'next' );
} else if ( e.key === 'ArrowLeft' ) {
e.preventDefault();
control( 'previous' );
}
} );
defineExpose( {
logIntoAppleMusic,
getPlaylists,
@@ -271,12 +407,12 @@
getAuth,
skipLogin,
selectPlaylist,
selectCustomPlaylist,
} );
</script>
<style scoped>
.player {
height: 15%;
width: 100%;
display: flex;
justify-content: center;
@@ -296,7 +432,14 @@
position: relative
}
.main-player.full-screen {
flex-direction: column;
height: 30vh;
min-height: 250px;
}
.song-name-wrapper {
margin-top: 10px;
cursor: pointer;
margin-left: 10px;
width: 100%;
@@ -305,22 +448,66 @@
font-weight: bold;
font-size: 1.25rem;
display: flex;
flex-direction: column;
flex-direction: row;
justify-content: center;
align-items: center;
flex-grow: 0;
}
.song-name-wrapper.full-screen {
flex-direction: row;
max-height: 50%;
align-items: center;
}
.name-time {
margin-right: auto;
margin-left: 10px;
}
.song-name {
margin: 0;
height: fit-content;
}
.slider-wrapper {
position: relative;
width: 90%;
margin-bottom: 5px;
}
.shuffle-repeat {
margin-top: 5px;
display: flex;
width: 80%;
position: relative;
z-index: 5;
}
.slider-pb-pos {
display: flex;
justify-content: center;
align-items: center;
}
.slider-pb-pos .playback-duration {
margin-top: 5px;
margin-left: auto;
user-select: none;
cursor: pointer;
}
.slider-pb-pos .playback-pos {
margin-top: 5px;
user-select: none;
user-select: none;
}
.logo-player {
cursor: pointer;
height: 80%;
margin-left: 30px;
}
.player-hidden {
height: 100%;
width: auto;
margin-left: 10px;
}
.hidden {
@@ -342,6 +529,17 @@
flex-wrap: nowrap;
}
.controls-wrapper.full-screen {
flex-direction: column;
width: 80%;
}
.main-controls {
display: flex;
justify-content: center;
align-items: center;
}
#play-pause {
font-size: 2.5rem;
}
@@ -390,7 +588,7 @@
}
.pl-wrapper {
height: 80vh;
height: 70vh;
}
.playback {
@@ -412,6 +610,7 @@
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
.playback-pos-wrapper p {
@@ -425,4 +624,27 @@
.playback-pos-wrapper.full-screen .playback-duration {
margin-left: auto;
}
@media only screen and (min-width: 800px) {
.slider-wrapper {
width: 40%;
}
.shuffle-repeat {
width: 35%;
}
.main-controls .controls {
font-size: 2rem;
}
#play-pause {
font-size: 3rem;
}
}
#local-audio {
position: fixed;
bottom: -50%;
}
</style>

View File

@@ -1,7 +1,12 @@
<template>
<div>
<h1>Playlist</h1>
<input type="file" multiple accept="audio/*" id="more-songs">
<button @click="addNewSongs()">Load local songs</button>
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
<div class="playlist-box" id="pl-box">
<!-- TODO: Allow sorting -->
<!-- TODO: Allow adding more songs with search on Apple Music or loading from local disk -->
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
@@ -16,16 +21,18 @@
<span class="material-symbols-outlined play-icon" @click="control( 'play' )" v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )">play_arrow</span>
<span class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>play_arrow</span>
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'up' )" title="Move song up" v-if="canBeMoved( 'up', song.id )">arrow_upward</span>
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'down' )" title="Move song down" v-if="canBeMoved( 'down', song.id )">arrow_downward</span>
<h3 class="song-title">{{ song.title }}</h3>
<p class="playing-in">playing in</p>
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Song } from '@/scripts/song';
import { computed } from 'vue';
import type { ReadFile, Song } from '@/scripts/song';
import { computed, ref } from 'vue';
const props = defineProps( {
'playlist': {
@@ -42,8 +49,14 @@
default: true,
required: true,
type: Boolean,
},
'pos': {
default: 0,
required: false,
type: Number,
}
} );
const hasSelectedSongs = ref( true );
const computedPlaylist = computed( () => {
let pl: Song[] = [];
@@ -54,31 +67,53 @@
return pl;
} );
// TODO: Implement
// const getTimeUntil = computed( () => {
// return ( song ) => {
// let timeRemaining = 0;
// for ( let i = this.queuePos; i < Object.keys( this.songs ).length; i++ ) {
// if ( this.songs[ i ] == song ) {
// break;
// }
// timeRemaining += parseInt( this.songs[ i ].duration );
// }
// if ( this.isPlaying ) {
// if ( timeRemaining === 0 ) {
// return 'Currently playing';
// } else {
// return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
// }
// } else {
// if ( timeRemaining === 0 ) {
// return 'Plays next';
// } else {
// return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
// }
// }
// }
// } );
const canBeMoved = computed( () => {
return ( direction: movementDirection, songID: string ): boolean => {
let id = 0;
for ( let song in props.playlist ) {
if ( props.playlist[ song ].id === songID ) {
id = parseInt( song );
break;
}
}
if ( direction === 'up' ) {
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
return false;
}
return true;
} else {
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
return false;
}
return true;
}
}
} )
const getTimeUntil = computed( () => {
return ( song: Song ) => {
let timeRemaining = 0;
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
if ( props.playlist[ i ] == song ) {
break;
}
timeRemaining += props.playlist[ i ].duration;
}
if ( props.isPlaying ) {
if ( timeRemaining === 0 ) {
return 'Currently playing';
} else {
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
}
} else {
if ( timeRemaining === 0 ) {
return 'Plays next';
} else {
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
}
}
}
} );
const control = ( action: string ) => {
@@ -89,12 +124,44 @@
emits( 'play-song', song );
}
const emits = defineEmits( [ 'play-song', 'control' ] );
const addNewSongs = () => {
// TODO: Also allow loading Apple Music songs
const fileURLList: ReadFile[] = [];
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
if ( allFiles.length > 0 ) {
hasSelectedSongs.value = true;
for ( let file = 0; file < allFiles.length; file++ ) {
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
}
emits( 'add-new-songs', fileURLList );
} else {
hasSelectedSongs.value = false;
}
}
type movementDirection = 'up' | 'down';
const moveSong = ( songID: string, direction: movementDirection ) => {
let newSongPos = 0;
let hasFoundSongToMove = false;
for ( let el in props.playlist ) {
if ( props.playlist[ el ].id === songID ) {
const currPos = parseInt( el );
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
hasFoundSongToMove = true;
break;
}
}
if ( hasFoundSongToMove ) {
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } );
}
}
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs' ] );
</script>
<style scoped>
.playlist-box {
height: 80vh !important;
height: calc( 100% - 100px );
width: 100%;
overflow: scroll;
display: flex;
@@ -222,4 +289,9 @@
display: none;
}
.move-icon {
font-size: 1.5rem;
cursor: pointer;
user-select: none;
}
</style>

View File

@@ -1,10 +1,16 @@
<template>
<div class="playlists">
<h3 style="width: fit-content;">Your playlists</h3>
<div v-if="$props.playlists ? $props.playlists.length < 1 : true">
loading...
<div v-if="( $props.playlists ? $props.playlists.length < 1 : true ) && $props.isLoggedIn">
Loading...
<!-- TODO: Make prettier -->
</div>
<div v-else-if="!$props.isLoggedIn">
<p>You are not logged into Apple Music.</p>
<input class="fancy-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
<button @click="loadPlaylistFromDisk()" class="fancy-button">Load custom playlist from disk</button>
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p>
</div>
<div class="playlist-wrapper">
<div v-for="pl in $props.playlists" v-bind:key="pl.id" class="playlist" @click="selectPlaylist( pl.id )">
{{ pl.attributes.name }}
@@ -14,15 +20,38 @@
</template>
<script setup lang="ts">
import type { ReadFile } from '@/scripts/song';
import { ref } from 'vue';
const hasSelectedSongs = ref( true );
defineProps( {
'playlists': {
'default': [],
'type': Array<any>,
'required': true,
},
'isLoggedIn': {
'default': false,
'type': Boolean,
'required': true,
}
} );
const emits = defineEmits( [ 'selected-playlist' ] );
const loadPlaylistFromDisk = () => {
const fileURLList: ReadFile[] = [];
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
if ( allFiles.length > 0 ) {
hasSelectedSongs.value = true;
for ( let file = 0; file < allFiles.length; file++ ) {
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
}
emits( 'custom-playlist', fileURLList );
} else {
hasSelectedSongs.value = false;
}
}
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] );
const selectPlaylist = ( id: string ) => {
emits( 'selected-playlist', id );

View File

@@ -0,0 +1,481 @@
<!-- eslint-disable no-undef -->
<template>
<div id="popup-backdrop">
<div class="popup-container">
<div class="popup" :class="size">
<div class="close-wrapper"><span class="material-symbols-outlined close-button" @click="closePopup( 'cancel' );" title="Close this popup">close</span></div>
<div class="message-container">
<div v-if="contentType === 'string'" class="options">
<h3>{{ data.message }}</h3>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Close popup" class="buttons fancy-button">Ok</button>
</div>
</div>
<div v-else-if="contentType === 'html'" v-html="data.message" class="options"></div>
<div v-else-if="contentType === 'code'" class="options">
<h3>{{ data.message }}</h3>
<button @click="copy()" id="code-copy" class="buttons fancy-button">Copy</button>
<pre>
<code>
{{ data.options.code }}
</code>
</pre>
</div>
<div v-else-if="contentType === 'long-text'" class="options">
<h3>{{ data.message }}</h3>
<p>{{ data.options.note }}</p>
<textarea cols="80" rows="10" v-model="data.selected" id="text-input"></textarea>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
</div>
</div>
<div v-else-if="contentType === 'text'" class="options">
<h3>{{ data.message }}</h3>
<input type="text" v-model="data.selected">
<p>{{ info }}</p>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
</div>
</div>
<div v-else-if="contentType === 'number'" class="options">
<h3>{{ data.message }}</h3>
<input type="number" v-model="data.selected">
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
</div>
</div>
<div v-else-if="contentType === 'settings'" class="options">
<h3>{{ data.message }}</h3>
<settings v-model:settings="data.options"></settings>
<div class="button-wrapper">
<button @click="submitSettings( 'ok' )" title="Save changes" class="buttons fancy-button">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'confirm'" class="confirm options">
<h3>{{ data.message }}</h3>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.ok ?? 'Ok' }}</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
</div>
</div>
<div v-else-if="contentType === 'dropdown'" class="options">
<h3>{{ data.message }}</h3>
<select id="select" v-model="data.selected">
<option v-for="selectOption in data.options" :key="selectOption.value" :value="selectOption.value">{{ selectOption.displayName }}</option>
</select>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
</div>
</div>
<div v-else-if="contentType === 'selection'" class="options selection">
<h3>{{ data.message }}</h3>
<div v-for="selectOption in data.options.selections" :key="selectOption.value" class="select-button-wrapper">
<button class="select-button" @click="closePopupAdvanced( 'ok', selectOption.value )">{{ selectOption.displayName }}</button>
</div>
</div>
<div v-else-if="contentType === 'iframe'" class="options iframe-wrapper">
<iframe :src="data.options.link" frameborder="0" class="popup-iframe"></iframe>
</div>
<div v-else-if="contentType === 'editor'" class="options">
<!-- Create the toolbar container -->
<h3>{{ data.message }}</h3>
<p v-if="data.options.note" v-html="data.options.note"></p>
<!-- Optional toggles (added via options object) -->
<table class="editor-options">
<tr v-for="element in data.options.settings" :key="element.id">
<td>
{{ element.displayName }}
</td>
<td>
<input type="text" v-if="element.type === 'text'" v-model="data.selected[ element.id ]">
<input type="number" v-else-if="element.type === 'number'" v-model="data.selected[ element.id ]">
<input type="email" v-else-if="element.type === 'email'" v-model="data.selected[ element.id ]">
<select v-else-if="element.type === 'dropdown'" v-model="data.selected[ element.id ]">
<option v-for="el in element.options" :key="el.value" :value="el.value">{{ el.displayName }}</option>
</select>
</td>
</tr>
</table>
<div id="quill-toolbar">
<span class="ql-formats">
<select class="ql-font" title="Fonts">
<option selected="" title="Default"></option>
<option value="serif" title="Serif"></option>
<option value="monospace" title="Monospace"></option>
</select>
<select class="ql-size" title="Font size">
<option value="small" title="Small"></option>
<option selected="" title="Default"></option>
<option value="large" title="Large"></option>
<option value="huge" title="Huge"></option>
</select>
</span>
<span class="ql-formats">
<button class="ql-bold" title="Bold"></button>
<button class="ql-italic" title="Italic"></button>
<button class="ql-underline" title="Underlined"></button>
<button class="ql-strike" title="Strikethrough"></button>
</span>
<span class="ql-formats">
<select class="ql-color" title="Text colour"></select>
<select class="ql-background" title="Background colour"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered" title="Ordered list"></button>
<button class="ql-list" value="bullet" title="Bullet points"></button>
<select class="ql-align" title="Alignment">
<option selected="" title="left"></option>
<option value="center" title="center"></option>
<option value="right" title="right"></option>
<option value="justify" title="block"></option>
</select>
</span>
<span class="ql-formats">
<button class="ql-link" title="Insert link"></button>
<button class="ql-image" title="Insert image"></button>
</span>
</div>
<!-- Create the editor container -->
<div id="quill-editor">
</div>
<div class="message-iframe" v-if="data.selected.oldMsg" style="height: 60vh;">
<p>Attached message: </p>
<iframe :srcdoc="data.selected.oldMsg" frameborder="0" class="message-iframe"></iframe>
</div>
<div class="button-wrapper">
<button @click="closePopupEditor()" :title="data.options.saveButtonHint" class="buttons fancy-button">{{ data.options.saveButtonDisplay }}</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">Cancel</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Options to be passed in: html, settings (for settings component), strings, confirm, dropdowns, selection -->
<script>
import settings from '@/components/settingsOptions.vue';
import hljs from 'highlight.js';
import beautify from 'json-beautify';
import Quill from 'quill';
import( 'quill/dist/quill.snow.css' );
export default {
name: 'popupsHandler',
components: {
settings,
},
props: {
size: {
type: String,
'default': 'normal',
},
},
data () {
return {
contentType: 'dropdown',
data: {
options: {
display: '',
},
},
info: '',
editor: null,
};
},
methods: {
closePopup( message ) {
if ( this.data.options.disallowedCharacters ) {
for ( let letter in this.data.selected ) {
if ( this.data.options.disallowedCharacters.includes( this.data.selected[ letter ] ) ) {
this.info = `Illegal character "${ this.data.selected[ letter ] }"`;
return false;
}
}
}
// eslint-disable-next-line no-undef
$( '#popup-backdrop' ).fadeOut( 300 );
if ( message ) {
this.$emit( 'data', { 'data': this.data.selected, 'status': message } );
}
},
closePopupEditor () {
this.data.selected;
this.data.selected.mail = document.getElementsByClassName( 'ql-editor' )[ 0 ].innerHTML + ( this.data.selected.oldMsg ?? '' );
this.closePopup( 'editor' );
},
selectTicket ( option ) {
let total = 0;
for ( let i in this.data.options.count ) {
total += this.data.options.count[ i ];
}
if ( total < this.data.options.max ) {
this.data.options.count[ option ] += 1;
}
},
deselectTicket ( option ) {
if ( this.data.options.count[ option ] > 0 ) {
this.data.options.count[ option ] -= 1;
}
},
submitTicket () {
// eslint-disable-next-line no-undef
$( '#popup-backdrop' ).fadeOut( 300 );
this.$emit( 'ticket', { 'data': this.data.options.count, 'component': this.data.options.id } );
},
closePopupAdvanced ( message, data ) {
this.data[ 'selected' ] = data;
this.closePopup( message );
},
submitSettings () {
// eslint-disable-next-line no-undef
$( '#popup-backdrop' ).fadeOut( 300 );
const dat = this.data.options;
let ret = {};
for ( let setting in dat ) {
if ( dat[ setting ][ 'type' ] !== 'link' ) {
ret[ setting ] = dat[ setting ][ 'value' ];
}
}
this.$emit( 'data', { 'data': ret, 'status': 'settings' } );
},
openPopup ( message, options, dataType, selected ) {
this.data = {
'message': message ?? 'No message defined on method call!!',
'options': options ?? { '1': { 'value': 'undefined', 'displayName': 'No options specified in call' } },
'selected': selected ?? ''
};
this.contentType = dataType ?? 'string';
// eslint-disable-next-line no-undef
$( '#popup-backdrop' ).fadeIn( 300 );
if ( dataType === 'code' ) {
if ( options.lang === 'json' ) {
this.data.options.code = beautify( options.code, null, 2, 50 );
}
setTimeout( () => {
hljs.highlightAll();
}, 200 );
} else if ( dataType === 'editor' ) {
setTimeout( () => {
if ( !document.getElementById( 'quill-editor' ).classList.contains( 'ql-container' ) ) {
this.editor = new Quill( '#quill-editor', {
modules: { toolbar: '#quill-toolbar' },
theme: 'snow',
} );
if ( this.data.selected === '' ) {
this.data.selected = {};
}
setTimeout( () => {
if ( selected.message ) {
console.log( selected.message );
document.getElementsByClassName( 'ql-editor' )[ 0 ].innerHTML = selected.message;
}
}, 500 );
}
}, 200 );
}
},
copy() {
const codeCopy = document.getElementById( 'code-copy' )
codeCopy.innerHTML = 'Copied!';
navigator.clipboard.writeText( this.data.options.code );
setTimeout( () => {
codeCopy.innerHTML = 'Copy';
}, 2000 );
}
},
};
</script>
<style scoped>
.message-iframe {
width: 100%;
height: 50vh;
margin-top: 1%;
}
pre {
width: 100%;
}
code {
text-align: left;
}
#popup-backdrop {
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100vw;
height: 100vh;
background-color: rgba( 0, 0, 0, 0.6 );
display: none;
}
.popup-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#text-input {
width: 90%;
resize: vertical;
}
.button-wrapper {
width: 100%;
margin-top: 3%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.close-wrapper {
width: 100%;
height: 5%;
display: flex;
justify-content: center;
align-items: flex-end;
flex-direction: column;
}
.close-button {
margin-right: 1vw;
margin-top: 2vw;
font-size: 200%;
cursor: pointer;
}
.popup {
border: none;
border-radius: 20px;
background-color: white;
width: 90vw;
height: 80vh;
}
.popup-iframe {
width: 100%;
height: 100%;
}
.iframe-wrapper {
height: 100%;
}
.message-container {
height: 90%;
width: 90%;
margin-left: 5%;
overflow: scroll;
}
.options {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
overflow: visible;
min-height: 100%;
width: 100%;
}
.options .buttons {
padding: 1% 2%;
margin: 5px;
display: inline-block;
}
.options .buttons:hover {
background-color: darkgreen;
}
.select-button-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.select-button {
background-color: rgba( 0, 0, 0, 0 ) !important;
color: black !important;
padding: 3vh 2vw !important;
border: solid black 1px;
border-radius: 5px;
transition: all 0.5s ease-in-out;
margin-bottom: 1vh;
width: 90%;
cursor: pointer;
}
.select-button:hover {
background-color: gray !important;
}
.controls {
user-select: none;
cursor: pointer;
font-size: 100%;
font-weight: bold;
border: solid var( --primary-color ) 1px;
border-radius: 100%;
}
@media only screen and (min-width: 999px) {
.small {
width: 40%;
height: 40%;
}
.normal {
width: 50%;
height: 50%;
}
.big {
width: 60%;
height: 60%;
}
.bigger {
width: 70%;
height: 70%;
}
.huge {
width: 80%;
height: 80%;
}
}
#quill-editor, #quill-toolbar {
width: 90%;
}
#quill-editor {
min-height: 20vh;
}
.editor-options {
width: 80%;
}
</style>

View File

@@ -1,4 +1,4 @@
import type { Song } from "./song";
import type { SearchResult, Song, SongMove } from "./song";
interface Config {
devToken: string;
@@ -20,6 +20,7 @@ class MusicKitJSWrapper {
isShuffleEnabled: boolean;
hasEncounteredAuthError: boolean;
queuePos: number;
audioPlayer: HTMLAudioElement;
constructor () {
this.playingSongID = 0;
@@ -35,6 +36,7 @@ class MusicKitJSWrapper {
this.isLoggedIn = false;
this.hasEncounteredAuthError = false;
this.queuePos = 0;
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
const self = this;
@@ -77,6 +79,7 @@ class MusicKitJSWrapper {
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => {
if ( res.status === 200 ) {
res.text().then( token => {
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
// MusicKit global is now defined
MusicKit.configure( {
developerToken: token,
@@ -204,7 +207,11 @@ class MusicKitJSWrapper {
console.log( err );
} );
} else {
// TODO: Implement
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
this.audioPlayer.src = this.playlist[ this.playingSongID ].id;
setTimeout( () => {
this.control( 'play' );
}, 500 );
}
return true;
} else {
@@ -225,8 +232,8 @@ class MusicKitJSWrapper {
this.musicKit.play();
return false;
} else {
this.audioPlayer.play();
return false;
// TODO: Implement
}
} else {
return false;
@@ -237,8 +244,8 @@ class MusicKitJSWrapper {
this.musicKit.pause();
return false;
} else {
this.audioPlayer.pause();
return false;
// TODO: Implement
}
} else {
return false;
@@ -248,8 +255,8 @@ class MusicKitJSWrapper {
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
return false;
} else {
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
return false;
// TODO: Implement
}
case "skip-10":
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
@@ -266,30 +273,30 @@ class MusicKitJSWrapper {
}
}
} else {
// TODO: Finish
// if ( this.audioPlayer.currentTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
// this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10;
// this.pos = this.audioPlayer.currentTime;
// this.sendUpdate( 'pos' );
// } else {
// if ( this.repeatMode !== 'one' ) {
// this.control( 'next' );
// } else {
// this.audioPlayer.currentTime = 0;
// this.pos = this.audioPlayer.currentTime;
// this.sendUpdate( 'pos' );
// }
// }
if ( this.audioPlayer.currentTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10;
} else {
if ( this.repeatMode !== 'once' ) {
this.control( 'next' );
} else {
this.audioPlayer.currentTime = 0;
}
}
return false;
}
case "next":
if ( this.queuePos < this.queue.length ) {
if ( this.queuePos < this.queue.length - 1 ) {
this.queuePos += 1;
this.prepare( this.queue[ this.queuePos ] );
return true;
} else {
this.queuePos = 0;
this.control( 'pause' );
if ( this.repeatMode !== 'all' ) {
this.control( 'pause' );
} else {
this.playingSongID = this.queue[ this.queuePos ];
this.prepare( this.queue[ this.queuePos ] );
}
return true;
}
case "previous":
@@ -324,6 +331,13 @@ class MusicKitJSWrapper {
this.queue.push( parseInt( song ) );
}
}
// Find current song ID in queue
for ( const el in this.queue ) {
if ( this.queue[ el ] === this.playingSongID ) {
this.queuePos = parseInt( el );
break;
}
}
}
setRepeatMode ( mode: RepeatMode ) {
@@ -334,10 +348,40 @@ class MusicKitJSWrapper {
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.seekToTime( pos );
} else {
// TODO: Implement
this.audioPlayer.currentTime = pos;
}
}
moveSong ( move: SongMove ) {
const newQueue = [];
const finishedQueue = [];
let songID = 0;
for ( const song in this.playlist ) {
if ( this.playlist[ song ].id === move.songID ) {
songID = parseInt( song );
break;
}
}
for ( const el in this.queue ) {
if ( this.queue[ el ] !== songID ) {
newQueue.push( this.queue[ el ] );
}
}
let hasBeenAdded = false;
for ( const el in newQueue ) {
if ( parseInt( el ) === move.newPos ) {
finishedQueue.push( songID );
hasBeenAdded = true;
}
finishedQueue.push( newQueue[ el ] );
}
if ( !hasBeenAdded ) {
finishedQueue.push( songID );
}
this.queue = finishedQueue;
}
/**
* Get the current position of the play heed. Will return in ms since start of the song
* @returns {number}
@@ -346,8 +390,7 @@ class MusicKitJSWrapper {
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
return this.musicKit.currentPlaybackTime;
} else {
return 0;
// TODO: Implement
return this.audioPlayer.currentTime;
}
}
@@ -367,6 +410,14 @@ class MusicKitJSWrapper {
return this.playingSongID;
}
/**
* Get the queue index of the currently playing song
* @returns {number}
*/
getQueueID (): number {
return this.queuePos;
}
/**
* Get the full playlist, as it is set currently, not ordered by queue settings, but as passed in originally
* @returns {Song[]}
@@ -405,14 +456,25 @@ class MusicKitJSWrapper {
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
return this.musicKit.isPlaying;
} else {
// TODO: Implement
return false;
return !this.audioPlayer.paused;
}
}
// findSongOnAppleMusic ( searchTerm: string ): Song => {
// TODO: Implement
// }
findSongOnAppleMusic ( searchTerm: string ): Promise<SearchResult> {
// TODO: Make storefront adjustable
return new Promise( ( resolve, reject ) => {
const queryParameters = {
term: ( searchTerm ),
types: [ 'songs' ],
};
this.musicKit.api.music( `v1/catalog/ch/search`, queryParameters ).then( results => {
resolve( results );
} ).catch( e => {
console.error( e );
reject( e );
} );
} );
}
}
export default MusicKitJSWrapper;

View File

@@ -1,7 +1,44 @@
const subscribe = ( handler: ( data: any ) => {} ): string => {
return '';
/*
* MusicPlayerV2 - notificationHandler.ts
*
* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
// These functions handle connections to the backend with socket.io
import { io, type Socket } from "socket.io-client"
class NotificationHandler {
socket: Socket;
constructor () {
this.socket = io();
}
/**
* Description
* @param {string} event The event to emit
* @param {any} data
* @returns {void}
*/
emit ( event: string, data: any ): void {
this.socket.emit( event, data );
}
registerListener ( event: string, cb: ( data: any ) => void ): void {
this.socket.on( event, cb );
}
disconnect (): void {
this.socket.disconnect();
}
joinRoom ( roomName: string ): void {
// this.socket.
}
}
const unsubscribe = ( id: string ) => {
}
export default NotificationHandler;

View File

@@ -40,4 +40,44 @@ export interface Song {
* (OPTIONAL) This will be displayed in brackets on the showcase screens
*/
additionalInfo?: string;
}
export interface ReadFile {
url: string;
filename: string;
}
export interface SearchResult {
data: {
results: {
songs: {
data: AppleMusicSongData[],
href: string;
}
};
}
}
export interface AppleMusicSongData {
id: string,
type: string;
href: string;
attributes: {
albumName: string;
artistName: string;
artwork: {
width: number,
height: number,
url: string
},
name: string;
genreNames: string[];
durationInMillis: number;
}
}
export interface SongMove {
songID: string;
newPos: number;
}

View File

@@ -1,14 +1,15 @@
<template>
<div class="app-view">
<div class="home-view" v-if="isLoggedIntoAppleMusic">
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"></libraryView>
<div class="home-view" v-if="isReady">
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"
:is-logged-in="isLoggedIntoAppleMusic" @custom-playlist="( pl ) => selectCustomPlaylist( pl )"></libraryView>
</div>
<div v-else class="login-view">
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">Log into Apple Music</button>
<button class="fancy-button" title="This allows you to use local playlists only. Cover images for your songs will be fetched from the apple music api as good as possible" @click="skipLogin()">Continue without logging in</button>
</div>
<playerView :class="'player-view' + ( isLoggedIntoAppleMusic ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
<playerView :class="'player-view' + ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
ref="player"></playerView>
</div>
</template>
@@ -17,8 +18,10 @@
import playerView from '@/components/playerView.vue';
import libraryView from '@/components/libraryView.vue';
import { ref } from 'vue';
import type { ReadFile } from '@/scripts/song';
const isLoggedIntoAppleMusic = ref( false );
const isReady = ref( false );
const isShowingFullScreenPlayer = ref( false );
const player = ref( playerView );
const playlists = ref( [] );
@@ -37,6 +40,7 @@
loginChecker = setInterval( () => {
if ( player.value.getAuth()[ 0 ] ) {
isLoggedIntoAppleMusic.value = true;
isReady.value = true;
player.value.getPlaylists( ( data ) => {
playlists.value = data.data.data;
} );
@@ -49,13 +53,18 @@
}
const skipLogin = () => {
isLoggedIntoAppleMusic.value = true;
isReady.value = true;
isLoggedIntoAppleMusic.value = false;
player.value.skipLogin();
}
const selectPlaylist = ( id: string ) => {
player.value.selectPlaylist( id );
}
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
player.value.selectCustomPlaylist( playlist );
}
</script>
<style scoped>

View File

@@ -1,8 +1,8 @@
<template>
<div class="home-view">
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
<h1>MusicPlayer</h1>
<button @click="login()" class="fancy-button">Log in</button>
<button @click="login()" class="fancy-button">Log in / Sign up</button>
<p>MusicPlayer is a browser based Music Player, that allows you to connect other devices, simply with another web-browser, where you can see the current playlist with sleek animations. You can log in using your Apple Music account or load a playlist from your local disk, simply by selecting the songs using a file picker.</p>
</div>
</template>
@@ -28,5 +28,6 @@ import router from '@/router';
.logo {
height: 50vh;
border-radius: 50px;
}
</style>

View File

@@ -2,6 +2,7 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'
// https://vitejs.dev/config/
export default defineConfig({
@@ -13,6 +14,18 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
optimizeDeps: {
esbuildOptions: {
define: {
global: 'globalThis',
},
plugins: [
NodeGlobalsPolyfillPlugin({
buffer: true,
}),
],
},
},
server: {
port: 8080
}

View File

@@ -1 +0,0 @@
../config/

View File

@@ -18,7 +18,8 @@
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"node-mysql": "^0.4.2",
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist"
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
"socket.io": "^4.7.5"
},
"devDependencies": {
"typescript": "^5.4.5"
@@ -40,6 +41,12 @@
"typescript": "^5.3.3"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -59,6 +66,12 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
@@ -174,6 +187,15 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/better-js-class": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/better-js-class/-/better-js-class-0.1.3.tgz",
@@ -375,6 +397,68 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io": {
"version": "6.5.5",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz",
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -1073,6 +1157,116 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
"integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.5.2",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-adapter/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
@@ -1186,6 +1380,27 @@
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -29,6 +29,7 @@
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"node-mysql": "^0.4.2",
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist"
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
"socket.io": "^4.7.5"
}
}

View File

@@ -19,8 +19,8 @@ if ( typeof( __dirname ) === 'undefined' ) {
}
const dbRef = {
'user': 'jh_store_users',
'users': 'jh_store_users',
'user': 'music_users',
'users': 'music_users',
};

View File

@@ -60,7 +60,6 @@ class SQLDB {
console.log( '[ SQL ] Connected to database successfully' );
self.sqlConnection.on( 'error', ( err ) => {
if ( err.code === 'ECONNRESET' ) {
console.error( '[ SQL ] Reconnecting to database, because connection was reset!' );
self.isRecovering = true;
setTimeout( () => {
self.disconnect();
@@ -84,7 +83,7 @@ class SQLDB {
if ( error ) throw error;
if ( results[ 0 ][ '@@default_storage_engine' ] !== 'InnoDB' ) throw 'DB HAS TO USE InnoDB!';
} );
this.sqlConnection.query( 'CREATE TABLE jh_store_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, data VARCHAR( 55000 ), uid TINYTEXT, lang TINYTEXT, username TINYTEXT, stripe_user_id TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', ( error ) => {
this.sqlConnection.query( 'CREATE TABLE music_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, uid TINYTEXT, lang TINYTEXT, username TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', ( error ) => {
if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error;
return 'DONE';
} );