mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 04:54:23 +00:00
Compare commits
35 Commits
V3.0.0
...
e0dcfa6964
| Author | SHA1 | Date | |
|---|---|---|---|
| e0dcfa6964 | |||
|
|
69d2db8c37 | ||
| 9a347c9206 | |||
| 60ea4669ab | |||
| 669cc620bf | |||
| 64d086dec4 | |||
| 1d714da494 | |||
| d63df5898b | |||
| bd18636141 | |||
| 51f0b5639a | |||
| 6a6b06a994 | |||
| e2e27e640f | |||
| ac575adedb | |||
| d15366e572 | |||
| 25cfc80925 | |||
| 952ef0ee85 | |||
| 80c497b80a | |||
| 8778740454 | |||
| 74075c2919 | |||
| 3e13a4ebf5 | |||
| 84b640ee5e | |||
| 0315241d76 | |||
| 6e93cfdf2c | |||
| 6b9d556e57 | |||
| 025c7ed111 | |||
| 057ea67a95 | |||
| 16543fb577 | |||
| f0c538126d | |||
| b0a1f9a538 | |||
|
|
9fac37203d | ||
| b10aeed1f8 | |||
| b8faeef189 | |||
| 4ef98f80cf | |||
| c9f487b981 | |||
| dc2327af33 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules
|
||||
apple_private_key.p8
|
||||
musicplayerv2-server.zip
|
||||
dist
|
||||
package-lock.json
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
714
MusicPlayerV2-GUI/eslint.config.mjs
Normal file
714
MusicPlayerV2-GUI/eslint.config.mjs
Normal file
@@ -0,0 +1,714 @@
|
||||
import vue from 'eslint-plugin-vue';
|
||||
import eslint from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import typescript from '@typescript-eslint/eslint-plugin';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const style = {
|
||||
'plugins': {
|
||||
'@stylistic': stylistic,
|
||||
'@stylistic/js': stylistic,
|
||||
'@stylistic/ts': stylistic,
|
||||
},
|
||||
'files': [
|
||||
'**/*.ts',
|
||||
'**/*.js',
|
||||
'**/*.mjs',
|
||||
'**/*.cjs',
|
||||
'**/*.tsx',
|
||||
'**/*.jsx'
|
||||
],
|
||||
'rules': {
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
"ignoreCase": false,
|
||||
"ignoreDeclarationSort": false,
|
||||
"ignoreMemberSort": false,
|
||||
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
|
||||
"allowSeparatedGroups": false
|
||||
}
|
||||
],
|
||||
// Formatting
|
||||
'@stylistic/array-bracket-newline': [
|
||||
'error',
|
||||
{
|
||||
'multiline': true,
|
||||
'minItems': 2
|
||||
}
|
||||
],
|
||||
'@stylistic/array-bracket-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/array-element-newline': [
|
||||
'error',
|
||||
{
|
||||
'multiline': true,
|
||||
'minItems': 2
|
||||
}
|
||||
],
|
||||
'@stylistic/arrow-parens': [
|
||||
'error',
|
||||
'as-needed'
|
||||
],
|
||||
'@stylistic/arrow-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': true,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/block-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/brace-style': [
|
||||
'error',
|
||||
'1tbs'
|
||||
],
|
||||
'@stylistic/comma-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': false,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/comma-style': [
|
||||
'error',
|
||||
'last'
|
||||
],
|
||||
'@stylistic/dot-location': [
|
||||
'error',
|
||||
'property'
|
||||
],
|
||||
'@stylistic/eol-last': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/function-call-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'@stylistic/function-paren-newline': [
|
||||
'error',
|
||||
{
|
||||
'minItems': 3
|
||||
}
|
||||
],
|
||||
'@stylistic/function-call-argument-newline': [
|
||||
'error',
|
||||
'consistent'
|
||||
],
|
||||
'@stylistic/implicit-arrow-linebreak': [
|
||||
'error',
|
||||
'beside'
|
||||
],
|
||||
'@stylistic/indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'@stylistic/key-spacing': [
|
||||
'error',
|
||||
{
|
||||
'beforeColon': false,
|
||||
'afterColon': true
|
||||
}
|
||||
],
|
||||
'@stylistic/keyword-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': true,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/lines-between-class-members': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/max-len': [
|
||||
'warn',
|
||||
{
|
||||
'code': 120,
|
||||
'comments': 140,
|
||||
'ignoreComments': false,
|
||||
'ignoreUrls': true,
|
||||
'ignoreStrings': false
|
||||
}
|
||||
],
|
||||
'@stylistic/new-parens': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/newline-per-chained-call': [ 'error' ],
|
||||
'@stylistic/no-extra-parens': [
|
||||
'error',
|
||||
'all',
|
||||
{
|
||||
'nestedBinaryExpressions': false,
|
||||
'ternaryOperandBinaryExpressions': false,
|
||||
'ignoreJSX': 'multi-line',
|
||||
'nestedConditionalExpressions': false
|
||||
}
|
||||
],
|
||||
'@stylistic/no-extra-semi': 'error',
|
||||
'@stylistic/no-floating-decimal': 'error',
|
||||
'@stylistic/no-mixed-operators': 'error',
|
||||
'@stylistic/no-mixed-spaces-and-tabs': 'error',
|
||||
'@stylistic/no-multi-spaces': 'error',
|
||||
'@stylistic/no-multiple-empty-lines': [
|
||||
'error',
|
||||
{
|
||||
'max': 3,
|
||||
'maxEOF': 2
|
||||
}
|
||||
],
|
||||
'@stylistic/no-tabs': 'error',
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
'@stylistic/no-whitespace-before-property': 'error',
|
||||
'@stylistic/object-curly-newline': [
|
||||
'error',
|
||||
{
|
||||
'multiline': true,
|
||||
'minProperties': 1
|
||||
}
|
||||
],
|
||||
'@stylistic/object-curly-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/object-property-newline': 'error',
|
||||
'@stylistic/operator-linebreak': [
|
||||
'error',
|
||||
'before'
|
||||
],
|
||||
'@stylistic/one-var-declaration-per-line': 'error',
|
||||
'@stylistic/padded-blocks': [
|
||||
'error',
|
||||
{
|
||||
'blocks': 'never',
|
||||
'classes': 'always',
|
||||
'switches': 'never',
|
||||
}
|
||||
],
|
||||
|
||||
// Padding lines. The most in-depth part of this config
|
||||
'@stylistic/padding-line-between-statements': [
|
||||
'error',
|
||||
// Variables, Constants
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'var',
|
||||
'next': 'var'
|
||||
},
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'let',
|
||||
'next': 'let'
|
||||
},
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'const',
|
||||
'next': 'const'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'var',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'cjs-import',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'import',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'let',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'cjs-import',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'import',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'const',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'cjs-import',
|
||||
'class',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'import',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
// Import
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'import',
|
||||
'next': 'import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'cjs-import',
|
||||
'next': 'cjs-import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
],
|
||||
'next': 'cjs-import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'cjs-import',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
],
|
||||
'next': 'import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'import',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
// If
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'if'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'if',
|
||||
'next': '*'
|
||||
},
|
||||
// For
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'for'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'for',
|
||||
'next': '*'
|
||||
},
|
||||
// While
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'while'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'while',
|
||||
'next': '*'
|
||||
},
|
||||
// Functions
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'function'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'function',
|
||||
'next': '*'
|
||||
},
|
||||
// Block Statements
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'block-like'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'block-like',
|
||||
'next': '*'
|
||||
},
|
||||
// Switch
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'switch'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'switch',
|
||||
'next': '*'
|
||||
},
|
||||
// Try-Catch
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'try'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'try',
|
||||
'next': '*'
|
||||
},
|
||||
// Throw
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'throw'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'throw',
|
||||
'next': '*'
|
||||
},
|
||||
// Return
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'return',
|
||||
'next': '*'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'return'
|
||||
},
|
||||
// Export
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'export'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'export',
|
||||
'next': '*'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'cjs-export'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'cjs-export',
|
||||
'next': '*'
|
||||
},
|
||||
// Classes
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'class'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'class',
|
||||
'next': '*'
|
||||
},
|
||||
],
|
||||
'@stylistic/quote-props': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'@stylistic/rest-spread-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'@stylistic/semi': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/semi-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': false,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/semi-style': [
|
||||
'error',
|
||||
'last'
|
||||
],
|
||||
'@stylistic/space-before-blocks': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/space-before-function-paren': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/space-in-parens': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/space-infix-ops': [
|
||||
'error',
|
||||
{
|
||||
'int32Hint': false
|
||||
}
|
||||
],
|
||||
'@stylistic/space-unary-ops': 'error',
|
||||
'@stylistic/spaced-comment': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/template-curly-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/switch-colon-spacing': 'error',
|
||||
'@stylistic/wrap-iife': [
|
||||
'error',
|
||||
'inside'
|
||||
],
|
||||
'@stylistic/wrap-regex': 'error',
|
||||
'@stylistic/ts/type-annotation-spacing': 'error',
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
export default tseslint.config(
|
||||
// Base JavaScript rules
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
style,
|
||||
|
||||
// Vue support (including TS and JSX inside SFCs)
|
||||
{
|
||||
'files': [ '**/*.vue' ],
|
||||
'languageOptions': {
|
||||
'sourceType': 'module',
|
||||
'ecmaVersion': 'latest',
|
||||
'globals': globals.browser,
|
||||
'parserOptions': {
|
||||
'parser': tseslint.parser,
|
||||
},
|
||||
},
|
||||
'plugins': {
|
||||
'vue': vue,
|
||||
'@stylistic': stylistic,
|
||||
'@stylistic/js': stylistic,
|
||||
'@stylistic/ts': stylistic,
|
||||
'@typescript-eslint': typescript,
|
||||
},
|
||||
'extends': [
|
||||
eslint.configs.recommended,
|
||||
...vue.configs['flat/recommended']
|
||||
],
|
||||
'rules': {
|
||||
...typescript.configs.recommended.rules,
|
||||
...style.rules,
|
||||
|
||||
// Vue specific rules
|
||||
'@stylistic/indent': 'off',
|
||||
'vue/html-indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'vue/html-comment-indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'vue/script-indent': [
|
||||
'error',
|
||||
4,
|
||||
{
|
||||
'baseIndent': 1,
|
||||
'switchCase': 1
|
||||
}
|
||||
],
|
||||
'vue/html-self-closing': [
|
||||
'error',
|
||||
{
|
||||
'html': {
|
||||
'void': 'never',
|
||||
'normal': 'never',
|
||||
'component': 'always'
|
||||
},
|
||||
'svg': 'always',
|
||||
'math': 'never'
|
||||
}
|
||||
],
|
||||
'vue/max-attributes-per-line': [
|
||||
'error',
|
||||
{
|
||||
'singleline': 3,
|
||||
'multiline': 1,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,22 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
|
||||
<!-- TODO: Update URL -->
|
||||
<script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js"></script>
|
||||
<script src="https://static.janishutz.com/libs/jquery/jquery.min.js"></script>
|
||||
<script src="https://id.janishutz.com/sdk/sdk.min.js"></script>
|
||||
<!-- <script src="http://localhost:8080/sdk/sdk.min.js"></script> -->
|
||||
<title>MusicPlayer</title>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>This application requires JavaScript to work!</noscript>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
localStorage.setItem( 'music-player-config', 'sse' );// Or 'ws'
|
||||
localStorage.setItem('music-player-config', 'sse');// Or 'ws'
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
5014
MusicPlayerV2-GUI/package-lock.json
generated
5014
MusicPlayerV2-GUI/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,31 +13,34 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@janishutz/login-sdk-browser": "^1.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@melloware/coloris": "^0.24.0",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"buffer": "^6.0.3",
|
||||
"colorthief": "^2.2.0",
|
||||
"music-metadata-browser": "^2.5.10",
|
||||
"colorthief": "^2.6.0",
|
||||
"music-metadata": "^11.9.0",
|
||||
"musickit-typescript": "^1.2.4",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@stylistic/eslint-plugin": "^5.4.0",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/node": "^20.11.10",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"sass-embedded": "^1.92.0",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.11",
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"vite": "^7.1.4",
|
||||
"vue-tsc": "^2.0.29"
|
||||
}
|
||||
}
|
||||
|
||||
193
MusicPlayerV2-GUI/public/bar-config.json
Normal file
193
MusicPlayerV2-GUI/public/bar-config.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"ages": {
|
||||
"below": "Orange",
|
||||
"16-18": "Yellow",
|
||||
"18+": "Turquoise"
|
||||
},
|
||||
"offering": {
|
||||
"big-bar": {
|
||||
"offering": {
|
||||
"softdrinks": {
|
||||
"name": "Softdrinks",
|
||||
"price": 400,
|
||||
"id": "softdrinks"
|
||||
},
|
||||
"energy": {
|
||||
"name": "Energy",
|
||||
"price": 400,
|
||||
"id": "energy"
|
||||
},
|
||||
"mate": {
|
||||
"name": "Mate",
|
||||
"price": 500,
|
||||
"id": "mate",
|
||||
"depot": 200
|
||||
},
|
||||
"sparkly-water": {
|
||||
"name": "Mineralwasser mit",
|
||||
"price": 300,
|
||||
"id": "sparkly-water",
|
||||
"showLine": true
|
||||
},
|
||||
"rose": {
|
||||
"name": "Rosé",
|
||||
"price": 1500,
|
||||
"id": "rose",
|
||||
"depot": 200
|
||||
},
|
||||
"red": {
|
||||
"name": "Rotwein",
|
||||
"price": 2000,
|
||||
"id": "red",
|
||||
"depot": 200
|
||||
},
|
||||
"wine-glasses": {
|
||||
"name": "Weingläser",
|
||||
"price": 0,
|
||||
"id": "wine-glasses",
|
||||
"depot": 200
|
||||
},
|
||||
"appenzeller": {
|
||||
"name": "Appenzeller Vollmond",
|
||||
"price": 500,
|
||||
"id": "appenzeller",
|
||||
"depot": 200
|
||||
},
|
||||
"feldschloesschen": {
|
||||
"name": "Feldschlösschen",
|
||||
"price": 500,
|
||||
"id": "feldschloesschen",
|
||||
"depot": 200
|
||||
},
|
||||
"sommersby": {
|
||||
"name": "Sommersby",
|
||||
"price": 500,
|
||||
"id": "sommersby",
|
||||
"depot": 200
|
||||
},
|
||||
"jever-fun": {
|
||||
"name": "Jever Fun",
|
||||
"price": 400,
|
||||
"id": "jever-fun",
|
||||
"depot": 200
|
||||
},
|
||||
"trojka-ice": {
|
||||
"name": "Trojka Ice",
|
||||
"price": 600,
|
||||
"id": "trojka-ice",
|
||||
"depot": 200,
|
||||
"showLine": true
|
||||
},
|
||||
"vodka-red-energy": {
|
||||
"name": "Vodka Rot Energy",
|
||||
"price": 800,
|
||||
"id": "vodka-red-energy"
|
||||
},
|
||||
"vodka-green-citro": {
|
||||
"name": "Vodka Grün Citro",
|
||||
"price": 800,
|
||||
"id": "vodka-green-citro"
|
||||
},
|
||||
"vodka-white-energy": {
|
||||
"name": "Vodka White Energy",
|
||||
"price": 900,
|
||||
"id": "vodka-white-energy"
|
||||
},
|
||||
"gin-tonic": {
|
||||
"name": "Gin Tonic",
|
||||
"price": 900,
|
||||
"id": "gin-tonic"
|
||||
},
|
||||
"rum-cola": {
|
||||
"name": "Rum Cola",
|
||||
"price": 900,
|
||||
"id": "rum-cola"
|
||||
},
|
||||
"whiskey-cola": {
|
||||
"name": "Whiskey Cola",
|
||||
"price": 900,
|
||||
"id": "whiskey-cola"
|
||||
},
|
||||
"mate-mit-schuss": {
|
||||
"name": "Mate mit Schuss",
|
||||
"price": 1200,
|
||||
"id": "mate-mit-schuss",
|
||||
"depot": 200,
|
||||
"showLine": true
|
||||
},
|
||||
"poseidon": {
|
||||
"name": "Poseidon",
|
||||
"price": 900,
|
||||
"id": "poseidon"
|
||||
},
|
||||
"arielle": {
|
||||
"name": "Arielle",
|
||||
"price": 900,
|
||||
"id": "arielle"
|
||||
},
|
||||
"pearl-driver": {
|
||||
"name": "Pearl Driver",
|
||||
"price": 400,
|
||||
"id": "pearl-driver"
|
||||
}
|
||||
},
|
||||
"name": "Poseidon's Quelle",
|
||||
"id": "big-bar"
|
||||
},
|
||||
"small-bar": {
|
||||
"offering": {
|
||||
"softdrinks": {
|
||||
"name": "Softdrinks (Alle)",
|
||||
"price": 300,
|
||||
"id": "softdrinks",
|
||||
"showLine": true
|
||||
},
|
||||
"appenzeller": {
|
||||
"name": "Appenzeller Vollmond",
|
||||
"price": 500,
|
||||
"id": "appenzeller",
|
||||
"depot": 200
|
||||
},
|
||||
"feldschloesschen": {
|
||||
"name": "Feldschlösschen",
|
||||
"price": 500,
|
||||
"id": "feldschloesschen",
|
||||
"depot": 200
|
||||
},
|
||||
"sommersby": {
|
||||
"name": "Sommersby",
|
||||
"price": 500,
|
||||
"id": "sommersby",
|
||||
"depot": 200
|
||||
},
|
||||
"jever-fun": {
|
||||
"name": "Jever Fun",
|
||||
"price": 400,
|
||||
"id": "jever-fun",
|
||||
"depot": 200,
|
||||
"showLine": true
|
||||
},
|
||||
"rose": {
|
||||
"name": "Rosé",
|
||||
"price": 1500,
|
||||
"id": "rose",
|
||||
"depot": 200
|
||||
},
|
||||
"red": {
|
||||
"name": "Rotwein",
|
||||
"price": 2000,
|
||||
"id": "red",
|
||||
"depot": 200
|
||||
},
|
||||
"wine-glasses": {
|
||||
"name": "Weingläser",
|
||||
"price": 0,
|
||||
"id": "wine-glasses",
|
||||
"depot": 200
|
||||
}
|
||||
},
|
||||
"name": "Seepferdchenbar",
|
||||
"id": "small-bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,52 @@
|
||||
<!--
|
||||
* libreevent - App.vue
|
||||
*
|
||||
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button @click="changeTheme();" id="themeSelector" title="Toggle between light and dark mode"><span class="material-symbols-outlined" v-html="theme"></span></button>
|
||||
<router-view v-slot="{ Component, route }" id="main-view">
|
||||
<button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="material-symbols-outlined" v-html="theme"></span>
|
||||
</button>
|
||||
<router-view id="main-view" v-slot="{ Component, route }">
|
||||
<transition :name="route.meta.transition ? String( route.meta.transition ) : 'fade'" mode="out-in">
|
||||
<component :is="Component"></component>
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
RouterView
|
||||
} from 'vue-router';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
|
||||
const theme = ref( '☼' );
|
||||
|
||||
const changeTheme = () => {
|
||||
if ( theme.value === '☽' ) {
|
||||
document.documentElement.classList.remove( 'dark' );
|
||||
document.documentElement.classList.add( 'light' );
|
||||
localStorage.setItem( 'theme', '☼' );
|
||||
theme.value = '☼';
|
||||
} else if ( theme.value === '☼' ) {
|
||||
document.documentElement.classList.remove( 'light' );
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
localStorage.setItem( 'theme', '☽' );
|
||||
theme.value = '☽';
|
||||
}
|
||||
};
|
||||
|
||||
theme.value = localStorage.getItem( 'theme' ) ?? '';
|
||||
|
||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === '☽' ) {
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
theme.value = '☽';
|
||||
} else {
|
||||
document.documentElement.classList.add( 'light' );
|
||||
theme.value = '☼';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: var( --background-color );
|
||||
@@ -178,33 +207,3 @@
|
||||
background-position: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
const theme = ref( 'light_mode' );
|
||||
|
||||
const changeTheme = () => {
|
||||
if ( theme.value === 'dark_mode' ) {
|
||||
document.documentElement.classList.remove( 'dark' );
|
||||
document.documentElement.classList.add( 'light' );
|
||||
localStorage.setItem( 'theme', 'light_mode' );
|
||||
theme.value = 'light_mode';
|
||||
} else if ( theme.value === 'light_mode' ) {
|
||||
document.documentElement.classList.remove( 'light' );
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
localStorage.setItem( 'theme', 'dark_mode' );
|
||||
theme.value = 'dark_mode';
|
||||
}
|
||||
}
|
||||
|
||||
theme.value = localStorage.getItem( 'theme' ) ?? '';
|
||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === 'dark_mode' ) {
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
theme.value = 'dark_mode';
|
||||
} else {
|
||||
document.documentElement.classList.add( 'light' );
|
||||
theme.value = 'light_mode';
|
||||
}
|
||||
</script>
|
||||
@@ -1,24 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Library</h1>
|
||||
<playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )" :is-logged-in="$props.isLoggedIn"
|
||||
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"></playlistsView>
|
||||
<playlistsView
|
||||
:playlists="$props.playlists"
|
||||
:is-logged-in="$props.isLoggedIn"
|
||||
@selected-playlist="( id ) => selectPlaylist( id )"
|
||||
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import playlistsView from '@/components/playlistsView.vue';
|
||||
import type { ReadFile } from '@/scripts/song';
|
||||
import type {
|
||||
ReadFile
|
||||
} from '@/scripts/song';
|
||||
|
||||
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] );
|
||||
const emits = defineEmits( [
|
||||
'selected-playlist',
|
||||
'custom-playlist'
|
||||
] );
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
emits( 'selected-playlist', id );
|
||||
}
|
||||
};
|
||||
|
||||
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||
emits( 'custom-playlist', playlist );
|
||||
}
|
||||
};
|
||||
|
||||
defineProps( {
|
||||
'playlists': {
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
<div id="notifications">
|
||||
<div class="message-box" :class="[ location, size ]" :style="'z-index: ' + ( messageType === 'hide' ? '-1' : '1000' )">
|
||||
<div class="message-container" :class="messageType">
|
||||
<button @click="handleNotifications();" class="close-notification"><span class="material-symbols-outlined close-notification-icon">close</span></button>
|
||||
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
|
||||
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
|
||||
<p class="message" @click="notificationAction()">{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}</p>
|
||||
<button class="close-notification" @click="handleNotifications();">
|
||||
<span class="material-symbols-outlined close-notification-icon">close</span>
|
||||
</button>
|
||||
<span v-if="messageType == 'hide'" class="material-symbols-outlined types hide">question_mark</span>
|
||||
<span v-else-if="messageType == 'ok'" class="material-symbols-outlined types" style="background-color: green;">done</span>
|
||||
<span v-else-if="messageType == 'error'" class="material-symbols-outlined types" style="background-color: red;">close</span>
|
||||
<span v-else-if="messageType == 'progress'" class="material-symbols-outlined types progress-spinner" style="background-color: blue;">progress_activity</span>
|
||||
<span v-else-if="messageType == 'info'" class="material-symbols-outlined types" style="background-color: lightblue;">info</span>
|
||||
<span v-else-if="messageType == 'warning'" class="material-symbols-outlined types" style="background-color: orangered;">warning</span>
|
||||
<p class="message" @click="notificationAction()">
|
||||
{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}
|
||||
</p>
|
||||
<div :class="'countdown countdown-' + messageType" :style="'width: ' + ( 100 - ( currentTime - notificationDisplayStartTime ) / ( notifications[ currentDID ] ? notifications[ currentDID ].showDuration : 1 ) / 10 ) + '%'"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,28 +23,30 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import router from '@/router';
|
||||
import { onUnmounted, ref, type Ref } from 'vue';
|
||||
import {
|
||||
type Ref, onUnmounted, ref
|
||||
} from 'vue';
|
||||
|
||||
defineProps( {
|
||||
location: {
|
||||
type: String,
|
||||
'location': {
|
||||
'type': String,
|
||||
'default': 'topleft',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
'size': {
|
||||
'type': String,
|
||||
'default': 'default',
|
||||
}
|
||||
// Size options: small, default (default option), big, bigger, huge
|
||||
} );
|
||||
|
||||
interface Notification {
|
||||
message: string;
|
||||
showDuration: number;
|
||||
messageType: string;
|
||||
priority: string;
|
||||
id: number;
|
||||
redirect?: string;
|
||||
openInNewTab?: boolean;
|
||||
'message': string;
|
||||
'showDuration': number;
|
||||
'messageType': string;
|
||||
'priority': string;
|
||||
'id': number;
|
||||
'redirect'?: string;
|
||||
'openInNewTab'?: boolean;
|
||||
}
|
||||
|
||||
interface NotificationList {
|
||||
@@ -51,11 +57,17 @@
|
||||
const queue: Ref<number[]> = ref( [] );
|
||||
const currentDID: Ref<number> = ref( 0 );
|
||||
const messageType: Ref<string> = ref( 'hide' );
|
||||
const currentID = ref( { 'critical': 0, 'medium': 1000, 'low': 10000 } );
|
||||
const currentID = ref( {
|
||||
'critical': 0,
|
||||
'medium': 1000,
|
||||
'low': 10000
|
||||
} );
|
||||
const notificationDisplayStartTime: Ref<number> = ref( 0 );
|
||||
const currentTime: Ref<number> = ref( 0 );
|
||||
|
||||
let progressBar = 0;
|
||||
let notificationTimeout = 0;
|
||||
|
||||
const notificationAction = () => {
|
||||
if ( notifications.value[ currentDID.value ] ) {
|
||||
if ( notifications.value[ currentDID.value ].redirect ) {
|
||||
@@ -76,7 +88,9 @@
|
||||
* @param {string} priority The priority of the message: 'low', 'normal', 'critical'
|
||||
* @returns {number}
|
||||
*/
|
||||
const createNotification = ( message: string, showDuration: number, msgType: string, priority: string, redirect?: string, openInNewTab?: boolean ): number => {
|
||||
const createNotification = (
|
||||
message: string, showDuration: number, msgType: string, priority: string, redirect?: string, openInNewTab?: boolean
|
||||
): number => {
|
||||
/*
|
||||
Takes a notification options array that contains: message, showDuration (in seconds), msgType (ok, error, progress, info) and priority (low, normal, critical).
|
||||
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
|
||||
@@ -94,14 +108,25 @@
|
||||
currentID.value[ 'low' ] += 1;
|
||||
id = currentID.value[ 'low' ];
|
||||
}
|
||||
notifications.value[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': msgType, 'priority': priority, 'id': id, redirect: redirect, openInNewTab: openInNewTab };
|
||||
|
||||
notifications.value[ id ] = {
|
||||
'message': message,
|
||||
'showDuration': showDuration,
|
||||
'messageType': msgType,
|
||||
'priority': priority,
|
||||
'id': id,
|
||||
'redirect': redirect,
|
||||
'openInNewTab': openInNewTab
|
||||
};
|
||||
queue.value.push( id );
|
||||
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
|
||||
|
||||
if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) || messageType.value === 'hide' ) {
|
||||
handleNotifications();
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a notification's message after creating it
|
||||
@@ -113,7 +138,7 @@
|
||||
if ( notifications.value[ id ] ) {
|
||||
notifications.value[ id ].message = message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
@@ -127,26 +152,31 @@
|
||||
} catch ( error ) {
|
||||
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
||||
}
|
||||
|
||||
try {
|
||||
queue.value.splice( queue.value.indexOf( id ), 1 );
|
||||
} catch {
|
||||
console.debug( 'queue empty' );
|
||||
}
|
||||
|
||||
if ( currentDID.value == id ) {
|
||||
try {
|
||||
clearTimeout( notificationTimeout );
|
||||
} catch (err) { /* empty */ }
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
handleNotifications();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifications = () => {
|
||||
notificationDisplayStartTime.value = new Date().getTime();
|
||||
queue.value.sort();
|
||||
|
||||
if ( queue.value.length > 0 ) {
|
||||
if ( currentDID.value !== 0 ) {
|
||||
delete notifications.value[ currentDID.value ];
|
||||
}
|
||||
|
||||
currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ];
|
||||
messageType.value = notifications.value[ queue.value[ 0 ] ].messageType;
|
||||
queue.value.reverse();
|
||||
@@ -158,22 +188,24 @@
|
||||
} else {
|
||||
try {
|
||||
clearInterval( progressBar );
|
||||
} catch (err) { /* empty */ }
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
messageType.value = 'hide';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const progressBarHandler = () => {
|
||||
currentTime.value = new Date().getTime();
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted( () => {
|
||||
try {
|
||||
clearInterval( progressBar );
|
||||
} catch (err) { /* empty */ }
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
try {
|
||||
clearInterval( notificationTimeout );
|
||||
} catch (err) { /* empty */ }
|
||||
} catch ( err ) { /* empty */ }
|
||||
} );
|
||||
|
||||
defineExpose( {
|
||||
|
||||
@@ -4,51 +4,109 @@
|
||||
<h3>WARNING!</h3>
|
||||
<p>A client display is being tampered with!</p>
|
||||
<p>A desktop notification with a warning has already been dispatched.</p>
|
||||
<button @click="dismissNotification()" class="simple-button">Ok</button>
|
||||
<button class="simple-button" @click="dismissNotification()">
|
||||
Ok
|
||||
</button>
|
||||
|
||||
<div class="flash"></div>
|
||||
</div>
|
||||
<div class="player">
|
||||
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
||||
<div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" v-if="coverArt === ''">
|
||||
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" v-else>
|
||||
<img
|
||||
v-if="coverArt === ''"
|
||||
src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png"
|
||||
alt="MusicPlayer Logo"
|
||||
class="logo-player"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
:src="coverArt"
|
||||
alt="MusicPlayer Logo"
|
||||
class="logo-player"
|
||||
>
|
||||
<div class="name-time">
|
||||
<p class="song-name">{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i></p>
|
||||
<div class="playback" v-if="!isShowingFullScreenPlayer">
|
||||
<p class="song-name">
|
||||
{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i>
|
||||
</p>
|
||||
<div v-if="!isShowingFullScreenPlayer" class="playback">
|
||||
<div class="playback-pos-wrapper">
|
||||
<p class="playback-pos">{{ nicePlaybackPos }}</p>
|
||||
<p class="playback-pos">
|
||||
{{ nicePlaybackPos }}
|
||||
</p>
|
||||
<p> / </p>
|
||||
<p class="playback-duration">{{ niceDuration }}</p>
|
||||
<p class="playback-duration">
|
||||
{{ niceDuration }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="'controls-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" :style="playlist.length > 0 ? '' : 'pointer-events: none'">
|
||||
<div class="main-controls">
|
||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous" v-if="isShowingFullScreenPlayer">skip_previous</span>
|
||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'" v-if="isShowingFullScreenPlayer">replay_10</span>
|
||||
<span class="material-symbols-outlined controls" v-if="isPlaying" @click="playPause()" id="play-pause">pause</span>
|
||||
<span class="material-symbols-outlined controls" v-else @click="playPause()" id="play-pause">play_arrow</span>
|
||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'forward' )" :style="'rotate: ' + 360 * clickCountForward + 'deg;'" v-if="isShowingFullScreenPlayer">forward_10</span>
|
||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'next' )" id="next">skip_next</span>
|
||||
<span
|
||||
v-if="isShowingFullScreenPlayer"
|
||||
id="previous"
|
||||
class="material-symbols-outlined controls next-previous"
|
||||
@click="control( 'previous' )"
|
||||
>skip_previous</span>
|
||||
<span
|
||||
v-if="isShowingFullScreenPlayer"
|
||||
class="material-symbols-outlined controls forward-back"
|
||||
:style="'rotate: -' + 360 * clickCountBack + 'deg;'"
|
||||
@click="control( 'back' )"
|
||||
>replay_10</span>
|
||||
<span
|
||||
v-if="isPlaying"
|
||||
id="play-pause"
|
||||
class="material-symbols-outlined controls"
|
||||
@click="playPause()"
|
||||
>pause</span>
|
||||
<span
|
||||
v-else
|
||||
id="play-pause"
|
||||
class="material-symbols-outlined controls"
|
||||
@click="playPause()"
|
||||
>play_arrow</span>
|
||||
<span
|
||||
v-if="isShowingFullScreenPlayer"
|
||||
class="material-symbols-outlined controls forward-back"
|
||||
:style="'rotate: ' + 360 * clickCountForward + 'deg;'"
|
||||
@click="control( 'forward' )"
|
||||
>forward_10</span>
|
||||
<span id="next" class="material-symbols-outlined controls next-previous" @click="control( 'next' )">skip_next</span>
|
||||
</div>
|
||||
|
||||
<div class="slider-wrapper" v-if="isShowingFullScreenPlayer">
|
||||
<div v-if="isShowingFullScreenPlayer" class="slider-wrapper">
|
||||
<div class="slider-pb-pos">
|
||||
<p class="playback-pos">{{ nicePlaybackPos }}</p>
|
||||
<p class="playback-duration" @click="toggleRemaining()" title="Toggle between remaining time and song duration">{{ niceDuration }}</p>
|
||||
<p class="playback-pos">
|
||||
{{ nicePlaybackPos }}
|
||||
</p>
|
||||
<p class="playback-duration" title="Toggle between remaining time and song duration" @click="toggleRemaining()">
|
||||
{{ niceDuration }}
|
||||
</p>
|
||||
</div>
|
||||
<sliderView :position="pos" :active="true" :duration="duration" name="main" @pos="( pos ) => goToPos( pos )"></sliderView>
|
||||
<sliderView
|
||||
:position="pos"
|
||||
:active="true"
|
||||
:duration="duration"
|
||||
name="main"
|
||||
@pos="( pos ) => goToPos( pos )"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="shuffle-repeat" v-if="isShowingFullScreenPlayer">
|
||||
<span class="material-symbols-outlined controls" @click="control( 'repeat' )" style="margin-right: auto;">repeat{{ repeatMode }}</span>
|
||||
<div v-if="isShowingFullScreenPlayer" class="shuffle-repeat">
|
||||
<span class="material-symbols-outlined controls" style="margin-right: auto;" @click="control( 'repeat' )">repeat{{ repeatMode }}</span>
|
||||
<div style="margin-right: auto; pointer-events: all;">
|
||||
<span class="material-symbols-outlined controls" @click="control( 'start-share' )" title="Share your playlist on a public playlist page (opens a configuration window)" v-if="!isConnectedToNotifier">share</span>
|
||||
<span
|
||||
v-if="!isConnectedToNotifier"
|
||||
class="material-symbols-outlined controls"
|
||||
title="Share your playlist on a public playlist page (opens a configuration window)"
|
||||
@click="control( 'start-share' )"
|
||||
>share</span>
|
||||
<div v-else>
|
||||
<span class="material-symbols-outlined controls" @click="control( 'stop-share' )" title="Stop sharing your playlist on a public playlist page">close</span>
|
||||
<span class="material-symbols-outlined controls" @click="control( 'show-share' )" title="Show information on the share, including URL to connect to">info</span>
|
||||
<span class="material-symbols-outlined controls" title="Stop sharing your playlist on a public playlist page" @click="control( 'stop-share' )">close</span>
|
||||
<span class="material-symbols-outlined controls" title="Show information on the share, including URL to connect to" @click="control( 'show-share' )">info</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
|
||||
@@ -58,33 +116,49 @@
|
||||
</div>
|
||||
<div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )">
|
||||
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
|
||||
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos"
|
||||
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"
|
||||
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )"
|
||||
<playlistView
|
||||
:playlist="playlist"
|
||||
class="pl-wrapper"
|
||||
:currently-playing="currentlyPlayingSongIndex"
|
||||
:is-playing="isPlaying"
|
||||
:pos="pos"
|
||||
:is-logged-into-apple-music="player.isLoggedIn"
|
||||
@control="( action ) => { control( action ) }"
|
||||
@play-song="( song ) => { playSong( song ) }"
|
||||
@add-new-songs="( songs ) => addNewSongs( songs )"
|
||||
@playlist-reorder="( move ) => moveSong( move )"
|
||||
@add-new-songs-apple-music="( song ) => addNewSongFromObject( song )"
|
||||
@delete-song="song => removeSongFromPlaylist( song )"
|
||||
@clear-playlist="() => clearPlaylist()"
|
||||
@send-additional-info="() => sendAdditionalInfo()"></playlistView>
|
||||
@send-additional-info="() => sendAdditionalInfo()"
|
||||
/>
|
||||
</div>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
||||
<popupModule @update="( data ) => popupReturnHandler( data )" ref="popup"></popupModule>
|
||||
<audio src="" id="local-audio" controls="false"></audio>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger" />
|
||||
<popupModule ref="popup" @update="( data ) => popupReturnHandler( data )" />
|
||||
<audio id="local-audio" src="" controls="false"></audio>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type Ref } from 'vue';
|
||||
import playlistView from '@/components/playlistView.vue';
|
||||
import type {
|
||||
ReadFile, Song, SongMove
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
type Ref, ref
|
||||
} from 'vue';
|
||||
import MusicKitJSWrapper from '@/scripts/music-player';
|
||||
import sliderView from './sliderView.vue';
|
||||
import type { ReadFile, Song, SongMove } from '@/scripts/song';
|
||||
import { parseBlob } from 'music-metadata-browser';
|
||||
import notificationsModule from './notificationsModule.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import NotificationHandler from '@/scripts/notificationHandler';
|
||||
import notificationsModule from './notificationsModule.vue';
|
||||
import {
|
||||
parseBlob
|
||||
} from 'music-metadata';
|
||||
import playlistView from '@/components/playlistView.vue';
|
||||
import popupModule from './popupModule.vue';
|
||||
import sliderView from './sliderView.vue';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/stores/userStore';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
const repeatMode = ref( '' );
|
||||
@@ -100,7 +174,9 @@
|
||||
const nicePlaybackPos = ref( '00:00' );
|
||||
const niceDuration = ref( '00:00' );
|
||||
const isShowingRemainingTime = ref( false );
|
||||
|
||||
let isShowingRemainingTimeBackend = false;
|
||||
|
||||
const currentlyPlayingSongArtist = ref( '' );
|
||||
const pos = ref( 0 );
|
||||
const duration = ref( 0 );
|
||||
@@ -110,6 +186,7 @@
|
||||
const popup = ref( popupModule );
|
||||
const roomName = ref( '' );
|
||||
const isShowingWarning = ref( false );
|
||||
|
||||
let currentlyOpenPopup = '';
|
||||
let logoutErrorNotification = -1;
|
||||
|
||||
@@ -118,19 +195,24 @@
|
||||
document.addEventListener( 'musicplayer:autherror', () => {
|
||||
localStorage.setItem( 'close-tab', 'true' );
|
||||
isConnectedToNotifier.value = false;
|
||||
logoutErrorNotification = notifications.value.createNotification( 'You appear to have been logged out. Click to log in again!', 600, 'error', 'critical', '/', true );
|
||||
logoutErrorNotification = notifications.value.createNotification(
|
||||
'You appear to have been logged out. Click to log in again!', 600, 'error', 'critical', '/', true
|
||||
);
|
||||
} );
|
||||
|
||||
window.addEventListener( 'storage', () => {
|
||||
if ( localStorage.getItem( 'login-ok' ) === 'true' ) {
|
||||
notifications.value.cancelNotification( logoutErrorNotification );
|
||||
notifications.value.createNotification( 'Logged in again. You will have to reconnect to the share!', 20, 'ok', 'normal' );
|
||||
notifications.value.createNotification(
|
||||
'Logged in again. You will have to reconnect to the share!', 20, 'ok', 'normal'
|
||||
);
|
||||
localStorage.removeItem( 'login-ok' );
|
||||
}
|
||||
} );
|
||||
|
||||
const playPause = () => {
|
||||
isPlaying.value = !isPlaying.value;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
player.control( 'play' );
|
||||
startProgressTracker();
|
||||
@@ -138,17 +220,17 @@
|
||||
player.control( 'pause' );
|
||||
stopProgressTracker();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const goToPos = ( position: number ) => {
|
||||
player.goToPos( position );
|
||||
pos.value = position;
|
||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRemaining = () => {
|
||||
isShowingRemainingTime.value = !isShowingRemainingTime.value;
|
||||
}
|
||||
};
|
||||
|
||||
const control = ( action: string ) => {
|
||||
if ( action === 'pause' ) {
|
||||
@@ -184,10 +266,13 @@
|
||||
getDetails();
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}
|
||||
|
||||
notificationHandler.emit( 'playlist-index-update', player.getQueueID() );
|
||||
getDetails();
|
||||
} else if ( action === 'forward' ) {
|
||||
clickCountForward.value += 1;
|
||||
if( player.control( 'skip-10' ) ) {
|
||||
|
||||
if ( player.control( 'skip-10' ) ) {
|
||||
startProgressTracker();
|
||||
} else {
|
||||
pos.value = player.getPlaybackPos();
|
||||
@@ -195,7 +280,8 @@
|
||||
}
|
||||
} else if ( action === 'back' ) {
|
||||
clickCountBack.value += 1;
|
||||
if( player.control( 'back-10' ) ) {
|
||||
|
||||
if ( player.control( 'back-10' ) ) {
|
||||
startProgressTracker();
|
||||
} else {
|
||||
pos.value = player.getPlaybackPos();
|
||||
@@ -217,19 +303,19 @@
|
||||
startProgressTracker();
|
||||
} else if ( action === 'start-share' ) {
|
||||
popup.value.openPopup( {
|
||||
title: 'Define a share name',
|
||||
popupType: 'input',
|
||||
subtitle: 'A share allows others to join your playlist and see the current song, the playback position and the upcoming songs. You can get the link to the page, once the share is set up. Please choose a name, which will then be part of the URL with which others can join the share. The anti tamper feature notifies you, whenever a user leaves the fancy view.',
|
||||
data: [
|
||||
'title': 'Define a share name',
|
||||
'popupType': 'input',
|
||||
'subtitle': 'A share allows others to join your playlist and see the current song, the playback position and the upcoming songs. You can get the link to the page, once the share is set up. Please choose a name, which will then be part of the URL with which others can join the share. The anti tamper feature notifies you, whenever a user leaves the fancy view.',
|
||||
'data': [
|
||||
{
|
||||
name: 'Share Name',
|
||||
dataType: 'text',
|
||||
id: 'roomName'
|
||||
'name': 'Share Name',
|
||||
'dataType': 'text',
|
||||
'id': 'roomName'
|
||||
},
|
||||
{
|
||||
name: 'Use Anti-Tamper?',
|
||||
dataType: 'checkbox',
|
||||
id: 'useAntiTamper'
|
||||
'name': 'Use Anti-Tamper?',
|
||||
'dataType': 'checkbox',
|
||||
'id': 'useAntiTamper'
|
||||
}
|
||||
]
|
||||
} );
|
||||
@@ -238,19 +324,21 @@
|
||||
if ( confirm( 'Do you really want to stop sharing?' ) ) {
|
||||
notificationHandler.disconnect();
|
||||
isConnectedToNotifier.value = false;
|
||||
notifications.value.createNotification( 'Disconnected successfully!', 5, 'ok', 'normal' );
|
||||
notifications.value.createNotification(
|
||||
'Disconnected successfully!', 5, 'ok', 'normal'
|
||||
);
|
||||
}
|
||||
} else if ( action === 'show-share' ) {
|
||||
popup.value.openPopup( {
|
||||
title: 'Details on share',
|
||||
subtitle: 'You are currently connected to share "' + roomName.value
|
||||
+ '". \nYou can connect to it via <a href="https://music.janishutz.com/share/' + roomName.value + '" target="_blank">https://music.janishutz.com/share/' + roomName.value + '</a>'
|
||||
+ '. \n\nYou can connect to the fancy showcase screen using this link: <a href="https://music.janishutz.com/fancy/' + roomName.value + '" target="_blank">https://music.janishutz.com/fancy/' + roomName.value + '</a>'
|
||||
+ '. Be aware that this one will use significantly more system AND network resources, so only use that for a screen that is front and center, not for a QR code to have all people connect to.'
|
||||
'title': 'Details on share',
|
||||
'subtitle': 'You are currently connected to share "' + roomName.value
|
||||
+ '". \nYou can connect to it via <a href="https://music.janishutz.com/share/' + roomName.value + '" target="_blank">https://music.janishutz.com/share/' + roomName.value + '</a>'
|
||||
+ '. \n\nYou can connect to the fancy showcase screen using this link: <a href="https://music.janishutz.com/fancy/' + roomName.value + '" target="_blank">https://music.janishutz.com/fancy/' + roomName.value + '</a>'
|
||||
+ '. Be aware that this one will use significantly more system AND network resources, so only use that for a screen that is front and center, not for a QR code to have all people connect to.'
|
||||
} );
|
||||
currentlyOpenPopup = 'share-details';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const controlUI = ( action: string ) => {
|
||||
@@ -262,28 +350,30 @@
|
||||
isShowingFullScreenPlayer.value = false;
|
||||
isShowingRemainingTimeBackend = isShowingRemainingTime.value;
|
||||
isShowingRemainingTime.value = false;
|
||||
|
||||
try {
|
||||
prepNiceDurationTime( player.getPlayingSong() );
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
emits( 'playerStateChange', 'hide' );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getPlaylists = ( cb: ( data: object ) => void ) => {
|
||||
player.getUserPlaylists( cb );
|
||||
}
|
||||
};
|
||||
|
||||
const logIntoAppleMusic = () => {
|
||||
player.logIn();
|
||||
}
|
||||
};
|
||||
|
||||
const getAuth = (): boolean[] => {
|
||||
return player.getAuth();
|
||||
}
|
||||
};
|
||||
|
||||
const skipLogin = () => {
|
||||
player.init();
|
||||
}
|
||||
};
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
currentlyPlayingSongArtist.value = '';
|
||||
@@ -297,20 +387,26 @@
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}, 2000 );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const selectCustomPlaylist = async ( pl: ReadFile[] ) => {
|
||||
let n = notifications.value.createNotification( 'Analyzing playlist', 200, 'progress', 'normal' );
|
||||
let n = notifications.value.createNotification(
|
||||
'Analyzing playlist', 200, 'progress', 'normal'
|
||||
);
|
||||
|
||||
playlist.value = [];
|
||||
let plLoad: Song[] = [];
|
||||
const plLoad: Song[] = [];
|
||||
|
||||
for ( let element in pl ) {
|
||||
try {
|
||||
plLoad.push( await fetchSongData( pl[ element ] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
}
|
||||
notifications.value.updateNotification( n, `Analyzing playlist (${element}/${pl.length})` );
|
||||
|
||||
notifications.value.updateNotification( n, `Analyzing playlist (${ element }/${ pl.length })` );
|
||||
}
|
||||
|
||||
playlist.value = plLoad;
|
||||
player.setPlaylist( playlist.value );
|
||||
player.prepare( 0 );
|
||||
@@ -321,82 +417,123 @@
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}, 2000 );
|
||||
notifications.value.cancelNotification( n );
|
||||
notifications.value.createNotification( 'Playlist loaded', 10, 'ok', 'normal' );
|
||||
}
|
||||
notifications.value.createNotification(
|
||||
'Playlist loaded', 10, 'ok', 'normal'
|
||||
);
|
||||
};
|
||||
|
||||
const fetchSongData = ( songDetails: ReadFile ): Promise<Song> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
fetch( songDetails.url ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.blob().then( blob => {
|
||||
parseBlob( blob ).then( data => {
|
||||
try {
|
||||
player.findSongOnAppleMusic( data.common.title ?? songDetails.filename.split( '.' )[ 0 ] ).then( d => {
|
||||
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
|
||||
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
|
||||
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
|
||||
const song: Song = {
|
||||
artist: data.common.artist ?? d.data.results.songs.data[ 0 ].attributes.artistName,
|
||||
title: data.common.title ?? d.data.results.songs.data[ 0 ].attributes.name,
|
||||
duration: data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
|
||||
id: songDetails.url,
|
||||
origin: 'disk',
|
||||
cover: url
|
||||
fetch( songDetails.url )
|
||||
.then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.blob().then( blob => {
|
||||
parseBlob( blob )
|
||||
.then( data => {
|
||||
try {
|
||||
const searchTerm = data.common.title
|
||||
? data.common.title + ( data.common.artist ? ' ' + data.common.artist : '' )
|
||||
: songDetails.filename.split( '.' )[ 0 ].replace( '_', ' ' );
|
||||
|
||||
|
||||
player.findSongOnAppleMusic( searchTerm )
|
||||
.then( d => {
|
||||
if ( d.data.results.songs ) {
|
||||
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
|
||||
|
||||
console.debug(
|
||||
'Result used for', searchTerm, 'is', d.data.results.songs.data[0]
|
||||
);
|
||||
|
||||
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
|
||||
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
|
||||
const song: Song = {
|
||||
'artist': d.data.results.songs.data[ 0 ].attributes.artistName,
|
||||
'title': d.data.results.songs.data[ 0 ].attributes.name,
|
||||
'duration': data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
|
||||
'id': songDetails.url,
|
||||
'origin': 'disk',
|
||||
'cover': url
|
||||
};
|
||||
|
||||
resolve( song );
|
||||
} else {
|
||||
const song: Song = {
|
||||
'artist': data.common.artist ?? 'Unknown artist',
|
||||
'title': data.common.title ?? 'Unknown song title',
|
||||
'duration': data.format.duration ?? 1000,
|
||||
'id': songDetails.url,
|
||||
'origin': 'disk',
|
||||
'cover': ''
|
||||
};
|
||||
|
||||
console.warn( 'No results found for', searchTerm );
|
||||
|
||||
resolve( song );
|
||||
}
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
const song: Song = {
|
||||
'artist': data.common.artist ?? 'Unknown artist',
|
||||
'title': data.common.title ?? 'Unknown song title',
|
||||
'duration': data.format.duration ?? 1000,
|
||||
'id': songDetails.url,
|
||||
'origin': 'disk',
|
||||
'cover': ''
|
||||
};
|
||||
|
||||
resolve( song );
|
||||
} );
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
alert( 'One of your songs was not loadable. (finalization-error)' );
|
||||
reject( err );
|
||||
}
|
||||
resolve( song );
|
||||
} ).catch( e => {
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
const song: Song = {
|
||||
artist: data.common.artist ?? 'Unknown artist',
|
||||
title: data.common.title ?? 'Unknown song title',
|
||||
duration: data.format.duration ?? 1000,
|
||||
id: songDetails.url,
|
||||
origin: 'disk',
|
||||
cover: ''
|
||||
}
|
||||
resolve( song );
|
||||
alert( 'One of your songs was not loadable. (parser-error)' );
|
||||
reject( e );
|
||||
} );
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
alert( 'One of your songs was not loadable. (finalization-error)' )
|
||||
}
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
alert( 'One of your songs was not loadable. (parser-error)' );
|
||||
reject( e );
|
||||
} );
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
alert( 'One of your songs was not loadable. (converter-error)' );
|
||||
reject( e );
|
||||
} );
|
||||
} else {
|
||||
console.error( res.status );
|
||||
alert( 'One of your songs was not loadable. (invalid-response-code)' );
|
||||
}
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
alert( 'One of your songs was not loadable. (could-not-connect)' );
|
||||
reject( e );
|
||||
} );
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
alert( 'One of your songs was not loadable. (converter-error)' );
|
||||
reject( e );
|
||||
} );
|
||||
} else {
|
||||
console.error( res.status );
|
||||
alert( 'One of your songs was not loadable. (invalid-response-code)' );
|
||||
reject( res.status );
|
||||
}
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
alert( 'One of your songs was not loadable. (could-not-connect)' );
|
||||
reject( e );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const getDetails = () => {
|
||||
const details = player.getPlayingSong();
|
||||
|
||||
currentlyPlayingSongName.value = details.title;
|
||||
coverArt.value = details.cover;
|
||||
currentlyPlayingSongIndex.value = player.getQueueID();
|
||||
playlist.value = player.getQueue();
|
||||
currentlyPlayingSongArtist.value = details.artist;
|
||||
}
|
||||
};
|
||||
|
||||
const playSong = ( id: string ) => {
|
||||
const p = player.getPlaylist();
|
||||
|
||||
currentlyPlayingSongArtist.value = '';
|
||||
coverArt.value = '';
|
||||
currentlyPlayingSongName.value = 'Loading...';
|
||||
stopProgressTracker();
|
||||
|
||||
for ( const s in p ) {
|
||||
if ( p[ s ].id === id ) {
|
||||
player.prepare( parseInt( s ) );
|
||||
@@ -404,23 +541,29 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let progressTracker = 0;
|
||||
let progressTracker: ReturnType<typeof setInterval> = setInterval( () => {}, 1000 );
|
||||
|
||||
clearInterval( progressTracker );
|
||||
let hasReachedEnd = false;
|
||||
let hasStarted = false;
|
||||
|
||||
const startProgressTracker = () => {
|
||||
hasReachedEnd = false;
|
||||
isPlaying.value = true;
|
||||
let playingSong = player.getPlayingSong();
|
||||
|
||||
hasStarted = false;
|
||||
pos.value = 0;
|
||||
progressTracker = setInterval( () => {
|
||||
pos.value = player.getPlaybackPos();
|
||||
|
||||
if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) {
|
||||
stopProgressTracker();
|
||||
hasReachedEnd = true;
|
||||
|
||||
if ( repeatMode.value === '_one_on' ) {
|
||||
player.goToPos( 0 );
|
||||
setTimeout( () => {
|
||||
@@ -432,21 +575,30 @@
|
||||
}
|
||||
|
||||
if ( pos.value > 0 && !hasStarted ) {
|
||||
getDetails();
|
||||
playingSong = player.getPlayingSong();
|
||||
prepNiceDurationTime( playingSong );
|
||||
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
||||
hasStarted = true;
|
||||
if ( player.getPlaying() ) {
|
||||
setTimeout( () => {
|
||||
getDetails();
|
||||
playingSong = player.getPlayingSong();
|
||||
pos.value = player.getPlaybackPos();
|
||||
prepNiceDurationTime( playingSong );
|
||||
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - ( pos.value * 1000 ) );
|
||||
hasStarted = true;
|
||||
}, 2000 );
|
||||
}
|
||||
}
|
||||
|
||||
const minuteCount = Math.floor( pos.value / 60 );
|
||||
|
||||
nicePlaybackPos.value = minuteCount + ':';
|
||||
|
||||
if ( ( '' + minuteCount ).length === 1 ) {
|
||||
nicePlaybackPos.value = '0' + minuteCount + ':';
|
||||
}
|
||||
const secondCount = Math.floor( pos.value - minuteCount * 60 );
|
||||
|
||||
const secondCount = Math.floor( pos.value - ( minuteCount * 60 ) );
|
||||
|
||||
if ( ( '' + secondCount ).length === 1 ) {
|
||||
nicePlaybackPos.value += '0' + secondCount;
|
||||
} else {
|
||||
@@ -455,11 +607,15 @@
|
||||
|
||||
if ( isShowingRemainingTime.value ) {
|
||||
const minuteCounts = Math.floor( ( playingSong.duration - pos.value ) / 60 );
|
||||
|
||||
niceDuration.value = '-' + String( minuteCounts ) + ':';
|
||||
|
||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
||||
niceDuration.value = '-0' + minuteCounts + ':';
|
||||
}
|
||||
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - minuteCounts * 60 );
|
||||
|
||||
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - ( minuteCounts * 60 ) );
|
||||
|
||||
if ( ( '' + secondCounts ).length === 1 ) {
|
||||
niceDuration.value += '0' + secondCounts;
|
||||
} else {
|
||||
@@ -467,82 +623,101 @@
|
||||
}
|
||||
}
|
||||
}, 100 );
|
||||
}
|
||||
};
|
||||
|
||||
const prepNiceDurationTime = ( playingSong: Song ) => {
|
||||
duration.value = playingSong.duration;
|
||||
const minuteCounts = Math.floor( ( playingSong.duration ) / 60 );
|
||||
const minuteCounts = Math.floor( playingSong.duration / 60 );
|
||||
|
||||
niceDuration.value = String( minuteCounts ) + ':';
|
||||
|
||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
||||
niceDuration.value = '0' + minuteCounts + ':';
|
||||
}
|
||||
const secondCounts = Math.floor( ( playingSong.duration ) - minuteCounts * 60 );
|
||||
|
||||
const secondCounts = Math.floor( playingSong.duration - minuteCounts * 60 );
|
||||
|
||||
if ( ( '' + secondCounts ).length === 1 ) {
|
||||
niceDuration.value += '0' + secondCounts;
|
||||
} else {
|
||||
niceDuration.value += secondCounts;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopProgressTracker = () => {
|
||||
try {
|
||||
clearInterval( progressTracker );
|
||||
} catch ( _ ) { /* empty */ }
|
||||
|
||||
isPlaying.value = false;
|
||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||
}
|
||||
};
|
||||
|
||||
const moveSong = ( move: SongMove ) => {
|
||||
player.moveSong( move );
|
||||
getDetails();
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}
|
||||
};
|
||||
|
||||
const addNewSongs = async ( songs: ReadFile[] ) => {
|
||||
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' );
|
||||
let n = notifications.value.createNotification(
|
||||
'Analyzing new songs', 200, 'progress', 'normal'
|
||||
);
|
||||
|
||||
playlist.value = player.getQueue();
|
||||
|
||||
for ( let element in songs ) {
|
||||
try {
|
||||
playlist.value.push( await fetchSongData( songs[ element ] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
}
|
||||
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
|
||||
|
||||
notifications.value.updateNotification( n, `Analyzing new songs (${ element }/${ songs.length })` );
|
||||
}
|
||||
|
||||
player.setPlaylist( playlist.value );
|
||||
|
||||
if ( !isPlaying.value ) {
|
||||
player.prepare( 0 );
|
||||
isPlaying.value = true;
|
||||
startProgressTracker();
|
||||
}
|
||||
|
||||
notifications.value.cancelNotification( n );
|
||||
notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' );
|
||||
notifications.value.createNotification(
|
||||
'New songs added', 10, 'ok', 'normal'
|
||||
);
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}
|
||||
};
|
||||
|
||||
const addNewSongFromObject = ( song: Song ) => {
|
||||
playlist.value = player.getQueue();
|
||||
playlist.value.push( song );
|
||||
player.setPlaylist( playlist.value );
|
||||
|
||||
if ( !isPlaying.value ) {
|
||||
player.prepare( 0 );
|
||||
isPlaying.value = true;
|
||||
startProgressTracker();
|
||||
}
|
||||
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}
|
||||
};
|
||||
|
||||
const removeSongFromPlaylist = ( song: number ) => {
|
||||
playlist.value = player.getQueue();
|
||||
playlist.value.splice( song, 1 );
|
||||
player.setPlaylist( playlist.value );
|
||||
|
||||
if ( !isPlaying.value ) {
|
||||
player.prepare( 0 );
|
||||
isPlaying.value = true;
|
||||
startProgressTracker();
|
||||
}
|
||||
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}
|
||||
};
|
||||
|
||||
const clearPlaylist = () => {
|
||||
playlist.value = [];
|
||||
@@ -555,18 +730,20 @@
|
||||
coverArt.value = '';
|
||||
pos.value = 0;
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}
|
||||
};
|
||||
|
||||
const sendAdditionalInfo = () => {
|
||||
notifications.value.createNotification( 'Additional song info transmitted', 5, 'ok', 'normal' );
|
||||
notifications.value.createNotification(
|
||||
'Additional song info transmitted', 5, 'ok', 'normal'
|
||||
);
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
}
|
||||
};
|
||||
|
||||
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
document.addEventListener( 'keydown', ( e ) => {
|
||||
document.addEventListener( 'keydown', e => {
|
||||
if ( !userStore.isUsingKeyboard ) {
|
||||
if ( e.key === ' ' ) {
|
||||
e.preventDefault();
|
||||
@@ -583,7 +760,7 @@
|
||||
|
||||
const dismissNotification = () => {
|
||||
isShowingWarning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const popupReturnHandler = ( data: any ) => {
|
||||
if ( currentlyOpenPopup === 'create-share' ) {
|
||||
@@ -594,26 +771,35 @@
|
||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||
notifications.value.createNotification( 'Joined share "' + data.roomName + '"!', 5, 'ok', 'normal' );
|
||||
notifications.value.createNotification(
|
||||
'Joined share "' + data.roomName + '"!', 5, 'ok', 'normal'
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
notificationHandler.registerListener( 'tampering-msg', ( _ ) => {
|
||||
notificationHandler.registerListener( 'tampering-msg', _ => {
|
||||
isShowingWarning.value = true;
|
||||
} );
|
||||
} ).catch( e => {
|
||||
if ( e === 'ERR_CONFLICT' ) {
|
||||
notifications.value.createNotification( 'A share with this name exists already!', 5, 'error', 'normal' );
|
||||
control( 'start-share' );
|
||||
} else if ( e === 'ERR_UNAUTHORIZED' ) {
|
||||
console.error( e );
|
||||
localStorage.setItem( 'close-tab', 'true' );
|
||||
logoutErrorNotification = notifications.value.createNotification( 'You appear to have been logged out. Click to log in again!', 20, 'error', 'normal', '/', true );
|
||||
} else {
|
||||
console.error( e );
|
||||
notifications.value.createNotification( 'Could not create share!', 5, 'error', 'normal' );
|
||||
}
|
||||
} );
|
||||
} )
|
||||
.catch( e => {
|
||||
if ( e === 'ERR_CONFLICT' ) {
|
||||
notifications.value.createNotification(
|
||||
'A share with this name exists already!', 5, 'error', 'normal'
|
||||
);
|
||||
control( 'start-share' );
|
||||
} else if ( e === 'ERR_UNAUTHORIZED' ) {
|
||||
console.error( e );
|
||||
localStorage.setItem( 'close-tab', 'true' );
|
||||
logoutErrorNotification = notifications.value.createNotification(
|
||||
'You appear to have been logged out. Click to log in again!', 20, 'error', 'normal', '/', true
|
||||
);
|
||||
} else {
|
||||
console.error( e );
|
||||
notifications.value.createNotification(
|
||||
'Could not create share!', 5, 'error', 'normal'
|
||||
);
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener( 'beforeunload', async () => {
|
||||
await notificationHandler.disconnect();
|
||||
|
||||
@@ -1,88 +1,153 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Queue</h1>
|
||||
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
|
||||
<button @click="addNewSongs()" class="small-buttons" title="Load selected files"><span class="material-symbols-outlined">upload</span></button>
|
||||
<button @click="openSearch()" v-if="$props.isLoggedIntoAppleMusic" class="small-buttons" title="Search Apple Music for the song"><span class="material-symbols-outlined">search</span></button>
|
||||
<button @click="clearPlaylist()" class="small-buttons" title="Clear the playlist"><span class="material-symbols-outlined">delete</span></button>
|
||||
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()"><span class="material-symbols-outlined">send</span></button>
|
||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
||||
<div class="playlist-box" id="pl-box">
|
||||
<input
|
||||
id="more-songs"
|
||||
type="file"
|
||||
multiple
|
||||
accept="audio/*"
|
||||
class="small-buttons"
|
||||
>
|
||||
<button class="small-buttons" title="Load selected files" @click="addNewSongs()">
|
||||
<span class="material-symbols-outlined">upload</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="$props.isLoggedIntoAppleMusic"
|
||||
class="small-buttons"
|
||||
title="Search Apple Music for the song"
|
||||
@click="openSearch()"
|
||||
>
|
||||
<span class="material-symbols-outlined">search</span>
|
||||
</button>
|
||||
<button class="small-buttons" title="Clear the playlist" @click="clearPlaylist()">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()">
|
||||
<span class="material-symbols-outlined">send</span>
|
||||
</button>
|
||||
<p v-if="!hasSelectedSongs">
|
||||
Please select at least one song to proceed
|
||||
</p>
|
||||
<div id="pl-box" class="playlist-box">
|
||||
<!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? Maybe using retrieved ID from Apple Music? -->
|
||||
<!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
|
||||
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
||||
<div
|
||||
v-for="song in computedPlaylist"
|
||||
:key="song.id"
|
||||
class="song"
|
||||
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
||||
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
|
||||
<img :src="song.cover" alt="Song cover" class="song-cover">
|
||||
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )"
|
||||
>
|
||||
<img
|
||||
v-if="song.cover"
|
||||
:src="song.cover"
|
||||
alt="Song cover"
|
||||
class="song-cover"
|
||||
>
|
||||
<span v-else class="material-symbols-outlined song-cover">music_note</span>
|
||||
<div v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && $props.isPlaying" class="playing-symbols">
|
||||
<div class="playing-symbols-wrapper">
|
||||
<div class="playing-bar" id="bar-1"></div>
|
||||
<div class="playing-bar" id="bar-2"></div>
|
||||
<div class="playing-bar" id="bar-3"></div>
|
||||
<div id="bar-1" class="playing-bar"></div>
|
||||
<div id="bar-2" class="playing-bar"></div>
|
||||
<div id="bar-3" class="playing-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined play-icon" @click="control( 'play' )" v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )">play_arrow</span>
|
||||
<span class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>play_arrow</span>
|
||||
<span v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )" class="material-symbols-outlined play-icon" @click="control( 'play' )">play_arrow</span>
|
||||
<span v-else class="material-symbols-outlined play-icon" @click="play( song.id )">play_arrow</span>
|
||||
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
|
||||
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'up' )" title="Move song up" v-if="canBeMoved( 'up', song.id )">arrow_upward</span>
|
||||
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'down' )" title="Move song down" v-if="canBeMoved( 'down', song.id )">arrow_downward</span>
|
||||
<h3 class="song-title">{{ song.title }}</h3>
|
||||
<span
|
||||
v-if="canBeMoved( 'up', song.id )"
|
||||
class="material-symbols-outlined move-icon"
|
||||
title="Move song up"
|
||||
@click="moveSong( song.id, 'up' )"
|
||||
>arrow_upward</span>
|
||||
<span
|
||||
v-if="canBeMoved( 'down', song.id )"
|
||||
class="material-symbols-outlined move-icon"
|
||||
title="Move song down"
|
||||
@click="moveSong( song.id, 'down' )"
|
||||
>arrow_downward</span>
|
||||
<h3 class="song-title">
|
||||
{{ song.title }}
|
||||
</h3>
|
||||
<div>
|
||||
<input type="text" placeholder="Additional information for remote display" title="Additional information for remote display" v-model="song.additionalInfo" @focusin="kbControl( 'on' )" @focusout="kbControl( 'off' )">
|
||||
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
||||
<input
|
||||
v-model="song.additionalInfo"
|
||||
type="text"
|
||||
placeholder="Additional information for remote display"
|
||||
title="Additional information for remote display"
|
||||
@focusin="kbControl( 'on' )"
|
||||
@focusout="kbControl( 'off' )"
|
||||
>
|
||||
<p class="playing-in">
|
||||
{{ getTimeUntil( song ) }}
|
||||
</p>
|
||||
</div>
|
||||
<button @click="deleteSong( song.id )" class="small-buttons" title="Remove this song from the queue" v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"><span class="material-symbols-outlined">delete</span></button>
|
||||
<button
|
||||
v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"
|
||||
class="small-buttons"
|
||||
title="Remove this song from the queue"
|
||||
@click="deleteSong( song.id )"
|
||||
>
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
|
||||
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO: Add logout button
|
||||
import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song';
|
||||
import { computed, ref } from 'vue';
|
||||
import type {
|
||||
AppleMusicSongData, ReadFile, Song
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
computed, ref
|
||||
} from 'vue';
|
||||
import searchView from './searchView.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/stores/userStore';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const search = ref( searchView );
|
||||
const props = defineProps( {
|
||||
'playlist': {
|
||||
default: [],
|
||||
required: true,
|
||||
type: Array<Song>
|
||||
'default': [],
|
||||
'required': true,
|
||||
'type': Array<Song>
|
||||
},
|
||||
'currentlyPlaying': {
|
||||
default: 0,
|
||||
required: true,
|
||||
type: Number,
|
||||
'default': 0,
|
||||
'required': true,
|
||||
'type': Number,
|
||||
},
|
||||
'isPlaying': {
|
||||
default: true,
|
||||
required: true,
|
||||
type: Boolean,
|
||||
'default': true,
|
||||
'required': true,
|
||||
'type': Boolean,
|
||||
},
|
||||
'pos': {
|
||||
default: 0,
|
||||
required: false,
|
||||
type: Number,
|
||||
'default': 0,
|
||||
'required': false,
|
||||
'type': Number,
|
||||
},
|
||||
'isLoggedIntoAppleMusic': {
|
||||
default: false,
|
||||
required: true,
|
||||
type: Boolean,
|
||||
'default': false,
|
||||
'required': true,
|
||||
'type': Boolean,
|
||||
}
|
||||
} );
|
||||
const hasSelectedSongs = ref( true );
|
||||
|
||||
const computedPlaylist = computed( () => {
|
||||
let pl: Song[] = [];
|
||||
|
||||
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
|
||||
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
|
||||
pl.push( props.playlist[ i ] );
|
||||
}
|
||||
|
||||
return pl;
|
||||
} );
|
||||
|
||||
@@ -92,60 +157,66 @@
|
||||
} else {
|
||||
userStore.setKeyboardUsageStatus( true );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openSearch = () => {
|
||||
if ( search.value ) {
|
||||
search.value.controlSearch( 'show' );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const canBeMoved = computed( () => {
|
||||
return ( direction: movementDirection, songID: string ): boolean => {
|
||||
let id = 0;
|
||||
|
||||
for ( let song in props.playlist ) {
|
||||
if ( props.playlist[ song ].id === songID ) {
|
||||
id = parseInt( song );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( direction === 'up' ) {
|
||||
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} )
|
||||
|
||||
};
|
||||
} );
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song: Song ) => {
|
||||
let timeRemaining = 0;
|
||||
|
||||
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
||||
if ( props.playlist[ i ] == song ) {
|
||||
break;
|
||||
}
|
||||
|
||||
timeRemaining += props.playlist[ i ].duration;
|
||||
}
|
||||
|
||||
if ( props.isPlaying ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} );
|
||||
|
||||
const deleteSong = ( songID: string ) => {
|
||||
@@ -154,69 +225,92 @@
|
||||
emits( 'delete-song', parseInt( song ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearPlaylist = () => {
|
||||
emits( 'clear-playlist', '' );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const control = ( action: string ) => {
|
||||
emits( 'control', action );
|
||||
}
|
||||
};
|
||||
|
||||
const play = ( song: string ) => {
|
||||
emits( 'play-song', song );
|
||||
}
|
||||
};
|
||||
|
||||
const addNewSongs = () => {
|
||||
const fileURLList: ReadFile[] = [];
|
||||
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
|
||||
|
||||
if ( allFiles.length > 0 ) {
|
||||
hasSelectedSongs.value = true;
|
||||
|
||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
|
||||
fileURLList.push( {
|
||||
'url': URL.createObjectURL( allFiles[ file ] ),
|
||||
'filename': allFiles[ file ].name
|
||||
} );
|
||||
}
|
||||
|
||||
emits( 'add-new-songs', fileURLList );
|
||||
} else {
|
||||
hasSelectedSongs.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
||||
const song: Song = {
|
||||
artist: songData.attributes.artistName,
|
||||
cover: songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||
duration: songData.attributes.durationInMillis / 1000,
|
||||
id: songData.id,
|
||||
origin: 'apple-music',
|
||||
title: songData.attributes.name
|
||||
}
|
||||
'artist': songData.attributes.artistName,
|
||||
'cover': songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||
'duration': songData.attributes.durationInMillis / 1000,
|
||||
'id': songData.id,
|
||||
'origin': 'apple-music',
|
||||
'title': songData.attributes.name
|
||||
};
|
||||
|
||||
emits( 'add-new-songs-apple-music', song );
|
||||
}
|
||||
};
|
||||
|
||||
type movementDirection = 'up' | 'down';
|
||||
|
||||
const moveSong = ( songID: string, direction: movementDirection ) => {
|
||||
let newSongPos = 0;
|
||||
let hasFoundSongToMove = false;
|
||||
|
||||
for ( let el in props.playlist ) {
|
||||
if ( props.playlist[ el ].id === songID ) {
|
||||
const currPos = parseInt( el );
|
||||
|
||||
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
||||
hasFoundSongToMove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( hasFoundSongToMove ) {
|
||||
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } );
|
||||
emits( 'playlist-reorder', {
|
||||
'songID': songID,
|
||||
'newPos': newSongPos
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendAdditionalInfo = () => {
|
||||
emits( 'send-additional-info' );
|
||||
}
|
||||
};
|
||||
|
||||
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs', 'add-new-songs-apple-music', 'delete-song', 'clear-playlist', 'send-additional-info' ] );
|
||||
const emits = defineEmits( [
|
||||
'play-song',
|
||||
'control',
|
||||
'playlist-reorder',
|
||||
'add-new-songs',
|
||||
'add-new-songs-apple-music',
|
||||
'delete-song',
|
||||
'clear-playlist',
|
||||
'send-additional-info'
|
||||
] );
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -249,6 +343,10 @@
|
||||
font-size: 6rem;
|
||||
}
|
||||
|
||||
.song img.song-cover {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.song-title {
|
||||
margin-left: 10px;
|
||||
margin-right: auto;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="playlists">
|
||||
<h3 style="width: fit-content;">Your playlists</h3>
|
||||
<h3 style="width: fit-content;">
|
||||
Your playlists
|
||||
</h3>
|
||||
<div v-if="( $props.playlists ? $props.playlists.length < 1 : true ) && $props.isLoggedIn">
|
||||
Loading...
|
||||
<!-- TODO: Make prettier -->
|
||||
@@ -8,12 +10,27 @@
|
||||
<div v-else-if="!$props.isLoggedIn" class="not-logged-in">
|
||||
<p>You are not logged into Apple Music. We therefore can't show you your playlists. <a href="" title="Refreshes the page, allowing you to log in">Change that</a></p>
|
||||
<p>Use the button below to load songs from your local disk</p>
|
||||
<input class="pl-loader-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
|
||||
<button @click="loadPlaylistFromDisk()" class="pl-loader-button" id="load-button">Load</button>
|
||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p>
|
||||
<input
|
||||
id="pl-loader"
|
||||
class="pl-loader-button"
|
||||
type="file"
|
||||
multiple="true"
|
||||
accept="audio/*"
|
||||
><br>
|
||||
<button id="load-button" class="pl-loader-button" @click="loadPlaylistFromDisk()">
|
||||
Load
|
||||
</button>
|
||||
<p v-if="!hasSelectedSongs">
|
||||
Please select at least one song to proceed!
|
||||
</p>
|
||||
</div>
|
||||
<div class="playlist-wrapper">
|
||||
<div v-for="pl in $props.playlists" v-bind:key="pl.id" class="playlist" @click="selectPlaylist( pl.id )">
|
||||
<div
|
||||
v-for="pl in $props.playlists"
|
||||
:key="pl.id"
|
||||
class="playlist"
|
||||
@click="selectPlaylist( pl.id )"
|
||||
>
|
||||
{{ pl.attributes.name }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,8 +38,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ReadFile } from '@/scripts/song';
|
||||
import { ref } from 'vue';
|
||||
import type {
|
||||
ReadFile
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
|
||||
const hasSelectedSongs = ref( true );
|
||||
|
||||
defineProps( {
|
||||
@@ -41,22 +63,31 @@
|
||||
const loadPlaylistFromDisk = () => {
|
||||
const fileURLList: ReadFile[] = [];
|
||||
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
|
||||
|
||||
if ( allFiles.length > 0 ) {
|
||||
hasSelectedSongs.value = true;
|
||||
|
||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
|
||||
fileURLList.push( {
|
||||
'url': URL.createObjectURL( allFiles[ file ] ),
|
||||
'filename': allFiles[ file ].name
|
||||
} );
|
||||
}
|
||||
|
||||
emits( 'custom-playlist', fileURLList );
|
||||
} else {
|
||||
hasSelectedSongs.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] );
|
||||
const emits = defineEmits( [
|
||||
'selected-playlist',
|
||||
'custom-playlist'
|
||||
] );
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
emits( 'selected-playlist', id );
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue';
|
||||
import {
|
||||
createApp
|
||||
} from 'vue';
|
||||
import {
|
||||
createPinia
|
||||
} from 'pinia';
|
||||
import router from './router';
|
||||
import sdk from '@janishutz/login-sdk-browser';
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
const app = createApp( App );
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use( createPinia() );
|
||||
app.use( router );
|
||||
|
||||
// localStorage.setItem( 'url', 'http://localhost:8082' );
|
||||
localStorage.setItem( 'url', 'https://music-api.janishutz.com' );
|
||||
sdk.setUp(
|
||||
'jh-music',
|
||||
String( localStorage.getItem( 'url' ) ),
|
||||
'/app',
|
||||
false // Set to false for deploy to actual backend
|
||||
);
|
||||
|
||||
app.mount('#app')
|
||||
app.mount( '#app' );
|
||||
|
||||
@@ -50,6 +50,15 @@ const router = createRouter( {
|
||||
'title': 'Fancy View'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tools/bar',
|
||||
name: 'tool-bar',
|
||||
component: () => import( '../views/BarView.vue' ),
|
||||
meta: {
|
||||
'authRequired': false,
|
||||
'title': 'Bar utility'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ColorThief from 'colorthief';
|
||||
|
||||
const colorThief = new ColorThief();
|
||||
|
||||
const getImageData = (): Promise<number[][]> => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement );
|
||||
return new Promise( resolve => {
|
||||
const img = document.getElementById( 'current-image' ) as HTMLImageElement;
|
||||
|
||||
if ( img.complete ) {
|
||||
resolve( colorThief.getPalette( img ) );
|
||||
} else {
|
||||
@@ -12,32 +14,39 @@ const getImageData = (): Promise<number[][]> => {
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const createBackground = () => {
|
||||
return new Promise( ( resolve ) => {
|
||||
return new Promise( resolve => {
|
||||
getImageData().then( palette => {
|
||||
const colourDetails: number[][] = [];
|
||||
const colours: string[] = [];
|
||||
|
||||
let differentEnough = true;
|
||||
|
||||
if ( palette[ 0 ] ) {
|
||||
for ( const i in palette ) {
|
||||
for ( const colour in colourDetails ) {
|
||||
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
||||
|
||||
if ( colourDiff > 15 ) {
|
||||
differentEnough = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( differentEnough ) {
|
||||
colourDetails.push( palette[ i ] );
|
||||
colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' );
|
||||
}
|
||||
|
||||
differentEnough = false;
|
||||
}
|
||||
}
|
||||
|
||||
let outColours = 'conic-gradient(';
|
||||
|
||||
if ( colours.length < 3 ) {
|
||||
for ( let i = 0; i < 3; i++ ) {
|
||||
if ( colours[ i ] ) {
|
||||
@@ -61,45 +70,56 @@ const createBackground = () => {
|
||||
outColours += colours[ i ] + ',';
|
||||
}
|
||||
}
|
||||
|
||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||
resolve( outColours );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
let callbackFun = () => {};
|
||||
|
||||
let callbackFun = () => {}
|
||||
const subscribeToBeatUpdate = ( cb: () => void ) => {
|
||||
callbackFun = cb;
|
||||
micAudioHandler();
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribeFromBeatUpdate = () => {
|
||||
callbackFun = () => {}
|
||||
callbackFun = () => {};
|
||||
|
||||
try {
|
||||
clearInterval( micAnalyzer );
|
||||
} catch ( e ) { /* empty */ }
|
||||
}
|
||||
};
|
||||
|
||||
const coolDown = () => {
|
||||
beatDetected = false;
|
||||
}
|
||||
};
|
||||
|
||||
let micAnalyzer = 0;
|
||||
let beatDetected = false;
|
||||
|
||||
const micAudioHandler = () => {
|
||||
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
|
||||
analyser.fftSize = 256;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array( bufferLength );
|
||||
|
||||
beatDetected = false;
|
||||
|
||||
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
||||
navigator.mediaDevices.getUserMedia( {
|
||||
'audio': true
|
||||
} ).then( stream => {
|
||||
const mic = audioContext.createMediaStreamSource( stream );
|
||||
|
||||
mic.connect( analyser );
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
let prevSpectrum: number[] = [];
|
||||
|
||||
const threshold = 10; // Adjust as needed
|
||||
|
||||
micAnalyzer = setInterval( () => {
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
// Convert the frequency data to a numeric array
|
||||
@@ -115,25 +135,27 @@ const micAudioHandler = () => {
|
||||
callbackFun();
|
||||
}
|
||||
}
|
||||
|
||||
prevSpectrum = currentSpectrum;
|
||||
}, 60 / 180 * 250 );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => {
|
||||
let flux = 0;
|
||||
|
||||
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
||||
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
||||
|
||||
flux += Math.max( 0, diff );
|
||||
}
|
||||
|
||||
return flux;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createBackground,
|
||||
subscribeToBeatUpdate,
|
||||
unsubscribeFromBeatUpdate,
|
||||
coolDown,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
/*
|
||||
* MusicPlayerV2 - notificationHandler.ts
|
||||
*
|
||||
* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
// These functions handle connections to the backend with socket.io
|
||||
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
import type { SSEMap } from "./song";
|
||||
import {
|
||||
io, type Socket
|
||||
} from 'socket.io-client';
|
||||
import type {
|
||||
SSEMap
|
||||
} from './song';
|
||||
|
||||
class SocketConnection {
|
||||
|
||||
socket: Socket;
|
||||
|
||||
roomName: string;
|
||||
|
||||
isConnected: boolean;
|
||||
|
||||
useSocket: boolean;
|
||||
|
||||
eventSource?: EventSource;
|
||||
|
||||
toBeListenedForItems: SSEMap;
|
||||
|
||||
reconnectRetryCount: number;
|
||||
|
||||
openConnectionsCount: number;
|
||||
|
||||
constructor () {
|
||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||
autoConnect: false,
|
||||
'autoConnect': false,
|
||||
} );
|
||||
this.roomName = location.pathname.split( '/' )[ 2 ];
|
||||
this.isConnected = false;
|
||||
@@ -38,48 +41,64 @@ class SocketConnection {
|
||||
* Create a room token and connect to
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
connect (): Promise<any> {
|
||||
connect (): Promise<unknown> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
if ( this.reconnectRetryCount < 5 ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.connect();
|
||||
this.socket.emit( 'join-room', this.roomName, ( res: { status: boolean, msg: string, data: any } ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve( res.data );
|
||||
} else {
|
||||
console.debug( res.msg );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
this.socket.emit(
|
||||
'join-room', this.roomName, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string,
|
||||
'data': unknown
|
||||
} ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve( res.data );
|
||||
} else {
|
||||
console.debug( res.msg );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
}
|
||||
} );
|
||||
);
|
||||
} else {
|
||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||
this.openConnectionsCount += 1;
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
||||
this.eventSource
|
||||
= new EventSource( localStorage.getItem( 'url' )
|
||||
+ '/socket/connection?room=' + this.roomName, {
|
||||
'withCredentials': true
|
||||
} );
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true;
|
||||
this.reconnectRetryCount = 0;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||
}
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString() + ': Connection successfully established!' );
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = ( e ) => {
|
||||
this.eventSource.onmessage = e => {
|
||||
const d = JSON.parse( e.data );
|
||||
|
||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||
this.toBeListenedForItems[ d.type ]( d.data );
|
||||
} else if ( d.type === 'basics' ) {
|
||||
resolve( d.data );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = () => {
|
||||
if ( this.isConnected ) {
|
||||
this.isConnected = false;
|
||||
this.openConnectionsCount -= 1;
|
||||
this.eventSource?.close();
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Reconnecting due to connection error!' );
|
||||
// console.debug( e );
|
||||
|
||||
this.eventSource = undefined;
|
||||
@@ -91,13 +110,18 @@ class SocketConnection {
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error ' + res.status );
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Could not connect due to error ' + res.status );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
} ).catch( () => {
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error.' );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
console.log( '[ SSE Connection ] - '
|
||||
+ new Date().toISOString()
|
||||
+ ': Could not connect due to error.' );
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} );
|
||||
} else {
|
||||
console.log( '[ SSE Connection ]: Trimmed connections' );
|
||||
reject( 'ERR_TOO_MANY_CONNECTIONS' );
|
||||
@@ -116,16 +140,23 @@ class SocketConnection {
|
||||
* @param {any} data
|
||||
* @returns {void}
|
||||
*/
|
||||
emit ( event: string, data: any ): void {
|
||||
emit ( event: string, data: unknown ): void {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( event, { 'roomName': this.roomName, 'data': data } );
|
||||
this.socket.emit( event, {
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} );
|
||||
} else {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'data': data } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'event': event,
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -140,7 +171,7 @@ class SocketConnection {
|
||||
* @param {( data: any ) => void} cb The callback function / listener function
|
||||
* @returns {void}
|
||||
*/
|
||||
registerListener ( event: string, cb: ( data: any ) => void ): void {
|
||||
registerListener ( event: string, cb: ( data: unknown ) => void ): void {
|
||||
if ( this.useSocket ) {
|
||||
if ( this.isConnected ) {
|
||||
this.socket.on( event, cb );
|
||||
@@ -171,9 +202,11 @@ class SocketConnection {
|
||||
if ( this.eventSource ) {
|
||||
return this.eventSource!.OPEN && this.isConnected;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SocketConnection;
|
||||
@@ -1,25 +1,39 @@
|
||||
import type { SearchResult, Song, SongMove } from "./song";
|
||||
import type {
|
||||
SearchResult, Song, SongMove
|
||||
} from './song';
|
||||
|
||||
interface Config {
|
||||
devToken: string;
|
||||
userToken: string;
|
||||
'devToken': string;
|
||||
'userToken': string;
|
||||
}
|
||||
|
||||
type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10';
|
||||
type RepeatMode = 'off' | 'once' | 'all';
|
||||
|
||||
class MusicKitJSWrapper {
|
||||
|
||||
playingSongID: number;
|
||||
|
||||
playlist: Song[];
|
||||
|
||||
queue: number[];
|
||||
|
||||
config: Config;
|
||||
|
||||
musicKit: any;
|
||||
|
||||
isLoggedIn: boolean;
|
||||
|
||||
isPreparedToPlay: boolean;
|
||||
|
||||
repeatMode: RepeatMode;
|
||||
|
||||
isShuffleEnabled: boolean;
|
||||
|
||||
hasEncounteredAuthError: boolean;
|
||||
|
||||
queuePos: number;
|
||||
|
||||
audioPlayer: HTMLAudioElement;
|
||||
|
||||
constructor () {
|
||||
@@ -27,8 +41,8 @@ class MusicKitJSWrapper {
|
||||
this.playlist = [];
|
||||
this.queue = [];
|
||||
this.config = {
|
||||
devToken: '',
|
||||
userToken: '',
|
||||
'devToken': '',
|
||||
'userToken': '',
|
||||
};
|
||||
this.isShuffleEnabled = false;
|
||||
this.repeatMode = 'off';
|
||||
@@ -58,16 +72,18 @@ class MusicKitJSWrapper {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} else {
|
||||
this.musicKit.authorize().then( () => {
|
||||
this.isLoggedIn = true;
|
||||
this.init();
|
||||
} ).catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.hasEncounteredAuthError = true;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,25 +92,29 @@ class MusicKitJSWrapper {
|
||||
* @returns {void}
|
||||
*/
|
||||
init (): void {
|
||||
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( token => {
|
||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||
// MusicKit global is now defined
|
||||
MusicKit.configure( {
|
||||
developerToken: token,
|
||||
app: {
|
||||
name: 'MusicPlayer',
|
||||
build: '3'
|
||||
'developerToken': token,
|
||||
'app': {
|
||||
'name': 'MusicPlayer',
|
||||
'build': '3'
|
||||
},
|
||||
storefrontId: 'CH',
|
||||
'storefrontId': 'CH',
|
||||
} ).then( () => {
|
||||
this.config.devToken = token;
|
||||
this.musicKit = MusicKit.getInstance();
|
||||
|
||||
if ( this.musicKit.isAuthorized ) {
|
||||
this.isLoggedIn = true;
|
||||
this.config.userToken = this.musicKit.musicUserToken;
|
||||
}
|
||||
|
||||
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off;
|
||||
} );
|
||||
} );
|
||||
@@ -107,7 +127,10 @@ class MusicKitJSWrapper {
|
||||
* @returns {boolean[]} Returns an array, where the first element indicates login status, the second one, if an error was encountered
|
||||
*/
|
||||
getAuth (): boolean[] {
|
||||
return [ this.isLoggedIn, this.hasEncounteredAuthError ];
|
||||
return [
|
||||
this.isLoggedIn,
|
||||
this.hasEncounteredAuthError
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,8 +142,8 @@ class MusicKitJSWrapper {
|
||||
apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
|
||||
if ( this.config.devToken != '' && this.config.userToken != '' ) {
|
||||
fetch( url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'method': 'GET',
|
||||
'headers': {
|
||||
'Authorization': `Bearer ${ this.config.devToken }`,
|
||||
'Music-User-Token': this.config.userToken
|
||||
}
|
||||
@@ -128,13 +151,19 @@ class MusicKitJSWrapper {
|
||||
if ( res.status === 200 ) {
|
||||
res.json().then( json => {
|
||||
try {
|
||||
callback( { 'status': 'ok', 'data': json } );
|
||||
} catch( err ) { /* empty */}
|
||||
callback( {
|
||||
'status': 'ok',
|
||||
'data': json
|
||||
} );
|
||||
} catch ( err ) { /* empty */ }
|
||||
} );
|
||||
} else {
|
||||
try {
|
||||
callback( { 'status': 'error', 'error': res.status } );
|
||||
} catch( err ) { /* empty */}
|
||||
callback( {
|
||||
'status': 'error',
|
||||
'error': res.status
|
||||
} );
|
||||
} catch ( err ) { /* empty */ }
|
||||
}
|
||||
} );
|
||||
} else return;
|
||||
@@ -152,34 +181,41 @@ class MusicKitJSWrapper {
|
||||
|
||||
setPlaylistByID ( id: string ): Promise<void> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
this.musicKit.setQueue( { playlist: id } ).then( () => {
|
||||
this.musicKit.setQueue( {
|
||||
'playlist': id
|
||||
} ).then( () => {
|
||||
const pl = this.musicKit.queue.items;
|
||||
const songs: Song[] = [];
|
||||
|
||||
for ( const item in pl ) {
|
||||
let url = pl[ item ].attributes.artwork.url;
|
||||
|
||||
url = url.replace( '{w}', pl[ item ].attributes.artwork.width );
|
||||
url = url.replace( '{h}', pl[ item ].attributes.artwork.height );
|
||||
const song: Song = {
|
||||
artist: pl[ item ].attributes.artistName,
|
||||
cover: url,
|
||||
duration: pl[ item ].attributes.durationInMillis / 1000,
|
||||
id: pl[ item ].id,
|
||||
origin: 'apple-music',
|
||||
title: pl[ item ].attributes.name,
|
||||
genres: pl[ item ].attributes.genreNames
|
||||
}
|
||||
'artist': pl[ item ].attributes.artistName,
|
||||
'cover': url,
|
||||
'duration': pl[ item ].attributes.durationInMillis / 1000,
|
||||
'id': pl[ item ].id,
|
||||
'origin': 'apple-music',
|
||||
'title': pl[ item ].attributes.name,
|
||||
'genres': pl[ item ].attributes.genreNames
|
||||
};
|
||||
|
||||
songs.push( song );
|
||||
}
|
||||
|
||||
this.playlist = songs;
|
||||
this.setShuffle( this.isShuffleEnabled );
|
||||
this.queuePos = 0;
|
||||
this.playingSongID = this.queue[ 0 ];
|
||||
this.prepare( this.playingSongID );
|
||||
resolve();
|
||||
} ).catch( err => {
|
||||
console.error( err );
|
||||
reject( err );
|
||||
} );
|
||||
} )
|
||||
.catch( err => {
|
||||
console.error( err );
|
||||
reject( err );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
@@ -192,20 +228,25 @@ class MusicKitJSWrapper {
|
||||
if ( this.playlist.length > 0 ) {
|
||||
this.playingSongID = playlistID;
|
||||
this.isPreparedToPlay = true;
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] === playlistID ) {
|
||||
this.queuePos = parseInt( el );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.setQueue( { 'song': this.playlist[ this.playingSongID ].id } ).then( () => {
|
||||
this.musicKit.setQueue( {
|
||||
'song': this.playlist[ this.playingSongID ].id
|
||||
} ).then( () => {
|
||||
setTimeout( () => {
|
||||
this.control( 'play' );
|
||||
}, 500 );
|
||||
} ).catch( ( err ) => {
|
||||
console.log( err );
|
||||
} );
|
||||
} )
|
||||
.catch( err => {
|
||||
console.log( err );
|
||||
} );
|
||||
} else {
|
||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||
this.audioPlayer.src = this.playlist[ this.playingSongID ].id;
|
||||
@@ -213,6 +254,7 @@ class MusicKitJSWrapper {
|
||||
this.control( 'play' );
|
||||
}, 500 );
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -226,50 +268,63 @@ class MusicKitJSWrapper {
|
||||
*/
|
||||
control ( action: ControlAction ): boolean {
|
||||
switch ( action ) {
|
||||
case "play":
|
||||
case 'play':
|
||||
if ( this.isPreparedToPlay ) {
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.play();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.play();
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case "pause":
|
||||
|
||||
case 'pause':
|
||||
if ( this.isPreparedToPlay ) {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.pause();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.pause();
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
case "back-10":
|
||||
|
||||
case 'back-10':
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
case "skip-10":
|
||||
|
||||
case 'skip-10':
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
|
||||
|
||||
return false;
|
||||
} else {
|
||||
if ( this.repeatMode !== 'once' ) {
|
||||
this.control( 'next' );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.musicKit.seekToTime( 0 );
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -283,32 +338,42 @@ class MusicKitJSWrapper {
|
||||
this.audioPlayer.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
case "next":
|
||||
|
||||
case 'next':
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.queuePos < this.queue.length - 1 ) {
|
||||
this.queuePos += 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.queuePos = 0;
|
||||
|
||||
if ( this.repeatMode !== 'all' ) {
|
||||
this.control( 'pause' );
|
||||
} else {
|
||||
this.playingSongID = this.queue[ this.queuePos ];
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case "previous":
|
||||
|
||||
case 'previous':
|
||||
this.control( 'pause' );
|
||||
|
||||
if ( this.queuePos > 0 ) {
|
||||
this.queuePos -= 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this.queuePos = this.queue.length - 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -317,15 +382,22 @@ class MusicKitJSWrapper {
|
||||
setShuffle ( enabled: boolean ) {
|
||||
this.isShuffleEnabled = enabled;
|
||||
this.queue = [];
|
||||
|
||||
if ( enabled ) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const d = [];
|
||||
|
||||
for ( const el in this.playlist ) {
|
||||
d.push( parseInt( el ) );
|
||||
}
|
||||
this.queue = d.map( value => ( { value, sort: Math.random() } ) )
|
||||
.sort( ( a, b ) => a.sort - b.sort )
|
||||
.map( ( { value } ) => value );
|
||||
|
||||
this.queue = d.map( value => ( {
|
||||
value,
|
||||
'sort': Math.random()
|
||||
} ) )
|
||||
.sort( ( a, b ) => a.sort - b.sort )
|
||||
.map( ( {
|
||||
value
|
||||
} ) => value );
|
||||
this.queue.splice( this.queue.indexOf( this.playingSongID ), 1 );
|
||||
this.queue.push( this.playingSongID );
|
||||
this.queue.reverse();
|
||||
@@ -334,6 +406,7 @@ class MusicKitJSWrapper {
|
||||
this.queue.push( parseInt( song ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Find current song ID in queue
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] === this.playingSongID ) {
|
||||
@@ -359,29 +432,37 @@ class MusicKitJSWrapper {
|
||||
moveSong ( move: SongMove ) {
|
||||
const newQueue = [];
|
||||
const finishedQueue = [];
|
||||
|
||||
let songID = 0;
|
||||
|
||||
for ( const song in this.playlist ) {
|
||||
if ( this.playlist[ song ].id === move.songID ) {
|
||||
songID = parseInt( song );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] !== songID ) {
|
||||
newQueue.push( this.queue[ el ] );
|
||||
}
|
||||
}
|
||||
|
||||
let hasBeenAdded = false;
|
||||
|
||||
for ( const el in newQueue ) {
|
||||
if ( parseInt( el ) === move.newPos ) {
|
||||
finishedQueue.push( songID );
|
||||
hasBeenAdded = true;
|
||||
}
|
||||
|
||||
finishedQueue.push( newQueue[ el ] );
|
||||
}
|
||||
|
||||
if ( !hasBeenAdded ) {
|
||||
finishedQueue.push( songID );
|
||||
}
|
||||
|
||||
this.queue = finishedQueue;
|
||||
}
|
||||
|
||||
@@ -435,9 +516,11 @@ class MusicKitJSWrapper {
|
||||
*/
|
||||
getQueue (): Song[] {
|
||||
const data = [];
|
||||
|
||||
for ( const el in this.queue ) {
|
||||
data.push( this.playlist[ this.queue[ el ] ] );
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -449,13 +532,14 @@ class MusicKitJSWrapper {
|
||||
getUserPlaylists ( cb: ( data: object ) => void ): boolean {
|
||||
if ( this.isLoggedIn ) {
|
||||
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getPlaying ( ): boolean {
|
||||
getPlaying ( ): boolean {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
return this.musicKit.isPlaying;
|
||||
} else {
|
||||
@@ -467,17 +551,21 @@ class MusicKitJSWrapper {
|
||||
// TODO: Make storefront adjustable
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const queryParameters = {
|
||||
term: ( searchTerm ),
|
||||
types: [ 'songs' ],
|
||||
'term': searchTerm,
|
||||
'types': [ 'songs' ],
|
||||
};
|
||||
this.musicKit.api.music( `v1/catalog/ch/search`, queryParameters ).then( results => {
|
||||
resolve( results );
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
reject( e );
|
||||
} );
|
||||
|
||||
this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters )
|
||||
.then( results => {
|
||||
resolve( results );
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
reject( e );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MusicKitJSWrapper;
|
||||
@@ -1,34 +1,41 @@
|
||||
/*
|
||||
* MusicPlayerV2 - notificationHandler.ts
|
||||
*
|
||||
* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
// These functions handle connections to the backend with socket.io
|
||||
|
||||
import { io, type Socket } from "socket.io-client"
|
||||
import type { SSEMap } from "./song";
|
||||
import {
|
||||
io, type Socket
|
||||
} from 'socket.io-client';
|
||||
import type {
|
||||
SSEMap
|
||||
} from './song';
|
||||
|
||||
class NotificationHandler {
|
||||
|
||||
socket: Socket;
|
||||
|
||||
roomName: string;
|
||||
|
||||
roomToken: string;
|
||||
|
||||
isConnected: boolean;
|
||||
|
||||
useSocket: boolean;
|
||||
|
||||
eventSource?: EventSource;
|
||||
|
||||
toBeListenedForItems: SSEMap;
|
||||
|
||||
reconnectRetryCount: number;
|
||||
|
||||
lastEmitTimestamp: number;
|
||||
|
||||
openConnectionsCount: number;
|
||||
|
||||
pendingRequestCount: number;
|
||||
|
||||
connectionWasSuccessful: boolean;
|
||||
|
||||
constructor () {
|
||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||
autoConnect: false,
|
||||
'autoConnect': false,
|
||||
} );
|
||||
this.roomName = '';
|
||||
this.roomToken = '';
|
||||
@@ -50,28 +57,37 @@ class NotificationHandler {
|
||||
*/
|
||||
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( text => {
|
||||
this.roomToken = text;
|
||||
this.roomName = roomName;
|
||||
|
||||
if ( this.useSocket ) {
|
||||
this.socket.connect();
|
||||
this.socket.emit( 'create-room', {
|
||||
name: this.roomName,
|
||||
token: this.roomToken
|
||||
}, ( res: { status: boolean, msg: string } ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
this.socket.emit(
|
||||
'create-room', {
|
||||
'name': this.roomName,
|
||||
'token': this.roomToken
|
||||
}, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string
|
||||
} ) => {
|
||||
if ( res.status === true ) {
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
}
|
||||
}
|
||||
} );
|
||||
);
|
||||
} else {
|
||||
this.sseConnect().then( () => {
|
||||
resolve();
|
||||
} ).catch( );
|
||||
} )
|
||||
.catch( );
|
||||
}
|
||||
} );
|
||||
} else if ( res.status === 409 ) {
|
||||
@@ -90,9 +106,13 @@ class NotificationHandler {
|
||||
if ( this.reconnectRetryCount < 5 ) {
|
||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||
this.openConnectionsCount += 1;
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
|
||||
'withCredentials': true
|
||||
} );
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true;
|
||||
@@ -100,22 +120,23 @@ class NotificationHandler {
|
||||
this.reconnectRetryCount = 0;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = ( e ) => {
|
||||
this.eventSource.onmessage = e => {
|
||||
const d = JSON.parse( e.data );
|
||||
|
||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||
this.toBeListenedForItems[ d.type ]( d.data );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = ( e ) => {
|
||||
this.eventSource.onerror = e => {
|
||||
if ( this.isConnected ) {
|
||||
this.isConnected = false;
|
||||
this.eventSource?.close();
|
||||
this.openConnectionsCount -= 1;
|
||||
console.debug( e );
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||
|
||||
this.eventSource = undefined;
|
||||
|
||||
@@ -125,27 +146,28 @@ class NotificationHandler {
|
||||
}, 1000 * this.reconnectRetryCount );
|
||||
}
|
||||
};
|
||||
} else if ( res.status === 403 || res.status === 401 || res.status === 404 ) {
|
||||
} else if ( res.status === 403 || res.status === 401 || res.status === 404 || res.status === 402 ) {
|
||||
document.dispatchEvent( new Event( 'musicplayer:autherror' ) );
|
||||
reject( 'ERR_UNAUTHORIZED' );
|
||||
} else {
|
||||
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
|
||||
}
|
||||
} ).catch( () => {
|
||||
if ( !this.connectionWasSuccessful ) {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} else {
|
||||
this.openConnectionsCount -= 1;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
|
||||
} )
|
||||
.catch( () => {
|
||||
if ( !this.connectionWasSuccessful ) {
|
||||
reject( 'ERR_ROOM_CONNECTING' );
|
||||
} else {
|
||||
this.openConnectionsCount -= 1;
|
||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
|
||||
|
||||
this.eventSource = undefined;
|
||||
this.eventSource = undefined;
|
||||
|
||||
this.reconnectRetryCount += 1;
|
||||
setTimeout( () => {
|
||||
this.sseConnect();
|
||||
}, 1000 * this.reconnectRetryCount );
|
||||
}
|
||||
} );
|
||||
this.reconnectRetryCount += 1;
|
||||
setTimeout( () => {
|
||||
this.sseConnect();
|
||||
}, 1000 * this.reconnectRetryCount );
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
@@ -169,9 +191,14 @@ class NotificationHandler {
|
||||
emit ( event: string, data: any ): void {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( event, { 'roomToken': this.roomToken, 'roomName': this.roomName, 'data': data } );
|
||||
this.socket.emit( event, {
|
||||
'roomToken': this.roomToken,
|
||||
'roomName': this.roomName,
|
||||
'data': data
|
||||
} );
|
||||
} else {
|
||||
const now = new Date().getTime();
|
||||
|
||||
if ( this.lastEmitTimestamp < now - 250 ) {
|
||||
this.lastEmitTimestamp = now;
|
||||
this.sendEmitConventionally( event, data );
|
||||
@@ -189,10 +216,15 @@ class NotificationHandler {
|
||||
|
||||
sendEmitConventionally ( event: string, data: any ): void {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'roomToken': this.roomToken, 'data': data } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'event': event,
|
||||
'roomName': this.roomName,
|
||||
'roomToken': this.roomToken,
|
||||
'data': data
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -222,22 +254,32 @@ class NotificationHandler {
|
||||
async disconnect (): Promise<void> {
|
||||
if ( this.isConnected ) {
|
||||
if ( this.useSocket ) {
|
||||
this.socket.emit( 'delete-room', {
|
||||
name: this.roomName,
|
||||
token: this.roomToken
|
||||
}, ( res: { status: boolean, msg: string } ) => {
|
||||
this.socket.disconnect();
|
||||
if ( !res.status ) {
|
||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||
this.socket.emit(
|
||||
'delete-room', {
|
||||
'name': this.roomName,
|
||||
'token': this.roomToken
|
||||
}, ( res: {
|
||||
'status': boolean,
|
||||
'msg': string
|
||||
} ) => {
|
||||
this.socket.disconnect();
|
||||
|
||||
if ( !res.status ) {
|
||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
} );
|
||||
);
|
||||
} else {
|
||||
fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'roomName': this.roomName, 'roomToken': this.roomToken } ),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'method': 'post',
|
||||
'body': JSON.stringify( {
|
||||
'roomName': this.roomName,
|
||||
'roomToken': this.roomToken
|
||||
} ),
|
||||
'credentials': 'include',
|
||||
'headers': {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
}
|
||||
@@ -247,10 +289,12 @@ class NotificationHandler {
|
||||
} else {
|
||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||
}
|
||||
|
||||
return;
|
||||
} ).catch( () => {
|
||||
return;
|
||||
} );
|
||||
} )
|
||||
.catch( () => {
|
||||
return;
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,6 +302,7 @@ class NotificationHandler {
|
||||
getRoomName (): string {
|
||||
return this.roomName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default NotificationHandler;
|
||||
70
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
70
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
@@ -4,90 +4,90 @@ export interface Song {
|
||||
/**
|
||||
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_
|
||||
*/
|
||||
id: string;
|
||||
'id': string;
|
||||
|
||||
/**
|
||||
* Origin of the song
|
||||
*/
|
||||
origin: Origin;
|
||||
'origin': Origin;
|
||||
|
||||
/**
|
||||
* The cover image as a URL
|
||||
*/
|
||||
cover: string;
|
||||
'cover': string;
|
||||
|
||||
/**
|
||||
* The artist of the song
|
||||
*/
|
||||
artist: string;
|
||||
'artist': string;
|
||||
|
||||
/**
|
||||
* The name of the song
|
||||
*/
|
||||
title: string;
|
||||
'title': string;
|
||||
|
||||
/**
|
||||
* Duration of the song in milliseconds
|
||||
*/
|
||||
duration: number;
|
||||
'duration': number;
|
||||
|
||||
/**
|
||||
* (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there
|
||||
*/
|
||||
genres?: string[];
|
||||
'genres'?: string[];
|
||||
|
||||
/**
|
||||
* (OPTIONAL) This will be displayed in brackets on the showcase screens
|
||||
*/
|
||||
additionalInfo?: string;
|
||||
'additionalInfo'?: string;
|
||||
}
|
||||
|
||||
export interface SongTransmitted {
|
||||
title: string;
|
||||
artist: string;
|
||||
duration: number;
|
||||
cover: string;
|
||||
additionalInfo?: string;
|
||||
'title': string;
|
||||
'artist': string;
|
||||
'duration': number;
|
||||
'cover': string;
|
||||
'additionalInfo'?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ReadFile {
|
||||
url: string;
|
||||
filename: string;
|
||||
'url': string;
|
||||
'filename': string;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
data: {
|
||||
results: {
|
||||
songs: {
|
||||
data: AppleMusicSongData[],
|
||||
href: string;
|
||||
'data': {
|
||||
'results': {
|
||||
'songs': {
|
||||
'data': AppleMusicSongData[],
|
||||
'href': string;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppleMusicSongData {
|
||||
id: string,
|
||||
type: string;
|
||||
href: string;
|
||||
attributes: {
|
||||
albumName: string;
|
||||
artistName: string;
|
||||
artwork: {
|
||||
width: number,
|
||||
height: number,
|
||||
url: string
|
||||
'id': string,
|
||||
'type': string;
|
||||
'href': string;
|
||||
'attributes': {
|
||||
'albumName': string;
|
||||
'artistName': string;
|
||||
'artwork': {
|
||||
'width': number,
|
||||
'height': number,
|
||||
'url': string
|
||||
},
|
||||
name: string;
|
||||
genreNames: string[];
|
||||
durationInMillis: number;
|
||||
'name': string;
|
||||
'genreNames': string[];
|
||||
'durationInMillis': number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SongMove {
|
||||
songID: string;
|
||||
newPos: number;
|
||||
'songID': string;
|
||||
'newPos': number;
|
||||
}
|
||||
|
||||
export interface SSEMap {
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
/*
|
||||
* LanguageSchoolHossegorBookingSystem - userStore.js
|
||||
*
|
||||
* Created by Janis Hutz 10/27/2023, Licensed under a proprietary License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
defineStore
|
||||
} from 'pinia';
|
||||
|
||||
|
||||
// FOSS-VERSION: To enable the UI to be used with the FOSS version, change "isUserAuth" to true, you will be "logged in"
|
||||
export const useUserStore = defineStore( 'user', {
|
||||
state: () => ( { 'isUserAuth': true, 'hasSubscribed': false, 'isUsingKeyboard': false, 'username': '', 'isFOSSVersion': false } ),
|
||||
getters: {
|
||||
getUserAuthenticated: ( state ) => state.isUserAuth,
|
||||
getSubscriptionStatus: ( state ) => state.hasSubscribed,
|
||||
'state': () => ( {
|
||||
'isUserAuth': false,
|
||||
'hasSubscribed': false,
|
||||
'isUsingKeyboard': false,
|
||||
'isFOSSVersion': false
|
||||
} ),
|
||||
'getters': {
|
||||
'getUserAuthenticated': state => state.isUserAuth,
|
||||
'getSubscriptionStatus': state => state.hasSubscribed,
|
||||
},
|
||||
actions: {
|
||||
'actions': {
|
||||
setUserAuth ( auth: boolean ) {
|
||||
this.isUserAuth = auth;
|
||||
},
|
||||
setSubscriptionStatus ( status: boolean ) {
|
||||
this.hasSubscribed = status;
|
||||
},
|
||||
setUsername ( username: string ) {
|
||||
this.username = username;
|
||||
},
|
||||
setKeyboardUsageStatus ( status: boolean ) {
|
||||
this.isUsingKeyboard = status;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
<template>
|
||||
<div class="app-view">
|
||||
<button id="logout" @click="logout()"><span class="material-symbols-outlined">logout</span></button>
|
||||
<div class="loading-view" v-if="!hasFinishedLoading">
|
||||
<button id="logout" @click="logout()">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
</button>
|
||||
<div v-if="!hasFinishedLoading" class="loading-view">
|
||||
<h1>Loading...</h1>
|
||||
</div>
|
||||
<div class="home-view" v-else-if="hasFinishedLoading && isReady">
|
||||
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"
|
||||
:is-logged-in="isLoggedIntoAppleMusic" @custom-playlist="( pl ) => selectCustomPlaylist( pl )"></libraryView>
|
||||
<div v-else-if="hasFinishedLoading && isReady" class="home-view">
|
||||
<libraryView
|
||||
class="library-view"
|
||||
:playlists="playlists"
|
||||
:is-logged-in="isLoggedIntoAppleMusic"
|
||||
@selected-playlist="( id ) => { selectPlaylist( id ) }"
|
||||
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="login-view">
|
||||
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
|
||||
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">Log into Apple Music</button>
|
||||
<button class="fancy-button" title="This allows you to use local playlists only. Cover images for your songs will be fetched from the apple music api as good as possible" @click="skipLogin()">Continue without logging in</button>
|
||||
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">
|
||||
Log into Apple Music
|
||||
</button>
|
||||
<button
|
||||
class="fancy-button"
|
||||
title="This allows you to use local playlists only.
|
||||
Cover images for your songs will be fetched from the apple music api as good as possible"
|
||||
@click="skipLogin()"
|
||||
>
|
||||
Continue without logging in
|
||||
</button>
|
||||
</div>
|
||||
<playerView :class="'player-view' + ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||
ref="player"></playerView>
|
||||
<playerView
|
||||
ref="player"
|
||||
:class="'player-view'
|
||||
+ ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )"
|
||||
@player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||
/>
|
||||
<!-- TODO: Call to backend to check if user has access -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,10 +42,16 @@
|
||||
<script setup lang="ts">
|
||||
import playerView from '@/components/playerView.vue';
|
||||
import libraryView from '@/components/libraryView.vue';
|
||||
import { ref } from 'vue';
|
||||
import type { ReadFile } from '@/scripts/song';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
import type {
|
||||
ReadFile
|
||||
} from '@/scripts/song';
|
||||
import router from '@/router';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/stores/userStore';
|
||||
|
||||
const isLoggedIntoAppleMusic = ref( false );
|
||||
const isReady = ref( false );
|
||||
@@ -41,7 +67,7 @@
|
||||
} else {
|
||||
isShowingFullScreenPlayer.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let loginChecker = 0;
|
||||
|
||||
@@ -51,7 +77,7 @@
|
||||
if ( player.value.getAuth()[ 0 ] ) {
|
||||
isLoggedIntoAppleMusic.value = true;
|
||||
isReady.value = true;
|
||||
player.value.getPlaylists( ( data ) => {
|
||||
player.value.getPlaylists( data => {
|
||||
playlists.value = data.data.data;
|
||||
} );
|
||||
clearInterval( loginChecker );
|
||||
@@ -60,25 +86,27 @@
|
||||
alert( 'An error occurred when logging you in. Please try again!' );
|
||||
}
|
||||
}, 500 );
|
||||
}
|
||||
};
|
||||
|
||||
const skipLogin = () => {
|
||||
isReady.value = true;
|
||||
isLoggedIntoAppleMusic.value = false;
|
||||
player.value.skipLogin();
|
||||
}
|
||||
};
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
player.value.selectPlaylist( id );
|
||||
player.value.controlUI( 'show' );
|
||||
}
|
||||
};
|
||||
|
||||
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||
player.value.selectCustomPlaylist( playlist );
|
||||
player.value.controlUI( 'show' );
|
||||
}
|
||||
};
|
||||
|
||||
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', { credentials: 'include' } ).then( res => {
|
||||
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', {
|
||||
'credentials': 'include'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( text => {
|
||||
if ( text === 'ok' ) {
|
||||
@@ -90,7 +118,7 @@
|
||||
router.push( '/get' );
|
||||
}
|
||||
} );
|
||||
} else if ( res.status === 404 ) {
|
||||
} else if ( res.status === 402 ) {
|
||||
userStore.setSubscriptionStatus( false );
|
||||
router.push( '/get' );
|
||||
sessionStorage.setItem( 'getRedirectionReason', 'notOwned' );
|
||||
@@ -102,7 +130,7 @@
|
||||
const logout = () => {
|
||||
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
229
MusicPlayerV2-GUI/src/views/BarView.vue
Normal file
229
MusicPlayerV2-GUI/src/views/BarView.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type Ref,
|
||||
computed, ref
|
||||
} from 'vue';
|
||||
|
||||
interface FullConfig {
|
||||
'offering': Bars;
|
||||
'ages': Ages;
|
||||
}
|
||||
|
||||
interface Bars {
|
||||
[name: string]: {
|
||||
'offering': BarConfig;
|
||||
'name': string;
|
||||
'id': string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Ages {
|
||||
'18+': string;
|
||||
'16-18': string;
|
||||
}
|
||||
|
||||
interface BarConfig {
|
||||
[id: string]: Offer
|
||||
}
|
||||
|
||||
interface Offer {
|
||||
'name': string;
|
||||
'price': number; // In cents
|
||||
'depot'?: number; // In cents
|
||||
'showLine'?: boolean;
|
||||
'id': string;
|
||||
}
|
||||
|
||||
interface Selection {
|
||||
[id: string]: number;
|
||||
}
|
||||
|
||||
const ages: Ref<Ages> = ref( {
|
||||
'18+': '',
|
||||
'16-18': '',
|
||||
'below': ''
|
||||
} );
|
||||
const offering: Ref<Bars> = ref( {} );
|
||||
const selection: Ref<Selection> = ref( {} );
|
||||
const selectedBar: Ref<string> = ref( '' );
|
||||
const enableDepotReminder = ref( true );
|
||||
|
||||
let cashinInDepot = false;
|
||||
|
||||
fetch( '/bar-config.json', {
|
||||
'cache': 'no-store'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.json().then( json => {
|
||||
const data: FullConfig = json;
|
||||
|
||||
offering.value = data.offering;
|
||||
ages.value = data.ages;
|
||||
} );
|
||||
} else {
|
||||
alert( 'Failed to load' );
|
||||
}
|
||||
} );
|
||||
|
||||
const reset = ( skipCheck = true ) => {
|
||||
if ( !skipCheck && !Object.keys( offering.value ).includes( selectedBar.value ) ) return;
|
||||
|
||||
if ( cashinInDepot && enableDepotReminder.value ) alert( 'Hand out chips for depot' );
|
||||
|
||||
cashinInDepot = false;
|
||||
|
||||
const keys = Object.keys( offering.value[ selectedBar.value ].offering );
|
||||
|
||||
selection.value = {};
|
||||
|
||||
keys.forEach( val => {
|
||||
selection.value[ val ] = 0;
|
||||
} );
|
||||
};
|
||||
|
||||
const total = computed( () => {
|
||||
const keys = Object.keys( selection.value );
|
||||
|
||||
let totalPrice = 0;
|
||||
let totalDepot = 0;
|
||||
|
||||
for ( let i = 0; i < keys.length; i++ ) {
|
||||
const o = selection.value[ keys[ i ] ];
|
||||
|
||||
totalPrice += o * offering.value[ selectedBar.value ].offering[ keys[ i ] ].price;
|
||||
totalDepot += o * ( offering.value[ selectedBar.value ].offering[ keys[ i ] ].depot ?? 0 );
|
||||
}
|
||||
|
||||
if ( totalDepot > 0 ) {
|
||||
cashinInDepot = true;
|
||||
}
|
||||
|
||||
totalPrice += totalDepot;
|
||||
|
||||
return ( totalPrice / 100 ) + ( totalDepot ? ` (Depot = ${ totalDepot })` : '' );
|
||||
} );
|
||||
|
||||
const changeValue = ( id: string, amount: number ) => {
|
||||
selection.value[ id ] += amount;
|
||||
|
||||
if ( selection.value[ id ] < 0 ) {
|
||||
selection.value[ id ] = 0;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bar-utility">
|
||||
<div style="margin: 0">
|
||||
<label> Depot chips reminder</label>
|
||||
<input v-model="enableDepotReminder" type="checkbox">
|
||||
</div>
|
||||
<h1 style="margin: 15px;">
|
||||
Bar utility
|
||||
</h1>
|
||||
<div>
|
||||
<label for="bar-select">Select bar </label>
|
||||
<select id="bar-select" v-model="selectedBar" @change="reset()">
|
||||
<option v-for="bar in Object.values( offering )" :key="bar.id" :value="bar.id">
|
||||
{{ bar.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="reset( false )">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<p>Check ages! (18+: {{ ages[ '18+' ] }}, 16-18: {{ ages[ '16-18' ] }})</p>
|
||||
<p v-if="Object.keys( offering ).includes( selectedBar )">
|
||||
Total: CHF {{ total }}
|
||||
</p>
|
||||
<table v-if="Object.keys( offering ).includes( selectedBar )" class="offering-wrapper">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="offer in offering[ selectedBar ].offering"
|
||||
:key="offer.id"
|
||||
:class="[ 'offering', offer.showLine ? 'show-line' : '' ]"
|
||||
>
|
||||
<td>
|
||||
<p>
|
||||
{{ offer.name }} (CHF {{ offer.price / 100 }}{{
|
||||
offer.depot ? ' + ' + ( offer.depot / 100 ) : '' }})
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<button class="inc-dec" @click="changeValue( offer.id, 1 )">
|
||||
+
|
||||
</button>
|
||||
<p>{{ selection[ offer.id ] }}</p>
|
||||
<button class="inc-dec" @click="changeValue( offer.id, -1 )">
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="Object.keys( offering ).includes( selectedBar )">
|
||||
Total: CHF {{ total }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bar-utility {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
>.offering-wrapper {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 5vh;
|
||||
|
||||
.offering {
|
||||
&.show-line {
|
||||
>td {
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
}
|
||||
|
||||
>td {
|
||||
padding: 5px;
|
||||
p {
|
||||
margin: 0;
|
||||
margin-right: 15px;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
p {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
>.inc-dec {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: solid var( --primary-color ) 1px;
|
||||
border-radius: 20px;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
color: var( --primary-color );
|
||||
touch-action: manipulation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,35 @@
|
||||
<div class="top-view">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||
<h1>MusicPlayer</h1>
|
||||
<p v-if="reasonForRedirectHere" style="color: red;">{{ reasons[ reasonForRedirectHere ] }}</p>
|
||||
<p v-if="!reasonForRedirectHere"><i>An Open Source, browser-based MusicPlayer with beautiful graphics</i></p>
|
||||
<p v-if="reasonForRedirectHere" style="color: red;">
|
||||
{{ reasons[ reasonForRedirectHere ] }}
|
||||
</p>
|
||||
<p v-if="!reasonForRedirectHere">
|
||||
<i>An Open Source, browser-based MusicPlayer with beautiful graphics</i>
|
||||
</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<a href="https://store.janishutz.com/product/com.janishutz.MusicPlayer" class="fancy-button" target="_blank">Subscribe</a>
|
||||
<a href="/" class="fancy-button" style="margin-left: 10px;" v-if="!reasonForRedirectHere">Log in</a>
|
||||
<button href="/" class="fancy-button" style="margin-left: 10px;" v-if="reasonForRedirectHere" @click="logout()">Log out</button>
|
||||
<a href="https://github.com/simplePCBuilding/MusicPlayerV2" class="fancy-button" style="margin-left: 10px;" target="_blank">GitHub</a>
|
||||
<a
|
||||
v-if="!reasonForRedirectHere"
|
||||
href="/"
|
||||
class="fancy-button"
|
||||
style="margin-left: 10px;"
|
||||
>Log in</a>
|
||||
<button
|
||||
v-if="reasonForRedirectHere"
|
||||
href="/"
|
||||
class="fancy-button"
|
||||
style="margin-left: 10px;"
|
||||
@click="logout()"
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/simplePCBuilding/MusicPlayerV2"
|
||||
class="fancy-button"
|
||||
style="margin-left: 10px;"
|
||||
target="_blank"
|
||||
>GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -20,7 +42,10 @@
|
||||
<p>Use MusicPlayer in conjunction with Apple Music</p>
|
||||
|
||||
<h2>Share your playlist</h2>
|
||||
<p>You can share your playlist on a beautifully animated public page, so that other people can join in and view your playlist</p>
|
||||
<p>
|
||||
You can share your playlist on a beautifully animated public page,
|
||||
so that other people can join in and view your playlist
|
||||
</p>
|
||||
|
||||
<h2>Fully browser based</h2>
|
||||
<p>No installation required when using MusicPlayer on <a href="https://music.janishutz.com">music.janishutz.com</a></p>
|
||||
@@ -29,7 +54,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type Ref } from 'vue';
|
||||
import {
|
||||
ref, type Ref
|
||||
} from 'vue';
|
||||
|
||||
interface Reasons {
|
||||
[key: string]: string;
|
||||
@@ -39,12 +66,13 @@
|
||||
'notOwned': 'Please subscribe to use MusicPlayer here, or download and install it manually from GitHub!',
|
||||
} );
|
||||
const reasonForRedirectHere = ref( sessionStorage.getItem( 'getRedirectionReason' ) );
|
||||
|
||||
sessionStorage.removeItem( 'getRedirectionReason' );
|
||||
|
||||
const logout = () => {
|
||||
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,54 +1,48 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||
<button :class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )" @click="login()"
|
||||
style="margin-top: 5vh;" title="Sign in or sign up with janishutz.com ID" v-if="status"
|
||||
>{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}</button>
|
||||
<p v-else>We are sorry, but we were unable to initialize the login services. Please reload the page if you wish to retry!</p>
|
||||
<p style="width: 80%;">MusicPlayer is a browser based Music Player, that allows you to connect other devices, simply with another web-browser, where you can see the current playlist with sleek animations. You can log in using your Apple Music account or load a playlist from your local disk, simply by selecting the songs using a file picker.</p>
|
||||
<router-link to="/get" class="fancy-button">More information</router-link>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
||||
<button
|
||||
:class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )"
|
||||
style="margin-top: 5vh;"
|
||||
title="Sign in or sign up with janishutz.com ID"
|
||||
@click="login()"
|
||||
>
|
||||
{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}
|
||||
</button>
|
||||
<p style="width: 80%;">
|
||||
MusicPlayer is a browser based Music Player, that allows you to connect other devices,
|
||||
simply with another web-browser, where you can see the current playlist with sleek animations.
|
||||
You can log in using your Apple Music account or load a playlist from your local disk,
|
||||
simply by selecting the songs using a file picker.
|
||||
</p>
|
||||
<router-link to="/get" class="fancy-button">
|
||||
More information
|
||||
</router-link>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO: Make possible to install and use without account, if using FOSS version
|
||||
import router from '@/router';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import {
|
||||
RouterLink
|
||||
} from 'vue-router';
|
||||
import notificationsModule from '@/components/notificationsModule.vue';
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
import router from '@/router';
|
||||
import sdk from '@janishutz/login-sdk-browser';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/stores/userStore';
|
||||
|
||||
const notifications = ref( notificationsModule );
|
||||
const isTryingToSignIn = ref( true );
|
||||
|
||||
interface JanishutzIDSDK {
|
||||
setLoginSDKURL: ( url: string ) => undefined;
|
||||
createSession: () => undefined;
|
||||
verifySession: () => Promise<JHIDSessionStatus>
|
||||
}
|
||||
|
||||
interface JHIDSessionStatus {
|
||||
status: boolean;
|
||||
username: string;
|
||||
}
|
||||
|
||||
let sdk: JanishutzIDSDK;
|
||||
const status = ref( true );
|
||||
|
||||
if ( typeof( JanishutzID ) !== 'undefined' ) {
|
||||
sdk = JanishutzID();
|
||||
sdk.setLoginSDKURL( localStorage.getItem( 'url' ) ?? '' );
|
||||
} else {
|
||||
setTimeout( () => {
|
||||
notifications.value.createNotification( 'Unable to initialize account services!', 5, 'error' );
|
||||
}, 1000 );
|
||||
status.value = false;
|
||||
}
|
||||
|
||||
const login = () => {
|
||||
sdk.createSession();
|
||||
}
|
||||
sdk.login();
|
||||
};
|
||||
|
||||
const store = useUserStore();
|
||||
|
||||
@@ -56,23 +50,27 @@
|
||||
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||
localStorage.removeItem( 'redirect' );
|
||||
} else {
|
||||
if ( typeof( sdk ) !== 'undefined' ) {
|
||||
sdk.verifySession().then( res => {
|
||||
if ( res.status ) {
|
||||
sdk.verifyFull()
|
||||
.then( res => {
|
||||
if ( res ) {
|
||||
store.isUserAuth = true;
|
||||
store.username = res.username;
|
||||
|
||||
if ( localStorage.getItem( 'close-tab' ) ) {
|
||||
localStorage.removeItem( 'close-tab' );
|
||||
window.close();
|
||||
}
|
||||
|
||||
localStorage.setItem( 'login-ok', 'true' );
|
||||
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||
localStorage.removeItem( 'redirect' );
|
||||
} else {
|
||||
isTryingToSignIn.value = false;
|
||||
}
|
||||
} )
|
||||
.catch( e => {
|
||||
console.debug( e );
|
||||
isTryingToSignIn.value = false;
|
||||
} );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
||||
<div class="info">
|
||||
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||
</div>
|
||||
<div class="remote-view">
|
||||
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
||||
<div class="current-song-wrapper">
|
||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||
<img
|
||||
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||
id="current-image"
|
||||
:src="playlist[ playingSong ].cover"
|
||||
class="fancy-view-song-art"
|
||||
crossorigin="anonymous"
|
||||
>
|
||||
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||
<div class="current-song">
|
||||
<h1 style="margin-bottom: 5px;">{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
||||
<h1 style="margin-bottom: 5px;">
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||
</h1>
|
||||
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
|
||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||
<p
|
||||
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||
class="additional-info"
|
||||
>
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||
</p>
|
||||
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" v-bind:key="song.id" class="song-list">
|
||||
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||
<div class="song-details-wrapper">
|
||||
<h3>{{ song.title }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
@@ -32,7 +47,9 @@
|
||||
<div v-else style="max-width: 80%;">
|
||||
<span class="material-symbols-outlined" style="font-size: 4rem;">wifi_off</span>
|
||||
<h1>Couldn't connect!</h1>
|
||||
<p>There does not appear to be a share with the specified name, or an error occurred when connecting.</p>
|
||||
<p>
|
||||
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||
</p>
|
||||
<p>You may <a href="">reload</a> the page to try again!</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,8 +58,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SocketConnection from '@/scripts/connection';
|
||||
import type { Song } from '@/scripts/song';
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
computed, ref, type Ref
|
||||
} from 'vue';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
const playlist: Ref<Song[]> = ref( [] );
|
||||
@@ -52,6 +73,7 @@
|
||||
const hasLoaded = ref( false );
|
||||
const showCouldNotFindRoom = ref( false );
|
||||
const playbackStart = ref( 0 );
|
||||
|
||||
let timeTracker = 0;
|
||||
|
||||
const conn = new SocketConnection();
|
||||
@@ -61,18 +83,22 @@
|
||||
isPlaying.value = d.playbackStatus;
|
||||
playingSong.value = d.playlistIndex;
|
||||
playbackStart.value = d.playbackStart;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
}
|
||||
|
||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ]
|
||||
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
hasLoaded.value = true;
|
||||
conn.registerListener( 'playlist', ( data ) => {
|
||||
conn.registerListener( 'playlist', data => {
|
||||
playlist.value = data;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback', ( data ) => {
|
||||
conn.registerListener( 'playback', data => {
|
||||
isPlaying.value = data;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
} else {
|
||||
@@ -80,72 +106,81 @@
|
||||
}
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback-start', ( data ) => {
|
||||
conn.registerListener( 'playback-start', data => {
|
||||
playbackStart.value = data;
|
||||
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playlist-index', ( data ) => {
|
||||
conn.registerListener( 'playlist-index', data => {
|
||||
playingSong.value = parseInt( data );
|
||||
} );
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
conn.registerListener( 'delete-share', ( _ ) => {
|
||||
alert( 'This share was just deleted. It is no longer available. The page will reload automatically to try and re-establish connection!' );
|
||||
conn.registerListener( 'delete-share', _ => {
|
||||
alert( `This share was just deleted. It is no longer available.
|
||||
The page will reload automatically to try and re-establish connection!` );
|
||||
conn.disconnect();
|
||||
location.reload();
|
||||
} );
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
showCouldNotFindRoom.value = true;
|
||||
} );
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
showCouldNotFindRoom.value = true;
|
||||
} );
|
||||
|
||||
const songQueue = computed( () => {
|
||||
let ret: Song[] = [];
|
||||
let pos = 0;
|
||||
|
||||
for ( let song in playlist.value ) {
|
||||
if ( pos >= playingSong.value ) {
|
||||
ret.push( playlist.value[ song ] );
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} );
|
||||
|
||||
// TODO: Handle disconnect from updater (=> have it disconnect)
|
||||
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song: string ) => {
|
||||
let timeRemaining = 0;
|
||||
|
||||
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||
if ( playlist.value[ i ].id == song ) {
|
||||
break;
|
||||
}
|
||||
|
||||
timeRemaining += playlist.value[ i ].duration;
|
||||
}
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min';
|
||||
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min after starting to play';
|
||||
return 'Playing less than '
|
||||
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} );
|
||||
|
||||
const startTimeTracker = () => {
|
||||
try {
|
||||
clearInterval( timeTracker );
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
timeTracker = setInterval( () => {
|
||||
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||
|
||||
if ( isNaN( progressBar.value ) ) {
|
||||
progressBar.value = 0;
|
||||
}
|
||||
@@ -156,11 +191,11 @@
|
||||
location.reload();
|
||||
}
|
||||
}, 100 );
|
||||
}
|
||||
};
|
||||
|
||||
const stopTimeTracker = () => {
|
||||
clearInterval( timeTracker );
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener( 'visibilitychange', () => {
|
||||
if ( !document.hidden ) {
|
||||
|
||||
@@ -1,34 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="anti-tamper material-symbols-outlined" v-if="isAntiTamperEnabled" @click="secureModeInfo( 'toggle' )">lock</span>
|
||||
<div class="anti-tamper-info" v-if="isShowingSecureModeInfo && isAntiTamperEnabled" @click="secureModeInfo( 'hide' )">Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!</div>
|
||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
||||
<span
|
||||
v-if="isAntiTamperEnabled"
|
||||
class="anti-tamper material-symbols-outlined"
|
||||
@click="secureModeInfo( 'toggle' )"
|
||||
>lock</span>
|
||||
<div
|
||||
v-if="isShowingSecureModeInfo && isAntiTamperEnabled"
|
||||
class="anti-tamper-info"
|
||||
@click="secureModeInfo( 'hide' )"
|
||||
>
|
||||
Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!
|
||||
</div>
|
||||
<div class="info">
|
||||
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||
</div>
|
||||
<div class="remote-view">
|
||||
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||
<div class="current-song-wrapper">
|
||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||
<img
|
||||
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||
id="current-image"
|
||||
:src="playlist[ playingSong ].cover"
|
||||
class="fancy-view-song-art"
|
||||
crossorigin="anonymous"
|
||||
>
|
||||
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||
<div class="current-song">
|
||||
<h1 style="margin-bottom: 5px;">{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
||||
<h1 style="margin-bottom: 5px;">
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||
</h1>
|
||||
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
|
||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||
<p
|
||||
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||
class="additional-info"
|
||||
>
|
||||
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||
</p>
|
||||
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-selector-wrapper">
|
||||
<select v-model="visualizationSettings" @change="handleAnimationChange()">
|
||||
<option value="mic">Microphone (Mic access required)</option>
|
||||
<option value="off">No visualization except background</option>
|
||||
<option value="mic">
|
||||
Microphone (Mic access required)
|
||||
</option>
|
||||
<option value="off">
|
||||
No visualization except background
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" v-bind:key="song.id" class="song-list">
|
||||
<img :src="song.cover" class="song-image">
|
||||
<div v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying" class="playing-symbols">
|
||||
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||
<img v-if="song.cover" :src="song.cover" class="song-image">
|
||||
<span v-else class="material-symbols-outlined song-cover">music_note</span>
|
||||
<div
|
||||
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying"
|
||||
class="playing-symbols"
|
||||
>
|
||||
<div class="playing-symbols-wrapper">
|
||||
<div class="playing-bar" id="bar-1"></div>
|
||||
<div class="playing-bar" id="bar-2"></div>
|
||||
<div class="playing-bar" id="bar-3"></div>
|
||||
<div id="bar-1" class="playing-bar"></div>
|
||||
<div id="bar-2" class="playing-bar"></div>
|
||||
<div id="bar-3" class="playing-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-details-wrapper">
|
||||
@@ -47,10 +80,12 @@
|
||||
</div>
|
||||
<div v-else class="showcase-wrapper">
|
||||
<h1>Couldn't connect!</h1>
|
||||
<p>There does not appear to be a share with the specified name, or an error occurred when connecting.</p>
|
||||
<p>
|
||||
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||
</p>
|
||||
<p>You may reload the page to try again!</p>
|
||||
</div>
|
||||
<div class="background" id="background">
|
||||
<div id="background" class="background">
|
||||
<div class="beat-manual"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,8 +94,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SocketConnection from '@/scripts/connection';
|
||||
import type { Song } from '@/scripts/song';
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
type Ref, computed, ref
|
||||
} from 'vue';
|
||||
import bizualizer from '@/scripts/bizualizer';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
@@ -71,10 +110,11 @@
|
||||
const hasLoaded = ref( false );
|
||||
const showCouldNotFindRoom = ref( false );
|
||||
const playbackStart = ref( 0 );
|
||||
|
||||
let timeTracker = 0;
|
||||
|
||||
const visualizationSettings = ref( 'mic' );
|
||||
const isAntiTamperEnabled = ref( false );
|
||||
|
||||
const conn = new SocketConnection();
|
||||
|
||||
conn.connect().then( d => {
|
||||
@@ -82,22 +122,29 @@
|
||||
isPlaying.value = d.playbackStatus;
|
||||
playingSong.value = d.playlistIndex;
|
||||
playbackStart.value = d.playbackStart;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
}
|
||||
|
||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
progressBar.value
|
||||
= ( pos.value / ( playlist.value[ playingSong.value ]
|
||||
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||
hasLoaded.value = true;
|
||||
|
||||
if ( d.useAntiTamper ) {
|
||||
isAntiTamperEnabled.value = true;
|
||||
notifier();
|
||||
}
|
||||
conn.registerListener( 'playlist', ( data ) => {
|
||||
|
||||
conn.registerListener( 'playlist', data => {
|
||||
playlist.value = data;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback', ( data ) => {
|
||||
conn.registerListener( 'playback', data => {
|
||||
isPlaying.value = data;
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
startTimeTracker();
|
||||
} else {
|
||||
@@ -105,12 +152,12 @@
|
||||
}
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playback-start', ( data ) => {
|
||||
conn.registerListener( 'playback-start', data => {
|
||||
playbackStart.value = data;
|
||||
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||
} );
|
||||
|
||||
conn.registerListener( 'playlist-index', ( data ) => {
|
||||
conn.registerListener( 'playlist-index', data => {
|
||||
playingSong.value = parseInt( data );
|
||||
setTimeout( () => {
|
||||
setBackground();
|
||||
@@ -118,58 +165,64 @@
|
||||
} );
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
conn.registerListener( 'delete-share', ( _ ) => {
|
||||
conn.registerListener( 'delete-share', _ => {
|
||||
alert( 'This share was just deleted. It is no longer available. This page will reload automatically!' );
|
||||
conn.disconnect();
|
||||
location.reload();
|
||||
} );
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
showCouldNotFindRoom.value = true;
|
||||
} );
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
showCouldNotFindRoom.value = true;
|
||||
} );
|
||||
|
||||
const songQueue = computed( () => {
|
||||
let ret: Song[] = [];
|
||||
let pos = 0;
|
||||
|
||||
for ( let song in playlist.value ) {
|
||||
if ( pos >= playingSong.value ) {
|
||||
ret.push( playlist.value[ song ] );
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} );
|
||||
|
||||
// TODO: Handle disconnect from updater (=> have it disconnect)
|
||||
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song: string ) => {
|
||||
let timeRemaining = 0;
|
||||
|
||||
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||
if ( playlist.value[ i ].id == song ) {
|
||||
break;
|
||||
}
|
||||
|
||||
timeRemaining += playlist.value[ i ].duration;
|
||||
}
|
||||
|
||||
if ( isPlaying.value ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min';
|
||||
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min after starting to play';
|
||||
return 'Playing less than '
|
||||
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} );
|
||||
|
||||
const startTimeTracker = () => {
|
||||
try {
|
||||
clearInterval( timeTracker );
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch ( err ) { /* empty */ }
|
||||
|
||||
setTimeout( () => {
|
||||
@@ -179,21 +232,23 @@
|
||||
timeTracker = setInterval( () => {
|
||||
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||
|
||||
if ( isNaN( progressBar.value ) ) {
|
||||
progressBar.value = 0;
|
||||
}
|
||||
}, 100 );
|
||||
}
|
||||
};
|
||||
|
||||
const stopTimeTracker = () => {
|
||||
clearInterval( timeTracker );
|
||||
|
||||
handleAnimationChange();
|
||||
}
|
||||
};
|
||||
|
||||
const animateBeat = () => {
|
||||
$( '.beat-manual' ).stop();
|
||||
const duration = Math.ceil( 60 / 180 * 500 ) - 50;
|
||||
|
||||
$( '.beat-manual' ).fadeIn( 50 );
|
||||
setTimeout( () => {
|
||||
$( '.beat-manual' ).fadeOut( duration );
|
||||
@@ -202,30 +257,31 @@
|
||||
$( '.beat-manual' ).stop();
|
||||
}, duration );
|
||||
}, 50 );
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnimationChange = () => {
|
||||
if ( visualizationSettings.value === 'mic' && isPlaying.value ) {
|
||||
bizualizer.subscribeToBeatUpdate( animateBeat );
|
||||
} else {
|
||||
bizualizer.unsubscribeFromBeatUpdate()
|
||||
bizualizer.unsubscribeFromBeatUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setBackground = () => {
|
||||
bizualizer.createBackground().then( bg => {
|
||||
$( '#background' ).css( 'background', bg );
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const notifier = () => {
|
||||
Notification.requestPermission();
|
||||
|
||||
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
||||
|
||||
// Detect if window is currently in focus
|
||||
window.onblur = () => {
|
||||
sendNotification();
|
||||
}
|
||||
};
|
||||
|
||||
// Detect if browser window becomes hidden (also with blur event)
|
||||
document.onvisibilitychange = () => {
|
||||
@@ -233,18 +289,19 @@
|
||||
sendNotification();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotification = () => {
|
||||
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
||||
body: 'Please return to the original webpage immediately!',
|
||||
requireInteraction: true,
|
||||
'body': 'Please return to the original webpage immediately!',
|
||||
'requireInteraction': true,
|
||||
} );
|
||||
|
||||
conn.emit( 'tampering', '' );
|
||||
}
|
||||
};
|
||||
|
||||
const isShowingSecureModeInfo = ref( false );
|
||||
|
||||
const secureModeInfo = ( action: string ) => {
|
||||
if ( action === 'toggle' ) {
|
||||
isShowingSecureModeInfo.value = !isShowingSecureModeInfo.value;
|
||||
@@ -253,7 +310,7 @@
|
||||
} else {
|
||||
isShowingSecureModeInfo.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
||||
<div class="content" id="app">
|
||||
<div v-if="hasLoaded" style="width: 100%">
|
||||
<div class="current-song-wrapper">
|
||||
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
|
||||
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art" id="current-image">
|
||||
<div class="current-song">
|
||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||
<h1>{{ playingSong.title }}</h1>
|
||||
<p class="dancing-style" v-if="playingSong.dancingStyle">{{ playingSong.dancingStyle }}</p>
|
||||
<p>{{ playingSong.artist }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-selector-wrapper">
|
||||
<select v-model="visualizationSettings" @change="setVisualization()">
|
||||
<option value="mic">Microphone (Mic access required)</option>
|
||||
<option value="bpm">BPM (might not be 100% accurate)</option>
|
||||
<option value="off">No visualization except background</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" class="song-list">
|
||||
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
|
||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin !== 'api'" :src="'/getSongCover?filename=' + song.filename" class="song-image">
|
||||
<div v-if="playingSong.filename === song.filename && isPlaying" class="playing-symbols">
|
||||
<div class="playing-symbols-wrapper">
|
||||
<div class="playing-bar" id="bar-1"></div>
|
||||
<div class="playing-bar" id="bar-2"></div>
|
||||
<div class="playing-bar" id="bar-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined pause-icon" v-if="!isPlaying && playingSong.filename === song.filename">pause</span>
|
||||
<div class="song-details-wrapper">
|
||||
<h3>{{ song.title }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
</div>
|
||||
<div class="time-until">
|
||||
{{ getTimeUntil( song ) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <img :src="" alt=""> -->
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>Loading...</h1>
|
||||
</div>
|
||||
<div class="background" id="background">
|
||||
<div class="beat"></div>
|
||||
<div class="beat-manual"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: Get ColorThief either from CDN or preferably as NPM module -->
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Song } from '@/scripts/song';
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import { ColorThief } from 'colorthief';
|
||||
|
||||
const hasLoaded = ref( false );
|
||||
const songs: Ref<Song[]> = ref( [] );
|
||||
const playingSong = ref( 0 );
|
||||
const isPlaying = ref( false );
|
||||
const pos = ref( 0 );
|
||||
const colourPalette: string[] = [];
|
||||
const progressBar = ref( 0 );
|
||||
const timeTracker = ref( 0 );
|
||||
const visualizationSettings = ref( 'mic' );
|
||||
const micAnalyzer = ref( 0 );
|
||||
const beatDetected = ref( false );
|
||||
const colorThief = new ColorThief();
|
||||
const songQueue = computed( () => {
|
||||
let ret = [];
|
||||
let pos = 0;
|
||||
for ( let song in songs.value ) {
|
||||
if ( pos >= playingSong.value ) {
|
||||
ret.push( songs.value[ song ] );
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
return ret;
|
||||
} );
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song ) => {
|
||||
let timeRemaining = 0;
|
||||
for ( let i = this.queuePos; i < Object.keys( this.songs ).length - 1; i++ ) {
|
||||
if ( this.songs[ i ] == song ) {
|
||||
break;
|
||||
}
|
||||
timeRemaining += parseInt( this.songs[ i ].duration );
|
||||
}
|
||||
if ( isPlaying.value ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
methods: {
|
||||
startTimeTracker () {
|
||||
this.timeTracker = setInterval( () => {
|
||||
this.pos = ( new Date().getTime() - this.playingSong.startTime ) / 1000 + this.oldPos;
|
||||
this.progressBar = ( this.pos / this.playingSong.duration ) * 1000;
|
||||
if ( isNaN( this.progressBar ) ) {
|
||||
this.progressBar = 0;
|
||||
}
|
||||
}, 100 );
|
||||
},
|
||||
stopTimeTracker () {
|
||||
clearInterval( this.timeTracker );
|
||||
this.oldPos = this.pos;
|
||||
},
|
||||
getImageData() {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
if ( this.playingSong.hasCoverArt ) {
|
||||
setTimeout( () => {
|
||||
const img = document.getElementById( 'current-image' );
|
||||
if ( img.complete ) {
|
||||
resolve( this.colorThief.getPalette( img ) );
|
||||
} else {
|
||||
img.addEventListener( 'load', () => {
|
||||
resolve( this.colorThief.getPalette( img ) );
|
||||
} );
|
||||
}
|
||||
}, 500 );
|
||||
} else {
|
||||
reject( 'no image' );
|
||||
}
|
||||
} );
|
||||
},
|
||||
connect() {
|
||||
this.colorThief = new ColorThief();
|
||||
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
|
||||
source.onmessage = ( e ) => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse( e.data );
|
||||
} catch ( err ) {
|
||||
data = { 'type': e.data };
|
||||
}
|
||||
if ( data.type === 'basics' ) {
|
||||
this.isPlaying = data.data.isPlaying ?? false;
|
||||
this.playingSong = data.data.playingSong ?? {};
|
||||
this.songs = data.data.songQueue ?? [];
|
||||
this.pos = data.data.pos ?? 0;
|
||||
this.oldPos = data.data.pos ?? 0;
|
||||
this.progressBar = this.pos / this.playingSong.duration * 1000;
|
||||
this.queuePos = data.data.queuePos ?? 0;
|
||||
this.getImageData().then( palette => {
|
||||
this.colourPalette = palette;
|
||||
this.handleBackground();
|
||||
} ).catch( () => {
|
||||
this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ];
|
||||
this.handleBackground();
|
||||
} );
|
||||
} else if ( data.type === 'pos' ) {
|
||||
this.pos = data.data;
|
||||
this.oldPos = data.data;
|
||||
this.progressBar = data.data / this.playingSong.duration * 1000;
|
||||
} else if ( data.type === 'isPlaying' ) {
|
||||
this.isPlaying = data.data;
|
||||
this.handleBackground();
|
||||
} else if ( data.type === 'songQueue' ) {
|
||||
this.songs = data.data;
|
||||
} else if ( data.type === 'playingSong' ) {
|
||||
this.playingSong = data.data;
|
||||
this.getImageData().then( palette => {
|
||||
this.colourPalette = palette;
|
||||
this.handleBackground();
|
||||
} ).catch( () => {
|
||||
this.colourPalette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
|
||||
this.handleBackground();
|
||||
} );
|
||||
} else if ( data.type === 'queuePos' ) {
|
||||
this.queuePos = data.data;
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = () => {
|
||||
this.isReconnecting = false;
|
||||
this.hasLoaded = true;
|
||||
};
|
||||
|
||||
let self = this;
|
||||
|
||||
source.addEventListener( 'error', function( e ) {
|
||||
if ( e.eventPhase == EventSource.CLOSED ) source.close();
|
||||
|
||||
if ( e.target.readyState == EventSource.CLOSED ) {
|
||||
console.log( 'disconnected' );
|
||||
}
|
||||
|
||||
// TODO: Notify about disconnect
|
||||
setTimeout( () => {
|
||||
if ( !self.isReconnecting ) {
|
||||
self.isReconnecting = true;
|
||||
self.tryReconnect();
|
||||
}
|
||||
}, 1000 );
|
||||
}, false );
|
||||
},
|
||||
tryReconnect() {
|
||||
const int = setInterval( () => {
|
||||
if ( !this.isReconnecting ) {
|
||||
clearInterval( int );
|
||||
} else {
|
||||
connectToSSESource();
|
||||
}
|
||||
}, 1000 );
|
||||
},
|
||||
handleBackground() {
|
||||
let colourDetails = [];
|
||||
let colours = [];
|
||||
let differentEnough = true;
|
||||
if ( this.colourPalette[ 0 ] ) {
|
||||
for ( let i in this.colourPalette ) {
|
||||
for ( let colour in colourDetails ) {
|
||||
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - this.colourPalette[ i ][ 0 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 1 ] - this.colourPalette[ i ][ 1 ] ) / 255
|
||||
+ Math.abs( colourDetails[ colour ][ 2 ] - this.colourPalette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
||||
if ( colourDiff > 15 ) {
|
||||
differentEnough = true;
|
||||
}
|
||||
}
|
||||
if ( differentEnough ) {
|
||||
colourDetails.push( this.colourPalette[ i ] );
|
||||
colours.push( 'rgb(' + this.colourPalette[ i ][ 0 ] + ',' + this.colourPalette[ i ][ 1 ] + ',' + this.colourPalette[ i ][ 2 ] + ')' );
|
||||
}
|
||||
differentEnough = false;
|
||||
}
|
||||
}
|
||||
let outColours = 'conic-gradient(';
|
||||
if ( colours.length < 3 ) {
|
||||
for ( let i = 0; i < 3; i++ ) {
|
||||
if ( colours[ i ] ) {
|
||||
outColours += colours[ i ] + ',';
|
||||
} else {
|
||||
if ( i === 0 ) {
|
||||
outColours += 'blue,';
|
||||
} else if ( i === 1 ) {
|
||||
outColours += 'green,';
|
||||
} else if ( i === 2 ) {
|
||||
outColours += 'red,';
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ( colours.length < 11 ) {
|
||||
for ( let i in colours ) {
|
||||
outColours += colours[ i ] + ',';
|
||||
}
|
||||
} else {
|
||||
for ( let i = 0; i < 10; i++ ) {
|
||||
outColours += colours[ i ] + ',';
|
||||
}
|
||||
}
|
||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||
|
||||
$( '#background' ).css( 'background', outColours );
|
||||
this.setVisualization();
|
||||
},
|
||||
setVisualization () {
|
||||
if ( Object.keys( this.playingSong ).length > 0 ) {
|
||||
if ( this.visualizationSettings === 'bpm' ) {
|
||||
if ( this.playingSong.bpm && this.isPlaying ) {
|
||||
$( '.beat' ).show();
|
||||
$( '.beat' ).css( 'animation-duration', 60 / this.playingSong.bpm );
|
||||
$( '.beat' ).css( 'animation-delay', this.pos % ( 60 / this.playingSong.bpm * this.pos ) + this.playingSong.bpmOffset - ( 60 / this.playingSong.bpm * this.pos / 2 ) );
|
||||
} else {
|
||||
$( '.beat' ).hide();
|
||||
}
|
||||
try {
|
||||
clearInterval( this.micAnalyzer );
|
||||
} catch ( err ) {}
|
||||
} else if ( this.visualizationSettings === 'off' ) {
|
||||
$( '.beat' ).hide();
|
||||
try {
|
||||
clearInterval( this.micAnalyzer );
|
||||
} catch ( err ) {}
|
||||
} else if ( this.visualizationSettings === 'mic' ) {
|
||||
$( '.beat-manual' ).hide();
|
||||
try {
|
||||
clearInterval( this.micAnalyzer );
|
||||
} catch ( err ) {}
|
||||
this.micAudioHandler();
|
||||
}
|
||||
} else {
|
||||
console.log( 'not playing yet' );
|
||||
}
|
||||
},
|
||||
micAudioHandler () {
|
||||
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array( bufferLength );
|
||||
|
||||
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
||||
const mic = audioContext.createMediaStreamSource( stream );
|
||||
mic.connect( analyser );
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
let prevSpectrum = null;
|
||||
let threshold = 10; // Adjust as needed
|
||||
this.beatDetected = false;
|
||||
this.micAnalyzer = setInterval( () => {
|
||||
analyser.getByteFrequencyData( dataArray );
|
||||
// Convert the frequency data to a numeric array
|
||||
const currentSpectrum = Array.from( dataArray );
|
||||
|
||||
if ( prevSpectrum ) {
|
||||
// Calculate the spectral flux
|
||||
const flux = this.calculateSpectralFlux( prevSpectrum, currentSpectrum );
|
||||
|
||||
if ( flux > threshold && !this.beatDetected ) {
|
||||
// Beat detected
|
||||
this.beatDetected = true;
|
||||
this.animateBeat();
|
||||
}
|
||||
}
|
||||
prevSpectrum = currentSpectrum;
|
||||
}, 20 );
|
||||
} );
|
||||
},
|
||||
animateBeat () {
|
||||
$( '.beat-manual' ).stop();
|
||||
const duration = Math.ceil( 60 / ( this.playingSong.bpm ?? 180 ) * 500 ) - 50;
|
||||
$( '.beat-manual' ).fadeIn( 50 );
|
||||
setTimeout( () => {
|
||||
$( '.beat-manual' ).fadeOut( duration );
|
||||
setTimeout( () => {
|
||||
$( '.beat-manual' ).stop();
|
||||
this.beatDetected = false;
|
||||
}, duration );
|
||||
}, 50 );
|
||||
},
|
||||
calculateSpectralFlux( prevSpectrum, currentSpectrum ) {
|
||||
let flux = 0;
|
||||
|
||||
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
||||
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
||||
flux += Math.max( 0, diff );
|
||||
}
|
||||
|
||||
return flux;
|
||||
},
|
||||
notifier() {
|
||||
if ( parseInt( this.lastDispatch ) + 5000 < new Date().getTime() ) {
|
||||
|
||||
}
|
||||
Notification.requestPermission();
|
||||
|
||||
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
||||
// Detect if window is currently in focus
|
||||
window.onblur = () => {
|
||||
this.sendNotification( 'blur' );
|
||||
}
|
||||
|
||||
// Detect if browser window becomes hidden (also with blur event)
|
||||
document.onvisibilitychange = () => {
|
||||
if ( document.visibilityState === 'hidden' ) {
|
||||
this.sendNotification( 'visibility' );
|
||||
}
|
||||
};
|
||||
},
|
||||
sendNotification( notification ) {
|
||||
let fetchOptions = {
|
||||
method: 'post',
|
||||
body: JSON.stringify( { 'type': notification } ),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'charset': 'utf-8'
|
||||
},
|
||||
};
|
||||
fetch( '/clientStatusUpdate', fetchOptions ).catch( err => {
|
||||
console.error( err );
|
||||
} );
|
||||
|
||||
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
||||
body: 'Please return to the original webpage immediately!',
|
||||
requireInteraction: true,
|
||||
} )
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connect();
|
||||
this.notifier();
|
||||
// if ( this.visualizationSettings === 'mic' ) {
|
||||
// this.micAudioHandler();
|
||||
// }
|
||||
},
|
||||
watch: {
|
||||
isPlaying( value ) {
|
||||
if ( value ) {
|
||||
this.startTimeTracker();
|
||||
} else {
|
||||
this.stopTimeTracker();
|
||||
}
|
||||
}
|
||||
}
|
||||
} ).mount( '#app' );
|
||||
|
||||
</script>
|
||||
@@ -3,7 +3,6 @@
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
@@ -14,6 +13,6 @@
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
"types": ["node", "jquery"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
A music player, specifically created for displaying song information on multiple different displays that are connected to the same network, just from the browser.
|
||||
|
||||
The [hosted version](https://music.janishutz.com) of this MusicPlayer, which is fully set up for you will be subscription-based and can be paid for on my [store](https://store.janishutz.com/product/com.janishutz.MusicPlayer). Not available yet though!
|
||||
The [hosted version](https://music.janishutz.com) of this MusicPlayer, which is fully set up for you is subscription-based and can be paid for on my [store](https://store.janishutz.com/product/com.janishutz).
|
||||
|
||||
<div id="donate" align="center">
|
||||
<a href="https://store.janishutz.com/donate" target="_blank"><img src="https://store-cdn.janishutz.com/static/support-me.jpg" width="150px"></a>
|
||||
|
||||
702
backend/eslint.config.mjs
Normal file
702
backend/eslint.config.mjs
Normal file
@@ -0,0 +1,702 @@
|
||||
import vue from 'eslint-plugin-vue';
|
||||
import eslint from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import typescript from '@typescript-eslint/eslint-plugin';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const style = {
|
||||
'plugins': {
|
||||
'@stylistic': stylistic,
|
||||
'@stylistic/js': stylistic,
|
||||
'@stylistic/ts': stylistic,
|
||||
},
|
||||
'files': [
|
||||
'**/*.ts',
|
||||
'**/*.js',
|
||||
'**/*.mjs',
|
||||
'**/*.cjs',
|
||||
'**/*.tsx',
|
||||
'**/*.jsx'
|
||||
],
|
||||
'rules': {
|
||||
// Formatting
|
||||
'@stylistic/array-bracket-newline': [
|
||||
'error',
|
||||
{
|
||||
'multiline': true,
|
||||
'minItems': 2
|
||||
}
|
||||
],
|
||||
'@stylistic/array-bracket-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/array-element-newline': [
|
||||
'error',
|
||||
{
|
||||
'multiline': true,
|
||||
'minItems': 2
|
||||
}
|
||||
],
|
||||
'@stylistic/arrow-parens': [
|
||||
'error',
|
||||
'as-needed'
|
||||
],
|
||||
'@stylistic/arrow-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': true,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/block-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/brace-style': [
|
||||
'error',
|
||||
'1tbs'
|
||||
],
|
||||
'@stylistic/comma-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': false,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/comma-style': [
|
||||
'error',
|
||||
'last'
|
||||
],
|
||||
'@stylistic/dot-location': [
|
||||
'error',
|
||||
'property'
|
||||
],
|
||||
'@stylistic/eol-last': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/function-call-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'@stylistic/function-paren-newline': [
|
||||
'error',
|
||||
'multiline'
|
||||
],
|
||||
'@stylistic/function-call-argument-newline': [
|
||||
'error',
|
||||
'consistent'
|
||||
],
|
||||
'@stylistic/implicit-arrow-linebreak': [
|
||||
'error',
|
||||
'beside'
|
||||
],
|
||||
'@stylistic/indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'@stylistic/key-spacing': [
|
||||
'error',
|
||||
{
|
||||
'beforeColon': false,
|
||||
'afterColon': true
|
||||
}
|
||||
],
|
||||
'@stylistic/keyword-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': true,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/lines-between-class-members': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/max-len': [
|
||||
'warn',
|
||||
{
|
||||
'code': 90,
|
||||
'comments': 100,
|
||||
'ignoreComments': false,
|
||||
'ignoreUrls': true,
|
||||
'ignoreStrings': false
|
||||
}
|
||||
],
|
||||
'@stylistic/new-parens': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/newline-per-chained-call': [ 'error' ],
|
||||
'@stylistic/no-extra-parens': [
|
||||
'error',
|
||||
'all',
|
||||
{
|
||||
'nestedBinaryExpressions': false,
|
||||
'ternaryOperandBinaryExpressions': false,
|
||||
'ignoreJSX': 'multi-line',
|
||||
'nestedConditionalExpressions': false
|
||||
}
|
||||
],
|
||||
'@stylistic/no-extra-semi': 'error',
|
||||
'@stylistic/no-floating-decimal': 'error',
|
||||
'@stylistic/no-mixed-operators': 'error',
|
||||
'@stylistic/no-mixed-spaces-and-tabs': 'error',
|
||||
'@stylistic/no-multi-spaces': 'error',
|
||||
'@stylistic/no-multiple-empty-lines': [
|
||||
'error',
|
||||
{
|
||||
'max': 3,
|
||||
'maxEOF': 2
|
||||
}
|
||||
],
|
||||
'@stylistic/no-tabs': 'error',
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
'@stylistic/no-whitespace-before-property': 'error',
|
||||
'@stylistic/object-curly-newline': [
|
||||
'error',
|
||||
{
|
||||
'multiline': true,
|
||||
'minProperties': 1
|
||||
}
|
||||
],
|
||||
'@stylistic/object-curly-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/object-property-newline': 'error',
|
||||
'@stylistic/operator-linebreak': [
|
||||
'error',
|
||||
'before'
|
||||
],
|
||||
'@stylistic/one-var-declaration-per-line': 'error',
|
||||
'@stylistic/padded-blocks': [
|
||||
'error',
|
||||
{
|
||||
'blocks': 'never',
|
||||
'classes': 'always',
|
||||
'switches': 'never',
|
||||
}
|
||||
],
|
||||
|
||||
// Padding lines. The most in-depth part of this config
|
||||
'@stylistic/padding-line-between-statements': [
|
||||
'error',
|
||||
// Variables, Constants
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'var',
|
||||
'next': 'var'
|
||||
},
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'let',
|
||||
'next': 'let'
|
||||
},
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'const',
|
||||
'next': 'const'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'var',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'cjs-import',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'import',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'let',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'cjs-import',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'import',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'const',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'cjs-import',
|
||||
'class',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'import',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
// Import
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'import',
|
||||
'next': 'import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'cjs-import',
|
||||
'next': 'cjs-import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
],
|
||||
'next': 'cjs-import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'cjs-import',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
],
|
||||
'next': 'import'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'import',
|
||||
'next': [
|
||||
'block',
|
||||
'block-like',
|
||||
'break',
|
||||
'cjs-export',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'directive',
|
||||
'do',
|
||||
'empty',
|
||||
'export',
|
||||
'expression',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'iife',
|
||||
'let',
|
||||
'return',
|
||||
'switch',
|
||||
'throw',
|
||||
'try',
|
||||
'var',
|
||||
'while',
|
||||
'with'
|
||||
]
|
||||
},
|
||||
// If
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'if'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'if',
|
||||
'next': '*'
|
||||
},
|
||||
// For
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'for'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'for',
|
||||
'next': '*'
|
||||
},
|
||||
// While
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'while'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'while',
|
||||
'next': '*'
|
||||
},
|
||||
// Functions
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'function'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'function',
|
||||
'next': '*'
|
||||
},
|
||||
// Block Statements
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'block-like'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'block-like',
|
||||
'next': '*'
|
||||
},
|
||||
// Switch
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'switch'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'switch',
|
||||
'next': '*'
|
||||
},
|
||||
// Try-Catch
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'try'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'try',
|
||||
'next': '*'
|
||||
},
|
||||
// Throw
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'throw'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'throw',
|
||||
'next': '*'
|
||||
},
|
||||
// Return
|
||||
{
|
||||
'blankLine': 'never',
|
||||
'prev': 'return',
|
||||
'next': '*'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'return'
|
||||
},
|
||||
// Export
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'export'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'export',
|
||||
'next': '*'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'cjs-export'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'cjs-export',
|
||||
'next': '*'
|
||||
},
|
||||
// Classes
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': '*',
|
||||
'next': 'class'
|
||||
},
|
||||
{
|
||||
'blankLine': 'always',
|
||||
'prev': 'class',
|
||||
'next': '*'
|
||||
},
|
||||
],
|
||||
'@stylistic/quote-props': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'@stylistic/rest-spread-spacing': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'@stylistic/semi': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/semi-spacing': [
|
||||
'error',
|
||||
{
|
||||
'before': false,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'@stylistic/semi-style': [
|
||||
'error',
|
||||
'last'
|
||||
],
|
||||
'@stylistic/space-before-blocks': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/space-before-function-paren': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/space-in-parens': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/space-infix-ops': [
|
||||
'error',
|
||||
{
|
||||
'int32Hint': false
|
||||
}
|
||||
],
|
||||
'@stylistic/space-unary-ops': 'error',
|
||||
'@stylistic/spaced-comment': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/template-curly-spacing': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'@stylistic/switch-colon-spacing': 'error',
|
||||
'@stylistic/wrap-iife': [
|
||||
'error',
|
||||
'inside'
|
||||
],
|
||||
'@stylistic/wrap-regex': 'error',
|
||||
'@stylistic/ts/type-annotation-spacing': 'error',
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
export default tseslint.config(
|
||||
// Base JavaScript rules
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
style,
|
||||
|
||||
// Vue support (including TS and JSX inside SFCs)
|
||||
{
|
||||
'files': [ '**/*.vue' ],
|
||||
'languageOptions': {
|
||||
'sourceType': 'module',
|
||||
'ecmaVersion': 'latest',
|
||||
'globals': globals.browser,
|
||||
'parserOptions': {
|
||||
'parser': tseslint.parser,
|
||||
},
|
||||
},
|
||||
'plugins': {
|
||||
'vue': vue,
|
||||
'@stylistic': stylistic,
|
||||
'@stylistic/js': stylistic,
|
||||
'@stylistic/ts': stylistic,
|
||||
'@typescript-eslint': typescript,
|
||||
},
|
||||
'extends': [
|
||||
eslint.configs.recommended,
|
||||
...vue.configs['flat/recommended']
|
||||
],
|
||||
'rules': {
|
||||
...typescript.configs.recommended.rules,
|
||||
...style.rules,
|
||||
|
||||
// Vue specific rules
|
||||
'@stylistic/indent': 'off',
|
||||
'vue/html-indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'vue/html-comment-indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'vue/script-indent': [
|
||||
'error',
|
||||
4,
|
||||
{
|
||||
'baseIndent': 1,
|
||||
'switchCase': 1
|
||||
}
|
||||
],
|
||||
'vue/html-self-closing': [
|
||||
'error',
|
||||
{
|
||||
'html': {
|
||||
'void': 'never',
|
||||
'normal': 'never',
|
||||
'component': 'always'
|
||||
},
|
||||
'svg': 'always',
|
||||
'math': 'never'
|
||||
}
|
||||
],
|
||||
'vue/max-attributes-per-line': [
|
||||
'error',
|
||||
{
|
||||
'singleline': 3,
|
||||
'multiline': 1,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
1503
backend/package-lock.json
generated
1503
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,43 @@
|
||||
{
|
||||
"name": "musicplayer-v2-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "The backend for MusicPlayerV2",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/simplePCBuilding/MusicPlayerV2.git"
|
||||
},
|
||||
"author": "Janis Hutz",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/simplePCBuilding/MusicPlayerV2/issues"
|
||||
},
|
||||
"homepage": "https://github.com/simplePCBuilding/MusicPlayerV2#readme",
|
||||
"devDependencies": {
|
||||
"@types/express-session": "^1.18.0",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-mysql": "^0.4.2",
|
||||
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
|
||||
"socket.io": "^4.7.5",
|
||||
"store.janishutz.com-sdk": "file:../../store/sdk/dist"
|
||||
}
|
||||
"name": "musicplayer-v2-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "The backend for MusicPlayerV2",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/simplePCBuilding/MusicPlayerV2.git"
|
||||
},
|
||||
"author": "Janis Hutz",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/simplePCBuilding/MusicPlayerV2/issues"
|
||||
},
|
||||
"homepage": "https://github.com/simplePCBuilding/MusicPlayerV2#readme",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/express-session": "^1.18.0",
|
||||
"eslint-plugin-vue": "^10.2.0",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.35.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@janishutz/login-sdk-server": "^1.2.0",
|
||||
"@janishutz/login-sdk-server-stubs": "^1.0.0",
|
||||
"@janishutz/store-sdk": "^1.1.0",
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-mysql": "^0.4.2",
|
||||
"socket.io": "^4.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import db from './storage/db';
|
||||
|
||||
|
||||
const createUser = ( uid: string, username: string, email: string ): Promise<boolean> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
db.writeDataSimple( 'users', 'uid', uid, { 'uid': uid, 'username': username, 'email': email } ).then( () => {
|
||||
resolve( true );
|
||||
} ).catch( err => {
|
||||
reject( err );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
const saveUserData = ( uid: string, data: object ): Promise<boolean> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
db.writeDataSimple( 'users', 'uid', uid, { 'data': data } ).then( () => {
|
||||
resolve( true );
|
||||
} ).catch( err => {
|
||||
reject( err );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
const checkUser = ( uid: string ): Promise<boolean> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
db.checkDataAvailability( 'users', 'uid', uid ).then( res => {
|
||||
resolve( res );
|
||||
} ).catch( err => {
|
||||
reject( err );
|
||||
} )
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
const getUserData = ( uid: string ): Promise<object> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
db.getDataSimple( 'users', 'uid', uid ).then( data => {
|
||||
resolve( data );
|
||||
} ).catch( err => {
|
||||
reject( err );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
export default {
|
||||
createUser,
|
||||
saveUserData,
|
||||
checkUser,
|
||||
getUserData
|
||||
}
|
||||
@@ -3,448 +3,277 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import cors from 'cors';
|
||||
import account from './account';
|
||||
import sdk from 'oauth-janishutz-client-server';
|
||||
import { createServer } from 'node:http';
|
||||
import { Server } from 'socket.io';
|
||||
import {
|
||||
createServer
|
||||
} from 'node:http';
|
||||
import crypto from 'node:crypto';
|
||||
import type { Room, Song } from './definitions';
|
||||
import storeSDK from 'store.janishutz.com-sdk';
|
||||
import bodyParser from 'body-parser';
|
||||
import {
|
||||
SocketData
|
||||
} from './definitions';
|
||||
|
||||
const isFossVersion = true;
|
||||
|
||||
declare let __dirname: string | undefined
|
||||
if ( typeof( __dirname ) === 'undefined' ) {
|
||||
__dirname = path.resolve( path.dirname( '' ) );
|
||||
}
|
||||
// ┌ ┐
|
||||
// │ Handle FOSS vs paid version │
|
||||
// └ ┘
|
||||
const isFossVersion = false;
|
||||
|
||||
import storeSDK from '@janishutz/store-sdk';
|
||||
import sdk from '@janishutz/login-sdk-server';
|
||||
import sse from './sse';
|
||||
import socket from './socket';
|
||||
|
||||
// const isFossVersion = true;
|
||||
//
|
||||
// import storeSDK from './sdk/store-sdk-stub';
|
||||
// import sdk from '@janishutz/login-sdk-server-stubs';
|
||||
|
||||
|
||||
const corsOpts: cors.CorsOptions = {
|
||||
'credentials': true,
|
||||
'origin': ( origin, cb ) => {
|
||||
if ( isFossVersion ) cb( null, true );
|
||||
else cb( null, origin === 'https://music.janishutz.com' );
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Change config file, as well as in main.ts, index.html, oauth, if deploying there
|
||||
// const sdkConfig = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/sdk.config.testing.json' ) ) );
|
||||
const sdkConfig = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/sdk.config.secret.json' ) ) );
|
||||
|
||||
const run = () => {
|
||||
let app = express();
|
||||
app.use( cors( {
|
||||
credentials: true,
|
||||
origin: true
|
||||
} ) );
|
||||
|
||||
if ( !isFossVersion ) {
|
||||
// storeSDK.configure( JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/store-sdk.config.testing.json' ) ) ) );
|
||||
storeSDK.configure( JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/store-sdk.config.secret.json' ) ) ) );
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer( app );
|
||||
|
||||
if ( !isFossVersion ) {
|
||||
console.error( '[ APP ] Starting in non-FOSS version' );
|
||||
|
||||
const storeConfig = JSON.parse( fs.readFileSync( path.join(
|
||||
__dirname,
|
||||
'/config/store-sdk.config.secret.json'
|
||||
) ).toString() );
|
||||
|
||||
console.error( storeConfig );
|
||||
|
||||
storeSDK.configure( storeConfig );
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
const sdkConfig = JSON.parse( fs.readFileSync( path.join(
|
||||
__dirname,
|
||||
'/config/sdk.config.secret.json'
|
||||
) ).toString() );
|
||||
|
||||
// Load id.janishutz.com SDK and allow signing in
|
||||
sdk.routes( app, ( uid: string ) => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
account.checkUser( uid ).then( stat => {
|
||||
resolve( stat );
|
||||
} ).catch( e => {
|
||||
reject( e );
|
||||
sdk.setUp(
|
||||
{
|
||||
'prod': false,
|
||||
'service': {
|
||||
'serviceID': 'jh-music',
|
||||
'serviceToken': sdkConfig[ 'token' ]
|
||||
},
|
||||
'user-agent': sdkConfig[ 'ua' ],
|
||||
'sessionType': 'memory',
|
||||
'frontendURL': 'https://music.janishutz.com',
|
||||
'corsWhitelist': [ 'https://music.janishutz.com' ],
|
||||
'recheckTimeout': 300 * 1000,
|
||||
'advancedVerification': 'sdk',
|
||||
},
|
||||
app,
|
||||
() => {
|
||||
return new Promise( resolve => {
|
||||
resolve( true );
|
||||
} );
|
||||
} );
|
||||
},
|
||||
( uid: string, email: string, username: string ) => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
account.createUser( uid, username, email ).then( stat => {
|
||||
resolve( stat );
|
||||
} ).catch( e => {
|
||||
reject( e );
|
||||
},
|
||||
() => {
|
||||
return new Promise( resolve => {
|
||||
resolve( true );
|
||||
} );
|
||||
} );
|
||||
}, sdkConfig );
|
||||
},
|
||||
() => {
|
||||
return new Promise( resolve => {
|
||||
resolve( true );
|
||||
} );
|
||||
},
|
||||
() => {
|
||||
return new Promise( resolve => {
|
||||
resolve( true );
|
||||
} );
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Websocket for events
|
||||
interface SocketData {
|
||||
[key: string]: Room;
|
||||
}
|
||||
const socketData: SocketData = {};
|
||||
const io = new Server( httpServer, {
|
||||
cors: {
|
||||
origin: true,
|
||||
credentials: true,
|
||||
}
|
||||
} );
|
||||
|
||||
io.on( 'connection', ( socket ) => {
|
||||
socket.on( 'create-room', ( room: { name: string, token: string }, cb: ( res: { status: boolean, msg: string } ) => void ) => {
|
||||
if ( socketData[ room.name ] ) {
|
||||
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||
socket.join( room.name );
|
||||
cb( {
|
||||
status: true,
|
||||
msg: 'ADDED_TO_ROOM'
|
||||
} );
|
||||
} else {
|
||||
cb( {
|
||||
status: false,
|
||||
msg: 'ERR_TOKEN_INVALID'
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
cb( {
|
||||
status: false,
|
||||
msg: 'ERR_NAME_INVALID'
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'delete-room', ( room: { name: string, token: string }, cb: ( res: { status: boolean, msg: string } ) => void ) => {
|
||||
if ( socketData[ room.name ] ) {
|
||||
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||
socket.leave( room.name );
|
||||
socket.to( room.name ).emit( 'delete-share', room.name );
|
||||
socketData[ room.name ] = undefined;
|
||||
cb( {
|
||||
status: true,
|
||||
msg: 'ROOM_DELETED'
|
||||
} );
|
||||
} else {
|
||||
cb( {
|
||||
status: false,
|
||||
msg: 'ERR_TOKEN_INVALID'
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
cb( {
|
||||
status: false,
|
||||
msg: 'ERR_NAME_INVALID'
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'join-room', ( room: string, cb: ( res: { status: boolean, msg: string, data?: { playbackStatus: boolean, playbackStart: number, playlist: Song[], playlistIndex: number, useAntiTamper: boolean } } ) => void ) => {
|
||||
if ( socketData[ room ] ) {
|
||||
socket.join( room );
|
||||
cb( {
|
||||
data: {
|
||||
playbackStart: socketData[ room ].playbackStart,
|
||||
playbackStatus: socketData[ room ].playbackStatus,
|
||||
playlist: socketData[ room ].playlist,
|
||||
playlistIndex: socketData[ room ].playlistIndex,
|
||||
useAntiTamper: socketData[ room ].useAntiTamper,
|
||||
},
|
||||
msg: 'STATUS_OK',
|
||||
status: true,
|
||||
} )
|
||||
} else {
|
||||
cb( {
|
||||
msg: 'ERR_NO_ROOM_WITH_THIS_ID',
|
||||
status: false,
|
||||
} );
|
||||
socket.disconnect();
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'tampering', ( data: { msg: string, roomName: string } ) => {
|
||||
if ( data.roomName ) {
|
||||
socket.to( data.roomName ).emit( 'tampering-msg', data.msg );
|
||||
}
|
||||
} )
|
||||
|
||||
socket.on( 'playlist-update', ( data: { roomName: string, roomToken: string, data: Song[] } ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
if ( socketData[ data.roomName ].playlist !== data.data ) {
|
||||
socketData[ data.roomName ].playlist = data.data;
|
||||
io.to( data.roomName ).emit( 'playlist', data.data );
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'playback-update', ( data: { roomName: string, roomToken: string, data: boolean } ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
socketData[ data.roomName ].playbackStatus = data.data;
|
||||
io.to( data.roomName ).emit( 'playback', data.data );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'playlist-index-update', ( data: { roomName: string, roomToken: string, data: number } ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
socketData[ data.roomName ].playlistIndex = data.data;
|
||||
io.to( data.roomName ).emit( 'playlist-index', data.data );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'playback-start-update', ( data: { roomName: string, roomToken: string, data: number } ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
socketData[ data.roomName ].playbackStart = data.data;
|
||||
io.to( data.roomName ).emit( 'playback-start', data.data );
|
||||
}
|
||||
}
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
/*
|
||||
ROUTES FOR SERVER SENT EVENTS VERSION
|
||||
Configuration of SSE or WebSocket
|
||||
*/
|
||||
// Connected clients have their session ID as key
|
||||
interface SocketClientList {
|
||||
[key: string]: SocketClient;
|
||||
}
|
||||
|
||||
interface SocketClient {
|
||||
response: express.Response;
|
||||
room: string;
|
||||
}
|
||||
|
||||
interface ClientReferenceList {
|
||||
/**
|
||||
* Find all clients connected to one room
|
||||
*/
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
const importantClients: SocketClientList = {};
|
||||
const connectedClients: SocketClientList = {};
|
||||
const clientReference: ClientReferenceList = {};
|
||||
|
||||
app.get( '/socket/connection', ( request: express.Request, response: express.Response ) => {
|
||||
if ( request.query.room ) {
|
||||
if ( socketData[ String( request.query.room ) ] ) {
|
||||
response.writeHead( 200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
} );
|
||||
response.status( 200 );
|
||||
response.flushHeaders();
|
||||
response.write( `data: ${ JSON.stringify( { 'type': 'basics', 'data': socketData[ String( request.query.room ) ] } ) }\n\n` );
|
||||
const sid = sdk.getSessionID( request );
|
||||
if ( sdk.checkAuth( request ) ) {
|
||||
importantClients[ sid ] = { 'response': response, 'room': String( request.query.room ) };
|
||||
}
|
||||
connectedClients[ sid ] = { 'response': response, 'room': String( request.query.room ) };
|
||||
if ( !clientReference[ String( request.query.room ) ] ) {
|
||||
clientReference[ String( request.query.room ) ] = [];
|
||||
}
|
||||
if ( !clientReference[ String( request.query.room ) ].includes( sid ) ) {
|
||||
clientReference[ String( request.query.room ) ].push( sid );
|
||||
}
|
||||
request.on( 'close', () => {
|
||||
try {
|
||||
importantClients[ sid ] = undefined;
|
||||
} catch ( e ) { /* empty */ }
|
||||
const cl = clientReference[ String( request.query.room ) ];
|
||||
for ( let c in cl ) {
|
||||
if ( cl[ c ] === sid ) {
|
||||
cl.splice( parseInt( c ), 1 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
connectedClients[ sid ] = undefined;
|
||||
} );
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||
}
|
||||
} );
|
||||
|
||||
app.get( '/socket/getData', ( request: express.Request, response: express.Response ) => {
|
||||
if ( request.query.room ) {
|
||||
response.send( socketData[ String( request.query.room ) ] );
|
||||
} else {
|
||||
response.status( 400 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||
}
|
||||
} );
|
||||
|
||||
app.get( '/socket/joinRoom', ( request: express.Request, response: express.Response ) => {
|
||||
if ( request.query.room ) {
|
||||
if ( socketData[ String( request.query.room ) ] ) {
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||
}
|
||||
} );
|
||||
|
||||
app.post( '/socket/update', bodyParser.json(), ( request: express.Request, response: express.Response ) => {
|
||||
if ( socketData[ request.body.roomName ] ) {
|
||||
if ( request.body.event === 'tampering' ) {
|
||||
const clients = clientReference[ request.body.roomName ];
|
||||
for ( let client in clients ) {
|
||||
if ( importantClients[ clients[ client ] ] ) {
|
||||
importantClients[ clients[ client ] ].response.write( 'data: ' + JSON.stringify( { 'type': 'tampering-msg', 'data': true } ) + '\n\n' );
|
||||
}
|
||||
}
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
if ( socketData[ request.body.roomName ].roomToken === request.body.roomToken ) {
|
||||
let send = false;
|
||||
let update = '';
|
||||
|
||||
if ( request.body.event === 'playback-start-update' ) {
|
||||
send = true;
|
||||
update = 'playback-start';
|
||||
socketData[ request.body.roomName ].playbackStart = request.body.data;
|
||||
} else if ( request.body.event === 'playback-update' ) {
|
||||
send = true;
|
||||
update = 'playback';
|
||||
socketData[ request.body.roomName ].playbackStatus = request.body.data;
|
||||
} else if ( request.body.event === 'playlist-update' ) {
|
||||
send = true;
|
||||
update = 'playlist';
|
||||
socketData[ request.body.roomName ].playlist = request.body.data;
|
||||
} else if ( request.body.event === 'playlist-index-update' ) {
|
||||
send = true;
|
||||
update = 'playlist-index';
|
||||
socketData[ request.body.roomName ].playlistIndex = request.body.data;
|
||||
}
|
||||
|
||||
if ( send ) {
|
||||
const clients = clientReference[ request.body.roomName ];
|
||||
for ( let client in clients ) {
|
||||
if ( connectedClients[ clients[ client ] ] ) {
|
||||
connectedClients[ clients[ client ] ].response.write( 'data: ' + JSON.stringify( { 'type': update, 'data': request.body.data } ) + '\n\n' );
|
||||
}
|
||||
}
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_CANNOT_SEND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.status( 400 ).send( 'ERR_WRONG_REQUEST' );
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
|
||||
app.post( '/socket/deleteRoom', bodyParser.json(), ( request: express.Request, response: express.Response ) => {
|
||||
if ( request.body.roomName ) {
|
||||
if ( socketData[ request.body.roomName ] ) {
|
||||
if ( socketData[ request.body.roomName ].roomToken === request.body.roomToken ) {
|
||||
socketData[ request.body.roomName ] = undefined;
|
||||
const clients = clientReference[ request.body.roomName ];
|
||||
for ( let client in clients ) {
|
||||
if ( connectedClients[ clients[ client ] ] ) {
|
||||
connectedClients[ clients[ client ] ].response.write( 'data: ' + JSON.stringify( { 'type': 'delete-share', 'data': true } ) + '\n\n' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.send( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||
}
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 400 ).send( 'ERR_NO_ROOM_NAME' );
|
||||
}
|
||||
} );
|
||||
const socketData: SocketData = {};
|
||||
|
||||
sse.useSSE( app, socketData, corsOpts, sdk.getSessionID, sdk.getSignedIn );
|
||||
socket.useWebSocket( httpServer, socketData );
|
||||
|
||||
|
||||
/*
|
||||
GENERAL ROUTES
|
||||
*/
|
||||
app.get( '/', ( request: express.Request, response: express.Response ) => {
|
||||
app.get( '/', ( _request: express.Request, response: express.Response ) => {
|
||||
response.send( 'Please visit <a href="https://music.janishutz.com">https://music.janishutz.com</a> to use this service' );
|
||||
} );
|
||||
|
||||
|
||||
app.get( '/createRoomToken', ( request: express.Request, response: express.Response ) => {
|
||||
if ( sdk.checkAuth( request ) ) {
|
||||
|
||||
app.get(
|
||||
'/createRoomToken',
|
||||
cors( corsOpts ),
|
||||
sdk.loginCheck(),
|
||||
( request: express.Request, response: express.Response ) => {
|
||||
// eslint-disable-next-line no-constant-binary-expression
|
||||
const roomName = String( request.query.roomName ) ?? '';
|
||||
|
||||
if ( !socketData[ roomName ] ) {
|
||||
const roomToken = crypto.randomUUID();
|
||||
|
||||
socketData[ roomName ] = {
|
||||
playbackStart: 0,
|
||||
playbackStatus: false,
|
||||
playlist: [],
|
||||
playlistIndex: 0,
|
||||
roomName: roomName,
|
||||
roomToken: roomToken,
|
||||
ownerUID: sdk.getUserData( request ).uid,
|
||||
useAntiTamper: request.query.useAntiTamper === 'true' ? true : false,
|
||||
'playbackStart': 0,
|
||||
'playbackStatus': false,
|
||||
'playlist': [],
|
||||
'playlistIndex': 0,
|
||||
'roomName': roomName,
|
||||
'roomToken': roomToken,
|
||||
'ownerUID': sdk.getUID( request ),
|
||||
'useAntiTamper': request.query.useAntiTamper === 'true'
|
||||
? true : false,
|
||||
};
|
||||
response.send( roomToken );
|
||||
} else {
|
||||
if ( socketData[ roomName ].ownerUID === sdk.getUserData( request ).uid ) {
|
||||
if (
|
||||
socketData[ roomName ].ownerUID
|
||||
=== sdk.getUID( request )
|
||||
) {
|
||||
response.send( socketData[ roomName ].roomToken );
|
||||
} else {
|
||||
response.status( 409 ).send( 'ERR_CONFLICT' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.status( 403 ).send( 'ERR_FORBIDDEN' );
|
||||
}
|
||||
} );
|
||||
);
|
||||
|
||||
|
||||
app.get( '/getAppleMusicDevToken', ( req, res ) => {
|
||||
// sign dev token
|
||||
const privateKey = fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString();
|
||||
// TODO: Remove secret
|
||||
const config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/apple-music-api.config.secret.json' ) ) );
|
||||
const now = new Date().getTime();
|
||||
const tomorrow = now + 24 * 3600 * 1000;
|
||||
const jwtToken = jwt.sign( {
|
||||
'iss': config.teamID,
|
||||
'iat': Math.floor( now / 1000 ),
|
||||
'exp': Math.floor( tomorrow / 1000 ),
|
||||
}, privateKey, {
|
||||
algorithm: "ES256",
|
||||
keyid: config.keyID
|
||||
} );
|
||||
res.send( jwtToken );
|
||||
} );
|
||||
app.get(
|
||||
'/getAppleMusicDevToken',
|
||||
cors( corsOpts ),
|
||||
sdk.loginCheck(),
|
||||
( req, res ) => {
|
||||
checkIfOwned( req )
|
||||
.then( owned => {
|
||||
if ( owned ) {
|
||||
// sign dev token
|
||||
const privateKey = fs.readFileSync( path.join(
|
||||
__dirname,
|
||||
'/config/apple_private_key.p8'
|
||||
) ).toString();
|
||||
const config = JSON.parse( fs.readFileSync( path.join(
|
||||
__dirname,
|
||||
'/config/apple-music-api.config.secret.json'
|
||||
) ).toString() );
|
||||
const now = new Date().getTime();
|
||||
const tomorrow = now + ( 24 * 3600 * 1000 );
|
||||
const jwtToken = jwt.sign( {
|
||||
'iss': config.teamID,
|
||||
'iat': Math.floor( now / 1000 ),
|
||||
'exp': Math.floor( tomorrow / 1000 ),
|
||||
}, privateKey, {
|
||||
'algorithm': 'ES256',
|
||||
'keyid': config.keyID
|
||||
} );
|
||||
|
||||
// TODO: Get user's subscriptions using store sdk
|
||||
|
||||
app.get( '/checkUserStatus', ( request: express.Request, response: express.Response ) => {
|
||||
if ( sdk.checkAuth( request ) ) {
|
||||
storeSDK.getSubscriptions( sdk.getUserData( request ).uid ).then( stat => {
|
||||
let owned = false;
|
||||
const now = new Date().getTime();
|
||||
for ( let sub in stat ) {
|
||||
if ( stat[ sub ].expires - now > 0
|
||||
&& ( stat[ sub ].id === 'com.janishutz.MusicPlayer.subscription' || stat[ sub ].id === 'com.janishutz.MusicPlayer.subscription-month' ) ) {
|
||||
owned = true;
|
||||
res.send( jwtToken );
|
||||
} else {
|
||||
res.status( 402 ).send( 'ERR_NOT_OWNED' );
|
||||
}
|
||||
}
|
||||
if ( owned ) {
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
response.send( 'ERR_NOT_OWNED' );
|
||||
}
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
response.status( 404 ).send( 'ERR_NOT_OWNED' );
|
||||
} );
|
||||
} else {
|
||||
response.status( 401 ).send( 'ERR_AUTH_REQUIRED' );
|
||||
} )
|
||||
.catch( e => {
|
||||
if ( e === 'ERR_NOT_OWNED' ) {
|
||||
res.status( 402 ).send( e );
|
||||
} else if ( e === 'ERR_AUTH_REQUIRED' ) {
|
||||
res.status( 401 ).send( e );
|
||||
} else {
|
||||
res.send( 500 ).send( e );
|
||||
}
|
||||
} );
|
||||
}
|
||||
} );
|
||||
);
|
||||
|
||||
app.use( ( request: express.Request, response: express.Response, next: express.NextFunction ) => {
|
||||
response.status( 404 ).send( 'ERR_NOT_FOUND' );
|
||||
// response.sendFile( path.join( __dirname + '' ) )
|
||||
|
||||
const ownedCache = {};
|
||||
|
||||
const checkIfOwned = ( request: express.Request ): Promise<boolean> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const uid = sdk.getUID( request );
|
||||
|
||||
if ( ownedCache[ uid ] ) {
|
||||
resolve( ownedCache[ uid ] );
|
||||
} else {
|
||||
storeSDK.getSubscriptions( uid )
|
||||
.then( stat => {
|
||||
console.error( 'Subscription check was successful' );
|
||||
const now = new Date().getTime();
|
||||
|
||||
for ( const sub in stat ) {
|
||||
if ( stat[ sub ].expires - now > 0
|
||||
&& (
|
||||
stat[ sub ].id
|
||||
=== 'com.janishutz.MusicPlayer.subscription'
|
||||
|| stat[ sub ].id
|
||||
=== 'com.janishutz.MusicPlayer.subscription-month'
|
||||
)
|
||||
) {
|
||||
ownedCache[ uid ] = true;
|
||||
resolve( true );
|
||||
}
|
||||
}
|
||||
|
||||
ownedCache[ uid ] = false;
|
||||
resolve( false );
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( 'Subscription check unsuccessful with error', e );
|
||||
reject( 'ERR_NOT_OWNED' );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
app.get(
|
||||
'/checkUserStatus',
|
||||
cors( corsOpts ),
|
||||
sdk.loginCheck(),
|
||||
( request: express.Request, response: express.Response ) => {
|
||||
checkIfOwned( request )
|
||||
.then( owned => {
|
||||
if ( owned ) {
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
response.status( 402 ).send( 'ERR_NOT_OWNED' );
|
||||
}
|
||||
} )
|
||||
.catch( e => {
|
||||
if ( e === 'ERR_NOT_OWNED' ) {
|
||||
response.status( 402 ).send( e );
|
||||
} else if ( e === 'ERR_AUTH_REQUIRED' ) {
|
||||
response.status( 401 ).send( e );
|
||||
} else {
|
||||
response.send( 500 ).send( e );
|
||||
}
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
app.use( ( request: express.Request, response: express.Response ) => {
|
||||
response.status( 404 ).send( 'ERR_NOT_FOUND: ' + request.path );
|
||||
} );
|
||||
|
||||
|
||||
const PORT = process.env.PORT || 8082;
|
||||
|
||||
httpServer.listen( PORT );
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
run
|
||||
}
|
||||
};
|
||||
|
||||
31
backend/src/definitions.d.ts
vendored
31
backend/src/definitions.d.ts
vendored
@@ -1,18 +1,23 @@
|
||||
export interface Room {
|
||||
playbackStatus: boolean;
|
||||
playbackStart: number;
|
||||
playlist: Song[];
|
||||
playlistIndex: number;
|
||||
roomName: string;
|
||||
roomToken: string;
|
||||
ownerUID: string;
|
||||
useAntiTamper: boolean;
|
||||
'playbackStatus': boolean;
|
||||
'playbackStart': number;
|
||||
'playlist': Song[];
|
||||
'playlistIndex': number;
|
||||
'roomName': string;
|
||||
'roomToken': string;
|
||||
'ownerUID': string;
|
||||
'useAntiTamper': boolean;
|
||||
}
|
||||
|
||||
export interface Song {
|
||||
title: string;
|
||||
artist: string;
|
||||
duration: number;
|
||||
cover: string;
|
||||
additionalInfo?: string;
|
||||
'title': string;
|
||||
'artist': string;
|
||||
'duration': number;
|
||||
'cover': string;
|
||||
'additionalInfo'?: string;
|
||||
}
|
||||
|
||||
export interface SocketData {
|
||||
[key: string]: Room;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import express from 'express';
|
||||
import expressSession from 'express-session';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
// TODO: Use also express-session to make it work with getSessionID and session referencing
|
||||
const checkAuth = ( request: express.Request ) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface AuthSDKConfig {
|
||||
token: string;
|
||||
name: string;
|
||||
client: string;
|
||||
backendURL: string;
|
||||
failReturnURL: string;
|
||||
useSecureCookie?: boolean;
|
||||
}
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
isAuth: boolean;
|
||||
uid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
additionalData: object;
|
||||
}
|
||||
}
|
||||
|
||||
const getUserData = ( request: express.Request ) => {
|
||||
if ( !request.session.uid ) {
|
||||
request.session.uid = crypto.randomUUID();
|
||||
request.session.username = 'FOSS-Version';
|
||||
request.session.email = 'example@example.com';
|
||||
}
|
||||
return { 'email': request.session.email, 'username': request.session.username, 'uid': request.session.uid, 'id': request.session.id };
|
||||
}
|
||||
|
||||
export default {
|
||||
checkAuth,
|
||||
getUserData
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
const getSubscriptions = ( uid: string ) => {
|
||||
return [ {
|
||||
'id': 'com.janishutz.MusicPlayer.subscription',
|
||||
'expires': new Date().getTime() + 200000,
|
||||
'status': true
|
||||
} ];
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const getSubscriptions = ( _uid: string ) => {
|
||||
return [
|
||||
{
|
||||
'id': 'com.janishutz.MusicPlayer.subscription',
|
||||
'expires': new Date().getTime() + 200000,
|
||||
'status': true
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export default {
|
||||
getSubscriptions,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
178
backend/src/socket.ts
Normal file
178
backend/src/socket.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import http from 'node:http';
|
||||
import {
|
||||
Server
|
||||
} from 'socket.io';
|
||||
import type {
|
||||
SocketData,
|
||||
Song
|
||||
} from './definitions';
|
||||
|
||||
const useWebSocket = ( httpServer: http.Server, socketData: SocketData ) => {
|
||||
// Websocket for events
|
||||
const io = new Server( httpServer, {
|
||||
'cors': {
|
||||
'origin': true,
|
||||
'credentials': true,
|
||||
}
|
||||
} );
|
||||
|
||||
io.on( 'connection', socket => {
|
||||
socket.on( 'create-room', ( room: {
|
||||
'name': string,
|
||||
'token': string
|
||||
}, cb: ( res: {
|
||||
'status': boolean,
|
||||
'msg': string
|
||||
} ) => void ) => {
|
||||
if ( socketData[ room.name ] ) {
|
||||
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||
socket.join( room.name );
|
||||
cb( {
|
||||
'status': true,
|
||||
'msg': 'ADDED_TO_ROOM'
|
||||
} );
|
||||
} else {
|
||||
cb( {
|
||||
'status': false,
|
||||
'msg': 'ERR_TOKEN_INVALID'
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
cb( {
|
||||
'status': false,
|
||||
'msg': 'ERR_NAME_INVALID'
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'delete-room', ( room: {
|
||||
'name': string,
|
||||
'token': string
|
||||
}, cb: ( res: {
|
||||
'status': boolean,
|
||||
'msg': string
|
||||
} ) => void ) => {
|
||||
if ( socketData[ room.name ] ) {
|
||||
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||
socket.leave( room.name );
|
||||
socket.to( room.name ).emit( 'delete-share', room.name );
|
||||
socketData[ room.name ] = undefined;
|
||||
cb( {
|
||||
'status': true,
|
||||
'msg': 'ROOM_DELETED'
|
||||
} );
|
||||
} else {
|
||||
cb( {
|
||||
'status': false,
|
||||
'msg': 'ERR_TOKEN_INVALID'
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
cb( {
|
||||
'status': false,
|
||||
'msg': 'ERR_NAME_INVALID'
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'join-room', ( room: string, cb: ( res: {
|
||||
'status': boolean,
|
||||
'msg': string,
|
||||
'data'?: {
|
||||
'playbackStatus': boolean,
|
||||
'playbackStart': number,
|
||||
'playlist': Song[],
|
||||
'playlistIndex': number,
|
||||
'useAntiTamper': boolean
|
||||
}
|
||||
} ) => void ) => {
|
||||
if ( socketData[ room ] ) {
|
||||
socket.join( room );
|
||||
cb( {
|
||||
'data': {
|
||||
'playbackStart': socketData[ room ].playbackStart,
|
||||
'playbackStatus': socketData[ room ].playbackStatus,
|
||||
'playlist': socketData[ room ].playlist,
|
||||
'playlistIndex': socketData[ room ].playlistIndex,
|
||||
'useAntiTamper': socketData[ room ].useAntiTamper,
|
||||
},
|
||||
'msg': 'STATUS_OK',
|
||||
'status': true,
|
||||
} );
|
||||
} else {
|
||||
cb( {
|
||||
'msg': 'ERR_NO_ROOM_WITH_THIS_ID',
|
||||
'status': false,
|
||||
} );
|
||||
socket.disconnect();
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'tampering', ( data: {
|
||||
'msg': string,
|
||||
'roomName': string
|
||||
} ) => {
|
||||
if ( data.roomName ) {
|
||||
socket.to( data.roomName ).emit( 'tampering-msg', data.msg );
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'playlist-update', ( data: {
|
||||
'roomName': string,
|
||||
'roomToken': string,
|
||||
'data': Song[]
|
||||
} ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
if ( socketData[ data.roomName ].playlist !== data.data ) {
|
||||
socketData[ data.roomName ].playlist = data.data;
|
||||
io.to( data.roomName ).emit( 'playlist', data.data );
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'playback-update', ( data: {
|
||||
'roomName': string,
|
||||
'roomToken': string,
|
||||
'data': boolean
|
||||
} ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
socketData[ data.roomName ].playbackStatus = data.data;
|
||||
io.to( data.roomName ).emit( 'playback', data.data );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'playlist-index-update', ( data: {
|
||||
'roomName': string,
|
||||
'roomToken': string,
|
||||
'data': number
|
||||
} ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
socketData[ data.roomName ].playlistIndex = data.data;
|
||||
io.to( data.roomName ).emit( 'playlist-index', data.data );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
socket.on( 'playback-start-update', ( data: {
|
||||
'roomName': string,
|
||||
'roomToken': string,
|
||||
'data': number
|
||||
} ) => {
|
||||
if ( socketData[ data.roomName ] ) {
|
||||
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||
socketData[ data.roomName ].playbackStart = data.data;
|
||||
io.to( data.roomName ).emit( 'playback-start', data.data );
|
||||
}
|
||||
}
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
export default {
|
||||
useWebSocket
|
||||
};
|
||||
252
backend/src/sse.ts
Normal file
252
backend/src/sse.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import {
|
||||
SocketData
|
||||
} from './definitions';
|
||||
|
||||
const useSSE = (
|
||||
app: express.Application,
|
||||
socketData: SocketData,
|
||||
corsOpts: cors.CorsOptions,
|
||||
getSessionID: ( request: express.Request ) => string,
|
||||
getSignedIn: ( request: express.Request ) => boolean
|
||||
) => {
|
||||
/*
|
||||
ROUTES FOR SERVER SENT EVENTS VERSION
|
||||
*/
|
||||
// Connected clients have their session ID as key
|
||||
interface SocketClientList {
|
||||
[key: string]: SocketClient;
|
||||
}
|
||||
|
||||
interface SocketClient {
|
||||
'response': express.Response;
|
||||
'room': string;
|
||||
}
|
||||
|
||||
interface ClientReferenceList {
|
||||
/**
|
||||
* Find all clients connected to one room
|
||||
*/
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
const importantClients: SocketClientList = {};
|
||||
const connectedClients: SocketClientList = {};
|
||||
const clientReference: ClientReferenceList = {};
|
||||
|
||||
app.get(
|
||||
'/socket/connection',
|
||||
cors( corsOpts ),
|
||||
( request: express.Request, response: express.Response ) => {
|
||||
if ( request.query.room ) {
|
||||
if ( socketData[ String( request.query.room ) ] ) {
|
||||
response.writeHead( 200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
} );
|
||||
response.status( 200 );
|
||||
response.flushHeaders();
|
||||
response.write( `data: ${ JSON.stringify( {
|
||||
'type': 'basics',
|
||||
'data': socketData[ String( request.query.room ) ]
|
||||
} ) }\n\n` );
|
||||
const sid = getSessionID( request );
|
||||
|
||||
if ( getSignedIn( request ) ) {
|
||||
importantClients[ sid ] = {
|
||||
'response': response,
|
||||
'room': String( request.query.room )
|
||||
};
|
||||
}
|
||||
|
||||
connectedClients[ sid ] = {
|
||||
'response': response,
|
||||
'room': String( request.query.room )
|
||||
};
|
||||
|
||||
if ( !clientReference[ String( request.query.room ) ] ) {
|
||||
clientReference[ String( request.query.room ) ] = [];
|
||||
}
|
||||
|
||||
if ( !clientReference[ String( request.query.room ) ]
|
||||
.includes( sid ) ) {
|
||||
clientReference[ String( request.query.room ) ].push( sid );
|
||||
}
|
||||
|
||||
request.on( 'close', () => {
|
||||
try {
|
||||
importantClients[ sid ] = undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch ( e ) { /* empty */ }
|
||||
|
||||
const cl = clientReference[ String( request.query.room ) ];
|
||||
|
||||
for ( const c in cl ) {
|
||||
if ( cl[ c ] === sid ) {
|
||||
cl.splice( parseInt( c ), 1 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
connectedClients[ sid ] = undefined;
|
||||
} );
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/socket/getData',
|
||||
cors( corsOpts ),
|
||||
( request: express.Request, response: express.Response ) => {
|
||||
if ( request.query.room ) {
|
||||
response.send( socketData[ String( request.query.room ) ] );
|
||||
} else {
|
||||
response.status( 400 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/socket/joinRoom',
|
||||
cors( corsOpts ),
|
||||
( request: express.Request, response: express.Response ) => {
|
||||
if ( request.query.room ) {
|
||||
if ( socketData[ String( request.query.room ) ] ) {
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.options( '/socket/update', cors( corsOpts ) );
|
||||
app.post(
|
||||
'/socket/update',
|
||||
cors( corsOpts ),
|
||||
bodyParser.json(),
|
||||
( request: express.Request, response: express.Response ) => {
|
||||
if ( socketData[ request.body.roomName ] ) {
|
||||
if ( request.body.event === 'tampering' ) {
|
||||
const clients = clientReference[ request.body.roomName ];
|
||||
|
||||
for ( const client in clients ) {
|
||||
if ( importantClients[ clients[ client ] ] ) {
|
||||
importantClients[ clients[ client ] ]
|
||||
.response.write( 'data: ' + JSON.stringify( {
|
||||
'type': 'tampering-msg',
|
||||
'data': true
|
||||
} ) + '\n\n' );
|
||||
}
|
||||
}
|
||||
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
if (
|
||||
socketData[ request.body.roomName ].roomToken
|
||||
=== request.body.roomToken
|
||||
) {
|
||||
let send = false;
|
||||
let update = '';
|
||||
|
||||
if ( request.body.event === 'playback-start-update' ) {
|
||||
send = true;
|
||||
update = 'playback-start';
|
||||
socketData[ request.body.roomName ]
|
||||
.playbackStart = request.body.data;
|
||||
} else if ( request.body.event === 'playback-update' ) {
|
||||
send = true;
|
||||
update = 'playback';
|
||||
socketData[ request.body.roomName ]
|
||||
.playbackStatus = request.body.data;
|
||||
} else if ( request.body.event === 'playlist-update' ) {
|
||||
send = true;
|
||||
update = 'playlist';
|
||||
socketData[ request.body.roomName ]
|
||||
.playlist = request.body.data;
|
||||
} else if ( request.body.event === 'playlist-index-update' ) {
|
||||
send = true;
|
||||
update = 'playlist-index';
|
||||
socketData[ request.body.roomName ]
|
||||
.playlistIndex = request.body.data;
|
||||
}
|
||||
|
||||
if ( send ) {
|
||||
const clients = clientReference[ request.body.roomName ];
|
||||
|
||||
for ( const client in clients ) {
|
||||
if ( connectedClients[ clients[ client ] ] ) {
|
||||
connectedClients[ clients[ client ] ]
|
||||
.response.write( 'data: ' + JSON.stringify( {
|
||||
'type': update,
|
||||
'data': request.body.data
|
||||
} ) + '\n\n' );
|
||||
}
|
||||
}
|
||||
|
||||
response.send( 'ok' );
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_CANNOT_SEND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.status( 400 ).send( 'ERR_WRONG_REQUEST' );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
app.options( '/socket/deleteRoom', cors( corsOpts ) );
|
||||
app.post(
|
||||
'/socket/deleteRoom',
|
||||
cors( corsOpts ),
|
||||
bodyParser.json(),
|
||||
( request: express.Request, response: express.Response ) => {
|
||||
if ( request.body.roomName ) {
|
||||
if ( socketData[ request.body.roomName ] ) {
|
||||
if (
|
||||
socketData[ request.body.roomName ].roomToken
|
||||
=== request.body.roomToken
|
||||
) {
|
||||
socketData[ request.body.roomName ] = undefined;
|
||||
const clients = clientReference[ request.body.roomName ];
|
||||
|
||||
for ( const client in clients ) {
|
||||
if ( connectedClients[ clients[ client ] ] ) {
|
||||
connectedClients[ clients[ client ] ]
|
||||
.response.write( 'data: ' + JSON.stringify( {
|
||||
'type': 'delete-share',
|
||||
'data': true
|
||||
} ) + '\n\n' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.send( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||
}
|
||||
} else {
|
||||
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||
}
|
||||
} else {
|
||||
response.status( 400 ).send( 'ERR_NO_ROOM_NAME' );
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
useSSE
|
||||
};
|
||||
@@ -11,8 +11,9 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import * as sqlDB from './mysqldb.js';
|
||||
|
||||
declare let __dirname: string | undefined
|
||||
if ( typeof( __dirname ) === 'undefined' ) {
|
||||
declare let __dirname: string | undefined;
|
||||
|
||||
if ( typeof __dirname === 'undefined' ) {
|
||||
__dirname = path.resolve( path.dirname( '' ) );
|
||||
} else {
|
||||
__dirname = __dirname + '/../';
|
||||
@@ -22,9 +23,8 @@ const dbRef = {
|
||||
'user': 'music_users',
|
||||
'users': 'music_users',
|
||||
};
|
||||
const dbh = new sqlDB.SQLDB();
|
||||
|
||||
|
||||
let dbh = new sqlDB.SQLDB();
|
||||
dbh.connect();
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ dbh.connect();
|
||||
* @returns {undefined}
|
||||
*/
|
||||
const initDB = (): undefined => {
|
||||
( async() => {
|
||||
( async () => {
|
||||
console.log( '[ DB ] Setting up...' );
|
||||
dbh.setupDB();
|
||||
console.log( '[ DB ] Setting up complete!' );
|
||||
@@ -48,11 +48,16 @@ const initDB = (): undefined => {
|
||||
*/
|
||||
const getDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
dbh.query( { 'command': 'getFilteredData', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( data => {
|
||||
dbh.query( {
|
||||
'command': 'getFilteredData',
|
||||
'property': column,
|
||||
'searchQuery': searchQuery
|
||||
}, dbRef[ db ] ).then( data => {
|
||||
resolve( data );
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
@@ -66,7 +71,7 @@ const getDataSimple = ( db: string, column: string, searchQuery: string ): Promi
|
||||
* @param {string} nameOfMatchingParam Which properties should be matched to get the data, e.g. order.user_id=users.id
|
||||
* @returns {Promise<Object | Error>} Returns all records from the db and all matching data specified with the matchingParam from the secondTable.
|
||||
*/
|
||||
const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: string, secondTable: string, columns: object, nameOfMatchingParam: string ): Promise<Object> => {
|
||||
const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: string, secondTable: string, columns: object, nameOfMatchingParam: string ): Promise<object> => {
|
||||
/*
|
||||
LeftJoin (Select values in first table and return all corresponding values of second table):
|
||||
- operation.property (the column to search for the value),
|
||||
@@ -76,24 +81,26 @@ const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: s
|
||||
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||
*/
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
let settings = {
|
||||
const settings = {
|
||||
'command': 'LeftJoin',
|
||||
'property': column,
|
||||
'searchQuery': searchQuery,
|
||||
'selection': '',
|
||||
'secondTable': dbRef[ secondTable ],
|
||||
'matchingParam': dbRef[ db ] + '.' + nameOfMatchingParam + '=' + dbRef[ secondTable ] + '.' + nameOfMatchingParam,
|
||||
}
|
||||
for ( let el in columns ) {
|
||||
};
|
||||
|
||||
for ( const el in columns ) {
|
||||
settings.selection += dbRef[ columns[ el ].db ] + '.' + columns[ el ].column + ',';
|
||||
}
|
||||
|
||||
settings.selection = settings.selection.slice( 0, settings.selection.length - 1 );
|
||||
dbh.query( settings, dbRef[ db ] ).then( data => {
|
||||
resolve( data );
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
@@ -102,13 +109,16 @@ const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: s
|
||||
* @param {string} db The database of which all data should be retrieved
|
||||
* @returns {Promise<object>} Returns an object containing all data
|
||||
*/
|
||||
const getData = ( db: string ): Promise<Object> => {
|
||||
const getData = ( db: string ): Promise<object> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
dbh.query( { 'command': 'getAllData' }, dbRef[ db ] ).then( data => {
|
||||
dbh.query( {
|
||||
'command': 'getAllData'
|
||||
}, dbRef[ db ] ).then( data => {
|
||||
resolve( data );
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
@@ -120,25 +130,40 @@ const getData = ( db: string ): Promise<Object> => {
|
||||
* @param {string} data The data to write. Also include the column & searchQuery parameters, if they also need to be added
|
||||
* @returns {Promise<object>} Returns a promise that resolves to the interaction module return.
|
||||
*/
|
||||
const writeDataSimple = ( db: string, column: string, searchQuery: string, data: any ): Promise<Object> => {
|
||||
const writeDataSimple = ( db: string, column: string, searchQuery: string, data: any ): Promise<object> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
dbh.query( { 'command': 'checkDataAvailability', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( res => {
|
||||
dbh.query( {
|
||||
'command': 'checkDataAvailability',
|
||||
'property': column,
|
||||
'searchQuery': searchQuery
|
||||
}, dbRef[ db ] ).then( res => {
|
||||
if ( res.length > 0 ) {
|
||||
dbh.query( { 'command': 'updateData', 'property': column, 'searchQuery': searchQuery, 'newValues': data }, dbRef[ db ] ).then( dat => {
|
||||
dbh.query( {
|
||||
'command': 'updateData',
|
||||
'property': column,
|
||||
'searchQuery': searchQuery,
|
||||
'newValues': data
|
||||
}, dbRef[ db ] ).then( dat => {
|
||||
resolve( dat );
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} else {
|
||||
dbh.query( { 'command': 'addData', 'data': data }, dbRef[ db ] ).then( dat => {
|
||||
dbh.query( {
|
||||
'command': 'addData',
|
||||
'data': data
|
||||
}, dbRef[ db ] ).then( dat => {
|
||||
resolve( dat );
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
}
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
@@ -151,11 +176,16 @@ const writeDataSimple = ( db: string, column: string, searchQuery: string, data:
|
||||
*/
|
||||
const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
dbh.query( { 'command': 'deleteData', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( dat => {
|
||||
dbh.query( {
|
||||
'command': 'deleteData',
|
||||
'property': column,
|
||||
'searchQuery': searchQuery
|
||||
}, dbRef[ db ] ).then( dat => {
|
||||
resolve( dat );
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
@@ -168,15 +198,20 @@ const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Pr
|
||||
*/
|
||||
const checkDataAvailability = ( db: string, column: string, searchQuery: string ): Promise<boolean> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
dbh.query( { 'command': 'checkDataAvailability', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( res => {
|
||||
dbh.query( {
|
||||
'command': 'checkDataAvailability',
|
||||
'property': column,
|
||||
'searchQuery': searchQuery
|
||||
}, dbRef[ db ] ).then( res => {
|
||||
if ( res.length > 0 ) {
|
||||
resolve( true );
|
||||
} else {
|
||||
resolve( false );
|
||||
}
|
||||
} ).catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} )
|
||||
.catch( error => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
@@ -186,16 +221,18 @@ const checkDataAvailability = ( db: string, column: string, searchQuery: string
|
||||
* @returns {Promise<object>} Returns the data from all files
|
||||
*/
|
||||
const getJSONDataBatch = async ( files: Array<string> ): Promise<object> => {
|
||||
let allFiles = {};
|
||||
for ( let file in files ) {
|
||||
const allFiles = {};
|
||||
|
||||
for ( const file in files ) {
|
||||
try {
|
||||
allFiles[ files[ file ] ] = await getJSONData( files[ file ] );
|
||||
} catch( err ) {
|
||||
} catch ( err ) {
|
||||
allFiles[ files[ file ] ] = 'ERROR: ' + err;
|
||||
}
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load all data from a JSON file
|
||||
@@ -245,7 +282,7 @@ const getJSONDataSimple = ( file: string, identifier: string ): Promise<object>
|
||||
* @param {string} file The file to be loaded (path relative to root)
|
||||
* @returns {object} Returns the JSON file
|
||||
*/
|
||||
const getJSONDataSync = ( file: string ): Object => {
|
||||
const getJSONDataSync = ( file: string ): object => {
|
||||
return JSON.parse( fs.readFileSync( path.join( __dirname + '/' + file ) ).toString() );
|
||||
};
|
||||
|
||||
@@ -263,14 +300,17 @@ const writeJSONDataSimple = ( db: string, identifier: string, values: any ) => {
|
||||
reject( 'Error occurred: Error trace: ' + error );
|
||||
} else {
|
||||
let dat = {};
|
||||
|
||||
if ( data.byteLength > 0 ) {
|
||||
dat = JSON.parse( data.toString() ) ?? {};
|
||||
}
|
||||
|
||||
dat[ identifier ] = values;
|
||||
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), ( error ) => {
|
||||
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), error => {
|
||||
if ( error ) {
|
||||
reject( 'Error occurred: Error trace: ' + error );
|
||||
}
|
||||
|
||||
resolve( true );
|
||||
} );
|
||||
}
|
||||
@@ -286,7 +326,7 @@ const writeJSONDataSimple = ( db: string, identifier: string, values: any ) => {
|
||||
*/
|
||||
const writeJSONData = ( db: string, data: object ): Promise<boolean> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( data ), ( error ) => {
|
||||
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( data ), error => {
|
||||
if ( error ) {
|
||||
reject( 'Error occurred: Error trace: ' + error );
|
||||
} else {
|
||||
@@ -309,14 +349,17 @@ const deleteJSONDataSimple = ( db: string, identifier: string ): Promise<boolean
|
||||
reject( 'Error occurred: Error trace: ' + error );
|
||||
} else {
|
||||
let dat = {};
|
||||
|
||||
if ( data.byteLength > 0 ) {
|
||||
dat = JSON.parse( data.toString() ) ?? {};
|
||||
}
|
||||
|
||||
delete dat[ identifier ];
|
||||
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), ( error ) => {
|
||||
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), error => {
|
||||
if ( error ) {
|
||||
reject( 'Error occurred: Error trace: ' + error );
|
||||
}
|
||||
|
||||
resolve( true );
|
||||
} );
|
||||
}
|
||||
@@ -324,7 +367,19 @@ const deleteJSONDataSimple = ( db: string, identifier: string ): Promise<boolean
|
||||
} );
|
||||
};
|
||||
|
||||
export default { initDB, checkDataAvailability, deleteDataSimple, deleteJSONDataSimple, getData,
|
||||
getDataSimple, getDataWithLeftJoinFunction, getJSONData, getJSONDataBatch, getJSONDataSimple,
|
||||
getJSONDataSync, writeDataSimple, writeJSONData, writeJSONDataSimple
|
||||
export default {
|
||||
initDB,
|
||||
checkDataAvailability,
|
||||
deleteDataSimple,
|
||||
deleteJSONDataSimple,
|
||||
getData,
|
||||
getDataSimple,
|
||||
getDataWithLeftJoinFunction,
|
||||
getJSONData,
|
||||
getJSONDataBatch,
|
||||
getJSONDataSimple,
|
||||
getJSONDataSync,
|
||||
writeDataSimple,
|
||||
writeJSONData,
|
||||
writeJSONDataSimple
|
||||
};
|
||||
|
||||
@@ -11,8 +11,9 @@ import mysql from 'mysql';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
declare let __dirname: string | undefined
|
||||
if ( typeof( __dirname ) === 'undefined' ) {
|
||||
declare let __dirname: string | undefined;
|
||||
|
||||
if ( typeof __dirname === 'undefined' ) {
|
||||
__dirname = path.resolve( path.dirname( '' ) );
|
||||
} else {
|
||||
__dirname = __dirname + '/../';
|
||||
@@ -22,21 +23,35 @@ if ( typeof( __dirname ) === 'undefined' ) {
|
||||
// to the whitelist of the database
|
||||
|
||||
class SQLConfig {
|
||||
|
||||
command: string;
|
||||
|
||||
property?: string;
|
||||
|
||||
searchQuery?: string;
|
||||
|
||||
selection?: string;
|
||||
|
||||
query?: string;
|
||||
|
||||
newValues?: object;
|
||||
|
||||
secondTable?: string;
|
||||
|
||||
matchingParam?: string;
|
||||
|
||||
data?: object;
|
||||
|
||||
}
|
||||
|
||||
class SQLDB {
|
||||
|
||||
sqlConnection: mysql.Connection;
|
||||
|
||||
isRecovering: boolean;
|
||||
|
||||
config: object;
|
||||
|
||||
constructor () {
|
||||
this.config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/db.config.secret.json' ) ) );
|
||||
this.sqlConnection = mysql.createConnection( this.config );
|
||||
@@ -46,19 +61,23 @@ class SQLDB {
|
||||
connect () {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const self = this;
|
||||
|
||||
if ( this.isRecovering ) {
|
||||
console.log( '[ SQL ] Attempting to recover from critical error' );
|
||||
this.sqlConnection = mysql.createConnection( this.config );
|
||||
this.isRecovering = false;
|
||||
}
|
||||
this.sqlConnection.connect( ( err ) => {
|
||||
|
||||
this.sqlConnection.connect( err => {
|
||||
if ( err ) {
|
||||
console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
|
||||
reject( err );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log( '[ SQL ] Connected to database successfully' );
|
||||
self.sqlConnection.on( 'error', ( err ) => {
|
||||
self.sqlConnection.on( 'error', err => {
|
||||
if ( err.code === 'ECONNRESET' ) {
|
||||
self.isRecovering = true;
|
||||
setTimeout( () => {
|
||||
@@ -81,15 +100,17 @@ class SQLDB {
|
||||
async setupDB () {
|
||||
this.sqlConnection.query( 'SELECT @@default_storage_engine;', ( error, results ) => {
|
||||
if ( error ) throw error;
|
||||
|
||||
if ( results[ 0 ][ '@@default_storage_engine' ] !== 'InnoDB' ) throw 'DB HAS TO USE InnoDB!';
|
||||
} );
|
||||
this.sqlConnection.query( 'CREATE TABLE music_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, uid TINYTEXT, lang TINYTEXT, username TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', ( error ) => {
|
||||
this.sqlConnection.query( 'CREATE TABLE music_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, uid TINYTEXT, lang TINYTEXT, username TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', error => {
|
||||
if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error;
|
||||
|
||||
return 'DONE';
|
||||
} );
|
||||
}
|
||||
|
||||
query ( operation: SQLConfig, table: string ): Promise<Array<Object>> {
|
||||
query ( operation: SQLConfig, table: string ): Promise<Array<object>> {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
/*
|
||||
Possible operation.command values (all need the table argument of the method call):
|
||||
@@ -141,6 +162,7 @@ class SQLDB {
|
||||
- operation.query (the SQL instruction to be executed) --> NOTE: This command will not be sanitised, so use only with proper sanitisation!
|
||||
*/
|
||||
let command = '';
|
||||
|
||||
if ( operation.command === 'getAllData' ) {
|
||||
command = 'SELECT * FROM ' + table;
|
||||
} else if ( operation.command === 'getFilteredData' || operation.command === 'checkDataAvailability' ) {
|
||||
@@ -150,19 +172,23 @@ class SQLDB {
|
||||
} else if ( operation.command === 'addData' ) {
|
||||
let keys = '';
|
||||
let values = '';
|
||||
for ( let key in operation.data ) {
|
||||
|
||||
for ( const key in operation.data ) {
|
||||
keys += String( key ) + ', ';
|
||||
values += this.sqlConnection.escape( String( operation.data[ key ] ) ) + ', ' ;
|
||||
values += this.sqlConnection.escape( String( operation.data[ key ] ) ) + ', ';
|
||||
}
|
||||
command = 'INSERT INTO ' + table + ' (' + keys.slice( 0, keys.length - 2 ) + ') VALUES (' + values.slice( 0, values.length - 2 ) + ');';
|
||||
|
||||
command = 'INSERT INTO ' + table + ' (' + keys.slice( 0, keys.length - 2 ) + ') VALUES (' + values.slice( 0, values.length - 2 ) + ');';
|
||||
} else if ( operation.command === 'updateData' ) {
|
||||
if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' );
|
||||
else {
|
||||
command = 'UPDATE ' + table + ' SET ';
|
||||
let updatedValues = '';
|
||||
for ( let value in operation.newValues ) {
|
||||
|
||||
for ( const value in operation.newValues ) {
|
||||
updatedValues += value + ' = ' + this.sqlConnection.escape( String( operation.newValues[ value ] ) ) + ', ';
|
||||
}
|
||||
|
||||
command += updatedValues.slice( 0, updatedValues.length - 2 );
|
||||
command += ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||
}
|
||||
@@ -178,12 +204,17 @@ class SQLDB {
|
||||
} else if ( operation.command === 'RightJoin' ) {
|
||||
command = 'SELECT ' + operation.selection + ' FROM ' + table + ' RIGHT JOIN ' + operation.secondTable + ' ON ' + operation.matchingParam + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||
}
|
||||
|
||||
this.sqlConnection.query( command, ( error, results ) => {
|
||||
if ( error ) reject( error );
|
||||
|
||||
resolve( results );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { SQLConfig, SQLDB };
|
||||
export {
|
||||
SQLConfig, SQLDB
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "MusicPlayer",
|
||||
"version": "3.0.0",
|
||||
"private": false,
|
||||
"private": false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user