diff --git a/README.md b/README.md
index 2c95c69..6286b15 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,21 @@
-

-

-

-

-

+

+

+

+

+
-

-

-

-

+

+

+

+
-
)
-
)
-

-

+
)
+
)
+

+
# Development Branch
@@ -38,6 +38,7 @@ The [hosted version](https://music.janishutz.com) of this MusicPlayer, which is
# Features
+**NOTICE:** This section has not been updated for the new version of MusicPlayer yet
- Browser based App that runs on all OS (Linux, MacOS, Windows, iOS, Android, iPadOS, ...)
- Fully featured Music Player
- Show all song information over the Internet on any amount of client displays
diff --git a/backend/config.schema.json b/backend/config.schema.json
index 4a0a782..3be4780 100644
--- a/backend/config.schema.json
+++ b/backend/config.schema.json
@@ -24,5 +24,10 @@
"type": "string",
"description": "The URL to the frontend. Just the host, in form https://music.janishutz.com"
}
- }
+ },
+ "required": [
+ "mode",
+ "clientMode",
+ "webUrl"
+ ]
}
diff --git a/backend/config/config.json b/backend/config/config.json
index 18b2732..d40d273 100644
--- a/backend/config/config.json
+++ b/backend/config/config.json
@@ -1,4 +1,6 @@
{
"$schema": "../config.schema.json",
- "mode": "foss"
+ "mode": "foss",
+ "clientMode": "sse",
+ "webUrl": "http://localhost:8081"
}
diff --git a/backend/package.json b/backend/package.json
index fe880c8..1986e3c 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -3,6 +3,7 @@
"@janishutz/login-sdk-server": "^1.4.0",
"@janishutz/login-sdk-server-stubs": "^1.4.0",
"@janishutz/store-sdk": "^1.2.1",
+ "body-parser": "^2.3.0",
"cors": "^2.8.6",
"express": "^5.2.1",
"express-session": "^1.19.0",
@@ -12,6 +13,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.10.0",
+ "@types/body-parser": "^1.19.6",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/express-session": "^1.19.0",
diff --git a/backend/src/app.ts b/backend/src/app.ts
index 636c28e..30816a5 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -2,35 +2,42 @@ import {
getLoginSdk,
getStoreSdk
} from './sdk';
+import {
+ Config
+} from './dtype/config';
import devtoken from './routes/devtoken';
import express from 'express';
import fs from 'fs';
import path from 'path';
-
-// TODO: configurable
-const sdk = getLoginSdk( false );
-const storeSdk = getStoreSdk( false );
-
+import room from './routes/room';
+import user from './routes/user';
const run = () => {
- const app = express();
const sdkConfig = JSON.parse( fs.readFileSync( path.join(
__dirname,
- '/config/sdk.config.testing.json'
+ '/../config/sdk.config.testing.json'
) ).toString() );
+ const config = JSON.parse( fs.readFileSync( path.join(
+ __dirname,
+ '/../config/config.json'
+ ) ).toString() ) as Config;
+ const foss = config.mode !== 'hosted';
+ const sdk = getLoginSdk( foss );
+ const storeSdk = getStoreSdk( foss );
+ const app = express();
// Load id.janishutz.com SDK and allow signing in
sdk.setUp(
{
'prod': false,
'service': {
- 'serviceID': 'jh-music',
+ 'serviceID': sdkConfig[ 'name' ],
'serviceToken': sdkConfig[ 'token' ]
},
'user-agent': sdkConfig[ 'ua' ],
'sessionType': 'memory',
- 'frontendURL': 'https://music.janishutz.com',
- 'corsWhitelist': [ 'https://music.janishutz.com' ],
+ 'frontendURL': config.webUrl,
+ 'corsWhitelist': [ config.webUrl ],
'recheckTimeout': 300 * 1000,
'advancedVerification': 'sdk'
},
@@ -55,22 +62,26 @@ const run = () => {
// Load store sdk
const storeConfig = JSON.parse( fs.readFileSync( path.join(
__dirname,
- '/config/store-sdk.config.secret.json'
+ '/../config/store-sdk.config.testing.json'
) ).toString() );
storeSdk.configure( storeConfig );
app.get( '/', ( _request: express.Request, response: express.Response ) => {
- response.send( 'HELLO WORLD' );
+ response.redirect( config.webUrl ?? 'https://music.janishutz.com' );
} );
+ // TODO: Need way for frontend to get the connection type for shares
+
// Load extra routes
- devtoken.routes( app );
+ devtoken.routes( app, foss );
+ room.routes( app, foss );
+ user.routes( app, foss, config );
app.use( ( _request: express.Request, response: express.Response ) => {
- response.sendFile( path.join( __dirname, '' ) );
+ response.sendStatus( 404 );
} );
const PORT = process.env.PORT || 8080;
diff --git a/backend/src/corsManager.ts b/backend/src/corsManager.ts
new file mode 100644
index 0000000..8e14fc4
--- /dev/null
+++ b/backend/src/corsManager.ts
@@ -0,0 +1,56 @@
+import {
+ CorsOptions
+} from 'cors';
+import cors from 'cors';
+
+let corsWhitelist: string[] = [];
+
+const configure = ( whitelist: string[] ) => {
+ corsWhitelist = whitelist;
+};
+
+const middleware = ( rest: boolean = false ) => {
+ return cors( genCorsOpts( rest ) );
+};
+
+/**
+ * Generate the options for the CORS library
+ * @param rest - [OPTIONAL] Whether to allow REST requests or not
+ * @returns CORS options object that can be used for the cors library
+ */
+const genCorsOpts = ( rest: boolean = false ): CorsOptions => {
+ const corsOpts: CorsOptions = {
+ 'credentials': true,
+ 'optionsSuccessStatus': 200,
+ 'origin': ( origin, cb ) => {
+ const status = blockREST( origin! );
+
+ cb( null, status );
+ }
+ };
+
+ if ( rest ) {
+ corsOpts[ 'origin' ] = ( origin, cb ) => {
+ const status = allowREST( origin! );
+
+ cb( null, status );
+ };
+ }
+
+ return corsOpts;
+};
+
+const blockREST = ( origin: string ): boolean => {
+ return corsWhitelist.includes( origin );
+};
+
+const allowREST = ( origin: string ): boolean => {
+ return blockREST( origin ) || !origin;
+};
+
+
+export default {
+ genCorsOpts,
+ configure,
+ middleware
+};
diff --git a/backend/src/dtype/config.d.ts b/backend/src/dtype/config.d.ts
new file mode 100644
index 0000000..9444f5c
--- /dev/null
+++ b/backend/src/dtype/config.d.ts
@@ -0,0 +1,5 @@
+export interface Config {
+ 'mode': 'foss' | 'hosted';
+ 'clientMode': 'sse' | 'poll';
+ 'webUrl': string;
+}
diff --git a/backend/src/logger.ts b/backend/src/logger.ts
index a2f3c20..4cd9fe3 100644
--- a/backend/src/logger.ts
+++ b/backend/src/logger.ts
@@ -2,27 +2,27 @@ import {
writeFile
} from 'node:fs';
-const log = ( ...msg: string[] ) => {
+const log = ( ...msg: unknown[] ) => {
output( 'log', log.caller.toString(), ...msg );
};
-const info = ( ...msg: string[] ) => {
+const info = ( ...msg: unknown[] ) => {
output( 'info', log.caller.toString(), ...msg );
};
-const debug = ( ...msg: string[] ) => {
+const debug = ( ...msg: unknown[] ) => {
output( 'debug', log.caller.toString(), ...msg );
};
-const warn = ( ...msg: string[] ) => {
+const warn = ( ...msg: unknown[] ) => {
output( 'warn', log.caller.toString(), ...msg );
};
-const error = ( ...msg: string[] ) => {
+const error = ( ...msg: unknown[] ) => {
output( 'error', log.caller.toString(), ...msg );
};
-const fatal = ( ...msg: string[] ) => {
+const fatal = ( ...msg: unknown[] ) => {
output( 'fatal', log.caller.toString(), ...msg );
};
@@ -51,7 +51,7 @@ const configure = ( location: 'stderr' | 'file', minLevel: LogLevel, file?: stri
const logfile: string[] = [];
-const output = ( level: LogLevel, caller: string, ...message: string[] ) => {
+const output = ( level: LogLevel, caller: string, ...message: unknown[] ) => {
if ( levels.indexOf( level ) < lev ) {
return;
}
diff --git a/backend/src/manager/rooms/index.ts b/backend/src/manager/rooms/index.ts
index 8ff011e..419e449 100644
--- a/backend/src/manager/rooms/index.ts
+++ b/backend/src/manager/rooms/index.ts
@@ -1,9 +1,178 @@
import {
+ Client,
RoomStore
} from './room';
+import {
+ PlaylistSongs
+} from '../../dtype/file';
+import {
+ Response
+} from 'express';
const rooms: RoomStore = {};
-const getRoom = ( room: string ) => {};
+/**
+ * The room to get
+ * @param room - The name of the room to get
+ * @returns The room, or undefined if it is not present
+ */
+const get = ( room: string ) => {
+ return rooms[room];
+};
-const createRoom = ( name: string ) => {};
+/**
+ * @param name - The name of the room to create
+ * @param uid - The ID of the user
+ */
+const create = ( name: string, uid: string ) => {
+ if ( rooms[ name ] ) {
+ return false;
+ } else {
+ rooms[ name ] = {
+ 'clients': [],
+ 'trackingClients': [],
+ 'owner': uid,
+ 'playlist': {
+ 'playlist': [],
+ 'lastUpdate': new Date().getTime()
+ },
+ 'state': {
+ 'index': 0,
+ 'lastUpdate': new Date().getTime(),
+ 'playing': false,
+ 'start': new Date().getTime()
+ }
+ };
+
+ return true;
+ }
+};
+
+/**
+ * Add a listening client to a room
+ * @param roomName - The room ID this client should belong to
+ * @param id - The ID of the client
+ * @param kind - The type of client that is to be removed
+ * @param response - The express response object
+ * @returns true if successful, false otherwise
+ */
+const addClient = ( roomName: string, id: string, kind: 'client' | 'trackingClient', response: Response ) => {
+ if ( !rooms[roomName] ) return false;
+
+ const room = rooms[roomName][kind + 's' as 'clients' | 'trackingClients'] as Client[];
+
+ room.push( {
+ 'response': response,
+ 'id': id
+ } );
+
+ return true;
+};
+
+/**
+ * Remove a listening client by its ID
+ * @param roomName - The room ID this client belongs to
+ * @param id - The ID of the client
+ * @param kind - The type of client that is to be removed
+ * @returns true if successful, false otherwise
+ */
+const removeClient = ( roomName: string, id: string, kind: 'client' | 'trackingClient' ) => {
+ if ( !rooms[roomName] ) return false;
+
+ const room = rooms[roomName][kind + 's' as 'clients' | 'trackingClients'] as Client[];
+ const idx = room.findIndex( client => client.id === id );
+
+ if ( idx < 0 )
+ return false;
+
+ room.splice( idx, 1 );
+
+ return true;
+};
+
+/**
+ * Close the room and disconnect all clients
+ * @param name - The name of the room
+ * @param uid - The name of the owner of the room
+ */
+const close = ( name: string, uid: string ) => {
+ if ( !rooms[name] || rooms[name].owner !== uid ) return false;
+
+ rooms[name].clients.forEach( client => client.response.send( 'data: closed\n\n' ) );
+ delete rooms[name];
+
+ return true;
+};
+
+/**
+ * Update the state of the specified room
+ * @param room - The room to update the state for
+ * @param uid - The UserID of the user that created the room (i.e. the owner)
+ * @param playing - Set to true if the player is playing currently
+ * @param index - The current playback index
+ * @param start - The timestamp where playback of the current song started
+ * @returns true if update succeeded
+ */
+const updateState = ( room: string, uid: string, playing: boolean, index: number, start: number ) => {
+ if ( !rooms[room] || rooms[room].owner !== uid ) return false;
+
+ rooms[room].state = {
+ 'playing': playing,
+ 'index': index,
+ 'lastUpdate': new Date().getTime(),
+ 'start': start
+ };
+
+ return sendUpdate( room, 'state' );
+};
+
+/**
+ * Update the playlist of the specified room
+ * @param room - The room to update the state for
+ * @param uid - The UserID of the user that created the room (i.e. the owner)
+ * @param playlist - The playlist that is currently playing
+ * @returns true if update succeeded
+ */
+const updatePlaylist = ( room: string, uid: string, playlist: PlaylistSongs ) => {
+ if ( !rooms[room] || rooms[room].owner !== uid ) return false;
+
+ rooms[room].playlist = {
+ 'playlist': playlist,
+ 'lastUpdate': new Date().getTime()
+ };
+
+ return sendUpdate( room, 'playlist' );
+};
+
+const sendUpdate = ( room: string, kind: 'state' | 'playlist' ) => {
+ const roomObject = rooms[room];
+
+ if ( !roomObject ) return false;
+
+ roomObject.clients.forEach( client => client.response.write( `data: ${ {
+ 'type': kind,
+ 'data': JSON.stringify( roomObject[kind] )
+ } }\n\n` ) );
+};
+
+/**
+ * Send an update from the trackking system to all admin clients
+ * @param room - The room to send it to
+ * @param update - The update to send
+ */
+const sendTrackingUpdate = ( room: string, update: string ) => {
+ if ( !rooms[room] ) return false;
+
+ rooms[room].trackingClients.forEach( client => client.response.write( `data: ${ update }\n\n` ) );
+};
+
+export default {
+ get,
+ create,
+ close,
+ updateState,
+ updatePlaylist,
+ sendTrackingUpdate,
+ addClient,
+ removeClient
+};
diff --git a/backend/src/manager/rooms/room.d.ts b/backend/src/manager/rooms/room.d.ts
index 8cbc383..3d85963 100644
--- a/backend/src/manager/rooms/room.d.ts
+++ b/backend/src/manager/rooms/room.d.ts
@@ -1,24 +1,30 @@
import {
- Playlist
+ PlaylistSongs
} from '../../dtype/file';
import {
Response
} from 'express';
+export interface Client {
+ 'id': string;
+ 'response': Response;
+}
+
export interface Room {
- 'playlist': Playlist;
- 'playing': boolean;
- 'index': number;
- 'start': number;
- 'token': string;
- 'lastUpdate': number;
+ 'playlist': {
+ 'lastUpdate': number;
+ 'playlist': PlaylistSongs;
+ }
+ 'state': {
+ 'index': number;
+ 'start': number;
+ 'playing': boolean;
+ 'lastUpdate': number;
+ };
+ 'owner': string;
+ 'clients': Client[];
+ 'trackingClients': Client[];
- /**
- * This is used to determine if the playlist needs to be sent in polling mode
- */
- 'lastPlaylistUpdate': number;
-
- 'clients': Response[]
}
export interface RoomStore {
diff --git a/backend/src/manager/rooms/sse.ts b/backend/src/manager/rooms/sse.ts
new file mode 100644
index 0000000..0ebf976
--- /dev/null
+++ b/backend/src/manager/rooms/sse.ts
@@ -0,0 +1,33 @@
+import express from 'express';
+import {
+ generateToken
+} from '../../token';
+import rooms from '.';
+
+export const sseMiddleware = ( kind: 'client' | 'trackingClient' ) => {
+ return ( request: express.Request, response: express.Response ) => {
+ if ( typeof request.params.id !== 'string' )
+ return response.sendStatus( 400 );
+
+ const room = rooms.get( request.params.id );
+
+ if ( !room ) response.sendStatus( 404 );
+
+ response.writeHead( 200, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive'
+ } );
+ response.status( 200 );
+ response.flushHeaders();
+ response.write( 'data: connected\n\n' );
+
+ const token = generateToken( 20 );
+
+ rooms.addClient( request.params.id, token, kind, response );
+
+ request.on( 'close', () => {
+ rooms.removeClient( request.params.id as string, token, kind );
+ } );
+ };
+};
diff --git a/backend/src/manager/users/index.ts b/backend/src/manager/users/index.ts
index e69de29..150239e 100644
--- a/backend/src/manager/users/index.ts
+++ b/backend/src/manager/users/index.ts
@@ -0,0 +1,47 @@
+import {
+ readFile,
+ stat,
+ writeFile
+} from 'node:fs';
+import {
+ UserPlaylistFile
+} from '../../dtype/file';
+import path from 'node:path';
+
+export const getUserFile = async ( uid: string ): Promise => {
+ return new Promise( ( resolve, reject ) => {
+ readFile( getUserFilePath( uid ), ( err, data ) => {
+ if ( err ) return reject( err );
+
+ resolve( JSON.parse( String( data ) ) as UserPlaylistFile );
+ } );
+ } );
+};
+
+export const getUserFilePath = ( uid: string ): string => {
+ return path.join( __dirname, '/../../../data/', uid + '.json' );
+};
+
+export const testUserFileExists = async ( uid: string ): Promise => {
+ return new Promise( resolve => {
+ stat( getUserFilePath( uid ), err => {
+ if ( err ) return resolve( false );
+
+ resolve( true );
+ } );
+ } );
+};
+
+export const writeUserFile = async ( uid: string, file: UserPlaylistFile ) => {
+ writeFile( getUserFilePath( uid ), JSON.stringify( file ), err => {
+ if ( err ) return Promise.reject( err );
+ else Promise.resolve();
+ } );
+};
+
+export default {
+ getUserFilePath,
+ getUserFile,
+ writeUserFile,
+ testUserFileExists
+};
diff --git a/backend/src/manager/users/ownership.ts b/backend/src/manager/users/ownership.ts
new file mode 100644
index 0000000..f637cae
--- /dev/null
+++ b/backend/src/manager/users/ownership.ts
@@ -0,0 +1,44 @@
+import {
+ getLoginSdk,
+ getStoreSdk
+} from '../../sdk';
+import express from 'express';
+
+const cache = {};
+
+export const getOwnershipManager = ( foss: boolean ) => {
+ const storeSdk = getStoreSdk( foss );
+ const sdk = getLoginSdk( foss );
+
+ const getOwned = async ( request: express.Request ) => {
+ const uid = sdk.getUID( request )!;
+
+ if ( cache[ uid ] ) {
+ return true;
+ } else {
+ const now = new Date().getTime();
+
+ if ( ( await storeSdk.getSubscriptions( uid ) ).findIndex(
+ sub => ( sub.id === 'com.janishutz.MusicPlayer.subscription' || sub.id === 'com.janishutz.MusicPlayer.subscription-month' ) && sub.expires - now > 0
+ ) > -1 ) {
+ cache[ uid ] = true;
+
+ return true;
+ } else {
+ return true;
+ }
+ }
+ };
+
+ const middleware = () => {
+ return async ( request: express.Request, response: express.Response, next: express.NextFunction ) => {
+ if ( !await getOwned( request ) ) response.sendStatus( 402 );
+ else next();
+ };
+ };
+
+ return {
+ getOwned,
+ middleware
+ };
+};
diff --git a/backend/src/routes/devtoken.ts b/backend/src/routes/devtoken.ts
index 1d5354e..1c3540d 100644
--- a/backend/src/routes/devtoken.ts
+++ b/backend/src/routes/devtoken.ts
@@ -1,10 +1,17 @@
-import cors from 'cors';
+import corsManager from '../corsManager';
import express from 'express';
import fs from 'fs';
+import {
+ getLoginSdk
+} from '../sdk';
+import {
+ getOwnershipManager
+} from '../manager/users/ownership';
import jwt from 'jsonwebtoken';
import path from 'path';
-const routes = ( app: express.Application ) => {
+const routes = ( app: express.Application, foss: boolean ) => {
+ const sdk = getLoginSdk( foss );
const privateKey = fs.readFileSync( path.join(
__dirname,
'/../config/apple_private_key.p8'
@@ -13,27 +20,31 @@ const routes = ( app: express.Application ) => {
__dirname,
'/../config/apple-music-api.config.secret.json'
) ).toString() );
+ const ownership = getOwnershipManager( foss );
- app.get( '/dev-token', cors( {
- 'origin': 'http://localhost:8081',
- 'credentials': true
- } ), ( _request: express.Request, response: express.Response ) => {
- // sign dev token
- const now = new Date().getTime();
- const tomorrow = now + ( 24 * 3600 * 1000 );
- const jwtToken = jwt.sign( {
- 'iss': musicApiConfig.teamID,
- 'iat': Math.floor( now / 1000 ),
- 'exp': Math.floor( tomorrow / 1000 )
- }, privateKey, {
- 'algorithm': 'ES256',
- 'keyid': musicApiConfig.keyID
- } );
+ app.get(
+ '/dev-token',
+ corsManager.middleware( false ),
+ sdk.loginCheck(),
+ ownership.middleware(),
+ ( _request: express.Request, response: express.Response ) => {
+ // sign dev token
+ const now = new Date().getTime();
+ const tomorrow = now + ( 24 * 3600 * 1000 );
+ const jwtToken = jwt.sign( {
+ 'iss': musicApiConfig.teamID,
+ 'iat': Math.floor( now / 1000 ),
+ 'exp': Math.floor( tomorrow / 1000 )
+ }, privateKey, {
+ 'algorithm': 'ES256',
+ 'keyid': musicApiConfig.keyID
+ } );
- response.send( jwtToken );
- } );
+ response.send( jwtToken );
+ }
+ );
};
export default {
routes
-}
+};
diff --git a/backend/src/routes/room/index.ts b/backend/src/routes/room/index.ts
index 93f82a4..c9d563b 100644
--- a/backend/src/routes/room/index.ts
+++ b/backend/src/routes/room/index.ts
@@ -1,18 +1,47 @@
import express from 'express';
+import {
+ getLoginSdk
+} from '../../sdk';
+import {
+ getOwnershipManager
+} from '../../manager/users/ownership';
+import rooms from '../../manager/rooms';
import tracking from './tracking';
import update from './update';
-const routes = ( app: express.Application ) => {
- tracking.routes( app );
- update.routes( app );
+const routes = ( app: express.Application, foss: boolean ) => {
+ const sdk = getLoginSdk( foss );
+ const ownership = getOwnershipManager( foss );
- app.get( '/room/create', ( request: express.Request, response: express.Response ) => {
+ tracking.routes( app, foss );
+ update.routes( app, foss );
- } );
+ app.get(
+ '/room/create',
+ sdk.loginCheck(),
+ ownership.middleware(),
+ ( request: express.Request, response: express.Response ) => {
+ if ( !request.query.room ) return response.sendStatus( 400 );
- app.get( '/room/:id/close', ( request: express.Request, response: express.Response ) => {
+ if ( rooms.create( String( request.query.room ), sdk.getUID( request )! ) )
+ response.sendStatus( 200 );
+ else
+ response.sendStatus( 500 );
+ }
+ );
- } );
+ app.get(
+ '/room/:id/close',
+ sdk.loginCheck(),
+ ( request: express.Request, response: express.Response ) => {
+ if ( typeof request.params.room !== 'string' ) return response.sendStatus( 400 );
+
+ if ( rooms.close( String( request.query.room ), sdk.getUID( request )! ) )
+ response.sendStatus( 200 );
+ else
+ response.sendStatus( 500 );
+ }
+ );
};
export default {
diff --git a/backend/src/routes/room/tracking.ts b/backend/src/routes/room/tracking.ts
index 7132378..2df844d 100644
--- a/backend/src/routes/room/tracking.ts
+++ b/backend/src/routes/room/tracking.ts
@@ -1,22 +1,46 @@
+import corsManager from '../../corsManager';
import express from 'express';
+import {
+ getLoginSdk
+} from '../../sdk';
+import rooms from '../../manager/rooms';
+import {
+ sseMiddleware
+} from '../../manager/rooms/sse';
-const routes = ( app: express.Application ) => {
- app.get( '/room/:id/tracking/exit', ( request: express.Request, response: express.Response ) => {
+const tackingUpdate = ( message: string ) => {
+ return ( request: express.Request, response: express.Response ) => {
+ if ( typeof request.params.id === 'string' ) {
+ if ( rooms.sendTrackingUpdate( request.params.id, message ) )
+ response.sendStatus( 200 );
+ else
+ response.sendStatus( 500 );
+ } else
+ response.sendStatus( 400 );
+ };
+};
- } );
+const routes = ( app: express.Application, foss: boolean ) => {
+ const sdk = getLoginSdk( foss );
- app.get( '/room/:id/tracking/ping', ( request: express.Request, response: express.Response ) => {
+ app.get(
+ '/room/:id/tracking/exit',
+ corsManager.middleware( false ),
+ tackingUpdate( 'exit' )
+ );
- } );
+ app.get(
+ '/room/:id/tracking/ping',
+ corsManager.middleware( false ),
+ tackingUpdate( 'ping' )
+ );
- app.get( '/room/:id/tracking/connect', ( request: express.Request, response: express.Response ) => {
-
- } );
-
- app.get( '/room/:id/admin', ( request: express.Request, response: express.Response ) => {
- // Endpoint for the streamer to connect to, all above events are sent there
-
- } );
+ app.get(
+ '/room/:id/admin',
+ corsManager.middleware( false ),
+ sdk.loginCheck(),
+ sseMiddleware( 'client' )
+ );
};
export default {
diff --git a/backend/src/routes/room/update.ts b/backend/src/routes/room/update.ts
index 2cc6b4d..812ebae 100644
--- a/backend/src/routes/room/update.ts
+++ b/backend/src/routes/room/update.ts
@@ -1,21 +1,91 @@
+import bodyParser from 'body-parser';
+import corsManager from '../../corsManager';
import express from 'express';
+import {
+ getLoginSdk
+} from '../../sdk';
+import logger from '../../logger';
+import rooms from '../../manager/rooms';
+import {
+ sseMiddleware
+} from '../../manager/rooms/sse';
-const routes = ( app: express.Application ) => {
- app.get( '/room/:id/connect', ( request: express.Request, response: express.Response ) => {
+const routes = ( app: express.Application, foss: boolean ) => {
+ const sdk = getLoginSdk( foss );
+ app.get( '/room/:id/connect', corsManager.middleware( false ), sseMiddleware( 'client' ) );
+
+ app.get( '/room/:id/poll', corsManager.middleware( false ), ( request: express.Request, response: express.Response ) => {
+ if ( typeof request.params.id !== 'string' )
+ return response.sendStatus( 400 );
+
+ const room = rooms.get( request.params.id );
+
+ if ( !room ) response.sendStatus( 404 );
+
+ const lastRequest = parseInt( String( request.query.last ) ) ?? 0;
+
+ response.send(
+ JSON.stringify( {
+ 'state': room.state,
+ 'playlist': lastRequest < room.playlist.lastUpdate ? room.playlist : undefined
+ } )
+ );
} );
- app.get( '/room/:id/poll', ( request: express.Request, response: express.Response ) => {
+ app.options( '/room/:id/update/playlist', corsManager.middleware( false ) );
+ app.post(
+ '/room/:id/update/playlist',
+ corsManager.middleware( false ),
+ sdk.loginCheck(),
+ bodyParser.json(),
+ ( request: express.Request, response: express.Response ) => {
+ if ( typeof request.params.id !== 'string' )
+ return response.sendStatus( 400 );
- } );
+ try {
+ if ( rooms.updatePlaylist(
+ request.params.id,
+ sdk.getUID( request )!,
+ request.body.playlist ? JSON.parse( request.body.playlist ) : []
+ ) )
+ response.sendStatus( 200 );
+ else
+ response.sendStatus( 400 );
+ } catch ( err ) {
+ logger.error( 'Failed sending updated playlists with error', err );
+ response.sendStatus( 500 );
+ }
+ }
+ );
- app.get( '/room/:id/update/playlist', ( request: express.Request, response: express.Response ) => {
+ app.options( '/room/:id/update/state', corsManager.middleware( false ) );
+ app.post(
+ '/room/:id/update/state',
+ corsManager.middleware( false ),
+ sdk.loginCheck(),
+ bodyParser.json(),
+ ( request: express.Request, response: express.Response ) => {
+ if ( typeof request.params.id !== 'string' )
+ return response.sendStatus( 400 );
- } );
-
- app.get( '/room/:id/update/state', ( request: express.Request, response: express.Response ) => {
-
- } );
+ try {
+ if ( rooms.updateState(
+ request.params.id,
+ sdk.getUID( request )!,
+ request.body.playing ?? false,
+ request.body.index ?? -1,
+ request.body.start ?? new Date().getTime()
+ ) )
+ response.sendStatus( 200 );
+ else
+ response.sendStatus( 400 );
+ } catch ( err ) {
+ logger.error( 'Failed sending updated state with error', err );
+ response.sendStatus( 500 );
+ }
+ }
+ );
};
export default {
diff --git a/backend/src/routes/user/index.ts b/backend/src/routes/user/index.ts
index b30205d..7c02636 100644
--- a/backend/src/routes/user/index.ts
+++ b/backend/src/routes/user/index.ts
@@ -1,11 +1,82 @@
+import {
+ Playlist,
+ UserPlaylistFile
+} from '../../dtype/file';
+import {
+ getUserFile,
+ writeUserFile
+} from '../../manager/users';
+import {
+ Config
+} from '../../dtype/config';
+import bodyParser from 'body-parser';
import express from 'express';
+import {
+ getLoginSdk
+} from '../../sdk';
+import {
+ getOwnershipManager
+} from '../../manager/users/ownership';
+import logger from '../../logger';
-const routes = ( app: express.Application ) => {
- app.get( '/user/playlists', ( request: express.Request, response: express.Response ) => {
+const routes = ( app: express.Application, foss: boolean, config: Config ) => {
+ const sdk = getLoginSdk( foss );
+ const ownership = getOwnershipManager( foss );
+ app.get( '/user/owned', sdk.loginCheck(), async ( request: express.Request, response: express.Response ) => {
+ if ( await ownership.getOwned( request ) )
+ response.send( JSON.stringify( {
+ 'status': true,
+ 'shareMode': config.clientMode
+ } ) );
+ else
+ response.send( JSON.stringify( {
+ 'status': false,
+ 'shareMode': config.clientMode
+ } ) );
} );
- app.post( '/user/playlists', ( request: express.Request, response: express.Response ) => {
+ app.get(
+ '/user/playlists',
+ sdk.loginCheck(),
+ async ( request: express.Request, response: express.Response ) => {
+ try {
+ const playlists = await getUserFile( sdk.getUID( request )! );
- } );
+ response.send( JSON.stringify( playlists.playlists ) );
+ } catch ( e ) {
+ logger.error( 'Failed retrieving user file with error', e );
+ }
+ }
+ );
+
+ app.post(
+ '/user/playlists',
+ sdk.loginCheck(),
+ ownership.middleware(),
+ bodyParser.json(),
+ async ( request: express.Request, response: express.Response ) => {
+ try {
+ const contents: UserPlaylistFile = {
+ 'playlists': request.body as Playlist[],
+ 'userid': sdk.getUID( request )!,
+ 'version': '1'
+ };
+
+ try {
+ await writeUserFile( contents.userid, contents );
+ response.sendStatus( 200 );
+ } catch ( e ) {
+ logger.error( 'Failed updating user playlists', e );
+ response.sendStatus( 500 );
+ }
+ } catch ( e ) {
+ logger.error( 'Failed retrieving user file with error', e );
+ }
+ }
+ );
+};
+
+export default {
+ routes
};
diff --git a/backend/src/sdk/store-sdk-stub.ts b/backend/src/sdk/store-sdk-stub.ts
index ec97683..70ca946 100644
--- a/backend/src/sdk/store-sdk-stub.ts
+++ b/backend/src/sdk/store-sdk-stub.ts
@@ -5,16 +5,24 @@ interface StoreSDKConfig {
'loglevel': 'debug' | 'info' | 'log' | 'warn' | 'error' | 'none';
}
+interface SubscriptionData {
+ 'id': string;
+ 'status': string;
+ 'expires': number;
+}
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const getSubscriptions = ( _uid: string ) => {
+const getSubscriptions = async ( _uid: string ): Promise => {
return [ {
'id': 'com.janishutz.MusicPlayer.subscription',
'expires': new Date().getTime() + 200000,
- 'status': true
+ 'status': 'active'
} ];
};
-const configure = ( _config: StoreSDKConfig ) => {}
+const configure = ( config: StoreSDKConfig ) => {
+ console.log( config );
+};
export default {
getSubscriptions,
diff --git a/backend/src/token.ts b/backend/src/token.ts
new file mode 100644
index 0000000..db2c2bf
--- /dev/null
+++ b/backend/src/token.ts
@@ -0,0 +1,35 @@
+import crypto from 'node:crypto';
+
+/**
+ * Generate a token for e.g. login, etc
+ * @param length - The number of characters of the token
+ * @returns The generated token
+ */
+export const generateToken = ( length: number ): string => {
+ let token = '';
+
+ const min = 45;
+ const max = 122;
+
+ for ( let i = 0; i < length; i++ ) {
+ const buf = new Uint8Array( 1 );
+
+ crypto.getRandomValues( buf );
+ let randomNumber = Math.floor( ( buf[ 0 ] / 256 * ( max - min ) ) + min );
+
+ while (
+ ( 58 < randomNumber && randomNumber < 63 )
+ || ( 90 < randomNumber && randomNumber < 95 )
+ || ( 95 < randomNumber && randomNumber < 97 )
+ ) {
+ const buf = new Uint8Array( 1 );
+
+ crypto.getRandomValues( buf );
+ randomNumber = Math.floor( ( buf[ 0 ] / 256 * ( max - min ) ) + min );
+ }
+
+ token += String.fromCharCode( randomNumber );
+ }
+
+ return token;
+};