diff --git a/common/lkt.js b/common/lkt.js index c7e1493e25e04ec9a8eb8808715e1cb44cfd8fa4..faa8942c35c87aad8c9ad725adf9a4104aac7807 100644 --- a/common/lkt.js +++ b/common/lkt.js @@ -134,7 +134,7 @@ class LktClient { return new Promise(resolv => { client.m_socket.on('data', data => { if (!once) { - client.m_socket.write(`command_list_begin\n${command}\ncommand_list_end\n`); + client.m_socket.write(`${command}\n`); once = true; return null; } else { @@ -170,6 +170,9 @@ class LktClient { if (String(data).includes('playlist')) { LktClient.setQueueUpdated(true); } + if(String(data).includes(' stored_playlist')) { + LktClient.setPlaylistsUpdated(true); + } if (String(data).includes('player')) { LktClient.reloadState(); } @@ -225,6 +228,74 @@ class LktClient { LktClient.setPlayState(status.state); } + static commandPlaylistsList() { + var client = new this(); + var once = false; + var result = {}; + var dataObj; + function __getResult(client) { + return new Promise(resolv => { + client.m_socket.setTimeout(0); + client.m_socket.on('data', data => { + if (!once) { + client.m_socket.write('listplaylists\n'); + once = true; + return null; + } else { + client.close(); + dataObj = __mpdToObject(data); + result = dataObj.name.split(" "); + resolv(result); + } + }); + }); + } + return __getResult(client); + } + + static commandPlaylistListKaras(playlist) { + var client = new this(); + var once = false; + var result = []; + var matches; + var dataObj; + var karaList; + const regex = /([0-9]+) (vo|va|amv|cdg|autres|vtuber) - (jp|fr|en|ru|sp|it|ch|latin|multi|undefined) \/ (.+) - (OP|ED|IS|AMV|PV|MV|LIVE)([0-9]*) - (.+) \[ (.+) \]/; + var reg = new RegExp(regex); + function __getResult(client) { + return new Promise(resolv => { + client.m_socket.setTimeout(0); + client.m_socket.on('data', data => { + if (!once) { + client.m_socket.write(`listplaylist ${playlist}\n`); + once = true; + return null; + } else { + client.close(); + dataObj = __mpdToObject(data); + karaList = data.split("\n"); + karaList.splice(dataObj.continue); + karaList.forEach(kara => { + matches = reg.exec(kara); + result.push( { + id:matches[1], + cat: matches[2], + language:matches[3], + source:matches[4], + type:matches[5]+matches[6], + title:matches[7], + author:matches[8] + }); + //logger.info("kara",matches[1]); + }); + resolv(result); + } + }); + }); + } + return __getResult(client); + } + static commandPlayPos(position) { return LktClient.__execSimple(`play ${position}`); } @@ -277,6 +348,29 @@ class LktClient { return LktClient.__execSimple(`delid ${id}`); } + static commandPlaylistAddId(playlist, id) { + return LktClient.__execSimple(`playlistadd ${playlist} id://${id}`); + } + + static commandPlaylistRemoveId(playlist, id) { + return LktClient.__execSimple(`playlistdelete ${playlist} ${id}`); + } + + static commandPlaylistClear(playlist) { + return LktClient.__execSimple(`playlistclear ${playlist}`); + } + static commandAddPlaylistToQueue(playlist) { + return LktClient.__execSimple(`add playlist://${playlist}`); + } + + static commandPlaylistCreate(playlist) { + return LktClient.__execSimple(`playlistadd ${playlist}`); + } + + static commandPlaylistDelete(playlist) { + return LktClient.__execSimple(`playlistdelete ${playlist}`); + } + static errorStatus(error) { logger.error('Unable to access lektor status:' + error); } @@ -295,6 +389,20 @@ class LktClient { return this.status_updated; } + static playlists_updated = true; + + static setPlaylistsUpdated(state) { + logger.debug("abab","playlistyodated"); + this.playlists_updated = state; + } + + static isPlaylistsUpdated() { + return this.playlists_updated; + } + static isStatusUpdated() { + return this.status_updated; + } + static timeData = { elapsed: 0, total: 100, state: 'stop', song: 0 }; static setPlayState(state) { @@ -380,7 +488,12 @@ function __mpdToObject(string) { string.split('\n').forEach(line => { if (line.length <= 1) return; var couple = line.split(/: (.+)/).slice(0, 2); - ret[couple[0]] = couple[1]; + if(!ret[couple[0]]) { + ret[couple[0]] = couple[1]; + } + else { + ret[couple[0]] += " " + couple[1]; + } }); return ret; } diff --git a/instance/index.js b/instance/index.js index e070fcc61369e93b18d9a6435cb3e2f17d72e189..ee4bf6e2a613b20dd13c82cef3cc33c2762db28e 100644 --- a/instance/index.js +++ b/instance/index.js @@ -12,6 +12,10 @@ var dragCounter = 0; var leavedElement; var isDnDFromDB = false; var isPlaying = false; +var currentPlaylist = ""; +var isQueueView = true +var clearMenuQueue; +var clearMenuPlaylist; function updatePlayPauseButton(state) { logger.debug('instance', `State was ${state}`); @@ -98,6 +102,12 @@ window.onload = () => { addIpcToButton('open-kurisu-view', ['toggle-kurisu']); addIpcToButton('open-user-view', ['toggle-client-view']); addIpcToButton('open-log-view', ['toggle-log-view']); + addIpcToButton('newPlaylist',['new-playlist']); + + var switchQP = document.getElementById('switchQueuePlaylist'); + switchQP.innerText = "View playlist"; + switchQP.onclick = ()=>switchQueuePlaylist(); + $('#openMdtView').click(() => $('#mdt-view').toggle()); @@ -152,42 +162,79 @@ window.onload = () => { .on('contextmenu', e => { var top = e.pageY - 10; var left = e.pageX - 90; - $('#context-menu-right') + if(isQueueView){ + $('#context-menu-right') .css({ display: 'block', top: top, left: left, }) .addClass('show'); + } + else { + $('#context-menu-right-playlists') + .css({ + display: 'block', + top: top, + left: left, + }) + .addClass('show'); + } return false; /* blocks default Webbrowser right click menu */ }) - .on('click', () => $('#context-menu-right').removeClass('show').hide()); + .on('click', () => { + $('#context-menu-right').removeClass('show').hide(); + $('#context-menu-right-playlists').removeClass('show').hide(); + }); - const clearMenu = () => $('#context-menu-right').removeClass('show').hide(); + clearMenuQueue = () => $('#context-menu-right').removeClass('show').hide(); + clearMenuPlaylist = () => $('#context-menu-right-playlists').removeClass('show').hide(); $('#queue-play').click(() => { - clearMenu(); + clearMenuQueue(); logger.debug('instance', 'Queue play'); ipcRenderer.send('cmd-unpause'); }); $('#queue-pause').click(() => { - clearMenu(); + clearMenuQueue(); logger.debug('instance', 'Queue pause'); ipcRenderer.send('cmd-pause'); }); $('#queue-find').click(() => { - clearMenu(); + clearMenuQueue(); logger.debug('instance', 'Queue find'); }); $('#queue-shuffle').click(() => { - clearMenu(); + clearMenuQueue(); logger.debug('instance', 'Queue shuffle'); ipcRenderer.send('cmd-shuffle'); }); $('#queue-clear').click(() => { - clearMenu(); + clearMenuQueue(); logger.debug('instance', 'Queue clear'); ipcRenderer.send('cmd-clear'); }); + $('#playlist-addToQueue').click(() => { + clearMenuPlaylist(); + logger.debug('instance', 'Playlist add to queue'); + ipcRenderer.send('add-current-playlist-queue'); + }); + $('#playlist-clear').click(() => { + clearMenuPlaylist(); + logger.debug('instance', 'Playlist clear'); + ipcRenderer.send('clear-playlist'); + }); + $('#playlist-delete').click(() => { + clearMenuPlaylist(); + logger.debug('instance', 'Playlist delete'); + ipcRenderer.send('delete-playlist'); + }); + + var playlistSelect = document.getElementById("playlist-selector"); + + playlistSelect.onchange = () => { + currentPlaylist = playlistSelect.value; + ipcRenderer.send('get-playlist-data',playlistSelect.value); + } }; /* Create the left panel */ @@ -207,15 +254,54 @@ ipcRenderer.on('reload-db-responce', (event, arg) => { ipcRenderer.on('reload-queue-responce', (event, arg) => { logger.debug('instance', `Web page got reload-queue`); countKaraInQueue = 0; - document.getElementById('panelRight').innerHTML = ''; + if(isQueueView){ + document.getElementById('panelRight').innerHTML = ''; + arg.forEach(kara => { + countKaraInQueue++; + ejs.renderFile(__dirname + '/views/karaQueueListItem.ejs', { kara: kara }, (err, data) => { + if (err) logger.error('instance', err); + $('#panelRight').append(data); + }); + }); + [].forEach.call(document.querySelectorAll('#panelRight .karaCard'), addQueueKaraEventHandlers); + } + else { + document.getElementById('panelBuffer').innerHTML = ''; arg.forEach(kara => { countKaraInQueue++; ejs.renderFile(__dirname + '/views/karaQueueListItem.ejs', { kara: kara }, (err, data) => { + if (err) logger.error('instance', err); + $('#panelBuffer').append(data); + }); + }); + } +}); + +ipcRenderer.on('playlist-data-responce', (event,karas) => { + var playListPanel = document.getElementById('panelRight'); + if(!isQueueView) { + document.getElementById('panelRight').innerHTML = ''; + karas.forEach(kara => { + ejs.renderFile(__dirname + '/views/karaPlaylistListItem.ejs', { kara: kara }, (err, data) => { if (err) logger.error('instance', err); $('#panelRight').append(data); }); }); - [].forEach.call(document.querySelectorAll('#panelRight .karaCard'), addQueueKaraEventHandlers); + [].forEach.call(document.querySelectorAll('#panelRightPlaylists .karaCard'), addPlayListKaraEventHandlers); + } + else { + document.getElementById('panelBuffer').innerHTML = ''; + karas.forEach(kara => { + ejs.renderFile(__dirname + '/views/karaPlaylistListItem.ejs', { kara: kara }, (err, data) => { + if (err) logger.error('instance', err); + $('#panelBuffer').append(data); + }); + }); + } + +}); +ipcRenderer.on('send-lektord-is-attached', (event, arg) => { + logger.debug('instance', `Lektord is in attached mode? ${arg}`); }); ipcRenderer.on('send-state', (event, state) => updatePlayPauseButton(state)); @@ -275,6 +361,11 @@ function addDBKaraEventHandlers(element) { id: element.getElementsByClassName('karaID')[0].innerText, }) ); + element.getElementsByClassName('karaAddPlaylistBtn')[0].addEventListener('click', () => + ipcRenderer.send('add-kara-playlist-id', { + id: element.getElementsByClassName('karaID')[0].innerText, + }) + ); } function addQueueKaraEventHandlers(element) { @@ -296,6 +387,14 @@ function addQueueKaraEventHandlers(element) { }); } +function addPlayListKaraEventHandlers(element) { + element.getElementsByClassName('karaDeleteBtn')[0].addEventListener('click', () => { + ipcRenderer.send('del-kara-playlist-id', { + id:element.getElementsByClassName('karaID')[0].innerText + }); + }); +} + function onDragStartDB(event) { isDnDFromDB = true; event.dataTransfer.effectAllowed = 'move'; @@ -356,3 +455,59 @@ function onDragEnd(event) { event.currentTarget.style.opacity = 1; dragCounter = 0; } + + +function switchQueuePlaylist() { + isQueueView = !isQueueView; + var switchQP = document.getElementById('switchQueuePlaylist'); + var tempHTML = document.getElementById("panelRight").innerHTML; + document.getElementById("panelRight").innerHTML = document.getElementById("panelBuffer").innerHTML; + document.getElementById("panelBuffer").innerHTML = tempHTML; + if(isQueueView){ + switchQP.innerText = "View playlist"; + [].forEach.call(document.querySelectorAll('#panelLeft .karaCard'), (element)=>{ + + element.getElementsByClassName('karaAddBtn')[0].hidden = false; + element.getElementsByClassName('karaInsertBtn')[0].hidden = false; + element.getElementsByClassName('karaAddPlaylistBtn')[0].hidden = true; + }); + [].forEach.call(document.querySelectorAll('#panelRight .karaCard'), addQueueKaraEventHandlers); + clearMenuPlaylist(); + } + else { + switchQP.innerText = "View queue"; + [].forEach.call(document.querySelectorAll('#panelLeft .karaCard'), (element)=>{ + element.getElementsByClassName('karaAddBtn')[0].hidden = true; + element.getElementsByClassName('karaInsertBtn')[0].hidden = true; + element.getElementsByClassName('karaAddPlaylistBtn')[0].hidden = false; + }); + [].forEach.call(document.querySelectorAll('#panelRight .karaCard'), addPlayListKaraEventHandlers); + clearMenuQueue(); + } +} + +ipcRenderer.on('playlists-updated', (event, playlists) => { + if(!playlists) { + return; + } + var selector = document.getElementById("playlist-selector") + var currentChoice = selector.value; + if(!currentChoice || !playlists.includes(currentChoice)) { + selector.innerHTML = '<option selected disabled> Select a playlist</option>' + } + else { + selector.innerHTML = '<option disabled> Select a playlist</option>'; + } + playlists.forEach(playlist => { + if(playlist == currentChoice) { + optionHtml = `<option selected value="${playlist}">${playlist}</option>`; + } + else { + optionHtml = `<option value="${playlist}">${playlist}</option>`; + } + selector.innerHTML = ` + ${selector.innerHTML} + ${optionHtml} + ` + }); +}); diff --git a/instance/views/karaDBListItem.ejs b/instance/views/karaDBListItem.ejs index 8f0f69d868613e5fad639e892272be08c3c603e3..62e8fea1fa7e130bf88a9641a7fa210a14916265 100644 --- a/instance/views/karaDBListItem.ejs +++ b/instance/views/karaDBListItem.ejs @@ -2,10 +2,11 @@ The template for the kara card in lists %> <li class="card p-2 bd-highlight shadow-none d-flex flex-row bd-highlight mb-3 karaCard" draggable="true"> <%- include('kara.ejs'); %> - <div class="karaElement bd-highlight"> + <div class="karaElement p-2 bd-highlight"> <div class="d-flex flex-row bd-highlight mb-3 btn-group karaActionBtnGroup" role="group"> <button class="btn btn-outline-light karaActionBtn karaAddBtn" title="Add"><i class="fas fa-plus"></i></button> <button class="btn btn-outline-light karaActionBtn karaInsertBtn" title="Insert"><i class="fas fa-level-up-alt"></i></button> + <button hidden class="btn btn-outline-light karaActionBtn karaAddPlaylistBtn" title="Add"><i class="fas fa-plus"></i></button> </div> </div> </li> diff --git a/instance/views/karaPlaylistListItem.ejs b/instance/views/karaPlaylistListItem.ejs new file mode 100644 index 0000000000000000000000000000000000000000..941e76c0f2244c0014a7e0bbaff09ea0802717ee --- /dev/null +++ b/instance/views/karaPlaylistListItem.ejs @@ -0,0 +1,10 @@ +<%# vim: ts=4 syntax=html + The template for the kara card in lists %> +<li class="card p-2 bd-highlight shadow-none d-flex flex-row bd-highlight mb-3 karaCard karaPlaylist"> + <%- include('kara.ejs'); %> + <div class="karaElement p-2 bd-highlight"> + <div class="d-flex flex-row bd-highlight mb-3 btn-group karaActionBtnGroup" role="group"> + <button class="btn btn-outline-light karaActionBtn karaDeleteBtn" title="Delete"><i class="fas fa-times"></i></button> + </div> + </div> +</li> diff --git a/instance/views/menu/playlist.ejs b/instance/views/menu/playlist.ejs new file mode 100644 index 0000000000000000000000000000000000000000..bfb5d06a9601a08e2d98ee1f883a2c3dfbc7b904 --- /dev/null +++ b/instance/views/menu/playlist.ejs @@ -0,0 +1,7 @@ +<%# vim: ts=4 syntax=html + The template for the right click menu on the queue %> +<div class="dropdown-menu dropdown-menu-sm" id="context-menu-right-playlists"> + <a class="dropdown-item" id="playlist-addToQueue" href="#">Add playlist to queue</a> + <a class="dropdown-item" id="playlist-clear" href="#">Clear playlist</a> + <a class="dropdown-item" id="playlist-delete" href="#">Delete playlist</a> +</div> diff --git a/instance/views/menubar.ejs b/instance/views/menubar.ejs index 785c6e084cd97d31839f43c546f21ad844a58170..b6f48567a93b77a8a6945589017dbd4e07c514a7 100644 --- a/instance/views/menubar.ejs +++ b/instance/views/menubar.ejs @@ -12,10 +12,18 @@ {id: 'selectPlaylist', tooltip: 'Playlist search', name: 'tag'}, {id: 'selectPool', tooltip: "Kurisu's pool search", name: 'bookmark'}, ]}); %></div> - <div class="p-1 bd-highlight mb-1 mr-auto ml-auto" style="width: 60%;-webkit-app-region: no-drag;"> + <div class="p-1 bd-highlight mb-1 mr-auto ml-auto" style="width: 40%;-webkit-app-region: no-drag;"> <input id="filterInput" type="text" class="form-control filterInput" placeholder="Filter..."> </div> + <select class="form-control bd-highlight mt-1" id="playlist-selector" aria-label="Default select example" style="width: 15%;-webkit-app-region: no-drag;"> + <option selected disabled>Select a playlist</option> + <option value="1">One</option> + <option value="2">Two</option> + <option value="3">Three</option> + </select> <div class="btn-group" role="group"><%- include('menubar/buttons.ejs', {type: 'secondary', buttons: [ + {id: 'newPlaylist', tooltip: 'Create new playlist', name: 'plus'}, + {id: 'switchQueuePlaylist', tooltip: 'Switch between queue and playlist', name: ''}, {id: 'openMdtView', tooltip: 'Open metadata view', name: 'music'}, {id: 'btn-settings', tooltip: 'Open settings', name: 'cogs'}, {id: 'closeButton', tooltip: 'Quit Amadeus', name: 'window-close'}, diff --git a/instance/views/panels.ejs b/instance/views/panels.ejs index 8cb01c6e4be357977ef0079077df1d61eacd7f37..6b74444b4e3744c105be954254b721461451a628 100644 --- a/instance/views/panels.ejs +++ b/instance/views/panels.ejs @@ -10,4 +10,6 @@ class="col panel d-flex flex-column bd-highlight mb-3"> </ul> <%- include('menu/queue.ejs'); %> + <%- include('menu/playlist.ejs'); %> </div> +<div hidden id="panelBuffer" style="visibility: hidden"></div> \ No newline at end of file diff --git a/main.js b/main.js index e9f03def1d1f8425b6c5e7bb6ab3981d89be7455..2ace93c74a9ef390223a9007284d34b57cb9a21b 100644 --- a/main.js +++ b/main.js @@ -7,6 +7,7 @@ const logger = require.main.require('./common/logger.js'), isRunning = require('./common/isRunning.js'); let electronEjs = require('electron-ejs'); +let prompt = require('electron-prompt'); let ejs = new electronEjs({ app: 'Amadeus' }, {}); const packageConfig = require('./package.json').config; @@ -16,6 +17,7 @@ var config = require('./common/config.js'); var db = require.main.require('./common/db.js'); var Tail = require('tail').Tail; + var client; /* Sub process for the express server */ /* Timers functions, needs to be clear on app quit */ @@ -249,15 +251,64 @@ ipcMain.on('queue-moved-kara', (event, movement) => { } }); +var currentPlaylist = ""; + ipcMain.on('play-kara-queue-pos', (event, arg) => lkt.commandPlayPos(arg.position)); ipcMain.on('add-kara-queue-id', (event, arg) => lkt.commandQueueAddId(arg.id)); ipcMain.on('insert-kara-queue-id', (event, arg) => lkt.commandQueueInsertId(arg.id)); ipcMain.on('delete-kara-queue-pos', (event, arg) => lkt.commandQueueDelPos(arg.position)); +ipcMain.on('add-kara-playlist-id', (event,arg) => lkt.commandPlaylistAddId(currentPlaylist,arg.id)); +ipcMain.on('del-kara-playlist-id', (event,arg) => lkt.commandPlaylistRemoveId(currentPlaylist,arg.id)); + +ipcMain.on('clear-playlist', (event,arg) => { + if(currentPlaylist != "") { + lkt.commandPlaylistClear(currentPlaylist); + } +}); + +ipcMain.on('delete-playlist', (event,arg) => { + if(currentPlaylist != "") { + lkt.commandPlaylistDelete(currentPlaylist); + } +}); + +ipcMain.on('add-current-playlist-queue', (event,arg) => { + if(currentPlaylist != "") { + lkt.commandAddPlaylistToQueue(currentPlaylist); + } +}); ipcMain.on('add-kara-queue-pos', (event, addparams) => { lkt.commandQueueAddId(addparams.id).then(() => lkt.commandMove(addparams.queueSize + 1, addparams.position)); }); +ipcMain.on('get-playlist-data', (event, playlist) => { + if(playlist) { + currentPlaylist = playlist; + lkt.commandPlaylistListKaras(currentPlaylist).then((karas) => { + win.webContents.send('playlist-data-responce',karas); + }); + } +}); + + + +ipcMain.on('new-playlist', (event,arg) => { + prompt({ + title: 'New playlist', + label: 'Enter playlist name', + type: 'input', + customStylesheet:"style/bootstrap-4.5.2-dist/css/bootstrap.min.css" + }) + .then((playlist) => { + if(playlist != null) { + logger.debug("test",playlist); + lkt.commandPlaylistCreate(playlist); + } + }) +}); + + ipcMain.on('verify-lektord', (event, arg) => { lkt.ping().then(sta => { logger.debug('main', `Status from ping is ${sta}`); @@ -350,6 +401,28 @@ ipcMain.on('verify-lektord', (event, arg) => { } }, 50), ]); + setInterval(() => { + if(lkt.isPlaylistsUpdated()) { + lkt.setPlaylistsUpdated(false); + lkt.commandPlaylistsList().then((playlists) => { + win.webContents.send('playlists-updated', playlists); + }); + if(currentPlaylist != "") { + lkt.commandPlaylistListKaras(currentPlaylist).then((karas) => { + win.webContents.send('playlist-data-responce',karas); + }); + } + } + }, 50); + + setInterval(() => { + if(currentPlaylist != "" && lkt.isPlaylistsUpdated()) { + lkt.setPlaylistsUpdated(false); + lkt.commandPlaylistListKaras(currentPlaylist).then((karas) => { + win.webContents.send('playlist-data-responce', karas); + }); + } + }, 50); }); }); diff --git a/package.json b/package.json index 9f78d99e3f8e68a800077bdc2327ee7fb385eea9..7f30cae9ca6ba6a74ea4fcda8d54333b95f55218 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "node": "<14.0.0 ", "sqlite3": "5.0.0", "tail": "^2.0.4", - "winston": "^3.3.3" + "winston": "^3.3.3", + "electron-prompt": "^1.6.2" }, "config": { "port": "8080",