mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04:23 +00:00
add basic functionality
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
--hover-color: rgb(165, 165, 165);
|
||||
--accent-background-hover: rgb(124, 140, 236);
|
||||
--overlay-color: rgba(0, 0, 0, 0.7);
|
||||
--inactive-color: rgb(100, 100, 100);
|
||||
--border-color: rgb(100, 100, 100);
|
||||
--highlight-backdrop: rgb(143, 134, 192);
|
||||
--hint-color: rgb(174, 210, 221);
|
||||
--PI: 3.14159265358979;
|
||||
@@ -33,7 +33,7 @@
|
||||
--hover-color: rgb(83, 83, 83);
|
||||
--accent-background-hover: #4380a8;
|
||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
||||
--inactive-color: rgb(190, 190, 190);
|
||||
--border-color: rgb(190, 190, 190);
|
||||
--highlight-backdrop: rgb(85, 63, 207);
|
||||
--hint-color: rgb(88, 91, 110);
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
--hover-color: rgb(83, 83, 83);
|
||||
--accent-background-hover: #4380a8;
|
||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
||||
--inactive-color: rgb(190, 190, 190);
|
||||
--border-color: rgb(190, 190, 190);
|
||||
--highlight-backdrop: rgb(85, 63, 207);
|
||||
--hint-color: rgb(88, 91, 110);
|
||||
}
|
||||
@@ -76,6 +76,8 @@
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var( --background-color );
|
||||
color: var( --primary-color );
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
@@ -4,43 +4,105 @@ const path = require( 'path' );
|
||||
const cors = require( 'cors' );
|
||||
const fs = require( 'fs' );
|
||||
const bodyParser = require( 'body-parser' );
|
||||
const musicMetadata = require( 'music-metadata' );
|
||||
const dialog = require( 'electron' ).dialog;
|
||||
|
||||
app.use( bodyParser.urlencoded( { extended: false } ) );
|
||||
app.use( bodyParser.json() );
|
||||
app.use( cors() );
|
||||
|
||||
let indexedData = {};
|
||||
let coverArtIndex = {};
|
||||
const allowedFileTypes = [ '.mp3', '.wav', '.flac' ]
|
||||
|
||||
app.get( '/', ( request, response ) => {
|
||||
response.send( 'Hello world' );
|
||||
} );
|
||||
|
||||
|
||||
app.get( '/openSongs', ( req, res ) => {
|
||||
res.send( '{ "data": [ "/home/janis/Music/KB2022" ] }' )
|
||||
// res.send( '{ "data": [ "/home/janis/Music/KB2022" ] }' );
|
||||
res.send( '{ "data": [ "/mnt/storage/SORTED/Music/audio/KB2022" ] }' );
|
||||
// res.send( { 'data': dialog.showOpenDialogSync( { properties: [ 'openDirectory' ], title: 'Open music library folder' } ) } );
|
||||
} );
|
||||
|
||||
app.get( '/indexDirs', ( req, res ) => {
|
||||
if ( req.query.dir ) {
|
||||
fs.readdir( req.query.dir, { encoding: 'utf-8' }, ( err, dat ) => {
|
||||
if ( err ) res.status( 500 ).send( 'err' );
|
||||
res.send( dat );
|
||||
} );
|
||||
if ( indexedData[ req.query.dir ] ) {
|
||||
console.log( 'using cache' );
|
||||
res.send( indexedData[ req.query.dir ] );
|
||||
} else {
|
||||
fs.readdir( req.query.dir, { encoding: 'utf-8' }, ( err, dat ) => {
|
||||
if ( err ) res.status( 500 ).send( err );
|
||||
( async() => {
|
||||
// TODO: Check for songlist.csv or songlist.json file and use the data provided there for each song to override
|
||||
// what was found automatically. If no song title was found in songlist or metadata, use filename
|
||||
let files = {};
|
||||
for ( let file in dat ) {
|
||||
if ( allowedFileTypes.includes( dat[ file ].slice( dat[ file ].indexOf( '.' ), dat[ file ].length ) ) ) {
|
||||
try {
|
||||
let metadata = await musicMetadata.parseFile( req.query.dir + '/' + dat[ file ] );
|
||||
files[ req.query.dir + '/' + dat[ file ] ] = {
|
||||
'artist': metadata[ 'common' ][ 'artist' ],
|
||||
'title': metadata[ 'common' ][ 'title' ],
|
||||
'year': metadata[ 'common' ][ 'year' ],
|
||||
'bpm': metadata[ 'common' ][ 'bpm' ],
|
||||
'genre': metadata[ 'common' ][ 'genre' ],
|
||||
'duration': metadata[ 'format' ][ 'duration' ],
|
||||
'isLossless': metadata[ 'format' ][ 'lossless' ],
|
||||
'sampleRate': metadata[ 'format' ][ 'sampleRate' ],
|
||||
'bitrate': metadata[ 'format' ][ 'bitrate' ],
|
||||
'numberOfChannels': metadata[ 'format' ][ 'numberOfChannels' ],
|
||||
'container': metadata[ 'format' ][ 'container' ],
|
||||
'filename': req.query.dir + '/' + dat[ file ],
|
||||
}
|
||||
if ( metadata[ 'common' ][ 'picture' ] ) {
|
||||
files[ req.query.dir + '/' + dat[ file ] ][ 'hasCoverArt' ] = true;
|
||||
if ( req.query.coverart == 'true' ) {
|
||||
coverArtIndex[ req.query.dir + '/' + dat[ file ] ] = metadata[ 'common' ][ 'picture' ] ? metadata[ 'common' ][ 'picture' ][ 0 ][ 'data' ] : undefined;
|
||||
}
|
||||
} else {
|
||||
files[ req.query.dir + '/' + dat[ file ] ][ 'hasCoverArt' ] = false;
|
||||
}
|
||||
} catch ( err ) {
|
||||
files[ req.query.dir + '/' + dat[ file ] ] = 'ERROR';
|
||||
}
|
||||
}
|
||||
}
|
||||
indexedData[ req.query.dir ] = files;
|
||||
res.send( files );
|
||||
} )();
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
|
||||
}
|
||||
} );
|
||||
|
||||
app.get( '/getSongDetails', ( req, res ) => {
|
||||
app.get( '/getSongCover', ( req, res ) => {
|
||||
if ( req.query.filename ) {
|
||||
fs.readFile( req.query.filename, ( err, data ) => {
|
||||
res.send( '' + data );
|
||||
} );
|
||||
if ( coverArtIndex[ req.query.filename ] ) {
|
||||
res.send( coverArtIndex[ req.query.filename ] );
|
||||
} else {
|
||||
res.status( 404 ).send( 'No cover image for this file' );
|
||||
}
|
||||
|
||||
} else {
|
||||
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
|
||||
}
|
||||
} );
|
||||
|
||||
app.get( '/getSongFile', ( req, res ) => {
|
||||
if ( req.query.filename ) {
|
||||
res.sendFile( req.query.filename );
|
||||
} else {
|
||||
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
|
||||
}
|
||||
} );
|
||||
|
||||
// TODO: Add get lyrics route later
|
||||
// 'lyrics': metadata[ 'common' ][ 'lyrics' ],
|
||||
|
||||
|
||||
app.use( ( request, response, next ) => {
|
||||
response.sendFile( path.join( __dirname + '' ) )
|
||||
|
||||
1
frontend/src/assets/loadingSymbol.svg
Normal file
1
frontend/src/assets/loadingSymbol.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M204-318q-22-38-33-78t-11-82q0-134 93-228t227-94h7l-64-64 56-56 160 160-160 160-56-56 64-64h-7q-100 0-170 70.5T240-478q0 26 6 51t18 49l-60 60ZM481-40 321-200l160-160 56 56-64 64h7q100 0 170-70.5T720-482q0-26-6-51t-18-49l60-60q22 38 33 78t11 82q0 134-93 228t-227 94h-7l64 64-56 56Z"/></svg>
|
||||
|
After Width: | Height: | Size: 386 B |
@@ -1,7 +1,17 @@
|
||||
<template>
|
||||
<div class="media-pool">
|
||||
<div v-if="hasLoadedSongs">
|
||||
<div v-for="song in songQueue">{{ song }}</div>
|
||||
<div v-if="hasLoadedSongs" style="width: 100%;" class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" class="song-list">
|
||||
<span class="material-symbols-outlined song-image" v-if="!loadCoverArtPreview || !song.hasCoverArt">music_note</span>
|
||||
<img v-else :src="'http://localhost:8081/getSongCover?filename=' + song.filename" class="song-image">
|
||||
<span class="material-symbols-outlined play-icon" @click="play( song )">play_arrow</span>
|
||||
<h3>{{ song.title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isLoadingSongs" class="no-songs">
|
||||
<h3>Loading songs...</h3>
|
||||
<p>Analyzing metadata...</p>
|
||||
<span class="material-symbols-outlined loading-spinner">autorenew</span>
|
||||
</div>
|
||||
<div v-else class="no-songs">
|
||||
<h3>No songs loaded</h3>
|
||||
@@ -11,11 +21,23 @@
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.loading-spinner {
|
||||
animation: spin 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate( 0deg );
|
||||
}
|
||||
to {
|
||||
transform: rotate( 720deg );
|
||||
}
|
||||
}
|
||||
|
||||
.media-pool {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -27,6 +49,58 @@
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.song-list-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.song-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 80%;
|
||||
margin: 2px;
|
||||
padding: 15px;
|
||||
border: 1px var( --border-color ) solid;
|
||||
}
|
||||
|
||||
.song-list h3 {
|
||||
margin: 0;
|
||||
display: block;
|
||||
margin-left: 10px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.song-list .song-image {
|
||||
width: 5vw;
|
||||
height: 5vw;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
font-size: 5vw;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
display: none;
|
||||
width: 5vw;
|
||||
height: 5vw;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
font-size: 5vw;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.song-list:hover .song-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.song-list:hover .play-icon {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -35,9 +109,12 @@
|
||||
data() {
|
||||
return {
|
||||
hasLoadedSongs: false,
|
||||
isLoadingSongs: false,
|
||||
loadCoverArtPreview: true,
|
||||
songQueue: [],
|
||||
loadedDirs: [],
|
||||
allowedFiletypes: [ '.mp3', '.wav' ]
|
||||
allowedFiletypes: [ '.mp3', '.wav' ],
|
||||
currentlyPlaying: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -45,10 +122,10 @@
|
||||
|
||||
},
|
||||
loadSongs() {
|
||||
this.isLoadingSongs = true;
|
||||
fetch( 'http://localhost:8081/openSongs' ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.json().then( json => {
|
||||
this.hasLoadedSongs = true;
|
||||
this.loadedDirs = json.data;
|
||||
this.indexFiles();
|
||||
} );
|
||||
@@ -57,19 +134,21 @@
|
||||
},
|
||||
indexFiles () {
|
||||
for ( let dir in this.loadedDirs ) {
|
||||
fetch( 'http://localhost:8081/indexDirs?dir=' + this.loadedDirs[ dir ] ).then( res => {
|
||||
fetch( 'http://localhost:8081/indexDirs?dir=' + this.loadedDirs[ dir ] + ( this.loadCoverArtPreview ? '&coverart=true' : '' ) ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.json().then( json => {
|
||||
for ( let file in json ) {
|
||||
const fileType = json[ file ].slice( json[ file ].indexOf( '.' ), json[ file ].length );
|
||||
if ( this.allowedFiletypes.includes( fileType ) ) {
|
||||
this.songQueue.push( json[ file ] );
|
||||
}
|
||||
for ( let song in json ) {
|
||||
this.songQueue.push( json[ song ] );
|
||||
}
|
||||
this.isLoadingSongs = false;
|
||||
this.hasLoadedSongs = true;
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
},
|
||||
play( song ) {
|
||||
this.$emit( 'playing', song );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<div class="player">
|
||||
<div class="controls"></div>
|
||||
<div class="song-info"></div>
|
||||
<div class="controls">
|
||||
<div v-if="audioLoaded">
|
||||
<span class="material-symbols-outlined control-icon" v-if="!isPlaying" @click="control( 'play' )">play_arrow</span>
|
||||
<span class="material-symbols-outlined control-icon" v-else @click="control( 'pause' )">pause</span>
|
||||
</div>
|
||||
<span class="material-symbols-outlined control-icon" style="cursor: default;" v-else>play_disabled</span>
|
||||
</div>
|
||||
<div class="song-info">
|
||||
<audio v-if="audioLoaded" :src="'http://localhost:8081/getSongFile?filename=' + playingSong.filename" id="music-player"></audio>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,4 +19,44 @@
|
||||
height: 80%;
|
||||
width: 50%;
|
||||
}
|
||||
</style>
|
||||
|
||||
.control-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
playingSong: {},
|
||||
audioLoaded: false,
|
||||
isPlaying: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play( song ) {
|
||||
this.playingSong = song;
|
||||
this.audioLoaded = true;
|
||||
this.init();
|
||||
},
|
||||
init() {
|
||||
setTimeout( () => {
|
||||
document.getElementById( 'music-player' ).play();
|
||||
this.isPlaying = true;
|
||||
}, 300 );
|
||||
},
|
||||
control( action ) {
|
||||
if ( document.getElementById( 'music-player' ) ) {
|
||||
if ( action === 'play' ) {
|
||||
document.getElementById( 'music-player' ).play();
|
||||
this.isPlaying = true;
|
||||
} else if ( action === 'pause' ) {
|
||||
document.getElementById( 'music-player' ).pause();
|
||||
this.isPlaying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="top-bar">
|
||||
<img src="@/assets/logo.png" alt="logo" class="logo">
|
||||
<div class="player-wrapper">
|
||||
|
||||
<Player ref="player"></Player>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pool-wrapper">
|
||||
<mediaPool></mediaPool>
|
||||
<mediaPool @playing="( song ) => { handlePlaying( song ) }"></mediaPool>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -51,11 +51,13 @@
|
||||
|
||||
<script>
|
||||
import mediaPool from '@/components/mediaPool.vue';
|
||||
import Player from '@/components/player.vue';
|
||||
|
||||
export default {
|
||||
name: 'HomeView',
|
||||
components: {
|
||||
mediaPool,
|
||||
Player,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -64,8 +66,8 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadSongs() {
|
||||
|
||||
handlePlaying ( song ) {
|
||||
this.$refs.player.play( song );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user