mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
feat(backend): login, most endpoints complete
This commit is contained in:
@@ -4,21 +4,21 @@
|
||||
</div>
|
||||
|
||||
<div id="badges" align="center">
|
||||
<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 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="GitHub Repo filecount" src="https://img.shields.io/github/directory-file-count/simplePCBuilding/MusicPlayerV2.svg">
|
||||
<img alt="Project License" src="https://img.shields.io/github/license/janishutz/MusicPlayer.svg">
|
||||
<img alt="GitHub Repo size" src="https://img.shields.io/github/repo-size/janishutz/MusicPlayer.svg">
|
||||
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues-pr-raw/janishutz/MusicPlayer">
|
||||
<img alt="Top Languages" src="https://img.shields.io/github/languages/top/janishutz/MusicPlayer">
|
||||
<img alt="GitHub Repo filecount" src="https://img.shields.io/github/directory-file-count/janishutz/MusicPlayer.svg">
|
||||
<br>
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/simplePCBuilding/MusicPlayerV2">
|
||||
<img alt="GitHub watchers" src="https://img.shields.io/github/watchers/simplePCBuilding/MusicPlayerV2">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/simplePCBuilding/MusicPlayerV2">
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/simplePCBuilding/MusicPlayerV2">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/janishutz/MusicPlayer">
|
||||
<img alt="GitHub watchers" src="https://img.shields.io/github/watchers/janishutz/MusicPlayer">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/janishutz/MusicPlayer">
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/janishutz/MusicPlayer">
|
||||
<br>
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/simplePCBuilding/MusicPlayerV2/total?label=Downloads (total)">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/downloads/simplePCBuilding/MusicPlayerV2/latest/total?label=Downloads (latest)">
|
||||
<img alt="Latest release" src="https://img.shields.io/github/release/simplePCBuilding/MusicPlayerV2.svg">
|
||||
<img alt="App Version" src="https://img.shields.io/github/package-json/v/simplePCBuilding/MusicPlayerV2.svg?label=Development Version">
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/janishutz/MusicPlayer/total?label=Downloads (total)">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/downloads/janishutz/MusicPlayer/latest/total?label=Downloads (latest)">
|
||||
<img alt="Latest release" src="https://img.shields.io/github/release/janishutz/MusicPlayer.svg">
|
||||
<img alt="App Version" src="https://img.shields.io/github/package-json/v/janishutz/MusicPlayer.svg?label=Development Version">
|
||||
</div>
|
||||
|
||||
# Development Branch
|
||||
@@ -38,6 +38,7 @@ The [hosted version](https://music.janishutz.com) of this MusicPlayer, which is
|
||||
</div>
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"$schema": "../config.schema.json",
|
||||
"mode": "foss"
|
||||
"mode": "foss",
|
||||
"clientMode": "sse",
|
||||
"webUrl": "http://localhost:8081"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+25
-14
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export interface Config {
|
||||
'mode': 'foss' | 'hosted';
|
||||
'clientMode': 'sse' | 'poll';
|
||||
'webUrl': string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Vendored
+19
-13
@@ -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 {
|
||||
|
||||
@@ -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 );
|
||||
} );
|
||||
};
|
||||
};
|
||||
@@ -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<UserPlaylistFile> => {
|
||||
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<boolean> => {
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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<SubscriptionData[]> => {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user