83 Commits
V1.0.0 ... dev

Author SHA1 Message Date
d7433e95a4 Prepare for rewrite 2026-01-08 12:42:42 +01:00
576b6f9490 Fix some errors, style updates 2025-11-22 22:37:36 +01:00
e0dcfa6964 Finish bar utility 2025-11-22 17:43:25 +01:00
Janis Hutz
69d2db8c37 Update BarView.vue
Fix bar config caching
2025-11-22 12:40:11 +00:00
9a347c9206 Bar utility: Fix error 2025-11-21 17:20:54 +01:00
60ea4669ab Bar utility: some updates 2025-11-21 17:18:16 +01:00
669cc620bf Fixes for uncommon songs with that are not on apple music 2025-11-20 17:15:39 +01:00
64d086dec4 Format 2025-11-20 16:55:41 +01:00
1d714da494 Player: Improve custom song loading song data resolution 2025-11-20 16:38:31 +01:00
d63df5898b Fix remotes 2025-11-06 15:13:52 +01:00
bd18636141 Fix custom playlist loading error 2025-11-06 15:09:55 +01:00
51f0b5639a Fix authentication button glitch 2025-11-02 11:24:53 +01:00
6a6b06a994 Fix auth errors 2025-11-02 11:23:48 +01:00
e2e27e640f Remove trailing comma from json 2025-10-30 11:08:49 +01:00
ac575adedb Symbol change complete 2025-10-30 11:05:16 +01:00
d15366e572 More symbol error fixes 2025-10-30 11:03:47 +01:00
25cfc80925 Fix wrong symbols 2025-10-30 11:01:42 +01:00
952ef0ee85 Address vulnerability in vite 2025-10-30 10:58:17 +01:00
80c497b80a Change to using HTML symbols for light & dark mode for better compat 2025-10-30 10:56:58 +01:00
8778740454 Bar system, add note for age check 2025-10-13 14:03:04 +02:00
74075c2919 Finish up refactor 2025-09-28 16:01:39 +02:00
3e13a4ebf5 More fixes, sdk stubs need update for this to work 2025-09-28 14:59:05 +02:00
84b640ee5e Finish up migration, currently not setup for foss version 2025-09-28 14:33:13 +02:00
0315241d76 Integrate new account backend 2025-09-28 14:23:31 +02:00
6e93cfdf2c Prepare for new sdk 2025-09-15 11:17:17 +02:00
6b9d556e57 Update bar utility 2025-09-08 15:24:02 +02:00
025c7ed111 Add bar utility 2025-09-04 16:34:54 +02:00
057ea67a95 Update eslint config 2025-09-04 16:26:42 +02:00
16543fb577 Update deps 2025-09-04 16:13:24 +02:00
f0c538126d Small bugfix, security patch 2025-06-25 10:51:16 +02:00
b0a1f9a538 fix small bug 2024-11-21 15:42:05 +01:00
Janis Hutz
9fac37203d Update README.md
Remove not available yet note for hosted version
2024-10-23 15:54:59 +02:00
b10aeed1f8 fix queue index jumping when enabling shuffle 2024-10-19 15:18:34 +02:00
b8faeef189 actually fix it now 2024-10-19 15:12:53 +02:00
4ef98f80cf fix playback pos update on song change 2024-10-19 15:05:34 +02:00
c9f487b981 security updates 2024-10-19 14:29:36 +02:00
dc2327af33 fix player playback pos update 2024-10-19 14:24:22 +02:00
Janis Hutz
360cc7c206 Create package.json 2024-10-16 10:58:13 +02:00
Janis Hutz
7e8170c794 Merge V3 Dev Branch
V3
2024-10-16 10:55:43 +02:00
Janis Hutz
83b800ad96 Merge branch 'master' into V2.1 2024-10-16 10:55:13 +02:00
52d7fa7de7 finish up FOSS version (mostly) 2024-10-02 15:59:42 +02:00
a2f6757d84 some more stubs, remove old app 2024-10-02 15:56:05 +02:00
da01326777 plan sdk stubs 2024-09-19 14:37:08 +02:00
Janis Hutz
c77bfc8336 Update README.md
Remove Tokei
2024-09-15 17:51:34 +00:00
372ebf1057 improved crash recovery 2024-09-13 13:58:04 +02:00
6cf9e72263 small UI fix 2024-08-21 17:10:18 +02:00
b989111849 final fixes 2024-08-19 13:36:49 +02:00
bd2e4cdac4 fix polyfills 2024-08-16 16:15:45 +02:00
4ae05bd060 small fix 2024-08-15 14:40:48 +02:00
e3e4bfc8bd small fixes 2024-08-14 15:09:01 +02:00
f8f0b27d52 finish for deploy 2024-08-14 14:30:29 +02:00
1567640634 fix logout stuff 2024-08-14 14:30:14 +02:00
070e9d158b update README.md 2024-08-14 14:27:07 +02:00
8d9607dbc6 finish for deploy 2024-08-14 14:24:45 +02:00
95170194af complete (foss version release expected in October) 2024-08-14 13:56:49 +02:00
e54f5178a1 basically done 2024-08-14 12:18:24 +02:00
a2711b76dd some marketing changes 2024-08-02 11:19:41 +02:00
d7f36b8e07 some updates to login & subscription 2024-08-01 08:11:59 +02:00
a5e2520d28 some marketing + other changes & fixes 2024-07-21 08:21:09 +02:00
221ae67ec2 start writing marketing materials 2024-07-18 17:45:42 +02:00
361523172d some more small updates 2024-06-30 17:32:42 +02:00
88ecea1761 Design changes 2024-06-30 14:46:22 +02:00
c3bff192bb some small updates 2024-06-30 12:10:57 +02:00
1c4aace806 basically done 2024-06-29 15:48:04 +02:00
429bb53f36 basically done (at least the essential part) 2024-06-29 12:05:50 +02:00
f314732f3f some small updates 2024-06-28 15:27:11 +02:00
7a42ab8b4e start adding showcase screen 2024-06-27 18:39:06 +02:00
1e11f1dc2e start integrating websocket, player basically done 2024-06-27 16:50:03 +02:00
76f543eb2f mostly complete base spec player 2024-06-26 17:44:07 +02:00
4ecf93d31b add login sdk 2024-06-26 09:52:53 +02:00
ed63ca77d6 almost complete player 2024-06-25 17:59:01 +02:00
b2d8180bb9 technically working player 2024-06-25 14:17:04 +02:00
1ffdc873a7 some progress on player + playlist loading 2024-06-25 11:45:11 +02:00
56a714ab9e some progress on the player 2024-06-24 15:23:40 +02:00
8f5fce8b97 small restructuring of player 2024-06-17 09:19:45 +02:00
ce82014826 some progress, interrupted because MusicKit bugs 2024-06-11 13:41:03 +02:00
17225d07bc some more progress on player 2024-06-11 11:00:41 +02:00
15d59d9cee some progress on player 2024-06-11 09:45:12 +02:00
28ae628f4d start implementing login 2024-06-10 14:42:08 +02:00
Janis Hutz
c903481739 Update README.md 2024-06-05 12:01:26 +00:00
eb15e1fc3f some initial setup work for GUI 2024-05-29 08:34:07 +02:00
cc09bb87f8 restructure for rewrite 2024-05-29 08:15:45 +02:00
eae13bd107 Express.js vulnerability fix 2024-04-02 19:19:33 +02:00
68 changed files with 869 additions and 33516 deletions

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@
node_modules node_modules
*.secret.json *.secret.json
apple_private_key.p8 apple_private_key.p8
musicplayerv2-server.zip musicplayerv2-server.zip
dist
package-lock.json

View File

@@ -1,12 +1,11 @@
<div id="title" align="center"> <div id="title" align="center">
<img src="./assets/logo.png" width="300"> <img src="./assets/logo.png" width="300">
<h1>MusicPlayerV2</h1> <h1>MusicPlayer</h1>
</div> </div>
<div id="badges" align="center"> <div id="badges" align="center">
<img alt="Project License" src="https://img.shields.io/github/license/simplePCBuilding/MusicPlayerV2.svg"> <img alt="Project License" src="https://img.shields.io/github/license/simplePCBuilding/MusicPlayerV2.svg">
<img alt="GitHub Repo size" src="https://img.shields.io/github/repo-size/simplePCBuilding/MusicPlayerV2.svg"> <img alt="GitHub Repo size" src="https://img.shields.io/github/repo-size/simplePCBuilding/MusicPlayerV2.svg">
<img alt="Project code lines count" src="https://img.shields.io/tokei/lines/github/simplePCBuilding/MusicPlayerV2">
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues-pr-raw/simplePCBuilding/MusicPlayerV2"> <img alt="GitHub Repo issues" src="https://img.shields.io/github/issues-pr-raw/simplePCBuilding/MusicPlayerV2">
<img alt="Top Languages" src="https://img.shields.io/github/languages/top/simplePCBuilding/MusicPlayerV2"> <img alt="Top Languages" src="https://img.shields.io/github/languages/top/simplePCBuilding/MusicPlayerV2">
<img alt="GitHub Repo filecount" src="https://img.shields.io/github/directory-file-count/simplePCBuilding/MusicPlayerV2.svg"> <img alt="GitHub Repo filecount" src="https://img.shields.io/github/directory-file-count/simplePCBuilding/MusicPlayerV2.svg">
@@ -22,12 +21,18 @@
<img alt="App Version" src="https://img.shields.io/github/package-json/v/simplePCBuilding/MusicPlayerV2.svg?label=Development Version"> <img alt="App Version" src="https://img.shields.io/github/package-json/v/simplePCBuilding/MusicPlayerV2.svg?label=Development Version">
</div> </div>
A music player, specifically created for displaying song information from a CSV or JSON file on multiple different displays that are connected A music player, specifically created for displaying song information on multiple different displays that are connected to the same network, just from the browser.
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 is subscription-based and can be paid for on my [store](https://store.janishutz.com/product/com.janishutz).
<div id="donate" align="center">
<a href="https://store.janishutz.com/donate" target="_blank"><img src="https://store-cdn.janishutz.com/static/support-me.jpg" width="150px"></a>
</div>
# Features # Features
- Electron App that runs on all major Desktop OS (Linux, MacOS & Windows) - Browser based App that runs on all OS (Linux, MacOS, Windows, iOS, Android, iPadOS, ...)
- Show all song information over the local network on any amount of client displays - Fully featured Music Player
- Client displays show the playback position and all information from the metadata and CSV / JSON file that contains all song information - Show all song information over the Internet on any amount of client displays
- Client displays show the playback position and all information from song metadata fetched from the Apple Music API
- Play most common music files - Play most common music files
- Backend to allow users to connect over the internet - No setup required when using the hosted version at [music.janishutz.com](https://music.janishutz.com)

View File

@@ -1,184 +0,0 @@
const express = require( 'express' );
let app = express();
const path = require( 'path' );
const expressSession = require( 'express-session' );
const fs = require( 'fs' );
const bodyParser = require( 'body-parser' );
// const favicon = require( 'serve-favicon' );
const authKey = '' + fs.readFileSync( path.join( __dirname + '/authorizationKey.txt' ) );
app.use( expressSession ( {
secret: 'akgfsdkgfösdolfgslöodfvolwseifvoiwefö',
resave: true,
saveUninitialized: true
} ) );
app.use( bodyParser.urlencoded( { extended: false } ) );
app.use( bodyParser.json() );
// app.use( favicon( path.join( __dirname + '' ) ) );
let connectedClients = {};
let currentDetails = {
'songQueue': [],
'playingSong': {},
'pos': 0,
'isPlaying': false,
'queuePos': 0,
};
app.get( '/', ( request, response ) => {
response.sendFile( path.join( __dirname + '/ui/index.html' ) );
} );
app.get( '/showcase.js', ( request, response ) => {
response.sendFile( path.join( __dirname + '/ui/showcase.js' ) );
} );
app.get( '/showcase.css', ( request, response ) => {
response.sendFile( path.join( __dirname + '/ui/showcase.css' ) );
} );
app.post( '/authSSE', ( req, res ) => {
if ( req.body.authKey === authKey ) {
req.session.isAuth = true;
res.send( 'ok' );
} else {
res.send( 'hello' );
}
} );
app.post( '/fancy/auth', ( req, res ) => {
if ( req.body.key === authKey ) {
req.session.isAuth = true;
res.redirect( '/fancy' );
} else {
res.send( 'wrong' );
}
} );
app.get( '/fancy', ( req, res ) => {
if ( req.session.isAuth ) {
res.sendFile( path.join( __dirname + '/ui/fancy/showcase.html' ) );
} else {
res.sendFile( path.join( __dirname + '/ui/fancy/auth.html' ) );
}
} );
app.get( '/fancy/showcase.js', ( req, res ) => {
if ( req.session.isAuth ) {
res.sendFile( path.join( __dirname + '/ui/fancy/showcase.js' ) );
} else {
res.redirect( '/' );
}
} );
app.get( '/fancy/showcase.css', ( req, res ) => {
if ( req.session.isAuth ) {
res.sendFile( path.join( __dirname + '/ui/fancy/showcase.css' ) );
} else {
res.redirect( '/' );
}
} );
app.get( '/fancy/backgroundAnim.css', ( req, res ) => {
if ( req.session.isAuth ) {
res.sendFile( path.join( __dirname + '/ui/fancy/backgroundAnim.css' ) );
} else {
res.redirect( '/' );
}
} );
let connectedMain = {};
app.get( '/mainNotifier', ( req, res ) => {
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
if ( req.session.isAuth ) {
res.writeHead( 200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
} );
res.status( 200 );
res.flushHeaders();
let det = { 'type': 'basics' };
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
connectedMain = res;
} else {
res.send( 'wrong' );
}
} );
// STATUS UPDATE from the client display to send to main ui
// Send update if page is closed
const allowedMainUpdates = [ 'blur', 'visibility' ];
app.post( '/clientStatusUpdate', ( req, res ) => {
if ( allowedMainUpdates.includes( req.body.type ) ) {
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
sendClientUpdate( req.body.type, ip );
res.send( 'ok' );
} else {
res.status( 400 ).send( 'ERR_UNKNOWN_TYPE' );
}
} );
const sendClientUpdate = ( update, ip ) => {
try {
connectedMain.write( 'data: ' + JSON.stringify( { 'type': update, 'ip': ip } ) + '\n\n' );
} catch ( err ) {}
}
app.post( '/connect', ( request, response ) => {
if ( request.body.authKey === authKey ) {
request.session.authorized = true;
response.send( 'Handshake OK' );
} else {
response.status( 403 ).send( 'AuthKey wrong' );
}
} );
app.get( '/clientDisplayNotifier', ( req, res ) => {
res.writeHead( 200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
} );
res.status( 200 );
res.flushHeaders();
let det = { 'type': 'basics', 'data': currentDetails };
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
connectedClients[ req.session.id ] = res;
req.on( 'close', () => {
connectedClients.splice( Object.keys( connectedClients ).indexOf( req.session.id ), 1 );
} );
} );
const sendUpdate = ( update ) => {
for ( let client in connectedClients ) {
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': update, 'data': currentDetails[ update ] } ) + '\n\n' );
}
};
const allowedTypes = [ 'playingSong', 'isPlaying', 'songQueue', 'pos', 'queuePos' ];
app.post( '/statusUpdate', ( req, res ) => {
if ( req.body.authKey === authKey ) {
if ( allowedTypes.includes( req.body.type ) ) {
currentDetails[ req.body.type ] = req.body.data
res.send( 'thanks' );
sendUpdate( req.body.type );
} else {
res.status( 400 ).send( 'incorrect request' );
}
} else {
res.status( 403 ).send( 'Unauthorized' );
}
} );
app.use( ( request, response, next ) => {
response.sendFile( path.join( __dirname + '' ) )
} );
const PORT = process.env.PORT || 3000;
app.listen( PORT );

View File

@@ -1 +0,0 @@
gaöwovwef89voawö8p9 odövefw8öoaewpf89wec

View File

@@ -0,0 +1,7 @@
{
"token": "phafowegoväbwpb$weapvbpvfwcvfäawef39'ü0wtäqgpt5^ü62q'ẗ9wäa3g",
"name": "localhost:8082",
"client": "localhost:8081",
"backendURL": "http://localhost:8080",
"failReturnURL": "http://localhost:8081"
}

View File

@@ -0,0 +1,5 @@
{
"backendURL": "http://localhost:8083",
"name": "testing",
"signingSecret": "test"
}

702
backend/eslint.config.mjs Normal file
View 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,
}
],
},
},
);

2
backend/index.js Normal file
View File

@@ -0,0 +1,2 @@
const app = require( './dist/app.js' ).default;
app.run();

View File

@@ -1,788 +0,0 @@
{
"name": "backend",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.18.2",
"express-session": "^1.17.3",
"express-static": "^1.2.6",
"serve-favicon": "^2.5.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"dependencies": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-static": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/express-static/-/express-static-1.2.6.tgz",
"integrity": "sha512-pmp8fSe+bCxGCTk5+X6HzIF2APYMvrAq3Y7sT/WOELc0JZZxJhMTVgCbxcoUgMRBSqnQ9EfTw/Uv3J1ONoIAuA==",
"dependencies": {
"mime2": "latest"
},
"bin": {
"express-static": "bin/server.js"
}
},
"node_modules/express/node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/express/node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime2": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/mime2/-/mime2-0.0.11.tgz",
"integrity": "sha512-Ch599Y8U4vUgG9AaQgLEnIdXRRLoZPfjAdWFDLYePBEJS1nsS43H2pzbZn0u8lkbYx+yInuKxLsUEgM3fa9ZLQ=="
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-favicon": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz",
"integrity": "sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==",
"dependencies": {
"etag": "~1.8.1",
"fresh": "0.5.2",
"ms": "2.1.1",
"parseurl": "~1.3.2",
"safe-buffer": "5.1.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve-favicon/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node_modules/serve-favicon/node_modules/safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@@ -1,9 +0,0 @@
{
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.18.2",
"express-session": "^1.17.3",
"express-static": "^1.2.6",
"serve-favicon": "^2.5.0"
}
}

0
backend/src/app.ts Normal file
View File

104
backend/src/logger.ts Normal file
View File

@@ -0,0 +1,104 @@
import {
writeFile
} from 'node:fs';
const log = ( ...msg: string[] ) => {
output( 'log', log.caller.toString(), ...msg );
};
const info = ( ...msg: string[] ) => {
output( 'info', log.caller.toString(), ...msg );
};
const debug = ( ...msg: string[] ) => {
output( 'debug', log.caller.toString(), ...msg );
};
const warn = ( ...msg: string[] ) => {
output( 'warn', log.caller.toString(), ...msg );
};
const error = ( ...msg: string[] ) => {
output( 'error', log.caller.toString(), ...msg );
};
const fatal = ( ...msg: string[] ) => {
output( 'fatal', log.caller.toString(), ...msg );
};
let loc = 'stderr';
let lev = 0;
type LogLevel = 'debug' | 'info' | 'log' | 'warn' | 'error' | 'fatal';
const levels = [
'debug',
'info',
'log',
'warn',
'error',
'fatal'
];
const configure = ( location: 'stderr' | 'file', minLevel: LogLevel, file?: string ) => {
if ( location === 'file' && !file ) {
throw new Error( 'File parameter required when location is "file"' );
}
loc = location === 'stderr' ? 'stderr' : file;
lev = levels.indexOf( minLevel );
};
const logfile: string[] = [];
const output = ( level: LogLevel, caller: string, ...message: string[] ) => {
if ( levels.indexOf( level ) < lev ) {
return;
}
const msg = message.join( ' ' );
const out = `[${ level.toUpperCase() }] (${ new Date().toISOString() }) in ${ caller }: ${ msg }`;
if ( loc === 'stderr' ) {
console.error( out );
} else {
logfile.push( out );
save();
}
};
let isSaving = false;
let waitingOnSave = false;
const save = () => {
if ( isSaving ) {
waitingOnSave = true;
return;
}
isSaving = true;
writeFile( loc, JSON.stringify( logfile ), err => {
if ( err )
console.error( '[LOGGER] Failed to save with error ' + err );
if ( waitingOnSave ) {
waitingOnSave = false;
isSaving = false;
save();
}
isSaving = false;
} );
};
export default {
log,
info,
debug,
warn,
error,
fatal,
configure
};

View File

@@ -0,0 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getSubscriptions = ( _uid: string ) => {
return [
{
'id': 'com.janishutz.MusicPlayer.subscription',
'expires': new Date().getTime() + 200000,
'status': true
}
];
};
export default {
getSubscriptions,
};

13
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": true,
"target": "ES6",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"types": ["node"],
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": [ "./src/**/*" ],
}

View File

@@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authenticate - Fancy Remote Display</title>
<style>
.aligner {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
width: 100vw;
}
html, body {
background-color: rgb(41, 41, 41);
color: white;
margin: 0;
width: 100%;
height: 100%;
padding: 0;
font-family: sans-serif;
}
form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#key {
padding: 1vh;
font-size: 120%;
border: none;
border-radius: 50px;
margin-top: 5px;
}
h1 {
font-size: 5vh;
}
#submit {
padding: 20px;
background-color: rgb(1, 1, 88);
color: white;
border: none;
border-radius: 50px;
transition: all 1s;
cursor: pointer;
font-size: 120%;
}
#submit:hover {
background-color: rgb(1, 1, 120);
border-radius: 20px;
}
</style>
</head>
<body>
<div class="aligner">
<h1>Authenticate - Fancy Remote Display</h1>
<form action="/fancy/auth" method="post">
<label for="key" style="font-size: 120%;">Authentication Key</label>
<input type="text" name="key" id="key" style="margin-bottom: 1vh;">
<input type="submit" value="Authenticate" id="submit">
</form>
</div>
</body>
</html>

View File

@@ -1,44 +0,0 @@
.background {
position: fixed;
left: -50vw;
width: 200vw;
height: 200vw;
top: -50vw;
z-index: -1;
filter: blur(10px);
background: conic-gradient( blue, green, red, blue );
animation: gradientAnim 10s infinite linear;
background-position: center;
}
.beat, .beat-manual {
height: 100%;
width: 100%;
background-color: rgba( 0, 0, 0, 0.15 );
display: none;
}
.beat {
animation: beatAnim 0.6s infinite linear;
}
@keyframes beatAnim {
0% {
background-color: rgba( 0, 0, 0, 0.2 );
}
20% {
background-color: rgba( 0, 0, 0, 0 );
}
100% {
background-color: rgba( 0, 0, 0, 0.2 );
}
}
@keyframes gradientAnim {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 360deg );
}
}

View File

@@ -1,208 +0,0 @@
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
color: white;
}
body {
font-family: sans-serif;
}
.content {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.playing-symbols {
position: absolute;
left: 10vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
width: 5vw;
height: 5vw;
background-color: rgba( 0, 0, 0, 0.6 );
}
.playing-symbols-wrapper {
width: 4vw;
height: 5vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.playing-bar {
height: 60%;
background-color: white;
width: 10%;
border-radius: 50px;
margin: auto;
}
#bar-1 {
animation: music-playing 0.9s infinite ease-in-out;
}
#bar-2 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.3s;
}
#bar-3 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.6s;
}
@keyframes music-playing {
0% {
transform: scaleY( 1 );
}
50% {
transform: scaleY( 0.5 );
}
100% {
transform: scaleY( 1 );
}
}
.song-list-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.song-list {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 80%;
margin: 2px;
padding: 1vh;
border: 1px white solid;
background-color: rgba( 0, 0, 0, 0.4 );
}
.song-details-wrapper {
margin: 0;
display: block;
margin-left: 10px;
margin-right: auto;
}
.song-list .song-image {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
}
.pause-icon {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw !important;
user-select: none;
}
.current-song-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 55vh;
width: 100%;
margin-bottom: 0.5%;
margin-top: 0.25%;
}
.current-song {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 1vh;
padding: 1vh;
text-align: center;
background-color: rgba( 0, 0, 0, 0.4 );
}
.fancy-view-song-art {
height: 30vh;
width: 30vh;
object-fit: cover;
object-position: center;
margin-bottom: 10px;
font-size: 30vh !important;
}
#app {
background-color: rgba( 0, 0, 0, 0 );
}
#progress, #progress::-webkit-progress-bar {
background-color: rgba(45, 28, 145);
color: rgba(45, 28, 145);
width: 30vw;
border: none;
border-radius: 0px;
accent-color: white;
-webkit-appearance: none;
appearance: none;
}
#progress::-moz-progress-bar {
background-color: white;
}
#progress::-webkit-progress-value {
background-color: white !important;
}
.mode-selector-wrapper {
opacity: 0;
position: fixed;
right: 0.5%;
top: 0.5%;
padding: 0.5%;
}
.mode-selector-wrapper:hover {
opacity: 1;
}
.dancing-style {
font-size: 250%;
margin: 0;
font-weight: bolder;
}
.info {
position: fixed;
font-size: 12px;
transform: rotate(270deg);
left: -150px;
margin: 0;
padding: 0;
top: 50%;
}

View File

@@ -1,72 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=7">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Showcase - MusicPlayerV2</title>
<link rel="stylesheet" href="/fancy/showcase.css">
<link rel="stylesheet" href="/fancy/backgroundAnim.css">
<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" />
</head>
<body>
<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>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
<script src="/fancy/showcase.js"></script>
</body>
</html>

View File

@@ -1,361 +0,0 @@
// eslint-disable-next-line no-undef
const { createApp } = Vue;
createApp( {
data() {
return {
hasLoaded: false,
songs: [],
playingSong: {},
isPlaying: false,
pos: 0,
queuePos: 0,
colourPalette: [],
progressBar: 0,
timeTracker: null,
visualizationSettings: 'mic',
micAnalyzer: null,
beatDetected: false,
colorThief: null,
lastDispatch: new Date().getTime() - 5000,
isReconnecting: false,
};
},
computed: {
songQueue() {
let ret = [];
let pos = 0;
for ( let song in this.songs ) {
if ( pos >= this.queuePos ) {
ret.push( this.songs[ song ] );
}
pos += 1;
}
return ret;
},
getTimeUntil() {
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 ( this.isPlaying ) {
if ( timeRemaining === 0 ) {
return 'Currently playing';
} else {
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
}
} else {
if ( timeRemaining === 0 ) {
return 'Plays next';
} else {
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
}
}
}
}
},
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' );

View File

@@ -1,52 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=7">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Showcase - MusicPlayerV2</title>
<link rel="stylesheet" href="/showcase.css">
<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" />
</head>
<body>
<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 loading" id="loading">
<h1>Loading...</h1>
<p>Please wait</p>
</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 || playingSong.coverArtOrigin !== 'api'">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">
<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="song-list-wrapper">
<div v-for="song in songQueue" class="song-list">
<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>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
<script src="/showcase.js"></script>
</body>
</html>

View File

@@ -1,188 +0,0 @@
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
color: white;
background-color: rgb(29, 29, 29);
}
body {
font-family: sans-serif;
}
.content {
width: 100%;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
}
.loaded {
display: block;
}
.loading {
display: flex;
height: 100vh;
}
.playing-symbols {
position: absolute;
left: 10vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
width: 5vw;
height: 5vw;
background-color: rgba( 0, 0, 0, 0.6 );
}
.playing-symbols-wrapper {
width: 4vw;
height: 5vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.song-list-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 5%;
}
.song-list {
display: flex;
flex-direction: row;
align-items: center;
width: 80%;
margin: 2px;
padding: 1vh;
border: 1px white solid;
background-color: rgba( 0, 0, 0, 0.4 );
}
.song-details-wrapper {
margin: 0;
display: block;
margin-left: 10px;
margin-right: auto;
width: 65%;
}
.song-list .song-image {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
}
.pause-icon {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw !important;
user-select: none;
}
.current-song-wrapper {
display: flex;
align-items: center;
flex-direction: column;
width: 100%;
margin-bottom: 2%;
margin-top: 1%;
}
.current-song {
display: flex;
align-items: center;
flex-direction: column;
margin-top: 1vh;
padding: 1vh;
max-width: 80%;
text-align: center;
background-color: rgba( 0, 0, 0, 0.4 );
}
.fancy-view-song-art {
height: 30vh;
width: 30vh;
object-fit: cover;
object-position: center;
margin-bottom: 10px;
font-size: 30vh !important;
}
#app {
background-color: rgba( 0, 0, 0, 0 );
}
#progress, #progress::-webkit-progress-bar {
background-color: rgba(45, 28, 145);
color: rgba(45, 28, 145);
width: 30vw;
border: none;
border-radius: 0px;
accent-color: white;
-webkit-appearance: none;
appearance: none;
}
#progress::-moz-progress-bar {
background-color: white;
}
#progress::-webkit-progress-value {
background-color: white !important;
}
.mode-selector-wrapper {
opacity: 0;
position: fixed;
right: 0.5%;
top: 0.5%;
padding: 0.5%;
}
.mode-selector-wrapper:hover {
opacity: 1;
}
.dancing-style {
font-size: 250%;
margin: 0;
font-weight: bolder;
}
.info {
position: fixed;
font-size: 12px;
transform: rotate(270deg);
left: -150px;
margin: 0;
padding: 0;
top: 50%;
}
.time-until {
width: 30%;
text-align: end;
}

View File

@@ -1,159 +0,0 @@
// eslint-disable-next-line no-undef
const { createApp } = Vue;
createApp( {
data() {
return {
hasLoaded: false,
songs: [],
playingSong: {},
isPlaying: false,
pos: 0,
queuePos: 0,
colourPalette: [],
progressBar: 0,
timeTracker: null,
isReconnecting: false,
};
},
computed: {
songQueue() {
let ret = [];
let pos = 0;
for ( let song in this.songs ) {
if ( pos >= this.queuePos ) {
ret.push( this.songs[ song ] );
}
pos += 1;
}
return ret;
},
getTimeUntil() {
return ( song ) => {
let timeRemaining = 0;
for ( let i = this.queuePos; i < Object.keys( this.songs ).length; i++ ) {
if ( this.songs[ i ] == song ) {
break;
}
timeRemaining += parseInt( this.songs[ i ].duration );
}
if ( this.isPlaying ) {
if ( timeRemaining === 0 ) {
return 'Currently playing';
} else {
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
}
} else {
if ( timeRemaining === 0 ) {
return 'Plays next';
} else {
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
}
}
}
}
},
methods: {
startTimeTracker () {
try {
clearInterval( this.timeTracker );
} catch ( err ) {}
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;
},
connect() {
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;
} 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;
} else if ( data.type === 'songQueue' ) {
this.songs = data.data;
} else if ( data.type === 'playingSong' ) {
this.playingSong = data.data;
} else if ( data.type === 'queuePos' ) {
this.queuePos = data.data;
}
};
source.onopen = () => {
this.isReconnecting = false;
this.hasLoaded = true;
if ( document.fonts.status === 'loaded' ) {
document.getElementById( 'loading' ).classList.remove( 'loading' );
document.getElementById( 'app' ).classList.add( 'loaded' );
} else {
document.fonts.onloadingdone = () => {
document.getElementById( 'loading' ).classList.remove( 'loading' );
document.getElementById( 'app' ).classList.add( 'loaded' );
};
}
};
let self = this;
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
if ( e.target.readyState == EventSource.CLOSED ) {
console.log( 'disconnected' );
}
setTimeout( () => {
if ( !self.isReconnecting ) {
self.isReconnecting = true;
self.tryReconnect();
}
}, 1000 );
}, false );
},
tryReconnect() {
const int = setInterval( () => {
if ( !this.isReconnecting ) {
clearInterval( int );
} else {
connectToSSESource();
}
}, 1000 );
},
},
mounted() {
this.connect();
},
watch: {
isPlaying( value ) {
if ( value ) {
this.startTimeTracker();
} else {
this.stopTimeTracker();
}
}
}
} ).mount( '#app' );

View File

@@ -1,4 +0,0 @@
> 1%
last 2 versions
not dead
not ie 11

27
frontend/.gitignore vendored
View File

@@ -1,27 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#Electron-builder output
/dist_electron
test

View File

@@ -1,19 +0,0 @@
# musicplayerv2
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

26497
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
{
"name": "musicplayerv2",
"version": "1.0.0",
"maintainers": [
"Janis Hutz <development@janishutz.com>"
],
"description": "A music player",
"homepage": "https://janishutz.com/projects/musicplayerv2",
"author": {
"name": "Janis Hutz",
"email": "development@janishutz.com",
"url": "https://janishutz.com"
},
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/simplePCBuilding/musicplayerv2/issues"
},
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"axios": "^1.6.1",
"core-js": "^3.8.3",
"cors": "^2.8.5",
"csv-parser": "^3.0.0",
"electron-squirrel-startup": "^1.0.0",
"eventsource": "^2.0.2",
"express-session": "^1.17.3",
"ip": "^1.1.8",
"jquery": "^3.7.1",
"json-beautify": "^1.1.1",
"jsonwebtoken": "^9.0.2",
"music-metadata": "^7.13.0",
"node-fetch": "^2.7.0",
"node-musickit-api": "^2.1.1",
"realtime-bpm-analyzer": "^3.2.1",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"web-audio-beat-detector": "^8.1.55"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"electron": "^13.0.0",
"electron-devtools-installer": "^3.1.0",
"vue-cli-plugin-electron-builder": "~2.1.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,24 +0,0 @@
/* fallback */
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 100 700;
src: url(/iconFont.woff2) format('woff2');
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
}

Binary file not shown.

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="/icon-font.css" />
<script src="/jquery.min.js"></script>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,177 +0,0 @@
<template>
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<style>
:root, :root.light {
--primary-color: #2c3e50;
--accent-background: rgb(30, 30, 82);
--secondary-color: white;
--background-color: white;
--popup-color: rgb(224, 224, 224);
--accent-color: #42b983;
--hover-color: rgb(165, 165, 165);
--accent-background-hover: rgb(124, 140, 236);
--overlay-color: rgba(0, 0, 0, 0.7);
--border-color: rgb(100, 100, 100);
--highlight-backdrop: rgb(143, 134, 192);
--hint-color: rgb(174, 210, 221);
--PI: 3.14159265358979;
}
:root.dark {
--primary-color: white;
--accent-background: rgb(56, 56, 112);
--secondary-color: white;
--background-color: rgb(32, 32, 32);
--popup-color: rgb(58, 58, 58);
--accent-color: #42b983;
--hover-color: rgb(83, 83, 83);
--accent-background-hover: #4380a8;
--overlay-color: rgba(104, 104, 104, 0.575);
--border-color: rgb(190, 190, 190);
--highlight-backdrop: rgb(85, 63, 207);
--hint-color: rgb(88, 91, 110);
}
@media ( prefers-color-scheme: dark ) {
:root {
--primary-color: white;
--accent-background: rgb(56, 56, 112);
--secondary-color: white;
--background-color: rgb(32, 32, 32);
--popup-color: rgb(58, 58, 58);
--accent-color: #42b983;
--hover-color: rgb(83, 83, 83);
--accent-background-hover: #4380a8;
--overlay-color: rgba(104, 104, 104, 0.575);
--border-color: rgb(190, 190, 190);
--highlight-backdrop: rgb(85, 63, 207);
--hint-color: rgb(88, 91, 110);
}
}
::selection {
background-color: var( --highlight-backdrop );
color: var( --secondary-color );
}
#themeSelector {
background-color: rgba( 0, 0, 0, 0 );
color: var( --primary-color );
font-size: 130%;
padding: 0;
margin: 0;
border: none;
cursor: pointer;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: var( --background-color );
color: var( --primary-color );
}
#app {
transition: 0.5s;
background-color: var( --background-color );
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: var( --primary-color );
height: 100%;
display: flex;
flex-direction: column;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: var( --primary-color );
}
nav a.router-link-exact-active {
color: #42b983;
}
.scale-enter-active,
.scale-leave-active {
transition: all 0.5s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.9);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 48
}
.clr-open {
border: black solid 1px !important;
}
</style>
<script>
export default {
name: 'app',
data () {
return {
theme: '',
}
},
methods: {
changeTheme () {
if ( this.theme === '&#9788;' ) {
document.documentElement.classList.remove( 'dark' );
document.documentElement.classList.add( 'light' );
localStorage.setItem( 'theme', '&#9789;' );
this.theme = '&#9789;';
} else if ( this.theme === '&#9789;' ) {
document.documentElement.classList.remove( 'light' );
document.documentElement.classList.add( 'dark' );
localStorage.setItem( 'theme', '&#9788;' );
this.theme = '&#9788;';
}
}
},
created () {
this.theme = localStorage.getItem( 'theme' ) ?? '';
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || this.theme === '&#9788;' ) {
document.documentElement.classList.add( 'dark' );
this.theme = '&#9788;';
} else {
document.documentElement.classList.add( 'light' );
this.theme = '&#9789;';
}
}
}
</script>

View File

@@ -1,382 +0,0 @@
const express = require( 'express' );
let app = express();
const path = require( 'path' );
const cors = require( 'cors' );
const fs = require( 'fs' );
const bodyParser = require( 'body-parser' );
const dialog = require( 'electron' ).dialog;
const session = require( 'express-session' );
const indexer = require( './indexer.js' );
const axios = require( 'axios' );
const ip = require( 'ip' );
const jwt = require( 'jsonwebtoken' );
const shell = require( 'electron' ).shell;
const beautify = require( 'json-beautify' );
const EventSource = require( 'eventsource' );
app.use( bodyParser.urlencoded( { extended: false } ) );
app.use( bodyParser.json() );
app.use( cors() );
app.use( session( {
secret: 'aeogetwöfaöow0ofö034eö8ptqw39eöavfui786uqew9t0ez9eauigwöfqewoöaiq938w0c8p9awöäf9¨äüöe',
saveUninitialized: true,
resave: false,
} ) );
const conf = JSON.parse( fs.readFileSync( path.join( __dirname + '/config/config.json' ) ) );
// TODO: Import from config
const remoteURL = conf.connectionURL ?? 'http://localhost:3000';
let hasConnected = false;
const connect = () => {
if ( authKey !== '' && conf.doConnect ) {
axios.post( remoteURL + '/connect', { 'authKey': authKey } ).then( res => {
if ( res.status === 200 ) {
console.log( '[ BACKEND INTEGRATION ] Connection successful' );
hasConnected = true;
} else {
console.error( '[ BACKEND INTEGRATION ] Connection error occurred' );
}
} ).catch( err => {
console.error( err );
} );
connectToSSESource();
return 'connecting';
} else {
return 'noAuthKey';
}
};
let isSSEAuth = false;
let sessionToken = '';
let errorCount = 0;
let isReconnecting = false;
const connectToSSESource = () => {
if ( isSSEAuth ) {
let source = new EventSource( remoteURL + '/mainNotifier', {
https: true,
withCredentials: true,
headers: {
'Cookie': sessionToken
}
} );
source.onmessage = ( e ) => {
let data;
try {
data = JSON.parse( e.data );
} catch ( err ) {
data = { 'type': e.data };
}
if ( data.type === 'blur' ) {
sendClientUpdate( data.type, data.ip );
} else if ( data.type === 'visibility' ) {
sendClientUpdate( data.type, data.ip );
}
};
source.onopen = () => {
isReconnecting = false;
console.log( '[ BACKEND INTEGRATION ] Connection to notifier successful' );
};
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
setTimeout( () => {
if ( !isReconnecting ) {
isReconnecting = true;
console.log( '[ BACKEND INTEGRATION ] Disconnected from notifier, reconnecting...' );
tryReconnect();
}
}, 1000 );
}, false );
} else {
axios.post( remoteURL + '/authSSE', { 'authKey': authKey } ).then( res => {
if ( res.status == 200 ) {
sessionToken = res.headers[ 'set-cookie' ][ 0 ].slice( 0, res.headers[ 'set-cookie' ][ 0 ].indexOf( ';' ) );
isSSEAuth = true;
connectToSSESource();
} else {
connectToSSESource();
}
} );
}
}
const tryReconnect = () => {
const int = setInterval( () => {
if ( !isReconnecting ) {
clearInterval( int );
} else {
if ( errorCount > 5 ) {
isSSEAuth = false;
errorCount = 0;
} else {
errorCount += 1;
}
connectToSSESource();
}
}, 1000 );
}
let authKey = conf.authKey ?? '';
connect();
let connectedClients = {};
let changedStatus = [];
let currentDetails = {
'songQueue': [],
'playingSong': {},
'pos': 0,
'isPlaying': false,
'queuePos': 0,
};
let connectedMain = {};
// TODO: Add backend integration
require( './appleMusicRoutes.js' )( app );
app.get( '/', ( request, response ) => {
response.sendFile( path.join( __dirname + '/client/showcase.html' ) );
} );
app.get( '/getLocalIP', ( req, res ) => {
res.send( ip.address() );
} );
app.get( '/useAppleMusic', ( req, res ) => {
shell.openExternal( 'http://localhost:8081/apple-music' );
res.send( 'ok' );
} );
app.get( '/openSongs', ( req, res ) => {
// res.send( '{ "data": [ "/home/janis/Music/KB2022" ] }' );
// res.send( '{ "data": [ "/mnt/storage/SORTED/Music/audio/KB2022" ] }' );
res.send( { 'data': dialog.showOpenDialogSync( { properties: [ 'openDirectory' ], title: 'Open music library folder' } ) } );
} );
app.get( '/showcase.js', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/showcase.js' ) );
} );
app.get( '/showcase.css', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/showcase.css' ) );
} );
app.get( '/backgroundAnim.css', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/backgroundAnim.css' ) );
} );
app.get( '/clientDisplayNotifier', ( req, res ) => {
res.writeHead( 200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
} );
res.status( 200 );
res.flushHeaders();
let det = { 'type': 'basics', 'data': currentDetails };
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
connectedClients[ req.session.id ] = res;
req.on( 'close', () => {
connectedClients.splice( Object.keys( connectedClients ).indexOf( req.session.id ), 1 );
} );
} );
app.get( '/mainNotifier', ( req, res ) => {
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
if ( ip === '::ffff:127.0.0.1' || ip === '::1' ) {
res.writeHead( 200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
} );
res.status( 200 );
res.flushHeaders();
let det = { 'type': 'basics' };
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
connectedMain = res;
} else {
res.send( 'wrong' );
}
} );
const sendUpdate = ( update ) => {
if ( update === 'pos' ) {
currentDetails[ 'playingSong' ][ 'startTime' ] = new Date().getTime();
for ( let client in connectedClients ) {
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ] } ) + '\n\n' );
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'pos', 'data': currentDetails[ 'pos' ] } ) + '\n\n' );
}
} else if ( update === 'playingSong' ) {
if ( !currentDetails[ 'playingSong' ] ) {
currentDetails[ 'playingSong' ] = {};
}
currentDetails[ 'playingSong' ][ 'startTime' ] = new Date().getTime();
for ( let client in connectedClients ) {
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'pos', 'data': currentDetails[ 'pos' ] } ) + '\n\n' );
}
} else if ( update === 'isPlaying' ) {
currentDetails[ 'playingSong' ][ 'startTime' ] = new Date().getTime();
for ( let client in connectedClients ) {
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ] } ) + '\n\n' );
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'pos', 'data': currentDetails[ 'pos' ] } ) + '\n\n' );
}
}
for ( let client in connectedClients ) {
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': update, 'data': currentDetails[ update ] } ) + '\n\n' );
}
// Check if connected and if not, try to authenticate with data from authKey file
if ( hasConnected ) {
if ( update === 'isPlaying' ) {
axios.post( remoteURL + '/statusUpdate', { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ], 'authKey': authKey } ).catch( err => {
console.error( err );
} );
axios.post( remoteURL + '/statusUpdate', { 'type': 'pos', 'data': currentDetails[ 'pos' ], 'authKey': authKey } ).catch( err => {
console.error( err );
} );
} else if ( update === 'pos' ) {
axios.post( remoteURL + '/statusUpdate', { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ], 'authKey': authKey } ).catch( err => {
console.error( err );
} );
axios.post( remoteURL + '/statusUpdate', { 'type': 'pos', 'data': currentDetails[ 'pos' ], 'authKey': authKey } ).catch( err => {
console.error( err );
} );
}
axios.post( remoteURL + '/statusUpdate', { 'type': update, 'data': currentDetails[ update ], 'authKey': authKey } ).catch( err => {
console.error( err );
} );
} else {
connect();
}
}
const allowedTypes = [ 'playingSong', 'isPlaying', 'songQueue', 'pos', 'queuePos' ];
app.post( '/statusUpdate', ( req, res ) => {
if ( allowedTypes.includes( req.body.type ) ) {
currentDetails[ req.body.type ] = req.body.data;
changedStatus.push( req.body.type );
sendUpdate( req.body.type );
res.send( 'ok' );
} else {
res.status( 400 ).send( 'ERR_UNKNOWN_TYPE' );
}
} );
// STATUS UPDATE from the client display to send to main ui
// Send update if page is closed
const allowedMainUpdates = [ 'blur', 'visibility' ];
app.post( '/clientStatusUpdate', ( req, res ) => {
if ( allowedMainUpdates.includes( req.body.type ) ) {
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
sendClientUpdate( req.body.type, ip );
res.send( 'ok' );
} else {
res.status( 400 ).send( 'ERR_UNKNOWN_TYPE' );
}
} );
const sendClientUpdate = ( update, ip ) => {
try {
connectedMain.write( 'data: ' + JSON.stringify( { 'type': update, 'ip': ip } ) + '\n\n' );
} catch ( err ) {}
}
app.get( '/indexDirs', ( req, res ) => {
if ( req.query.dir ) {
indexer.index( req ).then( dirIndex => {
res.send( dirIndex );
} ).catch( err => {
if ( err === 'ERR_DIR_NOT_FOUND' ) {
res.status( 404 ).send( 'ERR_DIR_NOT_FOUND' );
} else {
res.status( 500 ).send( 'unable to process' );
}
} );
} else {
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
}
} );
app.get( '/loadPlaylist', ( req, res ) => {
const selFile = dialog.showOpenDialogSync( { properties: [ 'openFile' ], title: 'Open file with playlist' } )[ 0 ];
res.send( { 'data': JSON.parse( fs.readFileSync( selFile ) ), 'path': selFile } );
} );
app.get( '/getMetadata', async ( req, res ) => {
res.send( await indexer.analyzeFile( req.query.file ) );
} );
app.post( '/savePlaylist', ( req, res ) => {
fs.writeFileSync( dialog.showSaveDialogSync( {
properties: [ 'createDirectory' ],
title: 'Save the playlist as a json file',
filters: [
{
extensions: [ 'json' ],
name: 'JSON files',
}
],
defaultPath: 'songs.json'
} ), beautify( req.body, null, 2, 50 ) );
res.send( 'ok' );
} );
app.get( '/getSongCover', ( req, res ) => {
if ( req.query.filename ) {
if ( indexer.getImages( req.query.filename ) ) {
res.send( indexer.getImages( req.query.filename ) );
} else {
res.status( 404 ).send( 'No cover image for this file' );
}
} else {
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
}
} );
app.get( '/getSongFile', ( req, res ) => {
if ( req.query.filename ) {
res.sendFile( req.query.filename );
} else {
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
}
} );
app.get( '/getAppleMusicDevToken', ( req, res ) => {
// sign dev token
const privateKey = fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString();
// TODO: Remove secret
const config = JSON.parse( fs.readFileSync( path.join( __dirname + '/config/apple-music-api.config.json' ) ) );
const jwtToken = jwt.sign( {}, privateKey, {
algorithm: "ES256",
expiresIn: "180d",
issuer: config.teamID,
header: {
alg: "ES256",
kid: config.keyID
}
} );
res.send( jwtToken );
} );
app.use( ( request, response, next ) => {
response.sendFile( path.join( __dirname + '' ) )
} );
app.listen( 8081 );

View File

@@ -1,84 +0,0 @@
/*
* MusicPlayerV2 - appleMusicRoutes.js
*
* Created by Janis Hutz 11/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const path = require( 'path' );
const fs = require( 'fs' );
const csv = require( 'csv-parser' );
const dialog = require( 'electron' ).dialog;
const analyzeFile = ( filepath ) => {
return new Promise( ( resolve, reject ) => {
if ( filepath.includes( '.csv' ) ) {
// This will assume that line #1 will be song #1 in the file list
// (when sorted by name)
let results = {};
let pos = 0;
fs.createReadStream( filepath )
.pipe( csv() )
.on( 'data', ( data ) => {
results[ pos ] = data;
pos += 1;
} ).on( 'end', () => {
resolve( results );
} );
} else if ( filepath.includes( '.json' ) ) {
resolve( JSON.parse( fs.readFileSync( filepath ) ) );
} else {
reject( 'NO_CSV_OR_JSON_FILE' );
}
} );
}
module.exports = ( app ) => {
app.get( '/apple-music', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/appleMusic/index.html' ) );
} );
app.get( '/apple-music/helpers/:file', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/appleMusic/' + req.params.file ) );
} );
app.get( '/icon-font.css', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/icon-font.css' ) );
} );
app.get( '/iconFont.woff2', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/iconFont.woff2' ) );
} );
app.get( '/logo.png', ( req, res ) => {
res.sendFile( path.join( __dirname + '/client/logo.png' ) );
} );
app.get( '/apple-music/getAdditionalData', ( req, res ) => {
const filepath = dialog.showOpenDialogSync( {
properties: [ 'openFile' ],
title: 'Open file with additional data on the songs',
filters: [
{
name: 'All supported files (.csv, .json)',
extensions: [ 'csv', 'json' ],
},
{
name: 'JSON',
extensions: [ 'json' ],
},
{
name: 'CSV',
extensions: [ 'csv' ],
}
],
} )[ 0 ];
analyzeFile( filepath ).then( analyzedFile => {
res.send( analyzedFile );
} ).catch( () => {
res.status( 500 ).send( 'no csv / json file' );
} )
} );
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M204-318q-22-38-33-78t-11-82q0-134 93-228t227-94h7l-64-64 56-56 160 160-160 160-56-56 64-64h-7q-100 0-170 70.5T240-478q0 26 6 51t18 49l-60 60ZM481-40 321-200l160-160 56 56-64 64h7q100 0 170-70.5T720-482q0-26-6-51t-18-49l60-60q22 38 33 78t11 82q0 134-93 228t-227 94h-7l64 64-56 56Z"/></svg>

Before

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

View File

@@ -1,82 +0,0 @@
'use strict'
import { app, protocol, BrowserWindow } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'
const isDevelopment = process.env.NODE_ENV !== 'production'
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
async function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
}
require( './app.js' );
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS3_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
createWindow()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="361px" height="361px" viewBox="0 0 361 361" style="enable-background:new 0 0 361 361;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<g id="Layer_5">
</g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="180" y1="358.6047" x2="180" y2="7.7586">
<stop offset="0" style="stop-color:#FA233B"/>
<stop offset="1" style="stop-color:#FB5C74"/>
</linearGradient>
<path class="st0" d="M360,112.61c0-4.3,0-8.6-0.02-12.9c-0.02-3.62-0.06-7.24-0.16-10.86c-0.21-7.89-0.68-15.84-2.08-23.64
c-1.42-7.92-3.75-15.29-7.41-22.49c-3.6-7.07-8.3-13.53-13.91-19.14c-5.61-5.61-12.08-10.31-19.15-13.91
c-7.19-3.66-14.56-5.98-22.47-7.41c-7.8-1.4-15.76-1.87-23.65-2.08c-3.62-0.1-7.24-0.14-10.86-0.16C255.99,0,251.69,0,247.39,0
H112.61c-4.3,0-8.6,0-12.9,0.02c-3.62,0.02-7.24,0.06-10.86,0.16C80.96,0.4,73,0.86,65.2,2.27c-7.92,1.42-15.28,3.75-22.47,7.41
c-7.07,3.6-13.54,8.3-19.15,13.91c-5.61,5.61-10.31,12.07-13.91,19.14c-3.66,7.2-5.99,14.57-7.41,22.49
c-1.4,7.8-1.87,15.76-2.08,23.64c-0.1,3.62-0.14,7.24-0.16,10.86C0,104.01,0,108.31,0,112.61v134.77c0,4.3,0,8.6,0.02,12.9
c0.02,3.62,0.06,7.24,0.16,10.86c0.21,7.89,0.68,15.84,2.08,23.64c1.42,7.92,3.75,15.29,7.41,22.49c3.6,7.07,8.3,13.53,13.91,19.14
c5.61,5.61,12.08,10.31,19.15,13.91c7.19,3.66,14.56,5.98,22.47,7.41c7.8,1.4,15.76,1.87,23.65,2.08c3.62,0.1,7.24,0.14,10.86,0.16
c4.3,0.03,8.6,0.02,12.9,0.02h134.77c4.3,0,8.6,0,12.9-0.02c3.62-0.02,7.24-0.06,10.86-0.16c7.89-0.21,15.85-0.68,23.65-2.08
c7.92-1.42,15.28-3.75,22.47-7.41c7.07-3.6,13.54-8.3,19.15-13.91c5.61-5.61,10.31-12.07,13.91-19.14
c3.66-7.2,5.99-14.57,7.41-22.49c1.4-7.8,1.87-15.76,2.08-23.64c0.1-3.62,0.14-7.24,0.16-10.86c0.03-4.3,0.02-8.6,0.02-12.9V112.61
z"/>
</g>
<g id="Glyph_2_">
<g>
<path class="st1" d="M254.5,55c-0.87,0.08-8.6,1.45-9.53,1.64l-107,21.59l-0.04,0.01c-2.79,0.59-4.98,1.58-6.67,3
c-2.04,1.71-3.17,4.13-3.6,6.95c-0.09,0.6-0.24,1.82-0.24,3.62c0,0,0,109.32,0,133.92c0,3.13-0.25,6.17-2.37,8.76
c-2.12,2.59-4.74,3.37-7.81,3.99c-2.33,0.47-4.66,0.94-6.99,1.41c-8.84,1.78-14.59,2.99-19.8,5.01
c-4.98,1.93-8.71,4.39-11.68,7.51c-5.89,6.17-8.28,14.54-7.46,22.38c0.7,6.69,3.71,13.09,8.88,17.82
c3.49,3.2,7.85,5.63,12.99,6.66c5.33,1.07,11.01,0.7,19.31-0.98c4.42-0.89,8.56-2.28,12.5-4.61c3.9-2.3,7.24-5.37,9.85-9.11
c2.62-3.75,4.31-7.92,5.24-12.35c0.96-4.57,1.19-8.7,1.19-13.26l0-116.15c0-6.22,1.76-7.86,6.78-9.08c0,0,88.94-17.94,93.09-18.75
c5.79-1.11,8.52,0.54,8.52,6.61l0,79.29c0,3.14-0.03,6.32-2.17,8.92c-2.12,2.59-4.74,3.37-7.81,3.99
c-2.33,0.47-4.66,0.94-6.99,1.41c-8.84,1.78-14.59,2.99-19.8,5.01c-4.98,1.93-8.71,4.39-11.68,7.51
c-5.89,6.17-8.49,14.54-7.67,22.38c0.7,6.69,3.92,13.09,9.09,17.82c3.49,3.2,7.85,5.56,12.99,6.6c5.33,1.07,11.01,0.69,19.31-0.98
c4.42-0.89,8.56-2.22,12.5-4.55c3.9-2.3,7.24-5.37,9.85-9.11c2.62-3.75,4.31-7.92,5.24-12.35c0.96-4.57,1-8.7,1-13.26V64.46
C263.54,58.3,260.29,54.5,254.5,55z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,129 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MusicPlayerV2</title>
<script src="/apple-music/helpers/musickit.js"></script>
<link rel="stylesheet" href="/icon-font.css">
<link rel="stylesheet" href="/apple-music/helpers/playerStyle.css">
<link rel="stylesheet" href="/apple-music/helpers/style.css">
</head>
<body>
<div id="app">
<div v-if="isShowingWarning" class="warning">
<h3>WARNING!</h3>
<p>A client display is being tampered with!</p>
<p>A desktop notification with a warning has already been dispatched.</p>
<button @click="dismissNotification()">Ok</button>
<div class="flash"></div>
</div>
<div v-if="isPreparingToPlay" class="preparingToPlay">
<span class="material-symbols-outlined loading-spinner">autorenew</span>
<h1>Loading player...</h1>
</div>
<div v-if="!isLoggedIn" class="start-page">
<div class="image-wrapper">
<img src="/logo.png" alt="Music player icon" id="logo-main">
<img src="/apple-music/helpers/appleMusicIcon.svg" alt="Apple Music Icon" id="apple-music-logo">
</div>
<h1>Apple Music integration</h1>
<button @click="logInto()" class="button">Log in</button>
</div>
<div v-else class="home">
<div v-if="!hasSelectedPlaylist" class="song-list-wrapper">
<h1>Your playlists</h1>
<button class="button" @click="selectPlaylistFromDisk()">Load playlist file from disk</button>
<div v-if="!hasLoadedPlaylists" style="display: flex; justify-content: center; align-items: center; flex-direction: column;">
<span class="material-symbols-outlined loading-spinner">autorenew</span>
<h3>Loading playlists...</h3>
</div>
<div v-for="playlist in playlists" class="song-list" @click="selectPlaylist( playlist.id )" style="cursor: pointer;">
<h3>{{ playlist.title }}</h3>
</div>
</div>
<div v-else class="home">
<div class="top-bar">
<img src="/logo.png" alt="logo" class="logo">
<audio :src="'/getSongFile?filename=' + basePath + '/' + playingSong.filename" preload="metadata" id="audio-player"></audio>
<div class="player-wrapper">
<div class="player">
<div class="controls">
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'previous' )">skip_previous</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'replay10' )">replay_10</span>
<span class="material-symbols-outlined control-icon play-pause" v-if="!isPlaying && hasSelectedPlaylist" @click="control( 'play' )">play_arrow</span>
<span class="material-symbols-outlined control-icon play-pause" v-else-if="isPlaying && hasSelectedPlaylist" @click="control( 'pause' )">pause</span>
<span class="material-symbols-outlined control-icon play-pause" style="cursor: default;" v-else>play_disabled</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'forward10' )">forward_10</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'next' )" style="margin-right: 1vw;">skip_next</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-if="!isShuffleEnabled" @click="control( 'shuffleOn' )">shuffle</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-else @click="control( 'shuffleOff' )">shuffle_on</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-if="repeatMode === 'off'" @click="control( 'repeatOne' )">repeat</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-else-if="repeatMode === 'one'" @click="control( 'repeatAll' )">repeat_one_on</span>
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-else-if="repeatMode === 'all'" @click="control( 'repeatOff' )">repeat_on</span>
<span class="material-symbols-outlined control-icon" :class="hasSelectedPlaylist ? 'active': 'inactive'" @click="getAdditionalSongInfo()" title="Load additional song information" style="margin-left: 1vw;">upload</span>
<span class="material-symbols-outlined control-icon" :class="hasSelectedPlaylist ? 'active': 'inactive'" @click="exportCurrentPlaylist()" title="Export current playlist" style="margin-right: 1vw;">ios_share</span>
<div class="control-icon" id="settings">
<span class="material-symbols-outlined">info</span>
<div id="showIP">
<h4>IP to connect to:</h4><br>
<p>{{ localIP }}:8081</p>
</div>
</div>
<button @click="search()">S</button>
</div>
<div class="song-info">
<div class="song-info-wrapper">
<img v-if="hasFinishedInit" :src="playingSong.coverArtURL" class="image">
<span class="material-symbols-outlined image" v-else>music_note</span>
<div class="name">
<h3>{{ playingSong.title ?? 'No song selected' }}</h3>
<p>{{ playingSong.artist }}</p>
</div>
<div class="image"></div>
</div>
<div class="playback-pos-info">
<div style="margin-right: auto;">{{ playbackPosBeautified }}</div>
<div @click="toggleShowMode()" style="cursor: pointer;">{{ durationBeautified }}</div>
</div>
<div class="slider">
<progress id="progress-slider" class="progress-slider" :value="sliderProgress" max="1000" @mousedown="( e ) => { setPos( e ) }"
:class="hasFinishedInit ? '' : 'slider-inactive'"></progress>
<div v-if="hasFinishedInit" id="slider-knob" @mousedown="( e ) => { startMove( e ) }"
:style="'left: ' + ( parseInt( originalPos ) + parseInt( sliderPos ) ) + 'px;'">
<div id="slider-knob-style"></div>
</div>
<div v-else id="slider-knob" class="slider-inactive" style="left: 0;">
<div id="slider-knob-style"></div>
</div>
<div id="drag-support" @mousemove="e => { handleDrag( e ) }" @mouseup="() => { stopMove(); }"></div>
</div>
</div>
</div>
</div>
</div>
<div class="pool-wrapper">
<div style="width: 100%;" class="song-list-wrapper">
<div v-for="song in songQueue" class="song-list" :class="[ isPlaying ? ( playingSong.queuePos == song.queuePos ? 'playing': 'not-playing' ) : 'not-playing', !isPlaying && playingSong.filename === song.filename ? 'active-song': undefined ]">
<img :src="song.coverArtURL" class="song-image">
<div v-if="playingSong.queuePos == song.queuePos && 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 play-icon" @click="play( song )">play_arrow</span>
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
<h3>{{ song.title }}</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="/apple-music/helpers/index.js"></script>
</body>
</html>

View File

@@ -1,720 +0,0 @@
/*
* MusicPlayerV2 - index.js
*
* Created by Janis Hutz 11/20/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
/*
Quick side note here: This is terribly ugly code, but I was in a hurry to finish it,
so I had no time to clean it up. I will do that at some point -jh
*/
const app = Vue.createApp( {
data() {
return {
musicKit: null,
isLoggedIn: false,
config: {
'devToken': '',
'userToken': ''
},
playlists: {},
hasSelectedPlaylist: false,
songQueue: {},
queuePos: 0,
pos: 0,
playingSong: {},
isPlaying: false,
isShuffleEnabled: false,
repeatMode: 'off',
playbackPosBeautified: '',
durationBeautified: '',
isShowingRemainingTime: false,
localIP: '',
hasLoadedPlaylists: false,
isPreparingToPlay: false,
additionalSongInfo: {},
hasFinishedInit: false,
isShowingWarning: false,
// For use with playlists that are partially from apple music and
// local drive
isUsingCustomPlaylist: false,
rawLoadedPlaylistData: {},
basePath: '',
audioPlayer: null,
isReconnecting: false,
// slider
offset: 0,
isDragging: false,
sliderPos: 0,
originalPos: 0,
sliderProgress: 0,
active: false,
}
},
methods: {
logInto() {
if ( !this.musicKit.isAuthorized ) {
this.musicKit.authorize().then( () => {
this.isLoggedIn = true;
this.initMusicKit();
} );
} else {
this.musicKit.authorize().then( () => {
this.isLoggedIn = true;
this.initMusicKit();
} );
}
},
initMusicKit () {
fetch( '/getAppleMusicDevToken' ).then( res => {
if ( res.status === 200 ) {
res.text().then( token => {
// MusicKit global is now defined
MusicKit.configure( {
developerToken: token,
app: {
name: 'MusicPlayer',
build: '2'
},
storefrontId: 'CH',
} ).then( () => {
this.config.devToken = token;
this.musicKit = MusicKit.getInstance();
if ( this.musicKit.isAuthorized ) {
this.isLoggedIn = true;
this.config.userToken = this.musicKit.musicUserToken;
}
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off;
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', this.playlistHandler );
} );
} );
}
} );
},
playlistHandler ( data ) {
if ( data.status === 'ok' ) {
const d = data.data.data;
this.playlist = {};
for ( let el in d ) {
this.playlists[ d[ el ].id ] = {
title: d[ el ].attributes.name,
id: d[ el ].id,
playParams: d[ el ].attributes.playParams,
}
}
this.hasLoadedPlaylists = true;
}
},
apiGetRequest( url, callback ) {
if ( this.config.devToken != '' && this.config.userToken != '' ) {
fetch( url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${ this.config.devToken }`,
'Music-User-Token': this.config.userToken
}
} ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
try {
callback( { 'status': 'ok', 'data': json } );
} catch( err ) {}
} );
} else {
try {
callback( { 'status': 'error', 'error': res.status } );
} catch( err ) {}
}
} );
} else return false;
},
getAdditionalSongInfo() {
if ( Object.keys( this.additionalSongInfo ).length < 1 ) {
fetch( '/apple-music/getAdditionalData' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.additionalSongInfo = json;
this.handleAdditionalData();
} );
}
} );
}
},
handleAdditionalData () {
if ( Object.keys( this.additionalSongInfo ).length > 0 ) {
for ( let item in this.songQueue ) {
if ( this.additionalSongInfo[ item ] ) {
for ( let d in this.additionalSongInfo[ item ] ) {
if ( !this.songQueue[ item ][ d ] ) {
this.songQueue[ item ][ d ] = this.additionalSongInfo[ item ][ d ];
}
}
}
}
this.playingSong = this.songQueue[ this.queuePos ];
this.sendUpdate( 'songQueue' );
this.sendUpdate( 'playingSong' );
}
},
selectPlaylist( id ) {
this.isPreparingToPlay = true;
this.musicKit.setQueue( { playlist: id } ).then( () => {
try {
this.loadPlaylist();
this.hasSelectedPlaylist = true;
this.isPreparingToPlay = false;
} catch( err ) {
this.hasSelectedPlaylist = false;
console.error( err );
alert( 'We were unable to play. Please ensure that DRM (yeah sorry it is Apple Music, we cannot do anything about that) is enabled and working' );
}
} ).catch( err => {
console.error( 'ERROR whilst settings Queue', err );
} );
},
handleDrag( e ) {
if ( this.isDragging ) {
if ( 0 < this.originalPos + e.screenX - this.offset && this.originalPos + e.screenX - this.offset < document.getElementById( 'progress-slider' ).clientWidth - 5 ) {
this.sliderPos = e.screenX - this.offset;
this.calcProgressPos();
}
}
},
startMove( e ) {
this.offset = e.screenX;
this.isDragging = true;
document.getElementById( 'drag-support' ).classList.add( 'drag-support-active' );
},
stopMove() {
this.originalPos += parseInt( this.sliderPos );
this.isDragging = false;
this.offset = 0;
this.sliderPos = 0;
document.getElementById( 'drag-support' ).classList.remove( 'drag-support-active' );
this.calcPlaybackPos();
},
setPos ( e ) {
if ( this.hasSelectedPlaylist ) {
this.originalPos = e.offsetX;
this.calcProgressPos();
this.calcPlaybackPos();
if ( this.playingSong.origin === 'apple-music' ) {
this.musicKit.seekToTime( this.pos );
} else {
this.audioPlayer.currentTime = this.pos;
}
this.sendUpdate( 'pos' );
}
},
calcProgressPos() {
this.sliderProgress = Math.ceil( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider' ).clientWidth - 5 ) * 1000 );
},
calcPlaybackPos() {
this.pos = Math.round( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider' ).clientWidth - 5 ) * this.playingSong.duration );
},
sendUpdate( update ) {
let data = {};
let up = update;
if ( update === 'pos' ) {
data = this.pos;
} else if ( update === 'playingSong' ) {
data = this.playingSong;
} else if ( update === 'isPlaying' ) {
data = this.isPlaying;
} else if ( update === 'songQueue' ) {
data = this.songQueue;
} else if ( update === 'queuePos' ) {
data = this.queuePos;
} else if ( update === 'posReset' ) {
data = 0;
up = 'pos';
}
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'type': up, 'data': data } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.error( err );
} );
},
loadPlaylist() {
const songQueue = this.musicKit.queue.items;
for ( let item in songQueue ) {
this.songQueue[ item ] = {
'artist': songQueue[ item ].attributes.artistName,
'title': songQueue[ item ].attributes.name,
'year': songQueue[ item ].attributes.releaseDate,
'genre': songQueue[ item ].attributes.genreNames,
'duration': Math.round( songQueue[ item ].attributes.durationInMillis / 1000 ),
'filename': songQueue[ item ].id,
'coverArtOrigin': 'api',
'hasCoverArt': true,
'queuePos': item,
'origin': 'apple-music',
}
let url = songQueue[ item ].attributes.artwork.url;
url = url.replace( '{w}', songQueue[ item ].attributes.artwork.width );
url = url.replace( '{h}', songQueue[ item ].attributes.artwork.height );
this.songQueue[ item ][ 'coverArtURL' ] = url;
this.handleAdditionalData();
this.sendUpdate( 'songQueue' );
}
},
control( action ) {
if ( action === 'play' ) {
if ( !this.playingSong.origin ) {
this.play( this.songQueue[ 0 ] );
this.isPlaying = true;
} else {
if ( this.playingSong.origin === 'apple-music' ) {
if( !this.musicKit || !this.isPlaying ) {
this.musicKit.play().then( () => {
this.sendUpdate( 'pos' );
} ).catch( err => {
console.log( 'player failed to start' );
console.log( err );
} );
} else {
this.musicKit.pause().then( () => {
this.musicKit.play().catch( err => {
console.log( 'player failed to start' );
console.log( err );
} );
} );
}
try {
this.audioPlayer.pause();
} catch ( err ) {}
} else {
this.audioPlayer.play();
this.musicKit.pause();
}
this.isPlaying = true;
try {
clearInterval( this.progressTracker );
} catch( err ) {};
this.progressTracker = setInterval( () => {
if ( this.playingSong.origin === 'apple-music' ) {
this.pos = parseInt( this.musicKit.currentPlaybackTime );
} else {
this.pos = parseInt( this.audioPlayer.currentTime );
}
if ( this.pos > this.playingSong.duration - 1 ) {
this.control( 'next' );
}
const minuteCount = Math.floor( this.pos / 60 );
this.playbackPosBeautified = minuteCount + ':';
if ( ( '' + minuteCount ).length === 1 ) {
this.playbackPosBeautified = '0' + minuteCount + ':';
}
const secondCount = Math.floor( this.pos - minuteCount * 60 );
if ( ( '' + secondCount ).length === 1 ) {
this.playbackPosBeautified += '0' + secondCount;
} else {
this.playbackPosBeautified += secondCount;
}
if ( this.isShowingRemainingTime ) {
const minuteCounts = Math.floor( ( this.playingSong.duration - this.pos ) / 60 );
this.durationBeautified = '-' + String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) {
this.durationBeautified = '-0' + minuteCounts + ':';
}
const secondCounts = Math.floor( ( this.playingSong.duration - this.pos ) - minuteCounts * 60 );
if ( ( '' + secondCounts ).length === 1 ) {
this.durationBeautified += '0' + secondCounts;
} else {
this.durationBeautified += secondCounts;
}
}
}, 50 );
this.sendUpdate( 'pos' );
this.sendUpdate( 'isPlaying' );
}
} else if ( action === 'pause' ) {
if ( this.playingSong.origin === 'apple-music' ) {
this.musicKit.pause();
} else {
this.audioPlayer.pause();
}
this.sendUpdate( 'pos' );
try {
clearInterval( this.progressTracker );
clearInterval( this.notifier );
} catch ( err ) {};
this.isPlaying = false;
this.sendUpdate( 'isPlaying' );
} else if ( action === 'replay10' ) {
if ( this.playingSong.origin === 'apple-music' ) {
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
this.pos = this.musicKit.currentPlaybackTime;
} else {
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
this.pos = this.audioPlayer.currentTime;
}
this.sendUpdate( 'pos' );
} else if ( action === 'forward10' ) {
if ( this.playingSong.origin === 'apple-music' ) {
if ( this.musicKit.currentPlaybackTime < ( this.playingSong.duration - 10 ) ) {
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
this.pos = this.musicKit.currentPlaybackTime;
this.sendUpdate( 'pos' );
} else {
if ( this.repeatMode !== 'one' ) {
this.control( 'next' );
} else {
this.musicKit.seekToTime( 0 );
this.pos = this.musicKit.currentPlaybackTime;
this.sendUpdate( 'pos' );
}
}
} else {
if ( this.audioPlayer.currentTime < ( this.playingSong.duration - 10 ) ) {
this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10;
this.pos = this.audioPlayer.currentTime;
this.sendUpdate( 'pos' );
} else {
if ( this.repeatMode !== 'one' ) {
this.control( 'next' );
} else {
this.audioPlayer.currentTime = 0;
this.pos = this.audioPlayer.currentTime;
this.sendUpdate( 'pos' );
}
}
}
} else if ( action === 'reset' ) {
clearInterval( this.progressTracker );
this.pos = 0;
if ( this.playingSong.origin === 'apple-music' ) {
this.musicKit.seekToTime( 0 );
} else {
this.audioPlayer.currentTime = 0;
}
this.sendUpdate( 'pos' );
} else if ( action === 'next' ) {
if ( this.queuePos < parseInt( Object.keys( this.songQueue ).length ) - 1 ) {
this.queuePos = parseInt( this.queuePos ) + 1;
this.play( this.songQueue[ this.queuePos ] );
} else {
if ( this.repeatMode === 'all' ) {
this.queuePos = 0;
this.play( this.songQueue[ 0 ] );
} else {
this.control( 'pause' );
}
}
} else if ( action === 'previous' ) {
if ( this.pos > 3 ) {
this.pos = 0;
if ( this.isUsingCustomPlaylist ) {
this.audioPlayer.currentTime = 0;
this.sendUpdate( 'pos' );
} else {
this.musicKit.seekToTime( 0 ).then( () => {
this.sendUpdate( 'pos' );
this.control( 'play' );
} );
}
} else {
if ( this.queuePos > 0 ) {
this.queuePos = parseInt( this.queuePos ) - 1;
this.play( this.songQueue[ this.queuePos ] );
} else {
this.queuePos = parseInt( Object.keys( this.songQueue ).length ) - 1;
this.play[ this.songQueue[ this.queuePos ] ];
}
}
} else if ( action === 'shuffleOff' ) {
// TODO: Make shuffle function
this.isShuffleEnabled = false;
// this.loadPlaylist();
alert( 'not implemented yet' );
} else if ( action === 'shuffleOn' ) {
this.isShuffleEnabled = true;
alert( 'not implemented yet' );
// this.loadPlaylist();
} else if ( action === 'repeatOne' ) {
this.repeatMode = 'one';
} else if ( action === 'repeatAll' ) {
this.repeatMode = 'all';
} else if ( action === 'repeatOff' ) {
this.musicKit.repeatMode = MusicKit.PlayerRepeatMode.none;
this.repeatMode = 'off';
}
},
play( song, specificID ) {
let foundSong = specificID ?? 0;
if ( !specificID ) {
for ( let s in this.songQueue ) {
if ( this.songQueue[ s ] === song ) {
foundSong = s;
}
}
}
this.queuePos = foundSong;
this.sendUpdate( 'queuePos' );
this.pos = 0;
this.sendUpdate( 'posReset' );
this.playingSong = song;
this.sendUpdate( 'playingSong' );
if ( song.origin === 'apple-music' ) {
this.musicKit.setQueue( { 'song': song.filename } ).then( () => {
setTimeout( () => {
this.control( 'play' );
}, 500 );
} ).catch( ( err ) => {
console.log( err );
} );
} else {
setTimeout( () => {
this.control( 'play' );
}, 500 );
}
const minuteCounts = Math.floor( ( this.playingSong.duration ) / 60 );
this.durationBeautified = String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) {
this.durationBeautified = '0' + minuteCounts + ':';
}
const secondCounts = Math.floor( ( this.playingSong.duration ) - minuteCounts * 60 );
if ( ( '' + secondCounts ).length === 1 ) {
this.durationBeautified += '0' + secondCounts;
} else {
this.durationBeautified += secondCounts;
}
this.hasFinishedInit = true;
},
toggleShowMode() {
this.isShowingRemainingTime = !this.isShowingRemainingTime;
},
exportCurrentPlaylist() {
let fetchOptions = {
method: 'post',
body: JSON.stringify( this.songQueue ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( '/savePlaylist', fetchOptions ).then( res => {
if ( res.status === 200 ) {
console.log( 'saved' );
}
} );
},
selectPlaylistFromDisk() {
this.isPreparingToPlay = true;
let playlistSongs = [];
fetch( '/loadPlaylist' ).then( res => {
res.json().then( data => {
this.rawLoadedPlaylistData = data.data;
this.basePath = data.path.slice( 0, data.path.lastIndexOf( '/' ) );
for ( let song in this.rawLoadedPlaylistData ) {
if ( this.rawLoadedPlaylistData[ song ].origin === 'apple-music' ) {
playlistSongs.push( this.rawLoadedPlaylistData[ song ].filename );
}
}
this.musicKit.setQueue( { songs: playlistSongs } ).then( () => {
this.isUsingCustomPlaylist = true;
try {
this.loadCustomPlaylist();
this.hasSelectedPlaylist = true;
this.isPreparingToPlay = false;
} catch( err ) {
this.hasSelectedPlaylist = false;
console.error( err );
alert( 'We were unable to play. Please ensure that DRM (yeah sorry it is Apple Music, we cannot do anything about that) is enabled and working' );
}
} ).catch( err => {
console.error( 'ERROR whilst settings Queue', err );
} );
} );
} );
},
loadCustomPlaylist() {
const songQueue = this.musicKit.queue.items;
let offset = 0;
( async() => {
for ( let item in this.rawLoadedPlaylistData ) {
if ( this.rawLoadedPlaylistData[ item ].origin === 'apple-music' ) {
this.songQueue[ item ] = {
'artist': songQueue[ item - offset ].attributes.artistName,
'title': songQueue[ item - offset ].attributes.name,
'year': songQueue[ item - offset ].attributes.releaseDate,
'genre': songQueue[ item - offset ].attributes.genreNames,
'duration': Math.round( songQueue[ item - offset ].attributes.durationInMillis / 1000 ),
'filename': songQueue[ item - offset ].id,
'coverArtOrigin': 'api',
'hasCoverArt': true,
'queuePos': item,
'origin': 'apple-music',
'offset': offset,
}
let url = songQueue[ item - offset ].attributes.artwork.url;
url = url.replace( '{w}', songQueue[ item - offset ].attributes.artwork.width );
url = url.replace( '{h}', songQueue[ item - offset ].attributes.artwork.height );
this.songQueue[ item ][ 'coverArtURL' ] = url;
} else {
offset += 1;
const queryParameters = {
term: ( this.rawLoadedPlaylistData[ item ].artist ?? '' ) + ' ' + ( this.rawLoadedPlaylistData[ item ].title ?? '' ),
types: [ 'songs' ],
};
// TODO: Make storefront adjustable
const result = await this.musicKit.api.music( '/v1/catalog/ch/search', queryParameters );
let json;
try {
const res = await fetch( '/getMetadata?file=' + this.basePath + '/' + this.rawLoadedPlaylistData[ item ].filename );
json = await res.json();
} catch( err ) {}
if ( result.data ) {
if ( result.data.results.songs ) {
const dat = result.data.results.songs.data[ 0 ];
console.log( json );
this.songQueue[ item ] = {
'artist': dat.attributes.artistName,
'title': dat.attributes.name,
'year': dat.attributes.releaseDate,
'genre': dat.attributes.genreNames,
'duration': json ? Math.round( json.duration ) : undefined,
'filename': this.rawLoadedPlaylistData[ item ].filename,
'coverArtOrigin': 'api',
'hasCoverArt': true,
'queuePos': item,
'origin': 'local',
}
let url = dat.attributes.artwork.url;
url = url.replace( '{w}', dat.attributes.artwork.width );
url = url.replace( '{h}', dat.attributes.artwork.height );
this.songQueue[ item ][ 'coverArtURL' ] = url;
}
}
}
this.handleAdditionalData();
this.sendUpdate( 'songQueue' );
}
} )();
setTimeout( () => {
this.audioPlayer = document.getElementById( 'audio-player' );
}, 1000 );
},
search() {
( async() => {
const searchTerm = prompt( 'Enter search term...' )
const queryParameters = {
term: ( searchTerm ),
types: [ 'songs' ],
};
// TODO: Make storefront adjustable
const result = await this.musicKit.api.music( '/v1/catalog/ch/search', queryParameters );
console.log( result );
} )();
},
connectToNotifier() {
let source = new EventSource( '/mainNotifier', { withCredentials: true } );
source.onmessage = ( e ) => {
let data;
try {
data = JSON.parse( e.data );
} catch ( err ) {
data = { 'type': e.data };
}
if ( data.type === 'blur' ) {
this.isShowingWarning = true;
} else if ( data.type === 'visibility' ) {
this.isShowingWarning = true;
}
};
source.onopen = () => {
this.isReconnecting = false;
console.log( 'client notifier connected successfully' );
};
let self = this;
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
if ( e.target.readyState == EventSource.CLOSED ) {
setTimeout( () => {
if ( !self.isReconnecting ) {
console.log( 'disconnected' );
console.log( 'reconnecting...' );
self.isReconnecting = true;
self.tryReconnect();
}
}, 1000 );
}
}, false );
},
tryReconnect() {
const int = setInterval( () => {
if ( !this.isReconnecting ) {
clearInterval( int );
} else {
this.connectToNotifier();
}
}, 1000 );
},
dismissNotification() {
this.isShowingWarning = false;
}
},
watch: {
pos() {
if ( !this.isDragging ) {
this.sliderProgress = Math.ceil( this.pos / this.playingSong.duration * 1000 + 2 );
this.originalPos = Math.ceil( this.pos / this.playingSong.duration * ( document.getElementById( 'progress-slider' ).scrollWidth - 5 ) );
}
}
},
created() {
document.addEventListener( 'keydown', ( e ) => {
if ( e.key === ' ' ) {
e.preventDefault();
if ( !this.isPlaying ) {
this.control( 'play' );
} else {
this.control( 'pause' );
}
} else if ( e.key === 'ArrowRight' ) {
e.preventDefault();
this.control( 'next' );
} else if ( e.key === 'ArrowLeft' ) {
e.preventDefault();
this.control( 'previous' );
}
} );
if ( !window.MusicKit ) {
document.addEventListener( 'musickitloaded', () => {
self.initMusicKit();
} );
} else {
this.initMusicKit();
}
this.connectToNotifier();
fetch( '/getLocalIP' ).then( res => {
if ( res.status === 200 ) {
res.text().then( ip => {
this.localIP = ip;
} );
}
} );
},
} ).mount( '#app' );

File diff suppressed because one or more lines are too long

View File

@@ -1,136 +0,0 @@
.song-info {
background-color: #8e9ced;
height: 13vh;
width: 50%;
margin-left: auto;
margin-right: auto;
position: relative;
}
.image {
width: 7vh;
height: 7vh;
object-fit: cover;
object-position: center;
font-size: 7vh;
margin-left: 1vh;
margin-top: 1vh;
}
.name {
margin-left: auto;
margin-right: auto;
}
.song-info-wrapper {
display: flex;
flex-direction: row;
}
.song-info-wrapper h3 {
margin: 0;
margin-bottom: 0.5vh;
margin-top: 1vh;
}
.controls {
margin-left: 5%;
display: flex;
justify-content: center;
align-items: center;
}
.control-icon {
cursor: pointer;
font-size: 3vh;
user-select: none;
}
.play-pause {
font-size: 5vh;
}
.inactive {
color: gray;
cursor: default;
}
.player {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.playback-pos-info {
display: flex;
flex-direction: row;
width: 98%;
margin-left: 1%;
position: absolute;
bottom: 17px;
}
#showIP {
background-color: rgb(63, 63, 63);
display: none;
position: absolute;
min-height: 16vh;
padding: 2vh;
min-width: 20vw;
z-index: 10;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 70%;
border-radius: 5px 10px 10px 10px;
}
#showIP h4, #showIP p {
margin: 0;
}
#settings:hover #showIP {
display: flex;
}
#showIP::before {
content: " ";
position: absolute;
bottom: 100%; /* At the bottom of the tooltip */
left: 0;
margin-left: 3px;
border-width: 10px;
border-style: solid;
border-color: transparent transparent rgb(63, 63, 63) transparent;
}
/* Prepare to play */
.preparingToPlay {
background-color: rgba(0, 0, 0, 0.7);
width: 100vw;
height: 100vh;
position: fixed;
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
top: 0;
left: 0;
}
.loading-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}

View File

@@ -1,359 +0,0 @@
/*
* MusicPlayerV2 - style.css
*
* Created by Janis Hutz 11/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: rgb(49, 49, 49);
font-family: sans-serif;
color: white;
}
/* Start page style */
.start-page {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.image-wrapper {
height: 50vh;
width: 50vh;
}
#logo-main {
height: 50vh;
}
#apple-music-logo {
height: 10vh;
position: relative;
bottom: 10vh;
left: 41vh;
}
.button {
padding: 20px;
background-color: rgb(1, 1, 88);
color: white;
border: none;
border-radius: 50px;
transition: all 1s;
cursor: pointer;
font-size: 120%;
}
.button:hover {
background-color: rgb(1, 1, 120);
border-radius: 20px;
}
/* Main style */
.home {
width: 100%;
height: 100%;
}
.pool-wrapper {
height: 84vh;
margin-top: 16vh;
}
.top-bar {
top: 0;
margin-left: auto;
margin-right: auto;
position: fixed;
z-index: 8;
width: 99%;
height: 15vh;
display: flex;
align-items: center;
flex-direction: row;
border: white 2px solid;
background-color: rgb(49, 49, 49);
}
.player-wrapper {
width: 70vw;
margin-right: auto;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.logo {
height: 13vh;
margin-left: 3%;
margin-right: auto;
}
/* Media Pool */
.playing-symbols {
position: absolute;
left: 10%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
margin: 0;
width: 5vw;
height: 5vw;
background-color: rgba( 0, 0, 0, 0.6 );
}
.playing-symbols-wrapper {
width: 4vw;
height: 5vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.playing-bar {
height: 60%;
background-color: white;
width: 10%;
border-radius: 50px;
margin: auto;
}
#bar-1 {
animation: music-playing 0.9s infinite ease-in-out;
}
#bar-2 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.3s;
}
#bar-3 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.6s;
}
@keyframes music-playing {
0% {
transform: scaleY( 1 );
}
50% {
transform: scaleY( 0.5 );
}
100% {
transform: scaleY( 1 );
}
}
.loading-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}
.media-pool {
width: 100%;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.no-songs {
height: 50vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.song-list-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.song-list {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 80%;
margin: 2px;
padding: 1vh;
border: 1px white solid;
}
.song-list h3 {
margin: 0;
display: block;
margin-left: 10px;
margin-right: auto;
}
.song-list .song-image {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
}
.play-icon, .pause-icon {
display: none;
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
cursor: pointer;
user-select: none;
}
.playing:hover .pause-icon {
display: block;
}
.playing:hover .playing-symbols {
display: none;
}
.song-list:hover .song-image {
display: none;
}
.not-playing:hover .play-icon {
display: block;
}
.active-song .pause-icon {
display: block;
}
.active-song .song-image, .active-song:hover .pause-icon {
display: none;
}
/* Slider */
.progress-slider {
width: 100%;
margin: 0;
position: absolute;
left: 0;
bottom: 0;
height: 5px;
cursor: pointer;
background-color: #baf4c9;
}
.progress-slider::-webkit-progress-value {
background-color: #baf4c9;
}
#slider-knob {
height: 20px;
width: 10px;
display: flex;
justify-content: flex-start;
align-items: flex-end;
position: absolute;
bottom: 0;
left: 0;
z-index: 2;
cursor: grab;
}
#slider-knob-style {
background-color: #baf4c9;
height: 15px;
width: 5px;
}
#drag-support {
display: none;
opacity: 0;
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
z-index: 10;
cursor: grabbing;
}
.drag-support-active {
display: block !important;
}
.slider-inactive {
cursor: default !important;
}
.warning {
display: flex;
justify-content: center;
align-items: center;
width: 40vw;
height: 50vh;
font-size: 2vh;
background-color: rgb(255, 0, 0);
color: white;
position: fixed;
right: 1vh;
top: 1vh;
flex-direction: column;
z-index: 100;
}
.warning h3 {
font-size: 4vh;
}
.warning .flash {
background-color: rgba(255, 0, 0, 0.4);
animation: flashing linear infinite 1s;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
position: fixed;
z-index: -1;
}
@keyframes flashing {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -1,44 +0,0 @@
.background {
position: fixed;
left: -50vw;
width: 200vw;
height: 200vw;
top: -50vw;
z-index: -1;
filter: blur(10px);
background: conic-gradient( blue, green, red, blue );
animation: gradientAnim 10s infinite linear;
background-position: center;
}
.beat, .beat-manual {
height: 100%;
width: 100%;
background-color: rgba( 0, 0, 0, 0.15 );
display: none;
}
.beat {
animation: beatAnim 0.6s infinite linear;
}
@keyframes beatAnim {
0% {
background-color: rgba( 0, 0, 0, 0.2 );
}
20% {
background-color: rgba( 0, 0, 0, 0 );
}
100% {
background-color: rgba( 0, 0, 0, 0.2 );
}
}
@keyframes gradientAnim {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 360deg );
}
}

View File

@@ -1,24 +0,0 @@
/* fallback */
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 100 700;
src: url(/iconFont.woff2) format('woff2');
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

View File

@@ -1,208 +0,0 @@
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
color: white;
}
body {
font-family: sans-serif;
}
.content {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.playing-symbols {
position: absolute;
left: 10vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
width: 5vw;
height: 5vw;
background-color: rgba( 0, 0, 0, 0.6 );
}
.playing-symbols-wrapper {
width: 4vw;
height: 5vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.playing-bar {
height: 60%;
background-color: white;
width: 10%;
border-radius: 50px;
margin: auto;
}
#bar-1 {
animation: music-playing 0.9s infinite ease-in-out;
}
#bar-2 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.3s;
}
#bar-3 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.6s;
}
@keyframes music-playing {
0% {
transform: scaleY( 1 );
}
50% {
transform: scaleY( 0.5 );
}
100% {
transform: scaleY( 1 );
}
}
.song-list-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.song-list {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 80%;
margin: 2px;
padding: 1vh;
border: 1px white solid;
background-color: rgba( 0, 0, 0, 0.4 );
}
.song-details-wrapper {
margin: 0;
display: block;
margin-left: 10px;
margin-right: auto;
}
.song-list .song-image {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
}
.pause-icon {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw !important;
user-select: none;
}
.current-song-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 55vh;
width: 100%;
margin-bottom: 0.5%;
margin-top: 0.25%;
}
.current-song {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 1vh;
padding: 1vh;
text-align: center;
background-color: rgba( 0, 0, 0, 0.4 );
}
.fancy-view-song-art {
height: 30vh;
width: 30vh;
object-fit: cover;
object-position: center;
margin-bottom: 10px;
font-size: 30vh !important;
}
#app {
background-color: rgba( 0, 0, 0, 0 );
}
#progress, #progress::-webkit-progress-bar {
background-color: rgba(45, 28, 145);
color: rgba(45, 28, 145);
width: 30vw;
border: none;
border-radius: 0px;
accent-color: white;
-webkit-appearance: none;
appearance: none;
}
#progress::-moz-progress-bar {
background-color: white;
}
#progress::-webkit-progress-value {
background-color: white !important;
}
.mode-selector-wrapper {
opacity: 0;
position: fixed;
right: 0.5%;
top: 0.5%;
padding: 0.5%;
}
.mode-selector-wrapper:hover {
opacity: 1;
}
.dancing-style {
font-size: 250%;
margin: 0;
font-weight: bolder;
}
.info {
position: fixed;
font-size: 12px;
transform: rotate(270deg);
left: -150px;
margin: 0;
padding: 0;
top: 50%;
}

View File

@@ -1,72 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=7">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Showcase - MusicPlayerV2</title>
<link rel="stylesheet" href="/showcase.css">
<link rel="stylesheet" href="/backgroundAnim.css">
<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" />
</head>
<body>
<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>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
<script src="/showcase.js"></script>
</body>
</html>

View File

@@ -1,359 +0,0 @@
// eslint-disable-next-line no-undef
const { createApp } = Vue;
createApp( {
data() {
return {
hasLoaded: false,
songs: [],
playingSong: {},
isPlaying: false,
pos: 0,
queuePos: 0,
colourPalette: [],
progressBar: 0,
timeTracker: null,
visualizationSettings: 'mic',
micAnalyzer: null,
beatDetected: false,
colorThief: null,
lastDispatch: new Date().getTime() - 5000,
isReconnecting: false,
};
},
computed: {
songQueue() {
let ret = [];
let pos = 0;
for ( let song in this.songs ) {
if ( pos >= this.queuePos ) {
ret.push( this.songs[ song ] );
}
pos += 1;
}
return ret;
},
getTimeUntil() {
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 ( this.isPlaying ) {
if ( timeRemaining === 0 ) {
return 'Currently playing';
} else {
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
}
} else {
if ( timeRemaining === 0 ) {
return 'Plays next';
} else {
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
}
}
}
}
},
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 ) {
setTimeout( () => {
if ( !self.isReconnecting ) {
console.log( 'disconnected' );
self.isReconnecting = true;
self.tryReconnect();
}
}, 1000 );
}
}, false );
},
tryReconnect() {
const int = setInterval( () => {
if ( !this.isReconnecting ) {
clearInterval( int );
} else {
this.connect();
}
}, 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' );

View File

@@ -1,162 +0,0 @@
<template>
<div class="fancy-view">
<span class="material-symbols-outlined fancy-view-song-art" v-if="!song.hasCoverArt">music_note</span>
<img v-else-if="song.hasCoverArt && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="fancy-view-song-art">
<img v-else :src="'http://localhost:8081/getSongCover?filename=' + song.filename" class="fancy-view-song-art">
<button @click="exit()" id="exit-button"><span class="material-symbols-outlined" style="font-size: 4vh;">close</span></button>
<div class="controls-wrapper">
<div class="song-info">
<h3>{{ song.title }}</h3>
<p>{{ song.artist }}</p>
</div>
<div class="controls">
<span class="material-symbols-outlined control-icon" @click="control( 'previous' )">skip_previous</span>
<span class="material-symbols-outlined control-icon" @click="control( 'replay10' )">replay_10</span>
<span class="material-symbols-outlined control-icon play-pause" v-if="!isPlaying" @click="control( 'play' )">play_arrow</span>
<span class="material-symbols-outlined control-icon play-pause" v-else-if="isPlaying" @click="control( 'pause' )">pause</span>
<span class="material-symbols-outlined control-icon" @click="control( 'forward10' )">forward_10</span>
<span class="material-symbols-outlined control-icon" @click="control( 'next' )">skip_next</span>
</div>
<div class="slider-wrapper">
<sliderView :active="true" :position="playbackPos" :duration="song.duration" @pos="( p ) => { setPos( p ) }"
name="fancy" class="slider"></sliderView>
<div class="playback-pos-info">
<div style="margin-right: auto;">{{ playbackPosBeautified }}</div>
<div>{{ durationBeautified }}</div>
</div>
</div>
<div class="shuffle-repeat-wrapper">
<span class="material-symbols-outlined control-icon" v-if="!shuffle" @click="control( 'shuffleOn' )">shuffle</span>
<span class="material-symbols-outlined control-icon" v-else @click="control( 'shuffleOff' )">shuffle_on</span>
<span class="material-symbols-outlined control-icon" v-if="repeatMode === 'off'" @click="control( 'repeatOne' )">repeat</span>
<span class="material-symbols-outlined control-icon" v-else-if="repeatMode === 'one'" @click="control( 'repeatAll' )">repeat_one_on</span>
<span class="material-symbols-outlined control-icon" v-else-if="repeatMode === 'all'" @click="control( 'repeatOff' )">repeat_on</span>
</div>
</div>
</div>
</template>
<style scoped>
#exit-button {
position: fixed;
right: 1vw;
top: 1vw;
background-color: rgba( 0,0,0,0 );
border: none;
cursor: pointer;
color: var( --primary-color );
}
.fancy-view {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
flex-direction: column;
z-index: 20;
height: 100vh;
width: 100vw;
top: 0;
left: 0;
background-color: var( --background-color );
}
.controls-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.slider-wrapper {
position: relative;
margin-top: 40px;
width: 40vh;
margin-bottom: 20px
}
.fancy-view-song-art {
height: 40vh;
width: 40vh;
object-fit: cover;
object-position: center;
margin-bottom: 20px;
font-size: 40vh;
}
.controls {
width: 50vw;
display: flex;
justify-content: center;
align-items: center;
}
.control-icon {
cursor: pointer;
font-size: 3vh;
user-select: none;
}
.play-pause {
font-size: 5vh;
}
.playback-pos-info {
display: flex;
flex-direction: row;
width: 98%;
margin-left: 1%;
position: absolute;
bottom: 17px;
left: 0;
}
.shuffle-repeat-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
</style>
<script>
import SliderView from './sliderView.vue';
export default {
methods: {
control ( instruction ) {
this.$emit( 'control', instruction );
},
setPos ( pos ) {
this.$emit( 'posUpdate', pos );
},
exit() {
this.$emit( 'control', 'exitFancyView' );
}
},
components: {
SliderView,
},
props: {
song: {
type: Object,
},
playbackPos: {
type: Number,
},
playbackPosBeautified: {
type: String,
},
durationBeautified: {
type: String,
},
shuffle: {
type: Boolean,
},
isPlaying: {
type: Boolean,
},
repeatMode: {
type: String,
}
}
}
</script>

View File

@@ -1,388 +0,0 @@
<template>
<div class="media-pool" :style="isShowingFancyView ? 'overflow: hidden;' : ''">
<div v-if="hasLoadedSongs" style="width: 100%;" class="song-list-wrapper">
<div v-for="song in songQueue" class="song-list" :class="[ isPlaying ? ( currentlyPlaying === song.filename ? 'playing': 'not-playing' ) : 'not-playing', !isPlaying && currentlyPlaying === song.filename ? 'active-song': undefined ]">
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt">music_note</span>
<img v-else-if="song.hasCoverArt && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
<img v-else :src="'http://localhost:8081/getSongCover?filename=' + song.filename" class="song-image">
<div v-if="currentlyPlaying === 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 play-icon" @click="play( song )">play_arrow</span>
<span class="material-symbols-outlined pause-icon" @click="pause( song )">pause</span>
<h3>{{ song.title }}</h3>
</div>
</div>
<div v-else-if="isLoadingSongs" class="no-songs">
<h3>Loading songs...</h3>
<p>Analyzing metadata...</p>
<span class="material-symbols-outlined loading-spinner">autorenew</span>
</div>
<div v-else-if="errorOccurredLoading" class="no-songs">
<h3>This directory does not exist!</h3>
<button @click="loadSongs()">Load songs</button>
</div>
<div v-else class="no-songs">
<h3>No songs loaded</h3>
<button @click="loadSongs()">Load songs</button>
<button @click="useAppleMusic()">Use AppleMusic (opens a web-browser)</button>
</div>
</div>
</template>
<style>
.playing-symbols {
position: absolute;
left: 10%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
margin: 0;
width: 5vw;
height: 5vw;
background-color: rgba( 0, 0, 0, 0.6 );
}
.playing-symbols-wrapper {
width: 4vw;
height: 5vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.playing-bar {
height: 60%;
background-color: white;
width: 10%;
border-radius: 50px;
margin: auto;
}
#bar-1 {
animation: music-playing 0.9s infinite ease-in-out;
}
#bar-2 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.3s;
}
#bar-3 {
animation: music-playing 0.9s infinite ease-in-out;
animation-delay: 0.6s;
}
@keyframes music-playing {
0% {
transform: scaleY( 1 );
}
50% {
transform: scaleY( 0.5 );
}
100% {
transform: scaleY( 1 );
}
}
.loading-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}
.media-pool {
width: 100%;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.no-songs {
height: 50vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.song-list-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.song-list {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 80%;
margin: 2px;
padding: 1vh;
border: 1px var( --border-color ) solid;
}
.song-list h3 {
margin: 0;
display: block;
margin-left: 10px;
margin-right: auto;
}
.song-list .song-image {
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
}
.play-icon, .pause-icon {
display: none;
width: 5vw;
height: 5vw;
object-fit: cover;
object-position: center;
font-size: 5vw;
cursor: pointer;
user-select: none;
}
.playing:hover .pause-icon {
display: block;
}
.playing:hover .playing-symbols {
display: none;
}
.song-list:hover .song-image {
display: none;
}
.not-playing:hover .play-icon {
display: block;
}
.active-song .pause-icon {
display: block;
}
.active-song .song-image, .active-song:hover .pause-icon {
display: none;
}
</style>
<script>
export default {
name: 'HomeView',
data() {
return {
hasLoadedSongs: false,
isLoadingSongs: false,
allSongs: [],
songQueue: [],
loadedDirs: [],
allowedFiletypes: [ '.mp3', '.wav' ],
currentlyPlaying: '',
isPlaying: false,
songPos: 0,
repeat: false,
isShowingFancyView: false,
errorOccurredLoading: false,
coverArtSetting: 'api',
doOverride: false,
}
},
methods: {
update( status ) {
if ( status.type === 'playback' ) {
this.isPlaying = status.status;
} else if ( status.type === 'next' ) {
if ( this.songPos < this.songQueue.length - 1 ) {
this.songPos += 1;
this.queueHandler( 'load' );
} else {
this.songPos = 0;
if ( this.repeat ) {
this.queueHandler( 'load' );
} else {
this.isPlaying = false;
this.currentlyPlaying = '';
this.$emit( 'com', { 'type': 'startPlayback', 'song': this.songQueue[ 0 ], 'autoplay': false } );
this.$emit( 'com', { 'type': 'pause' } );
}
}
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'type': 'queuePos', 'data': this.songPos } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.error( err );
} );
} else if ( status.type === 'previous' ) {
if ( this.songPos > 0 ) {
this.songPos -= 1;
} else {
this.songPos = this.songQueue.length - 1;
}
this.queueHandler( 'load' );
} else if ( status.type === 'shuffle' ) {
this.queueHandler( 'shuffle' );
} else if ( status.type === 'shuffleOff' ) {
this.queueHandler( 'shuffleOff' );
} else if ( status.type === 'repeat' ) {
this.repeat = true;
} else if ( status.type === 'repeatOff' ) {
this.repeat = false;
} else if ( status.type === 'fancyView' ) {
this.isShowingFancyView = status.status;
}
},
queueHandler ( command ) {
if ( command === 'load' ) {
this.play( this.songQueue[ this.songPos ] );
} else if ( command === 'shuffle' ) {
let processArray = JSON.parse( JSON.stringify( this.allSongs ) );
let newOrder = [];
for ( let i = 0; i < this.allSongs.length; i++ ) {
let randNum = Math.floor( Math.random() * this.allSongs.length );
while ( newOrder.includes( randNum ) ) {
randNum = Math.floor( Math.random() * this.allSongs.length );
}
newOrder.push( randNum );
}
this.songQueue = [];
for ( let el in newOrder ) {
this.songQueue.push( processArray[ newOrder[ el ] ] );
}
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'type': 'songQueue', 'data': this.songQueue } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.error( err );
} );
} else if ( command === 'shuffleOff' ) {
this.songQueue = JSON.parse( JSON.stringify( this.allSongs ) );
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'type': 'songQueue', 'data': this.songQueue } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.error( err );
} );
}
},
loadSongs() {
this.isLoadingSongs = true;
fetch( 'http://localhost:8081/openSongs' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
if ( Object.keys( json ).length > 0 ) {
this.loadedDirs = json.data;
this.indexFiles();
} else {
this.isLoadingSongs = false;
}
} );
}
} );
},
indexFiles () {
for ( let dir in this.loadedDirs ) {
fetch( 'http://localhost:8081/indexDirs?dir=' + this.loadedDirs[ dir ] + '&coverart=' + this.coverArtSetting + '&doOverride=' + this.doOverride ).then( res => {
if ( res.status === 200 ) {
this.errorOccurredLoading = false;
res.json().then( json => {
for ( let song in json ) {
this.songQueue.push( json[ song ] );
this.allSongs.push( json[ song ] );
}
this.queueHandler();
this.isLoadingSongs = false;
this.hasLoadedSongs = true;
this.$emit( 'com', { 'type': 'songsLoaded' } );
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'type': 'songQueue', 'data': this.songQueue } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.error( err );
} );
} );
} else if ( res.status === 404 ) {
this.isLoadingSongs = false;
this.errorOccurredLoading = true;
}
} );
}
},
play( song ) {
if ( song.filename === this.currentlyPlaying ) {
this.$emit( 'com', { 'type': 'play', 'song': song } );
} else {
for ( let s in this.songQueue ) {
if ( this.songQueue[ s ][ 'filename' ] === song.filename ) {
this.songPos = parseInt( s );
}
}
this.$emit( 'com', { 'type': 'startPlayback', 'song': song } );
}
this.currentlyPlaying = song.filename;
this.update( { 'type': 'playback', 'status': true } );
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'type': 'queuePos', 'data': this.songPos } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.error( err );
} );
},
pause( song ) {
this.update( { 'type': 'playback', 'status': false } );
this.$emit( 'com', { 'type': 'pause', 'song': song } );
},
useAppleMusic() {
fetch( 'http://localhost:8081/useAppleMusic' );
}
}
}
</script>

View File

@@ -1,282 +0,0 @@
<!-- eslint-disable no-undef -->
<template>
<div id="notifications" @click="handleNotifications();">
<div class="message-box" :class="[ location, size ]">
<div class="message-container" :class="messageType">
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
<p class="message">{{ message }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'notifications',
props: {
location: {
type: String,
'default': 'topleft',
},
size: {
type: String,
'default': 'default',
}
// Size options: small, default (default option), big, bigger, huge
},
data () {
return {
notifications: {},
queue: [],
message: '',
messageType: 'hide',
notificationDisplayTime: 0,
notificationPriority: 'normal',
currentlyDisplayedNotificationID: 0,
currentID: { 'critical': 0, 'medium': 1000, 'low': 100000 },
displayTimeCurrentNotification: 0,
notificationScheduler: null,
};
},
methods: {
createNotification( message, showDuration, messageType, priority ) {
/*
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
*/
let id = 0;
if ( priority === 'critical' ) {
this.currentID[ 'critical' ] += 1;
id = this.currentID[ 'critical' ];
} else if ( priority === 'normal' ) {
this.currentID[ 'medium' ] += 1;
id = this.currentID[ 'medium' ];
} else if ( priority === 'low' ) {
this.currentID[ 'low' ] += 1;
id = this.currentID[ 'low' ];
}
this.notifications[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id };
this.queue.push( id );
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
}
return id;
},
cancelNotification ( id ) {
/*
This method deletes a notification and, in case the notification is being displayed, hides it.
*/
try {
delete this.notifications[ id ];
} catch ( error ) {
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
}
try {
this.queue.splice( this.queue.indexOf( id ), 1 );
} catch {
console.debug( 'queue empty' );
}
if ( this.currentlyDisplayedNotificationID == id ) {
this.handleNotifications();
}
},
handleNotifications () {
/*
This methods should NOT be called in any other component than this one!
*/
this.displayTimeCurrentNotification = 0;
this.notificationDisplayTime = 0;
this.message = '';
this.queue.sort();
if ( this.queue.length > 0 ) {
this.message = this.notifications[ this.queue[ 0 ] ][ 'message' ];
this.messageType = this.notifications[ this.queue[ 0 ] ][ 'messageType' ];
this.priority = this.notifications[ this.queue[ 0 ] ][ 'priority' ];
this.currentlyDisplayedNotificationID = this.notifications[ this.queue[ 0 ] ][ 'id' ];
this.notificationDisplayTime = this.notifications[ this.queue[ 0 ] ][ 'showDuration' ];
delete this.notifications[ this.queue[ 0 ] ];
this.queue.reverse();
this.queue.pop();
$( '.message-box' ).css( 'z-index', 20 );
} else {
this.messageType = 'hide';
$( '.message-box' ).css( 'z-index', -1 );
}
}
},
created () {
window.$ = window.jQuery = require( 'jquery' );
this.notificationScheduler = setInterval( () => {
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
} else {
this.displayTimeCurrentNotification += 0.5;
}
}, 500 );
},
unmounted ( ) {
clearInterval( this.notificationScheduler );
}
};
</script>
<style scoped>
.message-box {
position: fixed;
z-index: -1;
color: white;
transition: all 0.5s;
width: 95vw;
right: 2.5vw;
top: 1vh;
height: 10vh;
}
.message-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
opacity: 1;
transition: all 0.5s;
cursor: default;
}
.types {
color: white;
border-radius: 100%;
margin-right: auto;
margin-left: 5%;
padding: 1.5%;
font-size: 200%;
}
.message {
margin-right: 5%;
text-align: end;
}
.ok {
background-color: rgb(1, 71, 1);
}
.error {
background-color: rgb(114, 1, 1);
}
.info {
background-color: rgb(44, 112, 151);
}
.warning {
background-color: orange;
}
.hide {
opacity: 0;
}
.progress {
z-index: 20;
background-color: rgb(0, 0, 99);
}
.progress-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}
@media only screen and (min-width: 750px) {
.default {
height: 10vh;
width: 32vw;
}
.small {
height: 7vh;
width: 27vw;
}
.big {
height: 12vh;
width: 38vw;
}
.bigger {
height: 15vh;
width: 43vw;
}
.huge {
height: 20vh;
width: 50vw;
}
.topleft {
top: 3vh;
left: 0.5vw;
}
.topright {
top: 3vh;
right: 0.5vw;
}
.bottomright {
bottom: 3vh;
right: 0.5vw;
}
.bottomleft {
top: 3vh;
right: 0.5vw;
}
}
@media only screen and (min-width: 1500px) {
.default {
height: 10vh;
width: 15vw;
}
.small {
height: 7vh;
width: 11vw;
}
.big {
height: 12vh;
width: 17vw;
}
.bigger {
height: 15vh;
width: 20vw;
}
.huge {
height: 20vh;
width: 25vw;
}
}
</style>

View File

@@ -1,451 +0,0 @@
<template>
<div class="player">
<div class="controls">
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'previous' )">skip_previous</span>
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'replay10' )">replay_10</span>
<span class="material-symbols-outlined control-icon play-pause" v-if="!isPlaying && audioLoaded" @click="control( 'play' )">play_arrow</span>
<span class="material-symbols-outlined control-icon play-pause" v-else-if="isPlaying && audioLoaded" @click="control( 'pause' )">pause</span>
<span class="material-symbols-outlined control-icon play-pause" style="cursor: default;" v-else>play_disabled</span>
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'forward10' )">forward_10</span>
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'next' )" style="margin-right: 1vw;">skip_next</span>
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-if="!isShuffleEnabled" @click="control( 'shuffleOn' )">shuffle</span>
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-else @click="control( 'shuffleOff' )">shuffle_on</span>
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-if="repeatMode === 'off'" @click="control( 'repeatOne' )">repeat</span>
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-else-if="repeatMode === 'one'" @click="control( 'repeatAll' )">repeat_one_on</span>
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-else-if="repeatMode === 'all'" @click="control( 'repeatOff' )">repeat_on</span>
<div class="control-icon" id="settings">
<span class="material-symbols-outlined">info</span>
<div id="showIP">
<h4>IP to connect to:</h4><br>
<p>{{ localIP }}:8081</p>
</div>
</div>
</div>
<div class="song-info">
<audio v-if="audioLoaded" :src="'http://localhost:8081/getSongFile?filename=' + playingSong.filename" id="music-player"></audio>
<div class="song-info-wrapper">
<div v-if="audioLoaded" @click="showFancyView()" style="cursor: pointer;">
<span class="material-symbols-outlined image" v-if="!playingSong.hasCoverArt">music_note</span>
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="image">
<img v-else :src="'http://localhost:8081/getSongCover?filename=' + playingSong.filename" class="image">
</div>
<span class="material-symbols-outlined image" v-else>music_note</span>
<div class="name">
<h3>{{ playingSong.title ?? 'No song selected' }}</h3>
<p>{{ playingSong.artist }}</p>
</div>
<div class="image"></div>
</div>
<div class="playback-pos-info">
<div style="margin-right: auto;">{{ playbackPosBeautified }}</div>
<div @click="toggleShowMode()" style="cursor: pointer;">{{ durationBeautified }}</div>
</div>
<sliderView :active="audioLoaded" :position="playbackPos" :duration="playingSong.duration" @pos="( p ) => { setPos( p ) }"
name="player"></sliderView>
</div>
<FancyView v-if="isShowingFancyView" :song="playingSong" @control="instruction => { control( instruction ) }" :isPlaying="isPlaying"
:shuffle="isShuffleEnabled" :repeatMode="repeatMode" :durationBeautified="durationBeautified"
:playbackPos="playbackPos" :playbackPosBeautified="playbackPosBeautified"
@posUpdate="pos => { setPos( pos ) }"></FancyView>
<Notifications ref="notifications" size="bigger" location="topright"></Notifications>
</div>
</template>
<style scoped>
.song-info {
background-color: #8e9ced;
height: 13vh;
width: 50%;
margin-left: auto;
margin-right: auto;
position: relative;
}
.image {
width: 7vh;
height: 7vh;
object-fit: cover;
object-position: center;
font-size: 7vh;
margin-left: 1vh;
margin-top: 1vh;
}
.name {
margin-left: auto;
margin-right: auto;
}
.song-info-wrapper {
display: flex;
flex-direction: row;
}
.song-info-wrapper h3 {
margin: 0;
margin-bottom: 0.5vh;
margin-top: 1vh;
}
.controls {
margin-left: 5%;
display: flex;
justify-content: center;
align-items: center;
}
.control-icon {
cursor: pointer;
font-size: 3vh;
user-select: none;
}
.play-pause {
font-size: 5vh;
}
.inactive {
color: gray;
cursor: default;
}
.player {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.playback-pos-info {
display: flex;
flex-direction: row;
width: 98%;
margin-left: 1%;
position: absolute;
bottom: 17px;
}
#showIP {
background-color: rgb(63, 63, 63);
display: none;
position: absolute;
min-height: 16vh;
padding: 2vh;
min-width: 20vw;
z-index: 10;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 70%;
border-radius: 5px 10px 10px 10px;
}
#showIP h4, #showIP p {
margin: 0;
}
#settings:hover #showIP {
display: flex;
}
#showIP::before {
content: " ";
position: absolute;
bottom: 100%; /* At the bottom of the tooltip */
left: 0;
margin-left: 3px;
border-width: 10px;
border-style: solid;
border-color: transparent transparent rgb(63, 63, 63) transparent;
}
</style>
<script>
import FancyView from './fancyView.vue';
import Notifications from './notifications.vue';
import SliderView from './sliderView.vue';
import { guess } from 'web-audio-beat-detector';
export default {
data() {
return {
playingSong: {},
audioLoaded: false,
isPlaying: false,
isShuffleEnabled: false,
repeatMode: 'off',
progressTracker: null,
playbackPos: 0,
playbackPosBeautified: '00:00',
durationBeautified: '--:--',
hasLoadedSongs: false,
isShowingFancyView: false,
notifier: null,
isShowingRemainingTime: false,
localIP: ''
}
},
components: {
SliderView,
FancyView,
Notifications,
},
methods: {
play( song, autoplay, doCrossFade = false ) {
this.playingSong = song;
this.audioLoaded = true;
this.init( doCrossFade, autoplay, song.filename );
},
// TODO: Make function that connects to status service and add various warnings.
init( doCrossFade, autoplay, filename ) {
this.control( 'reset' );
// TODO: make it support cross-fade
setTimeout( () => {
if ( autoplay ) {
this.control( 'play' );
this.isPlaying = true;
this.sendUpdate( 'isPlaying' );
this.sendUpdate( 'pos' );
}
this.sendUpdate( 'playingSong' );
const minuteCount = Math.floor( this.playingSong.duration / 60 );
this.durationBeautified = minuteCount + ':';
if ( ( '' + minuteCount ).length === 1 ) {
this.durationBeautified = '0' + minuteCount + ':';
}
const secondCount = Math.floor( this.playingSong.duration - minuteCount * 60 );
if ( ( '' + secondCount ).length === 1 ) {
this.durationBeautified += '0' + secondCount;
} else {
this.durationBeautified += secondCount;
}
let musicPlayer = document.getElementById( 'music-player' );
musicPlayer.onended = () => {
if ( musicPlayer.currentTime >= this.playingSong.duration - 1 ) {
if ( this.repeatMode !== 'one' ) {
this.control( 'next' );
} else {
musicPlayer.currentTime = 0;
this.control( 'play' );
this.playbackPos = musicPlayer.currentTime;
this.sendUpdate( 'pos' );
}
}
}
if ( !this.playingSong.bpm ) {
const audioContext = new AudioContext();
fetch( 'http://localhost:8081/getSongFile?filename=' + filename ).then( res => {
res.arrayBuffer().then( buf => {
audioContext.decodeAudioData( buf, audioBuffer => {
guess( audioBuffer ).then( ( data ) => {
this.playingSong.bpm = data.bpm;
this.playingSong.accurateTempo = data.tempo;
this.playingSong.bpmOffset = data.offset;
this.sendUpdate( 'playingSong' );
} );
} );
} );
} );
}
}, 300 );
},
toggleShowMode() {
this.isShowingRemainingTime = !this.isShowingRemainingTime;
if ( !this.isShowingRemainingTime ) {
const minuteCounts = Math.floor( this.playingSong.duration / 60 );
this.durationBeautified = String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) {
this.durationBeautified = '0' + minuteCounts + ':';
}
const secondCounts = Math.floor( this.playingSong.duration - minuteCounts * 60 );
if ( ( '' + secondCounts ).length === 1 ) {
this.durationBeautified += '0' + secondCounts;
} else {
this.durationBeautified += secondCounts;
}
}
},
sendUpdate( update ) {
let data = {};
if ( update === 'pos' ) {
data = this.playbackPos;
} else if ( update === 'playingSong' ) {
data = this.playingSong;
} else if ( update === 'isPlaying' ) {
data = this.isPlaying;
}
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'type': update, 'data': data } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
},
};
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
console.error( err );
} );
},
control( action ) {
// https://css-tricks.com/lets-create-a-custom-audio-player/
let musicPlayer = document.getElementById( 'music-player' );
if ( musicPlayer ) {
if ( action === 'play' ) {
this.$emit( 'update', { 'type': 'playback', 'status': true } );
musicPlayer.play();
this.isPlaying = true;
this.progressTracker = setInterval( () => {
this.playbackPos = musicPlayer.currentTime;
const minuteCount = Math.floor( this.playbackPos / 60 );
this.playbackPosBeautified = minuteCount + ':';
if ( ( '' + minuteCount ).length === 1 ) {
this.playbackPosBeautified = '0' + minuteCount + ':';
}
const secondCount = Math.floor( this.playbackPos - minuteCount * 60 );
if ( ( '' + secondCount ).length === 1 ) {
this.playbackPosBeautified += '0' + secondCount;
} else {
this.playbackPosBeautified += secondCount;
}
if ( this.isShowingRemainingTime ) {
const minuteCounts = Math.floor( ( this.playingSong.duration - this.playbackPos ) / 60 );
this.durationBeautified = '-' + String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) {
this.durationBeautified = '-0' + minuteCounts + ':';
}
const secondCounts = Math.floor( ( this.playingSong.duration - this.playbackPos ) - minuteCounts * 60 );
if ( ( '' + secondCounts ).length === 1 ) {
this.durationBeautified += '0' + secondCounts;
} else {
this.durationBeautified += secondCounts;
}
}
}, 20 );
this.sendUpdate( 'pos' );
this.sendUpdate( 'isPlaying' );
} else if ( action === 'pause' ) {
this.$emit( 'update', { 'type': 'playback', 'status': false } );
musicPlayer.pause();
this.sendUpdate( 'pos' );
try {
clearInterval( this.progressTracker );
clearInterval( this.notifier );
} catch ( err ) {};
this.isPlaying = false;
this.sendUpdate( 'isPlaying' );
} else if ( action === 'replay10' ) {
musicPlayer.currentTime = musicPlayer.currentTime > 10 ? musicPlayer.currentTime - 10 : 0;
this.playbackPos = musicPlayer.currentTime;
this.sendUpdate( 'pos' );
} else if ( action === 'forward10' ) {
if ( musicPlayer.currentTime < ( musicPlayer.duration - 10 ) ) {
musicPlayer.currentTime = musicPlayer.currentTime + 10;
this.playbackPos = musicPlayer.currentTime;
this.sendUpdate( 'pos' );
} else {
if ( this.repeatMode !== 'one' ) {
this.control( 'next' );
} else {
musicPlayer.currentTime = 0;
this.playbackPos = musicPlayer.currentTime;
this.sendUpdate( 'pos' );
}
}
} else if ( action === 'reset' ) {
clearInterval( this.progressTracker );
this.playbackPos = 0;
musicPlayer.currentTime = 0;
this.sendUpdate( 'pos' );
} else if ( action === 'next' ) {
this.$emit( 'update', { 'type': 'next' } );
} else if ( action === 'previous' ) {
if ( this.playbackPos > 3 ) {
this.playbackPos = 0;
musicPlayer.currentTime = 0;
this.sendUpdate( 'pos' );
} else {
this.$emit( 'update', { 'type': 'previous' } );
}
} else if ( action === 'shuffleOff' ) {
this.$emit( 'update', { 'type': 'shuffleOff' } );
this.isShuffleEnabled = false;
} else if ( action === 'shuffleOn' ) {
this.$emit( 'update', { 'type': 'shuffle' } );
this.isShuffleEnabled = true;
} else if ( action === 'repeatOne' ) {
this.repeatMode = 'one';
} else if ( action === 'repeatAll' ) {
this.$emit( 'update', { 'type': 'repeat' } );
this.repeatMode = 'all';
} else if ( action === 'repeatOff' ) {
this.$emit( 'update', { 'type': 'repeatOff' } );
this.repeatMode = 'off';
} else if ( action === 'exitFancyView' ) {
this.isShowingFancyView = false;
this.$emit( 'update', { 'type': 'fancyView', 'status': false } );
}
} else if ( action === 'songsLoaded' ) {
this.$refs.notifications.createNotification( 'Songs loaded successfully', 5, 'ok', 'default' );
this.hasLoadedSongs = true;
} else if ( action === 'shuffleOff' ) {
this.$emit( 'update', { 'type': 'shuffleOff' } );
this.isShuffleEnabled = false;
} else if ( action === 'shuffleOn' ) {
this.$emit( 'update', { 'type': 'shuffle' } );
this.isShuffleEnabled = true;
} else if ( action === 'repeatOne' ) {
this.repeatMode = 'one';
} else if ( action === 'repeatAll' ) {
this.$emit( 'update', { 'type': 'repeat' } );
this.repeatMode = 'all';
} else if ( action === 'repeatOff' ) {
this.$emit( 'update', { 'type': 'repeatOff' } );
this.repeatMode = 'off';
} else if ( action === 'exitFancyView' ) {
this.isShowingFancyView = false;
this.$emit( 'update', { 'type': 'fancyView', 'status': false } );
}
},
setPos( pos ) {
let musicPlayer = document.getElementById( 'music-player' );
this.playbackPos = pos;
musicPlayer.currentTime = pos;
this.sendUpdate( 'pos' );
},
showFancyView() {
this.$emit( 'update', { 'type': 'fancyView', 'status': true } );
this.isShowingFancyView = true;
},
},
created() {
document.addEventListener( 'keydown', ( e ) => {
if ( e.key === ' ' ) {
e.preventDefault();
if ( !this.isPlaying ) {
this.control( 'play' );
} else {
this.control( 'pause' );
}
} else if ( e.key === 'ArrowRight' ) {
e.preventDefault();
this.control( 'next' );
} else if ( e.key === 'ArrowLeft' ) {
e.preventDefault();
this.control( 'previous' );
}
} );
fetch( 'http://localhost:8081/getLocalIP' ).then( res => {
if ( res.status === 200 ) {
res.text().then( ip => {
this.localIP = ip;
} );
}
} ).catch( err => {
this.localIP = 'ERROR fetching';
} );
}
}
</script>

View File

@@ -1,149 +0,0 @@
<template>
<div style="width: 100%; height: 100%;">
<progress :id="'progress-slider-' + name" class="progress-slider" :value="sliderProgress" max="1000" @mousedown="( e ) => { setPos( e ) }"
:class="active ? '' : 'slider-inactive'"></progress>
<div v-if="active" id="slider-knob" @mousedown="( e ) => { startMove( e ) }"
:style="'left: ' + ( parseInt( originalPos ) + parseInt( sliderPos ) ) + 'px;'">
<div id="slider-knob-style"></div>
</div>
<div v-else id="slider-knob" class="slider-inactive" style="left: 0;">
<div id="slider-knob-style"></div>
</div>
<div id="drag-support" @mousemove="e => { handleDrag( e ) }" @mouseup="() => { stopMove(); }"></div>
</div>
</template>
<style scoped>
.progress-slider {
width: 100%;
margin: 0;
position: absolute;
left: 0;
bottom: 0;
height: 5px;
cursor: pointer;
background-color: #baf4c9;
}
.progress-slider::-webkit-progress-value {
background-color: #baf4c9;
}
#slider-knob {
height: 20px;
width: 10px;
display: flex;
justify-content: flex-start;
align-items: flex-end;
position: absolute;
bottom: 0;
left: 0;
z-index: 2;
cursor: grab;
}
#slider-knob-style {
background-color: #baf4c9;
height: 15px;
width: 5px;
}
#drag-support {
display: none;
opacity: 0;
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
z-index: 10;
cursor: grabbing;
}
.drag-support-active {
display: block !important;
}
.slider-inactive {
cursor: default !important;
}
</style>
<script>
export default {
props: {
style: {
type: Object,
},
position: {
type: Number,
default: 0,
},
duration: {
type: Number,
default: 100
},
active: {
type: Boolean,
default: true,
},
name: {
type: String,
default: '1',
}
},
data () {
return {
offset: 0,
isDragging: false,
sliderPos: 0,
originalPos: 0,
sliderProgress: 0,
}
},
methods: {
handleDrag( e ) {
if ( this.isDragging ) {
if ( 0 < this.originalPos + e.screenX - this.offset && this.originalPos + e.screenX - this.offset < document.getElementById( 'progress-slider-' + this.name ).clientWidth - 5 ) {
this.sliderPos = e.screenX - this.offset;
this.calcProgressPos();
}
}
},
startMove( e ) {
this.offset = e.screenX;
this.isDragging = true;
document.getElementById( 'drag-support' ).classList.add( 'drag-support-active' );
},
stopMove() {
this.originalPos += parseInt( this.sliderPos );
this.isDragging = false;
this.offset = 0;
this.sliderPos = 0;
document.getElementById( 'drag-support' ).classList.remove( 'drag-support-active' );
this.calcPlaybackPos();
},
setPos ( e ) {
if ( this.active ) {
this.originalPos = e.offsetX;
this.calcProgressPos();
this.calcPlaybackPos();
}
},
calcProgressPos() {
this.sliderProgress = Math.ceil( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider-' + this.name ).clientWidth - 5 ) * 1000 );
},
calcPlaybackPos() {
this.$emit( 'pos', Math.round( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider-' + this.name ).clientWidth - 5 ) * this.duration ) );
}
},
watch: {
position() {
if ( !this.isDragging ) {
this.sliderProgress = Math.ceil( this.position / this.duration * 1000 + 2 );
this.originalPos = Math.ceil( this.position / this.duration * ( document.getElementById( 'progress-slider-' + this.name ).scrollWidth - 5 ) );
}
}
}
}
</script>

View File

@@ -1,5 +0,0 @@
{
"connectionURL": "http://localhost:3000",
"authKey": "gaöwovwef89voawö8p9 odövefw8öoaewpf89wec",
"doConnect": true
}

View File

@@ -1,31 +0,0 @@
/*
* MusicPlayerV2 - imageFetcher.js
*
* Created by Janis Hutz 10/23/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const MusicKit = require( 'node-musickit-api' );
const fs = require( 'fs' );
const path = require( 'path' );
// TODO: deploy non-secret version
const settings = JSON.parse( fs.readFileSync( path.join( __dirname + '/config/apple-music-api.config.json' ) ) );
const music = new MusicKit( {
key: fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString(),
teamId: settings.teamID,
keyId: settings.keyID
} );
module.exports.fetch = ( type, searchQuery, callback ) => {
music.search( settings.storefront, type, searchQuery, ( err, data ) => {
if ( err ) {
callback( err, null );
return;
}
callback( null, data );
} );
}

View File

@@ -1,214 +0,0 @@
/*
* MusicPlayerV2 - indexer.js
*
* Created by Janis Hutz 11/05/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const fs = require( 'fs' );
const imageFetcher = require( './imageFetcher.js' );
const musicMetadata = require( 'music-metadata' );
const allowedFileTypes = [ '.mp3', '.wav', '.flac' ];
const csv = require( 'csv-parser' );
const path = require( 'path' );
let coverArtIndex = {};
module.exports.index = ( req ) => {
return new Promise( ( resolve, reject ) => {
fs.readdir( req.query.dir, { encoding: 'utf-8' }, ( err, dat ) => {
if ( err ) {
reject( 'ERR_DIR_NOT_FOUND' );
return;
};
( async() => {
// Check for songlist.csv or songlist.json file and use the data provided there for each song to override
// what was found automatically. If no song title was found in songlist or metadata, use filename
// additionally check if dir has been indexed (songs.json file)
if ( dat.includes( 'songs.json' ) ) {
parseExistingData( dat, req.query.dir ).then( data => {
resolve( data );
} ).catch( err => {
reject( err );
} );
} else if ( dat.includes( 'songlist.csv' ) || dat.includes( 'songlist.json' ) ) {
parseExistingData( dat, req.query.dir ).then( data => {
parseDir( dat, req, data ).then( indexedDir => {
resolve( indexedDir );
} );
} );
} else {
resolve( await parseDir( dat, req ) );
}
} )();
} );
} );
}
module.exports.getImages = ( filename ) => {
return coverArtIndex[ filename ];
};
const parseExistingData = ( dat, dir ) => {
return new Promise( ( resolve, reject ) => {
if ( dat.includes( 'songs.json' ) ) {
resolve( JSON.parse( fs.readFileSync( path.join( dir + '/songs.json' ) ) ) );
} else if ( dat.includes( 'songlist.csv' ) ) {
// This will assume that line #1 will be song #1 in the file list
// (when sorted by name)
let results = {};
let pos = 0;
fs.createReadStream( path.join( dir + '/songlist.csv' ) )
.pipe( csv() )
.on( 'data', ( data ) => {
results[ dir + '/' + dat[ pos ] ] = data;
pos += 1;
} ).on( 'end', () => {
resolve( results );
} );
} else if ( dat.includes( 'songlist.json' ) ) {
resolve( JSON.parse( fs.readFileSync( path.join( dir + '/songlist.json' ) ) ) );
}
} );
}
module.exports.analyzeFile = async ( filepath ) => {
let metadata = await musicMetadata.parseFile( filepath );
return {
'artist': metadata[ 'common' ][ 'artist' ],
'title': metadata[ 'common' ][ 'title' ],
'year': metadata[ 'common' ][ 'year' ],
'bpm': metadata[ 'common' ][ 'bpm' ],
'genre': metadata[ 'common' ][ 'genre' ],
'duration': Math.round( metadata[ 'format' ][ 'duration' ] ),
'isLossless': metadata[ 'format' ][ 'lossless' ],
'sampleRate': metadata[ 'format' ][ 'sampleRate' ],
'bitrate': metadata[ 'format' ][ 'bitrate' ],
'numberOfChannels': metadata[ 'format' ][ 'numberOfChannels' ],
'container': metadata[ 'format' ][ 'container' ],
'filename': filepath,
}
}
hasCompletedFetching = {};
let files = {};
const parseDir = ( dat, req, existingData ) => {
return new Promise( ( resolve, reject ) => {
( async() => {
files = {};
for ( let file in dat ) {
if ( allowedFileTypes.includes( dat[ file ].slice( dat[ file ].indexOf( '.' ), dat[ file ].length ) ) ) {
try {
let metadata = await musicMetadata.parseFile( req.query.dir + '/' + dat[ file ] );
files[ req.query.dir + '/' + dat[ file ] ] = {
'artist': metadata[ 'common' ][ 'artist' ],
'title': metadata[ 'common' ][ 'title' ],
'year': metadata[ 'common' ][ 'year' ],
'bpm': metadata[ 'common' ][ 'bpm' ],
'genre': metadata[ 'common' ][ 'genre' ],
'duration': Math.round( metadata[ 'format' ][ 'duration' ] ),
'isLossless': metadata[ 'format' ][ 'lossless' ],
'sampleRate': metadata[ 'format' ][ 'sampleRate' ],
'bitrate': metadata[ 'format' ][ 'bitrate' ],
'numberOfChannels': metadata[ 'format' ][ 'numberOfChannels' ],
'container': metadata[ 'format' ][ 'container' ],
'filename': req.query.dir + '/' + dat[ file ],
'coverArtOrigin': req.query.coverart ?? 'none',
}
runReplace( existingData, req.query.dir + '/' + dat[ file ], req.query.doOverride ?? false );
if ( req.query.coverart == 'meta' ) {
if ( metadata[ 'common' ][ 'picture' ] ) {
files[ req.query.dir + '/' + dat[ file ] ][ 'hasCoverArt' ] = true;
coverArtIndex[ req.query.dir + '/' + dat[ file ] ] = metadata[ 'common' ][ 'picture' ] ? metadata[ 'common' ][ 'picture' ][ 0 ][ 'data' ] : undefined;
} else {
files[ req.query.dir + '/' + dat[ file ] ][ 'hasCoverArt' ] = false;
}
hasCompletedFetching[ req.query.dir + '/' + dat[ file ] ] = true;
} else if ( req.query.coverart == 'api' ) {
hasCompletedFetching[ req.query.dir + '/' + dat[ file ] ] = false;
fetchImages( metadata[ 'common' ][ 'title' ], metadata[ 'common' ][ 'artist' ], metadata[ 'common' ][ 'year' ], req.query.dir, dat[ file ] );
} else {
files[ req.query.dir + '/' + dat[ file ] ][ 'hasCoverArt' ] = false;
}
} catch ( err ) {
console.error( err );
files[ req.query.dir + '/' + dat[ file ] ] = 'ERROR';
}
}
}
let ok = false;
let waiter = setInterval( () => {
for ( let song in hasCompletedFetching ) {
if ( !hasCompletedFetching[ song ] ) {
ok = false;
}
}
if ( ok ) {
saveToDisk( req.query.dir );
clearInterval( waiter );
resolve( files );
}
ok = true;
}, 250 );
} )();
} )
};
const runReplace = ( existingData, currentFile, doOverride ) => {
for ( let param in existingData[ currentFile ] ) {
if ( !files[ currentFile ][ param ] || doOverride ) {
files[ currentFile ][ param ] = existingData[ currentFile ][ param ];
}
}
};
let imageQueue = [];
let runInterval = null;
const fetchImages = ( title, artist, year, dir, filename ) => {
imageQueue.push( { 'title': title, 'artist': artist, 'year': year, 'dir': dir, 'filename': filename } );
if ( runInterval === null ) {
runInterval = setInterval( () => {
if ( imageQueue.length > 0 ) {
const cur = imageQueue.reverse().pop();
imageQueue.reverse();
runFetch( cur.title, cur.artist, cur.year, cur.dir, cur.filename );
} else {
clearInterval( runInterval );
runInterval = null;
}
}, 100 );
}
};
const runFetch = ( title, artist, year, dir, filename ) => {
imageFetcher.fetch( 'songs', ( artist ?? '' ) + ' ' + ( title ?? '' ) + ' ' + ( year ?? '' ), ( err, data ) => {
if ( err ) {
files[ dir + '/' + filename ][ 'hasCoverArt' ] = false;
console.error( dir + '/' + filename );
console.error( err );
hasCompletedFetching[ dir + '/' + filename ] = true;
return;
}
if ( data.results.songs ) {
if ( data.results.songs.data ) {
let url = data.results.songs.data[ 0 ].attributes.artwork.url;
url = url.replace( '{w}', data.results.songs.data[ 0 ].attributes.artwork.width );
url = url.replace( '{h}', data.results.songs.data[ 0 ].attributes.artwork.height );
files[ dir + '/' + filename ][ 'coverArtURL' ] = url;
files[ dir + '/' + filename ][ 'hasCoverArt' ] = true;
} else {
files[ dir + '/' + filename ][ 'hasCoverArt' ] = false;
}
} else {
files[ dir + '/' + filename ][ 'hasCoverArt' ] = false;
}
hasCompletedFetching[ dir + '/' + filename ] = true;
} );
}
const saveToDisk = ( dir ) => {
fs.writeFileSync( path.join( dir + '/songs.json' ), JSON.stringify( files ) );
};

View File

@@ -1,5 +0,0 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')

View File

@@ -1,17 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

View File

@@ -1,85 +0,0 @@
<template>
<div class="home">
<div class="top-bar">
<img src="@/assets/logo.png" alt="logo" class="logo">
<div class="player-wrapper">
<Player ref="player" @update="( info ) => { handleUpdates( info ) }"></Player>
</div>
</div>
<div class="pool-wrapper">
<mediaPool @com="( info ) => { handleCom( info ) }" ref="pool"></mediaPool>
</div>
</div>
</template>
<style>
.home {
width: 100%;
height: 100%;
}
.pool-wrapper {
height: 84vh;
margin-top: 16vh;
}
.top-bar {
margin-left: auto;
margin-right: auto;
position: fixed;
z-index: 8;
width: 99%;
height: 15vh;
display: flex;
align-items: center;
flex-direction: row;
border: var( --primary-color ) 2px solid;
background-color: var( --background-color );
}
.player-wrapper {
width: 70vw;
margin-right: auto;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.logo {
height: 13vh;
margin-left: 3%;
margin-right: auto;
}
</style>
<script>
import mediaPool from '@/components/mediaPool.vue';
import Player from '@/components/player.vue';
export default {
name: 'HomeView',
components: {
mediaPool,
Player,
},
data() {
return {
hasLoadedSongs: false,
songQueue: [],
}
},
methods: {
handleCom ( data ) {
if ( data.type === 'startPlayback' ) {
this.$refs.player.play( data.song, data.autoplay === undefined ? true : data.autoplay );
} else {
this.$refs.player.control( data.type );
}
},
handleUpdates ( data ) {
this.$refs.pool.update( data );
},
}
}
</script>

View File

@@ -1,39 +0,0 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
"appId": "com.janishutz.MusicPlayerV2",
"copyright": "Copyright (c) 2023 MusicPlayer contributors",
"buildVersion": "V2.0.0-dev2",
builderOptions: {
files: [
"**/*",
{
from: "./*",
to: "./*",
filter: [ "**/*" ]
},
{
from: "./public/*",
to: "./*",
filter: [ "**/*" ]
}
],
extraFiles: [
{
from: "./src/client",
to: "./client",
filter: [ "**/*" ]
},
{
from: "./src/config",
to: "./config",
filter: [ "*.config.json" ]
}
]
}
}
}
})

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "MusicPlayer",
"version": "3.0.0",
"private": false
}

View File

@@ -1,7 +0,0 @@
cd frontend/
npm i
npm run electron:build -- --linux deb rpm --win nsis
cd ..
zip -9r musicplayerv2-server.zip backend