Project Report
Project Report
On
Submitted to:
1
DAV COLLEGE, SECTOR 10,
CHANDIGARH
CERTIFICATE
Date:
Internal Guide:
Asstt. Prof.:
Deptt. of Comp. Sc.
2
ACKNOWLEDGEMENT
I express my heart full indebtedness and owe a deep sense of gratitude to my project
guide Mrs. Gunjan for her sincere guidance and inspiration in completing this project.
Without whom this project could not have been possible and taking a variety of hurdles with
implicit patience throughout my project and whose deep involvement and interest in project
infused in me great inspiration and confidence in taking up study in the right direction.
I am highly obliged in taking the opportunity to sincerely thanks to our HOD mam Mrs.
Meenaksi Bhardwaj for providing us this opportunity to develop our own project, and for
continually challenging us to improve and refine and extend our thinking.
I also acknowledge our principle Dr. Pawan Kumar Sharma for giving the opportunity to
our BCA Programs and Project and providing us all the facility that was required.
In the present world of competition, there is a race of existence in which those are having
the will to come forward to succeed. A project is like a bridge between theoretical and
practical working. I would like to thank the supreme power the almighty God who is
obviously the one has guided me to work and the one has always guided me to work on the
right path of life. Next to him are my parents, whom I am greatly in debt for me brought up
with love and encouragement to this stage.
3
INTRODUCTION
This project is based on Electron technology. The electron is an open source library
developed by GitHub for building cross-platform desktop applications with HTML, CSS, and
JavaScript.
The title of the project is “Music Player for Windows, Linux, and MacOS. This
project is based on the concept of a local music file player and further, it has additional
functionalities to fulfill the need of perfect music player. This project helps the user in
managing and playing local music files easily and efficiently. These files can be organized in
different ways that are provided to the user. Its interface gives the user a better experience
and is simple to use.
4
OBJECTIVE
Main objective behind starting this project was to learn the working of electron
framework to create an application that will be supported by all type of PC operating systems
and learn concepts of JavaScript and CSS in detail.
The aim of this project was to create a music player that will be Minimal and user-
friendly. Creating a player capable of handling data of the user in different forms of playlists,
that can be created and customized by the user.
Objective was to support multiple file type to play in our music player, so we chose
web platform, to make it simple because it will support all audio file type that is supported by
HTML. Some supported file types are:-mp3, m4a, ogg, wav, etc.
So the main idea behind developing this project was to create a music player that
fulfils all needs for track organisation. Plus developing a minimal interface that is interactive
and attractive both at the same time.
5
REQUIREMENT
ANALYSIS
6
PROBLEM ANALYSIS
We are working on some common problems that we faced while using a simple music
player that is provided by the operating systems, or the other music player that are available
to play the local music files present in the system of the user.
Our main idea behind creating this project was to make a music player that can play
music according to our needs and have an awesome UI.
So we check some of the points that we want our music player to perform, these
are as follow:-
o 10 recently added tracks added by the user
o Custom themes
o Special like feature to add a song to your favourite
o Tracks are divided according to albums and artists
o Sorting of tracks and be formed according to their Title, Album, Artist,
Duration and liked
o Universal search is added to search your desired song
o Creating custom Playlists
o Playing songs by a single artist or through a single album.
INTRODUCTION TO SRS
A Software Requirements Specification (SRS) is a document that describes the nature of a
project, software or application. In simple words, SRS document is a manual of a project
provided it is prepared before you kick-start a project/application. This document is also
known by the names SRS report, software document. A software document is primarily
prepared for a project, software or any kind of application.
7
GENERAL DESCRIPTION OF
PROJECT
Functions
Project created almost all the basic functions that are essential for general a music
player plus we added some more functions that we wanted our music player to perform.
Following are the functions performed by the player:-
Plays audio files of almost every format
Creates database of the files that are loaded to the player
Creates Playlists according to album and the artists separately
We’ll be able to sort all files according to their Name, Album, Artist, Duration, and
Liked
Liked songs are added to a separate playlist
Search box in songs page
Changes colours dynamically according to the album art of the song
Controlled using global shortcuts
o ctrl + shift + left = Previous Song
o ctrl + shift + right = Next Song
o ctrl + shift + down = Play/Pause
o ctrl + shift + PgUp = Volume Up
o ctrl + shift + PgDown = Volume Down
Support of media keys
Characteristics
Main characteristics of this project:-
Multiple file support
Database handling using a single .db file
Custom themes
Dynamic theme using colours fetched from album art
Custom playlist support
8
Individual album and artist playlist
Liked songs create different playlist
Search is conducted using OR condition of track name, album name, and artist’s
name
Supports media keys
Global shortcuts are used to control play, pause, forward and backward
Saves setting done by user
Constrains
Nothing is perfect in this world, same as with our project. Some major constarints we
noticed while testing our application are:-
No random song support
No repeat song feature available
No track of no. of times song is played
No feature available to mute the player
Assumptions
Some assumption we kept in mind while creating this application:-
Tracks user is going to use will contain all the details and album art
User will use original or close to original tracks
Application coded once and then get compiled on different platforms
System Requirements
o Hardware Requirements
Considering our project, strictly defining hardware requirements is not wise.
As a baseline, we need a System able to run the Latest version of Chrome perfectly, as
9
this software is web-based so the following should be minimum hardware
requirement.
Approximately 200MB of free hard drive.
128MB of RAM
Pentium 4 processor
o Software Requirements
As this software will be packed in a single file. The required software will be
already inside the package that will be created. Only software required will be
Operating system, Minimum OS are as follow:
Windows XP with service pack 2 installed or later.
Linux most distributions from 2010 and onwards. Major examples
include Ubuntu 10.04 and later, Debian 6 or later, OpenSUSE 11.3 or
later, and version 14 or later of Fedora.
MAC must use OS X version 10.5.6 or alter, which dates from early
2009.
Functional Requirements
o Adding files
User will add one or more file which he/she want to play by opening their
computer directory.
To open computer directory user need to click on top left heart and side bar
will appear containing settings access.
Then by clicking add music directory will be open.
o Playing songs
10
The media file will be added and now can be played by clicking on song.
Can further be controlled using play, pause, forward, and back keys available.
o Creating playlist
One way to make playlist of songs u like is to simply click in heart.
To make your personal playlist, right click on the song u want to add
There will be a pop menu which will show old playlist made by you and under
that there will be option of creat new
By clicking on create new a pop up menu will appear to write name of the
playlist.
Feasibility analysis
Economic Feasibility
Electron is free framework provided by nodeJS. Platform needed to run
application is web based, so no need any extra package required to run. Every JQuery
and any other script is open source. Editors used to code are totally free and open
sourced.
Technical Feasibility
Due to web based background there is very less chance of a technical
difficulty. Thus web web based application can be opened in almost any machine.
This feature also technically make this application compatible to all operating systems
available for a pc.
Social Feasibility
Nowadays everyone just loves to listen to music while doing there work and
this application with global key functionality is best for listening songs on
background. And feature of dynamic theme makes is it feel more interactive, because
it becomes just as the environment of the song is meant to be.
11
SOFTWARE
DESIGN
12
SYSTEM DESIGN
Systems design is the process of defining the architecture, modules, interfaces, and
data for a system to satisfy specified requirements. Systems design could be seen as the
application of systems theory to product development. There is some overlap with the
disciplines of systems analysis, systems architecture and systems engineering.
Architectural Design
Architectural Design refers to the high level structures of a software system and the
discipline of creating such structures and systems. Each structure comprises software
elements, relations among them, and properties of both elements and relations. It functions as
a blueprint for the system and the developing project, laying out the tasks necessary to be
executed by the design teams.
Level 0 DFD
This simple level context diagram shows, the start and the end point is the user and
the process held was the music player. And the cycle keeps going till its being used
13
Level 1 DFD
Function
GUI Database
Input Output
Input Output
Level 1
In this level 1 DFD we got the idea of the functional working of the project. So we
can see as the user gives the input through the UI or the DUI part of the player it triggers a
function that has to be performed by the application and then further it request or checks the
function that has to be performed. And the the flow goes to the database and database verifies
the song and the condition given while triggering the function. Atlast it gives the outp in the
form of playing the song.
Level 2
This DFD shows all the main tasks or functions that the application can perform or
execute. It all starts from the GUI, using which the user gives the command to do something.
Which includes adding musinc and changing themes. If the user is giving any input like
playing music or pausing it or even adding it to playlist, it will go to database and with the
help of JavaScript it will execute the particular task. Database and Script have the main role
in the working of this application. When function is required to perform javacrpt starts the
function and fetch the imp file needed from the database and finally execute the function.
14
Level 2 DFD (main functions)
15
Interface Design
User interface design (UI) or user interface engineering is the design of user interfaces
for machines and software, such as computers, home appliances, mobile devices, and other
electronic devices, with the focus on maximizing usability and the user experience. The goal
of user interface design is to make the user's interaction as simple and efficient as possible, in
terms of accomplishing user goals (user-centered design).
Conceptual Design
This was the first conceptual prototype created by us, using Adobe XD. This was
static mockup for getting an idea that how the application would looks like. But while
developing many things were changed.
16
Icon Design
This is the final design that was decided after creating total three icon design. Colors used are
inspired from the original color of the application.
Structural Design
This is structural concept where we seprated sections according to color. Black color conatan
logo and setting button, yellow is title bar, green area is for close, maximize, and minimize
button, blue is the sidebar area, dark grey is the contolling section, red is the dynamic section
which contain all the pages.
17
CODING
18
Approach Used
We know that a system is composed of more than one sub-systems and it contains a
number of components. Further, these sub-systems and components may have their on set of
sub-system and components and creates hierarchical structure in the system.
Top-down design takes the whole software system as one entity and then decomposes it to
achieve more than one sub-system or component based on some characteristics. Each sub-
system or component is then treated as a system and decomposed further. This process keeps
on running until the lowest level of system in the top-down hierarchy is achieved.
Top-down design starts with a generalized model of system and keeps on defining the more
specific part of it. When all components are composed the whole system comes into
existence.
Top-down design is more suitable when the software solution needs to be designed from
scratch and specific details are unknown.
Approach we used is top to down, as we started our project from completely scratch, top to
down was the best idea for the implementation of the complete project.
19
CODE
Package.json
{ "name": "mystt-music-player",
"version": "1.0.0",
"description": "Electron music player",
"main": "main.js",
"scripts": {
"start": "electron .",
"postinstall": "electron-builder install-app-deps" },
"repository": "",
"keywords": [
"Electron" ],
"author": "Trugamr",
"license": "",
"devDependencies": {
"electron": "4.0.3",
"electron-builder": "^20.38.5",
"electron-rebuild": "^1.8.4",
"electron-reload": "^1.4.0" },
"dependencies": {
"electron-window-state": "^5.0.3",
"jsdom": "^14.0.0",
"music-metadata": "^3.5.2",
"node-vibrant": "^3.2.0-alpha",
"recursive-readdir": "^2.2.2",
"sqlite3": "^4.0.6" }}
20
Main.js
21
mainWindow.webContents.send('play-previous-track');
console.log('playing previous track'); })
globalShortcut.register('CommandOrControl+Shift+Down', () => {
mainWindow.webContents.send('play-pause-track');
console.log('playing/pausing track'); })
globalShortcut.register('CommandOrControl+Shift+PageUp', () => {
mainWindow.webContents.send('player-volume-up'); console.log('increasing
volume'); })
globalShortcut.register('CommandOrControl+Shift+PageDown', () => {
mainWindow.webContents.send('player-volume-down'); console.log('decreasing
volume'); })
globalShortcut.register('MediaNextTrack', () => {
mainWindow.webContents.send('play-next-track');
console.log('playing next track'); })
globalShortcut.register('MediaPreviousTrack', () => {
mainWindow.webContents.send('play-previous-track');
console.log('playing previous track'); })
globalShortcut.register('MediaPlayPause', () => {
mainWindow.webContents.send('play-pause-track');
console.log('playing/pausing track'); })
globalShortcut.register('VolumeUp', () => {
mainWindow.webContents.send('player-volume-up'); console.log('increasing
volume');})
globalShortcut.register('VolumeDown', () => {
mainWindow.webContents.send('player-volume-down');console.log('decreasing
volume');})
globalShortcut.register('VolumeMute', () => {
mainWindow.webContents.send('player-volume-mute-unmute');
console.log('muting/unmuting volume'); })})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') { app.quit() }})
app.on('activate', function () {
if (mainWindow === null) {
createWindow() }})
22
Index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>mystt</title>
<link rel="stylesheet" href="./fonts/typicons/typicons.css">
<link rel="stylesheet" href="./css/Font Awesome/all.css">
<link rel="stylesheet" href="./css/main.css">
<link rel="stylesheet" href="./css/home.css">
<link rel="stylesheet" href="./css/songs.css">
<link rel="stylesheet" href="./css/albums.css">
<link rel="stylesheet" href="./css/artists.css">
<link rel="stylesheet" href="./css/liked.css">
<link rel="stylesheet" href="./css/playlists.css">
<link rel="stylesheet" href="./css/animate.css">
<link rel="stylesheet" href="./css/tingle.css">
<link rel="stylesheet" href="./css/contextmenu/jquery.contextMenu.css">
<script>if (typeof module === 'object') {window.module = module; module =
undefined;}</script>
<script src="./js/jquery-3.3.1.min.js"></script>
<script src="./js/list.js"></script>
<script src="./js/pivot.js"></script>
<script src="./js/tingle.js"></script>
<script src="./js/contextmenu/jquery.contextMenu.js"></script>
<script src="./js/contextmenu/jquery.ui.position.js"></script>
<script>if (window.module) module = window.module;</script> </head> <body>
<audio id="audioPlayer" autoplay="">
23
<source id="audioPlayerSrc" src=""> </audio>
<div id="toastContainer">
<i class="fas" id="toastIcon"></i> <p id="toastText"></p> </div>
<div id="container" class="flexChild columnParent">
<div id="settingsPanelOverlay"></div>
<div id="settingsPanel">
<div id="themeSettingsContainer">
<h2 id="themeHeading">Themes</h2>
<div id="themesContainer">
<div id="themeCircle" class="dynamicThemeCircle"> </div> </div> </div>
<h2 id="addMusicSettingsHeading">Add Music</h2>
<div id="selectBtn"><i class="fas fa-plus"></i></div> </div>
<div id="titlebar" class="flexChild">
<div id="titlebar-settings-btn"><i class="fas fa-heart"></i></div>
<div id="titlebar-title">mystt</div>
<div id="titlebar-buttons">
<div id="minimize-btn"><i class="fas fa-circle"></i></div>
<div id="maximize-btn"><i class="fas fa-circle"></i></div>
<div id="close-btn"><i class="fas fa-circle"></i></div> </div> </div>
<div id="wrapper" class="flexChild rowParent">
<div id="sidebar" class="flexChild">
<div id="sidebarWrapper">
<h5 id="sidebarHeading">MUSIC</h5>
<div class="sidebarLinks sbSelected" id="homePage"><span class="typcn typcn-
compass"> </span> Discover</div>
<div class="sidebarLinks" id="songsPage"><span class="typcn typcn-notes">
</span> Songs</div>
<div class="sidebarLinks" id="albumsPage"><span class="typcn typcn-th-
large"></span> Albums</div>
<div class="sidebarLinks" id="artistsPage"><span class="typcn typcn-
user"></span> Artists</div>
<div class="sidebarLinks" id="likedPage"><span class="typcn typcn-heart-full-
outline"></span> Liked</div>
24
<div class="sidebarLinks" id="playlistsPage"><span class="typcn typcn-th-
list"></span> Playlists</div> </div> </div>
<div id="main" class="flexChild"></div> </div>
<div id="playerBar">
<div id="playerBarArt">
<img src="./assets/images/art.png"> </div>
<div id="playerBarControls">
<div id="playerBarLeft">
<div id="playerBarHeart"><span data-liked='0' data-track-id="0"
onclick="likeTrack(event)" class="typcn typcn-heart-outline"></span></div>
<p id="playerBarTitle">mystt track</p>
<p id="playerBarArtist">mystt artist</p> </div>
<div id="playerBarMiddle">
<div id="playerBarMediaButtons">
<i id="playerBackBtn" class="fas fa-step-backward"></i>
<i id="playerPlayBtn" class="fas fa-play"></i>
<i id="playerForwardBtn" class="fas fa-step-forward"></i> </div>
<div id="playerProgressBarWrapper">
<span id="playerBarCurrentTime"></span>
<progress id="playerProgressBar" onclick="progressTo(event)" max=100
value="10"></progress>
<span id="playerBarTotalTime"></span> </div> </div>
<div id="playerBarRight">
<i id="playerVolumeBtn" class="fas fa-volume"></i>
<progress id="playerVolumeBar" onclick="progressTo(event)" max="100"
value="10"></progress></div> </div> </div> </div> <script>
25
Renderer.js
26
let sbLinks = [sbDiscoverLink, sbSongsLink, sbAlbumsLink, sbArtistsLink, sbLikedLink,
sbPlaylistsLink];
let win = remote.getCurrentWindow();
document.querySelector('#minimize-btn').addEventListener('click', () =>
{ win.minimize();})
let isMaximized = false;
document.querySelector('#maximize-btn').addEventListener('click',
()=>{win.isMaximized() ?win.unmaximize() : win.maximize()})
document.querySelector('#close-btn').addEventListener('click', () => win.close();})
const musicExtensions = ['.m4a', '.mp3'];
document.querySelector('#selectBtn').addEventListener('click', () => {
handleSettingsPanel();
addMusicFlow();});
function addMusicFlow() {
showAddDialog().then(directory => {
console.log(`directory chosen ${directory}`)
var addMusicFlowModal = new tingle.modal({
closeMethods: [],
cssClass: ['addMusicFlowModal'],
onOpen: function() {
var addMusicFlowStatus = document.querySelector('#addMusicFlowStatus');
var addMusicFlowIcon = document.querySelector('#addMusicFlowIcon');
var addMusicFlowProgress =
document.querySelector('#addMusicFlowProgress'); } })
addMusicFlowModal.setContent(`
<i id="addMusicFlowIcon" class="fas fa-redo-alt fa-spin"></i>
<p id="addMusicFlowStatus">adding music</p>
<progress id="addMusicFlowProgress" max=100 value="5"></progress> `)
addMusicFlowModal.open();
addMusicFlowStatus.textContent = `scanning directory for music files`;
addMusicFlowProgress.value = 20;
recursiveReadDir(directory).then(musicFiles => {
addMusicFlowStatus.textContent = `fetching metadata for ${musicFiles.length}
files`;
27
addMusicFlowProgress.value = 40;
console.log(`got all music files`, musicFiles)
getAllMetadata(musicFiles).then(metadata => {
addMusicFlowStatus.textContent = `pushing information to database`;
addMusicFlowProgress.value = 60;
console.log('got all metadata', metadata)
pushToDatabase(metadata).then(data => {
addMusicFlowStatus.textContent = `generating all pages`;
addMusicFlowProgress.value = 80;
console.log('pushed to database', metadata)
// Generating all pages now
Promise.all([generateHomePage(), generateSongsPage(), generateAlbumsPage(),
generateArtistsPage(), generateLikedPage(), generatePlaylistsPage()])
.then(data => {
console.log('generated all pages', data);
addMusicFlowProgress.value = 100;
addMusicFlowIcon.removeAttribute("class");
addMusicFlowIcon.classList.add('animated', 'fas', 'fa-heart', 'heartBeat',
'infinite');
addMusicFlowStatus.textContent = `all set, see you on flip side`;
setTimeout(win.reload(), 3000); }) }) }) }) }) }
function showAddDialog() {
return new Promise((resolve, reject) => {
// Selecting music directory
dialog.showOpenDialog({
title: 'Select Music Folder',
properties: [
'openDirectory' ]
}, (directory) => { if(directory) resolve(directory)
else reject('No directory selected') }) });}
function recursiveReadDir(directory) {
return new Promise((resolve, reject) => {
const dirPath = directory[0];
recursiveRead(dirPath)
28
.then(files => {
getMusicFiles(files)
.then(musicFiles => {
if(musicFiles) resolve(musicFiles)
else reject('Failed to fetch music files'); })
.catch(err => {
console.error(err) }) })
.catch(err => {
console.error(err); })
console.log(dirPath); }) }
function getMusicFiles(files) {
return new Promise((resolve, reject) => {
let musicFiles = files.filter(file => musicExtensions.includes(path.extname(file)))
if(musicFiles) resolve(musicFiles)
else reject('failed to get music files') }) }
function getAllMetadata(musicFiles) {
return new Promise((resolve, reject) => {
let allMusicPromises = [];
musicFiles.forEach(file => {
allMusicPromises.push(mm.parseFile(file,{ skipCovers: true })); })
Promise.all(allMusicPromises)
.then(allMetadata => {
allMetadata.map((trackData, index) => {
trackData.path = musicFiles[index];
trackData.birthtimeMs =
fs.statSync(musicFiles[index]).birthtimeMs; });
if(allMetadata) resolve(allMetadata) })
.catch(err => {
reject(err)
console.error(err) }) });}
let db = new sqlite3.Database('mystt.db');
function pushToDatabase(data) {
return new Promise((resolve, reject) => {
data.sort((first, second) => {
29
let firstTitle = first.common.title.toLowerCase();
let secondTitle = second.common.title.toLowerCase();
if (firstTitle < secondTitle) //sort string ascending
return -1
if (firstTitle > secondTitle)
return 1
return 0 //default return value (no sorting) })
let pushedTracksPromises = [];
db.serialize(function() {
db.run("DROP TABLE Music");
db.run("CREATE TABLE IF NOT EXISTS Music (id INTEGER PRIMARY KEY
AUTOINCREMENT, title TEXT, album TEXT, artist TEXT, year INT, duration INT, plays
INT, favourite INT, birthtime INT, path TEXT)");
let stmt = db.prepare("INSERT INTO Music (title, album, artist, year, duration, plays,
favourite, birthtime, path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
db.parallelize(() => {
data.forEach(track => {
pushedTracksPromises.push(
new Promise((resolve, reject) => {
let trackFileName = String.raw`${track.path}`.split('\\');
trackFileName = trackFileName[trackFileName.length -
1].replace(path.extname(track.path), '');
if(!track.common.title) track.common.title = trackFileName;
stmt.run( escapeRegExp(track.common.title),
track.common.album ? escapeRegExp(track.common.album) :
`${escapeRegExp(trackFileName)} - Single`,
track.common.artist ? escapeRegExp(track.common.artist) : `-`,
track.common.year ? track.common.year : `-`,
track.format.duration ? track.format.duration : '0', 0,
0,
track.birthtimeMs,
track.path,(err) => {
if(err) reject(err)
else resolve('successfully inserted') }); }) ) }) })
30
stmt.finalize();
Promise.all(pushedTracksPromises)
.then(data => {
resolve({ complete: data }) })
.catch(err => {
reject(err)
console.error(err) }) }); })}
function fetchMusic(args = "ORDER BY title COLLATE NOCASE ASC") {
let fetchedMusic = [];
return new Promise((resolve, reject) => {
db.each(`SELECT id, title, album, artist, year, duration, plays, favourite, birthtime, path
FROM Music ${args}`, (err, row) => { let track = {
id: row.id, title: row.title,
album: row.album, artist: row.artist,
year: row.year, duration: row.duration,
plays: row.plays, favourite: row.favourite,
birthtime: row.birthtime, path: row.path }
fetchedMusic.push(track); if(err) console.error(err);
}, (err, data) => { if(data) resolve(fetchedMusic)
else if(data == 0) resolve(null) if(err) reject(err) }); })}
function generateHomePage(withTop5 = true) {
const allHomePromises = [];
let homeMusicCards = '';
let homeMusicRecents = `
<li class="infoRow" id="categoryRow"> <p>Title</p>
<p>Album</p> <p>Artist</p>
<p>Duration</p> <p>Liked</p> </li> `;
return new Promise((resolve, reject) => {
fetchMusic('ORDER BY birthtime DESC LIMIT 15') .then(data => {
data.forEach((track, index) => { if(index < 5 && withTop5) {
allHomePromises.push( new Promise((resolve, reject) => {
mm.parseFile(track.path).then(metadata => {
let datajpg = metadata.common.picture ?
blobTob64(metadata.common.picture[0].data) :'./assets/images/art.png';
31
homeMusicCards += ` <div id="homeMusicCard"
onclick="playMusic(${track.id})" data-id=${track.id} data-title="${track.title}" data-
album="${track.album}" data-artist="${track.artist}" data-year=${track.year} data-
path="${track.path}" data-duration="${track.duration}">
<img src="${datajpg}" id="homeMusicCardArt">
<p id="homeMusicCardTitle">${track.title}</p>
<p id="homeMusicCardArtist">${track.artist}</p>
<p style="display: none;"><span id="likeHeart"
onclick="likeTrack(event)" data-track-id="${track.id}" data-liked="${track.favourite}"
class="typcn ${track.favourite ? 'typcn-heart' : 'typcn-heart-outline'}"></span></p>
</div> `
if(metadata) resolve(`got art for ${track.title}`)
else reject(`failed to get art for ${track.title}`) }) }) ) } else {
allHomePromises.push(
new Promise((resolve, reject) => { homeMusicRecents += `
<li class="infoRow" onclick="playMusic(${track.id})" data-
id=${track.id} data-title="${track.title}" data-album="${track.album}" data-
year=${track.year} data-artist="${track.artist}" data-path="${track.path}" data-
duration="${track.duration}">
<p>${track.title}</p> <p>${track.album}</p>
<p>${track.artist}</p> <p>${secondsToMinutes(track.duration)}</p>
<p><span id="likeHeart" onclick="likeTrack(event)" data-track-
id="${track.id}" data-liked="${track.favourite}" class="typcn ${track.favourite ? 'typcn-
heart' : 'typcn-heart-outline'}"></span></p> </li> `
if(track) resolve('recent added', track.title) else reject('problem with track',
track) }) ) } }) Promise.all(allHomePromises)
.then(data => { fs.readFile('./pages/home.htm', 'utf-8', (err, data) => {
if(err) console.err(err); const jsdomWindow = new JSDOM(data).window;
if(withTop5)
jsdomWindow.document.querySelector('#homeMusicRow').innerHTML = homeMusicCards;
jsdomWindow.document.querySelector('#homeMusicRecent>ul').innerHTML =
homeMusicRecents;
const generatedContent =
jsdomWindow.document.documentElement.outerHTML;
32
fs.writeFile('./pages/home.htm', generatedContent, (err) => {
if(err) reject(err)
else resolve(`home page generated ${withTop5 ? 'with top 5' : 'without top
5'}`)})})})})})}function generateSongsPage() {
let allSongs = ''; return new Promise((resolve, reject) => {
fetchMusic() .then(data => {
data.forEach(track => { allSongs += `
<li class="infoRowSongs" onclick="playMusic(${track.id})" data-
id=${track.id} data-title="${track.title}" data-album="${track.album}" data-
artist="${track.artist}" data-year=${track.year} data-path="${track.path}" data-
duration="${track.duration}">
<p class="songName">${track.title}</p>
<p class="songAlbum">${track.album}</p>
<p class="songArtist">${track.artist}</p>
<p class="songTime">${secondsToMinutes(track.duration)}</p>
<p class="songYear">${track.year ? track.year : '-'}</p>
<p class="songLiked"><span style="display: none;">${track.favourite ?
'Heart' : 'Nope'}</span> <span id="likeHeart" onclick="likeTrack(event)" data-track-
id="${track.id}" data-liked="${track.favourite}" class="typcn ${track.favourite ? 'typcn-
heart' : 'typcn-heart-outline'}"></span></p> </li> ` })
fs.readFile('./pages/songs.htm', 'utf-8', (err, data) => {
if(err) console.error(err);
const jsdomWindow = new JSDOM(data).window;
jsdomWindow.document.querySelector('#songsContainer>ul').innerHTML =
allSongs;
const generatedContent =
jsdomWindow.document.documentElement.outerHTML;
fs.writeFile('./pages/songs.htm', generatedContent, (err) => {
if(err) reject(err)
else resolve("songs page generated") })})})})}
function generateAlbumsPage() {
let allAlbums = '';
return new Promise((resolve, reject) => {
let allAlbumPromises = [];
33
// fetching all albums
db.all(`SELECT album, artist, path, COUNT(title) as tracks FROM Music GROUP BY
album`, (err, albumData) => { if(err) console.error(err);
albumData .filter(album => album.tracks > 1)
.forEach(album => { allAlbumPromises.push(
new Promise((res, rej) => { mm.parseFile(album.path)
.then(metadata => {
let datajpg = metadata.common.picture[0].data ?
blobTob64(metadata.common.picture[0].data) : './assets/images/art.png'; allAlbums += `
<div id="albumCard"
onclick="showAlbumTracks('${album.album}')" data-album="${album.album}" data-
artist="${album.artist}" data-tracks="${album.tracks}"> <div id="albumCardArt">
<img src="${datajpg}"> </div>
<p id="albumCardTitle">${album.album}</p>
<p id="albumCardArtist">${album.artist}</p> </div> `
if(metadata) res(`got metadata for ${album.album} by ${album.artist}`)
else rej(`failed to get metadata for ${album.album}} by ${album.artist}`);})}))});
Promise.all(allAlbumPromises) .then(data => {
fs.readFile('./pages/albums.htm', 'utf-8', (err, data) => { if(err) console.error(err);
const jsdomWindow = new JSDOM(data).window;
jsdomWindow.document.querySelector('#albumsContainer').innerHTML = allAlbums;
const generatedContent = jsdomWindow.document.documentElement.outerHTML;
fs.writeFile('./pages/albums.htm', generatedContent, (err) => { if(err) reject(err)
else resolve(`album page generated`)})})}) .catch(err => reject(err)) }) })}
function generateArtistsPage() { let allArtists = '';
return new Promise((resolve, reject) => { let allArtistsPromises = [];
db.all(`SELECT artist, path, COUNT(title) as tracks FROM Music GROUP BY artist`,
(err, artistData) =>{ if(err) console.error(err); artistData
.filter(artist => artist.tracks > 1 && artist.artist != '-')
.forEach(artist => { allArtistsPromises.push(
new Promise((res, rej) => { mm.parseFile(artist.path)
.then(metadata => {let datajpg = metadata.common.picture ?
blobTob64(metadata.common.picture[0].data) : './assets/images/art.png';
allArtists += `<div id="artistCard" onclick="showArtistTracks('${artist.artist}')" data-
34
artist="${artist.artist}" data-tracks="${artist.tracks}"> <div id="artistCardArt"> <img
src="${datajpg}"> </div><p id="artistCardTitle">${artist.artist}</p><p
id="artistCardTracks">${artist.tracks} tracks</p></div>` if(metadata) res(`got metadata for
${artist.artist} with ${artist.tracks} tracks`)
else rej(`failed to get metadata for ${artist.artist}} with ${artist.artist}
tracks`);})}))});
Promise.all(allArtistsPromises)
.then(data => { console.log(data);
fs.readFile('./pages/artists.htm', 'utf-8', (err, data) => {
if(err) console.error(err);
const jsdomWindow = new JSDOM(data).window;
jsdomWindow.document.querySelector('#artistsContainer').innerHTML =
allArtists;
const generatedContent =
jsdomWindow.document.documentElement.outerHTML;
fs.writeFile('./pages/artists.htm', generatedContent, (err) => {
if(err) reject(err)
else resolve(`artists page generated`) })})})
.catch(err => reject(err))})})}
function generateLikedPage() { let allLiked = '';
return new Promise((resolve, reject) => {
fetchMusic('WHERE FAVOURITE = 1 ORDER BY title COLLATE NOCASE
ASC') .then(data => {
if(data) data.forEach(track => { allLiked += `
<li class="infoRowLiked" onclick="playMusic(${track.id})" data-id=${track.id}
data-title="${track.title}" data-album="${track.album}" data-artist="${track.artist}" data-
year=${track.year} data-path="${track.path}" data-duration="${track.duration}">
<p class="likedName">${track.title}</p>
<p class="likedAlbum">${track.album}</p>
<p class="likedArtist">${track.artist}</p>
<p class="likedTime">${secondsToMinutes(track.duration)}</p>
<p class="likedYear">${track.year ? track.year : '-'}</p>
<p class="likedLiked"><span style="display: none;">${track.favourite ?
'Heart' : 'Nope'}</span> <span id="likeHeart" onclick="likeTrack(event)" data-track-
35
id="${track.id}" data-fromliked='true' data-liked="${track.favourite}" class="typcn
${track.favourite ? 'typcn-heart' : 'typcn-heart-outline'}"></span></p> </li>
` })
fs.readFile('./pages/liked.htm', 'utf-8', (err, data) => {
if(err) console.error(err);
const jsdomWindow = new JSDOM(data).window;
jsdomWindow.document.querySelector('#likedContainer>ul').innerHTML =
allLiked;
const generatedContent =
jsdomWindow.document.documentElement.outerHTML;
fs.writeFile('./pages/liked.htm', generatedContent, (err) => {
if(err) reject(err)
else resolve("liked page generated") })})}).catch(err => console.error(err));})}
function generatePlaylistsPage() { let allPlaylists = '';
return new Promise((resolve, reject) => { let allPlaylistsPromises = [];
db.all(`SELECT name FROM sqlite_master WHERE type='table'`, (err, tables) => {
if(err) console.error(err);
.filter(table => table.name.endsWith('_playlist')) .forEach(playlist => {
allPlaylistsPromises.push( new Promise((res, rej) => {
db.all(`SELECT COUNT(*) as tracks, path FROM ${playlist.name}`, (err,
row) => {
if(err) console.error(err);
if(row[0].tracks)
{ mm.parseFile(row[0].path) .then(metadata => {
let datajpg = metadata.common.picture ?
blobTob64(metadata.common.picture[0].data) : './assets/images/art.png';
allPlaylists += ` <div id="playlistCard"
onclick="showPlaylistTracks('${playlist.name}')" data-playlist="${playlist.name}" data-
tracks="${row[0].tracks}"> <div id="playlistCardArt"> <img src="${datajpg}"> </div>
<div id="playlistCardInfo"> <p
id="playlistCardTitle">${capitalizeFirstLetter(playlist.name.replace('_playlist', '').replace('_',
' '))}</p>
<p id="playlistCardTracks">${row[0].tracks}
tracks</p></div></div> `
36
if(metadata) res(`got metadata for ${playlist.name} with
${row[0].tracks} tracks`)
else rej(`failed to get metadata for ${playlist.name}} with
${row[0].tracks} tracks`);})
} else { res(`${ playlist.name } has 0 tracks`); }})}) ) })
Promise.all(allPlaylistsPromises) .then(data => {
console.log(data); fs.readFile('./pages/playlists.htm', 'utf-8', (err, data)
=> {
if(err) console.error(err); const jsdomWindow = new
JSDOM(data).window;
jsdomWindow.document.querySelector('#playlistsContainer').innerHTML =
allPlaylists;
const generatedContent =
jsdomWindow.document.documentElement.outerHTML;
fs.writeFile('./pages/playlists.htm', generatedContent, (err)
=>{ if(err) reject(err)
else resolve(`playlists page generated`)})})}).catch(err => reject(err))})})}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);}
function showAlbumTracks(albumName) {
let album = albumName; let albumSongsList = '';
fetchMusic(`WHERE album = '${album}' ORDER BY title COLLATE NOCASE ASC`)
.then(data => { data.forEach(track => { albumSongsList += `
<li class="infoRowAlbums" onclick="playMusic(${track.id})" data-id=${track.id}
data-title="${track.title}" data-album="${track.album}" data-artist="${track.artist}" data-
year=${track.year} data-path="${track.path}" data-duration="${track.duration}">
<p class="albumName">${track.title}</p>
<p class="albumArtist">${track.artist}</p>
<p class="albumTime">${secondsToMinutes(track.duration)}</p>
<p class="albumLiked"><span style="display: none;">${track.favourite ?
'Heart' : 'Nope'}</span> <span id="likeHeart" onclick="likeTrack(event)" data-track-
id="${track.id}" data-liked="${track.favourite}" class="typcn ${track.favourite ? 'typcn-
heart' : 'typcn-heart-outline'}"></span></p></li> ` })
var modal = new tingle.modal({
37
closeMethods: ['overlay', 'escape'] })
modal.setContent(`
<div id="albumTracksContainer">
<span id="playAlbumBtn" onclick="playMusic('${albumName}', { playBy:
'albumName', fromQueue: true })"><i class="fas fa-play"></i> Play</span>
<h1 id="albumNameHeading"><span>${albumName}</span></h1>
<li class="infoRowAlbums" id="categoryRowAlbums">
<p class="albumSort albumName" data-sort="albumName">Title <span
class="typcn"></span></p> <p class="albumSort albumArtist" data-
sort="albumArtist">Artist <span class="typcn"></span></p> <p
class="albumSort albumTime" data-sort="albumTime">Duration <span
class="typcn"></span></p> <p class="albumSort albumLiked" data-
sort="albumLiked">Liked <span class="typcn"></span></p> </li> <ul class="list">
${albumSongsList} </ul> </div>`) modal.open();
var albumTracksSortCategories =
Array.from(document.querySelectorAll('.albumSort'));
albumTracksSortCategories.forEach(category => {
category.addEventListener('click', handleSortingIcons); })
function handleSortingIcons(e) {
var iconSpan = e.target.childNodes[1];
albumTracksSortCategories.forEach(category => { if(category !=
e.target) {
category.childNodes[1].classList.remove('typcn-arrow-sorted-down', 'typcn-
arrow-sorted-up');}})
if(iconSpan.classList.contains('typcn-arrow-sorted-down')) {
iconSpan.classList.remove('typcn-arrow-sorted-down');
iconSpan.classList.add('typcn-arrow-sorted-up');
} else if(iconSpan.classList.contains('typcn-arrow-sorted-up')) {
iconSpan.classList.remove('typcn-arrow-sorted-up');
iconSpan.classList.add('typcn-arrow-sorted-down'); } else {
iconSpan.classList.add('typcn-arrow-sorted-down');} }
var options = {
valueNames: [ 'albumName', 'albumArtist', 'albumTime', 'albumLiked' ],
sortClass : 'albumSort' }
38
var trackList = new List('albumTracksContainer', options); })}
function showArtistTracks(artistName) {
let artist = artistName;
let artistSongsList = '';
fetchMusic(`WHERE artist = '${artist}' ORDER BY title COLLATE NOCASE ASC`)
.then(data => {
data.forEach(track => {
artistSongsList += `
<li class="infoRowArtists" onclick="playMusic(${track.id})" data-id=${track.id}
data-title="${track.title}" data-album="${track.album}" data-artist="${track.artist}" data-
year=${track.year} data-path="${track.path}" data-duration="${track.duration}">
<p class="artistName">${track.title}</p>
<p class="artistAlbum">${track.album}</p>
<p class="artistTime">${secondsToMinutes(track.duration)}</p>
<p class="artistLiked"><span style="display: none;">${track.favourite ? 'Heart' :
'Nope'}</span> <span id="likeHeart" onclick="likeTrack(event)" data-track-id="${track.id}"
data-liked="${track.favourite}" class="typcn ${track.favourite ? 'typcn-heart' : 'typcn-heart-
outline'}"></span></p></li> ` })
var modal = new tingle.modal({
closeMethods: ['overlay', 'escape'] }) modal.setContent(` <div
id="artistTracksContainer">
<span id="playArtistBtn" onclick="playMusic('${artistName}', { playBy:
'artistName', fromQueue: true })"><i class="fas fa-play"></i> Play</span>
<h1 id="artistNameHeading"><span>${artistName}</span></h1>
<li class="infoRowArtists" id="categoryRowArtists">
<p class="artistSort artistName" data-sort="artistName">Title <span
class="typcn"></span></p>
<p class="artistSort artistAlbum" data-sort="artistAlbum">Album <span
class="typcn"></span></p>
<p class="artistSort artistTime" data-sort="artistTime">Duration <span
class="typcn"></span></p>
<p class="artistSort artistLiked" data-sort="artistLiked">Liked <span
class="typcn"></span></p></li> <ul class="list"> ${artistSongsList} </ul>
</div> `) modal.open();
39
var artistTracksSortCategories =
Array.from(document.querySelectorAll('.artistSort'));
artistTracksSortCategories.forEach(category => {
category.addEventListener('click', handleSortingIcons); })
function handleSortingIcons(e) {
var iconSpan = e.target.childNodes[1];
artistTracksSortCategories.forEach(category => {
if(category != e.target) {
category.childNodes[1].classList.remove('typcn-arrow-sorted-down', 'typcn-
arrow-sorted-up');}})
if(iconSpan.classList.contains('typcn-arrow-sorted-down')) {
iconSpan.classList.remove('typcn-arrow-sorted-down');
iconSpan.classList.add('typcn-arrow-sorted-up');
} else if(iconSpan.classList.contains('typcn-arrow-sorted-up')) {
iconSpan.classList.remove('typcn-arrow-sorted-up');
iconSpan.classList.add('typcn-arrow-sorted-down');
} else {iconSpan.classList.add('typcn-arrow-sorted-down'); } }
var options = { valueNames: [‘artistName','artistAlbum', 'artistTime','artistLiked' ],
sortClass : 'artistSort' }
var trackList = new List('artistTracksContainer', options); })}
function showPlaylistTracks(playlist) {
let playlistSongsList = '';
db.all(`SELECT * FROM ${playlist}`, (err, rows) => {
if(err) console.error(err);
rows.forEach(row => {
let track = { id: row.id, title: row.title, album: row.album,
artist: row.artist, year: row.year, duration: row.duration,
favourite: row.favourite, path: row.path }
playlistSongsList += `
<li class="infoRowPlaylists" onclick="playMusic(${track.id})" data-id=${track.id}
data-title="${track.title}" data-album="${track.album}" data-artist="${track.artist}" data-
year=${track.year} data-path="${track.path}" data-duration="${track.duration}">
<p class="playlistName">${track.title}</p>
<p class="playlistArtist">${track.artist}</p>
40
<p class="playlistTime">${secondsToMinutes(track.duration)}</p>
<p class="playlistDelete"><span onclick="deleteFromPlaylist(event, ${track.id},
'${playlist}')" class="typcn typcn-times"></span></p> </li> ` })
var modal = new tingle.modal({
closeMethods: ['overlay', 'escape'] })
modal.setContent(` <div id="playlistTracksContainer">
<span id="playPlaylistBtn" onclick="playMusic('${playlist}', { playBy:
'playlistName', fromQueue: true })"><i class="fas fa-play"></i> Play</span>
<h1
id="playlistNameHeading"><span>${capitalizeFirstLetter(playlist.replace('_playlist',
'').replace('_', ' '))}</span></h1> <li class="infoRowPlaylists"
id="categoryRowPlaylists"> <p class="playlistSort playlistName" data-
sort="playlistName">Title <span class="typcn"></span></p>
<p class="playlistSort playlistArtist" data-sort="playlistArtist">Artist <span
class="typcn"></span></p>
<p class="playlistSort playlistTime" data-sort="playlistTime">Duration <span
class="typcn"></span></p> <p class="playlistDelete">Delete</p> </li><ul class="list">
${playlistSongsList}
</ul>
<p id="removePlaylistText" onclick="deletePlaylist(event, '${playlist}')">remove this
playlist</p></div> `) modal.open();
var playlistTracksSortCategories =
Array.from(document.querySelectorAll('.playlistSort'));
playlistTracksSortCategories.forEach(category => {
category.addEventListener('click', handleSortingIcons); })
function handleSortingIcons(e) { var iconSpan = e.target.childNodes[1];
playlistTracksSortCategories.forEach(category => { if(category != e.target)
{
category.childNodes[1].classList.remove('typcn-arrow-sorted-down', 'typcn-
arrow-sorted-up');} })
if(iconSpan.classList.contains('typcn-arrow-sorted-down')) {
iconSpan.classList.remove('typcn-arrow-sorted-down');
iconSpan.classList.add('typcn-arrow-sorted-up');
} else if(iconSpan.classList.contains('typcn-arrow-sorted-up')) {
41
iconSpan.classList.remove('typcn-arrow-sorted-up');
iconSpan.classList.add('typcn-arrow-sorted-down'); } else {
iconSpan.classList.add('typcn-arrow-sorted-down'); }}
var options = {valueNames: [ 'playlistName', 'playlistArtist','playlistTime' ],
sortClass : 'playlistSort' }
var playlistList = new List('playlistTracksContainer', options);})}
function blobTob64(blob) {
return("data:image/jpg;base64," + btoa(new Uint8Array(blob).reduce((data, byte) => data
+ String.fromCharCode(byte), '')));}
function secondsToMinutes(duration) {
let minutes = Math.floor(duration/60);
let seconds = Math.floor(duration) - minutes*60;
if(seconds.toString().length == 1) seconds = `0${seconds}`;
return(`${minutes}:${seconds}`);}
let regeneratePage = {
homePage: false,
likedPage: false, playlistsPage: false};
function loadHomePage (e) { if(e) highlightSbLink(e);
$('#main').fadeOut('fast',function(){
$('#main').load('./pages/home.htm',function(data){
$('#main').fadeIn('fast'); }); })}
function loadArtistsPage (e) { if(e) highlightSbLink(e);
$('#main').fadeOut('fast',function(){
$('#main').load('./pages/artists.htm',function(data){
$('#main').fadeIn('fast'); }); })}
function loadSongsPage (e) { if(e) highlightSbLink(e);
if(regeneratePage.songsPage) { regeneratePage.songsPage = false;
generateSongsPage() .then(data => {
console.log(data); $('#main').fadeOut('fast',function(){
$('#main').load('./pages/songs.htm',function(data){
$('#main').fadeIn('fast'); }); }) }) } else
{ $('#main').fadeOut('fast',function(){
$('#main').load('./pages/songs.htm',function(data){
$('#main').fadeIn('fast'); }); }) }}function loadAlbumsPage (e) {
42
if(e) highlightSbLink(e); $('#main').fadeOut('fast',function(){
$('#main').load('./pages/albums.htm',function(data){
$('#main').fadeIn('fast'); }); });}
function loadArtistsPage (e) { if(e) highlightSbLink(e);
$('#main').fadeOut('fast',function(){
$('#main').load('./pages/artists.htm',function(data){
$('#main').fadeIn('fast'); }); })}
function loadLikedPage (e) { if(e) highlightSbLink(e);
if(regeneratePage.likedPage) { regeneratePage.likedPage = false;
generateLikedPage() .then(data => { console.log(data);
$('#main').fadeOut('fast',function(){
$('#main').load('./pages/liked.htm',function(data){
$('#main').fadeIn('fast'); }); }) }) } else
{ $('#main').fadeOut('fast',function(){
$('#main').load('./pages/liked.htm',function(data){
$('#main').fadeIn('fast'); }); }) }}
function loadPlaylistsPage (e) { if(e) highlightSbLink(e);
// Loading artists page in #main content at start
if(regeneratePage.playlistsPage) { regeneratePage.playlistsPage = false;
generatePlaylistsPage() .then(data => { console.log(data);
$('#main').fadeOut('fast',function(){ $('#main').load('./pages/playlists.htm',functio
n(data){
$('#main').fadeIn('fast'); }); }) }) } else
{ $('#main').fadeOut('fast',function(){
$('#main').load('./pages/playlists.htm',function(data){
$('#main').fadeIn('fast'); }); }) }}
sbDiscoverLink.addEventListener('click', loadHomePage)
sbSongsLink.addEventListener('click', loadSongsPage)
sbAlbumsLink.addEventListener('click', loadAlbumsPage)
sbArtistsLink.addEventListener('click', loadArtistsPage)
sbLikedLink.addEventListener('click', loadLikedPage)
sbPlaylistsLink.addEventListener('click', loadPlaylistsPage)loadHomePage();
function highlightSbLink(event) {
sbLinks.forEach(link => link.classList.remove('sbSelected'))
43
event.target.classList.add('sbSelected');}function likeTrack(e) {
e.stopPropagation(); let icon = e.target;
let iconParent = icon.parentElement; let isLiked = parseInt(e.target.dataset.liked);
let trackId = e.target.dataset.trackId; if(isLiked)
{ iconParent.classList.remove('animated', 'heartBeat');
iconParent.classList.add('animated', 'jello');
db.run(`UPDATE Music SET favourite = 0 WHERE id = ${trackId}`, (err) => {
if(err) console.error(err); if(icon.dataset.fromliked) {
iconParent.parentElement.addEventListener('animationend', () =>
{iconParent.parentElement.style = "display: none;"} )
iconParent.parentElement.classList.add('animated', 'fadeOut')
regenerateLikePage = true; }
regeneratePage.likedPage = true; regeneratePage.songsPage = true;
console.log(`${trackId} set like to 0`); e.target.dataset.liked = 0; });
icon.classList.remove('typcn-heart'); icon.classList.add('typcn-heart-outline'); }
else {
iconParent.classList.remove('animated', 'jello'); iconParent.classList.add('animated',
'heartBeat');
db.run(`UPDATE Music SET favourite = 1 WHERE id = ${trackId}`, (err) => { if(err)
console.error(err);
regeneratePage.likedPage = true; regeneratePage.songsPage = true;
console.log(`${trackId} set to like 1`); e.target.dataset.liked = 1;
isLiked = isLiked ? 0 : 1; });
icon.classList.remove('typcn-heart-outline'); icon.classList.add('typcn-heart'); }}
const playerBar = document.querySelector('#playerBar')
const backButton = document.querySelector('#playerBackBtn');
const playButton = document.querySelector('#playerPlayBtn');
const forwardButton = document.querySelector('#playerForwardBtn');
const playerBarHeart = document.querySelector('#playerBarHeart>span')
const playerBarTitle = document.querySelector('#playerBarTitle')
const playerBarArtist = document.querySelector('#playerBarArtist')
const playerBarArt = document.querySelector('#playerBarArt>img')
const playerBarTotalTime = document.querySelector('#playerBarTotalTime')
const playerBarCurrentTime = document.querySelector('#playerBarCurrentTime')
44
const playerBarVolumeBtn = document.querySelector('#playerVolumeBtn')
playerBarVolumeBtn.addEventListener('click', muteUnmuteAudio)
function muteUnmuteAudio() {
if(audioPlayer.muted || audioPlayer.volume == 0) { audioPlayer.muted = false;
playerBarVolumeBtn.classList.remove('fa-volume-mute')
playerBarVolumeBtn.classList.add('fa-volume') } else { audioPlayer.muted =
true;
playerBarVolumeBtn.classList.remove('fa-volume')
playerBarVolumeBtn.classList.add('fa-volume-mute') }}
const audioPlayer = document.querySelector('#audioPlayer');
const audioPlayerSrc = document.querySelector('#audioPlayerSrc');audioPlayer.volume =
0.35;
let totalTracks;db.each('SELECT COUNT(*) AS count from MUSIC', (err, data) => {
totalTracks = data.count;})let currentlyPlayingTrack = 0;
let currentQueue = [];let currentQueueTrackIndex = 0;
let playingQueue = false;function playTrack(trackId) {
if(trackId > totalTracks) { trackId = 1; } else if(trackId < 1) { trackId =
totalTracks; }
fetchMusic(`WHERE id = ${trackId}`)
.then(foundTrack => {
let track = foundTrack[0];
console.log(`playing ${track.title}`);
currentlyPlayingTrack = track.id;
playerBarTitle.textContent = track.title;
playerBarArtist.textContent = track.artist;
playerBarTotalTime.textContent = secondsToMinutes(track.duration);
playButton.classList.remove('fa-play');
playButton.classList.add('fa-pause');
audioPlayerSrc.src = track.path; audioPlayer.load(); audioPlayer.play();
mm.parseFile(track.path) .then(metadata => {
let datajpg = metadata.common.picture[0].data ?
blobTob64(metadata.common.picture[0].data) : './assets/images/art.png';
playerBarArt.src = datajpg; })
if(track.favourite) {
45
playerBarHeart.dataset.trackId = track.id;
playerBarHeart.dataset.liked = track.favourite;
playerBarHeart.classList.remove('typcn-heart-outline');
playerBarHeart.classList.add('typcn-heart'); } else {
playerBarHeart.dataset.trackId = track.id;
playerBarHeart.dataset.liked = track.favourite;
playerBarHeart.classList.remove('typcn-heart');
playerBarHeart.classList.add('typcn-heart-outline'); } })}
function playMusic(data, options = { playBy: 'trackId', fromQueue: false}) {
if(options.playBy == 'trackId') { let trackId = data;
if(options.fromQueue) playingQueue = true;
else playingQueue = false;
fetchMusic(`WHERE id = ${trackId}`) .then(tracks => {
audioPlayerSrc.src = tracks[0].path; audioPlayer.load();
audioPlayer.play(); currentlyPlayingTrack = trackId;
console.log('playing id', trackId); playButton.classList.remove('fa-play');
playButton.classList.add('fa-pause');
updateCurrentlyPlayingInfo(tracks[0]); }) } else if(options.playBy == 'albumName')
{ playingQueue = true;
fetchMusic(`WHERE album = '${data}' ORDER BY title COLLATE NOCASE ASC`)
.then(data => { currentQueue = data; currentQueueTrackIndex = 0;
playMusic(currentQueue[currentQueueTrackIndex].id, { playBy: 'trackId',
fromQueue: true })
console.log(currentQueue); }); } else if(options.playBy == 'artistName') {
playingQueue = true;
fetchMusic(`WHERE artist = '${data}' ORDER BY title COLLATE NOCASE ASC`)
.then(data => { currentQueue = data; currentQueueTrackIndex = 0;
playMusic(currentQueue[currentQueueTrackIndex].id, { playBy: 'trackId',
fromQueue: true })
console.log(currentQueue); }); } else if(options.playBy == 'playlistName')
{
db.all(`SELECT * FROM ${data}`, (err, data) => { if(err) console.error(err);
currentQueue = data; currentQueueTrackIndex = 0;
46
playMusic(currentQueue[currentQueueTrackIndex].id, { playBy: 'trackId',
fromQueue: true })
console.log(currentQueue); }) }}
function updateCurrentlyPlayingInfo(trackInfo) { playerBarTitle.textContent =
trackInfo.title;
playerBarArtist.textContent = trackInfo.artist; document.title = `${trackInfo.title} |
${trackInfo.artist}`;
playerBarCurrentTime.textContent = '0:00';
playerBarTotalTime.textContent = secondsToMinutes(trackInfo.duration);
if(trackInfo.favourite) {
playerBarHeart.dataset.trackId = trackInfo.id;
playerBarHeart.dataset.liked = trackInfo.favourite;
playerBarHeart.classList.remove('typcn-heart-outline');
playerBarHeart.classList.add('typcn-heart');
} else { playerBarHeart.dataset.trackId = trackInfo.id;
playerBarHeart.dataset.liked = trackInfo.favourite;
playerBarHeart.classList.remove('typcn-heart');
playerBarHeart.classList.add('typcn-heart-outline'); }
mm.parseFile(trackInfo.path) .then(metadata => {
if(metadata.common.picture) { let buffer = metadata.common.picture[0].data;
let datajpg = blobTob64(buffer); playerBarArt.src = datajpg;
} else { playerBarArt.src = datajpg = `./assets/images/art.png`; }
if(isDynamicThemeSelected) dynamicColorFetch(metadata.common.picture ?
metadata.common.picture[0].data : `./assets/images/art.png`); })}
function nextTrack() { if(playingQueue) { ++currentQueueTrackIndex;
if(currentQueueTrackIndex > currentQueue.length-1) { currentQueueTrackIndex
= 0;
playMusic(currentQueue[currentQueueTrackIndex].id, { playBy: 'trackId',
fromQueue: true }); } else {
playMusic(currentQueue[currentQueueTrackIndex].id, { playBy: 'trackId',
fromQueue: true });
} } else { ++currentlyPlayingTrack; if(currentlyPlayingTrack >=
totalTracks){currentlyPlayingTrack =1;
47
playMusic(currentlyPlayingTrack, { playBy: 'trackId', fromQueue: false }) } else
{
playMusic(currentlyPlayingTrack, { playBy: 'trackId', fromQueue: false }) } }}
function previousTrack() {
if(playingQueue) { --currentQueueTrackIndex; if(currentQueueTrackIndex < 0) {
console.log('first', currentQueueTrackIndex); currentQueueTrackIndex =
currentQueue.length-1;
playMusic(currentQueue[currentQueueTrackIndex].id, { playBy: 'trackId',
fromQueue: true }); } else {
console.log('second', currentQueueTrackIndex);
playMusic(currentQueue[currentQueueTrackIndex].id, { playBy: 'trackId',
fromQueue: true }); } } else { --currentlyPlayingTrack;
if(currentlyPlayingTrack <= 0) { currentlyPlayingTrack = totalTracks;
playMusic(currentlyPlayingTrack, { playBy: 'trackId', fromQueue: false }) } else
{ playMusic(currentlyPlayingTrack, { playBy: 'trackId', fromQueue:
false }) } }}
backButton.addEventListener('click', previousTrack)
forwardButton.addEventListener('click', nextTrack)
playButton.addEventListener('click', playOrPauseTrack)
function playOrPauseTrack() { if(audioPlayer.paused) {
audioPlayer.play(); playButton.classList.remove('fa-play');
playButton.classList.add('fa-pause');
} else { audioPlayer.pause(); playButton.classList.remove('fa-pause');
playButton.classList.add('fa-play'); }}function volumeUp() {
if(audioPlayer.volume < 1) {
audioPlayer.volume += 0.05;
volumeBar.value = audioPlayer.volume * 100; };}function volumeDown()
{ if(audioPlayer.volume > 0) { audioPlayer.volume -= 0.05; volumeBar.value =
audioPlayer.volume * 100; };}
ipcRenderer.on('play-pause-track', playOrPauseTrack)
ipcRenderer.on('play-next-track', nextTrack)
ipcRenderer.on('play-previous-track', previousTrack)
ipcRenderer.on('player-volume-up', volumeUp)
ipcRenderer.on('player-volume-down', volumeDown)
48
ipcRenderer.on('player-volume-mute-unmute', muteUnmuteAudio)
const progressBar = document.querySelector('#playerProgressBar');
const volumeBar = document.querySelector('#playerVolumeBar');
function progressTo(e) { e.target.value = e.offsetX/e.target.clientWidth * 100; }
progressBar.addEventListener('click', (e) => {
audioPlayer.currentTime = (e.offsetX/progressBar.clientWidth * audioPlayer.duration);
})volumeBar.addEventListener('click', () => { audioPlayer.volume =
volumeBar.value/100; })
audioPlayer.addEventListener('timeupdate', () => {
if(audioPlayer.currentTime) { progressBar.value =
audioPlayer.currentTime/audioPlayer.duration * 100; playerBarCurrentTime.textContent
= secondsToMinutes(audioPlayer.currentTime); }
if(audioPlayer.duration == audioPlayer.currentTime) {
nextTrack() }}) const settingsBtn = document.querySelector('#titlebar-settings-btn')
const settingsPanel = document.querySelector('#settingsPanel')
const settingsPanelOverlay = document.querySelector('#settingsPanelOverlay')
function handleSettingsPanel() { if(settingsPanel.classList.contains('showSettingsPanel')) {
settingsPanel.classList.remove('showSettingsPanel');
settingsPanelOverlay.classList.remove('showSettingsOverlay');
} else { settingsPanel.classList.add('showSettingsPanel');
settingsPanelOverlay.classList.add('showSettingsOverlay'); } }
settingsBtn.addEventListener('click', handleSettingsPanel)
settingsPanelOverlay.addEventListener('click', handleSettingsPanel)
var themes = require('./js/themes.js');
var lightenColor = function(color, percent) { var num = parseInt(color,16),
amt = Math.round(2.55 * percent), R = (num >> 16) + amt, B = (num >> 8 &
0x00FF) + amt,
G = (num & 0x0000FF) + amt; return (0x1000000 + (R<255?R<1?0:R:255)*0x10000
+ (B<255?B<1?0:B:255)*0x100 + (G<255?G<1?0:G:255)).toString(16).slice(1);};
function applyTheme(value, options = { by: 'name' }) {
if(options.by == 'name') { theme = themes.allThemes[`${value}`] }
else if(options.by == 'themeObject') {
theme = value };
currentlySelectedTheme = theme.name;
49
if(theme.name == 'dynamic') { isDynamicThemeSelected = true; } else
{ isDynamicThemeSelected = false; } document.documentElement.style.setProperty('--
primary-color', theme.first); document.documentElement.style.setProperty('--primary-
color-light', theme.second);
document.documentElement.style.setProperty('--primary-color-white', theme.third);
document.documentElement.style.setProperty('--primary-color-gray', theme.fourth);
document.documentElement.style.setProperty('--primary-color-gray-light', theme.fifth);
console.log(theme.name);}
const themesContainer = document.querySelector('#themesContainer');
Object.keys(themes.allThemes).forEach((theme, index) => { themesContainer.innerHTML
+= `
<div id="themeCircle" onclick="applyTheme('${themes.allThemes[theme].name}')">
<div id="themeCircleFirst" style="background-color:
${themes.allThemes[theme].second};"></div>
<div id="themeCircleSecond" style="background-color:
${themes.allThemes[theme].fourth};"></div></div>`})
let dynamicThemeBtn = document.querySelector('.dynamicThemeCircle');
dynamicThemeBtn.addEventListener('click', applyDynamicTheme)
function applyDynamicTheme() {
isDynamicThemeSelected = true;
fetchMusic(`WHERE ID = ${currentlyPlayingTrack}`)
.then(track => { mm.parseFile(track[0].path)
.then(metadata => { console.log(metadata);
dynamicColorFetch(metadata.common.picture ?
metadata.common.picture[0].data : `./assets/images/art.png`, { by:
'themeObject' }); }) }) }function dynamicColorFetch(image) {
Vibrant.from(image, { ImageClass: Image.Node }).getPalette()
.then(palette => { let themeObject = { name: 'dynamic', first:
palette.LightVibrant.getHex(), second: palette.Vibrant.getHex(), third:
palette.DarkVibrant.getHex(), fourth: palette.DarkVibrant.getHex(), fifth:
palette.DarkVibrant.getHex() }
themeObject.third = `#${lightenColor(palette.DarkVibrant.getHex().replace('#',''), -
32)}`
50
themeObject.fourth = `#${lightenColor(palette.DarkVibrant.getHex().replace('#',''), -
28)}`;
themeObject.fifth = `#${lightenColor(palette.DarkVibrant.getHex().replace('#',''), -
24)}`;
applyTheme(themeObject, { by: 'themeObject' }); }) .catch(err =>
console.error(err)); }function restoreUserState() {Settings WHERE rowid = 1", (err, row) =>
{
if(err) console.error(err); console.log(row);
if(row.lastPlayed) { currentlyPlayingTrack = row.lastPlayed; } else
{ currentlyPlayingTrack = 1;
} db.each(`SELECT * FROM Music WHERE ID = ${currentlyPlayingTrack}`,
(err, track) => {
if(err) console.error(err); updateCurrentlyPlayingInfo(track);
audioPlayerSrc.src = track.path; audioPlayer.load();
audioPlayer.volume = row.volume/100; volumeBar.value = row.volume;
audioPlayer.currentTime = row.lastPlayedDuration; progressBar.value =
row.progressBarValue;
playerBarCurrentTime.textContent =
secondsToMinutes(row.lastPlayedDuration); })
if(row.selectedTheme) {
if(row.selectedTheme == 'dynamic') {
applyDynamicTheme() } else
{ applyTheme(row.selectedTheme); }
} else { } })}ipcRenderer.on('restore-user-state', restoreUserState);
function saveUserState() { let currentSettings = [currentlySelectedTheme,
currentlyPlayingTrack, audioPlayer.currentTime, progressBar.value, volumeBar.value] let
sql = `UPDATE Settings SET selectedTheme = ?, lastPlayed = ?, lastPlayedDuration
= ?, progressBarValue = ?, volume = ? WHERE rowid = 1` db.run(sql,
currentSettings, (err) => { if(err) console.error(err);
ipcRenderer.send('save-state-success'); }) }document.addEventListener('keypress',
saveUserState);ipcRenderer.on('save-user-state', saveUserState);
function addToPlaylist(trackInfo, playlistName) { regeneratePage.playlistsPage = true;
51
let track = [trackInfo.id, trackInfo.title, trackInfo.album, trackInfo.artist, trackInfo.year,
trackInfo.duration, trackInfo.path];let sql = ` INSERT INTO
${playlistName}_playlist(id, title, album, artist, year, duration, path)
VALUES(?, ?, ?, ?, ?, ?, ?) `; db.run(sql, track, (err) => { if(err) console.error(err)
else console.log(`added ${trackInfo.title} to ${playlistName}`); }) }
function deleteFromPlaylist(event, trackId, playlistName) {
regeneratePage.playlistsPage = true; event.stopPropagation();
let sql = `DELETE FROM ${playlistName} WHERE ID = "${trackId}"`;db.run(sql, (err)
=> {
if(err) console.error(err) else console.log(`deleted ${trackId} from
${playlistName}`);
}) console.log(event);
event.target.parentElement.parentElement.addEventListener('animationend', () =>
{event.target.parentElement.parentElement.style = "display: none;"} )
event.target.parentElement.parentElement.classList.add('animated', 'fadeOut')}
function createNewPlaylist(playlistName) {
return new Promise((resolve, reject) => {
if(!(playlistName.length < 5)) {
db.run(`CREATE TABLE IF NOT EXISTS ${playlistName.replace(' ', '_')}_playlist
(id INTEGER, title TEXT, album TET, artist TEXT, year INT, duration INT, favourite INT,
path TEXT, UNIQUE(id))`, (err) => {
if(err) reject(err) else {
showToast(`${playlistName} playlist created`, 'fa-check');
resolve(`${playlistName} playlist created`); } }) } else {
showToast(`playlist name should be more than 5 characters`, 'fa-edit');
reject('playlist name should 5 or more characters') } })}
function deletePlaylist(event, playlistName) {
regeneratePage.playlistsPage = true;
return new Promise((resolve, reject) => {
db.run(`DROP TABLE ${playlistName}`, (err) => {
if(err) { reject(err) } else {
document.querySelector(`[data-
playlist='${playlistName}']`).addEventListener('animationend', () =>
{document.querySelector(`[data-playlist='${playlistName}']`).style = "display: none;"} )
52
document.querySelector(`[data-
playlist='${playlistName}']`).classList.add('animated', 'fadeOut')
loadPlaylistsPage();
showToast(`${capitalizeFirstLetter(playlistName.replace('_playlist', '').replace('_', '
'))} playlist deleted`, 'fa-trash'); resolve(`${playlistName.replace('_', ' ')} playlist
deleted`);
console.log(`${playlistName} playlist deleted`) } }) })}
function fetchPlaylistNames() {
return new Promise((resolve, reject) => {
db.all(`SELECT name FROM sqlite_master WHERE type='table'`, (err, data) =>
{ if(err) reject(err)
else resolve(data.filter(row => row.name.endsWith('_playlist')).map(row =>
row.name.replace('_playlist', ''))); }) })}
function escapeRegExp(text) { return text.replace(/([\"\'])/g,'');
function showToast(text, icon = 'fa-times') {
document.querySelector('#toastIcon').classList.add(`${icon}`);
document.querySelector('#toastText').textContent = text;
document.querySelector('#toastContainer').classList.add('showToast'); setTimeout(() =>
{
document.querySelector('#toastIcon').classList.remove(`${icon}`);
document.querySelector('#toastContainer').classList.remove('showToast');
}, 3000)
}function firstLaunch() {
let firstLaunchContainer = document.createElement('div');
firstLaunchContainer.setAttribute('id', 'firstLaunchContainer');
let firstLaunchText = document.createElement('p');
firstLaunchText.setAttribute('id', 'firstLaunchText');
firstLaunchText.textContent = 'welcome to mystt.';
let firstLaunchAdd = document.createElement('p');
firstLaunchAdd.setAttribute('id', 'firstLaunchAdd');
firstLaunchAdd.textContent = 'Add Music';
firstLaunchAdd.addEventListener('click',() => { addMusicFlow(); });
let container = document.querySelector('#container');
firstLaunchContainer.append(firstLaunchText, firstLaunchAdd);
53
container.appendChild(firstLaunchContainer);}
db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name='Music'`, (err,
data) => {
if(err) console.error(err); if(data.length != 1) { firstLaunch(); } else {
db.all(`SELECT COUNT(*) AS tracks FROM Music`, (err, table) => {
if(err) console.error(err);
if(table[0].tracks < 1) { firstLaunch() }; }) }})
module.exports.likeTrack = likeTrack;
module.exports.progressTo = progressTo;
module.exports.showAlbumTracks = showAlbumTracks;
module.exports.showArtistTracks = showArtistTracks;
module.exports.showPlaylistTracks = showPlaylistTracks;
module.exports.playTrack = playTrack;
module.exports.playMusic = playMusic;
module.exports.applyTheme = applyTheme;
module.exports.restoreUserState = restoreUserState;
module.exports.saveUserState = saveUserState;
module.exports.addToPlaylist = addToPlaylist;
module.exports.deleteFromPlaylist = deleteFromPlaylist;
module.exports.fetchPlaylistNames = fetchPlaylistNames;
module.exports.createNewPlaylist = createNewPlaylist;
module.exports.deletePlaylist = deletePlaylist;
module.exports.showToast = showToast;
module.exports.firstLaunch = firstLaunch;
54
TESTING
55
TEST CASES AND CRITERIA
We put our project under 3 levels of testing while creating and work on this project.
These levels were used step by step.
Levels of testing are:-
Unit Testing
Integration Testing
System Testing
o Unit Testing
Unit testing is done when a single file or functionality of the music
player gets completed. Steps that were taken while performing unit testing
are:-
Firstly html file of the particular functionalty where made and first
HTML file was the index.html.
Then these html files where given static functionalities and where
tested live using live feature present in brackats code editor.
Side by side CSS files were created to customize the page according to
the need.
Chrome also played a major role. As it was the default browser we
used to preview what we made.
o Integration Testing
Integration Test Case differs from other test cases in the sense it
focuses mainly on the interfaces & flow of data/information between the
modules. Here priority is to be given for the integrating links rather than the
unit functions which are already tested.
As we used Top-Down integration in out project, so we tested flow of
the functions approaching from top to bottom.
We made a simple prototype in the early stages of our project
We started by making base design of the elements we need and
keep on integrating with each other
56
After making design and adding functionality we added
features function by function according to needs and use in the
music player
o System Testing
System testing is the last phase when we test our project as a whole.
Every integrated function and elements are tested to give the best and the
final result that we are expecting from a project to look and perform like.
For further advice and reviews we gave out projects funtionalty
videos to our friends and worked according to the feedback.
57
SOME RESULTS FROM TESTING
58
IMPLEMENTATION
& EVALUATION OF
PROJECT
59
Creating Application
Electron enables you to create desktop applications with pure JavaScript by
providing a runtime with rich native (operating system) APIs. You could see it as a
variant of the Node.js runtime that is focused on desktop applications instead of web
servers.
This doesn't mean Electron is a JavaScript binding to graphical user interface
(GUI) libraries. Instead, Electron uses web pages as its GUI, so you could also see it
as a minimal Chromium browser, controlled by JavaScript.
As far as development is concerned, an Electron application is essentially a
Node.js application. The starting point is a package.json that is identical to that of a
Node.js module. A most basic Electron app would have the following folder structure:
mystt-music-player/
├── package.json
├── main.js
└── index.htm
Create a new empty folder for your new Electron application. Open up your command
line client and run npm init from that very folder.
npm init
npm will guide you through creating a basic package.json file. The script specified by
the main field is the startup script of your app, which will run the main process. An
example of your package.json might look like this:
{
"name": " mystt-music-player",
"version": "0.1.0",
"main": "main.js"
}
Turning this Node application into an Electron application is quite simple - we merely
replace the node runtime with the electron runtime.
{
"name": " mystt-music-player ",
"version": "0.1.0",
60
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
Installing Electron
At this point, you'll need to install electron itself. The recommended way of
doing so is to install it as a development dependency in your app, which allows you to
work on multiple apps with different Electron versions. To do so, run the following
command from your app's directory:
npm install --save-dev electron
61