mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04: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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ node_modules
|
|||||||
*.secret.json
|
*.secret.json
|
||||||
apple_private_key.p8
|
apple_private_key.p8
|
||||||
musicplayerv2-server.zip
|
musicplayerv2-server.zip
|
||||||
dist
|
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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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 -->
|
<!-- TODO: Update URL -->
|
||||||
<script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js"></script>
|
<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://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>
|
<title>MusicPlayer</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
<body>
|
||||||
|
<noscript>This application requires JavaScript to work!</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script>
|
<script>
|
||||||
localStorage.setItem( 'music-player-config', 'sse' );// Or 'ws'
|
localStorage.setItem('music-player-config', 'sse');// Or 'ws'
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</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": {
|
"dependencies": {
|
||||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||||
|
"@janishutz/login-sdk-browser": "^1.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
"@melloware/coloris": "^0.24.0",
|
"@melloware/coloris": "^0.24.0",
|
||||||
"@rollup/plugin-inject": "^5.0.5",
|
"@rollup/plugin-inject": "^5.0.5",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"colorthief": "^2.2.0",
|
"colorthief": "^2.6.0",
|
||||||
"music-metadata-browser": "^2.5.10",
|
"music-metadata": "^11.9.0",
|
||||||
"musickit-typescript": "^1.2.4",
|
"musickit-typescript": "^1.2.4",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"socket.io-client": "^4.7.5",
|
"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": "^3.4.15",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.3.3",
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@stylistic/eslint-plugin": "^5.4.0",
|
||||||
"@tsconfig/node20": "^20.1.2",
|
"@tsconfig/node20": "^20.1.2",
|
||||||
|
"@types/jquery": "^3.5.33",
|
||||||
"@types/node": "^20.11.10",
|
"@types/node": "^20.11.10",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"eslint": "^8.49.0",
|
"eslint-plugin-vue": "^10.5.0",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
|
||||||
"npm-run-all2": "^6.1.1",
|
"npm-run-all2": "^6.1.1",
|
||||||
|
"sass-embedded": "^1.92.0",
|
||||||
"typescript": "~5.3.0",
|
"typescript": "~5.3.0",
|
||||||
"vite": "^5.0.11",
|
"typescript-eslint": "^8.44.1",
|
||||||
|
"vite": "^7.1.4",
|
||||||
"vue-tsc": "^2.0.29"
|
"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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button @click="changeTheme();" id="themeSelector" title="Toggle between light and dark mode"><span class="material-symbols-outlined" v-html="theme"></span></button>
|
<button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
|
||||||
<router-view v-slot="{ Component, route }" id="main-view">
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span class="material-symbols-outlined" v-html="theme"></span>
|
||||||
|
</button>
|
||||||
|
<router-view id="main-view" v-slot="{ Component, route }">
|
||||||
<transition :name="route.meta.transition ? String( route.meta.transition ) : 'fade'" mode="out-in">
|
<transition :name="route.meta.transition ? String( route.meta.transition ) : 'fade'" mode="out-in">
|
||||||
<component :is="Component"></component>
|
<component :is="Component" />
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
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>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: var( --background-color );
|
background-color: var( --background-color );
|
||||||
@@ -178,33 +207,3 @@
|
|||||||
background-position: 0px;
|
background-position: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { RouterView } from 'vue-router';
|
|
||||||
|
|
||||||
const theme = ref( 'light_mode' );
|
|
||||||
|
|
||||||
const changeTheme = () => {
|
|
||||||
if ( theme.value === 'dark_mode' ) {
|
|
||||||
document.documentElement.classList.remove( 'dark' );
|
|
||||||
document.documentElement.classList.add( 'light' );
|
|
||||||
localStorage.setItem( 'theme', 'light_mode' );
|
|
||||||
theme.value = 'light_mode';
|
|
||||||
} else if ( theme.value === 'light_mode' ) {
|
|
||||||
document.documentElement.classList.remove( 'light' );
|
|
||||||
document.documentElement.classList.add( 'dark' );
|
|
||||||
localStorage.setItem( 'theme', 'dark_mode' );
|
|
||||||
theme.value = 'dark_mode';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
theme.value = localStorage.getItem( 'theme' ) ?? '';
|
|
||||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === 'dark_mode' ) {
|
|
||||||
document.documentElement.classList.add( 'dark' );
|
|
||||||
theme.value = 'dark_mode';
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.add( 'light' );
|
|
||||||
theme.value = 'light_mode';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,26 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Library</h1>
|
<h1>Library</h1>
|
||||||
<playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )" :is-logged-in="$props.isLoggedIn"
|
<playlistsView
|
||||||
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"></playlistsView>
|
:playlists="$props.playlists"
|
||||||
|
:is-logged-in="$props.isLoggedIn"
|
||||||
|
@selected-playlist="( id ) => selectPlaylist( id )"
|
||||||
|
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import playlistsView from '@/components/playlistsView.vue';
|
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 ) => {
|
const selectPlaylist = ( id: string ) => {
|
||||||
emits( 'selected-playlist', id );
|
emits( 'selected-playlist', id );
|
||||||
}
|
};
|
||||||
|
|
||||||
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||||
emits( 'custom-playlist', playlist );
|
emits( 'custom-playlist', playlist );
|
||||||
}
|
};
|
||||||
|
|
||||||
defineProps( {
|
defineProps( {
|
||||||
'playlists': {
|
'playlists': {
|
||||||
'default': [],
|
'default': [],
|
||||||
'type': Array<any>,
|
'type': Array<any>,
|
||||||
@@ -32,4 +41,4 @@
|
|||||||
'required': true,
|
'required': true,
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
<div id="notifications">
|
<div id="notifications">
|
||||||
<div class="message-box" :class="[ location, size ]" :style="'z-index: ' + ( messageType === 'hide' ? '-1' : '1000' )">
|
<div class="message-box" :class="[ location, size ]" :style="'z-index: ' + ( messageType === 'hide' ? '-1' : '1000' )">
|
||||||
<div class="message-container" :class="messageType">
|
<div class="message-container" :class="messageType">
|
||||||
<button @click="handleNotifications();" class="close-notification"><span class="material-symbols-outlined close-notification-icon">close</span></button>
|
<button class="close-notification" @click="handleNotifications();">
|
||||||
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
|
<span class="material-symbols-outlined close-notification-icon">close</span>
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
|
</button>
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
|
<span v-if="messageType == 'hide'" class="material-symbols-outlined types hide">question_mark</span>
|
||||||
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
|
<span v-else-if="messageType == 'ok'" class="material-symbols-outlined types" style="background-color: green;">done</span>
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
|
<span v-else-if="messageType == 'error'" class="material-symbols-outlined types" style="background-color: red;">close</span>
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
|
<span v-else-if="messageType == 'progress'" class="material-symbols-outlined types progress-spinner" style="background-color: blue;">progress_activity</span>
|
||||||
<p class="message" @click="notificationAction()">{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}</p>
|
<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 :class="'countdown countdown-' + messageType" :style="'width: ' + ( 100 - ( currentTime - notificationDisplayStartTime ) / ( notifications[ currentDID ] ? notifications[ currentDID ].showDuration : 1 ) / 10 ) + '%'"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,28 +23,30 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { onUnmounted, ref, type Ref } from 'vue';
|
import {
|
||||||
|
type Ref, onUnmounted, ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
defineProps( {
|
defineProps( {
|
||||||
location: {
|
'location': {
|
||||||
type: String,
|
'type': String,
|
||||||
'default': 'topleft',
|
'default': 'topleft',
|
||||||
},
|
},
|
||||||
size: {
|
'size': {
|
||||||
type: String,
|
'type': String,
|
||||||
'default': 'default',
|
'default': 'default',
|
||||||
}
|
}
|
||||||
// Size options: small, default (default option), big, bigger, huge
|
// Size options: small, default (default option), big, bigger, huge
|
||||||
} );
|
} );
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
message: string;
|
'message': string;
|
||||||
showDuration: number;
|
'showDuration': number;
|
||||||
messageType: string;
|
'messageType': string;
|
||||||
priority: string;
|
'priority': string;
|
||||||
id: number;
|
'id': number;
|
||||||
redirect?: string;
|
'redirect'?: string;
|
||||||
openInNewTab?: boolean;
|
'openInNewTab'?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationList {
|
interface NotificationList {
|
||||||
@@ -51,11 +57,17 @@
|
|||||||
const queue: Ref<number[]> = ref( [] );
|
const queue: Ref<number[]> = ref( [] );
|
||||||
const currentDID: Ref<number> = ref( 0 );
|
const currentDID: Ref<number> = ref( 0 );
|
||||||
const messageType: Ref<string> = ref( 'hide' );
|
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 notificationDisplayStartTime: Ref<number> = ref( 0 );
|
||||||
const currentTime: Ref<number> = ref( 0 );
|
const currentTime: Ref<number> = ref( 0 );
|
||||||
|
|
||||||
let progressBar = 0;
|
let progressBar = 0;
|
||||||
let notificationTimeout = 0;
|
let notificationTimeout = 0;
|
||||||
|
|
||||||
const notificationAction = () => {
|
const notificationAction = () => {
|
||||||
if ( notifications.value[ currentDID.value ] ) {
|
if ( notifications.value[ currentDID.value ] ) {
|
||||||
if ( notifications.value[ currentDID.value ].redirect ) {
|
if ( notifications.value[ currentDID.value ].redirect ) {
|
||||||
@@ -76,8 +88,10 @@
|
|||||||
* @param {string} priority The priority of the message: 'low', 'normal', 'critical'
|
* @param {string} priority The priority of the message: 'low', 'normal', 'critical'
|
||||||
* @returns {number}
|
* @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).
|
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
|
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
|
||||||
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
|
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
|
||||||
@@ -94,14 +108,25 @@
|
|||||||
currentID.value[ 'low' ] += 1;
|
currentID.value[ 'low' ] += 1;
|
||||||
id = currentID.value[ 'low' ];
|
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 );
|
queue.value.push( id );
|
||||||
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
|
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' ) {
|
if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) || messageType.value === 'hide' ) {
|
||||||
handleNotifications();
|
handleNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a notification's message after creating it
|
* Update a notification's message after creating it
|
||||||
@@ -113,7 +138,7 @@
|
|||||||
if ( notifications.value[ id ] ) {
|
if ( notifications.value[ id ] ) {
|
||||||
notifications.value[ id ].message = message;
|
notifications.value[ id ].message = message;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,31 +147,36 @@
|
|||||||
* @returns {undefined}
|
* @returns {undefined}
|
||||||
*/
|
*/
|
||||||
const cancelNotification = ( id: number ): undefined => {
|
const cancelNotification = ( id: number ): undefined => {
|
||||||
try {
|
try {
|
||||||
delete notifications.value[ id ];
|
delete notifications.value[ id ];
|
||||||
} catch ( error ) {
|
} catch ( error ) {
|
||||||
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
queue.value.splice( queue.value.indexOf( id ), 1 );
|
queue.value.splice( queue.value.indexOf( id ), 1 );
|
||||||
} catch {
|
} catch {
|
||||||
console.debug( 'queue empty' );
|
console.debug( 'queue empty' );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( currentDID.value == id ) {
|
if ( currentDID.value == id ) {
|
||||||
try {
|
try {
|
||||||
clearTimeout( notificationTimeout );
|
clearTimeout( notificationTimeout );
|
||||||
} catch (err) { /* empty */ }
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
handleNotifications();
|
handleNotifications();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleNotifications = () => {
|
const handleNotifications = () => {
|
||||||
notificationDisplayStartTime.value = new Date().getTime();
|
notificationDisplayStartTime.value = new Date().getTime();
|
||||||
queue.value.sort();
|
queue.value.sort();
|
||||||
|
|
||||||
if ( queue.value.length > 0 ) {
|
if ( queue.value.length > 0 ) {
|
||||||
if ( currentDID.value !== 0 ) {
|
if ( currentDID.value !== 0 ) {
|
||||||
delete notifications.value[ currentDID.value ];
|
delete notifications.value[ currentDID.value ];
|
||||||
}
|
}
|
||||||
|
|
||||||
currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ];
|
currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ];
|
||||||
messageType.value = notifications.value[ queue.value[ 0 ] ].messageType;
|
messageType.value = notifications.value[ queue.value[ 0 ] ].messageType;
|
||||||
queue.value.reverse();
|
queue.value.reverse();
|
||||||
@@ -158,22 +188,24 @@
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
clearInterval( progressBar );
|
clearInterval( progressBar );
|
||||||
} catch (err) { /* empty */ }
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
messageType.value = 'hide';
|
messageType.value = 'hide';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const progressBarHandler = () => {
|
const progressBarHandler = () => {
|
||||||
currentTime.value = new Date().getTime();
|
currentTime.value = new Date().getTime();
|
||||||
}
|
};
|
||||||
|
|
||||||
onUnmounted( () => {
|
onUnmounted( () => {
|
||||||
try {
|
try {
|
||||||
clearInterval( progressBar );
|
clearInterval( progressBar );
|
||||||
} catch (err) { /* empty */ }
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clearInterval( notificationTimeout );
|
clearInterval( notificationTimeout );
|
||||||
} catch (err) { /* empty */ }
|
} catch ( err ) { /* empty */ }
|
||||||
} );
|
} );
|
||||||
|
|
||||||
defineExpose( {
|
defineExpose( {
|
||||||
@@ -217,7 +249,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-container {
|
.message-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -228,7 +260,7 @@
|
|||||||
transition: all 0.5s;
|
transition: all 0.5s;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.types {
|
.types {
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
@@ -237,7 +269,7 @@
|
|||||||
padding: 1.5%;
|
padding: 1.5%;
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
margin-right: calc( 5% + 30px );
|
margin-right: calc( 5% + 30px );
|
||||||
text-align: end;
|
text-align: end;
|
||||||
@@ -247,7 +279,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ok {
|
.ok {
|
||||||
background-color: rgb(1, 71, 1);
|
background-color: rgb(1, 71, 1);
|
||||||
}
|
}
|
||||||
@@ -255,7 +287,7 @@
|
|||||||
.countdown-ok {
|
.countdown-ok {
|
||||||
background-color: green;
|
background-color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background-color: rgb(114, 1, 1);
|
background-color: rgb(114, 1, 1);
|
||||||
}
|
}
|
||||||
@@ -263,7 +295,7 @@
|
|||||||
.countdown-error {
|
.countdown-error {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
background-color: rgb(44, 112, 151);
|
background-color: rgb(44, 112, 151);
|
||||||
}
|
}
|
||||||
@@ -271,7 +303,7 @@
|
|||||||
.countdown-info {
|
.countdown-info {
|
||||||
background-color: blue;
|
background-color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
background-color: orange;
|
background-color: orange;
|
||||||
}
|
}
|
||||||
@@ -279,16 +311,16 @@
|
|||||||
.countdown-warning {
|
.countdown-warning {
|
||||||
background-color: orangered;
|
background-color: orangered;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide {
|
.hide {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-color: rgb(0, 0, 99);
|
background-color: rgb(0, 0, 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
.countdown-ok {
|
.countdown-ok {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
@@ -296,7 +328,7 @@
|
|||||||
.progress-spinner {
|
.progress-spinner {
|
||||||
animation: spin 2s infinite linear;
|
animation: spin 2s infinite linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate( 0deg );
|
transform: rotate( 0deg );
|
||||||
@@ -381,4 +413,4 @@
|
|||||||
width: 25vw;
|
width: 25vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,51 +4,109 @@
|
|||||||
<h3>WARNING!</h3>
|
<h3>WARNING!</h3>
|
||||||
<p>A client display is being tampered with!</p>
|
<p>A client display is being tampered with!</p>
|
||||||
<p>A desktop notification with a warning has already been dispatched.</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 class="flash"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="player">
|
<div class="player">
|
||||||
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
||||||
<div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )">
|
<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
|
||||||
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" v-else>
|
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">
|
<div class="name-time">
|
||||||
<p class="song-name">{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i></p>
|
<p class="song-name">
|
||||||
<div class="playback" v-if="!isShowingFullScreenPlayer">
|
{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i>
|
||||||
|
</p>
|
||||||
|
<div v-if="!isShowingFullScreenPlayer" class="playback">
|
||||||
<div class="playback-pos-wrapper">
|
<div class="playback-pos-wrapper">
|
||||||
<p class="playback-pos">{{ nicePlaybackPos }}</p>
|
<p class="playback-pos">
|
||||||
|
{{ nicePlaybackPos }}
|
||||||
|
</p>
|
||||||
<p> / </p>
|
<p> / </p>
|
||||||
<p class="playback-duration">{{ niceDuration }}</p>
|
<p class="playback-duration">
|
||||||
|
{{ niceDuration }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="'controls-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" :style="playlist.length > 0 ? '' : 'pointer-events: none'">
|
<div :class="'controls-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" :style="playlist.length > 0 ? '' : 'pointer-events: none'">
|
||||||
<div class="main-controls">
|
<div class="main-controls">
|
||||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous" v-if="isShowingFullScreenPlayer">skip_previous</span>
|
<span
|
||||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'" v-if="isShowingFullScreenPlayer">replay_10</span>
|
v-if="isShowingFullScreenPlayer"
|
||||||
<span class="material-symbols-outlined controls" v-if="isPlaying" @click="playPause()" id="play-pause">pause</span>
|
id="previous"
|
||||||
<span class="material-symbols-outlined controls" v-else @click="playPause()" id="play-pause">play_arrow</span>
|
class="material-symbols-outlined controls next-previous"
|
||||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'forward' )" :style="'rotate: ' + 360 * clickCountForward + 'deg;'" v-if="isShowingFullScreenPlayer">forward_10</span>
|
@click="control( 'previous' )"
|
||||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'next' )" id="next">skip_next</span>
|
>skip_previous</span>
|
||||||
</div>
|
<span
|
||||||
|
v-if="isShowingFullScreenPlayer"
|
||||||
<div class="slider-wrapper" v-if="isShowingFullScreenPlayer">
|
class="material-symbols-outlined controls forward-back"
|
||||||
<div class="slider-pb-pos">
|
:style="'rotate: -' + 360 * clickCountBack + 'deg;'"
|
||||||
<p class="playback-pos">{{ nicePlaybackPos }}</p>
|
@click="control( 'back' )"
|
||||||
<p class="playback-duration" @click="toggleRemaining()" title="Toggle between remaining time and song duration">{{ niceDuration }}</p>
|
>replay_10</span>
|
||||||
</div>
|
<span
|
||||||
<sliderView :position="pos" :active="true" :duration="duration" name="main" @pos="( pos ) => goToPos( pos )"></sliderView>
|
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>
|
||||||
|
|
||||||
<div class="shuffle-repeat" v-if="isShowingFullScreenPlayer">
|
<div v-if="isShowingFullScreenPlayer" class="slider-wrapper">
|
||||||
<span class="material-symbols-outlined controls" @click="control( 'repeat' )" style="margin-right: auto;">repeat{{ repeatMode }}</span>
|
<div class="slider-pb-pos">
|
||||||
|
<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 )"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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;">
|
<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>
|
<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" title="Stop sharing your playlist on a public playlist page" @click="control( 'stop-share' )">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="Show information on the share, including URL to connect to" @click="control( 'show-share' )">info</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
|
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
|
||||||
@@ -58,33 +116,49 @@
|
|||||||
</div>
|
</div>
|
||||||
<div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )">
|
<div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )">
|
||||||
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
|
<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"
|
<playlistView
|
||||||
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"
|
:playlist="playlist"
|
||||||
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )"
|
class="pl-wrapper"
|
||||||
|
:currently-playing="currentlyPlayingSongIndex"
|
||||||
|
:is-playing="isPlaying"
|
||||||
|
:pos="pos"
|
||||||
:is-logged-into-apple-music="player.isLoggedIn"
|
: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 )"
|
@add-new-songs-apple-music="( song ) => addNewSongFromObject( song )"
|
||||||
@delete-song="song => removeSongFromPlaylist( song )"
|
@delete-song="song => removeSongFromPlaylist( song )"
|
||||||
@clear-playlist="() => clearPlaylist()"
|
@clear-playlist="() => clearPlaylist()"
|
||||||
@send-additional-info="() => sendAdditionalInfo()"></playlistView>
|
@send-additional-info="() => sendAdditionalInfo()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
<notificationsModule ref="notifications" location="bottomleft" size="bigger" />
|
||||||
<popupModule @update="( data ) => popupReturnHandler( data )" ref="popup"></popupModule>
|
<popupModule ref="popup" @update="( data ) => popupReturnHandler( data )" />
|
||||||
<audio src="" id="local-audio" controls="false"></audio>
|
<audio id="local-audio" src="" controls="false"></audio>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, type Ref } from 'vue';
|
import type {
|
||||||
import playlistView from '@/components/playlistView.vue';
|
ReadFile, Song, SongMove
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
type Ref, ref
|
||||||
|
} from 'vue';
|
||||||
import MusicKitJSWrapper from '@/scripts/music-player';
|
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 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 popupModule from './popupModule.vue';
|
||||||
|
import sliderView from './sliderView.vue';
|
||||||
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/stores/userStore';
|
||||||
|
|
||||||
const isPlaying = ref( false );
|
const isPlaying = ref( false );
|
||||||
const repeatMode = ref( '' );
|
const repeatMode = ref( '' );
|
||||||
@@ -100,7 +174,9 @@
|
|||||||
const nicePlaybackPos = ref( '00:00' );
|
const nicePlaybackPos = ref( '00:00' );
|
||||||
const niceDuration = ref( '00:00' );
|
const niceDuration = ref( '00:00' );
|
||||||
const isShowingRemainingTime = ref( false );
|
const isShowingRemainingTime = ref( false );
|
||||||
|
|
||||||
let isShowingRemainingTimeBackend = false;
|
let isShowingRemainingTimeBackend = false;
|
||||||
|
|
||||||
const currentlyPlayingSongArtist = ref( '' );
|
const currentlyPlayingSongArtist = ref( '' );
|
||||||
const pos = ref( 0 );
|
const pos = ref( 0 );
|
||||||
const duration = ref( 0 );
|
const duration = ref( 0 );
|
||||||
@@ -110,6 +186,7 @@
|
|||||||
const popup = ref( popupModule );
|
const popup = ref( popupModule );
|
||||||
const roomName = ref( '' );
|
const roomName = ref( '' );
|
||||||
const isShowingWarning = ref( false );
|
const isShowingWarning = ref( false );
|
||||||
|
|
||||||
let currentlyOpenPopup = '';
|
let currentlyOpenPopup = '';
|
||||||
let logoutErrorNotification = -1;
|
let logoutErrorNotification = -1;
|
||||||
|
|
||||||
@@ -118,19 +195,24 @@
|
|||||||
document.addEventListener( 'musicplayer:autherror', () => {
|
document.addEventListener( 'musicplayer:autherror', () => {
|
||||||
localStorage.setItem( 'close-tab', 'true' );
|
localStorage.setItem( 'close-tab', 'true' );
|
||||||
isConnectedToNotifier.value = false;
|
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', () => {
|
window.addEventListener( 'storage', () => {
|
||||||
if ( localStorage.getItem( 'login-ok' ) === 'true' ) {
|
if ( localStorage.getItem( 'login-ok' ) === 'true' ) {
|
||||||
notifications.value.cancelNotification( logoutErrorNotification );
|
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' );
|
localStorage.removeItem( 'login-ok' );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const playPause = () => {
|
const playPause = () => {
|
||||||
isPlaying.value = !isPlaying.value;
|
isPlaying.value = !isPlaying.value;
|
||||||
|
|
||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
player.control( 'play' );
|
player.control( 'play' );
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
@@ -138,17 +220,17 @@
|
|||||||
player.control( 'pause' );
|
player.control( 'pause' );
|
||||||
stopProgressTracker();
|
stopProgressTracker();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const goToPos = ( position: number ) => {
|
const goToPos = ( position: number ) => {
|
||||||
player.goToPos( position );
|
player.goToPos( position );
|
||||||
pos.value = position;
|
pos.value = position;
|
||||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
||||||
}
|
};
|
||||||
|
|
||||||
const toggleRemaining = () => {
|
const toggleRemaining = () => {
|
||||||
isShowingRemainingTime.value = !isShowingRemainingTime.value;
|
isShowingRemainingTime.value = !isShowingRemainingTime.value;
|
||||||
}
|
};
|
||||||
|
|
||||||
const control = ( action: string ) => {
|
const control = ( action: string ) => {
|
||||||
if ( action === 'pause' ) {
|
if ( action === 'pause' ) {
|
||||||
@@ -184,10 +266,13 @@
|
|||||||
getDetails();
|
getDetails();
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notificationHandler.emit( 'playlist-index-update', player.getQueueID() );
|
||||||
getDetails();
|
getDetails();
|
||||||
} else if ( action === 'forward' ) {
|
} else if ( action === 'forward' ) {
|
||||||
clickCountForward.value += 1;
|
clickCountForward.value += 1;
|
||||||
if( player.control( 'skip-10' ) ) {
|
|
||||||
|
if ( player.control( 'skip-10' ) ) {
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
} else {
|
} else {
|
||||||
pos.value = player.getPlaybackPos();
|
pos.value = player.getPlaybackPos();
|
||||||
@@ -195,7 +280,8 @@
|
|||||||
}
|
}
|
||||||
} else if ( action === 'back' ) {
|
} else if ( action === 'back' ) {
|
||||||
clickCountBack.value += 1;
|
clickCountBack.value += 1;
|
||||||
if( player.control( 'back-10' ) ) {
|
|
||||||
|
if ( player.control( 'back-10' ) ) {
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
} else {
|
} else {
|
||||||
pos.value = player.getPlaybackPos();
|
pos.value = player.getPlaybackPos();
|
||||||
@@ -217,19 +303,19 @@
|
|||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
} else if ( action === 'start-share' ) {
|
} else if ( action === 'start-share' ) {
|
||||||
popup.value.openPopup( {
|
popup.value.openPopup( {
|
||||||
title: 'Define a share name',
|
'title': 'Define a share name',
|
||||||
popupType: 'input',
|
'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.',
|
'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: [
|
'data': [
|
||||||
{
|
{
|
||||||
name: 'Share Name',
|
'name': 'Share Name',
|
||||||
dataType: 'text',
|
'dataType': 'text',
|
||||||
id: 'roomName'
|
'id': 'roomName'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Use Anti-Tamper?',
|
'name': 'Use Anti-Tamper?',
|
||||||
dataType: 'checkbox',
|
'dataType': 'checkbox',
|
||||||
id: 'useAntiTamper'
|
'id': 'useAntiTamper'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} );
|
} );
|
||||||
@@ -238,19 +324,21 @@
|
|||||||
if ( confirm( 'Do you really want to stop sharing?' ) ) {
|
if ( confirm( 'Do you really want to stop sharing?' ) ) {
|
||||||
notificationHandler.disconnect();
|
notificationHandler.disconnect();
|
||||||
isConnectedToNotifier.value = false;
|
isConnectedToNotifier.value = false;
|
||||||
notifications.value.createNotification( 'Disconnected successfully!', 5, 'ok', 'normal' );
|
notifications.value.createNotification(
|
||||||
|
'Disconnected successfully!', 5, 'ok', 'normal'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if ( action === 'show-share' ) {
|
} else if ( action === 'show-share' ) {
|
||||||
popup.value.openPopup( {
|
popup.value.openPopup( {
|
||||||
title: 'Details on share',
|
'title': 'Details on share',
|
||||||
subtitle: 'You are currently connected to share "' + roomName.value
|
'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>'
|
+ '". \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>'
|
+ '. \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.'
|
+ '. 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';
|
currentlyOpenPopup = 'share-details';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const controlUI = ( action: string ) => {
|
const controlUI = ( action: string ) => {
|
||||||
@@ -262,28 +350,30 @@
|
|||||||
isShowingFullScreenPlayer.value = false;
|
isShowingFullScreenPlayer.value = false;
|
||||||
isShowingRemainingTimeBackend = isShowingRemainingTime.value;
|
isShowingRemainingTimeBackend = isShowingRemainingTime.value;
|
||||||
isShowingRemainingTime.value = false;
|
isShowingRemainingTime.value = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
prepNiceDurationTime( player.getPlayingSong() );
|
prepNiceDurationTime( player.getPlayingSong() );
|
||||||
} catch ( err ) { /* empty */ }
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
emits( 'playerStateChange', 'hide' );
|
emits( 'playerStateChange', 'hide' );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const getPlaylists = ( cb: ( data: object ) => void ) => {
|
const getPlaylists = ( cb: ( data: object ) => void ) => {
|
||||||
player.getUserPlaylists( cb );
|
player.getUserPlaylists( cb );
|
||||||
}
|
};
|
||||||
|
|
||||||
const logIntoAppleMusic = () => {
|
const logIntoAppleMusic = () => {
|
||||||
player.logIn();
|
player.logIn();
|
||||||
}
|
};
|
||||||
|
|
||||||
const getAuth = (): boolean[] => {
|
const getAuth = (): boolean[] => {
|
||||||
return player.getAuth();
|
return player.getAuth();
|
||||||
}
|
};
|
||||||
|
|
||||||
const skipLogin = () => {
|
const skipLogin = () => {
|
||||||
player.init();
|
player.init();
|
||||||
}
|
};
|
||||||
|
|
||||||
const selectPlaylist = ( id: string ) => {
|
const selectPlaylist = ( id: string ) => {
|
||||||
currentlyPlayingSongArtist.value = '';
|
currentlyPlayingSongArtist.value = '';
|
||||||
@@ -297,20 +387,26 @@
|
|||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}, 2000 );
|
}, 2000 );
|
||||||
} );
|
} );
|
||||||
}
|
};
|
||||||
|
|
||||||
const selectCustomPlaylist = async ( pl: ReadFile[] ) => {
|
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 = [];
|
playlist.value = [];
|
||||||
let plLoad: Song[] = [];
|
const plLoad: Song[] = [];
|
||||||
|
|
||||||
for ( let element in pl ) {
|
for ( let element in pl ) {
|
||||||
try {
|
try {
|
||||||
plLoad.push( await fetchSongData( pl[ element ] ) );
|
plLoad.push( await fetchSongData( pl[ element ] ) );
|
||||||
} catch ( e ) {
|
} catch ( e ) {
|
||||||
console.error( 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;
|
playlist.value = plLoad;
|
||||||
player.setPlaylist( playlist.value );
|
player.setPlaylist( playlist.value );
|
||||||
player.prepare( 0 );
|
player.prepare( 0 );
|
||||||
@@ -321,82 +417,123 @@
|
|||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}, 2000 );
|
}, 2000 );
|
||||||
notifications.value.cancelNotification( n );
|
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> => {
|
const fetchSongData = ( songDetails: ReadFile ): Promise<Song> => {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
fetch( songDetails.url ).then( res => {
|
fetch( songDetails.url )
|
||||||
if ( res.status === 200 ) {
|
.then( res => {
|
||||||
res.blob().then( blob => {
|
if ( res.status === 200 ) {
|
||||||
parseBlob( blob ).then( data => {
|
res.blob().then( blob => {
|
||||||
try {
|
parseBlob( blob )
|
||||||
player.findSongOnAppleMusic( data.common.title ?? songDetails.filename.split( '.' )[ 0 ] ).then( d => {
|
.then( data => {
|
||||||
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
|
try {
|
||||||
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
|
const searchTerm = data.common.title
|
||||||
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
|
? data.common.title + ( data.common.artist ? ' ' + data.common.artist : '' )
|
||||||
const song: Song = {
|
: songDetails.filename.split( '.' )[ 0 ].replace( '_', ' ' );
|
||||||
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 ),
|
player.findSongOnAppleMusic( searchTerm )
|
||||||
id: songDetails.url,
|
.then( d => {
|
||||||
origin: 'disk',
|
if ( d.data.results.songs ) {
|
||||||
cover: url
|
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 );
|
console.error( e );
|
||||||
const song: Song = {
|
alert( 'One of your songs was not loadable. (parser-error)' );
|
||||||
artist: data.common.artist ?? 'Unknown artist',
|
reject( e );
|
||||||
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 );
|
.catch( e => {
|
||||||
alert( 'One of your songs was not loadable. (finalization-error)' )
|
console.error( e );
|
||||||
}
|
alert( 'One of your songs was not loadable. (converter-error)' );
|
||||||
} ).catch( e => {
|
reject( e );
|
||||||
console.error( e );
|
} );
|
||||||
alert( 'One of your songs was not loadable. (parser-error)' );
|
} else {
|
||||||
reject( e );
|
console.error( res.status );
|
||||||
} );
|
alert( 'One of your songs was not loadable. (invalid-response-code)' );
|
||||||
} ).catch( e => {
|
reject( res.status );
|
||||||
console.error( e );
|
}
|
||||||
alert( 'One of your songs was not loadable. (converter-error)' );
|
} )
|
||||||
reject( e );
|
.catch( e => {
|
||||||
} );
|
console.error( e );
|
||||||
} else {
|
alert( 'One of your songs was not loadable. (could-not-connect)' );
|
||||||
console.error( res.status );
|
reject( e );
|
||||||
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 );
|
|
||||||
} );
|
|
||||||
} );
|
} );
|
||||||
}
|
};
|
||||||
|
|
||||||
const getDetails = () => {
|
const getDetails = () => {
|
||||||
const details = player.getPlayingSong();
|
const details = player.getPlayingSong();
|
||||||
|
|
||||||
currentlyPlayingSongName.value = details.title;
|
currentlyPlayingSongName.value = details.title;
|
||||||
coverArt.value = details.cover;
|
coverArt.value = details.cover;
|
||||||
currentlyPlayingSongIndex.value = player.getQueueID();
|
currentlyPlayingSongIndex.value = player.getQueueID();
|
||||||
playlist.value = player.getQueue();
|
playlist.value = player.getQueue();
|
||||||
currentlyPlayingSongArtist.value = details.artist;
|
currentlyPlayingSongArtist.value = details.artist;
|
||||||
}
|
};
|
||||||
|
|
||||||
const playSong = ( id: string ) => {
|
const playSong = ( id: string ) => {
|
||||||
const p = player.getPlaylist();
|
const p = player.getPlaylist();
|
||||||
|
|
||||||
currentlyPlayingSongArtist.value = '';
|
currentlyPlayingSongArtist.value = '';
|
||||||
coverArt.value = '';
|
coverArt.value = '';
|
||||||
currentlyPlayingSongName.value = 'Loading...';
|
currentlyPlayingSongName.value = 'Loading...';
|
||||||
stopProgressTracker();
|
stopProgressTracker();
|
||||||
|
|
||||||
for ( const s in p ) {
|
for ( const s in p ) {
|
||||||
if ( p[ s ].id === id ) {
|
if ( p[ s ].id === id ) {
|
||||||
player.prepare( parseInt( s ) );
|
player.prepare( parseInt( s ) );
|
||||||
@@ -404,23 +541,29 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
let progressTracker = 0;
|
let progressTracker: ReturnType<typeof setInterval> = setInterval( () => {}, 1000 );
|
||||||
|
|
||||||
|
clearInterval( progressTracker );
|
||||||
let hasReachedEnd = false;
|
let hasReachedEnd = false;
|
||||||
let hasStarted = false;
|
let hasStarted = false;
|
||||||
|
|
||||||
const startProgressTracker = () => {
|
const startProgressTracker = () => {
|
||||||
hasReachedEnd = false;
|
hasReachedEnd = false;
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
let playingSong = player.getPlayingSong();
|
let playingSong = player.getPlayingSong();
|
||||||
|
|
||||||
hasStarted = false;
|
hasStarted = false;
|
||||||
pos.value = 0;
|
pos.value = 0;
|
||||||
progressTracker = setInterval( () => {
|
progressTracker = setInterval( () => {
|
||||||
pos.value = player.getPlaybackPos();
|
pos.value = player.getPlaybackPos();
|
||||||
|
|
||||||
if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) {
|
if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) {
|
||||||
stopProgressTracker();
|
stopProgressTracker();
|
||||||
hasReachedEnd = true;
|
hasReachedEnd = true;
|
||||||
|
|
||||||
if ( repeatMode.value === '_one_on' ) {
|
if ( repeatMode.value === '_one_on' ) {
|
||||||
player.goToPos( 0 );
|
player.goToPos( 0 );
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
@@ -432,21 +575,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( pos.value > 0 && !hasStarted ) {
|
if ( pos.value > 0 && !hasStarted ) {
|
||||||
getDetails();
|
if ( player.getPlaying() ) {
|
||||||
playingSong = player.getPlayingSong();
|
setTimeout( () => {
|
||||||
prepNiceDurationTime( playingSong );
|
getDetails();
|
||||||
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
playingSong = player.getPlayingSong();
|
||||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
pos.value = player.getPlaybackPos();
|
||||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
prepNiceDurationTime( playingSong );
|
||||||
hasStarted = true;
|
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 );
|
const minuteCount = Math.floor( pos.value / 60 );
|
||||||
|
|
||||||
nicePlaybackPos.value = minuteCount + ':';
|
nicePlaybackPos.value = minuteCount + ':';
|
||||||
|
|
||||||
if ( ( '' + minuteCount ).length === 1 ) {
|
if ( ( '' + minuteCount ).length === 1 ) {
|
||||||
nicePlaybackPos.value = '0' + minuteCount + ':';
|
nicePlaybackPos.value = '0' + minuteCount + ':';
|
||||||
}
|
}
|
||||||
const secondCount = Math.floor( pos.value - minuteCount * 60 );
|
|
||||||
|
const secondCount = Math.floor( pos.value - ( minuteCount * 60 ) );
|
||||||
|
|
||||||
if ( ( '' + secondCount ).length === 1 ) {
|
if ( ( '' + secondCount ).length === 1 ) {
|
||||||
nicePlaybackPos.value += '0' + secondCount;
|
nicePlaybackPos.value += '0' + secondCount;
|
||||||
} else {
|
} else {
|
||||||
@@ -455,11 +607,15 @@
|
|||||||
|
|
||||||
if ( isShowingRemainingTime.value ) {
|
if ( isShowingRemainingTime.value ) {
|
||||||
const minuteCounts = Math.floor( ( playingSong.duration - pos.value ) / 60 );
|
const minuteCounts = Math.floor( ( playingSong.duration - pos.value ) / 60 );
|
||||||
|
|
||||||
niceDuration.value = '-' + String( minuteCounts ) + ':';
|
niceDuration.value = '-' + String( minuteCounts ) + ':';
|
||||||
|
|
||||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
if ( ( '' + minuteCounts ).length === 1 ) {
|
||||||
niceDuration.value = '-0' + minuteCounts + ':';
|
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 ) {
|
if ( ( '' + secondCounts ).length === 1 ) {
|
||||||
niceDuration.value += '0' + secondCounts;
|
niceDuration.value += '0' + secondCounts;
|
||||||
} else {
|
} else {
|
||||||
@@ -467,82 +623,101 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 100 );
|
}, 100 );
|
||||||
}
|
};
|
||||||
|
|
||||||
const prepNiceDurationTime = ( playingSong: Song ) => {
|
const prepNiceDurationTime = ( playingSong: Song ) => {
|
||||||
duration.value = playingSong.duration;
|
duration.value = playingSong.duration;
|
||||||
const minuteCounts = Math.floor( ( playingSong.duration ) / 60 );
|
const minuteCounts = Math.floor( playingSong.duration / 60 );
|
||||||
|
|
||||||
niceDuration.value = String( minuteCounts ) + ':';
|
niceDuration.value = String( minuteCounts ) + ':';
|
||||||
|
|
||||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
if ( ( '' + minuteCounts ).length === 1 ) {
|
||||||
niceDuration.value = '0' + minuteCounts + ':';
|
niceDuration.value = '0' + minuteCounts + ':';
|
||||||
}
|
}
|
||||||
const secondCounts = Math.floor( ( playingSong.duration ) - minuteCounts * 60 );
|
|
||||||
|
const secondCounts = Math.floor( playingSong.duration - minuteCounts * 60 );
|
||||||
|
|
||||||
if ( ( '' + secondCounts ).length === 1 ) {
|
if ( ( '' + secondCounts ).length === 1 ) {
|
||||||
niceDuration.value += '0' + secondCounts;
|
niceDuration.value += '0' + secondCounts;
|
||||||
} else {
|
} else {
|
||||||
niceDuration.value += secondCounts;
|
niceDuration.value += secondCounts;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const stopProgressTracker = () => {
|
const stopProgressTracker = () => {
|
||||||
try {
|
try {
|
||||||
clearInterval( progressTracker );
|
clearInterval( progressTracker );
|
||||||
} catch ( _ ) { /* empty */ }
|
} catch ( _ ) { /* empty */ }
|
||||||
|
|
||||||
isPlaying.value = false;
|
isPlaying.value = false;
|
||||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||||
}
|
};
|
||||||
|
|
||||||
const moveSong = ( move: SongMove ) => {
|
const moveSong = ( move: SongMove ) => {
|
||||||
player.moveSong( move );
|
player.moveSong( move );
|
||||||
getDetails();
|
getDetails();
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
};
|
||||||
|
|
||||||
const addNewSongs = async ( songs: ReadFile[] ) => {
|
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();
|
playlist.value = player.getQueue();
|
||||||
|
|
||||||
for ( let element in songs ) {
|
for ( let element in songs ) {
|
||||||
try {
|
try {
|
||||||
playlist.value.push( await fetchSongData( songs[ element ] ) );
|
playlist.value.push( await fetchSongData( songs[ element ] ) );
|
||||||
} catch ( e ) {
|
} catch ( e ) {
|
||||||
console.error( 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 );
|
player.setPlaylist( playlist.value );
|
||||||
|
|
||||||
if ( !isPlaying.value ) {
|
if ( !isPlaying.value ) {
|
||||||
player.prepare( 0 );
|
player.prepare( 0 );
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.value.cancelNotification( n );
|
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 );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
};
|
||||||
|
|
||||||
const addNewSongFromObject = ( song: Song ) => {
|
const addNewSongFromObject = ( song: Song ) => {
|
||||||
playlist.value = player.getQueue();
|
playlist.value = player.getQueue();
|
||||||
playlist.value.push( song );
|
playlist.value.push( song );
|
||||||
player.setPlaylist( playlist.value );
|
player.setPlaylist( playlist.value );
|
||||||
|
|
||||||
if ( !isPlaying.value ) {
|
if ( !isPlaying.value ) {
|
||||||
player.prepare( 0 );
|
player.prepare( 0 );
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeSongFromPlaylist = ( song: number ) => {
|
const removeSongFromPlaylist = ( song: number ) => {
|
||||||
playlist.value = player.getQueue();
|
playlist.value = player.getQueue();
|
||||||
playlist.value.splice( song, 1 );
|
playlist.value.splice( song, 1 );
|
||||||
player.setPlaylist( playlist.value );
|
player.setPlaylist( playlist.value );
|
||||||
|
|
||||||
if ( !isPlaying.value ) {
|
if ( !isPlaying.value ) {
|
||||||
player.prepare( 0 );
|
player.prepare( 0 );
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearPlaylist = () => {
|
const clearPlaylist = () => {
|
||||||
playlist.value = [];
|
playlist.value = [];
|
||||||
@@ -555,18 +730,20 @@
|
|||||||
coverArt.value = '';
|
coverArt.value = '';
|
||||||
pos.value = 0;
|
pos.value = 0;
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendAdditionalInfo = () => {
|
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 );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
};
|
||||||
|
|
||||||
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
document.addEventListener( 'keydown', ( e ) => {
|
document.addEventListener( 'keydown', e => {
|
||||||
if ( !userStore.isUsingKeyboard ) {
|
if ( !userStore.isUsingKeyboard ) {
|
||||||
if ( e.key === ' ' ) {
|
if ( e.key === ' ' ) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -583,7 +760,7 @@
|
|||||||
|
|
||||||
const dismissNotification = () => {
|
const dismissNotification = () => {
|
||||||
isShowingWarning.value = false;
|
isShowingWarning.value = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
const popupReturnHandler = ( data: any ) => {
|
const popupReturnHandler = ( data: any ) => {
|
||||||
if ( currentlyOpenPopup === 'create-share' ) {
|
if ( currentlyOpenPopup === 'create-share' ) {
|
||||||
@@ -594,26 +771,35 @@
|
|||||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
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
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
notificationHandler.registerListener( 'tampering-msg', ( _ ) => {
|
notificationHandler.registerListener( 'tampering-msg', _ => {
|
||||||
isShowingWarning.value = true;
|
isShowingWarning.value = true;
|
||||||
} );
|
} );
|
||||||
} ).catch( e => {
|
} )
|
||||||
if ( e === 'ERR_CONFLICT' ) {
|
.catch( e => {
|
||||||
notifications.value.createNotification( 'A share with this name exists already!', 5, 'error', 'normal' );
|
if ( e === 'ERR_CONFLICT' ) {
|
||||||
control( 'start-share' );
|
notifications.value.createNotification(
|
||||||
} else if ( e === 'ERR_UNAUTHORIZED' ) {
|
'A share with this name exists already!', 5, 'error', 'normal'
|
||||||
console.error( e );
|
);
|
||||||
localStorage.setItem( 'close-tab', 'true' );
|
control( 'start-share' );
|
||||||
logoutErrorNotification = notifications.value.createNotification( 'You appear to have been logged out. Click to log in again!', 20, 'error', 'normal', '/', true );
|
} else if ( e === 'ERR_UNAUTHORIZED' ) {
|
||||||
} else {
|
console.error( e );
|
||||||
console.error( e );
|
localStorage.setItem( 'close-tab', 'true' );
|
||||||
notifications.value.createNotification( 'Could not create share!', 5, 'error', 'normal' );
|
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 () => {
|
window.addEventListener( 'beforeunload', async () => {
|
||||||
await notificationHandler.disconnect();
|
await notificationHandler.disconnect();
|
||||||
@@ -702,7 +888,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-pb-pos {
|
.slider-pb-pos {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -926,4 +1112,4 @@
|
|||||||
.simple-button:hover {
|
.simple-button:hover {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,88 +1,153 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Queue</h1>
|
<h1>Queue</h1>
|
||||||
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
|
<input
|
||||||
<button @click="addNewSongs()" class="small-buttons" title="Load selected files"><span class="material-symbols-outlined">upload</span></button>
|
id="more-songs"
|
||||||
<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>
|
type="file"
|
||||||
<button @click="clearPlaylist()" class="small-buttons" title="Clear the playlist"><span class="material-symbols-outlined">delete</span></button>
|
multiple
|
||||||
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()"><span class="material-symbols-outlined">send</span></button>
|
accept="audio/*"
|
||||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
class="small-buttons"
|
||||||
<div class="playlist-box" id="pl-box">
|
>
|
||||||
|
<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: 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 -->
|
<!-- 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
|
||||||
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
v-for="song in computedPlaylist"
|
||||||
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
|
:key="song.id"
|
||||||
<img :src="song.cover" alt="Song cover" class="song-cover">
|
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
|
||||||
|
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 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-symbols-wrapper">
|
||||||
<div class="playing-bar" id="bar-1"></div>
|
<div id="bar-1" class="playing-bar"></div>
|
||||||
<div class="playing-bar" id="bar-2"></div>
|
<div id="bar-2" class="playing-bar"></div>
|
||||||
<div class="playing-bar" id="bar-3"></div>
|
<div id="bar-3" class="playing-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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 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 class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>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 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
|
||||||
<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>
|
v-if="canBeMoved( 'up', song.id )"
|
||||||
<h3 class="song-title">{{ song.title }}</h3>
|
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>
|
<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' )">
|
<input
|
||||||
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
|
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// TODO: Add logout button
|
// TODO: Add logout button
|
||||||
import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song';
|
import type {
|
||||||
import { computed, ref } from 'vue';
|
AppleMusicSongData, ReadFile, Song
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
computed, ref
|
||||||
|
} from 'vue';
|
||||||
import searchView from './searchView.vue';
|
import searchView from './searchView.vue';
|
||||||
import { useUserStore } from '@/stores/userStore';
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/stores/userStore';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const search = ref( searchView );
|
const search = ref( searchView );
|
||||||
const props = defineProps( {
|
const props = defineProps( {
|
||||||
'playlist': {
|
'playlist': {
|
||||||
default: [],
|
'default': [],
|
||||||
required: true,
|
'required': true,
|
||||||
type: Array<Song>
|
'type': Array<Song>
|
||||||
},
|
},
|
||||||
'currentlyPlaying': {
|
'currentlyPlaying': {
|
||||||
default: 0,
|
'default': 0,
|
||||||
required: true,
|
'required': true,
|
||||||
type: Number,
|
'type': Number,
|
||||||
},
|
},
|
||||||
'isPlaying': {
|
'isPlaying': {
|
||||||
default: true,
|
'default': true,
|
||||||
required: true,
|
'required': true,
|
||||||
type: Boolean,
|
'type': Boolean,
|
||||||
},
|
},
|
||||||
'pos': {
|
'pos': {
|
||||||
default: 0,
|
'default': 0,
|
||||||
required: false,
|
'required': false,
|
||||||
type: Number,
|
'type': Number,
|
||||||
},
|
},
|
||||||
'isLoggedIntoAppleMusic': {
|
'isLoggedIntoAppleMusic': {
|
||||||
default: false,
|
'default': false,
|
||||||
required: true,
|
'required': true,
|
||||||
type: Boolean,
|
'type': Boolean,
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
const hasSelectedSongs = ref( true );
|
const hasSelectedSongs = ref( true );
|
||||||
|
|
||||||
const computedPlaylist = computed( () => {
|
const computedPlaylist = computed( () => {
|
||||||
let pl: Song[] = [];
|
let pl: Song[] = [];
|
||||||
|
|
||||||
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
|
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
|
||||||
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
|
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
|
||||||
pl.push( props.playlist[ i ] );
|
pl.push( props.playlist[ i ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
return pl;
|
return pl;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@@ -92,60 +157,66 @@
|
|||||||
} else {
|
} else {
|
||||||
userStore.setKeyboardUsageStatus( true );
|
userStore.setKeyboardUsageStatus( true );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const openSearch = () => {
|
const openSearch = () => {
|
||||||
if ( search.value ) {
|
if ( search.value ) {
|
||||||
search.value.controlSearch( 'show' );
|
search.value.controlSearch( 'show' );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const canBeMoved = computed( () => {
|
const canBeMoved = computed( () => {
|
||||||
return ( direction: movementDirection, songID: string ): boolean => {
|
return ( direction: movementDirection, songID: string ): boolean => {
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
||||||
for ( let song in props.playlist ) {
|
for ( let song in props.playlist ) {
|
||||||
if ( props.playlist[ song ].id === songID ) {
|
if ( props.playlist[ song ].id === songID ) {
|
||||||
id = parseInt( song );
|
id = parseInt( song );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( direction === 'up' ) {
|
if ( direction === 'up' ) {
|
||||||
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} )
|
} );
|
||||||
|
|
||||||
const getTimeUntil = computed( () => {
|
const getTimeUntil = computed( () => {
|
||||||
return ( song: Song ) => {
|
return ( song: Song ) => {
|
||||||
let timeRemaining = 0;
|
let timeRemaining = 0;
|
||||||
|
|
||||||
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
||||||
if ( props.playlist[ i ] == song ) {
|
if ( props.playlist[ i ] == song ) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRemaining += props.playlist[ i ].duration;
|
timeRemaining += props.playlist[ i ].duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( props.isPlaying ) {
|
if ( props.isPlaying ) {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Currently playing';
|
return 'Currently playing';
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Plays next';
|
return 'Plays next';
|
||||||
} else {
|
} 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 ) => {
|
const deleteSong = ( songID: string ) => {
|
||||||
@@ -154,69 +225,92 @@
|
|||||||
emits( 'delete-song', parseInt( song ) );
|
emits( 'delete-song', parseInt( song ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearPlaylist = () => {
|
const clearPlaylist = () => {
|
||||||
emits( 'clear-playlist', '' );
|
emits( 'clear-playlist', '' );
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const control = ( action: string ) => {
|
const control = ( action: string ) => {
|
||||||
emits( 'control', action );
|
emits( 'control', action );
|
||||||
}
|
};
|
||||||
|
|
||||||
const play = ( song: string ) => {
|
const play = ( song: string ) => {
|
||||||
emits( 'play-song', song );
|
emits( 'play-song', song );
|
||||||
}
|
};
|
||||||
|
|
||||||
const addNewSongs = () => {
|
const addNewSongs = () => {
|
||||||
const fileURLList: ReadFile[] = [];
|
const fileURLList: ReadFile[] = [];
|
||||||
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
|
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
|
||||||
|
|
||||||
if ( allFiles.length > 0 ) {
|
if ( allFiles.length > 0 ) {
|
||||||
hasSelectedSongs.value = true;
|
hasSelectedSongs.value = true;
|
||||||
|
|
||||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
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 );
|
emits( 'add-new-songs', fileURLList );
|
||||||
} else {
|
} else {
|
||||||
hasSelectedSongs.value = false;
|
hasSelectedSongs.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
||||||
const song: Song = {
|
const song: Song = {
|
||||||
artist: songData.attributes.artistName,
|
'artist': songData.attributes.artistName,
|
||||||
cover: songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
'cover': songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||||
duration: songData.attributes.durationInMillis / 1000,
|
'duration': songData.attributes.durationInMillis / 1000,
|
||||||
id: songData.id,
|
'id': songData.id,
|
||||||
origin: 'apple-music',
|
'origin': 'apple-music',
|
||||||
title: songData.attributes.name
|
'title': songData.attributes.name
|
||||||
}
|
};
|
||||||
|
|
||||||
emits( 'add-new-songs-apple-music', song );
|
emits( 'add-new-songs-apple-music', song );
|
||||||
}
|
};
|
||||||
|
|
||||||
type movementDirection = 'up' | 'down';
|
type movementDirection = 'up' | 'down';
|
||||||
|
|
||||||
const moveSong = ( songID: string, direction: movementDirection ) => {
|
const moveSong = ( songID: string, direction: movementDirection ) => {
|
||||||
let newSongPos = 0;
|
let newSongPos = 0;
|
||||||
let hasFoundSongToMove = false;
|
let hasFoundSongToMove = false;
|
||||||
|
|
||||||
for ( let el in props.playlist ) {
|
for ( let el in props.playlist ) {
|
||||||
if ( props.playlist[ el ].id === songID ) {
|
if ( props.playlist[ el ].id === songID ) {
|
||||||
const currPos = parseInt( el );
|
const currPos = parseInt( el );
|
||||||
|
|
||||||
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
||||||
hasFoundSongToMove = true;
|
hasFoundSongToMove = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( hasFoundSongToMove ) {
|
if ( hasFoundSongToMove ) {
|
||||||
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } );
|
emits( 'playlist-reorder', {
|
||||||
|
'songID': songID,
|
||||||
|
'newPos': newSongPos
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendAdditionalInfo = () => {
|
const sendAdditionalInfo = () => {
|
||||||
emits( 'send-additional-info' );
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -249,6 +343,10 @@
|
|||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.song img.song-cover {
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.song-title {
|
.song-title {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
@@ -372,4 +470,4 @@
|
|||||||
.small-buttons:hover .material-symbols-outlined {
|
.small-buttons:hover .material-symbols-outlined {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="playlists">
|
<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">
|
<div v-if="( $props.playlists ? $props.playlists.length < 1 : true ) && $props.isLoggedIn">
|
||||||
Loading...
|
Loading...
|
||||||
<!-- TODO: Make prettier -->
|
<!-- TODO: Make prettier -->
|
||||||
@@ -8,12 +10,27 @@
|
|||||||
<div v-else-if="!$props.isLoggedIn" class="not-logged-in">
|
<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>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>
|
<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>
|
<input
|
||||||
<button @click="loadPlaylistFromDisk()" class="pl-loader-button" id="load-button">Load</button>
|
id="pl-loader"
|
||||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p>
|
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>
|
||||||
<div class="playlist-wrapper">
|
<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 }}
|
{{ pl.attributes.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,8 +38,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReadFile } from '@/scripts/song';
|
import type {
|
||||||
import { ref } from 'vue';
|
ReadFile
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
const hasSelectedSongs = ref( true );
|
const hasSelectedSongs = ref( true );
|
||||||
|
|
||||||
defineProps( {
|
defineProps( {
|
||||||
@@ -41,22 +63,31 @@
|
|||||||
const loadPlaylistFromDisk = () => {
|
const loadPlaylistFromDisk = () => {
|
||||||
const fileURLList: ReadFile[] = [];
|
const fileURLList: ReadFile[] = [];
|
||||||
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
|
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
|
||||||
|
|
||||||
if ( allFiles.length > 0 ) {
|
if ( allFiles.length > 0 ) {
|
||||||
hasSelectedSongs.value = true;
|
hasSelectedSongs.value = true;
|
||||||
|
|
||||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
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 );
|
emits( 'custom-playlist', fileURLList );
|
||||||
} else {
|
} else {
|
||||||
hasSelectedSongs.value = false;
|
hasSelectedSongs.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] );
|
const emits = defineEmits( [
|
||||||
|
'selected-playlist',
|
||||||
|
'custom-playlist'
|
||||||
|
] );
|
||||||
|
|
||||||
const selectPlaylist = ( id: string ) => {
|
const selectPlaylist = ( id: string ) => {
|
||||||
emits( 'selected-playlist', id );
|
emits( 'selected-playlist', id );
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -103,4 +134,4 @@
|
|||||||
.not-logged-in {
|
.not-logged-in {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
import { createApp } from 'vue'
|
import App from './App.vue';
|
||||||
import { createPinia } from 'pinia'
|
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( createPinia() );
|
||||||
app.use(router)
|
app.use( router );
|
||||||
|
|
||||||
// localStorage.setItem( 'url', 'http://localhost:8082' );
|
// localStorage.setItem( 'url', 'http://localhost:8082' );
|
||||||
localStorage.setItem( 'url', 'https://music-api.janishutz.com' );
|
localStorage.setItem( 'url', 'https://music-api.janishutz.com' );
|
||||||
|
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'
|
'title': 'Fancy View'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tools/bar',
|
||||||
|
name: 'tool-bar',
|
||||||
|
component: () => import( '../views/BarView.vue' ),
|
||||||
|
meta: {
|
||||||
|
'authRequired': false,
|
||||||
|
'title': 'Bar utility'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'NotFound',
|
name: 'NotFound',
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import ColorThief from 'colorthief';
|
import ColorThief from 'colorthief';
|
||||||
|
|
||||||
const colorThief = new ColorThief();
|
const colorThief = new ColorThief();
|
||||||
|
|
||||||
const getImageData = (): Promise<number[][]> => {
|
const getImageData = (): Promise<number[][]> => {
|
||||||
return new Promise( ( resolve ) => {
|
return new Promise( resolve => {
|
||||||
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement );
|
const img = document.getElementById( 'current-image' ) as HTMLImageElement;
|
||||||
|
|
||||||
if ( img.complete ) {
|
if ( img.complete ) {
|
||||||
resolve( colorThief.getPalette( img ) );
|
resolve( colorThief.getPalette( img ) );
|
||||||
} else {
|
} else {
|
||||||
@@ -12,32 +14,39 @@ const getImageData = (): Promise<number[][]> => {
|
|||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
}
|
};
|
||||||
|
|
||||||
const createBackground = () => {
|
const createBackground = () => {
|
||||||
return new Promise( ( resolve ) => {
|
return new Promise( resolve => {
|
||||||
getImageData().then( palette => {
|
getImageData().then( palette => {
|
||||||
const colourDetails: number[][] = [];
|
const colourDetails: number[][] = [];
|
||||||
const colours: string[] = [];
|
const colours: string[] = [];
|
||||||
|
|
||||||
let differentEnough = true;
|
let differentEnough = true;
|
||||||
|
|
||||||
if ( palette[ 0 ] ) {
|
if ( palette[ 0 ] ) {
|
||||||
for ( const i in palette ) {
|
for ( const i in palette ) {
|
||||||
for ( const colour in colourDetails ) {
|
for ( const colour in colourDetails ) {
|
||||||
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255
|
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255
|
||||||
+ Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255
|
+ Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255
|
||||||
+ Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
+ Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
||||||
|
|
||||||
if ( colourDiff > 15 ) {
|
if ( colourDiff > 15 ) {
|
||||||
differentEnough = true;
|
differentEnough = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( differentEnough ) {
|
if ( differentEnough ) {
|
||||||
colourDetails.push( palette[ i ] );
|
colourDetails.push( palette[ i ] );
|
||||||
colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' );
|
colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' );
|
||||||
}
|
}
|
||||||
|
|
||||||
differentEnough = false;
|
differentEnough = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let outColours = 'conic-gradient(';
|
let outColours = 'conic-gradient(';
|
||||||
|
|
||||||
if ( colours.length < 3 ) {
|
if ( colours.length < 3 ) {
|
||||||
for ( let i = 0; i < 3; i++ ) {
|
for ( let i = 0; i < 3; i++ ) {
|
||||||
if ( colours[ i ] ) {
|
if ( colours[ i ] ) {
|
||||||
@@ -61,45 +70,56 @@ const createBackground = () => {
|
|||||||
outColours += colours[ i ] + ',';
|
outColours += colours[ i ] + ',';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||||
resolve( outColours );
|
resolve( outColours );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let callbackFun = () => {};
|
||||||
|
|
||||||
let callbackFun = () => {}
|
|
||||||
const subscribeToBeatUpdate = ( cb: () => void ) => {
|
const subscribeToBeatUpdate = ( cb: () => void ) => {
|
||||||
callbackFun = cb;
|
callbackFun = cb;
|
||||||
micAudioHandler();
|
micAudioHandler();
|
||||||
}
|
};
|
||||||
|
|
||||||
const unsubscribeFromBeatUpdate = () => {
|
const unsubscribeFromBeatUpdate = () => {
|
||||||
callbackFun = () => {}
|
callbackFun = () => {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clearInterval( micAnalyzer );
|
clearInterval( micAnalyzer );
|
||||||
} catch ( e ) { /* empty */ }
|
} catch ( e ) { /* empty */ }
|
||||||
}
|
};
|
||||||
|
|
||||||
const coolDown = () => {
|
const coolDown = () => {
|
||||||
beatDetected = false;
|
beatDetected = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
let micAnalyzer = 0;
|
let micAnalyzer = 0;
|
||||||
let beatDetected = false;
|
let beatDetected = false;
|
||||||
|
|
||||||
const micAudioHandler = () => {
|
const micAudioHandler = () => {
|
||||||
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
||||||
const analyser = audioContext.createAnalyser();
|
const analyser = audioContext.createAnalyser();
|
||||||
|
|
||||||
analyser.fftSize = 256;
|
analyser.fftSize = 256;
|
||||||
const bufferLength = analyser.frequencyBinCount;
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
const dataArray = new Uint8Array( bufferLength );
|
const dataArray = new Uint8Array( bufferLength );
|
||||||
|
|
||||||
beatDetected = false;
|
beatDetected = false;
|
||||||
|
|
||||||
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
navigator.mediaDevices.getUserMedia( {
|
||||||
|
'audio': true
|
||||||
|
} ).then( stream => {
|
||||||
const mic = audioContext.createMediaStreamSource( stream );
|
const mic = audioContext.createMediaStreamSource( stream );
|
||||||
|
|
||||||
mic.connect( analyser );
|
mic.connect( analyser );
|
||||||
analyser.getByteFrequencyData( dataArray );
|
analyser.getByteFrequencyData( dataArray );
|
||||||
let prevSpectrum: number[] = [];
|
let prevSpectrum: number[] = [];
|
||||||
|
|
||||||
const threshold = 10; // Adjust as needed
|
const threshold = 10; // Adjust as needed
|
||||||
|
|
||||||
micAnalyzer = setInterval( () => {
|
micAnalyzer = setInterval( () => {
|
||||||
analyser.getByteFrequencyData( dataArray );
|
analyser.getByteFrequencyData( dataArray );
|
||||||
// Convert the frequency data to a numeric array
|
// Convert the frequency data to a numeric array
|
||||||
@@ -115,25 +135,27 @@ const micAudioHandler = () => {
|
|||||||
callbackFun();
|
callbackFun();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prevSpectrum = currentSpectrum;
|
prevSpectrum = currentSpectrum;
|
||||||
}, 60 / 180 * 250 );
|
}, 60 / 180 * 250 );
|
||||||
} );
|
} );
|
||||||
}
|
};
|
||||||
|
|
||||||
const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => {
|
const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => {
|
||||||
let flux = 0;
|
let flux = 0;
|
||||||
|
|
||||||
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
||||||
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
||||||
|
|
||||||
flux += Math.max( 0, diff );
|
flux += Math.max( 0, diff );
|
||||||
}
|
}
|
||||||
|
|
||||||
return flux;
|
return flux;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createBackground,
|
createBackground,
|
||||||
subscribeToBeatUpdate,
|
subscribeToBeatUpdate,
|
||||||
unsubscribeFromBeatUpdate,
|
unsubscribeFromBeatUpdate,
|
||||||
coolDown,
|
coolDown,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
/*
|
|
||||||
* MusicPlayerV2 - notificationHandler.ts
|
|
||||||
*
|
|
||||||
* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License
|
|
||||||
* https://janishutz.com, development@janishutz.com
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
// These functions handle connections to the backend with socket.io
|
// These functions handle connections to the backend with socket.io
|
||||||
|
|
||||||
import { io, type Socket } from "socket.io-client";
|
import {
|
||||||
import type { SSEMap } from "./song";
|
io, type Socket
|
||||||
|
} from 'socket.io-client';
|
||||||
|
import type {
|
||||||
|
SSEMap
|
||||||
|
} from './song';
|
||||||
|
|
||||||
class SocketConnection {
|
class SocketConnection {
|
||||||
|
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
|
|
||||||
roomName: string;
|
roomName: string;
|
||||||
|
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
|
||||||
useSocket: boolean;
|
useSocket: boolean;
|
||||||
|
|
||||||
eventSource?: EventSource;
|
eventSource?: EventSource;
|
||||||
|
|
||||||
toBeListenedForItems: SSEMap;
|
toBeListenedForItems: SSEMap;
|
||||||
|
|
||||||
reconnectRetryCount: number;
|
reconnectRetryCount: number;
|
||||||
|
|
||||||
openConnectionsCount: number;
|
openConnectionsCount: number;
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||||
autoConnect: false,
|
'autoConnect': false,
|
||||||
} );
|
} );
|
||||||
this.roomName = location.pathname.split( '/' )[ 2 ];
|
this.roomName = location.pathname.split( '/' )[ 2 ];
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
@@ -35,55 +38,71 @@ class SocketConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a room token and connect to
|
* Create a room token and connect to
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
connect (): Promise<any> {
|
connect (): Promise<unknown> {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
if ( this.reconnectRetryCount < 5 ) {
|
if ( this.reconnectRetryCount < 5 ) {
|
||||||
if ( this.useSocket ) {
|
if ( this.useSocket ) {
|
||||||
this.socket.connect();
|
this.socket.connect();
|
||||||
this.socket.emit( 'join-room', this.roomName, ( res: { status: boolean, msg: string, data: any } ) => {
|
this.socket.emit(
|
||||||
if ( res.status === true ) {
|
'join-room', this.roomName, ( res: {
|
||||||
this.isConnected = true;
|
'status': boolean,
|
||||||
resolve( res.data );
|
'msg': string,
|
||||||
} else {
|
'data': unknown
|
||||||
console.debug( res.msg );
|
} ) => {
|
||||||
reject( 'ERR_ROOM_CONNECTING' );
|
if ( res.status === true ) {
|
||||||
|
this.isConnected = true;
|
||||||
|
resolve( res.data );
|
||||||
|
} else {
|
||||||
|
console.debug( res.msg );
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} );
|
);
|
||||||
} else {
|
} else {
|
||||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||||
this.openConnectionsCount += 1;
|
this.openConnectionsCount += 1;
|
||||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
this.eventSource
|
||||||
|
= new EventSource( localStorage.getItem( 'url' )
|
||||||
|
+ '/socket/connection?room=' + this.roomName, {
|
||||||
|
'withCredentials': true
|
||||||
|
} );
|
||||||
|
|
||||||
this.eventSource.onopen = () => {
|
this.eventSource.onopen = () => {
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
this.reconnectRetryCount = 0;
|
this.reconnectRetryCount = 0;
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
console.log( '[ SSE Connection ] - '
|
||||||
}
|
+ new Date().toISOString() + ': Connection successfully established!' );
|
||||||
|
};
|
||||||
|
|
||||||
this.eventSource.onmessage = ( e ) => {
|
this.eventSource.onmessage = e => {
|
||||||
const d = JSON.parse( e.data );
|
const d = JSON.parse( e.data );
|
||||||
|
|
||||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||||
this.toBeListenedForItems[ d.type ]( d.data );
|
this.toBeListenedForItems[ d.type ]( d.data );
|
||||||
} else if ( d.type === 'basics' ) {
|
} else if ( d.type === 'basics' ) {
|
||||||
resolve( d.data );
|
resolve( d.data );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
this.eventSource.onerror = () => {
|
this.eventSource.onerror = () => {
|
||||||
if ( this.isConnected ) {
|
if ( this.isConnected ) {
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
this.openConnectionsCount -= 1;
|
this.openConnectionsCount -= 1;
|
||||||
this.eventSource?.close();
|
this.eventSource?.close();
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString()
|
||||||
|
+ ': Reconnecting due to connection error!' );
|
||||||
// console.debug( e );
|
// console.debug( e );
|
||||||
|
|
||||||
this.eventSource = undefined;
|
this.eventSource = undefined;
|
||||||
|
|
||||||
this.reconnectRetryCount += 1;
|
this.reconnectRetryCount += 1;
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
this.connect();
|
this.connect();
|
||||||
@@ -91,13 +110,18 @@ class SocketConnection {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error ' + res.status );
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString()
|
||||||
|
+ ': Could not connect due to error ' + res.status );
|
||||||
reject( 'ERR_ROOM_CONNECTING' );
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
}
|
}
|
||||||
} ).catch( () => {
|
} )
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error.' );
|
.catch( () => {
|
||||||
reject( 'ERR_ROOM_CONNECTING' );
|
console.log( '[ SSE Connection ] - '
|
||||||
} );
|
+ new Date().toISOString()
|
||||||
|
+ ': Could not connect due to error.' );
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
} );
|
||||||
} else {
|
} else {
|
||||||
console.log( '[ SSE Connection ]: Trimmed connections' );
|
console.log( '[ SSE Connection ]: Trimmed connections' );
|
||||||
reject( 'ERR_TOO_MANY_CONNECTIONS' );
|
reject( 'ERR_TOO_MANY_CONNECTIONS' );
|
||||||
@@ -116,16 +140,23 @@ class SocketConnection {
|
|||||||
* @param {any} data
|
* @param {any} data
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
emit ( event: string, data: any ): void {
|
emit ( event: string, data: unknown ): void {
|
||||||
if ( this.isConnected ) {
|
if ( this.isConnected ) {
|
||||||
if ( this.useSocket ) {
|
if ( this.useSocket ) {
|
||||||
this.socket.emit( event, { 'roomName': this.roomName, 'data': data } );
|
this.socket.emit( event, {
|
||||||
|
'roomName': this.roomName,
|
||||||
|
'data': data
|
||||||
|
} );
|
||||||
} else {
|
} else {
|
||||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||||
method: 'post',
|
'method': 'post',
|
||||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'data': data } ),
|
'body': JSON.stringify( {
|
||||||
credentials: 'include',
|
'event': event,
|
||||||
headers: {
|
'roomName': this.roomName,
|
||||||
|
'data': data
|
||||||
|
} ),
|
||||||
|
'credentials': 'include',
|
||||||
|
'headers': {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'charset': 'utf-8'
|
'charset': 'utf-8'
|
||||||
}
|
}
|
||||||
@@ -140,11 +171,11 @@ class SocketConnection {
|
|||||||
* @param {( data: any ) => void} cb The callback function / listener function
|
* @param {( data: any ) => void} cb The callback function / listener function
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
registerListener ( event: string, cb: ( data: any ) => void ): void {
|
registerListener ( event: string, cb: ( data: unknown ) => void ): void {
|
||||||
if ( this.useSocket ) {
|
if ( this.useSocket ) {
|
||||||
if ( this.isConnected ) {
|
if ( this.isConnected ) {
|
||||||
this.socket.on( event, cb );
|
this.socket.on( event, cb );
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.toBeListenedForItems[ event ] = cb;
|
this.toBeListenedForItems[ event ] = cb;
|
||||||
}
|
}
|
||||||
@@ -171,9 +202,11 @@ class SocketConnection {
|
|||||||
if ( this.eventSource ) {
|
if ( this.eventSource ) {
|
||||||
return this.eventSource!.OPEN && this.isConnected;
|
return this.eventSource!.OPEN && this.isConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SocketConnection;
|
export default SocketConnection;
|
||||||
|
|||||||
@@ -1,25 +1,39 @@
|
|||||||
import type { SearchResult, Song, SongMove } from "./song";
|
import type {
|
||||||
|
SearchResult, Song, SongMove
|
||||||
|
} from './song';
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
devToken: string;
|
'devToken': string;
|
||||||
userToken: string;
|
'userToken': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10';
|
type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10';
|
||||||
type RepeatMode = 'off' | 'once' | 'all';
|
type RepeatMode = 'off' | 'once' | 'all';
|
||||||
|
|
||||||
class MusicKitJSWrapper {
|
class MusicKitJSWrapper {
|
||||||
|
|
||||||
playingSongID: number;
|
playingSongID: number;
|
||||||
|
|
||||||
playlist: Song[];
|
playlist: Song[];
|
||||||
|
|
||||||
queue: number[];
|
queue: number[];
|
||||||
|
|
||||||
config: Config;
|
config: Config;
|
||||||
|
|
||||||
musicKit: any;
|
musicKit: any;
|
||||||
|
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
|
||||||
isPreparedToPlay: boolean;
|
isPreparedToPlay: boolean;
|
||||||
|
|
||||||
repeatMode: RepeatMode;
|
repeatMode: RepeatMode;
|
||||||
|
|
||||||
isShuffleEnabled: boolean;
|
isShuffleEnabled: boolean;
|
||||||
|
|
||||||
hasEncounteredAuthError: boolean;
|
hasEncounteredAuthError: boolean;
|
||||||
|
|
||||||
queuePos: number;
|
queuePos: number;
|
||||||
|
|
||||||
audioPlayer: HTMLAudioElement;
|
audioPlayer: HTMLAudioElement;
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -27,8 +41,8 @@ class MusicKitJSWrapper {
|
|||||||
this.playlist = [];
|
this.playlist = [];
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.config = {
|
this.config = {
|
||||||
devToken: '',
|
'devToken': '',
|
||||||
userToken: '',
|
'userToken': '',
|
||||||
};
|
};
|
||||||
this.isShuffleEnabled = false;
|
this.isShuffleEnabled = false;
|
||||||
this.repeatMode = 'off';
|
this.repeatMode = 'off';
|
||||||
@@ -58,16 +72,18 @@ class MusicKitJSWrapper {
|
|||||||
this.musicKit.authorize().then( () => {
|
this.musicKit.authorize().then( () => {
|
||||||
this.isLoggedIn = true;
|
this.isLoggedIn = true;
|
||||||
this.init();
|
this.init();
|
||||||
} ).catch( () => {
|
} )
|
||||||
this.hasEncounteredAuthError = true;
|
.catch( () => {
|
||||||
} );
|
this.hasEncounteredAuthError = true;
|
||||||
|
} );
|
||||||
} else {
|
} else {
|
||||||
this.musicKit.authorize().then( () => {
|
this.musicKit.authorize().then( () => {
|
||||||
this.isLoggedIn = true;
|
this.isLoggedIn = true;
|
||||||
this.init();
|
this.init();
|
||||||
} ).catch( () => {
|
} )
|
||||||
this.hasEncounteredAuthError = true;
|
.catch( () => {
|
||||||
} );
|
this.hasEncounteredAuthError = true;
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,25 +92,29 @@ class MusicKitJSWrapper {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
init (): void {
|
init (): void {
|
||||||
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => {
|
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
res.text().then( token => {
|
res.text().then( token => {
|
||||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||||
// MusicKit global is now defined
|
// MusicKit global is now defined
|
||||||
MusicKit.configure( {
|
MusicKit.configure( {
|
||||||
developerToken: token,
|
'developerToken': token,
|
||||||
app: {
|
'app': {
|
||||||
name: 'MusicPlayer',
|
'name': 'MusicPlayer',
|
||||||
build: '3'
|
'build': '3'
|
||||||
},
|
},
|
||||||
storefrontId: 'CH',
|
'storefrontId': 'CH',
|
||||||
} ).then( () => {
|
} ).then( () => {
|
||||||
this.config.devToken = token;
|
this.config.devToken = token;
|
||||||
this.musicKit = MusicKit.getInstance();
|
this.musicKit = MusicKit.getInstance();
|
||||||
|
|
||||||
if ( this.musicKit.isAuthorized ) {
|
if ( this.musicKit.isAuthorized ) {
|
||||||
this.isLoggedIn = true;
|
this.isLoggedIn = true;
|
||||||
this.config.userToken = this.musicKit.musicUserToken;
|
this.config.userToken = this.musicKit.musicUserToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off;
|
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off;
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
@@ -107,7 +127,10 @@ class MusicKitJSWrapper {
|
|||||||
* @returns {boolean[]} Returns an array, where the first element indicates login status, the second one, if an error was encountered
|
* @returns {boolean[]} Returns an array, where the first element indicates login status, the second one, if an error was encountered
|
||||||
*/
|
*/
|
||||||
getAuth (): boolean[] {
|
getAuth (): boolean[] {
|
||||||
return [ this.isLoggedIn, this.hasEncounteredAuthError ];
|
return [
|
||||||
|
this.isLoggedIn,
|
||||||
|
this.hasEncounteredAuthError
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,8 +142,8 @@ class MusicKitJSWrapper {
|
|||||||
apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
|
apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
|
||||||
if ( this.config.devToken != '' && this.config.userToken != '' ) {
|
if ( this.config.devToken != '' && this.config.userToken != '' ) {
|
||||||
fetch( url, {
|
fetch( url, {
|
||||||
method: 'GET',
|
'method': 'GET',
|
||||||
headers: {
|
'headers': {
|
||||||
'Authorization': `Bearer ${ this.config.devToken }`,
|
'Authorization': `Bearer ${ this.config.devToken }`,
|
||||||
'Music-User-Token': this.config.userToken
|
'Music-User-Token': this.config.userToken
|
||||||
}
|
}
|
||||||
@@ -128,13 +151,19 @@ class MusicKitJSWrapper {
|
|||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
res.json().then( json => {
|
res.json().then( json => {
|
||||||
try {
|
try {
|
||||||
callback( { 'status': 'ok', 'data': json } );
|
callback( {
|
||||||
} catch( err ) { /* empty */}
|
'status': 'ok',
|
||||||
|
'data': json
|
||||||
|
} );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
} );
|
} );
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
callback( { 'status': 'error', 'error': res.status } );
|
callback( {
|
||||||
} catch( err ) { /* empty */}
|
'status': 'error',
|
||||||
|
'error': res.status
|
||||||
|
} );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
} else return;
|
} else return;
|
||||||
@@ -152,34 +181,41 @@ class MusicKitJSWrapper {
|
|||||||
|
|
||||||
setPlaylistByID ( id: string ): Promise<void> {
|
setPlaylistByID ( id: string ): Promise<void> {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
this.musicKit.setQueue( { playlist: id } ).then( () => {
|
this.musicKit.setQueue( {
|
||||||
|
'playlist': id
|
||||||
|
} ).then( () => {
|
||||||
const pl = this.musicKit.queue.items;
|
const pl = this.musicKit.queue.items;
|
||||||
const songs: Song[] = [];
|
const songs: Song[] = [];
|
||||||
|
|
||||||
for ( const item in pl ) {
|
for ( const item in pl ) {
|
||||||
let url = pl[ item ].attributes.artwork.url;
|
let url = pl[ item ].attributes.artwork.url;
|
||||||
|
|
||||||
url = url.replace( '{w}', pl[ item ].attributes.artwork.width );
|
url = url.replace( '{w}', pl[ item ].attributes.artwork.width );
|
||||||
url = url.replace( '{h}', pl[ item ].attributes.artwork.height );
|
url = url.replace( '{h}', pl[ item ].attributes.artwork.height );
|
||||||
const song: Song = {
|
const song: Song = {
|
||||||
artist: pl[ item ].attributes.artistName,
|
'artist': pl[ item ].attributes.artistName,
|
||||||
cover: url,
|
'cover': url,
|
||||||
duration: pl[ item ].attributes.durationInMillis / 1000,
|
'duration': pl[ item ].attributes.durationInMillis / 1000,
|
||||||
id: pl[ item ].id,
|
'id': pl[ item ].id,
|
||||||
origin: 'apple-music',
|
'origin': 'apple-music',
|
||||||
title: pl[ item ].attributes.name,
|
'title': pl[ item ].attributes.name,
|
||||||
genres: pl[ item ].attributes.genreNames
|
'genres': pl[ item ].attributes.genreNames
|
||||||
}
|
};
|
||||||
|
|
||||||
songs.push( song );
|
songs.push( song );
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playlist = songs;
|
this.playlist = songs;
|
||||||
this.setShuffle( this.isShuffleEnabled );
|
this.setShuffle( this.isShuffleEnabled );
|
||||||
this.queuePos = 0;
|
this.queuePos = 0;
|
||||||
this.playingSongID = this.queue[ 0 ];
|
this.playingSongID = this.queue[ 0 ];
|
||||||
this.prepare( this.playingSongID );
|
this.prepare( this.playingSongID );
|
||||||
resolve();
|
resolve();
|
||||||
} ).catch( err => {
|
} )
|
||||||
console.error( err );
|
.catch( err => {
|
||||||
reject( err );
|
console.error( err );
|
||||||
} );
|
reject( err );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,20 +228,25 @@ class MusicKitJSWrapper {
|
|||||||
if ( this.playlist.length > 0 ) {
|
if ( this.playlist.length > 0 ) {
|
||||||
this.playingSongID = playlistID;
|
this.playingSongID = playlistID;
|
||||||
this.isPreparedToPlay = true;
|
this.isPreparedToPlay = true;
|
||||||
|
|
||||||
for ( const el in this.queue ) {
|
for ( const el in this.queue ) {
|
||||||
if ( this.queue[ el ] === playlistID ) {
|
if ( this.queue[ el ] === playlistID ) {
|
||||||
this.queuePos = parseInt( el );
|
this.queuePos = parseInt( el );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
this.musicKit.setQueue( { 'song': this.playlist[ this.playingSongID ].id } ).then( () => {
|
this.musicKit.setQueue( {
|
||||||
|
'song': this.playlist[ this.playingSongID ].id
|
||||||
|
} ).then( () => {
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
this.control( 'play' );
|
this.control( 'play' );
|
||||||
}, 500 );
|
}, 500 );
|
||||||
} ).catch( ( err ) => {
|
} )
|
||||||
console.log( err );
|
.catch( err => {
|
||||||
} );
|
console.log( err );
|
||||||
|
} );
|
||||||
} else {
|
} else {
|
||||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||||
this.audioPlayer.src = this.playlist[ this.playingSongID ].id;
|
this.audioPlayer.src = this.playlist[ this.playingSongID ].id;
|
||||||
@@ -213,6 +254,7 @@ class MusicKitJSWrapper {
|
|||||||
this.control( 'play' );
|
this.control( 'play' );
|
||||||
}, 500 );
|
}, 500 );
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@@ -226,50 +268,63 @@ class MusicKitJSWrapper {
|
|||||||
*/
|
*/
|
||||||
control ( action: ControlAction ): boolean {
|
control ( action: ControlAction ): boolean {
|
||||||
switch ( action ) {
|
switch ( action ) {
|
||||||
case "play":
|
case 'play':
|
||||||
if ( this.isPreparedToPlay ) {
|
if ( this.isPreparedToPlay ) {
|
||||||
this.control( 'pause' );
|
this.control( 'pause' );
|
||||||
|
|
||||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
this.musicKit.play();
|
this.musicKit.play();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
this.audioPlayer.play();
|
this.audioPlayer.play();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case "pause":
|
|
||||||
|
case 'pause':
|
||||||
if ( this.isPreparedToPlay ) {
|
if ( this.isPreparedToPlay ) {
|
||||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
this.musicKit.pause();
|
this.musicKit.pause();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
this.audioPlayer.pause();
|
this.audioPlayer.pause();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case "back-10":
|
|
||||||
|
case 'back-10':
|
||||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
|
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case "skip-10":
|
|
||||||
|
case 'skip-10':
|
||||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
|
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
if ( this.repeatMode !== 'once' ) {
|
if ( this.repeatMode !== 'once' ) {
|
||||||
this.control( 'next' );
|
this.control( 'next' );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.musicKit.seekToTime( 0 );
|
this.musicKit.seekToTime( 0 );
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,32 +338,42 @@ class MusicKitJSWrapper {
|
|||||||
this.audioPlayer.currentTime = 0;
|
this.audioPlayer.currentTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case "next":
|
|
||||||
|
case 'next':
|
||||||
this.control( 'pause' );
|
this.control( 'pause' );
|
||||||
|
|
||||||
if ( this.queuePos < this.queue.length - 1 ) {
|
if ( this.queuePos < this.queue.length - 1 ) {
|
||||||
this.queuePos += 1;
|
this.queuePos += 1;
|
||||||
this.prepare( this.queue[ this.queuePos ] );
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.queuePos = 0;
|
this.queuePos = 0;
|
||||||
|
|
||||||
if ( this.repeatMode !== 'all' ) {
|
if ( this.repeatMode !== 'all' ) {
|
||||||
this.control( 'pause' );
|
this.control( 'pause' );
|
||||||
} else {
|
} else {
|
||||||
this.playingSongID = this.queue[ this.queuePos ];
|
this.playingSongID = this.queue[ this.queuePos ];
|
||||||
this.prepare( this.queue[ this.queuePos ] );
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case "previous":
|
|
||||||
|
case 'previous':
|
||||||
this.control( 'pause' );
|
this.control( 'pause' );
|
||||||
|
|
||||||
if ( this.queuePos > 0 ) {
|
if ( this.queuePos > 0 ) {
|
||||||
this.queuePos -= 1;
|
this.queuePos -= 1;
|
||||||
this.prepare( this.queue[ this.queuePos ] );
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.queuePos = this.queue.length - 1;
|
this.queuePos = this.queue.length - 1;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,15 +382,22 @@ class MusicKitJSWrapper {
|
|||||||
setShuffle ( enabled: boolean ) {
|
setShuffle ( enabled: boolean ) {
|
||||||
this.isShuffleEnabled = enabled;
|
this.isShuffleEnabled = enabled;
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
|
|
||||||
if ( enabled ) {
|
if ( enabled ) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const d = [];
|
const d = [];
|
||||||
|
|
||||||
for ( const el in this.playlist ) {
|
for ( const el in this.playlist ) {
|
||||||
d.push( parseInt( el ) );
|
d.push( parseInt( el ) );
|
||||||
}
|
}
|
||||||
this.queue = d.map( value => ( { value, sort: Math.random() } ) )
|
|
||||||
.sort( ( a, b ) => a.sort - b.sort )
|
this.queue = d.map( value => ( {
|
||||||
.map( ( { value } ) => 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.splice( this.queue.indexOf( this.playingSongID ), 1 );
|
||||||
this.queue.push( this.playingSongID );
|
this.queue.push( this.playingSongID );
|
||||||
this.queue.reverse();
|
this.queue.reverse();
|
||||||
@@ -334,6 +406,7 @@ class MusicKitJSWrapper {
|
|||||||
this.queue.push( parseInt( song ) );
|
this.queue.push( parseInt( song ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find current song ID in queue
|
// Find current song ID in queue
|
||||||
for ( const el in this.queue ) {
|
for ( const el in this.queue ) {
|
||||||
if ( this.queue[ el ] === this.playingSongID ) {
|
if ( this.queue[ el ] === this.playingSongID ) {
|
||||||
@@ -359,29 +432,37 @@ class MusicKitJSWrapper {
|
|||||||
moveSong ( move: SongMove ) {
|
moveSong ( move: SongMove ) {
|
||||||
const newQueue = [];
|
const newQueue = [];
|
||||||
const finishedQueue = [];
|
const finishedQueue = [];
|
||||||
|
|
||||||
let songID = 0;
|
let songID = 0;
|
||||||
|
|
||||||
for ( const song in this.playlist ) {
|
for ( const song in this.playlist ) {
|
||||||
if ( this.playlist[ song ].id === move.songID ) {
|
if ( this.playlist[ song ].id === move.songID ) {
|
||||||
songID = parseInt( song );
|
songID = parseInt( song );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for ( const el in this.queue ) {
|
for ( const el in this.queue ) {
|
||||||
if ( this.queue[ el ] !== songID ) {
|
if ( this.queue[ el ] !== songID ) {
|
||||||
newQueue.push( this.queue[ el ] );
|
newQueue.push( this.queue[ el ] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasBeenAdded = false;
|
let hasBeenAdded = false;
|
||||||
|
|
||||||
for ( const el in newQueue ) {
|
for ( const el in newQueue ) {
|
||||||
if ( parseInt( el ) === move.newPos ) {
|
if ( parseInt( el ) === move.newPos ) {
|
||||||
finishedQueue.push( songID );
|
finishedQueue.push( songID );
|
||||||
hasBeenAdded = true;
|
hasBeenAdded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
finishedQueue.push( newQueue[ el ] );
|
finishedQueue.push( newQueue[ el ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !hasBeenAdded ) {
|
if ( !hasBeenAdded ) {
|
||||||
finishedQueue.push( songID );
|
finishedQueue.push( songID );
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queue = finishedQueue;
|
this.queue = finishedQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,9 +516,11 @@ class MusicKitJSWrapper {
|
|||||||
*/
|
*/
|
||||||
getQueue (): Song[] {
|
getQueue (): Song[] {
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
||||||
for ( const el in this.queue ) {
|
for ( const el in this.queue ) {
|
||||||
data.push( this.playlist[ this.queue[ el ] ] );
|
data.push( this.playlist[ this.queue[ el ] ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,13 +532,14 @@ class MusicKitJSWrapper {
|
|||||||
getUserPlaylists ( cb: ( data: object ) => void ): boolean {
|
getUserPlaylists ( cb: ( data: object ) => void ): boolean {
|
||||||
if ( this.isLoggedIn ) {
|
if ( this.isLoggedIn ) {
|
||||||
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb );
|
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaying ( ): boolean {
|
getPlaying ( ): boolean {
|
||||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
return this.musicKit.isPlaying;
|
return this.musicKit.isPlaying;
|
||||||
} else {
|
} else {
|
||||||
@@ -466,18 +550,22 @@ class MusicKitJSWrapper {
|
|||||||
findSongOnAppleMusic ( searchTerm: string ): Promise<SearchResult> {
|
findSongOnAppleMusic ( searchTerm: string ): Promise<SearchResult> {
|
||||||
// TODO: Make storefront adjustable
|
// TODO: Make storefront adjustable
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
const queryParameters = {
|
const queryParameters = {
|
||||||
term: ( searchTerm ),
|
'term': searchTerm,
|
||||||
types: [ 'songs' ],
|
'types': [ 'songs' ],
|
||||||
};
|
};
|
||||||
this.musicKit.api.music( `v1/catalog/ch/search`, queryParameters ).then( results => {
|
|
||||||
resolve( results );
|
this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters )
|
||||||
} ).catch( e => {
|
.then( results => {
|
||||||
console.error( e );
|
resolve( results );
|
||||||
reject( e );
|
} )
|
||||||
} );
|
.catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
reject( e );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MusicKitJSWrapper;
|
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
|
// These functions handle connections to the backend with socket.io
|
||||||
|
|
||||||
import { io, type Socket } from "socket.io-client"
|
import {
|
||||||
import type { SSEMap } from "./song";
|
io, type Socket
|
||||||
|
} from 'socket.io-client';
|
||||||
|
import type {
|
||||||
|
SSEMap
|
||||||
|
} from './song';
|
||||||
|
|
||||||
class NotificationHandler {
|
class NotificationHandler {
|
||||||
|
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
|
|
||||||
roomName: string;
|
roomName: string;
|
||||||
|
|
||||||
roomToken: string;
|
roomToken: string;
|
||||||
|
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
|
||||||
useSocket: boolean;
|
useSocket: boolean;
|
||||||
|
|
||||||
eventSource?: EventSource;
|
eventSource?: EventSource;
|
||||||
|
|
||||||
toBeListenedForItems: SSEMap;
|
toBeListenedForItems: SSEMap;
|
||||||
|
|
||||||
reconnectRetryCount: number;
|
reconnectRetryCount: number;
|
||||||
|
|
||||||
lastEmitTimestamp: number;
|
lastEmitTimestamp: number;
|
||||||
|
|
||||||
openConnectionsCount: number;
|
openConnectionsCount: number;
|
||||||
|
|
||||||
pendingRequestCount: number;
|
pendingRequestCount: number;
|
||||||
|
|
||||||
connectionWasSuccessful: boolean;
|
connectionWasSuccessful: boolean;
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||||
autoConnect: false,
|
'autoConnect': false,
|
||||||
} );
|
} );
|
||||||
this.roomName = '';
|
this.roomName = '';
|
||||||
this.roomToken = '';
|
this.roomToken = '';
|
||||||
@@ -43,35 +50,44 @@ class NotificationHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a room token and connect to
|
* Create a room token and connect to
|
||||||
* @param {string} roomName
|
* @param {string} roomName
|
||||||
* @param {boolean} useAntiTamper
|
* @param {boolean} useAntiTamper
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, { credentials: 'include' } ).then( res => {
|
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
res.text().then( text => {
|
res.text().then( text => {
|
||||||
this.roomToken = text;
|
this.roomToken = text;
|
||||||
this.roomName = roomName;
|
this.roomName = roomName;
|
||||||
|
|
||||||
if ( this.useSocket ) {
|
if ( this.useSocket ) {
|
||||||
this.socket.connect();
|
this.socket.connect();
|
||||||
this.socket.emit( 'create-room', {
|
this.socket.emit(
|
||||||
name: this.roomName,
|
'create-room', {
|
||||||
token: this.roomToken
|
'name': this.roomName,
|
||||||
}, ( res: { status: boolean, msg: string } ) => {
|
'token': this.roomToken
|
||||||
if ( res.status === true ) {
|
}, ( res: {
|
||||||
this.isConnected = true;
|
'status': boolean,
|
||||||
resolve();
|
'msg': string
|
||||||
} else {
|
} ) => {
|
||||||
reject( 'ERR_ROOM_CONNECTING' );
|
if ( res.status === true ) {
|
||||||
|
this.isConnected = true;
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} );
|
);
|
||||||
} else {
|
} else {
|
||||||
this.sseConnect().then( () => {
|
this.sseConnect().then( () => {
|
||||||
resolve();
|
resolve();
|
||||||
} ).catch( );
|
} )
|
||||||
|
.catch( );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
} else if ( res.status === 409 ) {
|
} else if ( res.status === 409 ) {
|
||||||
@@ -90,9 +106,13 @@ class NotificationHandler {
|
|||||||
if ( this.reconnectRetryCount < 5 ) {
|
if ( this.reconnectRetryCount < 5 ) {
|
||||||
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||||
this.openConnectionsCount += 1;
|
this.openConnectionsCount += 1;
|
||||||
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, { credentials: 'include' } ).then( res => {
|
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, { withCredentials: true } );
|
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
|
||||||
|
'withCredentials': true
|
||||||
|
} );
|
||||||
|
|
||||||
this.eventSource.onopen = () => {
|
this.eventSource.onopen = () => {
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
@@ -100,52 +120,54 @@ class NotificationHandler {
|
|||||||
this.reconnectRetryCount = 0;
|
this.reconnectRetryCount = 0;
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||||
resolve();
|
resolve();
|
||||||
}
|
};
|
||||||
|
|
||||||
this.eventSource.onmessage = ( e ) => {
|
this.eventSource.onmessage = e => {
|
||||||
const d = JSON.parse( e.data );
|
const d = JSON.parse( e.data );
|
||||||
|
|
||||||
if ( this.toBeListenedForItems[ d.type ] ) {
|
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||||
this.toBeListenedForItems[ d.type ]( d.data );
|
this.toBeListenedForItems[ d.type ]( d.data );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
this.eventSource.onerror = ( e ) => {
|
this.eventSource.onerror = e => {
|
||||||
if ( this.isConnected ) {
|
if ( this.isConnected ) {
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
this.eventSource?.close();
|
this.eventSource?.close();
|
||||||
this.openConnectionsCount -= 1;
|
this.openConnectionsCount -= 1;
|
||||||
console.debug( e );
|
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;
|
this.eventSource = undefined;
|
||||||
|
|
||||||
this.reconnectRetryCount += 1;
|
this.reconnectRetryCount += 1;
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
this.sseConnect();
|
this.sseConnect();
|
||||||
}, 1000 * this.reconnectRetryCount );
|
}, 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' ) );
|
document.dispatchEvent( new Event( 'musicplayer:autherror' ) );
|
||||||
reject( 'ERR_UNAUTHORIZED' );
|
reject( 'ERR_UNAUTHORIZED' );
|
||||||
} else {
|
} else {
|
||||||
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
|
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
|
||||||
}
|
}
|
||||||
} ).catch( () => {
|
} )
|
||||||
if ( !this.connectionWasSuccessful ) {
|
.catch( () => {
|
||||||
reject( 'ERR_ROOM_CONNECTING' );
|
if ( !this.connectionWasSuccessful ) {
|
||||||
} else {
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
this.openConnectionsCount -= 1;
|
} else {
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
|
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.reconnectRetryCount += 1;
|
||||||
this.sseConnect();
|
setTimeout( () => {
|
||||||
}, 1000 * this.reconnectRetryCount );
|
this.sseConnect();
|
||||||
}
|
}, 1000 * this.reconnectRetryCount );
|
||||||
} );
|
}
|
||||||
|
} );
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
@@ -169,9 +191,14 @@ class NotificationHandler {
|
|||||||
emit ( event: string, data: any ): void {
|
emit ( event: string, data: any ): void {
|
||||||
if ( this.isConnected ) {
|
if ( this.isConnected ) {
|
||||||
if ( this.useSocket ) {
|
if ( this.useSocket ) {
|
||||||
this.socket.emit( event, { 'roomToken': this.roomToken, 'roomName': this.roomName, 'data': data } );
|
this.socket.emit( event, {
|
||||||
|
'roomToken': this.roomToken,
|
||||||
|
'roomName': this.roomName,
|
||||||
|
'data': data
|
||||||
|
} );
|
||||||
} else {
|
} else {
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
|
|
||||||
if ( this.lastEmitTimestamp < now - 250 ) {
|
if ( this.lastEmitTimestamp < now - 250 ) {
|
||||||
this.lastEmitTimestamp = now;
|
this.lastEmitTimestamp = now;
|
||||||
this.sendEmitConventionally( event, data );
|
this.sendEmitConventionally( event, data );
|
||||||
@@ -189,10 +216,15 @@ class NotificationHandler {
|
|||||||
|
|
||||||
sendEmitConventionally ( event: string, data: any ): void {
|
sendEmitConventionally ( event: string, data: any ): void {
|
||||||
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||||
method: 'post',
|
'method': 'post',
|
||||||
body: JSON.stringify( { 'event': event, 'roomName': this.roomName, 'roomToken': this.roomToken, 'data': data } ),
|
'body': JSON.stringify( {
|
||||||
credentials: 'include',
|
'event': event,
|
||||||
headers: {
|
'roomName': this.roomName,
|
||||||
|
'roomToken': this.roomToken,
|
||||||
|
'data': data
|
||||||
|
} ),
|
||||||
|
'credentials': 'include',
|
||||||
|
'headers': {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'charset': 'utf-8'
|
'charset': 'utf-8'
|
||||||
}
|
}
|
||||||
@@ -209,7 +241,7 @@ class NotificationHandler {
|
|||||||
if ( this.useSocket ) {
|
if ( this.useSocket ) {
|
||||||
if ( this.isConnected ) {
|
if ( this.isConnected ) {
|
||||||
this.socket.on( event, cb );
|
this.socket.on( event, cb );
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.toBeListenedForItems[ event ] = cb;
|
this.toBeListenedForItems[ event ] = cb;
|
||||||
}
|
}
|
||||||
@@ -222,22 +254,32 @@ class NotificationHandler {
|
|||||||
async disconnect (): Promise<void> {
|
async disconnect (): Promise<void> {
|
||||||
if ( this.isConnected ) {
|
if ( this.isConnected ) {
|
||||||
if ( this.useSocket ) {
|
if ( this.useSocket ) {
|
||||||
this.socket.emit( 'delete-room', {
|
this.socket.emit(
|
||||||
name: this.roomName,
|
'delete-room', {
|
||||||
token: this.roomToken
|
'name': this.roomName,
|
||||||
}, ( res: { status: boolean, msg: string } ) => {
|
'token': this.roomToken
|
||||||
this.socket.disconnect();
|
}, ( res: {
|
||||||
if ( !res.status ) {
|
'status': boolean,
|
||||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
'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 {
|
} else {
|
||||||
fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', {
|
fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', {
|
||||||
method: 'post',
|
'method': 'post',
|
||||||
body: JSON.stringify( { 'roomName': this.roomName, 'roomToken': this.roomToken } ),
|
'body': JSON.stringify( {
|
||||||
credentials: 'include',
|
'roomName': this.roomName,
|
||||||
headers: {
|
'roomToken': this.roomToken
|
||||||
|
} ),
|
||||||
|
'credentials': 'include',
|
||||||
|
'headers': {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'charset': 'utf-8'
|
'charset': 'utf-8'
|
||||||
}
|
}
|
||||||
@@ -247,10 +289,12 @@ class NotificationHandler {
|
|||||||
} else {
|
} else {
|
||||||
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} ).catch( () => {
|
} )
|
||||||
return;
|
.catch( () => {
|
||||||
} );
|
return;
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,6 +302,7 @@ class NotificationHandler {
|
|||||||
getRoomName (): string {
|
getRoomName (): string {
|
||||||
return this.roomName;
|
return this.roomName;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationHandler;
|
export default NotificationHandler;
|
||||||
|
|||||||
72
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
72
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
@@ -4,92 +4,92 @@ export interface Song {
|
|||||||
/**
|
/**
|
||||||
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_
|
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_
|
||||||
*/
|
*/
|
||||||
id: string;
|
'id': string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Origin of the song
|
* Origin of the song
|
||||||
*/
|
*/
|
||||||
origin: Origin;
|
'origin': Origin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The cover image as a URL
|
* The cover image as a URL
|
||||||
*/
|
*/
|
||||||
cover: string;
|
'cover': string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The artist of the song
|
* The artist of the song
|
||||||
*/
|
*/
|
||||||
artist: string;
|
'artist': string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the song
|
* The name of the song
|
||||||
*/
|
*/
|
||||||
title: string;
|
'title': string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duration of the song in milliseconds
|
* Duration of the song in milliseconds
|
||||||
*/
|
*/
|
||||||
duration: number;
|
'duration': number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there
|
* (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there
|
||||||
*/
|
*/
|
||||||
genres?: string[];
|
'genres'?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (OPTIONAL) This will be displayed in brackets on the showcase screens
|
* (OPTIONAL) This will be displayed in brackets on the showcase screens
|
||||||
*/
|
*/
|
||||||
additionalInfo?: string;
|
'additionalInfo'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SongTransmitted {
|
export interface SongTransmitted {
|
||||||
title: string;
|
'title': string;
|
||||||
artist: string;
|
'artist': string;
|
||||||
duration: number;
|
'duration': number;
|
||||||
cover: string;
|
'cover': string;
|
||||||
additionalInfo?: string;
|
'additionalInfo'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ReadFile {
|
export interface ReadFile {
|
||||||
url: string;
|
'url': string;
|
||||||
filename: string;
|
'filename': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
data: {
|
'data': {
|
||||||
results: {
|
'results': {
|
||||||
songs: {
|
'songs': {
|
||||||
data: AppleMusicSongData[],
|
'data': AppleMusicSongData[],
|
||||||
href: string;
|
'href': string;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppleMusicSongData {
|
export interface AppleMusicSongData {
|
||||||
id: string,
|
'id': string,
|
||||||
type: string;
|
'type': string;
|
||||||
href: string;
|
'href': string;
|
||||||
attributes: {
|
'attributes': {
|
||||||
albumName: string;
|
'albumName': string;
|
||||||
artistName: string;
|
'artistName': string;
|
||||||
artwork: {
|
'artwork': {
|
||||||
width: number,
|
'width': number,
|
||||||
height: number,
|
'height': number,
|
||||||
url: string
|
'url': string
|
||||||
},
|
},
|
||||||
name: string;
|
'name': string;
|
||||||
genreNames: string[];
|
'genreNames': string[];
|
||||||
durationInMillis: number;
|
'durationInMillis': number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SongMove {
|
export interface SongMove {
|
||||||
songID: string;
|
'songID': string;
|
||||||
newPos: number;
|
'newPos': number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSEMap {
|
export interface SSEMap {
|
||||||
[key: string]: ( data: any ) => void;
|
[key: string]: ( data: any ) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,29 @@
|
|||||||
/*
|
import {
|
||||||
* LanguageSchoolHossegorBookingSystem - userStore.js
|
defineStore
|
||||||
*
|
} from 'pinia';
|
||||||
* Created by Janis Hutz 10/27/2023, Licensed under a proprietary License
|
|
||||||
* https://janishutz.com, development@janishutz.com
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
|
|
||||||
// FOSS-VERSION: To enable the UI to be used with the FOSS version, change "isUserAuth" to true, you will be "logged in"
|
// FOSS-VERSION: To enable the UI to be used with the FOSS version, change "isUserAuth" to true, you will be "logged in"
|
||||||
export const useUserStore = defineStore( 'user', {
|
export const useUserStore = defineStore( 'user', {
|
||||||
state: () => ( { 'isUserAuth': true, 'hasSubscribed': false, 'isUsingKeyboard': false, 'username': '', 'isFOSSVersion': false } ),
|
'state': () => ( {
|
||||||
getters: {
|
'isUserAuth': false,
|
||||||
getUserAuthenticated: ( state ) => state.isUserAuth,
|
'hasSubscribed': false,
|
||||||
getSubscriptionStatus: ( state ) => state.hasSubscribed,
|
'isUsingKeyboard': false,
|
||||||
|
'isFOSSVersion': false
|
||||||
|
} ),
|
||||||
|
'getters': {
|
||||||
|
'getUserAuthenticated': state => state.isUserAuth,
|
||||||
|
'getSubscriptionStatus': state => state.hasSubscribed,
|
||||||
},
|
},
|
||||||
actions: {
|
'actions': {
|
||||||
setUserAuth ( auth: boolean ) {
|
setUserAuth ( auth: boolean ) {
|
||||||
this.isUserAuth = auth;
|
this.isUserAuth = auth;
|
||||||
},
|
},
|
||||||
setSubscriptionStatus ( status: boolean ) {
|
setSubscriptionStatus ( status: boolean ) {
|
||||||
this.hasSubscribed = status;
|
this.hasSubscribed = status;
|
||||||
},
|
},
|
||||||
setUsername ( username: string ) {
|
|
||||||
this.username = username;
|
|
||||||
},
|
|
||||||
setKeyboardUsageStatus ( status: boolean ) {
|
setKeyboardUsageStatus ( status: boolean ) {
|
||||||
this.isUsingKeyboard = status;
|
this.isUsingKeyboard = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|||||||
@@ -21,4 +21,4 @@
|
|||||||
.logo {
|
.logo {
|
||||||
height: 50vh;
|
height: 50vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-view">
|
<div class="app-view">
|
||||||
<button id="logout" @click="logout()"><span class="material-symbols-outlined">logout</span></button>
|
<button id="logout" @click="logout()">
|
||||||
<div class="loading-view" v-if="!hasFinishedLoading">
|
<span class="material-symbols-outlined">logout</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="!hasFinishedLoading" class="loading-view">
|
||||||
<h1>Loading...</h1>
|
<h1>Loading...</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="home-view" v-else-if="hasFinishedLoading && isReady">
|
<div v-else-if="hasFinishedLoading && isReady" class="home-view">
|
||||||
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"
|
<libraryView
|
||||||
:is-logged-in="isLoggedIntoAppleMusic" @custom-playlist="( pl ) => selectCustomPlaylist( pl )"></libraryView>
|
class="library-view"
|
||||||
|
:playlists="playlists"
|
||||||
|
:is-logged-in="isLoggedIntoAppleMusic"
|
||||||
|
@selected-playlist="( id ) => { selectPlaylist( id ) }"
|
||||||
|
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="login-view">
|
<div v-else class="login-view">
|
||||||
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
|
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
|
||||||
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">Log into Apple Music</button>
|
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">
|
||||||
<button class="fancy-button" title="This allows you to use local playlists only. Cover images for your songs will be fetched from the apple music api as good as possible" @click="skipLogin()">Continue without logging in</button>
|
Log into Apple Music
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="fancy-button"
|
||||||
|
title="This allows you to use local playlists only.
|
||||||
|
Cover images for your songs will be fetched from the apple music api as good as possible"
|
||||||
|
@click="skipLogin()"
|
||||||
|
>
|
||||||
|
Continue without logging in
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<playerView :class="'player-view' + ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
<playerView
|
||||||
ref="player"></playerView>
|
ref="player"
|
||||||
|
:class="'player-view'
|
||||||
|
+ ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )"
|
||||||
|
@player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||||
|
/>
|
||||||
<!-- TODO: Call to backend to check if user has access -->
|
<!-- TODO: Call to backend to check if user has access -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -22,11 +42,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import playerView from '@/components/playerView.vue';
|
import playerView from '@/components/playerView.vue';
|
||||||
import libraryView from '@/components/libraryView.vue';
|
import libraryView from '@/components/libraryView.vue';
|
||||||
import { ref } from 'vue';
|
import {
|
||||||
import type { ReadFile } from '@/scripts/song';
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
import type {
|
||||||
|
ReadFile
|
||||||
|
} from '@/scripts/song';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { useUserStore } from '@/stores/userStore';
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/stores/userStore';
|
||||||
|
|
||||||
const isLoggedIntoAppleMusic = ref( false );
|
const isLoggedIntoAppleMusic = ref( false );
|
||||||
const isReady = ref( false );
|
const isReady = ref( false );
|
||||||
const isShowingFullScreenPlayer = ref( false );
|
const isShowingFullScreenPlayer = ref( false );
|
||||||
@@ -41,7 +67,7 @@
|
|||||||
} else {
|
} else {
|
||||||
isShowingFullScreenPlayer.value = true;
|
isShowingFullScreenPlayer.value = true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
let loginChecker = 0;
|
let loginChecker = 0;
|
||||||
|
|
||||||
@@ -51,7 +77,7 @@
|
|||||||
if ( player.value.getAuth()[ 0 ] ) {
|
if ( player.value.getAuth()[ 0 ] ) {
|
||||||
isLoggedIntoAppleMusic.value = true;
|
isLoggedIntoAppleMusic.value = true;
|
||||||
isReady.value = true;
|
isReady.value = true;
|
||||||
player.value.getPlaylists( ( data ) => {
|
player.value.getPlaylists( data => {
|
||||||
playlists.value = data.data.data;
|
playlists.value = data.data.data;
|
||||||
} );
|
} );
|
||||||
clearInterval( loginChecker );
|
clearInterval( loginChecker );
|
||||||
@@ -60,25 +86,27 @@
|
|||||||
alert( 'An error occurred when logging you in. Please try again!' );
|
alert( 'An error occurred when logging you in. Please try again!' );
|
||||||
}
|
}
|
||||||
}, 500 );
|
}, 500 );
|
||||||
}
|
};
|
||||||
|
|
||||||
const skipLogin = () => {
|
const skipLogin = () => {
|
||||||
isReady.value = true;
|
isReady.value = true;
|
||||||
isLoggedIntoAppleMusic.value = false;
|
isLoggedIntoAppleMusic.value = false;
|
||||||
player.value.skipLogin();
|
player.value.skipLogin();
|
||||||
}
|
};
|
||||||
|
|
||||||
const selectPlaylist = ( id: string ) => {
|
const selectPlaylist = ( id: string ) => {
|
||||||
player.value.selectPlaylist( id );
|
player.value.selectPlaylist( id );
|
||||||
player.value.controlUI( 'show' );
|
player.value.controlUI( 'show' );
|
||||||
}
|
};
|
||||||
|
|
||||||
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||||
player.value.selectCustomPlaylist( playlist );
|
player.value.selectCustomPlaylist( playlist );
|
||||||
player.value.controlUI( 'show' );
|
player.value.controlUI( 'show' );
|
||||||
}
|
};
|
||||||
|
|
||||||
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', { credentials: 'include' } ).then( res => {
|
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
res.text().then( text => {
|
res.text().then( text => {
|
||||||
if ( text === 'ok' ) {
|
if ( text === 'ok' ) {
|
||||||
@@ -90,7 +118,7 @@
|
|||||||
router.push( '/get' );
|
router.push( '/get' );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
} else if ( res.status === 404 ) {
|
} else if ( res.status === 402 ) {
|
||||||
userStore.setSubscriptionStatus( false );
|
userStore.setSubscriptionStatus( false );
|
||||||
router.push( '/get' );
|
router.push( '/get' );
|
||||||
sessionStorage.setItem( 'getRedirectionReason', 'notOwned' );
|
sessionStorage.setItem( 'getRedirectionReason', 'notOwned' );
|
||||||
@@ -102,7 +130,7 @@
|
|||||||
const logout = () => {
|
const logout = () => {
|
||||||
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||||
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -166,4 +194,4 @@
|
|||||||
.player-hidden {
|
.player-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
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">
|
<div class="top-view">
|
||||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||||
<h1>MusicPlayer</h1>
|
<h1>MusicPlayer</h1>
|
||||||
<p v-if="reasonForRedirectHere" style="color: red;">{{ reasons[ reasonForRedirectHere ] }}</p>
|
<p v-if="reasonForRedirectHere" style="color: red;">
|
||||||
<p v-if="!reasonForRedirectHere"><i>An Open Source, browser-based MusicPlayer with beautiful graphics</i></p>
|
{{ reasons[ reasonForRedirectHere ] }}
|
||||||
|
</p>
|
||||||
|
<p v-if="!reasonForRedirectHere">
|
||||||
|
<i>An Open Source, browser-based MusicPlayer with beautiful graphics</i>
|
||||||
|
</p>
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<a href="https://store.janishutz.com/product/com.janishutz.MusicPlayer" class="fancy-button" target="_blank">Subscribe</a>
|
<a href="https://store.janishutz.com/product/com.janishutz.MusicPlayer" class="fancy-button" target="_blank">Subscribe</a>
|
||||||
<a href="/" class="fancy-button" style="margin-left: 10px;" v-if="!reasonForRedirectHere">Log in</a>
|
<a
|
||||||
<button href="/" class="fancy-button" style="margin-left: 10px;" v-if="reasonForRedirectHere" @click="logout()">Log out</button>
|
v-if="!reasonForRedirectHere"
|
||||||
<a href="https://github.com/simplePCBuilding/MusicPlayerV2" class="fancy-button" style="margin-left: 10px;" target="_blank">GitHub</a>
|
href="/"
|
||||||
|
class="fancy-button"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
>Log in</a>
|
||||||
|
<button
|
||||||
|
v-if="reasonForRedirectHere"
|
||||||
|
href="/"
|
||||||
|
class="fancy-button"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
@click="logout()"
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://github.com/simplePCBuilding/MusicPlayerV2"
|
||||||
|
class="fancy-button"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
target="_blank"
|
||||||
|
>GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -20,7 +42,10 @@
|
|||||||
<p>Use MusicPlayer in conjunction with Apple Music</p>
|
<p>Use MusicPlayer in conjunction with Apple Music</p>
|
||||||
|
|
||||||
<h2>Share your playlist</h2>
|
<h2>Share your playlist</h2>
|
||||||
<p>You can share your playlist on a beautifully animated public page, so that other people can join in and view your playlist</p>
|
<p>
|
||||||
|
You can share your playlist on a beautifully animated public page,
|
||||||
|
so that other people can join in and view your playlist
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>Fully browser based</h2>
|
<h2>Fully browser based</h2>
|
||||||
<p>No installation required when using MusicPlayer on <a href="https://music.janishutz.com">music.janishutz.com</a></p>
|
<p>No installation required when using MusicPlayer on <a href="https://music.janishutz.com">music.janishutz.com</a></p>
|
||||||
@@ -29,7 +54,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, type Ref } from 'vue';
|
import {
|
||||||
|
ref, type Ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
interface Reasons {
|
interface Reasons {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
@@ -39,12 +66,13 @@
|
|||||||
'notOwned': 'Please subscribe to use MusicPlayer here, or download and install it manually from GitHub!',
|
'notOwned': 'Please subscribe to use MusicPlayer here, or download and install it manually from GitHub!',
|
||||||
} );
|
} );
|
||||||
const reasonForRedirectHere = ref( sessionStorage.getItem( 'getRedirectionReason' ) );
|
const reasonForRedirectHere = ref( sessionStorage.getItem( 'getRedirectionReason' ) );
|
||||||
|
|
||||||
sessionStorage.removeItem( 'getRedirectionReason' );
|
sessionStorage.removeItem( 'getRedirectionReason' );
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||||
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -60,7 +88,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-height {
|
.full-height {
|
||||||
@@ -75,4 +103,4 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vh;
|
width: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,54 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-view">
|
<div class="home-view">
|
||||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||||
<button :class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )" @click="login()"
|
<button
|
||||||
style="margin-top: 5vh;" title="Sign in or sign up with janishutz.com ID" v-if="status"
|
:class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )"
|
||||||
>{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}</button>
|
style="margin-top: 5vh;"
|
||||||
<p v-else>We are sorry, but we were unable to initialize the login services. Please reload the page if you wish to retry!</p>
|
title="Sign in or sign up with janishutz.com ID"
|
||||||
<p style="width: 80%;">MusicPlayer is a browser based Music Player, that allows you to connect other devices, simply with another web-browser, where you can see the current playlist with sleek animations. You can log in using your Apple Music account or load a playlist from your local disk, simply by selecting the songs using a file picker.</p>
|
@click="login()"
|
||||||
<router-link to="/get" class="fancy-button">More information</router-link>
|
>
|
||||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}
|
||||||
|
</button>
|
||||||
|
<p style="width: 80%;">
|
||||||
|
MusicPlayer is a browser based Music Player, that allows you to connect other devices,
|
||||||
|
simply with another web-browser, where you can see the current playlist with sleek animations.
|
||||||
|
You can log in using your Apple Music account or load a playlist from your local disk,
|
||||||
|
simply by selecting the songs using a file picker.
|
||||||
|
</p>
|
||||||
|
<router-link to="/get" class="fancy-button">
|
||||||
|
More information
|
||||||
|
</router-link>
|
||||||
|
<notificationsModule ref="notifications" location="bottomleft" size="bigger" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// TODO: Make possible to install and use without account, if using FOSS version
|
// TODO: Make possible to install and use without account, if using FOSS version
|
||||||
import router from '@/router';
|
import {
|
||||||
import { RouterLink } from 'vue-router';
|
RouterLink
|
||||||
import { useUserStore } from '@/stores/userStore';
|
} from 'vue-router';
|
||||||
import notificationsModule from '@/components/notificationsModule.vue';
|
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 notifications = ref( notificationsModule );
|
||||||
const isTryingToSignIn = ref( true );
|
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 = () => {
|
const login = () => {
|
||||||
sdk.createSession();
|
sdk.login();
|
||||||
}
|
};
|
||||||
|
|
||||||
const store = useUserStore();
|
const store = useUserStore();
|
||||||
|
|
||||||
@@ -56,23 +50,27 @@
|
|||||||
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||||
localStorage.removeItem( 'redirect' );
|
localStorage.removeItem( 'redirect' );
|
||||||
} else {
|
} else {
|
||||||
if ( typeof( sdk ) !== 'undefined' ) {
|
sdk.verifyFull()
|
||||||
sdk.verifySession().then( res => {
|
.then( res => {
|
||||||
if ( res.status ) {
|
if ( res ) {
|
||||||
store.isUserAuth = true;
|
store.isUserAuth = true;
|
||||||
store.username = res.username;
|
|
||||||
if ( localStorage.getItem( 'close-tab' ) ) {
|
if ( localStorage.getItem( 'close-tab' ) ) {
|
||||||
localStorage.removeItem( 'close-tab' );
|
localStorage.removeItem( 'close-tab' );
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem( 'login-ok', 'true' );
|
localStorage.setItem( 'login-ok', 'true' );
|
||||||
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||||
localStorage.removeItem( 'redirect' );
|
localStorage.removeItem( 'redirect' );
|
||||||
} else {
|
} else {
|
||||||
isTryingToSignIn.value = false;
|
isTryingToSignIn.value = false;
|
||||||
}
|
}
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
console.debug( e );
|
||||||
|
isTryingToSignIn.value = false;
|
||||||
} );
|
} );
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -89,4 +87,4 @@
|
|||||||
height: 50vh;
|
height: 50vh;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
<div class="info">
|
||||||
|
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||||
|
</div>
|
||||||
<div class="remote-view">
|
<div class="remote-view">
|
||||||
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
||||||
<div class="current-song-wrapper">
|
<div class="current-song-wrapper">
|
||||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
<img
|
||||||
|
v-if="playlist[ playingSong ] && 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>
|
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||||
<div class="current-song">
|
<div class="current-song">
|
||||||
<h1 style="margin-bottom: 5px;">{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
<h1 style="margin-bottom: 5px;">
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||||
|
</h1>
|
||||||
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||||
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
|
<p
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||||
|
class="additional-info"
|
||||||
|
>
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||||
|
</p>
|
||||||
|
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="song-list-wrapper">
|
<div class="song-list-wrapper">
|
||||||
<div v-for="song in songQueue" v-bind:key="song.id" class="song-list">
|
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||||
<div class="song-details-wrapper">
|
<div class="song-details-wrapper">
|
||||||
<h3>{{ song.title }}</h3>
|
<h3>{{ song.title }}</h3>
|
||||||
<p>{{ song.artist }}</p>
|
<p>{{ song.artist }}</p>
|
||||||
@@ -32,7 +47,9 @@
|
|||||||
<div v-else style="max-width: 80%;">
|
<div v-else style="max-width: 80%;">
|
||||||
<span class="material-symbols-outlined" style="font-size: 4rem;">wifi_off</span>
|
<span class="material-symbols-outlined" style="font-size: 4rem;">wifi_off</span>
|
||||||
<h1>Couldn't connect!</h1>
|
<h1>Couldn't connect!</h1>
|
||||||
<p>There does not appear to be a share with the specified name, or an error occurred when connecting.</p>
|
<p>
|
||||||
|
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||||
|
</p>
|
||||||
<p>You may <a href="">reload</a> the page to try again!</p>
|
<p>You may <a href="">reload</a> the page to try again!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,8 +58,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SocketConnection from '@/scripts/connection';
|
import SocketConnection from '@/scripts/connection';
|
||||||
import type { Song } from '@/scripts/song';
|
import type {
|
||||||
import { computed, ref, type Ref } from 'vue';
|
Song
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
computed, ref, type Ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
const isPlaying = ref( false );
|
const isPlaying = ref( false );
|
||||||
const playlist: Ref<Song[]> = ref( [] );
|
const playlist: Ref<Song[]> = ref( [] );
|
||||||
@@ -52,6 +73,7 @@
|
|||||||
const hasLoaded = ref( false );
|
const hasLoaded = ref( false );
|
||||||
const showCouldNotFindRoom = ref( false );
|
const showCouldNotFindRoom = ref( false );
|
||||||
const playbackStart = ref( 0 );
|
const playbackStart = ref( 0 );
|
||||||
|
|
||||||
let timeTracker = 0;
|
let timeTracker = 0;
|
||||||
|
|
||||||
const conn = new SocketConnection();
|
const conn = new SocketConnection();
|
||||||
@@ -61,18 +83,22 @@
|
|||||||
isPlaying.value = d.playbackStatus;
|
isPlaying.value = d.playbackStatus;
|
||||||
playingSong.value = d.playlistIndex;
|
playingSong.value = d.playlistIndex;
|
||||||
playbackStart.value = d.playbackStart;
|
playbackStart.value = d.playbackStart;
|
||||||
|
|
||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
startTimeTracker();
|
startTimeTracker();
|
||||||
}
|
}
|
||||||
|
|
||||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ]
|
||||||
|
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||||
hasLoaded.value = true;
|
hasLoaded.value = true;
|
||||||
conn.registerListener( 'playlist', ( data ) => {
|
conn.registerListener( 'playlist', data => {
|
||||||
playlist.value = data;
|
playlist.value = data;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
conn.registerListener( 'playback', ( data ) => {
|
conn.registerListener( 'playback', data => {
|
||||||
isPlaying.value = data;
|
isPlaying.value = data;
|
||||||
|
|
||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
startTimeTracker();
|
startTimeTracker();
|
||||||
} else {
|
} else {
|
||||||
@@ -80,72 +106,81 @@
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
conn.registerListener( 'playback-start', ( data ) => {
|
conn.registerListener( 'playback-start', data => {
|
||||||
playbackStart.value = data;
|
playbackStart.value = data;
|
||||||
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
conn.registerListener( 'playlist-index', ( data ) => {
|
conn.registerListener( 'playlist-index', data => {
|
||||||
playingSong.value = parseInt( data );
|
playingSong.value = parseInt( data );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
conn.registerListener( 'delete-share', ( _ ) => {
|
conn.registerListener( 'delete-share', _ => {
|
||||||
alert( 'This share was just deleted. It is no longer available. The page will reload automatically to try and re-establish connection!' );
|
alert( `This share was just deleted. It is no longer available.
|
||||||
|
The page will reload automatically to try and re-establish connection!` );
|
||||||
conn.disconnect();
|
conn.disconnect();
|
||||||
location.reload();
|
location.reload();
|
||||||
} );
|
} );
|
||||||
} ).catch( e => {
|
} )
|
||||||
console.error( e );
|
.catch( e => {
|
||||||
showCouldNotFindRoom.value = true;
|
console.error( e );
|
||||||
} );
|
showCouldNotFindRoom.value = true;
|
||||||
|
} );
|
||||||
|
|
||||||
const songQueue = computed( () => {
|
const songQueue = computed( () => {
|
||||||
let ret: Song[] = [];
|
let ret: Song[] = [];
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|
||||||
for ( let song in playlist.value ) {
|
for ( let song in playlist.value ) {
|
||||||
if ( pos >= playingSong.value ) {
|
if ( pos >= playingSong.value ) {
|
||||||
ret.push( playlist.value[ song ] );
|
ret.push( playlist.value[ song ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// TODO: Handle disconnect from updater (=> have it disconnect)
|
|
||||||
|
|
||||||
const getTimeUntil = computed( () => {
|
const getTimeUntil = computed( () => {
|
||||||
return ( song: string ) => {
|
return ( song: string ) => {
|
||||||
let timeRemaining = 0;
|
let timeRemaining = 0;
|
||||||
|
|
||||||
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||||
if ( playlist.value[ i ].id == song ) {
|
if ( playlist.value[ i ].id == song ) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRemaining += playlist.value[ i ].duration;
|
timeRemaining += playlist.value[ i ].duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Currently playing';
|
return 'Currently playing';
|
||||||
} else {
|
} else {
|
||||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min';
|
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Plays next';
|
return 'Plays next';
|
||||||
} else {
|
} else {
|
||||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min after starting to play';
|
return 'Playing less than '
|
||||||
|
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const startTimeTracker = () => {
|
const startTimeTracker = () => {
|
||||||
try {
|
try {
|
||||||
clearInterval( timeTracker );
|
clearInterval( timeTracker );
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch ( err ) { /* empty */ }
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
timeTracker = setInterval( () => {
|
timeTracker = setInterval( () => {
|
||||||
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||||
|
|
||||||
if ( isNaN( progressBar.value ) ) {
|
if ( isNaN( progressBar.value ) ) {
|
||||||
progressBar.value = 0;
|
progressBar.value = 0;
|
||||||
}
|
}
|
||||||
@@ -156,11 +191,11 @@
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
}, 100 );
|
}, 100 );
|
||||||
}
|
};
|
||||||
|
|
||||||
const stopTimeTracker = () => {
|
const stopTimeTracker = () => {
|
||||||
clearInterval( timeTracker );
|
clearInterval( timeTracker );
|
||||||
}
|
};
|
||||||
|
|
||||||
document.addEventListener( 'visibilitychange', () => {
|
document.addEventListener( 'visibilitychange', () => {
|
||||||
if ( !document.hidden ) {
|
if ( !document.hidden ) {
|
||||||
@@ -351,4 +386,4 @@
|
|||||||
width: 30%;
|
width: 30%;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,34 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<span class="anti-tamper material-symbols-outlined" v-if="isAntiTamperEnabled" @click="secureModeInfo( 'toggle' )">lock</span>
|
<span
|
||||||
<div class="anti-tamper-info" v-if="isShowingSecureModeInfo && isAntiTamperEnabled" @click="secureModeInfo( 'hide' )">Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!</div>
|
v-if="isAntiTamperEnabled"
|
||||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
class="anti-tamper material-symbols-outlined"
|
||||||
|
@click="secureModeInfo( 'toggle' )"
|
||||||
|
>lock</span>
|
||||||
|
<div
|
||||||
|
v-if="isShowingSecureModeInfo && isAntiTamperEnabled"
|
||||||
|
class="anti-tamper-info"
|
||||||
|
@click="secureModeInfo( 'hide' )"
|
||||||
|
>
|
||||||
|
Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||||
|
</div>
|
||||||
<div class="remote-view">
|
<div class="remote-view">
|
||||||
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||||
<div class="current-song-wrapper">
|
<div class="current-song-wrapper">
|
||||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
<img
|
||||||
|
v-if="playlist[ playingSong ] && 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>
|
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||||
<div class="current-song">
|
<div class="current-song">
|
||||||
<h1 style="margin-bottom: 5px;">{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
<h1 style="margin-bottom: 5px;">
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||||
|
</h1>
|
||||||
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||||
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
|
<p
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||||
|
class="additional-info"
|
||||||
|
>
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||||
|
</p>
|
||||||
|
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-selector-wrapper">
|
<div class="mode-selector-wrapper">
|
||||||
<select v-model="visualizationSettings" @change="handleAnimationChange()">
|
<select v-model="visualizationSettings" @change="handleAnimationChange()">
|
||||||
<option value="mic">Microphone (Mic access required)</option>
|
<option value="mic">
|
||||||
<option value="off">No visualization except background</option>
|
Microphone (Mic access required)
|
||||||
|
</option>
|
||||||
|
<option value="off">
|
||||||
|
No visualization except background
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="song-list-wrapper">
|
<div class="song-list-wrapper">
|
||||||
<div v-for="song in songQueue" v-bind:key="song.id" class="song-list">
|
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||||
<img :src="song.cover" class="song-image">
|
<img v-if="song.cover" :src="song.cover" class="song-image">
|
||||||
<div v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying" class="playing-symbols">
|
<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-symbols-wrapper">
|
||||||
<div class="playing-bar" id="bar-1"></div>
|
<div id="bar-1" class="playing-bar"></div>
|
||||||
<div class="playing-bar" id="bar-2"></div>
|
<div id="bar-2" class="playing-bar"></div>
|
||||||
<div class="playing-bar" id="bar-3"></div>
|
<div id="bar-3" class="playing-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="song-details-wrapper">
|
<div class="song-details-wrapper">
|
||||||
@@ -47,10 +80,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="showcase-wrapper">
|
<div v-else class="showcase-wrapper">
|
||||||
<h1>Couldn't connect!</h1>
|
<h1>Couldn't connect!</h1>
|
||||||
<p>There does not appear to be a share with the specified name, or an error occurred when connecting.</p>
|
<p>
|
||||||
|
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||||
|
</p>
|
||||||
<p>You may reload the page to try again!</p>
|
<p>You may reload the page to try again!</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="background" id="background">
|
<div id="background" class="background">
|
||||||
<div class="beat-manual"></div>
|
<div class="beat-manual"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,8 +94,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SocketConnection from '@/scripts/connection';
|
import SocketConnection from '@/scripts/connection';
|
||||||
import type { Song } from '@/scripts/song';
|
import type {
|
||||||
import { computed, ref, type Ref } from 'vue';
|
Song
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
type Ref, computed, ref
|
||||||
|
} from 'vue';
|
||||||
import bizualizer from '@/scripts/bizualizer';
|
import bizualizer from '@/scripts/bizualizer';
|
||||||
|
|
||||||
const isPlaying = ref( false );
|
const isPlaying = ref( false );
|
||||||
@@ -71,10 +110,11 @@
|
|||||||
const hasLoaded = ref( false );
|
const hasLoaded = ref( false );
|
||||||
const showCouldNotFindRoom = ref( false );
|
const showCouldNotFindRoom = ref( false );
|
||||||
const playbackStart = ref( 0 );
|
const playbackStart = ref( 0 );
|
||||||
|
|
||||||
let timeTracker = 0;
|
let timeTracker = 0;
|
||||||
|
|
||||||
const visualizationSettings = ref( 'mic' );
|
const visualizationSettings = ref( 'mic' );
|
||||||
const isAntiTamperEnabled = ref( false );
|
const isAntiTamperEnabled = ref( false );
|
||||||
|
|
||||||
const conn = new SocketConnection();
|
const conn = new SocketConnection();
|
||||||
|
|
||||||
conn.connect().then( d => {
|
conn.connect().then( d => {
|
||||||
@@ -82,22 +122,29 @@
|
|||||||
isPlaying.value = d.playbackStatus;
|
isPlaying.value = d.playbackStatus;
|
||||||
playingSong.value = d.playlistIndex;
|
playingSong.value = d.playlistIndex;
|
||||||
playbackStart.value = d.playbackStart;
|
playbackStart.value = d.playbackStart;
|
||||||
|
|
||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
startTimeTracker();
|
startTimeTracker();
|
||||||
}
|
}
|
||||||
|
|
||||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||||
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
progressBar.value
|
||||||
|
= ( pos.value / ( playlist.value[ playingSong.value ]
|
||||||
|
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||||
hasLoaded.value = true;
|
hasLoaded.value = true;
|
||||||
|
|
||||||
if ( d.useAntiTamper ) {
|
if ( d.useAntiTamper ) {
|
||||||
isAntiTamperEnabled.value = true;
|
isAntiTamperEnabled.value = true;
|
||||||
notifier();
|
notifier();
|
||||||
}
|
}
|
||||||
conn.registerListener( 'playlist', ( data ) => {
|
|
||||||
|
conn.registerListener( 'playlist', data => {
|
||||||
playlist.value = data;
|
playlist.value = data;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
conn.registerListener( 'playback', ( data ) => {
|
conn.registerListener( 'playback', data => {
|
||||||
isPlaying.value = data;
|
isPlaying.value = data;
|
||||||
|
|
||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
startTimeTracker();
|
startTimeTracker();
|
||||||
} else {
|
} else {
|
||||||
@@ -105,12 +152,12 @@
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
conn.registerListener( 'playback-start', ( data ) => {
|
conn.registerListener( 'playback-start', data => {
|
||||||
playbackStart.value = data;
|
playbackStart.value = data;
|
||||||
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
conn.registerListener( 'playlist-index', ( data ) => {
|
conn.registerListener( 'playlist-index', data => {
|
||||||
playingSong.value = parseInt( data );
|
playingSong.value = parseInt( data );
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
setBackground();
|
setBackground();
|
||||||
@@ -118,58 +165,64 @@
|
|||||||
} );
|
} );
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
conn.registerListener( 'delete-share', ( _ ) => {
|
conn.registerListener( 'delete-share', _ => {
|
||||||
alert( 'This share was just deleted. It is no longer available. This page will reload automatically!' );
|
alert( 'This share was just deleted. It is no longer available. This page will reload automatically!' );
|
||||||
conn.disconnect();
|
conn.disconnect();
|
||||||
location.reload();
|
location.reload();
|
||||||
} );
|
} );
|
||||||
} ).catch( e => {
|
} )
|
||||||
console.error( e );
|
.catch( e => {
|
||||||
showCouldNotFindRoom.value = true;
|
console.error( e );
|
||||||
} );
|
showCouldNotFindRoom.value = true;
|
||||||
|
} );
|
||||||
|
|
||||||
const songQueue = computed( () => {
|
const songQueue = computed( () => {
|
||||||
let ret: Song[] = [];
|
let ret: Song[] = [];
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|
||||||
for ( let song in playlist.value ) {
|
for ( let song in playlist.value ) {
|
||||||
if ( pos >= playingSong.value ) {
|
if ( pos >= playingSong.value ) {
|
||||||
ret.push( playlist.value[ song ] );
|
ret.push( playlist.value[ song ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// TODO: Handle disconnect from updater (=> have it disconnect)
|
|
||||||
|
|
||||||
const getTimeUntil = computed( () => {
|
const getTimeUntil = computed( () => {
|
||||||
return ( song: string ) => {
|
return ( song: string ) => {
|
||||||
let timeRemaining = 0;
|
let timeRemaining = 0;
|
||||||
|
|
||||||
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||||
if ( playlist.value[ i ].id == song ) {
|
if ( playlist.value[ i ].id == song ) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRemaining += playlist.value[ i ].duration;
|
timeRemaining += playlist.value[ i ].duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( isPlaying.value ) {
|
if ( isPlaying.value ) {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Currently playing';
|
return 'Currently playing';
|
||||||
} else {
|
} else {
|
||||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min';
|
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Plays next';
|
return 'Plays next';
|
||||||
} else {
|
} else {
|
||||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - pos.value / 60 ) + 'min after starting to play';
|
return 'Playing less than '
|
||||||
|
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const startTimeTracker = () => {
|
const startTimeTracker = () => {
|
||||||
try {
|
try {
|
||||||
clearInterval( timeTracker );
|
clearInterval( timeTracker );
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch ( err ) { /* empty */ }
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
@@ -179,21 +232,23 @@
|
|||||||
timeTracker = setInterval( () => {
|
timeTracker = setInterval( () => {
|
||||||
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||||
|
|
||||||
if ( isNaN( progressBar.value ) ) {
|
if ( isNaN( progressBar.value ) ) {
|
||||||
progressBar.value = 0;
|
progressBar.value = 0;
|
||||||
}
|
}
|
||||||
}, 100 );
|
}, 100 );
|
||||||
}
|
};
|
||||||
|
|
||||||
const stopTimeTracker = () => {
|
const stopTimeTracker = () => {
|
||||||
clearInterval( timeTracker );
|
clearInterval( timeTracker );
|
||||||
|
|
||||||
handleAnimationChange();
|
handleAnimationChange();
|
||||||
}
|
};
|
||||||
|
|
||||||
const animateBeat = () => {
|
const animateBeat = () => {
|
||||||
$( '.beat-manual' ).stop();
|
$( '.beat-manual' ).stop();
|
||||||
const duration = Math.ceil( 60 / 180 * 500 ) - 50;
|
const duration = Math.ceil( 60 / 180 * 500 ) - 50;
|
||||||
|
|
||||||
$( '.beat-manual' ).fadeIn( 50 );
|
$( '.beat-manual' ).fadeIn( 50 );
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
$( '.beat-manual' ).fadeOut( duration );
|
$( '.beat-manual' ).fadeOut( duration );
|
||||||
@@ -202,30 +257,31 @@
|
|||||||
$( '.beat-manual' ).stop();
|
$( '.beat-manual' ).stop();
|
||||||
}, duration );
|
}, duration );
|
||||||
}, 50 );
|
}, 50 );
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleAnimationChange = () => {
|
const handleAnimationChange = () => {
|
||||||
if ( visualizationSettings.value === 'mic' && isPlaying.value ) {
|
if ( visualizationSettings.value === 'mic' && isPlaying.value ) {
|
||||||
bizualizer.subscribeToBeatUpdate( animateBeat );
|
bizualizer.subscribeToBeatUpdate( animateBeat );
|
||||||
} else {
|
} else {
|
||||||
bizualizer.unsubscribeFromBeatUpdate()
|
bizualizer.unsubscribeFromBeatUpdate();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const setBackground = () => {
|
const setBackground = () => {
|
||||||
bizualizer.createBackground().then( bg => {
|
bizualizer.createBackground().then( bg => {
|
||||||
$( '#background' ).css( 'background', bg );
|
$( '#background' ).css( 'background', bg );
|
||||||
} );
|
} );
|
||||||
}
|
};
|
||||||
|
|
||||||
const notifier = () => {
|
const notifier = () => {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
|
|
||||||
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
||||||
|
|
||||||
// Detect if window is currently in focus
|
// Detect if window is currently in focus
|
||||||
window.onblur = () => {
|
window.onblur = () => {
|
||||||
sendNotification();
|
sendNotification();
|
||||||
}
|
};
|
||||||
|
|
||||||
// Detect if browser window becomes hidden (also with blur event)
|
// Detect if browser window becomes hidden (also with blur event)
|
||||||
document.onvisibilitychange = () => {
|
document.onvisibilitychange = () => {
|
||||||
@@ -233,18 +289,19 @@
|
|||||||
sendNotification();
|
sendNotification();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendNotification = () => {
|
const sendNotification = () => {
|
||||||
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
||||||
body: 'Please return to the original webpage immediately!',
|
'body': 'Please return to the original webpage immediately!',
|
||||||
requireInteraction: true,
|
'requireInteraction': true,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
conn.emit( 'tampering', '' );
|
conn.emit( 'tampering', '' );
|
||||||
}
|
};
|
||||||
|
|
||||||
const isShowingSecureModeInfo = ref( false );
|
const isShowingSecureModeInfo = ref( false );
|
||||||
|
|
||||||
const secureModeInfo = ( action: string ) => {
|
const secureModeInfo = ( action: string ) => {
|
||||||
if ( action === 'toggle' ) {
|
if ( action === 'toggle' ) {
|
||||||
isShowingSecureModeInfo.value = !isShowingSecureModeInfo.value;
|
isShowingSecureModeInfo.value = !isShowingSecureModeInfo.value;
|
||||||
@@ -253,7 +310,7 @@
|
|||||||
} else {
|
} else {
|
||||||
isShowingSecureModeInfo.value = false;
|
isShowingSecureModeInfo.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -535,4 +592,4 @@
|
|||||||
transform: rotate( 360deg );
|
transform: rotate( 360deg );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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": [
|
"include": [
|
||||||
"vite.config.*",
|
"vite.config.*",
|
||||||
"vitest.config.*",
|
"vitest.config.*",
|
||||||
"cypress.config.*",
|
|
||||||
"nightwatch.conf.*",
|
"nightwatch.conf.*",
|
||||||
"playwright.config.*"
|
"playwright.config.*"
|
||||||
],
|
],
|
||||||
@@ -14,6 +13,6 @@
|
|||||||
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"types": ["node"]
|
"types": ["node", "jquery"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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">
|
<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>
|
<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,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
const app = require( './dist/app.js' ).default;
|
const app = require( './dist/app.js' ).default;
|
||||||
app.run();
|
app.run();
|
||||||
|
|||||||
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",
|
"name": "musicplayer-v2-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "The backend for MusicPlayerV2",
|
"description": "The backend for MusicPlayerV2",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/simplePCBuilding/MusicPlayerV2.git"
|
"url": "git+https://github.com/simplePCBuilding/MusicPlayerV2.git"
|
||||||
},
|
},
|
||||||
"author": "Janis Hutz",
|
"author": "Janis Hutz",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/simplePCBuilding/MusicPlayerV2/issues"
|
"url": "https://github.com/simplePCBuilding/MusicPlayerV2/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/simplePCBuilding/MusicPlayerV2#readme",
|
"homepage": "https://github.com/simplePCBuilding/MusicPlayerV2#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express-session": "^1.18.0",
|
"@eslint/js": "^9.29.0",
|
||||||
"typescript": "^5.4.5"
|
"@stylistic/eslint-plugin": "^5.0.0",
|
||||||
},
|
"@types/express-session": "^1.18.0",
|
||||||
"dependencies": {
|
"eslint-plugin-vue": "^10.2.0",
|
||||||
"@types/body-parser": "^1.19.5",
|
"typescript": "^5.4.5",
|
||||||
"@types/cors": "^2.8.17",
|
"typescript-eslint": "^8.35.0"
|
||||||
"@types/express": "^4.17.21",
|
},
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"dependencies": {
|
||||||
"body-parser": "^1.20.2",
|
"@janishutz/login-sdk-server": "^1.2.0",
|
||||||
"cors": "^2.8.5",
|
"@janishutz/login-sdk-server-stubs": "^1.0.0",
|
||||||
"express": "^4.19.2",
|
"@janishutz/store-sdk": "^1.1.0",
|
||||||
"express-session": "^1.18.0",
|
"@types/body-parser": "^1.19.5",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"@types/cors": "^2.8.17",
|
||||||
"node-mysql": "^0.4.2",
|
"@types/express": "^4.17.21",
|
||||||
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"socket.io": "^4.7.5",
|
"body-parser": "^1.20.2",
|
||||||
"store.janishutz.com-sdk": "file:../../store/sdk/dist"
|
"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 fs from 'fs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import account from './account';
|
import {
|
||||||
import sdk from 'oauth-janishutz-client-server';
|
createServer
|
||||||
import { createServer } from 'node:http';
|
} from 'node:http';
|
||||||
import { Server } from 'socket.io';
|
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import type { Room, Song } from './definitions';
|
import {
|
||||||
import storeSDK from 'store.janishutz.com-sdk';
|
SocketData
|
||||||
import bodyParser from 'body-parser';
|
} from './definitions';
|
||||||
|
|
||||||
const isFossVersion = true;
|
|
||||||
|
|
||||||
declare let __dirname: string | undefined
|
// ┌ ┐
|
||||||
if ( typeof( __dirname ) === 'undefined' ) {
|
// │ Handle FOSS vs paid version │
|
||||||
__dirname = path.resolve( path.dirname( '' ) );
|
// └ ┘
|
||||||
}
|
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 = () => {
|
const run = () => {
|
||||||
let app = express();
|
const 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 httpServer = createServer( app );
|
const httpServer = createServer( app );
|
||||||
|
|
||||||
if ( !isFossVersion ) {
|
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
|
// Load id.janishutz.com SDK and allow signing in
|
||||||
sdk.routes( app, ( uid: string ) => {
|
sdk.setUp(
|
||||||
return new Promise( ( resolve, reject ) => {
|
{
|
||||||
account.checkUser( uid ).then( stat => {
|
'prod': false,
|
||||||
resolve( stat );
|
'service': {
|
||||||
} ).catch( e => {
|
'serviceID': 'jh-music',
|
||||||
reject( e );
|
'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 => {
|
||||||
return new Promise( ( resolve, reject ) => {
|
resolve( true );
|
||||||
account.createUser( uid, username, email ).then( stat => {
|
|
||||||
resolve( stat );
|
|
||||||
} ).catch( e => {
|
|
||||||
reject( e );
|
|
||||||
} );
|
} );
|
||||||
} );
|
},
|
||||||
}, 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 ) => {
|
Configuration of SSE or WebSocket
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
// Connected clients have their session ID as key
|
const socketData: SocketData = {};
|
||||||
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' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
|
sse.useSSE( app, socketData, corsOpts, sdk.getSessionID, sdk.getSignedIn );
|
||||||
|
socket.useWebSocket( httpServer, socketData );
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GENERAL ROUTES
|
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' );
|
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 ) ?? '';
|
const roomName = String( request.query.roomName ) ?? '';
|
||||||
|
|
||||||
if ( !socketData[ roomName ] ) {
|
if ( !socketData[ roomName ] ) {
|
||||||
const roomToken = crypto.randomUUID();
|
const roomToken = crypto.randomUUID();
|
||||||
|
|
||||||
socketData[ roomName ] = {
|
socketData[ roomName ] = {
|
||||||
playbackStart: 0,
|
'playbackStart': 0,
|
||||||
playbackStatus: false,
|
'playbackStatus': false,
|
||||||
playlist: [],
|
'playlist': [],
|
||||||
playlistIndex: 0,
|
'playlistIndex': 0,
|
||||||
roomName: roomName,
|
'roomName': roomName,
|
||||||
roomToken: roomToken,
|
'roomToken': roomToken,
|
||||||
ownerUID: sdk.getUserData( request ).uid,
|
'ownerUID': sdk.getUID( request ),
|
||||||
useAntiTamper: request.query.useAntiTamper === 'true' ? true : false,
|
'useAntiTamper': request.query.useAntiTamper === 'true'
|
||||||
|
? true : false,
|
||||||
};
|
};
|
||||||
response.send( roomToken );
|
response.send( roomToken );
|
||||||
} else {
|
} else {
|
||||||
if ( socketData[ roomName ].ownerUID === sdk.getUserData( request ).uid ) {
|
if (
|
||||||
|
socketData[ roomName ].ownerUID
|
||||||
|
=== sdk.getUID( request )
|
||||||
|
) {
|
||||||
response.send( socketData[ roomName ].roomToken );
|
response.send( socketData[ roomName ].roomToken );
|
||||||
} else {
|
} else {
|
||||||
response.status( 409 ).send( 'ERR_CONFLICT' );
|
response.status( 409 ).send( 'ERR_CONFLICT' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
response.status( 403 ).send( 'ERR_FORBIDDEN' );
|
|
||||||
}
|
}
|
||||||
} );
|
);
|
||||||
|
|
||||||
|
|
||||||
app.get( '/getAppleMusicDevToken', ( req, res ) => {
|
app.get(
|
||||||
// sign dev token
|
'/getAppleMusicDevToken',
|
||||||
const privateKey = fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString();
|
cors( corsOpts ),
|
||||||
// TODO: Remove secret
|
sdk.loginCheck(),
|
||||||
const config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/apple-music-api.config.secret.json' ) ) );
|
( req, res ) => {
|
||||||
const now = new Date().getTime();
|
checkIfOwned( req )
|
||||||
const tomorrow = now + 24 * 3600 * 1000;
|
.then( owned => {
|
||||||
const jwtToken = jwt.sign( {
|
if ( owned ) {
|
||||||
'iss': config.teamID,
|
// sign dev token
|
||||||
'iat': Math.floor( now / 1000 ),
|
const privateKey = fs.readFileSync( path.join(
|
||||||
'exp': Math.floor( tomorrow / 1000 ),
|
__dirname,
|
||||||
}, privateKey, {
|
'/config/apple_private_key.p8'
|
||||||
algorithm: "ES256",
|
) ).toString();
|
||||||
keyid: config.keyID
|
const config = JSON.parse( fs.readFileSync( path.join(
|
||||||
} );
|
__dirname,
|
||||||
res.send( jwtToken );
|
'/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
|
res.send( jwtToken );
|
||||||
|
} else {
|
||||||
app.get( '/checkUserStatus', ( request: express.Request, response: express.Response ) => {
|
res.status( 402 ).send( 'ERR_NOT_OWNED' );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
} )
|
||||||
if ( owned ) {
|
.catch( e => {
|
||||||
response.send( 'ok' );
|
if ( e === 'ERR_NOT_OWNED' ) {
|
||||||
} else {
|
res.status( 402 ).send( e );
|
||||||
response.send( 'ERR_NOT_OWNED' );
|
} else if ( e === 'ERR_AUTH_REQUIRED' ) {
|
||||||
}
|
res.status( 401 ).send( e );
|
||||||
} ).catch( e => {
|
} else {
|
||||||
console.error( e );
|
res.send( 500 ).send( e );
|
||||||
response.status( 404 ).send( 'ERR_NOT_OWNED' );
|
}
|
||||||
} );
|
} );
|
||||||
} else {
|
|
||||||
response.status( 401 ).send( 'ERR_AUTH_REQUIRED' );
|
|
||||||
}
|
}
|
||||||
} );
|
);
|
||||||
|
|
||||||
app.use( ( request: express.Request, response: express.Response, next: express.NextFunction ) => {
|
|
||||||
response.status( 404 ).send( 'ERR_NOT_FOUND' );
|
const ownedCache = {};
|
||||||
// response.sendFile( path.join( __dirname + '' ) )
|
|
||||||
|
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;
|
const PORT = process.env.PORT || 8082;
|
||||||
|
|
||||||
httpServer.listen( PORT );
|
httpServer.listen( PORT );
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
run
|
run
|
||||||
}
|
};
|
||||||
|
|||||||
33
backend/src/definitions.d.ts
vendored
33
backend/src/definitions.d.ts
vendored
@@ -1,18 +1,23 @@
|
|||||||
export interface Room {
|
export interface Room {
|
||||||
playbackStatus: boolean;
|
'playbackStatus': boolean;
|
||||||
playbackStart: number;
|
'playbackStart': number;
|
||||||
playlist: Song[];
|
'playlist': Song[];
|
||||||
playlistIndex: number;
|
'playlistIndex': number;
|
||||||
roomName: string;
|
'roomName': string;
|
||||||
roomToken: string;
|
'roomToken': string;
|
||||||
ownerUID: string;
|
'ownerUID': string;
|
||||||
useAntiTamper: boolean;
|
'useAntiTamper': boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Song {
|
export interface Song {
|
||||||
title: string;
|
'title': string;
|
||||||
artist: string;
|
'artist': string;
|
||||||
duration: number;
|
'duration': number;
|
||||||
cover: string;
|
'cover': string;
|
||||||
additionalInfo?: 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 ) => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
return [ {
|
const getSubscriptions = ( _uid: string ) => {
|
||||||
'id': 'com.janishutz.MusicPlayer.subscription',
|
return [
|
||||||
'expires': new Date().getTime() + 200000,
|
{
|
||||||
'status': true
|
'id': 'com.janishutz.MusicPlayer.subscription',
|
||||||
} ];
|
'expires': new Date().getTime() + 200000,
|
||||||
}
|
'status': true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getSubscriptions,
|
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,20 +11,20 @@ import path from 'path';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as sqlDB from './mysqldb.js';
|
import * as sqlDB from './mysqldb.js';
|
||||||
|
|
||||||
declare let __dirname: string | undefined
|
declare let __dirname: string | undefined;
|
||||||
if ( typeof( __dirname ) === 'undefined' ) {
|
|
||||||
|
if ( typeof __dirname === 'undefined' ) {
|
||||||
__dirname = path.resolve( path.dirname( '' ) );
|
__dirname = path.resolve( path.dirname( '' ) );
|
||||||
} else {
|
} else {
|
||||||
__dirname = __dirname + '/../';
|
__dirname = __dirname + '/../';
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbRef = {
|
const dbRef = {
|
||||||
'user': 'music_users',
|
'user': 'music_users',
|
||||||
'users': 'music_users',
|
'users': 'music_users',
|
||||||
};
|
};
|
||||||
|
const dbh = new sqlDB.SQLDB();
|
||||||
|
|
||||||
|
|
||||||
let dbh = new sqlDB.SQLDB();
|
|
||||||
dbh.connect();
|
dbh.connect();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +32,7 @@ dbh.connect();
|
|||||||
* @returns {undefined}
|
* @returns {undefined}
|
||||||
*/
|
*/
|
||||||
const initDB = (): undefined => {
|
const initDB = (): undefined => {
|
||||||
( async() => {
|
( async () => {
|
||||||
console.log( '[ DB ] Setting up...' );
|
console.log( '[ DB ] Setting up...' );
|
||||||
dbh.setupDB();
|
dbh.setupDB();
|
||||||
console.log( '[ DB ] Setting up complete!' );
|
console.log( '[ DB ] Setting up complete!' );
|
||||||
@@ -48,11 +48,16 @@ const initDB = (): undefined => {
|
|||||||
*/
|
*/
|
||||||
const getDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
|
const getDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
|
||||||
return new Promise( ( resolve, reject ) => {
|
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 );
|
resolve( data );
|
||||||
} ).catch( error => {
|
} )
|
||||||
reject( error );
|
.catch( error => {
|
||||||
} );
|
reject( error );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,34 +71,36 @@ 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
|
* @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.
|
* @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):
|
LeftJoin (Select values in first table and return all corresponding values of second table):
|
||||||
- operation.property (the column to search for the value),
|
- operation.property (the column to search for the value),
|
||||||
- operation.searchQuery (the value to search for [will be sanitised by method])
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
- operation.columns (The columns of both tables to be selected, list of objects: { 'db': TABLE NAME, 'column': COLUMN NAME })
|
- operation.columns (The columns of both tables to be selected, list of objects: { 'db': TABLE NAME, 'column': COLUMN NAME })
|
||||||
- operation.secondTable (The second table to perform Join operation with)
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||||
*/
|
*/
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
let settings = {
|
const settings = {
|
||||||
'command': 'LeftJoin',
|
'command': 'LeftJoin',
|
||||||
'property': column,
|
'property': column,
|
||||||
'searchQuery': searchQuery,
|
'searchQuery': searchQuery,
|
||||||
'selection': '',
|
'selection': '',
|
||||||
'secondTable': dbRef[ secondTable ],
|
'secondTable': dbRef[ secondTable ],
|
||||||
'matchingParam': dbRef[ db ] + '.' + nameOfMatchingParam + '=' + dbRef[ secondTable ] + '.' + nameOfMatchingParam,
|
'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 += dbRef[ columns[ el ].db ] + '.' + columns[ el ].column + ',';
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.selection = settings.selection.slice( 0, settings.selection.length - 1 );
|
settings.selection = settings.selection.slice( 0, settings.selection.length - 1 );
|
||||||
dbh.query( settings, dbRef[ db ] ).then( data => {
|
dbh.query( settings, dbRef[ db ] ).then( data => {
|
||||||
resolve( 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
|
* @param {string} db The database of which all data should be retrieved
|
||||||
* @returns {Promise<object>} Returns an object containing all data
|
* @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 ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
dbh.query( { 'command': 'getAllData' }, dbRef[ db ] ).then( data => {
|
dbh.query( {
|
||||||
|
'command': 'getAllData'
|
||||||
|
}, dbRef[ db ] ).then( data => {
|
||||||
resolve( 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
|
* @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.
|
* @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 ) => {
|
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 ) {
|
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 );
|
resolve( dat );
|
||||||
} ).catch( error => {
|
} )
|
||||||
reject( error );
|
.catch( error => {
|
||||||
} );
|
reject( error );
|
||||||
|
} );
|
||||||
} else {
|
} else {
|
||||||
dbh.query( { 'command': 'addData', 'data': data }, dbRef[ db ] ).then( dat => {
|
dbh.query( {
|
||||||
|
'command': 'addData',
|
||||||
|
'data': data
|
||||||
|
}, dbRef[ db ] ).then( dat => {
|
||||||
resolve( 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> => {
|
const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
|
||||||
return new Promise( ( resolve, reject ) => {
|
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 );
|
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> => {
|
const checkDataAvailability = ( db: string, column: string, searchQuery: string ): Promise<boolean> => {
|
||||||
return new Promise( ( resolve, reject ) => {
|
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 ) {
|
if ( res.length > 0 ) {
|
||||||
resolve( true );
|
resolve( true );
|
||||||
} else {
|
} else {
|
||||||
resolve( false );
|
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
|
* @returns {Promise<object>} Returns the data from all files
|
||||||
*/
|
*/
|
||||||
const getJSONDataBatch = async ( files: Array<string> ): Promise<object> => {
|
const getJSONDataBatch = async ( files: Array<string> ): Promise<object> => {
|
||||||
let allFiles = {};
|
const allFiles = {};
|
||||||
for ( let file in files ) {
|
|
||||||
|
for ( const file in files ) {
|
||||||
try {
|
try {
|
||||||
allFiles[ files[ file ] ] = await getJSONData( files[ file ] );
|
allFiles[ files[ file ] ] = await getJSONData( files[ file ] );
|
||||||
} catch( err ) {
|
} catch ( err ) {
|
||||||
allFiles[ files[ file ] ] = 'ERROR: ' + err;
|
allFiles[ files[ file ] ] = 'ERROR: ' + err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allFiles;
|
return allFiles;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all data from a JSON file
|
* 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)
|
* @param {string} file The file to be loaded (path relative to root)
|
||||||
* @returns {object} Returns the JSON file
|
* @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() );
|
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 );
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
} else {
|
} else {
|
||||||
let dat = {};
|
let dat = {};
|
||||||
|
|
||||||
if ( data.byteLength > 0 ) {
|
if ( data.byteLength > 0 ) {
|
||||||
dat = JSON.parse( data.toString() ) ?? {};
|
dat = JSON.parse( data.toString() ) ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
dat[ identifier ] = values;
|
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 ) {
|
if ( error ) {
|
||||||
reject( 'Error occurred: Error trace: ' + error );
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve( true );
|
resolve( true );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
@@ -286,7 +326,7 @@ const writeJSONDataSimple = ( db: string, identifier: string, values: any ) => {
|
|||||||
*/
|
*/
|
||||||
const writeJSONData = ( db: string, data: object ): Promise<boolean> => {
|
const writeJSONData = ( db: string, data: object ): Promise<boolean> => {
|
||||||
return new Promise( ( resolve, reject ) => {
|
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 ) {
|
if ( error ) {
|
||||||
reject( 'Error occurred: Error trace: ' + error );
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
} else {
|
} else {
|
||||||
@@ -309,14 +349,17 @@ const deleteJSONDataSimple = ( db: string, identifier: string ): Promise<boolean
|
|||||||
reject( 'Error occurred: Error trace: ' + error );
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
} else {
|
} else {
|
||||||
let dat = {};
|
let dat = {};
|
||||||
|
|
||||||
if ( data.byteLength > 0 ) {
|
if ( data.byteLength > 0 ) {
|
||||||
dat = JSON.parse( data.toString() ) ?? {};
|
dat = JSON.parse( data.toString() ) ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
delete dat[ identifier ];
|
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 ) {
|
if ( error ) {
|
||||||
reject( 'Error occurred: Error trace: ' + error );
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve( true );
|
resolve( true );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
@@ -324,7 +367,19 @@ const deleteJSONDataSimple = ( db: string, identifier: string ): Promise<boolean
|
|||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { initDB, checkDataAvailability, deleteDataSimple, deleteJSONDataSimple, getData,
|
export default {
|
||||||
getDataSimple, getDataWithLeftJoinFunction, getJSONData, getJSONDataBatch, getJSONDataSimple,
|
initDB,
|
||||||
getJSONDataSync, writeDataSimple, writeJSONData, writeJSONDataSimple
|
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 fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
declare let __dirname: string | undefined
|
declare let __dirname: string | undefined;
|
||||||
if ( typeof( __dirname ) === 'undefined' ) {
|
|
||||||
|
if ( typeof __dirname === 'undefined' ) {
|
||||||
__dirname = path.resolve( path.dirname( '' ) );
|
__dirname = path.resolve( path.dirname( '' ) );
|
||||||
} else {
|
} else {
|
||||||
__dirname = __dirname + '/../';
|
__dirname = __dirname + '/../';
|
||||||
@@ -22,21 +23,35 @@ if ( typeof( __dirname ) === 'undefined' ) {
|
|||||||
// to the whitelist of the database
|
// to the whitelist of the database
|
||||||
|
|
||||||
class SQLConfig {
|
class SQLConfig {
|
||||||
|
|
||||||
command: string;
|
command: string;
|
||||||
|
|
||||||
property?: string;
|
property?: string;
|
||||||
|
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
|
|
||||||
selection?: string;
|
selection?: string;
|
||||||
|
|
||||||
query?: string;
|
query?: string;
|
||||||
|
|
||||||
newValues?: object;
|
newValues?: object;
|
||||||
|
|
||||||
secondTable?: string;
|
secondTable?: string;
|
||||||
|
|
||||||
matchingParam?: string;
|
matchingParam?: string;
|
||||||
|
|
||||||
data?: object;
|
data?: object;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SQLDB {
|
class SQLDB {
|
||||||
|
|
||||||
sqlConnection: mysql.Connection;
|
sqlConnection: mysql.Connection;
|
||||||
|
|
||||||
isRecovering: boolean;
|
isRecovering: boolean;
|
||||||
|
|
||||||
config: object;
|
config: object;
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/db.config.secret.json' ) ) );
|
this.config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/db.config.secret.json' ) ) );
|
||||||
this.sqlConnection = mysql.createConnection( this.config );
|
this.sqlConnection = mysql.createConnection( this.config );
|
||||||
@@ -46,19 +61,23 @@ class SQLDB {
|
|||||||
connect () {
|
connect () {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if ( this.isRecovering ) {
|
if ( this.isRecovering ) {
|
||||||
console.log( '[ SQL ] Attempting to recover from critical error' );
|
console.log( '[ SQL ] Attempting to recover from critical error' );
|
||||||
this.sqlConnection = mysql.createConnection( this.config );
|
this.sqlConnection = mysql.createConnection( this.config );
|
||||||
this.isRecovering = false;
|
this.isRecovering = false;
|
||||||
}
|
}
|
||||||
this.sqlConnection.connect( ( err ) => {
|
|
||||||
|
this.sqlConnection.connect( err => {
|
||||||
if ( err ) {
|
if ( err ) {
|
||||||
console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
|
console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
|
||||||
reject( err );
|
reject( err );
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log( '[ SQL ] Connected to database successfully' );
|
console.log( '[ SQL ] Connected to database successfully' );
|
||||||
self.sqlConnection.on( 'error', ( err ) => {
|
self.sqlConnection.on( 'error', err => {
|
||||||
if ( err.code === 'ECONNRESET' ) {
|
if ( err.code === 'ECONNRESET' ) {
|
||||||
self.isRecovering = true;
|
self.isRecovering = true;
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
@@ -81,22 +100,24 @@ class SQLDB {
|
|||||||
async setupDB () {
|
async setupDB () {
|
||||||
this.sqlConnection.query( 'SELECT @@default_storage_engine;', ( error, results ) => {
|
this.sqlConnection.query( 'SELECT @@default_storage_engine;', ( error, results ) => {
|
||||||
if ( error ) throw error;
|
if ( error ) throw error;
|
||||||
|
|
||||||
if ( results[ 0 ][ '@@default_storage_engine' ] !== 'InnoDB' ) throw 'DB HAS TO USE InnoDB!';
|
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;
|
if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error;
|
||||||
|
|
||||||
return 'DONE';
|
return 'DONE';
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
query ( operation: SQLConfig, table: string ): Promise<Array<Object>> {
|
query ( operation: SQLConfig, table: string ): Promise<Array<object>> {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
/*
|
/*
|
||||||
Possible operation.command values (all need the table argument of the method call):
|
Possible operation.command values (all need the table argument of the method call):
|
||||||
- getAllData: no additional instructions needed
|
- getAllData: no additional instructions needed
|
||||||
|
|
||||||
- getFilteredData:
|
- getFilteredData:
|
||||||
- operation.property (the column to search for the value),
|
- operation.property (the column to search for the value),
|
||||||
- operation.searchQuery (the value to search for [will be sanitised by method])
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
- InnerJoin (Select values that match in both tables):
|
- InnerJoin (Select values that match in both tables):
|
||||||
@@ -106,15 +127,15 @@ class SQLDB {
|
|||||||
- operation.secondTable (The second table to perform Join operation with)
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||||
|
|
||||||
- LeftJoin (Select values in first table and return all corresponding values of second table):
|
- LeftJoin (Select values in first table and return all corresponding values of second table):
|
||||||
- operation.property (the column to search for the value),
|
- operation.property (the column to search for the value),
|
||||||
- operation.searchQuery (the value to search for [will be sanitised by method])
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
- operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id)
|
- operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id)
|
||||||
- operation.secondTable (The second table to perform Join operation with)
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||||
|
|
||||||
- RightJoin (Select values in second table and return all corresponding values of first table):
|
- RightJoin (Select values in second table and return all corresponding values of first table):
|
||||||
- operation.property (the column to search for the value),
|
- operation.property (the column to search for the value),
|
||||||
- operation.searchQuery (the value to search for [will be sanitised by method])
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
- operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id)
|
- operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id)
|
||||||
- operation.secondTable (The second table to perform Join operation with)
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
@@ -122,25 +143,26 @@ class SQLDB {
|
|||||||
|
|
||||||
- addData:
|
- addData:
|
||||||
- operation.data (key-value pair with all data as values and column to insert into as key)
|
- operation.data (key-value pair with all data as values and column to insert into as key)
|
||||||
|
|
||||||
- deleteData:
|
- deleteData:
|
||||||
- operation.property (the column to search for the value)
|
- operation.property (the column to search for the value)
|
||||||
- operation.searchQuery (the value to search for [will be sanitised by method])
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
- updateData:
|
- updateData:
|
||||||
- operation.newValues (a object with keys being the column and value being the value to be inserted into that column, values are being
|
- operation.newValues (a object with keys being the column and value being the value to be inserted into that column, values are being
|
||||||
sanitised by the function)
|
sanitised by the function)
|
||||||
- operation.property (the column to search for the value),
|
- operation.property (the column to search for the value),
|
||||||
- operation.searchQuery (the value to search for [will be sanitised by method])
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
- checkDataAvailability:
|
- checkDataAvailability:
|
||||||
- operation.property (the column to search for the value),
|
- operation.property (the column to search for the value),
|
||||||
- operation.searchQuery (the value to search for [will be sanitised by method])
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
- fullCustomCommand:
|
- fullCustomCommand:
|
||||||
- operation.query (the SQL instruction to be executed) --> NOTE: This command will not be sanitised, so use only with proper sanitisation!
|
- operation.query (the SQL instruction to be executed) --> NOTE: This command will not be sanitised, so use only with proper sanitisation!
|
||||||
*/
|
*/
|
||||||
let command = '';
|
let command = '';
|
||||||
|
|
||||||
if ( operation.command === 'getAllData' ) {
|
if ( operation.command === 'getAllData' ) {
|
||||||
command = 'SELECT * FROM ' + table;
|
command = 'SELECT * FROM ' + table;
|
||||||
} else if ( operation.command === 'getFilteredData' || operation.command === 'checkDataAvailability' ) {
|
} else if ( operation.command === 'getFilteredData' || operation.command === 'checkDataAvailability' ) {
|
||||||
@@ -150,19 +172,23 @@ class SQLDB {
|
|||||||
} else if ( operation.command === 'addData' ) {
|
} else if ( operation.command === 'addData' ) {
|
||||||
let keys = '';
|
let keys = '';
|
||||||
let values = '';
|
let values = '';
|
||||||
for ( let key in operation.data ) {
|
|
||||||
|
for ( const key in operation.data ) {
|
||||||
keys += String( key ) + ', ';
|
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' ) {
|
} else if ( operation.command === 'updateData' ) {
|
||||||
if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' );
|
if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' );
|
||||||
else {
|
else {
|
||||||
command = 'UPDATE ' + table + ' SET ';
|
command = 'UPDATE ' + table + ' SET ';
|
||||||
let updatedValues = '';
|
let updatedValues = '';
|
||||||
for ( let value in operation.newValues ) {
|
|
||||||
|
for ( const value in operation.newValues ) {
|
||||||
updatedValues += value + ' = ' + this.sqlConnection.escape( String( operation.newValues[ value ] ) ) + ', ';
|
updatedValues += value + ' = ' + this.sqlConnection.escape( String( operation.newValues[ value ] ) ) + ', ';
|
||||||
}
|
}
|
||||||
|
|
||||||
command += updatedValues.slice( 0, updatedValues.length - 2 );
|
command += updatedValues.slice( 0, updatedValues.length - 2 );
|
||||||
command += ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
command += ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||||
}
|
}
|
||||||
@@ -178,12 +204,17 @@ class SQLDB {
|
|||||||
} else if ( operation.command === 'RightJoin' ) {
|
} 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 );
|
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 ) => {
|
this.sqlConnection.query( command, ( error, results ) => {
|
||||||
if ( error ) reject( error );
|
if ( error ) reject( error );
|
||||||
|
|
||||||
resolve( results );
|
resolve( results );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { SQLConfig, SQLDB };
|
export {
|
||||||
|
SQLConfig, SQLDB
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "MusicPlayer",
|
"name": "MusicPlayer",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"private": false,
|
"private": false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user