mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 04:54:23 +00:00
Prepare for new sdk
This commit is contained in:
@@ -75,7 +75,7 @@ const style = {
|
||||
],
|
||||
'@stylistic/eol-last': [
|
||||
'error',
|
||||
'always'
|
||||
'never'
|
||||
],
|
||||
'@stylistic/function-call-spacing': [
|
||||
'error',
|
||||
@@ -83,7 +83,9 @@ const style = {
|
||||
],
|
||||
'@stylistic/function-paren-newline': [
|
||||
'error',
|
||||
'multiline'
|
||||
{
|
||||
'minItems': 3
|
||||
}
|
||||
],
|
||||
'@stylistic/function-call-argument-newline': [
|
||||
'error',
|
||||
|
||||
184
MusicPlayerV2-GUI/package-lock.json
generated
184
MusicPlayerV2-GUI/package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"@melloware/coloris": "^0.24.0",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"buffer": "^6.0.3",
|
||||
"colorthief": "^2.2.0",
|
||||
"colorthief": "^2.6.0",
|
||||
"music-metadata-browser": "^2.5.10",
|
||||
"musickit-typescript": "^1.2.4",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -23,9 +23,10 @@
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/node": "^20.11.10",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
@@ -33,7 +34,7 @@
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"sass-embedded": "^1.92.0",
|
||||
"typescript": "~5.3.0",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"vite": "^7.1.4",
|
||||
"vue-tsc": "^2.0.29"
|
||||
}
|
||||
@@ -639,9 +640,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
|
||||
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
|
||||
"version": "9.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
|
||||
"integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1816,6 +1817,16 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
|
||||
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -1840,18 +1851,25 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
|
||||
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
|
||||
"integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz",
|
||||
"integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.42.0",
|
||||
"@typescript-eslint/type-utils": "8.42.0",
|
||||
"@typescript-eslint/utils": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
||||
"@typescript-eslint/scope-manager": "8.43.0",
|
||||
"@typescript-eslint/type-utils": "8.43.0",
|
||||
"@typescript-eslint/utils": "8.43.0",
|
||||
"@typescript-eslint/visitor-keys": "8.43.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1865,7 +1883,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"@typescript-eslint/parser": "^8.43.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -1881,16 +1899,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz",
|
||||
"integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz",
|
||||
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.42.0",
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/typescript-estree": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
||||
"@typescript-eslint/scope-manager": "8.43.0",
|
||||
"@typescript-eslint/types": "8.43.0",
|
||||
"@typescript-eslint/typescript-estree": "8.43.0",
|
||||
"@typescript-eslint/visitor-keys": "8.43.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1906,14 +1924,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz",
|
||||
"integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz",
|
||||
"integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.42.0",
|
||||
"@typescript-eslint/types": "^8.42.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.43.0",
|
||||
"@typescript-eslint/types": "^8.43.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1928,14 +1946,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz",
|
||||
"integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz",
|
||||
"integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0"
|
||||
"@typescript-eslint/types": "8.43.0",
|
||||
"@typescript-eslint/visitor-keys": "8.43.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1946,9 +1964,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz",
|
||||
"integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz",
|
||||
"integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1963,15 +1981,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz",
|
||||
"integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz",
|
||||
"integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/typescript-estree": "8.42.0",
|
||||
"@typescript-eslint/utils": "8.42.0",
|
||||
"@typescript-eslint/types": "8.43.0",
|
||||
"@typescript-eslint/typescript-estree": "8.43.0",
|
||||
"@typescript-eslint/utils": "8.43.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -1988,9 +2006,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz",
|
||||
"integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz",
|
||||
"integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2002,16 +2020,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz",
|
||||
"integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz",
|
||||
"integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.42.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.42.0",
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
||||
"@typescript-eslint/project-service": "8.43.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.43.0",
|
||||
"@typescript-eslint/types": "8.43.0",
|
||||
"@typescript-eslint/visitor-keys": "8.43.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -2057,16 +2075,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz",
|
||||
"integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz",
|
||||
"integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.42.0",
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/typescript-estree": "8.42.0"
|
||||
"@typescript-eslint/scope-manager": "8.43.0",
|
||||
"@typescript-eslint/types": "8.43.0",
|
||||
"@typescript-eslint/typescript-estree": "8.43.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2081,13 +2099,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz",
|
||||
"integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz",
|
||||
"integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/types": "8.43.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3420,6 +3438,20 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/@eslint/js": {
|
||||
"version": "9.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
|
||||
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
@@ -4014,9 +4046,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-buffer": {
|
||||
@@ -5923,9 +5955,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
|
||||
"integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6040,9 +6072,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
@@ -6406,16 +6438,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.42.0.tgz",
|
||||
"integrity": "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.43.0.tgz",
|
||||
"integrity": "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"@typescript-eslint/typescript-estree": "8.42.0",
|
||||
"@typescript-eslint/utils": "8.42.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.43.0",
|
||||
"@typescript-eslint/parser": "8.43.0",
|
||||
"@typescript-eslint/typescript-estree": "8.43.0",
|
||||
"@typescript-eslint/utils": "8.43.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@melloware/coloris": "^0.24.0",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"buffer": "^6.0.3",
|
||||
"colorthief": "^2.2.0",
|
||||
"colorthief": "^2.6.0",
|
||||
"music-metadata-browser": "^2.5.10",
|
||||
"musickit-typescript": "^1.2.4",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -27,9 +27,10 @@
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/node": "^20.11.10",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
@@ -37,7 +38,7 @@
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"sass-embedded": "^1.92.0",
|
||||
"typescript": "~5.3.0",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"vite": "^7.1.4",
|
||||
"vue-tsc": "^2.0.29"
|
||||
}
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
<!--
|
||||
* libreevent - App.vue
|
||||
*
|
||||
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button @click="changeTheme();" id="themeSelector" title="Toggle between light and dark mode"><span class="material-symbols-outlined" v-html="theme"></span></button>
|
||||
<router-view v-slot="{ Component, route }" id="main-view">
|
||||
<button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="material-symbols-outlined" v-html="theme"></span>
|
||||
</button>
|
||||
<router-view id="main-view" v-slot="{ Component, route }">
|
||||
<transition :name="route.meta.transition ? String( route.meta.transition ) : 'fade'" mode="out-in">
|
||||
<component :is="Component"></component>
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
import {
|
||||
RouterView
|
||||
} from 'vue-router';
|
||||
|
||||
const theme = ref( 'light_mode' );
|
||||
|
||||
const changeTheme = () => {
|
||||
if ( theme.value === 'dark_mode' ) {
|
||||
document.documentElement.classList.remove( 'dark' );
|
||||
document.documentElement.classList.add( 'light' );
|
||||
localStorage.setItem( 'theme', 'light_mode' );
|
||||
theme.value = 'light_mode';
|
||||
} else if ( theme.value === 'light_mode' ) {
|
||||
document.documentElement.classList.remove( 'light' );
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
localStorage.setItem( 'theme', 'dark_mode' );
|
||||
theme.value = 'dark_mode';
|
||||
}
|
||||
};
|
||||
|
||||
theme.value = localStorage.getItem( 'theme' ) ?? '';
|
||||
|
||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === 'dark_mode' ) {
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
theme.value = 'dark_mode';
|
||||
} else {
|
||||
document.documentElement.classList.add( 'light' );
|
||||
theme.value = 'light_mode';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: var( --background-color );
|
||||
@@ -178,33 +207,3 @@
|
||||
background-position: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
const theme = ref( 'light_mode' );
|
||||
|
||||
const changeTheme = () => {
|
||||
if ( theme.value === 'dark_mode' ) {
|
||||
document.documentElement.classList.remove( 'dark' );
|
||||
document.documentElement.classList.add( 'light' );
|
||||
localStorage.setItem( 'theme', 'light_mode' );
|
||||
theme.value = 'light_mode';
|
||||
} else if ( theme.value === 'light_mode' ) {
|
||||
document.documentElement.classList.remove( 'light' );
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
localStorage.setItem( 'theme', 'dark_mode' );
|
||||
theme.value = 'dark_mode';
|
||||
}
|
||||
}
|
||||
|
||||
theme.value = localStorage.getItem( 'theme' ) ?? '';
|
||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === 'dark_mode' ) {
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
theme.value = 'dark_mode';
|
||||
} else {
|
||||
document.documentElement.classList.add( 'light' );
|
||||
theme.value = 'light_mode';
|
||||
}
|
||||
</script>
|
||||
@@ -1,15 +1,18 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import {
|
||||
createApp
|
||||
} from 'vue';
|
||||
import {
|
||||
createPinia
|
||||
} from 'pinia';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
const app = createApp( App );
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use( createPinia() );
|
||||
app.use( router );
|
||||
|
||||
// localStorage.setItem( 'url', 'http://localhost:8082' );
|
||||
localStorage.setItem( 'url', 'https://music-api.janishutz.com' );
|
||||
|
||||
app.mount('#app')
|
||||
app.mount( '#app' );
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ColorThief from 'colorthief';
|
||||
|
||||
const colorThief = new ColorThief();
|
||||
|
||||
const getImageData = (): Promise<number[][]> => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement );
|
||||
return new Promise( resolve => {
|
||||
const img = document.getElementById( 'current-image' ) as HTMLImageElement;
|
||||
|
||||
if ( img.complete ) {
|
||||
resolve( colorThief.getPalette( img ) );
|
||||
} else {
|
||||
@@ -12,32 +14,39 @@ const getImageData = (): Promise<number[][]> => {
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const createBackground = () => {
|
||||
return new Promise( ( resolve ) => {
|
||||
return new Promise( resolve => {
|
||||
getImageData().then( palette => {
|
||||
const colourDetails: number[][] = [];
|
||||
const colours: string[] = [];
|
||||
|
||||
let differentEnough = true;
|
||||
|
||||
if ( palette[ 0 ] ) {
|
||||
for ( const i in palette ) {
|
||||
for ( const colour in colourDetails ) {
|
||||
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
||||
|
||||
if ( colourDiff > 15 ) {
|
||||
differentEnough = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( differentEnough ) {
|
||||
colourDetails.push( palette[ i ] );
|
||||
colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' );
|
||||
}
|
||||
|
||||
differentEnough = false;
|
||||
}
|
||||
}
|
||||
|
||||
let outColours = 'conic-gradient(';
|
||||
|
||||
if ( colours.length < 3 ) {
|
||||
for ( let i = 0; i < 3; i++ ) {
|
||||
if ( colours[ i ] ) {
|
||||
@@ -61,45 +70,56 @@ const createBackground = () => {
|
||||
outColours += colours[ i ] + ',';
|
||||
}
|
||||
}
|
||||
|
||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||
resolve( outColours );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
let callbackFun = () => {};
|
||||
|
||||
let callbackFun = () => {}
|
||||
const subscribeToBeatUpdate = ( cb: () => void ) => {
|
||||
callbackFun = cb;
|
||||
micAudioHandler();
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribeFromBeatUpdate = () => {
|
||||
callbackFun = () => {}
|
||||
callbackFun = () => {};
|
||||
|
||||
try {
|
||||
clearInterval( micAnalyzer );
|
||||
} catch ( e ) { /* empty */ }
|
||||
}
|
||||
};
|
||||
|
||||
const coolDown = () => {
|
||||
beatDetected = false;
|
||||
}
|
||||
};
|
||||
|
||||
let micAnalyzer = 0;
|
||||
let beatDetected = false;
|
||||
|
||||
const micAudioHandler = () => {
|
||||
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
|
||||
analyser.fftSize = 256;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array( bufferLength );
|
||||
|
||||
beatDetected = false;
|
||||
|
||||
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
||||
navigator.mediaDevices.getUserMedia( {
|
||||
'audio': true
|
||||
} ).then( stream => {
|
||||
const mic = audioContext.createMediaStreamSource( stream );
|
||||
|
||||
mic.connect( analyser );
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
let prevSpectrum: number[] = [];
|
||||
|
||||
const threshold = 10; // Adjust as needed
|
||||
|
||||
micAnalyzer = setInterval( () => {
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
// Convert the frequency data to a numeric array
|
||||
@@ -115,25 +135,27 @@ const micAudioHandler = () => {
|
||||
callbackFun();
|
||||
}
|
||||
}
|
||||
|
||||
prevSpectrum = currentSpectrum;
|
||||
}, 60 / 180 * 250 );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => {
|
||||
let flux = 0;
|
||||
|
||||
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
||||
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
||||
|
||||
flux += Math.max( 0, diff );
|
||||
}
|
||||
|
||||
return flux;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createBackground,
|
||||
subscribeToBeatUpdate,
|
||||
unsubscribeFromBeatUpdate,
|
||||
coolDown,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
/*
|
||||
* 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";
|
||||
import type { SSEMap } from "./song";
|
||||
import {
|
||||
io, type Socket
|
||||
} from 'socket.io-client';
|
||||
import type {
|
||||
SSEMap
|
||||
} from './song';
|
||||
|
||||
class SocketConnection {
|
||||
|
||||
socket: Socket;
|
||||
|
||||
roomName: string;
|
||||
|
||||
isConnected: boolean;
|
||||
|
||||
useSocket: boolean;
|
||||
|
||||
eventSource?: EventSource;
|
||||
|
||||
toBeListenedForItems: SSEMap;
|
||||
|
||||
reconnectRetryCount: number;
|
||||
|
||||
openConnectionsCount: number;
|
||||
|
||||
constructor () {
|
||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||
autoConnect: false,
|
||||
'autoConnect': false,
|
||||
} );
|
||||
this.roomName = location.pathname.split( '/' )[ 2 ];
|
||||
this.isConnected = false;
|
||||
@@ -38,12 +41,17 @@ class SocketConnection {
|
||||
* Create a room token and connect to
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
connect (): Promise<any> {
|
||||
connect (): Promise<unknown> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
if ( this.reconnectRetryCount < 5 ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.connect();
|
||||
this.socket.emit( 'join-room', this.roomName, ( res: { status: boolean, msg: string, data: any } ) => {
|
||||
this.socket.emit(
|
||||
'join-room', this.roomName, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string,
|
||||
'data': unknown
|
||||
} ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve( res.data );
|
||||
@@ -51,35 +59,46 @@ class SocketConnection {
|
||||
console.debug( res.msg );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
} );
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||
this.openConnectionsCount += 1;
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
||||
this.eventSource
|
||||
= new EventSource( localStorage.getItem( 'url' )
|
||||
+ '/socket/connection?room=' + this.roomName, {
|
||||
'withCredentials': true
|
||||
} );
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true;
|
||||
this.reconnectRetryCount = 0;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||
}
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString() + ': Connection successfully established!' );
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = ( e ) => {
|
||||
this.eventSource.onmessage = e => {
|
||||
const d = JSON.parse( e.data );
|
||||
|
||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||
this.toBeListenedForItems[ d.type ]( d.data );
|
||||
} else if ( d.type === 'basics' ) {
|
||||
resolve( d.data );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = () => {
|
||||
if ( this.isConnected ) {
|
||||
this.isConnected = false;
|
||||
this.openConnectionsCount -= 1;
|
||||
this.eventSource?.close();
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Reconnecting due to connection error!' );
|
||||
// console.debug( e );
|
||||
|
||||
this.eventSource = undefined;
|
||||
@@ -91,11 +110,16 @@ class SocketConnection {
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error ' + res.status );
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Could not connect due to error ' + res.status );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
} ).catch( () => {
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error.' );
|
||||
} )
|
||||
.catch( () => {
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Could not connect due to error.' );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} );
|
||||
} else {
|
||||
@@ -116,16 +140,23 @@ class SocketConnection {
|
||||
* @param {any} data
|
||||
* @returns {void}
|
||||
*/
|
||||
emit ( event: string, data: any ): void {
|
||||
emit ( event: string, data: unknown ): void {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( event, { 'roomName': this.roomName, 'data': data } );
|
||||
this.socket.emit( event, {
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} );
|
||||
} else {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'data': data } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'event': event,
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -140,7 +171,7 @@ class SocketConnection {
|
||||
* @param {( data: any ) => void} cb The callback function / listener function
|
||||
* @returns {void}
|
||||
*/
|
||||
registerListener ( event: string, cb: ( data: any ) => void ): void {
|
||||
registerListener ( event: string, cb: ( data: unknown ) => void ): void {
|
||||
if ( this.useSocket ) {
|
||||
if ( this.isConnected ) {
|
||||
this.socket.on( event, cb );
|
||||
@@ -171,9 +202,11 @@ class SocketConnection {
|
||||
if ( this.eventSource ) {
|
||||
return this.eventSource!.OPEN && this.isConnected;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SocketConnection;
|
||||
@@ -1,25 +1,39 @@
|
||||
import type { SearchResult, Song, SongMove } from "./song";
|
||||
import type {
|
||||
SearchResult, Song, SongMove
|
||||
} from './song';
|
||||
|
||||
interface Config {
|
||||
devToken: string;
|
||||
userToken: string;
|
||||
'devToken': string;
|
||||
'userToken': string;
|
||||
}
|
||||
|
||||
type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10';
|
||||
type RepeatMode = 'off' | 'once' | 'all';
|
||||
|
||||
class MusicKitJSWrapper {
|
||||
|
||||
playingSongID: number;
|
||||
|
||||
playlist: Song[];
|
||||
|
||||
queue: number[];
|
||||
|
||||
config: Config;
|
||||
|
||||
musicKit: any;
|
||||
|
||||
isLoggedIn: boolean;
|
||||
|
||||
isPreparedToPlay: boolean;
|
||||
|
||||
repeatMode: RepeatMode;
|
||||
|
||||
isShuffleEnabled: boolean;
|
||||
|
||||
hasEncounteredAuthError: boolean;
|
||||
|
||||
queuePos: number;
|
||||
|
||||
audioPlayer: HTMLAudioElement;
|
||||
|
||||
constructor () {
|
||||
@@ -27,8 +41,8 @@ class MusicKitJSWrapper {
|
||||
this.playlist = [];
|
||||
this.queue = [];
|
||||
this.config = {
|
||||
devToken: '',
|
||||
userToken: '',
|
||||
'devToken': '',
|
||||
'userToken': '',
|
||||
};
|
||||
this.isShuffleEnabled = false;
|
||||
this.repeatMode = 'off';
|
||||
@@ -58,14 +72,16 @@ class MusicKitJSWrapper {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
} )
|
||||
.catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} else {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
} )
|
||||
.catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
}
|
||||
@@ -76,25 +92,29 @@ class MusicKitJSWrapper {
|
||||
* @returns {void}
|
||||
*/
|
||||
init (): void {
|
||||
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => {
|
||||
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,
|
||||
app: {
|
||||
name: 'MusicPlayer',
|
||||
build: '3'
|
||||
'developerToken': token,
|
||||
'app': {
|
||||
'name': 'MusicPlayer',
|
||||
'build': '3'
|
||||
},
|
||||
storefrontId: 'CH',
|
||||
'storefrontId': 'CH',
|
||||
} ).then( () => {
|
||||
this.config.devToken = token;
|
||||
this.musicKit = MusicKit.getInstance();
|
||||
|
||||
if ( this.musicKit.isAuthorized ) {
|
||||
this.isLoggedIn = true;
|
||||
this.config.userToken = this.musicKit.musicUserToken;
|
||||
}
|
||||
|
||||
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off;
|
||||
} );
|
||||
} );
|
||||
@@ -107,7 +127,10 @@ class MusicKitJSWrapper {
|
||||
* @returns {boolean[]} Returns an array, where the first element indicates login status, the second one, if an error was encountered
|
||||
*/
|
||||
getAuth (): boolean[] {
|
||||
return [ this.isLoggedIn, this.hasEncounteredAuthError ];
|
||||
return [
|
||||
this.isLoggedIn,
|
||||
this.hasEncounteredAuthError
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,8 +142,8 @@ class MusicKitJSWrapper {
|
||||
apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
|
||||
if ( this.config.devToken != '' && this.config.userToken != '' ) {
|
||||
fetch( url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'method': 'GET',
|
||||
'headers': {
|
||||
'Authorization': `Bearer ${ this.config.devToken }`,
|
||||
'Music-User-Token': this.config.userToken
|
||||
}
|
||||
@@ -128,12 +151,18 @@ class MusicKitJSWrapper {
|
||||
if ( res.status === 200 ) {
|
||||
res.json().then( json => {
|
||||
try {
|
||||
callback( { 'status': 'ok', 'data': json } );
|
||||
callback( {
|
||||
'status': 'ok',
|
||||
'data': json
|
||||
} );
|
||||
} catch ( err ) { /* empty */ }
|
||||
} );
|
||||
} else {
|
||||
try {
|
||||
callback( { 'status': 'error', 'error': res.status } );
|
||||
callback( {
|
||||
'status': 'error',
|
||||
'error': res.status
|
||||
} );
|
||||
} catch ( err ) { /* empty */ }
|
||||
}
|
||||
} );
|
||||
@@ -152,31 +181,38 @@ class MusicKitJSWrapper {
|
||||
|
||||
setPlaylistByID ( id: string ): Promise<void> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
this.musicKit.setQueue( { playlist: id } ).then( () => {
|
||||
this.musicKit.setQueue( {
|
||||
'playlist': id
|
||||
} ).then( () => {
|
||||
const pl = this.musicKit.queue.items;
|
||||
const songs: Song[] = [];
|
||||
|
||||
for ( const item in pl ) {
|
||||
let url = pl[ item ].attributes.artwork.url;
|
||||
|
||||
url = url.replace( '{w}', pl[ item ].attributes.artwork.width );
|
||||
url = url.replace( '{h}', pl[ item ].attributes.artwork.height );
|
||||
const song: Song = {
|
||||
artist: pl[ item ].attributes.artistName,
|
||||
cover: url,
|
||||
duration: pl[ item ].attributes.durationInMillis / 1000,
|
||||
id: pl[ item ].id,
|
||||
origin: 'apple-music',
|
||||
title: pl[ item ].attributes.name,
|
||||
genres: pl[ item ].attributes.genreNames
|
||||
}
|
||||
'artist': pl[ item ].attributes.artistName,
|
||||
'cover': url,
|
||||
'duration': pl[ item ].attributes.durationInMillis / 1000,
|
||||
'id': pl[ item ].id,
|
||||
'origin': 'apple-music',
|
||||
'title': pl[ item ].attributes.name,
|
||||
'genres': pl[ item ].attributes.genreNames
|
||||
};
|
||||
|
||||
songs.push( song );
|
||||
}
|
||||
|
||||
this.playlist = songs;
|
||||
this.setShuffle( this.isShuffleEnabled );
|
||||
this.queuePos = 0;
|
||||
this.playingSongID = this.queue[ 0 ];
|
||||
this.prepare( this.playingSongID );
|
||||
resolve();
|
||||
} ).catch( err => {
|
||||
} )
|
||||
.catch( err => {
|
||||
console.error( err );
|
||||
reject( err );
|
||||
} );
|
||||
@@ -192,18 +228,23 @@ class MusicKitJSWrapper {
|
||||
if ( this.playlist.length > 0 ) {
|
||||
this.playingSongID = playlistID;
|
||||
this.isPreparedToPlay = true;
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] === playlistID ) {
|
||||
this.queuePos = parseInt( el );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.setQueue( { 'song': this.playlist[ this.playingSongID ].id } ).then( () => {
|
||||
this.musicKit.setQueue( {
|
||||
'song': this.playlist[ this.playingSongID ].id
|
||||
} ).then( () => {
|
||||
setTimeout( () => {
|
||||
this.control( 'play' );
|
||||
}, 500 );
|
||||
} ).catch( ( err ) => {
|
||||
} )
|
||||
.catch( err => {
|
||||
console.log( err );
|
||||
} );
|
||||
} else {
|
||||
@@ -213,6 +254,7 @@ class MusicKitJSWrapper {
|
||||
this.control( 'play' );
|
||||
}, 500 );
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -226,50 +268,63 @@ class MusicKitJSWrapper {
|
||||
*/
|
||||
control ( action: ControlAction ): boolean {
|
||||
switch ( action ) {
|
||||
case "play":
|
||||
case 'play':
|
||||
if ( this.isPreparedToPlay ) {
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.play();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.play();
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case "pause":
|
||||
|
||||
case 'pause':
|
||||
if ( this.isPreparedToPlay ) {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.pause();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.pause();
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case "back-10":
|
||||
|
||||
case 'back-10':
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
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;
|
||||
}
|
||||
case "skip-10":
|
||||
|
||||
case 'skip-10':
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
|
||||
|
||||
return false;
|
||||
} else {
|
||||
if ( this.repeatMode !== 'once' ) {
|
||||
this.control( 'next' );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.musicKit.seekToTime( 0 );
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -283,32 +338,42 @@ class MusicKitJSWrapper {
|
||||
this.audioPlayer.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
case "next":
|
||||
|
||||
case 'next':
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.queuePos < this.queue.length - 1 ) {
|
||||
this.queuePos += 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.queuePos = 0;
|
||||
|
||||
if ( this.repeatMode !== 'all' ) {
|
||||
this.control( 'pause' );
|
||||
} else {
|
||||
this.playingSongID = this.queue[ this.queuePos ];
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case "previous":
|
||||
|
||||
case 'previous':
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.queuePos > 0 ) {
|
||||
this.queuePos -= 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.queuePos = this.queue.length - 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -317,15 +382,22 @@ class MusicKitJSWrapper {
|
||||
setShuffle ( enabled: boolean ) {
|
||||
this.isShuffleEnabled = enabled;
|
||||
this.queue = [];
|
||||
|
||||
if ( enabled ) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const d = [];
|
||||
|
||||
for ( const el in this.playlist ) {
|
||||
d.push( parseInt( el ) );
|
||||
}
|
||||
this.queue = d.map( value => ( { value, sort: Math.random() } ) )
|
||||
|
||||
this.queue = d.map( value => ( {
|
||||
value,
|
||||
'sort': Math.random()
|
||||
} ) )
|
||||
.sort( ( a, b ) => a.sort - b.sort )
|
||||
.map( ( { value } ) => value );
|
||||
.map( ( {
|
||||
value
|
||||
} ) => value );
|
||||
this.queue.splice( this.queue.indexOf( this.playingSongID ), 1 );
|
||||
this.queue.push( this.playingSongID );
|
||||
this.queue.reverse();
|
||||
@@ -334,6 +406,7 @@ 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 ) {
|
||||
@@ -359,29 +432,37 @@ class MusicKitJSWrapper {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -435,9 +516,11 @@ class MusicKitJSWrapper {
|
||||
*/
|
||||
getQueue (): Song[] {
|
||||
const data = [];
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
data.push( this.playlist[ this.queue[ el ] ] );
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -449,6 +532,7 @@ class MusicKitJSWrapper {
|
||||
getUserPlaylists ( cb: ( data: object ) => void ): boolean {
|
||||
if ( this.isLoggedIn ) {
|
||||
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -467,17 +551,20 @@ class MusicKitJSWrapper {
|
||||
// TODO: Make storefront adjustable
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const queryParameters = {
|
||||
term: ( searchTerm ),
|
||||
types: [ 'songs' ],
|
||||
'term': searchTerm,
|
||||
'types': [ 'songs' ],
|
||||
};
|
||||
this.musicKit.api.music( `v1/catalog/ch/search`, queryParameters ).then( results => {
|
||||
|
||||
this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters ).then( results => {
|
||||
resolve( results );
|
||||
} ).catch( e => {
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
reject( e );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MusicKitJSWrapper;
|
||||
@@ -1,34 +1,41 @@
|
||||
/*
|
||||
* 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"
|
||||
import type { SSEMap } from "./song";
|
||||
import {
|
||||
io, type Socket
|
||||
} from 'socket.io-client';
|
||||
import type {
|
||||
SSEMap
|
||||
} from './song';
|
||||
|
||||
class NotificationHandler {
|
||||
|
||||
socket: Socket;
|
||||
|
||||
roomName: string;
|
||||
|
||||
roomToken: string;
|
||||
|
||||
isConnected: boolean;
|
||||
|
||||
useSocket: boolean;
|
||||
|
||||
eventSource?: EventSource;
|
||||
|
||||
toBeListenedForItems: SSEMap;
|
||||
|
||||
reconnectRetryCount: number;
|
||||
|
||||
lastEmitTimestamp: number;
|
||||
|
||||
openConnectionsCount: number;
|
||||
|
||||
pendingRequestCount: number;
|
||||
|
||||
connectionWasSuccessful: boolean;
|
||||
|
||||
constructor () {
|
||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||
autoConnect: false,
|
||||
'autoConnect': false,
|
||||
} );
|
||||
this.roomName = '';
|
||||
this.roomToken = '';
|
||||
@@ -50,28 +57,37 @@ class NotificationHandler {
|
||||
*/
|
||||
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( text => {
|
||||
this.roomToken = text;
|
||||
this.roomName = roomName;
|
||||
|
||||
if ( this.useSocket ) {
|
||||
this.socket.connect();
|
||||
this.socket.emit( 'create-room', {
|
||||
name: this.roomName,
|
||||
token: this.roomToken
|
||||
}, ( res: { status: boolean, msg: string } ) => {
|
||||
this.socket.emit(
|
||||
'create-room', {
|
||||
'name': this.roomName,
|
||||
'token': this.roomToken
|
||||
}, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string
|
||||
} ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
} );
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.sseConnect().then( () => {
|
||||
resolve();
|
||||
} ).catch( );
|
||||
} )
|
||||
.catch( );
|
||||
}
|
||||
} );
|
||||
} else if ( res.status === 409 ) {
|
||||
@@ -90,9 +106,13 @@ class NotificationHandler {
|
||||
if ( this.reconnectRetryCount < 5 ) {
|
||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||
this.openConnectionsCount += 1;
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
|
||||
'withCredentials': true
|
||||
} );
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true;
|
||||
@@ -100,16 +120,17 @@ class NotificationHandler {
|
||||
this.reconnectRetryCount = 0;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = ( e ) => {
|
||||
this.eventSource.onmessage = e => {
|
||||
const d = JSON.parse( e.data );
|
||||
|
||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||
this.toBeListenedForItems[ d.type ]( d.data );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = ( e ) => {
|
||||
this.eventSource.onerror = e => {
|
||||
if ( this.isConnected ) {
|
||||
this.isConnected = false;
|
||||
this.eventSource?.close();
|
||||
@@ -131,7 +152,8 @@ class NotificationHandler {
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
|
||||
}
|
||||
} ).catch( () => {
|
||||
} )
|
||||
.catch( () => {
|
||||
if ( !this.connectionWasSuccessful ) {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} else {
|
||||
@@ -169,9 +191,14 @@ class NotificationHandler {
|
||||
emit ( event: string, data: any ): void {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( event, { 'roomToken': this.roomToken, 'roomName': this.roomName, 'data': data } );
|
||||
this.socket.emit( event, {
|
||||
'roomToken': this.roomToken,
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} );
|
||||
} else {
|
||||
const now = new Date().getTime();
|
||||
|
||||
if ( this.lastEmitTimestamp < now - 250 ) {
|
||||
this.lastEmitTimestamp = now;
|
||||
this.sendEmitConventionally( event, data );
|
||||
@@ -189,10 +216,15 @@ class NotificationHandler {
|
||||
|
||||
sendEmitConventionally ( event: string, data: any ): void {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'roomToken': this.roomToken, 'data': data } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'event': event,
|
||||
'roomName': this.roomName,
|
||||
'roomToken': this.roomToken,
|
||||
'data': data
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -222,22 +254,32 @@ class NotificationHandler {
|
||||
async disconnect (): Promise<void> {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( 'delete-room', {
|
||||
name: this.roomName,
|
||||
token: this.roomToken
|
||||
}, ( res: { status: boolean, msg: string } ) => {
|
||||
this.socket.emit(
|
||||
'delete-room', {
|
||||
'name': this.roomName,
|
||||
'token': this.roomToken
|
||||
}, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string
|
||||
} ) => {
|
||||
this.socket.disconnect();
|
||||
|
||||
if ( !res.status ) {
|
||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||
}
|
||||
|
||||
return;
|
||||
} );
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'roomName': this.roomName, 'roomToken': this.roomToken } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'roomName': this.roomName,
|
||||
'roomToken': this.roomToken
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -247,8 +289,10 @@ class NotificationHandler {
|
||||
} else {
|
||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||
}
|
||||
|
||||
return;
|
||||
} ).catch( () => {
|
||||
} )
|
||||
.catch( () => {
|
||||
return;
|
||||
} );
|
||||
}
|
||||
@@ -258,6 +302,7 @@ class NotificationHandler {
|
||||
getRoomName (): string {
|
||||
return this.roomName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default NotificationHandler;
|
||||
|
||||
70
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
70
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
@@ -4,90 +4,90 @@ export interface Song {
|
||||
/**
|
||||
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_
|
||||
*/
|
||||
id: string;
|
||||
'id': string;
|
||||
|
||||
/**
|
||||
* Origin of the song
|
||||
*/
|
||||
origin: Origin;
|
||||
'origin': Origin;
|
||||
|
||||
/**
|
||||
* The cover image as a URL
|
||||
*/
|
||||
cover: string;
|
||||
'cover': string;
|
||||
|
||||
/**
|
||||
* The artist of the song
|
||||
*/
|
||||
artist: string;
|
||||
'artist': string;
|
||||
|
||||
/**
|
||||
* The name of the song
|
||||
*/
|
||||
title: string;
|
||||
'title': string;
|
||||
|
||||
/**
|
||||
* Duration of the song in milliseconds
|
||||
*/
|
||||
duration: number;
|
||||
'duration': number;
|
||||
|
||||
/**
|
||||
* (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there
|
||||
*/
|
||||
genres?: string[];
|
||||
'genres'?: string[];
|
||||
|
||||
/**
|
||||
* (OPTIONAL) This will be displayed in brackets on the showcase screens
|
||||
*/
|
||||
additionalInfo?: string;
|
||||
'additionalInfo'?: string;
|
||||
}
|
||||
|
||||
export interface SongTransmitted {
|
||||
title: string;
|
||||
artist: string;
|
||||
duration: number;
|
||||
cover: string;
|
||||
additionalInfo?: string;
|
||||
'title': string;
|
||||
'artist': string;
|
||||
'duration': number;
|
||||
'cover': string;
|
||||
'additionalInfo'?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ReadFile {
|
||||
url: string;
|
||||
filename: string;
|
||||
'url': string;
|
||||
'filename': string;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
data: {
|
||||
results: {
|
||||
songs: {
|
||||
data: AppleMusicSongData[],
|
||||
href: string;
|
||||
'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
|
||||
'id': string,
|
||||
'type': string;
|
||||
'href': string;
|
||||
'attributes': {
|
||||
'albumName': string;
|
||||
'artistName': string;
|
||||
'artwork': {
|
||||
'width': number,
|
||||
'height': number,
|
||||
'url': string
|
||||
},
|
||||
name: string;
|
||||
genreNames: string[];
|
||||
durationInMillis: number;
|
||||
'name': string;
|
||||
'genreNames': string[];
|
||||
'durationInMillis': number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SongMove {
|
||||
songID: string;
|
||||
newPos: number;
|
||||
'songID': string;
|
||||
'newPos': number;
|
||||
}
|
||||
|
||||
export interface SSEMap {
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
/*
|
||||
* LanguageSchoolHossegorBookingSystem - userStore.js
|
||||
*
|
||||
* Created by Janis Hutz 10/27/2023, Licensed under a proprietary License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
defineStore
|
||||
} from 'pinia';
|
||||
|
||||
|
||||
// FOSS-VERSION: To enable the UI to be used with the FOSS version, change "isUserAuth" to true, you will be "logged in"
|
||||
export const useUserStore = defineStore( 'user', {
|
||||
state: () => ( { 'isUserAuth': true, 'hasSubscribed': false, 'isUsingKeyboard': false, 'username': '', 'isFOSSVersion': false } ),
|
||||
getters: {
|
||||
getUserAuthenticated: ( state ) => state.isUserAuth,
|
||||
getSubscriptionStatus: ( state ) => state.hasSubscribed,
|
||||
'state': () => ( {
|
||||
'isUserAuth': true,
|
||||
'hasSubscribed': false,
|
||||
'isUsingKeyboard': false,
|
||||
'username': '',
|
||||
'isFOSSVersion': false
|
||||
} ),
|
||||
'getters': {
|
||||
'getUserAuthenticated': state => state.isUserAuth,
|
||||
'getSubscriptionStatus': state => state.hasSubscribed,
|
||||
},
|
||||
actions: {
|
||||
'actions': {
|
||||
setUserAuth ( auth: boolean ) {
|
||||
this.isUserAuth = auth;
|
||||
},
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
<template>
|
||||
<div class="app-view">
|
||||
<button id="logout" @click="logout()"><span class="material-symbols-outlined">logout</span></button>
|
||||
<div class="loading-view" v-if="!hasFinishedLoading">
|
||||
<button id="logout" @click="logout()">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
</button>
|
||||
<div v-if="!hasFinishedLoading" class="loading-view">
|
||||
<h1>Loading...</h1>
|
||||
</div>
|
||||
<div class="home-view" v-else-if="hasFinishedLoading && isReady">
|
||||
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"
|
||||
:is-logged-in="isLoggedIntoAppleMusic" @custom-playlist="( pl ) => selectCustomPlaylist( pl )"></libraryView>
|
||||
<div v-else-if="hasFinishedLoading && isReady" class="home-view">
|
||||
<libraryView
|
||||
class="library-view"
|
||||
:playlists="playlists"
|
||||
:is-logged-in="isLoggedIntoAppleMusic"
|
||||
@selected-playlist="( id ) => { selectPlaylist( id ) }"
|
||||
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
|
||||
/>
|
||||
</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>
|
||||
<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' + ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||
ref="player"></playerView>
|
||||
<playerView
|
||||
ref="player"
|
||||
:class="'player-view'
|
||||
+ ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )"
|
||||
@player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||
/>
|
||||
<!-- TODO: Call to backend to check if user has access -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,10 +42,16 @@
|
||||
<script setup lang="ts">
|
||||
import playerView from '@/components/playerView.vue';
|
||||
import libraryView from '@/components/libraryView.vue';
|
||||
import { ref } from 'vue';
|
||||
import type { ReadFile } from '@/scripts/song';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
import type {
|
||||
ReadFile
|
||||
} from '@/scripts/song';
|
||||
import router from '@/router';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/stores/userStore';
|
||||
|
||||
const isLoggedIntoAppleMusic = ref( false );
|
||||
const isReady = ref( false );
|
||||
@@ -41,7 +67,7 @@
|
||||
} else {
|
||||
isShowingFullScreenPlayer.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let loginChecker = 0;
|
||||
|
||||
@@ -51,7 +77,7 @@
|
||||
if ( player.value.getAuth()[ 0 ] ) {
|
||||
isLoggedIntoAppleMusic.value = true;
|
||||
isReady.value = true;
|
||||
player.value.getPlaylists( ( data ) => {
|
||||
player.value.getPlaylists( data => {
|
||||
playlists.value = data.data.data;
|
||||
} );
|
||||
clearInterval( loginChecker );
|
||||
@@ -60,25 +86,27 @@
|
||||
alert( 'An error occurred when logging you in. Please try again!' );
|
||||
}
|
||||
}, 500 );
|
||||
}
|
||||
};
|
||||
|
||||
const skipLogin = () => {
|
||||
isReady.value = true;
|
||||
isLoggedIntoAppleMusic.value = false;
|
||||
player.value.skipLogin();
|
||||
}
|
||||
};
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
player.value.selectPlaylist( id );
|
||||
player.value.controlUI( 'show' );
|
||||
}
|
||||
};
|
||||
|
||||
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||
player.value.selectCustomPlaylist( playlist );
|
||||
player.value.controlUI( 'show' );
|
||||
}
|
||||
};
|
||||
|
||||
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( text => {
|
||||
if ( text === 'ok' ) {
|
||||
@@ -102,7 +130,7 @@
|
||||
const logout = () => {
|
||||
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,13 +3,35 @@
|
||||
<div class="top-view">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||
<h1>MusicPlayer</h1>
|
||||
<p v-if="reasonForRedirectHere" style="color: red;">{{ reasons[ reasonForRedirectHere ] }}</p>
|
||||
<p v-if="!reasonForRedirectHere"><i>An Open Source, browser-based MusicPlayer with beautiful graphics</i></p>
|
||||
<p v-if="reasonForRedirectHere" style="color: red;">
|
||||
{{ reasons[ reasonForRedirectHere ] }}
|
||||
</p>
|
||||
<p v-if="!reasonForRedirectHere">
|
||||
<i>An Open Source, browser-based MusicPlayer with beautiful graphics</i>
|
||||
</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<a href="https://store.janishutz.com/product/com.janishutz.MusicPlayer" class="fancy-button" target="_blank">Subscribe</a>
|
||||
<a href="/" class="fancy-button" style="margin-left: 10px;" v-if="!reasonForRedirectHere">Log in</a>
|
||||
<button href="/" class="fancy-button" style="margin-left: 10px;" v-if="reasonForRedirectHere" @click="logout()">Log out</button>
|
||||
<a href="https://github.com/simplePCBuilding/MusicPlayerV2" class="fancy-button" style="margin-left: 10px;" target="_blank">GitHub</a>
|
||||
<a
|
||||
v-if="!reasonForRedirectHere"
|
||||
href="/"
|
||||
class="fancy-button"
|
||||
style="margin-left: 10px;"
|
||||
>Log in</a>
|
||||
<button
|
||||
v-if="reasonForRedirectHere"
|
||||
href="/"
|
||||
class="fancy-button"
|
||||
style="margin-left: 10px;"
|
||||
@click="logout()"
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/simplePCBuilding/MusicPlayerV2"
|
||||
class="fancy-button"
|
||||
style="margin-left: 10px;"
|
||||
target="_blank"
|
||||
>GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -20,7 +42,10 @@
|
||||
<p>Use MusicPlayer in conjunction with Apple Music</p>
|
||||
|
||||
<h2>Share your playlist</h2>
|
||||
<p>You can share your playlist on a beautifully animated public page, so that other people can join in and view your playlist</p>
|
||||
<p>
|
||||
You can share your playlist on a beautifully animated public page,
|
||||
so that other people can join in and view your playlist
|
||||
</p>
|
||||
|
||||
<h2>Fully browser based</h2>
|
||||
<p>No installation required when using MusicPlayer on <a href="https://music.janishutz.com">music.janishutz.com</a></p>
|
||||
@@ -29,7 +54,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type Ref } from 'vue';
|
||||
import {
|
||||
ref, type Ref
|
||||
} from 'vue';
|
||||
|
||||
interface Reasons {
|
||||
[key: string]: string;
|
||||
@@ -39,12 +66,13 @@
|
||||
'notOwned': 'Please subscribe to use MusicPlayer here, or download and install it manually from GitHub!',
|
||||
} );
|
||||
const reasonForRedirectHere = ref( sessionStorage.getItem( 'getRedirectionReason' ) );
|
||||
|
||||
sessionStorage.removeItem( 'getRedirectionReason' );
|
||||
|
||||
const logout = () => {
|
||||
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,54 +1,60 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||
<button :class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )" @click="login()"
|
||||
style="margin-top: 5vh;" title="Sign in or sign up with janishutz.com ID" v-if="status"
|
||||
>{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}</button>
|
||||
<p v-else>We are sorry, but we were unable to initialize the login services. Please reload the page if you wish to retry!</p>
|
||||
<p style="width: 80%;">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>
|
||||
<router-link to="/get" class="fancy-button">More information</router-link>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
||||
<button
|
||||
:class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )"
|
||||
style="margin-top: 5vh;"
|
||||
title="Sign in or sign up with janishutz.com ID"
|
||||
@click="login()"
|
||||
>
|
||||
{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}
|
||||
</button>
|
||||
<p style="width: 80%;">
|
||||
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>
|
||||
<router-link to="/get" class="fancy-button">
|
||||
More information
|
||||
</router-link>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO: Make possible to install and use without account, if using FOSS version
|
||||
import router from '@/router';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import {
|
||||
RouterLink
|
||||
} from 'vue-router';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/stores/userStore';
|
||||
import notificationsModule from '@/components/notificationsModule.vue';
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
|
||||
const notifications = ref( notificationsModule );
|
||||
const isTryingToSignIn = ref( true );
|
||||
|
||||
interface JanishutzIDSDK {
|
||||
setLoginSDKURL: ( url: string ) => undefined;
|
||||
createSession: () => undefined;
|
||||
verifySession: () => Promise<JHIDSessionStatus>
|
||||
'setLoginSDKURL': ( url: string ) => undefined;
|
||||
'createSession': () => undefined;
|
||||
'verifySession': () => Promise<JHIDSessionStatus>
|
||||
}
|
||||
|
||||
interface JHIDSessionStatus {
|
||||
status: boolean;
|
||||
username: string;
|
||||
'status': boolean;
|
||||
'username': string;
|
||||
}
|
||||
|
||||
let sdk: JanishutzIDSDK;
|
||||
const status = ref( true );
|
||||
|
||||
if ( typeof( JanishutzID ) !== 'undefined' ) {
|
||||
sdk = JanishutzID();
|
||||
sdk.setLoginSDKURL( localStorage.getItem( 'url' ) ?? '' );
|
||||
} else {
|
||||
setTimeout( () => {
|
||||
notifications.value.createNotification( 'Unable to initialize account services!', 5, 'error' );
|
||||
}, 1000 );
|
||||
status.value = false;
|
||||
}
|
||||
|
||||
const login = () => {
|
||||
sdk.createSession();
|
||||
}
|
||||
};
|
||||
|
||||
const store = useUserStore();
|
||||
|
||||
@@ -56,15 +62,17 @@
|
||||
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||
localStorage.removeItem( 'redirect' );
|
||||
} else {
|
||||
if ( typeof( sdk ) !== 'undefined' ) {
|
||||
if ( typeof sdk !== 'undefined' ) {
|
||||
sdk.verifySession().then( res => {
|
||||
if ( res.status ) {
|
||||
store.isUserAuth = true;
|
||||
store.username = res.username;
|
||||
|
||||
if ( localStorage.getItem( 'close-tab' ) ) {
|
||||
localStorage.removeItem( 'close-tab' );
|
||||
window.close();
|
||||
}
|
||||
|
||||
localStorage.setItem( 'login-ok', 'true' );
|
||||
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||
localStorage.removeItem( 'redirect' );
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
||||
<div class="info">
|
||||
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||
</div>
|
||||
<div class="remote-view">
|
||||
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
||||
<div class="current-song-wrapper">
|
||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||
<img
|
||||
v-if="playlist[ playingSong ]"
|
||||
id="current-image"
|
||||
:src="playlist[ playingSong ].cover"
|
||||
class="fancy-view-song-art"
|
||||
crossorigin="anonymous"
|
||||
>
|
||||
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||
<div class="current-song">
|
||||
<h1 style="margin-bottom: 5px;">{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
||||
<h1 style="margin-bottom: 5px;">
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||
</h1>
|
||||
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
|
||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||
<p
|
||||
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||
class="additional-info"
|
||||
>
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||
</p>
|
||||
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" v-bind:key="song.id" class="song-list">
|
||||
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||
<div class="song-details-wrapper">
|
||||
<h3>{{ song.title }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
@@ -32,7 +47,9 @@
|
||||
<div v-else style="max-width: 80%;">
|
||||
<span class="material-symbols-outlined" style="font-size: 4rem;">wifi_off</span>
|
||||
<h1>Couldn't connect!</h1>
|
||||
<p>There does not appear to be a share with the specified name, or an error occurred when connecting.</p>
|
||||
<p>
|
||||
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||
</p>
|
||||
<p>You may <a href="">reload</a> the page to try again!</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,8 +58,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SocketConnection from '@/scripts/connection';
|
||||
import type { Song } from '@/scripts/song';
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
computed, ref, type Ref
|
||||
} from 'vue';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
const playlist: Ref<Song[]> = ref( [] );
|
||||
@@ -52,6 +73,7 @@
|
||||
const hasLoaded = ref( false );
|
||||
const showCouldNotFindRoom = ref( false );
|
||||
const playbackStart = ref( 0 );
|
||||
|
||||
let timeTracker = 0;
|
||||
|
||||
const conn = new SocketConnection();
|
||||
@@ -61,18 +83,22 @@
|
||||
isPlaying.value = d.playbackStatus;
|
||||
playingSong.value = d.playlistIndex;
|
||||
playbackStart.value = d.playbackStart;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
}
|
||||
|
||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ]
|
||||
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
hasLoaded.value = true;
|
||||
conn.registerListener( 'playlist', ( data ) => {
|
||||
conn.registerListener( 'playlist', data => {
|
||||
playlist.value = data;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback', ( data ) => {
|
||||
conn.registerListener( 'playback', data => {
|
||||
isPlaying.value = data;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
} else {
|
||||
@@ -80,22 +106,24 @@
|
||||
}
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback-start', ( data ) => {
|
||||
conn.registerListener( 'playback-start', data => {
|
||||
playbackStart.value = data;
|
||||
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playlist-index', ( data ) => {
|
||||
conn.registerListener( 'playlist-index', data => {
|
||||
playingSong.value = parseInt( data );
|
||||
} );
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
conn.registerListener( 'delete-share', ( _ ) => {
|
||||
alert( 'This share was just deleted. It is no longer available. The page will reload automatically to try and re-establish connection!' );
|
||||
conn.registerListener( 'delete-share', _ => {
|
||||
alert( `This share was just deleted. It is no longer available.
|
||||
The page will reload automatically to try and re-establish connection!` );
|
||||
conn.disconnect();
|
||||
location.reload();
|
||||
} );
|
||||
} ).catch( e => {
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
showCouldNotFindRoom.value = true;
|
||||
} );
|
||||
@@ -103,49 +131,56 @@
|
||||
const songQueue = computed( () => {
|
||||
let ret: Song[] = [];
|
||||
let pos = 0;
|
||||
|
||||
for ( let song in playlist.value ) {
|
||||
if ( pos >= playingSong.value ) {
|
||||
ret.push( playlist.value[ song ] );
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} );
|
||||
|
||||
// TODO: Handle disconnect from updater (=> have it disconnect)
|
||||
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song: string ) => {
|
||||
let timeRemaining = 0;
|
||||
|
||||
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||
if ( playlist.value[ i ].id == song ) {
|
||||
break;
|
||||
}
|
||||
|
||||
timeRemaining += playlist.value[ i ].duration;
|
||||
}
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min';
|
||||
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min after starting to play';
|
||||
}
|
||||
return 'Playing less than '
|
||||
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
};
|
||||
} );
|
||||
|
||||
const startTimeTracker = () => {
|
||||
try {
|
||||
clearInterval( timeTracker );
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
timeTracker = setInterval( () => {
|
||||
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||
|
||||
if ( isNaN( progressBar.value ) ) {
|
||||
progressBar.value = 0;
|
||||
}
|
||||
@@ -156,11 +191,11 @@
|
||||
location.reload();
|
||||
}
|
||||
}, 100 );
|
||||
}
|
||||
};
|
||||
|
||||
const stopTimeTracker = () => {
|
||||
clearInterval( timeTracker );
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener( 'visibilitychange', () => {
|
||||
if ( !document.hidden ) {
|
||||
|
||||
@@ -1,34 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="anti-tamper material-symbols-outlined" v-if="isAntiTamperEnabled" @click="secureModeInfo( 'toggle' )">lock</span>
|
||||
<div class="anti-tamper-info" v-if="isShowingSecureModeInfo && isAntiTamperEnabled" @click="secureModeInfo( 'hide' )">Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!</div>
|
||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
||||
<span
|
||||
v-if="isAntiTamperEnabled"
|
||||
class="anti-tamper material-symbols-outlined"
|
||||
@click="secureModeInfo( 'toggle' )"
|
||||
>lock</span>
|
||||
<div
|
||||
v-if="isShowingSecureModeInfo && isAntiTamperEnabled"
|
||||
class="anti-tamper-info"
|
||||
@click="secureModeInfo( 'hide' )"
|
||||
>
|
||||
Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!
|
||||
</div>
|
||||
<div class="info">
|
||||
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||
</div>
|
||||
<div class="remote-view">
|
||||
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||
<div class="current-song-wrapper">
|
||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||
<img
|
||||
v-if="playlist[ playingSong ]"
|
||||
id="current-image"
|
||||
:src="playlist[ playingSong ].cover"
|
||||
class="fancy-view-song-art"
|
||||
crossorigin="anonymous"
|
||||
>
|
||||
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||
<div class="current-song">
|
||||
<h1 style="margin-bottom: 5px;">{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
||||
<h1 style="margin-bottom: 5px;">
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||
</h1>
|
||||
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
|
||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||
<p
|
||||
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||
class="additional-info"
|
||||
>
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||
</p>
|
||||
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-selector-wrapper">
|
||||
<select v-model="visualizationSettings" @change="handleAnimationChange()">
|
||||
<option value="mic">Microphone (Mic access required)</option>
|
||||
<option value="off">No visualization except background</option>
|
||||
<option value="mic">
|
||||
Microphone (Mic access required)
|
||||
</option>
|
||||
<option value="off">
|
||||
No visualization except background
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" v-bind:key="song.id" class="song-list">
|
||||
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||
<img :src="song.cover" class="song-image">
|
||||
<div v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying" class="playing-symbols">
|
||||
<div
|
||||
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying"
|
||||
class="playing-symbols"
|
||||
>
|
||||
<div class="playing-symbols-wrapper">
|
||||
<div class="playing-bar" id="bar-1"></div>
|
||||
<div class="playing-bar" id="bar-2"></div>
|
||||
<div class="playing-bar" id="bar-3"></div>
|
||||
<div id="bar-1" class="playing-bar"></div>
|
||||
<div id="bar-2" class="playing-bar"></div>
|
||||
<div id="bar-3" class="playing-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-details-wrapper">
|
||||
@@ -47,10 +79,12 @@
|
||||
</div>
|
||||
<div v-else class="showcase-wrapper">
|
||||
<h1>Couldn't connect!</h1>
|
||||
<p>There does not appear to be a share with the specified name, or an error occurred when connecting.</p>
|
||||
<p>
|
||||
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||
</p>
|
||||
<p>You may reload the page to try again!</p>
|
||||
</div>
|
||||
<div class="background" id="background">
|
||||
<div id="background" class="background">
|
||||
<div class="beat-manual"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,8 +93,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SocketConnection from '@/scripts/connection';
|
||||
import type { Song } from '@/scripts/song';
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
computed, ref, type Ref
|
||||
} from 'vue';
|
||||
import bizualizer from '@/scripts/bizualizer';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
@@ -71,10 +109,11 @@
|
||||
const hasLoaded = ref( false );
|
||||
const showCouldNotFindRoom = ref( false );
|
||||
const playbackStart = ref( 0 );
|
||||
|
||||
let timeTracker = 0;
|
||||
|
||||
const visualizationSettings = ref( 'mic' );
|
||||
const isAntiTamperEnabled = ref( false );
|
||||
|
||||
const conn = new SocketConnection();
|
||||
|
||||
conn.connect().then( d => {
|
||||
@@ -82,22 +121,29 @@
|
||||
isPlaying.value = d.playbackStatus;
|
||||
playingSong.value = d.playlistIndex;
|
||||
playbackStart.value = d.playbackStart;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
}
|
||||
|
||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
progressBar.value
|
||||
= ( pos.value / ( playlist.value[ playingSong.value ]
|
||||
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
hasLoaded.value = true;
|
||||
|
||||
if ( d.useAntiTamper ) {
|
||||
isAntiTamperEnabled.value = true;
|
||||
notifier();
|
||||
}
|
||||
conn.registerListener( 'playlist', ( data ) => {
|
||||
|
||||
conn.registerListener( 'playlist', data => {
|
||||
playlist.value = data;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback', ( data ) => {
|
||||
conn.registerListener( 'playback', data => {
|
||||
isPlaying.value = data;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
} else {
|
||||
@@ -105,12 +151,12 @@
|
||||
}
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback-start', ( data ) => {
|
||||
conn.registerListener( 'playback-start', data => {
|
||||
playbackStart.value = data;
|
||||
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playlist-index', ( data ) => {
|
||||
conn.registerListener( 'playlist-index', data => {
|
||||
playingSong.value = parseInt( data );
|
||||
setTimeout( () => {
|
||||
setBackground();
|
||||
@@ -118,12 +164,13 @@
|
||||
} );
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
conn.registerListener( 'delete-share', ( _ ) => {
|
||||
conn.registerListener( 'delete-share', _ => {
|
||||
alert( 'This share was just deleted. It is no longer available. This page will reload automatically!' );
|
||||
conn.disconnect();
|
||||
location.reload();
|
||||
} );
|
||||
} ).catch( e => {
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
showCouldNotFindRoom.value = true;
|
||||
} );
|
||||
@@ -131,45 +178,50 @@
|
||||
const songQueue = computed( () => {
|
||||
let ret: Song[] = [];
|
||||
let pos = 0;
|
||||
|
||||
for ( let song in playlist.value ) {
|
||||
if ( pos >= playingSong.value ) {
|
||||
ret.push( playlist.value[ song ] );
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} );
|
||||
|
||||
// TODO: Handle disconnect from updater (=> have it disconnect)
|
||||
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song: string ) => {
|
||||
let timeRemaining = 0;
|
||||
|
||||
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||
if ( playlist.value[ i ].id == song ) {
|
||||
break;
|
||||
}
|
||||
|
||||
timeRemaining += playlist.value[ i ].duration;
|
||||
}
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min';
|
||||
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min after starting to play';
|
||||
}
|
||||
return 'Playing less than '
|
||||
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
};
|
||||
} );
|
||||
|
||||
const startTimeTracker = () => {
|
||||
try {
|
||||
clearInterval( timeTracker );
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
setTimeout( () => {
|
||||
@@ -179,21 +231,23 @@
|
||||
timeTracker = setInterval( () => {
|
||||
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||
|
||||
if ( isNaN( progressBar.value ) ) {
|
||||
progressBar.value = 0;
|
||||
}
|
||||
}, 100 );
|
||||
}
|
||||
};
|
||||
|
||||
const stopTimeTracker = () => {
|
||||
clearInterval( timeTracker );
|
||||
|
||||
handleAnimationChange();
|
||||
}
|
||||
};
|
||||
|
||||
const animateBeat = () => {
|
||||
$( '.beat-manual' ).stop();
|
||||
const duration = Math.ceil( 60 / 180 * 500 ) - 50;
|
||||
|
||||
$( '.beat-manual' ).fadeIn( 50 );
|
||||
setTimeout( () => {
|
||||
$( '.beat-manual' ).fadeOut( duration );
|
||||
@@ -202,30 +256,31 @@
|
||||
$( '.beat-manual' ).stop();
|
||||
}, duration );
|
||||
}, 50 );
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnimationChange = () => {
|
||||
if ( visualizationSettings.value === 'mic' && isPlaying.value ) {
|
||||
bizualizer.subscribeToBeatUpdate( animateBeat );
|
||||
} else {
|
||||
bizualizer.unsubscribeFromBeatUpdate()
|
||||
}
|
||||
bizualizer.unsubscribeFromBeatUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const setBackground = () => {
|
||||
bizualizer.createBackground().then( bg => {
|
||||
$( '#background' ).css( 'background', bg );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const notifier = () => {
|
||||
Notification.requestPermission();
|
||||
|
||||
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
||||
|
||||
// Detect if window is currently in focus
|
||||
window.onblur = () => {
|
||||
sendNotification();
|
||||
}
|
||||
};
|
||||
|
||||
// Detect if browser window becomes hidden (also with blur event)
|
||||
document.onvisibilitychange = () => {
|
||||
@@ -233,18 +288,19 @@
|
||||
sendNotification();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotification = () => {
|
||||
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
||||
body: 'Please return to the original webpage immediately!',
|
||||
requireInteraction: true,
|
||||
'body': 'Please return to the original webpage immediately!',
|
||||
'requireInteraction': true,
|
||||
} );
|
||||
|
||||
conn.emit( 'tampering', '' );
|
||||
}
|
||||
};
|
||||
|
||||
const isShowingSecureModeInfo = ref( false );
|
||||
|
||||
const secureModeInfo = ( action: string ) => {
|
||||
if ( action === 'toggle' ) {
|
||||
isShowingSecureModeInfo.value = !isShowingSecureModeInfo.value;
|
||||
@@ -253,7 +309,7 @@
|
||||
} else {
|
||||
isShowingSecureModeInfo.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
||||
<div class="content" id="app">
|
||||
<div v-if="hasLoaded" style="width: 100%">
|
||||
<div class="current-song-wrapper">
|
||||
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
|
||||
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art" id="current-image">
|
||||
<div class="current-song">
|
||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||
<h1>{{ playingSong.title }}</h1>
|
||||
<p class="dancing-style" v-if="playingSong.dancingStyle">{{ playingSong.dancingStyle }}</p>
|
||||
<p>{{ playingSong.artist }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-selector-wrapper">
|
||||
<select v-model="visualizationSettings" @change="setVisualization()">
|
||||
<option value="mic">Microphone (Mic access required)</option>
|
||||
<option value="bpm">BPM (might not be 100% accurate)</option>
|
||||
<option value="off">No visualization except background</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" class="song-list">
|
||||
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
|
||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin !== 'api'" :src="'/getSongCover?filename=' + song.filename" class="song-image">
|
||||
<div v-if="playingSong.filename === song.filename && isPlaying" class="playing-symbols">
|
||||
<div class="playing-symbols-wrapper">
|
||||
<div class="playing-bar" id="bar-1"></div>
|
||||
<div class="playing-bar" id="bar-2"></div>
|
||||
<div class="playing-bar" id="bar-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined pause-icon" v-if="!isPlaying && playingSong.filename === song.filename">pause</span>
|
||||
<div class="song-details-wrapper">
|
||||
<h3>{{ song.title }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
</div>
|
||||
<div class="time-until">
|
||||
{{ getTimeUntil( song ) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <img :src="" alt=""> -->
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>Loading...</h1>
|
||||
</div>
|
||||
<div class="background" id="background">
|
||||
<div class="beat"></div>
|
||||
<div class="beat-manual"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: Get ColorThief either from CDN or preferably as NPM module -->
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Song } from '@/scripts/song';
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import { ColorThief } from 'colorthief';
|
||||
|
||||
const hasLoaded = ref( false );
|
||||
const songs: Ref<Song[]> = ref( [] );
|
||||
const playingSong = ref( 0 );
|
||||
const isPlaying = ref( false );
|
||||
const pos = ref( 0 );
|
||||
const colourPalette: string[] = [];
|
||||
const progressBar = ref( 0 );
|
||||
const timeTracker = ref( 0 );
|
||||
const visualizationSettings = ref( 'mic' );
|
||||
const micAnalyzer = ref( 0 );
|
||||
const beatDetected = ref( false );
|
||||
const colorThief = new ColorThief();
|
||||
const songQueue = computed( () => {
|
||||
let ret = [];
|
||||
let pos = 0;
|
||||
for ( let song in songs.value ) {
|
||||
if ( pos >= playingSong.value ) {
|
||||
ret.push( songs.value[ song ] );
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
return ret;
|
||||
} );
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song ) => {
|
||||
let timeRemaining = 0;
|
||||
for ( let i = this.queuePos; i < Object.keys( this.songs ).length - 1; i++ ) {
|
||||
if ( this.songs[ i ] == song ) {
|
||||
break;
|
||||
}
|
||||
timeRemaining += parseInt( this.songs[ i ].duration );
|
||||
}
|
||||
if ( isPlaying.value ) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
methods: {
|
||||
startTimeTracker () {
|
||||
this.timeTracker = setInterval( () => {
|
||||
this.pos = ( new Date().getTime() - this.playingSong.startTime ) / 1000 + this.oldPos;
|
||||
this.progressBar = ( this.pos / this.playingSong.duration ) * 1000;
|
||||
if ( isNaN( this.progressBar ) ) {
|
||||
this.progressBar = 0;
|
||||
}
|
||||
}, 100 );
|
||||
},
|
||||
stopTimeTracker () {
|
||||
clearInterval( this.timeTracker );
|
||||
this.oldPos = this.pos;
|
||||
},
|
||||
getImageData() {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
if ( this.playingSong.hasCoverArt ) {
|
||||
setTimeout( () => {
|
||||
const img = document.getElementById( 'current-image' );
|
||||
if ( img.complete ) {
|
||||
resolve( this.colorThief.getPalette( img ) );
|
||||
} else {
|
||||
img.addEventListener( 'load', () => {
|
||||
resolve( this.colorThief.getPalette( img ) );
|
||||
} );
|
||||
}
|
||||
}, 500 );
|
||||
} else {
|
||||
reject( 'no image' );
|
||||
}
|
||||
} );
|
||||
},
|
||||
connect() {
|
||||
this.colorThief = new ColorThief();
|
||||
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
|
||||
source.onmessage = ( e ) => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse( e.data );
|
||||
} catch ( err ) {
|
||||
data = { 'type': e.data };
|
||||
}
|
||||
if ( data.type === 'basics' ) {
|
||||
this.isPlaying = data.data.isPlaying ?? false;
|
||||
this.playingSong = data.data.playingSong ?? {};
|
||||
this.songs = data.data.songQueue ?? [];
|
||||
this.pos = data.data.pos ?? 0;
|
||||
this.oldPos = data.data.pos ?? 0;
|
||||
this.progressBar = this.pos / this.playingSong.duration * 1000;
|
||||
this.queuePos = data.data.queuePos ?? 0;
|
||||
this.getImageData().then( palette => {
|
||||
this.colourPalette = palette;
|
||||
this.handleBackground();
|
||||
} ).catch( () => {
|
||||
this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ];
|
||||
this.handleBackground();
|
||||
} );
|
||||
} else if ( data.type === 'pos' ) {
|
||||
this.pos = data.data;
|
||||
this.oldPos = data.data;
|
||||
this.progressBar = data.data / this.playingSong.duration * 1000;
|
||||
} else if ( data.type === 'isPlaying' ) {
|
||||
this.isPlaying = data.data;
|
||||
this.handleBackground();
|
||||
} else if ( data.type === 'songQueue' ) {
|
||||
this.songs = data.data;
|
||||
} else if ( data.type === 'playingSong' ) {
|
||||
this.playingSong = data.data;
|
||||
this.getImageData().then( palette => {
|
||||
this.colourPalette = palette;
|
||||
this.handleBackground();
|
||||
} ).catch( () => {
|
||||
this.colourPalette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
|
||||
this.handleBackground();
|
||||
} );
|
||||
} else if ( data.type === 'queuePos' ) {
|
||||
this.queuePos = data.data;
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = () => {
|
||||
this.isReconnecting = false;
|
||||
this.hasLoaded = true;
|
||||
};
|
||||
|
||||
let self = this;
|
||||
|
||||
source.addEventListener( 'error', function( e ) {
|
||||
if ( e.eventPhase == EventSource.CLOSED ) source.close();
|
||||
|
||||
if ( e.target.readyState == EventSource.CLOSED ) {
|
||||
console.log( 'disconnected' );
|
||||
}
|
||||
|
||||
// TODO: Notify about disconnect
|
||||
setTimeout( () => {
|
||||
if ( !self.isReconnecting ) {
|
||||
self.isReconnecting = true;
|
||||
self.tryReconnect();
|
||||
}
|
||||
}, 1000 );
|
||||
}, false );
|
||||
},
|
||||
tryReconnect() {
|
||||
const int = setInterval( () => {
|
||||
if ( !this.isReconnecting ) {
|
||||
clearInterval( int );
|
||||
} else {
|
||||
connectToSSESource();
|
||||
}
|
||||
}, 1000 );
|
||||
},
|
||||
handleBackground() {
|
||||
let colourDetails = [];
|
||||
let colours = [];
|
||||
let differentEnough = true;
|
||||
if ( this.colourPalette[ 0 ] ) {
|
||||
for ( let i in this.colourPalette ) {
|
||||
for ( let colour in colourDetails ) {
|
||||
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - this.colourPalette[ i ][ 0 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 1 ] - this.colourPalette[ i ][ 1 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 2 ] - this.colourPalette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
||||
if ( colourDiff > 15 ) {
|
||||
differentEnough = true;
|
||||
}
|
||||
}
|
||||
if ( differentEnough ) {
|
||||
colourDetails.push( this.colourPalette[ i ] );
|
||||
colours.push( 'rgb(' + this.colourPalette[ i ][ 0 ] + ',' + this.colourPalette[ i ][ 1 ] + ',' + this.colourPalette[ i ][ 2 ] + ')' );
|
||||
}
|
||||
differentEnough = false;
|
||||
}
|
||||
}
|
||||
let outColours = 'conic-gradient(';
|
||||
if ( colours.length < 3 ) {
|
||||
for ( let i = 0; i < 3; i++ ) {
|
||||
if ( colours[ i ] ) {
|
||||
outColours += colours[ i ] + ',';
|
||||
} else {
|
||||
if ( i === 0 ) {
|
||||
outColours += 'blue,';
|
||||
} else if ( i === 1 ) {
|
||||
outColours += 'green,';
|
||||
} else if ( i === 2 ) {
|
||||
outColours += 'red,';
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ( colours.length < 11 ) {
|
||||
for ( let i in colours ) {
|
||||
outColours += colours[ i ] + ',';
|
||||
}
|
||||
} else {
|
||||
for ( let i = 0; i < 10; i++ ) {
|
||||
outColours += colours[ i ] + ',';
|
||||
}
|
||||
}
|
||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||
|
||||
$( '#background' ).css( 'background', outColours );
|
||||
this.setVisualization();
|
||||
},
|
||||
setVisualization () {
|
||||
if ( Object.keys( this.playingSong ).length > 0 ) {
|
||||
if ( this.visualizationSettings === 'bpm' ) {
|
||||
if ( this.playingSong.bpm && this.isPlaying ) {
|
||||
$( '.beat' ).show();
|
||||
$( '.beat' ).css( 'animation-duration', 60 / this.playingSong.bpm );
|
||||
$( '.beat' ).css( 'animation-delay', this.pos % ( 60 / this.playingSong.bpm * this.pos ) + this.playingSong.bpmOffset - ( 60 / this.playingSong.bpm * this.pos / 2 ) );
|
||||
} else {
|
||||
$( '.beat' ).hide();
|
||||
}
|
||||
try {
|
||||
clearInterval( this.micAnalyzer );
|
||||
} catch ( err ) {}
|
||||
} else if ( this.visualizationSettings === 'off' ) {
|
||||
$( '.beat' ).hide();
|
||||
try {
|
||||
clearInterval( this.micAnalyzer );
|
||||
} catch ( err ) {}
|
||||
} else if ( this.visualizationSettings === 'mic' ) {
|
||||
$( '.beat-manual' ).hide();
|
||||
try {
|
||||
clearInterval( this.micAnalyzer );
|
||||
} catch ( err ) {}
|
||||
this.micAudioHandler();
|
||||
}
|
||||
} else {
|
||||
console.log( 'not playing yet' );
|
||||
}
|
||||
},
|
||||
micAudioHandler () {
|
||||
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array( bufferLength );
|
||||
|
||||
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
||||
const mic = audioContext.createMediaStreamSource( stream );
|
||||
mic.connect( analyser );
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
let prevSpectrum = null;
|
||||
let threshold = 10; // Adjust as needed
|
||||
this.beatDetected = false;
|
||||
this.micAnalyzer = setInterval( () => {
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
// Convert the frequency data to a numeric array
|
||||
const currentSpectrum = Array.from( dataArray );
|
||||
|
||||
if ( prevSpectrum ) {
|
||||
// Calculate the spectral flux
|
||||
const flux = this.calculateSpectralFlux( prevSpectrum, currentSpectrum );
|
||||
|
||||
if ( flux > threshold && !this.beatDetected ) {
|
||||
// Beat detected
|
||||
this.beatDetected = true;
|
||||
this.animateBeat();
|
||||
}
|
||||
}
|
||||
prevSpectrum = currentSpectrum;
|
||||
}, 20 );
|
||||
} );
|
||||
},
|
||||
animateBeat () {
|
||||
$( '.beat-manual' ).stop();
|
||||
const duration = Math.ceil( 60 / ( this.playingSong.bpm ?? 180 ) * 500 ) - 50;
|
||||
$( '.beat-manual' ).fadeIn( 50 );
|
||||
setTimeout( () => {
|
||||
$( '.beat-manual' ).fadeOut( duration );
|
||||
setTimeout( () => {
|
||||
$( '.beat-manual' ).stop();
|
||||
this.beatDetected = false;
|
||||
}, duration );
|
||||
}, 50 );
|
||||
},
|
||||
calculateSpectralFlux( prevSpectrum, currentSpectrum ) {
|
||||
let flux = 0;
|
||||
|
||||
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
||||
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
||||
flux += Math.max( 0, diff );
|
||||
}
|
||||
|
||||
return flux;
|
||||
},
|
||||
notifier() {
|
||||
if ( parseInt( this.lastDispatch ) + 5000 < new Date().getTime() ) {
|
||||
|
||||
}
|
||||
Notification.requestPermission();
|
||||
|
||||
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
||||
// Detect if window is currently in focus
|
||||
window.onblur = () => {
|
||||
this.sendNotification( 'blur' );
|
||||
}
|
||||
|
||||
// Detect if browser window becomes hidden (also with blur event)
|
||||
document.onvisibilitychange = () => {
|
||||
if ( document.visibilityState === 'hidden' ) {
|
||||
this.sendNotification( 'visibility' );
|
||||
}
|
||||
};
|
||||
},
|
||||
sendNotification( notification ) {
|
||||
let fetchOptions = {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'type': notification } ),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
},
|
||||
};
|
||||
fetch( '/clientStatusUpdate', fetchOptions ).catch( err => {
|
||||
console.error( err );
|
||||
} );
|
||||
|
||||
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
||||
body: 'Please return to the original webpage immediately!',
|
||||
requireInteraction: true,
|
||||
} )
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connect();
|
||||
this.notifier();
|
||||
// if ( this.visualizationSettings === 'mic' ) {
|
||||
// this.micAudioHandler();
|
||||
// }
|
||||
},
|
||||
watch: {
|
||||
isPlaying( value ) {
|
||||
if ( value ) {
|
||||
this.startTimeTracker();
|
||||
} else {
|
||||
this.stopTimeTracker();
|
||||
}
|
||||
}
|
||||
}
|
||||
} ).mount( '#app' );
|
||||
|
||||
</script>
|
||||
@@ -3,7 +3,6 @@
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
@@ -14,6 +13,6 @@
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
"types": ["node", "jquery"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user