mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 04:54:23 +00:00
basically done
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<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 class="player">
|
<div class="player">
|
||||||
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
||||||
<div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )">
|
<div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )">
|
||||||
@@ -100,6 +108,7 @@
|
|||||||
const isConnectedToNotifier = ref( false );
|
const isConnectedToNotifier = ref( false );
|
||||||
const popup = ref( popupModule );
|
const popup = ref( popupModule );
|
||||||
const roomName = ref( '' );
|
const roomName = ref( '' );
|
||||||
|
const isShowingWarning = ref( false );
|
||||||
let currentlyOpenPopup = '';
|
let currentlyOpenPopup = '';
|
||||||
|
|
||||||
const emits = defineEmits( [ 'playerStateChange' ] );
|
const emits = defineEmits( [ 'playerStateChange' ] );
|
||||||
@@ -194,12 +203,17 @@
|
|||||||
popup.value.openPopup( {
|
popup.value.openPopup( {
|
||||||
title: 'Define a share name',
|
title: 'Define a share name',
|
||||||
popupType: 'input',
|
popupType: 'input',
|
||||||
subtitle: 'A share allows others to join your playlist and see the current song, the playback position and the upcoming songs. You can get the link to the page, once the share is set up. Please choose a name, which will then be part of the URL with which others can join the share',
|
subtitle: 'A share allows others to join your playlist and see the current song, the playback position and the upcoming songs. You can get the link to the page, once the share is set up. Please choose a name, which will then be part of the URL with which others can join the share. The anti tamper feature notifies you, whenever a user leaves the fancy view.',
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
name: 'Share Name',
|
name: 'Share Name',
|
||||||
dataType: 'text',
|
dataType: 'text',
|
||||||
id: 'roomName'
|
id: 'roomName'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Use Anti-Tamper?',
|
||||||
|
dataType: 'checkbox',
|
||||||
|
id: 'useAntiTamper'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} );
|
} );
|
||||||
@@ -377,7 +391,6 @@
|
|||||||
if ( pos.value > 0 && !hasStarted ) {
|
if ( pos.value > 0 && !hasStarted ) {
|
||||||
getDetails();
|
getDetails();
|
||||||
playingSong = player.getPlayingSong();
|
playingSong = player.getPlayingSong();
|
||||||
console.log( pos.value );
|
|
||||||
prepNiceDurationTime( playingSong );
|
prepNiceDurationTime( playingSong );
|
||||||
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
||||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||||
@@ -472,8 +485,8 @@
|
|||||||
player.prepare( 0 );
|
player.prepare( 0 );
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
startProgressTracker();
|
startProgressTracker();
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
|
||||||
}
|
}
|
||||||
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
}
|
}
|
||||||
|
|
||||||
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
||||||
@@ -495,9 +508,13 @@
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
const dismissNotification = () => {
|
||||||
|
isShowingWarning.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
const popupReturnHandler = ( data: any ) => {
|
const popupReturnHandler = ( data: any ) => {
|
||||||
if ( currentlyOpenPopup === 'create-share' ) {
|
if ( currentlyOpenPopup === 'create-share' ) {
|
||||||
notificationHandler.connect( data.roomName ).then( () => {
|
notificationHandler.connect( data.roomName, data.useAntiTamper ?? false ).then( () => {
|
||||||
roomName.value = notificationHandler.getRoomName();
|
roomName.value = notificationHandler.getRoomName();
|
||||||
isConnectedToNotifier.value = true;
|
isConnectedToNotifier.value = true;
|
||||||
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
||||||
@@ -505,6 +522,10 @@
|
|||||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
||||||
notificationHandler.emit( 'playlist-update', playlist.value );
|
notificationHandler.emit( 'playlist-update', playlist.value );
|
||||||
notifications.value.createNotification( 'Joined share "' + data.roomName + '"!', 5, 'ok', 'normal' );
|
notifications.value.createNotification( 'Joined share "' + data.roomName + '"!', 5, 'ok', 'normal' );
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
notificationHandler.registerListener( 'tampering-msg', ( _ ) => {
|
||||||
|
isShowingWarning.value = true;
|
||||||
|
} );
|
||||||
} ).catch( e => {
|
} ).catch( e => {
|
||||||
if ( e === 'ERR_CONFLICT' ) {
|
if ( e === 'ERR_CONFLICT' ) {
|
||||||
notifications.value.createNotification( 'A share with this name exists already!', 5, 'error', 'normal' );
|
notifications.value.createNotification( 'A share with this name exists already!', 5, 'error', 'normal' );
|
||||||
@@ -517,6 +538,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener( 'beforeunload', () => {
|
||||||
|
notificationHandler.disconnect();
|
||||||
|
} );
|
||||||
|
|
||||||
defineExpose( {
|
defineExpose( {
|
||||||
logIntoAppleMusic,
|
logIntoAppleMusic,
|
||||||
getPlaylists,
|
getPlaylists,
|
||||||
@@ -764,4 +789,49 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: -50%;
|
bottom: -50%;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.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: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
<button @click="openSearch()" v-if="$props.isLoggedIntoAppleMusic" class="small-buttons" title="Search Apple Music for the song"><span class="material-symbols-outlined">search</span></button>
|
<button @click="openSearch()" v-if="$props.isLoggedIntoAppleMusic" class="small-buttons" title="Search Apple Music for the song"><span class="material-symbols-outlined">search</span></button>
|
||||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
||||||
<div class="playlist-box" id="pl-box">
|
<div class="playlist-box" id="pl-box">
|
||||||
<!-- TODO: Allow adding more songs with search on Apple Music or loading from local disk -->
|
<!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? -->
|
||||||
|
<!-- TODO: Allow deleting songs, as well as whole playlist -->
|
||||||
|
<!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
|
||||||
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
||||||
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
||||||
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
|
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
|
||||||
@@ -186,7 +188,7 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.playlist-box {
|
.playlist-box {
|
||||||
height: calc( 100% - 100px );
|
height: calc( 100% - 150px );
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
const closePopupReturn = () => {
|
const closePopupReturn = () => {
|
||||||
for ( let el in popupContent.value.data ) {
|
for ( let el in popupContent.value.data ) {
|
||||||
if ( !data.value[ popupContent.value.data[ parseInt( el ) ].id ] ) {
|
if ( !data.value[ popupContent.value.data[ parseInt( el ) ].id ] && popupContent.value.data[ parseInt( el ) ].dataType !== 'checkbox' ) {
|
||||||
isShowingIncomplete.value = true;
|
isShowingIncomplete.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,6 +150,11 @@
|
|||||||
}
|
}
|
||||||
isShowingPopup.value = true;
|
isShowingPopup.value = true;
|
||||||
popupContent.value = popupConfig;
|
popupContent.value = popupConfig;
|
||||||
|
for ( const el in popupContent.value.data ) {
|
||||||
|
if ( popupContent.value.data[ parseInt( el ) ].dataType === 'checkbox' ) {
|
||||||
|
data.value[ popupContent.value.data[ parseInt( el ) ].id ] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose( {
|
defineExpose( {
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ const router = createRouter( {
|
|||||||
'title': 'App'
|
'title': 'App'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/get',
|
||||||
|
name: 'get',
|
||||||
|
component: () => import( '../views/GetView.vue' ),
|
||||||
|
meta: {
|
||||||
|
'authRequired': false,
|
||||||
|
'title': 'Get'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/share/:name',
|
path: '/share/:name',
|
||||||
name: 'share',
|
name: 'share',
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const colorThief = new ColorThief();
|
|||||||
const getImageData = (): Promise<number[][]> => {
|
const getImageData = (): Promise<number[][]> => {
|
||||||
return new Promise( ( resolve ) => {
|
return new Promise( ( resolve ) => {
|
||||||
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement );
|
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement );
|
||||||
console.log( img );
|
|
||||||
if ( img.complete ) {
|
if ( img.complete ) {
|
||||||
resolve( colorThief.getPalette( img ) );
|
resolve( colorThief.getPalette( img ) );
|
||||||
} else {
|
} else {
|
||||||
@@ -18,7 +17,6 @@ const getImageData = (): Promise<number[][]> => {
|
|||||||
const createBackground = () => {
|
const createBackground = () => {
|
||||||
return new Promise( ( resolve ) => {
|
return new Promise( ( resolve ) => {
|
||||||
getImageData().then( palette => {
|
getImageData().then( palette => {
|
||||||
console.log( palette );
|
|
||||||
const colourDetails: number[][] = [];
|
const colourDetails: number[][] = [];
|
||||||
const colours: string[] = [];
|
const colours: string[] = [];
|
||||||
let differentEnough = true;
|
let differentEnough = true;
|
||||||
@@ -118,7 +116,7 @@ const micAudioHandler = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
prevSpectrum = currentSpectrum;
|
prevSpectrum = currentSpectrum;
|
||||||
}, 20 );
|
}, 60 / 180 * 250 );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ class NotificationHandler {
|
|||||||
/**
|
/**
|
||||||
* Create a room token and connect to
|
* Create a room token and connect to
|
||||||
* @param {string} roomName
|
* @param {string} roomName
|
||||||
|
* @param {boolean} useAntiTamper
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
connect ( roomName: string ): Promise<void> {
|
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName, { credentials: 'include' } ).then( res => {
|
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, { credentials: 'include' } ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
res.text().then( text => {
|
res.text().then( text => {
|
||||||
this.roomToken = text;
|
this.roomToken = text;
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
import libraryView from '@/components/libraryView.vue';
|
import libraryView from '@/components/libraryView.vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import type { ReadFile } from '@/scripts/song';
|
import type { ReadFile } from '@/scripts/song';
|
||||||
|
import router from '@/router';
|
||||||
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
|
||||||
const isLoggedIntoAppleMusic = ref( false );
|
const isLoggedIntoAppleMusic = ref( false );
|
||||||
const isReady = ref( false );
|
const isReady = ref( false );
|
||||||
const isShowingFullScreenPlayer = ref( false );
|
const isShowingFullScreenPlayer = ref( false );
|
||||||
const player = ref( playerView );
|
const player = ref( playerView );
|
||||||
const playlists = ref( [] );
|
const playlists = ref( [] );
|
||||||
const hasFinishedLoading = ref( true );
|
const hasFinishedLoading = ref( false );
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const handlePlayerStateChange = ( newState: string ) => {
|
const handlePlayerStateChange = ( newState: string ) => {
|
||||||
if ( newState === 'hide' ) {
|
if ( newState === 'hide' ) {
|
||||||
@@ -72,13 +75,21 @@
|
|||||||
player.value.selectCustomPlaylist( playlist );
|
player.value.selectCustomPlaylist( playlist );
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', { credentials: 'include' } ).then( res => {
|
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', { credentials: 'include' } ).then( res => {
|
||||||
// if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
// res.json().then( json => {
|
res.text().then( text => {
|
||||||
|
if ( text === 'ok' ) {
|
||||||
// } );
|
hasFinishedLoading.value = true;
|
||||||
// }
|
userStore.setSubscriptionStatus( true );
|
||||||
// } );
|
} else {
|
||||||
|
userStore.setSubscriptionStatus( false );
|
||||||
|
router.push( '/get' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
console.log( res.status );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
7
MusicPlayerV2-GUI/src/views/GetView.vue
Normal file
7
MusicPlayerV2-GUI/src/views/GetView.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Get MusicPlayer</h1>
|
||||||
|
<p>MusicPlayer offers</p>
|
||||||
|
<a href="https://store.janishutz.com/product/com.janishutz.MusicPlayer" class="fancy-button">Subscribe</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
style="margin-top: 5vh;" title="Sign in or sign up with janishutz.com ID" v-if="status"
|
style="margin-top: 5vh;" title="Sign in or sign up with janishutz.com ID" v-if="status"
|
||||||
>{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}</button>
|
>{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}</button>
|
||||||
<p v-else>We are sorry, but we were unable to initialize the login services. Please reload the page if you wish to retry!</p>
|
<p v-else>We are sorry, but we were unable to initialize the login services. Please reload the page if you wish to retry!</p>
|
||||||
<p>MusicPlayer is a browser based Music Player, that allows you to connect other devices, simply with another web-browser, where you can see the current playlist with sleek animations. You can log in using your Apple Music account or load a playlist from your local disk, simply by selecting the songs using a file picker.</p>
|
<p style="width: 80%;">MusicPlayer is a browser based Music Player, that allows you to connect other devices, simply with another web-browser, where you can see the current playlist with sleek animations. You can log in using your Apple Music account or load a playlist from your local disk, simply by selecting the songs using a file picker.</p>
|
||||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
||||||
<div class="current-song-wrapper">
|
<div class="current-song-wrapper">
|
||||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||||
<span v-else class="material-symbols-outlined">music_note</span>
|
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||||
<div class="current-song">
|
<div class="current-song">
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||||
<h1>{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
<h1>{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
startTimeTracker();
|
startTimeTracker();
|
||||||
}
|
}
|
||||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||||
hasLoaded.value = true;
|
hasLoaded.value = true;
|
||||||
conn.registerListener( 'playlist', ( data ) => {
|
conn.registerListener( 'playlist', ( data ) => {
|
||||||
playlist.value = data;
|
playlist.value = data;
|
||||||
@@ -87,7 +87,15 @@
|
|||||||
conn.registerListener( 'playlist-index', ( data ) => {
|
conn.registerListener( 'playlist-index', ( data ) => {
|
||||||
playingSong.value = parseInt( data );
|
playingSong.value = parseInt( data );
|
||||||
} );
|
} );
|
||||||
} ).catch( () => {
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
conn.registerListener( 'delete-share', ( _ ) => {
|
||||||
|
alert( 'This share was just deleted. It is no longer available. The page will reload automatically to try and re-establish connection!' );
|
||||||
|
conn.disconnect();
|
||||||
|
location.reload();
|
||||||
|
} );
|
||||||
|
} ).catch( e => {
|
||||||
|
console.error( e );
|
||||||
showCouldNotFindRoom.value = true;
|
showCouldNotFindRoom.value = true;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<span class="anti-tamper material-symbols-outlined" title="Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!" v-if="isAntiTamperEnabled">lock</span>
|
||||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
||||||
<div class="remote-view">
|
<div class="remote-view">
|
||||||
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||||
<div class="current-song-wrapper">
|
<div class="current-song-wrapper">
|
||||||
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
<img v-if="playlist[ playingSong ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
||||||
<span v-else class="material-symbols-outlined">music_note</span>
|
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||||
<div class="current-song">
|
<div class="current-song">
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||||
<h1>{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
<h1>{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}</h1>
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
const playbackStart = ref( 0 );
|
const playbackStart = ref( 0 );
|
||||||
let timeTracker = 0;
|
let timeTracker = 0;
|
||||||
const visualizationSettings = ref( 'mic' );
|
const visualizationSettings = ref( 'mic' );
|
||||||
|
const isAntiTamperEnabled = ref( false );
|
||||||
|
|
||||||
const conn = new SocketConnection();
|
const conn = new SocketConnection();
|
||||||
|
|
||||||
@@ -83,8 +85,12 @@
|
|||||||
startTimeTracker();
|
startTimeTracker();
|
||||||
}
|
}
|
||||||
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||||
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ] ? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||||
hasLoaded.value = true;
|
hasLoaded.value = true;
|
||||||
|
if ( d.useAntiTamper ) {
|
||||||
|
isAntiTamperEnabled.value = true;
|
||||||
|
notifier();
|
||||||
|
}
|
||||||
conn.registerListener( 'playlist', ( data ) => {
|
conn.registerListener( 'playlist', ( data ) => {
|
||||||
playlist.value = data;
|
playlist.value = data;
|
||||||
} );
|
} );
|
||||||
@@ -107,9 +113,17 @@
|
|||||||
playingSong.value = parseInt( data );
|
playingSong.value = parseInt( data );
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
setBackground();
|
setBackground();
|
||||||
}, 1000 )
|
}, 1000 );
|
||||||
} );
|
} );
|
||||||
} ).catch( () => {
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
conn.registerListener( 'delete-share', ( _ ) => {
|
||||||
|
alert( 'This share was just deleted. It is no longer available. This page will reload automatically!' );
|
||||||
|
conn.disconnect();
|
||||||
|
location.reload();
|
||||||
|
} );
|
||||||
|
} ).catch( e => {
|
||||||
|
console.error( e );
|
||||||
showCouldNotFindRoom.value = true;
|
showCouldNotFindRoom.value = true;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@@ -178,7 +192,7 @@
|
|||||||
|
|
||||||
const animateBeat = () => {
|
const animateBeat = () => {
|
||||||
$( '.beat-manual' ).stop();
|
$( '.beat-manual' ).stop();
|
||||||
const duration = Math.ceil( 60 / 120 * 500 ) - 50;
|
const duration = Math.ceil( 60 / 180 * 500 ) - 50;
|
||||||
$( '.beat-manual' ).fadeIn( 50 );
|
$( '.beat-manual' ).fadeIn( 50 );
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
$( '.beat-manual' ).fadeOut( duration );
|
$( '.beat-manual' ).fadeOut( duration );
|
||||||
@@ -225,10 +239,20 @@
|
|||||||
body: 'Please return to the original webpage immediately!',
|
body: 'Please return to the original webpage immediately!',
|
||||||
requireInteraction: true,
|
requireInteraction: true,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
conn.emit( 'tampering', '' );
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.anti-tamper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
bottom: 5px;
|
||||||
|
right: 5px;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.remote-view {
|
.remote-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -236,6 +260,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showcase-wrapper {
|
.showcase-wrapper {
|
||||||
@@ -428,6 +453,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
0
backend/config/store-sdk.config.testing.json
Normal file
0
backend/config/store-sdk.config.testing.json
Normal file
16
backend/package-lock.json
generated
16
backend/package-lock.json
generated
@@ -19,7 +19,8 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"node-mysql": "^0.4.2",
|
"node-mysql": "^0.4.2",
|
||||||
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
|
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
|
||||||
"socket.io": "^4.7.5"
|
"socket.io": "^4.7.5",
|
||||||
|
"store.janishutz.com-sdk": "file:../../store/sdk/dist"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
@@ -41,6 +42,15 @@
|
|||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../store/sdk/dist": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"express": "^4.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@socket.io/component-emitter": {
|
"node_modules/@socket.io/component-emitter": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
@@ -1285,6 +1295,10 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/store.janishutz.com-sdk": {
|
||||||
|
"resolved": "../../store/sdk/dist",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"node-mysql": "^0.4.2",
|
"node-mysql": "^0.4.2",
|
||||||
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
|
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist",
|
||||||
"socket.io": "^4.7.5"
|
"socket.io": "^4.7.5",
|
||||||
|
"store.janishutz.com-sdk": "file:../../store/sdk/dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { createServer } from 'node:http';
|
|||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import type { Room, Song } from './definitions';
|
import type { Room, Song } from './definitions';
|
||||||
|
import storeSDK from 'store.janishutz.com-sdk';
|
||||||
|
|
||||||
declare let __dirname: string | undefined
|
declare let __dirname: string | undefined
|
||||||
if ( typeof( __dirname ) === 'undefined' ) {
|
if ( typeof( __dirname ) === 'undefined' ) {
|
||||||
@@ -25,6 +26,11 @@ const run = () => {
|
|||||||
credentials: true,
|
credentials: true,
|
||||||
origin: true
|
origin: true
|
||||||
} ) );
|
} ) );
|
||||||
|
storeSDK.configure( {
|
||||||
|
backendURL: 'http://localhost:8083',
|
||||||
|
name: 'testing',
|
||||||
|
signingSecret: 'test',
|
||||||
|
} )
|
||||||
|
|
||||||
const httpServer = createServer( app );
|
const httpServer = createServer( app );
|
||||||
|
|
||||||
@@ -79,6 +85,7 @@ const run = () => {
|
|||||||
socket.on( 'delete-room', ( room: { name: string, token: string }, cb: ( res: { status: boolean, msg: string } ) => void ) => {
|
socket.on( 'delete-room', ( room: { name: string, token: string }, cb: ( res: { status: boolean, msg: string } ) => void ) => {
|
||||||
if ( room.token === socketData[ room.name ].roomToken ) {
|
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||||
socket.leave( room.name );
|
socket.leave( room.name );
|
||||||
|
socket.to( room.name ).emit( 'delete-share', room.name );
|
||||||
socketData[ room.name ] = undefined;
|
socketData[ room.name ] = undefined;
|
||||||
cb( {
|
cb( {
|
||||||
status: true,
|
status: true,
|
||||||
@@ -92,7 +99,7 @@ const run = () => {
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
socket.on( 'join-room', ( room: string, cb: ( res: { status: boolean, msg: string, data?: { playbackStatus: boolean, playbackStart: number, playlist: Song[], playlistIndex: number } } ) => void ) => {
|
socket.on( 'join-room', ( room: string, cb: ( res: { status: boolean, msg: string, data?: { playbackStatus: boolean, playbackStart: number, playlist: Song[], playlistIndex: number, useAntiTamper: boolean } } ) => void ) => {
|
||||||
if ( socketData[ room ] ) {
|
if ( socketData[ room ] ) {
|
||||||
socket.join( room );
|
socket.join( room );
|
||||||
cb( {
|
cb( {
|
||||||
@@ -101,6 +108,7 @@ const run = () => {
|
|||||||
playbackStatus: socketData[ room ].playbackStatus,
|
playbackStatus: socketData[ room ].playbackStatus,
|
||||||
playlist: socketData[ room ].playlist,
|
playlist: socketData[ room ].playlist,
|
||||||
playlistIndex: socketData[ room ].playlistIndex,
|
playlistIndex: socketData[ room ].playlistIndex,
|
||||||
|
useAntiTamper: socketData[ room ].useAntiTamper,
|
||||||
},
|
},
|
||||||
msg: 'STATUS_OK',
|
msg: 'STATUS_OK',
|
||||||
status: true,
|
status: true,
|
||||||
@@ -177,6 +185,7 @@ const run = () => {
|
|||||||
roomName: roomName,
|
roomName: roomName,
|
||||||
roomToken: roomToken,
|
roomToken: roomToken,
|
||||||
ownerUID: sdk.getUserData( request ).uid,
|
ownerUID: sdk.getUserData( request ).uid,
|
||||||
|
useAntiTamper: request.query.useAntiTamper === 'true' ? true : false,
|
||||||
};
|
};
|
||||||
response.send( roomToken );
|
response.send( roomToken );
|
||||||
} else {
|
} else {
|
||||||
@@ -212,6 +221,31 @@ const run = () => {
|
|||||||
|
|
||||||
// TODO: Get user's subscriptions using store sdk
|
// TODO: Get user's subscriptions using store sdk
|
||||||
|
|
||||||
|
app.get( '/checkUserStatus', ( request: express.Request, response: express.Response ) => {
|
||||||
|
if ( sdk.checkAuth( request ) ) {
|
||||||
|
storeSDK.getSubscriptions( sdk.getUserData( request ).uid ).then( stat => {
|
||||||
|
let owned = false;
|
||||||
|
const now = new Date().getTime();
|
||||||
|
for ( let sub in stat ) {
|
||||||
|
if ( stat[ sub ].expires - now > 0
|
||||||
|
&& ( stat[ sub ].id === 'com.janishutz.MusicPlayer.subscription' || stat[ sub ].id === 'com.janishutz.MusicPlayer.subscription-month' ) ) {
|
||||||
|
owned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( owned ) {
|
||||||
|
response.send( 'ok' );
|
||||||
|
} else {
|
||||||
|
response.send( 'ERR_NOT_OWNED' );
|
||||||
|
}
|
||||||
|
} ).catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
response.status( 404 ).send( 'ERR_NOT_OWNED' );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
response.status( 401 ).send( 'ERR_AUTH_REQUIRED' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
app.use( ( request: express.Request, response: express.Response, next: express.NextFunction ) => {
|
app.use( ( request: express.Request, response: express.Response, next: express.NextFunction ) => {
|
||||||
response.status( 404 ).send( 'ERR_NOT_FOUND' );
|
response.status( 404 ).send( 'ERR_NOT_FOUND' );
|
||||||
// response.sendFile( path.join( __dirname + '' ) )
|
// response.sendFile( path.join( __dirname + '' ) )
|
||||||
|
|||||||
1
backend/src/definitions.d.ts
vendored
1
backend/src/definitions.d.ts
vendored
@@ -6,6 +6,7 @@ export interface Room {
|
|||||||
roomName: string;
|
roomName: string;
|
||||||
roomToken: string;
|
roomToken: string;
|
||||||
ownerUID: string;
|
ownerUID: string;
|
||||||
|
useAntiTamper: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Song {
|
export interface Song {
|
||||||
|
|||||||
Reference in New Issue
Block a user