Prepare for new sdk

This commit is contained in:
2025-09-15 11:17:17 +02:00
parent 6b9d556e57
commit 6e93cfdf2c
19 changed files with 899 additions and 938 deletions

View File

@@ -75,7 +75,7 @@ const style = {
], ],
'@stylistic/eol-last': [ '@stylistic/eol-last': [
'error', 'error',
'always' 'never'
], ],
'@stylistic/function-call-spacing': [ '@stylistic/function-call-spacing': [
'error', 'error',
@@ -83,7 +83,9 @@ const style = {
], ],
'@stylistic/function-paren-newline': [ '@stylistic/function-paren-newline': [
'error', 'error',
'multiline' {
'minItems': 3
}
], ],
'@stylistic/function-call-argument-newline': [ '@stylistic/function-call-argument-newline': [
'error', 'error',

View File

@@ -13,7 +13,7 @@
"@melloware/coloris": "^0.24.0", "@melloware/coloris": "^0.24.0",
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "^5.0.5",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"colorthief": "^2.2.0", "colorthief": "^2.6.0",
"music-metadata-browser": "^2.5.10", "music-metadata-browser": "^2.5.10",
"musickit-typescript": "^1.2.4", "musickit-typescript": "^1.2.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@@ -23,9 +23,10 @@
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.34.0", "@eslint/js": "^9.35.0",
"@stylistic/eslint-plugin": "^5.3.1", "@stylistic/eslint-plugin": "^5.3.1",
"@tsconfig/node20": "^20.1.2", "@tsconfig/node20": "^20.1.2",
"@types/jquery": "^3.5.33",
"@types/node": "^20.11.10", "@types/node": "^20.11.10",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
@@ -33,7 +34,7 @@
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"sass-embedded": "^1.92.0", "sass-embedded": "^1.92.0",
"typescript": "~5.3.0", "typescript": "~5.3.0",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.43.0",
"vite": "^7.1.4", "vite": "^7.1.4",
"vue-tsc": "^2.0.29" "vue-tsc": "^2.0.29"
} }
@@ -639,9 +640,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.34.0", "version": "9.35.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1816,6 +1817,16 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "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": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1840,18 +1851,25 @@
"undici-types": "~6.21.0" "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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz",
"integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==", "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/type-utils": "8.43.0",
"@typescript-eslint/utils": "8.42.0", "@typescript-eslint/utils": "8.43.0",
"@typescript-eslint/visitor-keys": "8.42.0", "@typescript-eslint/visitor-keys": "8.43.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1865,7 +1883,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.42.0", "@typescript-eslint/parser": "^8.43.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
@@ -1881,16 +1899,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz",
"integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/types": "8.42.0", "@typescript-eslint/types": "8.43.0",
"@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/typescript-estree": "8.43.0",
"@typescript-eslint/visitor-keys": "8.42.0", "@typescript-eslint/visitor-keys": "8.43.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1906,14 +1924,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz",
"integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==", "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.42.0", "@typescript-eslint/tsconfig-utils": "^8.43.0",
"@typescript-eslint/types": "^8.42.0", "@typescript-eslint/types": "^8.43.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1928,14 +1946,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz",
"integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==", "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.42.0", "@typescript-eslint/types": "8.43.0",
"@typescript-eslint/visitor-keys": "8.42.0" "@typescript-eslint/visitor-keys": "8.43.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1946,9 +1964,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz",
"integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==", "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1963,15 +1981,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz",
"integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==", "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.42.0", "@typescript-eslint/types": "8.43.0",
"@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/typescript-estree": "8.43.0",
"@typescript-eslint/utils": "8.42.0", "@typescript-eslint/utils": "8.43.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@@ -1988,9 +2006,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz",
"integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==", "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2002,16 +2020,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz",
"integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==", "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.42.0", "@typescript-eslint/project-service": "8.43.0",
"@typescript-eslint/tsconfig-utils": "8.42.0", "@typescript-eslint/tsconfig-utils": "8.43.0",
"@typescript-eslint/types": "8.42.0", "@typescript-eslint/types": "8.43.0",
"@typescript-eslint/visitor-keys": "8.42.0", "@typescript-eslint/visitor-keys": "8.43.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -2057,16 +2075,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz",
"integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==", "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/types": "8.42.0", "@typescript-eslint/types": "8.43.0",
"@typescript-eslint/typescript-estree": "8.42.0" "@typescript-eslint/typescript-estree": "8.43.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2081,13 +2099,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz",
"integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==", "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.42.0", "@typescript-eslint/types": "8.43.0",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -3420,6 +3438,20 @@
"url": "https://opencollective.com/eslint" "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": { "node_modules/espree": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -4014,9 +4046,9 @@
} }
}, },
"node_modules/is-arrayish": { "node_modules/is-arrayish": {
"version": "0.3.2", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-buffer": { "node_modules/is-buffer": {
@@ -5923,9 +5955,9 @@
} }
}, },
"node_modules/sharp/node_modules/detect-libc": { "node_modules/sharp/node_modules/detect-libc": {
"version": "2.0.4", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -6040,9 +6072,9 @@
} }
}, },
"node_modules/simple-swizzle": { "node_modules/simple-swizzle": {
"version": "0.2.2", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-arrayish": "^0.3.1" "is-arrayish": "^0.3.1"
@@ -6406,16 +6438,16 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.42.0", "version": "8.43.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.42.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.43.0.tgz",
"integrity": "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==", "integrity": "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.42.0", "@typescript-eslint/parser": "8.43.0",
"@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/typescript-estree": "8.43.0",
"@typescript-eslint/utils": "8.42.0" "@typescript-eslint/utils": "8.43.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -17,7 +17,7 @@
"@melloware/coloris": "^0.24.0", "@melloware/coloris": "^0.24.0",
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "^5.0.5",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"colorthief": "^2.2.0", "colorthief": "^2.6.0",
"music-metadata-browser": "^2.5.10", "music-metadata-browser": "^2.5.10",
"musickit-typescript": "^1.2.4", "musickit-typescript": "^1.2.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@@ -27,9 +27,10 @@
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.34.0", "@eslint/js": "^9.35.0",
"@stylistic/eslint-plugin": "^5.3.1", "@stylistic/eslint-plugin": "^5.3.1",
"@tsconfig/node20": "^20.1.2", "@tsconfig/node20": "^20.1.2",
"@types/jquery": "^3.5.33",
"@types/node": "^20.11.10", "@types/node": "^20.11.10",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
@@ -37,7 +38,7 @@
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"sass-embedded": "^1.92.0", "sass-embedded": "^1.92.0",
"typescript": "~5.3.0", "typescript": "~5.3.0",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.43.0",
"vite": "^7.1.4", "vite": "^7.1.4",
"vue-tsc": "^2.0.29" "vue-tsc": "^2.0.29"
} }

View File

@@ -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> <template>
<div> <div>
<button @click="changeTheme();" id="themeSelector" title="Toggle between light and dark mode"><span class="material-symbols-outlined" v-html="theme"></span></button> <button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
<router-view v-slot="{ Component, route }" id="main-view"> <!-- 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"> <transition :name="route.meta.transition ? String( route.meta.transition ) : 'fade'" mode="out-in">
<component :is="Component"></component> <component :is="Component" />
</transition> </transition>
</router-view> </router-view>
</div> </div>
</template> </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> <style>
body { body {
background-color: var( --background-color ); background-color: var( --background-color );
@@ -178,33 +207,3 @@
background-position: 0px; background-position: 0px;
} }
</style> </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>

View File

@@ -1,15 +1,18 @@
import { createApp } from 'vue' import {
import { createPinia } from 'pinia' createApp
} from 'vue';
import {
createPinia
} from 'pinia';
import App from './App.vue';
import router from './router';
import App from './App.vue' const app = createApp( App );
import router from './router'
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', 'http://localhost:8082' );
localStorage.setItem( 'url', 'https://music-api.janishutz.com' ); localStorage.setItem( 'url', 'https://music-api.janishutz.com' );
app.mount('#app') app.mount( '#app' );

View File

@@ -1,9 +1,11 @@
import ColorThief from 'colorthief'; import ColorThief from 'colorthief';
const colorThief = new ColorThief(); const colorThief = new ColorThief();
const getImageData = (): Promise<number[][]> => { const getImageData = (): Promise<number[][]> => {
return new Promise( ( resolve ) => { return new Promise( resolve => {
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement ); const img = document.getElementById( 'current-image' ) as HTMLImageElement;
if ( img.complete ) { if ( img.complete ) {
resolve( colorThief.getPalette( img ) ); resolve( colorThief.getPalette( img ) );
} else { } else {
@@ -12,32 +14,39 @@ const getImageData = (): Promise<number[][]> => {
} ); } );
} }
} ); } );
} };
const createBackground = () => { const createBackground = () => {
return new Promise( ( resolve ) => { return new Promise( resolve => {
getImageData().then( palette => { getImageData().then( palette => {
const colourDetails: number[][] = []; const colourDetails: number[][] = [];
const colours: string[] = []; const colours: string[] = [];
let differentEnough = true; let differentEnough = true;
if ( palette[ 0 ] ) { if ( palette[ 0 ] ) {
for ( const i in palette ) { for ( const i in palette ) {
for ( const colour in colourDetails ) { for ( const colour in colourDetails ) {
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255 const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255
+ Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255 + Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255
+ Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100; + Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100;
if ( colourDiff > 15 ) { if ( colourDiff > 15 ) {
differentEnough = true; differentEnough = true;
} }
} }
if ( differentEnough ) { if ( differentEnough ) {
colourDetails.push( palette[ i ] ); colourDetails.push( palette[ i ] );
colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' ); colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' );
} }
differentEnough = false; differentEnough = false;
} }
} }
let outColours = 'conic-gradient('; let outColours = 'conic-gradient(';
if ( colours.length < 3 ) { if ( colours.length < 3 ) {
for ( let i = 0; i < 3; i++ ) { for ( let i = 0; i < 3; i++ ) {
if ( colours[ i ] ) { if ( colours[ i ] ) {
@@ -61,45 +70,56 @@ const createBackground = () => {
outColours += colours[ i ] + ','; outColours += colours[ i ] + ',';
} }
} }
outColours += colours[ 0 ] ?? 'blue' + ')'; outColours += colours[ 0 ] ?? 'blue' + ')';
resolve( outColours ); resolve( outColours );
} ); } );
} ); } );
} };
let callbackFun = () => {};
let callbackFun = () => {}
const subscribeToBeatUpdate = ( cb: () => void ) => { const subscribeToBeatUpdate = ( cb: () => void ) => {
callbackFun = cb; callbackFun = cb;
micAudioHandler(); micAudioHandler();
} };
const unsubscribeFromBeatUpdate = () => { const unsubscribeFromBeatUpdate = () => {
callbackFun = () => {} callbackFun = () => {};
try { try {
clearInterval( micAnalyzer ); clearInterval( micAnalyzer );
} catch ( e ) { /* empty */ } } catch ( e ) { /* empty */ }
} };
const coolDown = () => { const coolDown = () => {
beatDetected = false; beatDetected = false;
} };
let micAnalyzer = 0; let micAnalyzer = 0;
let beatDetected = false; let beatDetected = false;
const micAudioHandler = () => { const micAudioHandler = () => {
const audioContext = new ( window.AudioContext || window.webkitAudioContext )(); const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
const analyser = audioContext.createAnalyser(); const analyser = audioContext.createAnalyser();
analyser.fftSize = 256; analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount; const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array( bufferLength ); const dataArray = new Uint8Array( bufferLength );
beatDetected = false; beatDetected = false;
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => { navigator.mediaDevices.getUserMedia( {
'audio': true
} ).then( stream => {
const mic = audioContext.createMediaStreamSource( stream ); const mic = audioContext.createMediaStreamSource( stream );
mic.connect( analyser ); mic.connect( analyser );
analyser.getByteFrequencyData( dataArray ); analyser.getByteFrequencyData( dataArray );
let prevSpectrum: number[] = []; let prevSpectrum: number[] = [];
const threshold = 10; // Adjust as needed const threshold = 10; // Adjust as needed
micAnalyzer = setInterval( () => { micAnalyzer = setInterval( () => {
analyser.getByteFrequencyData( dataArray ); analyser.getByteFrequencyData( dataArray );
// Convert the frequency data to a numeric array // Convert the frequency data to a numeric array
@@ -115,25 +135,27 @@ const micAudioHandler = () => {
callbackFun(); callbackFun();
} }
} }
prevSpectrum = currentSpectrum; prevSpectrum = currentSpectrum;
}, 60 / 180 * 250 ); }, 60 / 180 * 250 );
} ); } );
} };
const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => { const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => {
let flux = 0; let flux = 0;
for ( let i = 0; i < prevSpectrum.length; i++ ) { for ( let i = 0; i < prevSpectrum.length; i++ ) {
const diff = currentSpectrum[ i ] - prevSpectrum[ i ]; const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
flux += Math.max( 0, diff ); flux += Math.max( 0, diff );
} }
return flux; return flux;
} };
export default { export default {
createBackground, createBackground,
subscribeToBeatUpdate, subscribeToBeatUpdate,
unsubscribeFromBeatUpdate, unsubscribeFromBeatUpdate,
coolDown, coolDown,
} };

View File

@@ -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 // These functions handle connections to the backend with socket.io
import { io, type Socket } from "socket.io-client"; import {
import type { SSEMap } from "./song"; io, type Socket
} from 'socket.io-client';
import type {
SSEMap
} from './song';
class SocketConnection { class SocketConnection {
socket: Socket; socket: Socket;
roomName: string; roomName: string;
isConnected: boolean; isConnected: boolean;
useSocket: boolean; useSocket: boolean;
eventSource?: EventSource; eventSource?: EventSource;
toBeListenedForItems: SSEMap; toBeListenedForItems: SSEMap;
reconnectRetryCount: number; reconnectRetryCount: number;
openConnectionsCount: number; openConnectionsCount: number;
constructor () { constructor () {
this.socket = io( localStorage.getItem( 'url' ) ?? '', { this.socket = io( localStorage.getItem( 'url' ) ?? '', {
autoConnect: false, 'autoConnect': false,
} ); } );
this.roomName = location.pathname.split( '/' )[ 2 ]; this.roomName = location.pathname.split( '/' )[ 2 ];
this.isConnected = false; this.isConnected = false;
@@ -38,12 +41,17 @@ class SocketConnection {
* Create a room token and connect to * Create a room token and connect to
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
connect (): Promise<any> { connect (): Promise<unknown> {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
if ( this.reconnectRetryCount < 5 ) { if ( this.reconnectRetryCount < 5 ) {
if ( this.useSocket ) { if ( this.useSocket ) {
this.socket.connect(); 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 ) { if ( res.status === true ) {
this.isConnected = true; this.isConnected = true;
resolve( res.data ); resolve( res.data );
@@ -51,35 +59,46 @@ class SocketConnection {
console.debug( res.msg ); console.debug( res.msg );
reject( 'ERR_ROOM_CONNECTING' ); reject( 'ERR_ROOM_CONNECTING' );
} }
} ); }
);
} else { } else {
if ( this.openConnectionsCount < 1 && !this.isConnected ) { if ( this.openConnectionsCount < 1 && !this.isConnected ) {
this.openConnectionsCount += 1; 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 ) { 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.eventSource.onopen = () => {
this.isConnected = true; this.isConnected = true;
this.reconnectRetryCount = 0; 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 ); const d = JSON.parse( e.data );
if ( this.toBeListenedForItems[ d.type ] ) { if ( this.toBeListenedForItems[ d.type ] ) {
this.toBeListenedForItems[ d.type ]( d.data ); this.toBeListenedForItems[ d.type ]( d.data );
} else if ( d.type === 'basics' ) { } else if ( d.type === 'basics' ) {
resolve( d.data ); resolve( d.data );
} }
} };
this.eventSource.onerror = () => { this.eventSource.onerror = () => {
if ( this.isConnected ) { if ( this.isConnected ) {
this.isConnected = false; this.isConnected = false;
this.openConnectionsCount -= 1; this.openConnectionsCount -= 1;
this.eventSource?.close(); 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 ); // console.debug( e );
this.eventSource = undefined; this.eventSource = undefined;
@@ -91,11 +110,16 @@ class SocketConnection {
} }
}; };
} else { } 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' ); 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' ); reject( 'ERR_ROOM_CONNECTING' );
} ); } );
} else { } else {
@@ -116,16 +140,23 @@ class SocketConnection {
* @param {any} data * @param {any} data
* @returns {void} * @returns {void}
*/ */
emit ( event: string, data: any ): void { emit ( event: string, data: unknown ): void {
if ( this.isConnected ) { if ( this.isConnected ) {
if ( this.useSocket ) { if ( this.useSocket ) {
this.socket.emit( event, { 'roomName': this.roomName, 'data': data } ); this.socket.emit( event, {
'roomName': this.roomName,
'data': data
} );
} else { } else {
fetch( localStorage.getItem( 'url' ) + '/socket/update', { fetch( localStorage.getItem( 'url' ) + '/socket/update', {
method: 'post', 'method': 'post',
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'data': data } ), 'body': JSON.stringify( {
credentials: 'include', 'event': event,
headers: { 'roomName': this.roomName,
'data': data
} ),
'credentials': 'include',
'headers': {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'charset': 'utf-8' 'charset': 'utf-8'
} }
@@ -140,7 +171,7 @@ class SocketConnection {
* @param {( data: any ) => void} cb The callback function / listener function * @param {( data: any ) => void} cb The callback function / listener function
* @returns {void} * @returns {void}
*/ */
registerListener ( event: string, cb: ( data: any ) => void ): void { registerListener ( event: string, cb: ( data: unknown ) => void ): void {
if ( this.useSocket ) { if ( this.useSocket ) {
if ( this.isConnected ) { if ( this.isConnected ) {
this.socket.on( event, cb ); this.socket.on( event, cb );
@@ -171,9 +202,11 @@ class SocketConnection {
if ( this.eventSource ) { if ( this.eventSource ) {
return this.eventSource!.OPEN && this.isConnected; return this.eventSource!.OPEN && this.isConnected;
} }
return false; return false;
} }
} }
} }
export default SocketConnection; export default SocketConnection;

View File

@@ -1,25 +1,39 @@
import type { SearchResult, Song, SongMove } from "./song"; import type {
SearchResult, Song, SongMove
} from './song';
interface Config { interface Config {
devToken: string; 'devToken': string;
userToken: string; 'userToken': string;
} }
type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10'; type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10';
type RepeatMode = 'off' | 'once' | 'all'; type RepeatMode = 'off' | 'once' | 'all';
class MusicKitJSWrapper { class MusicKitJSWrapper {
playingSongID: number; playingSongID: number;
playlist: Song[]; playlist: Song[];
queue: number[]; queue: number[];
config: Config; config: Config;
musicKit: any; musicKit: any;
isLoggedIn: boolean; isLoggedIn: boolean;
isPreparedToPlay: boolean; isPreparedToPlay: boolean;
repeatMode: RepeatMode; repeatMode: RepeatMode;
isShuffleEnabled: boolean; isShuffleEnabled: boolean;
hasEncounteredAuthError: boolean; hasEncounteredAuthError: boolean;
queuePos: number; queuePos: number;
audioPlayer: HTMLAudioElement; audioPlayer: HTMLAudioElement;
constructor () { constructor () {
@@ -27,8 +41,8 @@ class MusicKitJSWrapper {
this.playlist = []; this.playlist = [];
this.queue = []; this.queue = [];
this.config = { this.config = {
devToken: '', 'devToken': '',
userToken: '', 'userToken': '',
}; };
this.isShuffleEnabled = false; this.isShuffleEnabled = false;
this.repeatMode = 'off'; this.repeatMode = 'off';
@@ -58,14 +72,16 @@ class MusicKitJSWrapper {
this.musicKit.authorize().then( () => { this.musicKit.authorize().then( () => {
this.isLoggedIn = true; this.isLoggedIn = true;
this.init(); this.init();
} ).catch( () => { } )
.catch( () => {
this.hasEncounteredAuthError = true; this.hasEncounteredAuthError = true;
} ); } );
} else { } else {
this.musicKit.authorize().then( () => { this.musicKit.authorize().then( () => {
this.isLoggedIn = true; this.isLoggedIn = true;
this.init(); this.init();
} ).catch( () => { } )
.catch( () => {
this.hasEncounteredAuthError = true; this.hasEncounteredAuthError = true;
} ); } );
} }
@@ -76,25 +92,29 @@ class MusicKitJSWrapper {
* @returns {void} * @returns {void}
*/ */
init (): 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 ) { if ( res.status === 200 ) {
res.text().then( token => { res.text().then( token => {
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement; this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
// MusicKit global is now defined // MusicKit global is now defined
MusicKit.configure( { MusicKit.configure( {
developerToken: token, 'developerToken': token,
app: { 'app': {
name: 'MusicPlayer', 'name': 'MusicPlayer',
build: '3' 'build': '3'
}, },
storefrontId: 'CH', 'storefrontId': 'CH',
} ).then( () => { } ).then( () => {
this.config.devToken = token; this.config.devToken = token;
this.musicKit = MusicKit.getInstance(); this.musicKit = MusicKit.getInstance();
if ( this.musicKit.isAuthorized ) { if ( this.musicKit.isAuthorized ) {
this.isLoggedIn = true; this.isLoggedIn = true;
this.config.userToken = this.musicKit.musicUserToken; this.config.userToken = this.musicKit.musicUserToken;
} }
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off; 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 * @returns {boolean[]} Returns an array, where the first element indicates login status, the second one, if an error was encountered
*/ */
getAuth (): boolean[] { 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 { apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
if ( this.config.devToken != '' && this.config.userToken != '' ) { if ( this.config.devToken != '' && this.config.userToken != '' ) {
fetch( url, { fetch( url, {
method: 'GET', 'method': 'GET',
headers: { 'headers': {
'Authorization': `Bearer ${ this.config.devToken }`, 'Authorization': `Bearer ${ this.config.devToken }`,
'Music-User-Token': this.config.userToken 'Music-User-Token': this.config.userToken
} }
@@ -128,12 +151,18 @@ class MusicKitJSWrapper {
if ( res.status === 200 ) { if ( res.status === 200 ) {
res.json().then( json => { res.json().then( json => {
try { try {
callback( { 'status': 'ok', 'data': json } ); callback( {
'status': 'ok',
'data': json
} );
} catch ( err ) { /* empty */ } } catch ( err ) { /* empty */ }
} ); } );
} else { } else {
try { try {
callback( { 'status': 'error', 'error': res.status } ); callback( {
'status': 'error',
'error': res.status
} );
} catch ( err ) { /* empty */ } } catch ( err ) { /* empty */ }
} }
} ); } );
@@ -152,31 +181,38 @@ class MusicKitJSWrapper {
setPlaylistByID ( id: string ): Promise<void> { setPlaylistByID ( id: string ): Promise<void> {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
this.musicKit.setQueue( { playlist: id } ).then( () => { this.musicKit.setQueue( {
'playlist': id
} ).then( () => {
const pl = this.musicKit.queue.items; const pl = this.musicKit.queue.items;
const songs: Song[] = []; const songs: Song[] = [];
for ( const item in pl ) { for ( const item in pl ) {
let url = pl[ item ].attributes.artwork.url; let url = pl[ item ].attributes.artwork.url;
url = url.replace( '{w}', pl[ item ].attributes.artwork.width ); url = url.replace( '{w}', pl[ item ].attributes.artwork.width );
url = url.replace( '{h}', pl[ item ].attributes.artwork.height ); url = url.replace( '{h}', pl[ item ].attributes.artwork.height );
const song: Song = { const song: Song = {
artist: pl[ item ].attributes.artistName, 'artist': pl[ item ].attributes.artistName,
cover: url, 'cover': url,
duration: pl[ item ].attributes.durationInMillis / 1000, 'duration': pl[ item ].attributes.durationInMillis / 1000,
id: pl[ item ].id, 'id': pl[ item ].id,
origin: 'apple-music', 'origin': 'apple-music',
title: pl[ item ].attributes.name, 'title': pl[ item ].attributes.name,
genres: pl[ item ].attributes.genreNames 'genres': pl[ item ].attributes.genreNames
} };
songs.push( song ); songs.push( song );
} }
this.playlist = songs; this.playlist = songs;
this.setShuffle( this.isShuffleEnabled ); this.setShuffle( this.isShuffleEnabled );
this.queuePos = 0; this.queuePos = 0;
this.playingSongID = this.queue[ 0 ]; this.playingSongID = this.queue[ 0 ];
this.prepare( this.playingSongID ); this.prepare( this.playingSongID );
resolve(); resolve();
} ).catch( err => { } )
.catch( err => {
console.error( err ); console.error( err );
reject( err ); reject( err );
} ); } );
@@ -192,18 +228,23 @@ class MusicKitJSWrapper {
if ( this.playlist.length > 0 ) { if ( this.playlist.length > 0 ) {
this.playingSongID = playlistID; this.playingSongID = playlistID;
this.isPreparedToPlay = true; this.isPreparedToPlay = true;
for ( const el in this.queue ) { for ( const el in this.queue ) {
if ( this.queue[ el ] === playlistID ) { if ( this.queue[ el ] === playlistID ) {
this.queuePos = parseInt( el ); this.queuePos = parseInt( el );
break; break;
} }
} }
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { 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( () => { setTimeout( () => {
this.control( 'play' ); this.control( 'play' );
}, 500 ); }, 500 );
} ).catch( ( err ) => { } )
.catch( err => {
console.log( err ); console.log( err );
} ); } );
} else { } else {
@@ -213,6 +254,7 @@ class MusicKitJSWrapper {
this.control( 'play' ); this.control( 'play' );
}, 500 ); }, 500 );
} }
return true; return true;
} else { } else {
return false; return false;
@@ -226,50 +268,63 @@ class MusicKitJSWrapper {
*/ */
control ( action: ControlAction ): boolean { control ( action: ControlAction ): boolean {
switch ( action ) { switch ( action ) {
case "play": case 'play':
if ( this.isPreparedToPlay ) { if ( this.isPreparedToPlay ) {
this.control( 'pause' ); this.control( 'pause' );
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.play(); this.musicKit.play();
return false; return false;
} else { } else {
this.audioPlayer.play(); this.audioPlayer.play();
return false; return false;
} }
} else { } else {
return false; return false;
} }
case "pause":
case 'pause':
if ( this.isPreparedToPlay ) { if ( this.isPreparedToPlay ) {
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.pause(); this.musicKit.pause();
return false; return false;
} else { } else {
this.audioPlayer.pause(); this.audioPlayer.pause();
return false; return false;
} }
} else { } else {
return false; return false;
} }
case "back-10":
case 'back-10':
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 ); this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
return false; return false;
} else { } else {
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0; this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
return false; return false;
} }
case "skip-10":
case 'skip-10':
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) { if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) { if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 ); this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
return false; return false;
} else { } else {
if ( this.repeatMode !== 'once' ) { if ( this.repeatMode !== 'once' ) {
this.control( 'next' ); this.control( 'next' );
return true; return true;
} else { } else {
this.musicKit.seekToTime( 0 ); this.musicKit.seekToTime( 0 );
return false; return false;
} }
} }
@@ -283,32 +338,42 @@ class MusicKitJSWrapper {
this.audioPlayer.currentTime = 0; this.audioPlayer.currentTime = 0;
} }
} }
return false; return false;
} }
case "next":
case 'next':
this.control( 'pause' ); this.control( 'pause' );
if ( this.queuePos < this.queue.length - 1 ) { if ( this.queuePos < this.queue.length - 1 ) {
this.queuePos += 1; this.queuePos += 1;
this.prepare( this.queue[ this.queuePos ] ); this.prepare( this.queue[ this.queuePos ] );
return true; return true;
} else { } else {
this.queuePos = 0; this.queuePos = 0;
if ( this.repeatMode !== 'all' ) { if ( this.repeatMode !== 'all' ) {
this.control( 'pause' ); this.control( 'pause' );
} else { } else {
this.playingSongID = this.queue[ this.queuePos ]; this.playingSongID = this.queue[ this.queuePos ];
this.prepare( this.queue[ this.queuePos ] ); this.prepare( this.queue[ this.queuePos ] );
} }
return true; return true;
} }
case "previous":
case 'previous':
this.control( 'pause' ); this.control( 'pause' );
if ( this.queuePos > 0 ) { if ( this.queuePos > 0 ) {
this.queuePos -= 1; this.queuePos -= 1;
this.prepare( this.queue[ this.queuePos ] ); this.prepare( this.queue[ this.queuePos ] );
return true; return true;
} else { } else {
this.queuePos = this.queue.length - 1; this.queuePos = this.queue.length - 1;
return true; return true;
} }
} }
@@ -317,15 +382,22 @@ class MusicKitJSWrapper {
setShuffle ( enabled: boolean ) { setShuffle ( enabled: boolean ) {
this.isShuffleEnabled = enabled; this.isShuffleEnabled = enabled;
this.queue = []; this.queue = [];
if ( enabled ) { if ( enabled ) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const d = []; const d = [];
for ( const el in this.playlist ) { for ( const el in this.playlist ) {
d.push( parseInt( el ) ); 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 ) .sort( ( a, b ) => a.sort - b.sort )
.map( ( { value } ) => value ); .map( ( {
value
} ) => value );
this.queue.splice( this.queue.indexOf( this.playingSongID ), 1 ); this.queue.splice( this.queue.indexOf( this.playingSongID ), 1 );
this.queue.push( this.playingSongID ); this.queue.push( this.playingSongID );
this.queue.reverse(); this.queue.reverse();
@@ -334,6 +406,7 @@ class MusicKitJSWrapper {
this.queue.push( parseInt( song ) ); this.queue.push( parseInt( song ) );
} }
} }
// Find current song ID in queue // Find current song ID in queue
for ( const el in this.queue ) { for ( const el in this.queue ) {
if ( this.queue[ el ] === this.playingSongID ) { if ( this.queue[ el ] === this.playingSongID ) {
@@ -359,29 +432,37 @@ class MusicKitJSWrapper {
moveSong ( move: SongMove ) { moveSong ( move: SongMove ) {
const newQueue = []; const newQueue = [];
const finishedQueue = []; const finishedQueue = [];
let songID = 0; let songID = 0;
for ( const song in this.playlist ) { for ( const song in this.playlist ) {
if ( this.playlist[ song ].id === move.songID ) { if ( this.playlist[ song ].id === move.songID ) {
songID = parseInt( song ); songID = parseInt( song );
break; break;
} }
} }
for ( const el in this.queue ) { for ( const el in this.queue ) {
if ( this.queue[ el ] !== songID ) { if ( this.queue[ el ] !== songID ) {
newQueue.push( this.queue[ el ] ); newQueue.push( this.queue[ el ] );
} }
} }
let hasBeenAdded = false; let hasBeenAdded = false;
for ( const el in newQueue ) { for ( const el in newQueue ) {
if ( parseInt( el ) === move.newPos ) { if ( parseInt( el ) === move.newPos ) {
finishedQueue.push( songID ); finishedQueue.push( songID );
hasBeenAdded = true; hasBeenAdded = true;
} }
finishedQueue.push( newQueue[ el ] ); finishedQueue.push( newQueue[ el ] );
} }
if ( !hasBeenAdded ) { if ( !hasBeenAdded ) {
finishedQueue.push( songID ); finishedQueue.push( songID );
} }
this.queue = finishedQueue; this.queue = finishedQueue;
} }
@@ -435,9 +516,11 @@ class MusicKitJSWrapper {
*/ */
getQueue (): Song[] { getQueue (): Song[] {
const data = []; const data = [];
for ( const el in this.queue ) { for ( const el in this.queue ) {
data.push( this.playlist[ this.queue[ el ] ] ); data.push( this.playlist[ this.queue[ el ] ] );
} }
return data; return data;
} }
@@ -449,6 +532,7 @@ class MusicKitJSWrapper {
getUserPlaylists ( cb: ( data: object ) => void ): boolean { getUserPlaylists ( cb: ( data: object ) => void ): boolean {
if ( this.isLoggedIn ) { if ( this.isLoggedIn ) {
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb ); this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb );
return true; return true;
} else { } else {
return false; return false;
@@ -467,17 +551,20 @@ class MusicKitJSWrapper {
// TODO: Make storefront adjustable // TODO: Make storefront adjustable
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
const queryParameters = { const queryParameters = {
term: ( searchTerm ), 'term': searchTerm,
types: [ 'songs' ], '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 ); resolve( results );
} ).catch( e => { } )
.catch( e => {
console.error( e ); console.error( e );
reject( e ); reject( e );
} ); } );
} ); } );
} }
} }
export default MusicKitJSWrapper; export default MusicKitJSWrapper;

View File

@@ -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 // These functions handle connections to the backend with socket.io
import { io, type Socket } from "socket.io-client" import {
import type { SSEMap } from "./song"; io, type Socket
} from 'socket.io-client';
import type {
SSEMap
} from './song';
class NotificationHandler { class NotificationHandler {
socket: Socket; socket: Socket;
roomName: string; roomName: string;
roomToken: string; roomToken: string;
isConnected: boolean; isConnected: boolean;
useSocket: boolean; useSocket: boolean;
eventSource?: EventSource; eventSource?: EventSource;
toBeListenedForItems: SSEMap; toBeListenedForItems: SSEMap;
reconnectRetryCount: number; reconnectRetryCount: number;
lastEmitTimestamp: number; lastEmitTimestamp: number;
openConnectionsCount: number; openConnectionsCount: number;
pendingRequestCount: number; pendingRequestCount: number;
connectionWasSuccessful: boolean; connectionWasSuccessful: boolean;
constructor () { constructor () {
this.socket = io( localStorage.getItem( 'url' ) ?? '', { this.socket = io( localStorage.getItem( 'url' ) ?? '', {
autoConnect: false, 'autoConnect': false,
} ); } );
this.roomName = ''; this.roomName = '';
this.roomToken = ''; this.roomToken = '';
@@ -50,28 +57,37 @@ class NotificationHandler {
*/ */
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> { connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
return new Promise( ( resolve, reject ) => { 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 ) { if ( res.status === 200 ) {
res.text().then( text => { res.text().then( text => {
this.roomToken = text; this.roomToken = text;
this.roomName = roomName; this.roomName = roomName;
if ( this.useSocket ) { if ( this.useSocket ) {
this.socket.connect(); this.socket.connect();
this.socket.emit( 'create-room', { this.socket.emit(
name: this.roomName, 'create-room', {
token: this.roomToken 'name': this.roomName,
}, ( res: { status: boolean, msg: string } ) => { 'token': this.roomToken
}, ( res: {
'status': boolean,
'msg': string
} ) => {
if ( res.status === true ) { if ( res.status === true ) {
this.isConnected = true; this.isConnected = true;
resolve(); resolve();
} else { } else {
reject( 'ERR_ROOM_CONNECTING' ); reject( 'ERR_ROOM_CONNECTING' );
} }
} ); }
);
} else { } else {
this.sseConnect().then( () => { this.sseConnect().then( () => {
resolve(); resolve();
} ).catch( ); } )
.catch( );
} }
} ); } );
} else if ( res.status === 409 ) { } else if ( res.status === 409 ) {
@@ -90,9 +106,13 @@ class NotificationHandler {
if ( this.reconnectRetryCount < 5 ) { if ( this.reconnectRetryCount < 5 ) {
if ( this.openConnectionsCount < 1 && !this.isConnected ) { if ( this.openConnectionsCount < 1 && !this.isConnected ) {
this.openConnectionsCount += 1; 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 ) { 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.eventSource.onopen = () => {
this.isConnected = true; this.isConnected = true;
@@ -100,16 +120,17 @@ class NotificationHandler {
this.reconnectRetryCount = 0; this.reconnectRetryCount = 0;
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' ); console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
resolve(); resolve();
} };
this.eventSource.onmessage = ( e ) => { this.eventSource.onmessage = e => {
const d = JSON.parse( e.data ); const d = JSON.parse( e.data );
if ( this.toBeListenedForItems[ d.type ] ) { if ( this.toBeListenedForItems[ d.type ] ) {
this.toBeListenedForItems[ d.type ]( d.data ); this.toBeListenedForItems[ d.type ]( d.data );
} }
} };
this.eventSource.onerror = ( e ) => { this.eventSource.onerror = e => {
if ( this.isConnected ) { if ( this.isConnected ) {
this.isConnected = false; this.isConnected = false;
this.eventSource?.close(); this.eventSource?.close();
@@ -131,7 +152,8 @@ class NotificationHandler {
} else { } else {
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' ); reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
} }
} ).catch( () => { } )
.catch( () => {
if ( !this.connectionWasSuccessful ) { if ( !this.connectionWasSuccessful ) {
reject( 'ERR_ROOM_CONNECTING' ); reject( 'ERR_ROOM_CONNECTING' );
} else { } else {
@@ -169,9 +191,14 @@ class NotificationHandler {
emit ( event: string, data: any ): void { emit ( event: string, data: any ): void {
if ( this.isConnected ) { if ( this.isConnected ) {
if ( this.useSocket ) { 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 { } else {
const now = new Date().getTime(); const now = new Date().getTime();
if ( this.lastEmitTimestamp < now - 250 ) { if ( this.lastEmitTimestamp < now - 250 ) {
this.lastEmitTimestamp = now; this.lastEmitTimestamp = now;
this.sendEmitConventionally( event, data ); this.sendEmitConventionally( event, data );
@@ -189,10 +216,15 @@ class NotificationHandler {
sendEmitConventionally ( event: string, data: any ): void { sendEmitConventionally ( event: string, data: any ): void {
fetch( localStorage.getItem( 'url' ) + '/socket/update', { fetch( localStorage.getItem( 'url' ) + '/socket/update', {
method: 'post', 'method': 'post',
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'roomToken': this.roomToken, 'data': data } ), 'body': JSON.stringify( {
credentials: 'include', 'event': event,
headers: { 'roomName': this.roomName,
'roomToken': this.roomToken,
'data': data
} ),
'credentials': 'include',
'headers': {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'charset': 'utf-8' 'charset': 'utf-8'
} }
@@ -222,22 +254,32 @@ class NotificationHandler {
async disconnect (): Promise<void> { async disconnect (): Promise<void> {
if ( this.isConnected ) { if ( this.isConnected ) {
if ( this.useSocket ) { if ( this.useSocket ) {
this.socket.emit( 'delete-room', { this.socket.emit(
name: this.roomName, 'delete-room', {
token: this.roomToken 'name': this.roomName,
}, ( res: { status: boolean, msg: string } ) => { 'token': this.roomToken
}, ( res: {
'status': boolean,
'msg': string
} ) => {
this.socket.disconnect(); this.socket.disconnect();
if ( !res.status ) { if ( !res.status ) {
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' ); alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
} }
return; return;
} ); }
);
} else { } else {
fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', { fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', {
method: 'post', 'method': 'post',
body: JSON.stringify( { 'roomName': this.roomName, 'roomToken': this.roomToken } ), 'body': JSON.stringify( {
credentials: 'include', 'roomName': this.roomName,
headers: { 'roomToken': this.roomToken
} ),
'credentials': 'include',
'headers': {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'charset': 'utf-8' 'charset': 'utf-8'
} }
@@ -247,8 +289,10 @@ class NotificationHandler {
} else { } else {
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' ); alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
} }
return; return;
} ).catch( () => { } )
.catch( () => {
return; return;
} ); } );
} }
@@ -258,6 +302,7 @@ class NotificationHandler {
getRoomName (): string { getRoomName (): string {
return this.roomName; return this.roomName;
} }
} }
export default NotificationHandler; export default NotificationHandler;

View File

@@ -4,90 +4,90 @@ export interface Song {
/** /**
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_ * 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 of the song
*/ */
origin: Origin; 'origin': Origin;
/** /**
* The cover image as a URL * The cover image as a URL
*/ */
cover: string; 'cover': string;
/** /**
* The artist of the song * The artist of the song
*/ */
artist: string; 'artist': string;
/** /**
* The name of the song * The name of the song
*/ */
title: string; 'title': string;
/** /**
* Duration of the song in milliseconds * 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 * (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 * (OPTIONAL) This will be displayed in brackets on the showcase screens
*/ */
additionalInfo?: string; 'additionalInfo'?: string;
} }
export interface SongTransmitted { export interface SongTransmitted {
title: string; 'title': string;
artist: string; 'artist': string;
duration: number; 'duration': number;
cover: string; 'cover': string;
additionalInfo?: string; 'additionalInfo'?: string;
} }
export interface ReadFile { export interface ReadFile {
url: string; 'url': string;
filename: string; 'filename': string;
} }
export interface SearchResult { export interface SearchResult {
data: { 'data': {
results: { 'results': {
songs: { 'songs': {
data: AppleMusicSongData[], 'data': AppleMusicSongData[],
href: string; 'href': string;
} }
}; };
} }
} }
export interface AppleMusicSongData { export interface AppleMusicSongData {
id: string, 'id': string,
type: string; 'type': string;
href: string; 'href': string;
attributes: { 'attributes': {
albumName: string; 'albumName': string;
artistName: string; 'artistName': string;
artwork: { 'artwork': {
width: number, 'width': number,
height: number, 'height': number,
url: string 'url': string
}, },
name: string; 'name': string;
genreNames: string[]; 'genreNames': string[];
durationInMillis: number; 'durationInMillis': number;
} }
} }
export interface SongMove { export interface SongMove {
songID: string; 'songID': string;
newPos: number; 'newPos': number;
} }
export interface SSEMap { export interface SSEMap {

View File

@@ -1,23 +1,22 @@
/* import {
* LanguageSchoolHossegorBookingSystem - userStore.js defineStore
* } from 'pinia';
* Created by Janis Hutz 10/27/2023, Licensed under a proprietary License
* https://janishutz.com, development@janishutz.com
*
*
*/
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" // 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', { export const useUserStore = defineStore( 'user', {
state: () => ( { 'isUserAuth': true, 'hasSubscribed': false, 'isUsingKeyboard': false, 'username': '', 'isFOSSVersion': false } ), 'state': () => ( {
getters: { 'isUserAuth': true,
getUserAuthenticated: ( state ) => state.isUserAuth, 'hasSubscribed': false,
getSubscriptionStatus: ( state ) => state.hasSubscribed, 'isUsingKeyboard': false,
'username': '',
'isFOSSVersion': false
} ),
'getters': {
'getUserAuthenticated': state => state.isUserAuth,
'getSubscriptionStatus': state => state.hasSubscribed,
}, },
actions: { 'actions': {
setUserAuth ( auth: boolean ) { setUserAuth ( auth: boolean ) {
this.isUserAuth = auth; this.isUserAuth = auth;
}, },

View File

@@ -1,20 +1,40 @@
<template> <template>
<div class="app-view"> <div class="app-view">
<button id="logout" @click="logout()"><span class="material-symbols-outlined">logout</span></button> <button id="logout" @click="logout()">
<div class="loading-view" v-if="!hasFinishedLoading"> <span class="material-symbols-outlined">logout</span>
</button>
<div v-if="!hasFinishedLoading" class="loading-view">
<h1>Loading...</h1> <h1>Loading...</h1>
</div> </div>
<div class="home-view" v-else-if="hasFinishedLoading && isReady"> <div v-else-if="hasFinishedLoading && isReady" class="home-view">
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }" <libraryView
:is-logged-in="isLoggedIntoAppleMusic" @custom-playlist="( pl ) => selectCustomPlaylist( pl )"></libraryView> class="library-view"
:playlists="playlists"
:is-logged-in="isLoggedIntoAppleMusic"
@selected-playlist="( id ) => { selectPlaylist( id ) }"
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
/>
</div> </div>
<div v-else class="login-view"> <div v-else class="login-view">
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon"> <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" style="margin-top: 20px;" @click="logIntoAppleMusic()">
<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> 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> </div>
<playerView :class="'player-view' + ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }" <playerView
ref="player"></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 --> <!-- TODO: Call to backend to check if user has access -->
</div> </div>
</template> </template>
@@ -22,10 +42,16 @@
<script setup lang="ts"> <script setup lang="ts">
import playerView from '@/components/playerView.vue'; import playerView from '@/components/playerView.vue';
import libraryView from '@/components/libraryView.vue'; import libraryView from '@/components/libraryView.vue';
import { ref } from 'vue'; import {
import type { ReadFile } from '@/scripts/song'; ref
} from 'vue';
import type {
ReadFile
} from '@/scripts/song';
import router from '@/router'; import router from '@/router';
import { useUserStore } from '@/stores/userStore'; import {
useUserStore
} from '@/stores/userStore';
const isLoggedIntoAppleMusic = ref( false ); const isLoggedIntoAppleMusic = ref( false );
const isReady = ref( false ); const isReady = ref( false );
@@ -41,7 +67,7 @@
} else { } else {
isShowingFullScreenPlayer.value = true; isShowingFullScreenPlayer.value = true;
} }
} };
let loginChecker = 0; let loginChecker = 0;
@@ -51,7 +77,7 @@
if ( player.value.getAuth()[ 0 ] ) { if ( player.value.getAuth()[ 0 ] ) {
isLoggedIntoAppleMusic.value = true; isLoggedIntoAppleMusic.value = true;
isReady.value = true; isReady.value = true;
player.value.getPlaylists( ( data ) => { player.value.getPlaylists( data => {
playlists.value = data.data.data; playlists.value = data.data.data;
} ); } );
clearInterval( loginChecker ); clearInterval( loginChecker );
@@ -60,25 +86,27 @@
alert( 'An error occurred when logging you in. Please try again!' ); alert( 'An error occurred when logging you in. Please try again!' );
} }
}, 500 ); }, 500 );
} };
const skipLogin = () => { const skipLogin = () => {
isReady.value = true; isReady.value = true;
isLoggedIntoAppleMusic.value = false; isLoggedIntoAppleMusic.value = false;
player.value.skipLogin(); player.value.skipLogin();
} };
const selectPlaylist = ( id: string ) => { const selectPlaylist = ( id: string ) => {
player.value.selectPlaylist( id ); player.value.selectPlaylist( id );
player.value.controlUI( 'show' ); player.value.controlUI( 'show' );
} };
const selectCustomPlaylist = ( playlist: ReadFile[] ) => { const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
player.value.selectCustomPlaylist( playlist ); player.value.selectCustomPlaylist( playlist );
player.value.controlUI( 'show' ); 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 ) { if ( res.status === 200 ) {
res.text().then( text => { res.text().then( text => {
if ( text === 'ok' ) { if ( text === 'ok' ) {
@@ -102,7 +130,7 @@
const logout = () => { const logout = () => {
// location.href = 'http://localhost:8080/logout?return=' + location.href; // location.href = 'http://localhost:8080/logout?return=' + location.href;
location.href = 'https://id.janishutz.com/logout?return=' + location.href; location.href = 'https://id.janishutz.com/logout?return=' + location.href;
} };
</script> </script>
<style scoped> <style scoped>

View File

@@ -3,13 +3,35 @@
<div class="top-view"> <div class="top-view">
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo"> <img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
<h1>MusicPlayer</h1> <h1>MusicPlayer</h1>
<p v-if="reasonForRedirectHere" style="color: red;">{{ reasons[ reasonForRedirectHere ] }}</p> <p v-if="reasonForRedirectHere" style="color: red;">
<p v-if="!reasonForRedirectHere"><i>An Open Source, browser-based MusicPlayer with beautiful graphics</i></p> {{ reasons[ reasonForRedirectHere ] }}
</p>
<p v-if="!reasonForRedirectHere">
<i>An Open Source, browser-based MusicPlayer with beautiful graphics</i>
</p>
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<a href="https://store.janishutz.com/product/com.janishutz.MusicPlayer" class="fancy-button" target="_blank">Subscribe</a> <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> <a
<button href="/" class="fancy-button" style="margin-left: 10px;" v-if="reasonForRedirectHere" @click="logout()">Log out</button> v-if="!reasonForRedirectHere"
<a href="https://github.com/simplePCBuilding/MusicPlayerV2" class="fancy-button" style="margin-left: 10px;" target="_blank">GitHub</a> 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> </div>
<div> <div>
@@ -20,7 +42,10 @@
<p>Use MusicPlayer in conjunction with Apple Music</p> <p>Use MusicPlayer in conjunction with Apple Music</p>
<h2>Share your playlist</h2> <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> <h2>Fully browser based</h2>
<p>No installation required when using MusicPlayer on <a href="https://music.janishutz.com">music.janishutz.com</a></p> <p>No installation required when using MusicPlayer on <a href="https://music.janishutz.com">music.janishutz.com</a></p>
@@ -29,7 +54,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, type Ref } from 'vue'; import {
ref, type Ref
} from 'vue';
interface Reasons { interface Reasons {
[key: string]: string; [key: string]: string;
@@ -39,12 +66,13 @@
'notOwned': 'Please subscribe to use MusicPlayer here, or download and install it manually from GitHub!', 'notOwned': 'Please subscribe to use MusicPlayer here, or download and install it manually from GitHub!',
} ); } );
const reasonForRedirectHere = ref( sessionStorage.getItem( 'getRedirectionReason' ) ); const reasonForRedirectHere = ref( sessionStorage.getItem( 'getRedirectionReason' ) );
sessionStorage.removeItem( 'getRedirectionReason' ); sessionStorage.removeItem( 'getRedirectionReason' );
const logout = () => { const logout = () => {
// location.href = 'http://localhost:8080/logout?return=' + location.href; // location.href = 'http://localhost:8080/logout?return=' + location.href;
location.href = 'https://id.janishutz.com/logout?return=' + location.href; location.href = 'https://id.janishutz.com/logout?return=' + location.href;
} };
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,54 +1,60 @@
<template> <template>
<div class="home-view"> <div class="home-view">
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo"> <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()" <button
style="margin-top: 5vh;" title="Sign in or sign up with janishutz.com ID" v-if="status" :class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )"
>{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}</button> style="margin-top: 5vh;"
<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> title="Sign in or sign up with janishutz.com ID"
<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> @click="login()"
<router-link to="/get" class="fancy-button">More information</router-link> >
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule> {{ 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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO: Make possible to install and use without account, if using FOSS version // TODO: Make possible to install and use without account, if using FOSS version
import router from '@/router'; import router from '@/router';
import { RouterLink } from 'vue-router'; import {
import { useUserStore } from '@/stores/userStore'; RouterLink
} from 'vue-router';
import {
useUserStore
} from '@/stores/userStore';
import notificationsModule from '@/components/notificationsModule.vue'; import notificationsModule from '@/components/notificationsModule.vue';
import { ref } from 'vue'; import {
ref
} from 'vue';
const notifications = ref( notificationsModule ); const notifications = ref( notificationsModule );
const isTryingToSignIn = ref( true ); const isTryingToSignIn = ref( true );
interface JanishutzIDSDK { interface JanishutzIDSDK {
setLoginSDKURL: ( url: string ) => undefined; 'setLoginSDKURL': ( url: string ) => undefined;
createSession: () => undefined; 'createSession': () => undefined;
verifySession: () => Promise<JHIDSessionStatus> 'verifySession': () => Promise<JHIDSessionStatus>
} }
interface JHIDSessionStatus { interface JHIDSessionStatus {
status: boolean; 'status': boolean;
username: string; 'username': string;
} }
let sdk: JanishutzIDSDK; 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 = () => { const login = () => {
sdk.createSession(); sdk.createSession();
} };
const store = useUserStore(); const store = useUserStore();
@@ -56,15 +62,17 @@
router.push( localStorage.getItem( 'redirect' ) ?? '/app' ); router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
localStorage.removeItem( 'redirect' ); localStorage.removeItem( 'redirect' );
} else { } else {
if ( typeof( sdk ) !== 'undefined' ) { if ( typeof sdk !== 'undefined' ) {
sdk.verifySession().then( res => { sdk.verifySession().then( res => {
if ( res.status ) { if ( res.status ) {
store.isUserAuth = true; store.isUserAuth = true;
store.username = res.username; store.username = res.username;
if ( localStorage.getItem( 'close-tab' ) ) { if ( localStorage.getItem( 'close-tab' ) ) {
localStorage.removeItem( 'close-tab' ); localStorage.removeItem( 'close-tab' );
window.close(); window.close();
} }
localStorage.setItem( 'login-ok', 'true' ); localStorage.setItem( 'login-ok', 'true' );
router.push( localStorage.getItem( 'redirect' ) ?? '/app' ); router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
localStorage.removeItem( 'redirect' ); localStorage.removeItem( 'redirect' );

View File

@@ -1,20 +1,35 @@
<template> <template>
<div> <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 class="remote-view">
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%"> <div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
<div class="current-song-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> <span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
<div class="current-song"> <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>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p> <p
<progress max="1000" id="progress" :value="progressBar"></progress> 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> </div>
<div class="song-list-wrapper"> <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"> <div class="song-details-wrapper">
<h3>{{ song.title }}</h3> <h3>{{ song.title }}</h3>
<p>{{ song.artist }}</p> <p>{{ song.artist }}</p>
@@ -32,7 +47,9 @@
<div v-else style="max-width: 80%;"> <div v-else style="max-width: 80%;">
<span class="material-symbols-outlined" style="font-size: 4rem;">wifi_off</span> <span class="material-symbols-outlined" style="font-size: 4rem;">wifi_off</span>
<h1>Couldn't connect!</h1> <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> <p>You may <a href="">reload</a> the page to try again!</p>
</div> </div>
</div> </div>
@@ -41,8 +58,12 @@
<script setup lang="ts"> <script setup lang="ts">
import SocketConnection from '@/scripts/connection'; import SocketConnection from '@/scripts/connection';
import type { Song } from '@/scripts/song'; import type {
import { computed, ref, type Ref } from 'vue'; Song
} from '@/scripts/song';
import {
computed, ref, type Ref
} from 'vue';
const isPlaying = ref( false ); const isPlaying = ref( false );
const playlist: Ref<Song[]> = ref( [] ); const playlist: Ref<Song[]> = ref( [] );
@@ -52,6 +73,7 @@
const hasLoaded = ref( false ); const hasLoaded = ref( false );
const showCouldNotFindRoom = ref( false ); const showCouldNotFindRoom = ref( false );
const playbackStart = ref( 0 ); const playbackStart = ref( 0 );
let timeTracker = 0; let timeTracker = 0;
const conn = new SocketConnection(); const conn = new SocketConnection();
@@ -61,18 +83,22 @@
isPlaying.value = d.playbackStatus; isPlaying.value = d.playbackStatus;
playingSong.value = d.playlistIndex; playingSong.value = d.playlistIndex;
playbackStart.value = d.playbackStart; playbackStart.value = d.playbackStart;
if ( isPlaying.value ) { if ( isPlaying.value ) {
startTimeTracker(); startTimeTracker();
} }
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000; 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; hasLoaded.value = true;
conn.registerListener( 'playlist', ( data ) => { conn.registerListener( 'playlist', data => {
playlist.value = data; playlist.value = data;
} ); } );
conn.registerListener( 'playback', ( data ) => { conn.registerListener( 'playback', data => {
isPlaying.value = data; isPlaying.value = data;
if ( isPlaying.value ) { if ( isPlaying.value ) {
startTimeTracker(); startTimeTracker();
} else { } else {
@@ -80,22 +106,24 @@
} }
} ); } );
conn.registerListener( 'playback-start', ( data ) => { conn.registerListener( 'playback-start', data => {
playbackStart.value = data; playbackStart.value = data;
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000; pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
} ); } );
conn.registerListener( 'playlist-index', ( data ) => { conn.registerListener( 'playlist-index', data => {
playingSong.value = parseInt( data ); playingSong.value = parseInt( data );
} ); } );
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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. The page will reload automatically to try and re-establish connection!' ); alert( `This share was just deleted. It is no longer available.
The page will reload automatically to try and re-establish connection!` );
conn.disconnect(); conn.disconnect();
location.reload(); location.reload();
} ); } );
} ).catch( e => { } )
.catch( e => {
console.error( e ); console.error( e );
showCouldNotFindRoom.value = true; showCouldNotFindRoom.value = true;
} ); } );
@@ -103,49 +131,56 @@
const songQueue = computed( () => { const songQueue = computed( () => {
let ret: Song[] = []; let ret: Song[] = [];
let pos = 0; let pos = 0;
for ( let song in playlist.value ) { for ( let song in playlist.value ) {
if ( pos >= playingSong.value ) { if ( pos >= playingSong.value ) {
ret.push( playlist.value[ song ] ); ret.push( playlist.value[ song ] );
} }
pos += 1; pos += 1;
} }
return ret; return ret;
} ); } );
// TODO: Handle disconnect from updater (=> have it disconnect)
const getTimeUntil = computed( () => { const getTimeUntil = computed( () => {
return ( song: string ) => { return ( song: string ) => {
let timeRemaining = 0; let timeRemaining = 0;
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) { for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
if ( playlist.value[ i ].id == song ) { if ( playlist.value[ i ].id == song ) {
break; break;
} }
timeRemaining += playlist.value[ i ].duration; timeRemaining += playlist.value[ i ].duration;
} }
if ( isPlaying.value ) { if ( isPlaying.value ) {
if ( timeRemaining === 0 ) { if ( timeRemaining === 0 ) {
return 'Currently playing'; return 'Currently playing';
} else { } 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 { } else {
if ( timeRemaining === 0 ) { if ( timeRemaining === 0 ) {
return 'Plays next'; return 'Plays next';
} else { } 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 = () => { const startTimeTracker = () => {
try { try {
clearInterval( timeTracker ); clearInterval( timeTracker );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch ( err ) { /* empty */ } } catch ( err ) { /* empty */ }
timeTracker = setInterval( () => { timeTracker = setInterval( () => {
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000; pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000; progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
if ( isNaN( progressBar.value ) ) { if ( isNaN( progressBar.value ) ) {
progressBar.value = 0; progressBar.value = 0;
} }
@@ -156,11 +191,11 @@
location.reload(); location.reload();
} }
}, 100 ); }, 100 );
} };
const stopTimeTracker = () => { const stopTimeTracker = () => {
clearInterval( timeTracker ); clearInterval( timeTracker );
} };
document.addEventListener( 'visibilitychange', () => { document.addEventListener( 'visibilitychange', () => {
if ( !document.hidden ) { if ( !document.hidden ) {

View File

@@ -1,34 +1,66 @@
<template> <template>
<div> <div>
<span class="anti-tamper material-symbols-outlined" v-if="isAntiTamperEnabled" @click="secureModeInfo( 'toggle' )">lock</span> <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> v-if="isAntiTamperEnabled"
<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> 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 class="remote-view">
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper"> <div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
<div class="current-song-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> <span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
<div class="current-song"> <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>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p> <p
<progress max="1000" id="progress" :value="progressBar"></progress> 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> </div>
<div class="mode-selector-wrapper"> <div class="mode-selector-wrapper">
<select v-model="visualizationSettings" @change="handleAnimationChange()"> <select v-model="visualizationSettings" @change="handleAnimationChange()">
<option value="mic">Microphone (Mic access required)</option> <option value="mic">
<option value="off">No visualization except background</option> Microphone (Mic access required)
</option>
<option value="off">
No visualization except background
</option>
</select> </select>
</div> </div>
<div class="song-list-wrapper"> <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"> <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-symbols-wrapper">
<div class="playing-bar" id="bar-1"></div> <div id="bar-1" class="playing-bar"></div>
<div class="playing-bar" id="bar-2"></div> <div id="bar-2" class="playing-bar"></div>
<div class="playing-bar" id="bar-3"></div> <div id="bar-3" class="playing-bar"></div>
</div> </div>
</div> </div>
<div class="song-details-wrapper"> <div class="song-details-wrapper">
@@ -47,10 +79,12 @@
</div> </div>
<div v-else class="showcase-wrapper"> <div v-else class="showcase-wrapper">
<h1>Couldn't connect!</h1> <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> <p>You may reload the page to try again!</p>
</div> </div>
<div class="background" id="background"> <div id="background" class="background">
<div class="beat-manual"></div> <div class="beat-manual"></div>
</div> </div>
</div> </div>
@@ -59,8 +93,12 @@
<script setup lang="ts"> <script setup lang="ts">
import SocketConnection from '@/scripts/connection'; import SocketConnection from '@/scripts/connection';
import type { Song } from '@/scripts/song'; import type {
import { computed, ref, type Ref } from 'vue'; Song
} from '@/scripts/song';
import {
computed, ref, type Ref
} from 'vue';
import bizualizer from '@/scripts/bizualizer'; import bizualizer from '@/scripts/bizualizer';
const isPlaying = ref( false ); const isPlaying = ref( false );
@@ -71,10 +109,11 @@
const hasLoaded = ref( false ); const hasLoaded = ref( false );
const showCouldNotFindRoom = ref( false ); const showCouldNotFindRoom = ref( false );
const playbackStart = ref( 0 ); const playbackStart = ref( 0 );
let timeTracker = 0; let timeTracker = 0;
const visualizationSettings = ref( 'mic' ); const visualizationSettings = ref( 'mic' );
const isAntiTamperEnabled = ref( false ); const isAntiTamperEnabled = ref( false );
const conn = new SocketConnection(); const conn = new SocketConnection();
conn.connect().then( d => { conn.connect().then( d => {
@@ -82,22 +121,29 @@
isPlaying.value = d.playbackStatus; isPlaying.value = d.playbackStatus;
playingSong.value = d.playlistIndex; playingSong.value = d.playlistIndex;
playbackStart.value = d.playbackStart; playbackStart.value = d.playbackStart;
if ( isPlaying.value ) { if ( isPlaying.value ) {
startTimeTracker(); startTimeTracker();
} }
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000; 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; hasLoaded.value = true;
if ( d.useAntiTamper ) { if ( d.useAntiTamper ) {
isAntiTamperEnabled.value = true; isAntiTamperEnabled.value = true;
notifier(); notifier();
} }
conn.registerListener( 'playlist', ( data ) => {
conn.registerListener( 'playlist', data => {
playlist.value = data; playlist.value = data;
} ); } );
conn.registerListener( 'playback', ( data ) => { conn.registerListener( 'playback', data => {
isPlaying.value = data; isPlaying.value = data;
if ( isPlaying.value ) { if ( isPlaying.value ) {
startTimeTracker(); startTimeTracker();
} else { } else {
@@ -105,12 +151,12 @@
} }
} ); } );
conn.registerListener( 'playback-start', ( data ) => { conn.registerListener( 'playback-start', data => {
playbackStart.value = data; playbackStart.value = data;
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000; pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
} ); } );
conn.registerListener( 'playlist-index', ( data ) => { conn.registerListener( 'playlist-index', data => {
playingSong.value = parseInt( data ); playingSong.value = parseInt( data );
setTimeout( () => { setTimeout( () => {
setBackground(); setBackground();
@@ -118,12 +164,13 @@
} ); } );
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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!' ); alert( 'This share was just deleted. It is no longer available. This page will reload automatically!' );
conn.disconnect(); conn.disconnect();
location.reload(); location.reload();
} ); } );
} ).catch( e => { } )
.catch( e => {
console.error( e ); console.error( e );
showCouldNotFindRoom.value = true; showCouldNotFindRoom.value = true;
} ); } );
@@ -131,45 +178,50 @@
const songQueue = computed( () => { const songQueue = computed( () => {
let ret: Song[] = []; let ret: Song[] = [];
let pos = 0; let pos = 0;
for ( let song in playlist.value ) { for ( let song in playlist.value ) {
if ( pos >= playingSong.value ) { if ( pos >= playingSong.value ) {
ret.push( playlist.value[ song ] ); ret.push( playlist.value[ song ] );
} }
pos += 1; pos += 1;
} }
return ret; return ret;
} ); } );
// TODO: Handle disconnect from updater (=> have it disconnect)
const getTimeUntil = computed( () => { const getTimeUntil = computed( () => {
return ( song: string ) => { return ( song: string ) => {
let timeRemaining = 0; let timeRemaining = 0;
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) { for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
if ( playlist.value[ i ].id == song ) { if ( playlist.value[ i ].id == song ) {
break; break;
} }
timeRemaining += playlist.value[ i ].duration; timeRemaining += playlist.value[ i ].duration;
} }
if ( isPlaying.value ) { if ( isPlaying.value ) {
if ( timeRemaining === 0 ) { if ( timeRemaining === 0 ) {
return 'Currently playing'; return 'Currently playing';
} else { } 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 { } else {
if ( timeRemaining === 0 ) { if ( timeRemaining === 0 ) {
return 'Plays next'; return 'Plays next';
} else { } 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 = () => { const startTimeTracker = () => {
try { try {
clearInterval( timeTracker ); clearInterval( timeTracker );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch ( err ) { /* empty */ } } catch ( err ) { /* empty */ }
setTimeout( () => { setTimeout( () => {
@@ -179,21 +231,23 @@
timeTracker = setInterval( () => { timeTracker = setInterval( () => {
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000; pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000; progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
if ( isNaN( progressBar.value ) ) { if ( isNaN( progressBar.value ) ) {
progressBar.value = 0; progressBar.value = 0;
} }
}, 100 ); }, 100 );
} };
const stopTimeTracker = () => { const stopTimeTracker = () => {
clearInterval( timeTracker ); clearInterval( timeTracker );
handleAnimationChange(); handleAnimationChange();
} };
const animateBeat = () => { const animateBeat = () => {
$( '.beat-manual' ).stop(); $( '.beat-manual' ).stop();
const duration = Math.ceil( 60 / 180 * 500 ) - 50; const duration = Math.ceil( 60 / 180 * 500 ) - 50;
$( '.beat-manual' ).fadeIn( 50 ); $( '.beat-manual' ).fadeIn( 50 );
setTimeout( () => { setTimeout( () => {
$( '.beat-manual' ).fadeOut( duration ); $( '.beat-manual' ).fadeOut( duration );
@@ -202,30 +256,31 @@
$( '.beat-manual' ).stop(); $( '.beat-manual' ).stop();
}, duration ); }, duration );
}, 50 ); }, 50 );
} };
const handleAnimationChange = () => { const handleAnimationChange = () => {
if ( visualizationSettings.value === 'mic' && isPlaying.value ) { if ( visualizationSettings.value === 'mic' && isPlaying.value ) {
bizualizer.subscribeToBeatUpdate( animateBeat ); bizualizer.subscribeToBeatUpdate( animateBeat );
} else { } else {
bizualizer.unsubscribeFromBeatUpdate() bizualizer.unsubscribeFromBeatUpdate();
}
} }
};
const setBackground = () => { const setBackground = () => {
bizualizer.createBackground().then( bg => { bizualizer.createBackground().then( bg => {
$( '#background' ).css( 'background', bg ); $( '#background' ).css( 'background', bg );
} ); } );
} };
const notifier = () => { const notifier = () => {
Notification.requestPermission(); 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' ); 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 // Detect if window is currently in focus
window.onblur = () => { window.onblur = () => {
sendNotification(); sendNotification();
} };
// Detect if browser window becomes hidden (also with blur event) // Detect if browser window becomes hidden (also with blur event)
document.onvisibilitychange = () => { document.onvisibilitychange = () => {
@@ -233,18 +288,19 @@
sendNotification(); sendNotification();
} }
}; };
} };
const sendNotification = () => { const sendNotification = () => {
new Notification( 'YOU ARE UNDER SURVEILLANCE', { new Notification( 'YOU ARE UNDER SURVEILLANCE', {
body: 'Please return to the original webpage immediately!', 'body': 'Please return to the original webpage immediately!',
requireInteraction: true, 'requireInteraction': true,
} ); } );
conn.emit( 'tampering', '' ); conn.emit( 'tampering', '' );
} };
const isShowingSecureModeInfo = ref( false ); const isShowingSecureModeInfo = ref( false );
const secureModeInfo = ( action: string ) => { const secureModeInfo = ( action: string ) => {
if ( action === 'toggle' ) { if ( action === 'toggle' ) {
isShowingSecureModeInfo.value = !isShowingSecureModeInfo.value; isShowingSecureModeInfo.value = !isShowingSecureModeInfo.value;
@@ -253,7 +309,7 @@
} else { } else {
isShowingSecureModeInfo.value = false; isShowingSecureModeInfo.value = false;
} }
} };
</script> </script>
<style scoped> <style scoped>

View File

@@ -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>

View File

@@ -3,7 +3,6 @@
"include": [ "include": [
"vite.config.*", "vite.config.*",
"vitest.config.*", "vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*", "nightwatch.conf.*",
"playwright.config.*" "playwright.config.*"
], ],
@@ -14,6 +13,6 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"types": ["node"] "types": ["node", "jquery"]
} }
} }