Merge branch 'develop' for release v3.31.0
This commit is contained in:
@@ -13,7 +13,7 @@ ARG BUILD_PACKAGES="\
|
|||||||
taglib \
|
taglib \
|
||||||
wt"
|
wt"
|
||||||
|
|
||||||
RUN pacman -Syy
|
RUN pacman -Syu --noconfirm
|
||||||
RUN pacman -S --noconfirm ${BUILD_PACKAGES}
|
RUN pacman -S --noconfirm ${BUILD_PACKAGES}
|
||||||
|
|
||||||
# LMS
|
# LMS
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<message id="Lms.Admin.User.user-already-exists">User already exists!</message>
|
<message id="Lms.Admin.User.user-already-exists">User already exists!</message>
|
||||||
<message id="Lms.Admin.User.user-create">New user</message>
|
<message id="Lms.Admin.User.user-create">New user</message>
|
||||||
<message id="Lms.Admin.User.user-created">New user created!</message>
|
<message id="Lms.Admin.User.user-created">New user created!</message>
|
||||||
<message id="Lms.Admin.User.user-edit">Edit user '{1}'</message>
|
<message id="Lms.Admin.User.user-edit">User '{1}'</message>
|
||||||
<message id="Lms.Admin.User.user-updated">User updated!</message>
|
<message id="Lms.Admin.User.user-updated">User updated!</message>
|
||||||
|
|
||||||
<!--Wizard-->
|
<!--Wizard-->
|
||||||
@@ -133,6 +133,7 @@
|
|||||||
<message id="Lms.Explore.recently-modified">Recently modified</message>
|
<message id="Lms.Explore.recently-modified">Recently modified</message>
|
||||||
<message id="Lms.Explore.recently-played">Recently played</message>
|
<message id="Lms.Explore.recently-played">Recently played</message>
|
||||||
<message id="Lms.Explore.releases">Albums</message>
|
<message id="Lms.Explore.releases">Albums</message>
|
||||||
|
<message id="Lms.Explore.search">Search</message>
|
||||||
<message id="Lms.Explore.star">Star</message>
|
<message id="Lms.Explore.star">Star</message>
|
||||||
<message id="Lms.Explore.starred">Starred</message>
|
<message id="Lms.Explore.starred">Starred</message>
|
||||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||||
@@ -187,7 +188,6 @@
|
|||||||
<!--PlayHistory-->
|
<!--PlayHistory-->
|
||||||
<message id="Lms.PlayHistory.playhistory">Play History</message>
|
<message id="Lms.PlayHistory.playhistory">Play History</message>
|
||||||
|
|
||||||
|
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
<message id="Lms.Settings.audio">Audio</message>
|
<message id="Lms.Settings.audio">Audio</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
||||||
@@ -208,6 +208,7 @@
|
|||||||
<message id="Lms.Settings.scrobbling.scrobbler.internal">Internal</message>
|
<message id="Lms.Settings.scrobbling.scrobbler.internal">Internal</message>
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
||||||
<message id="Lms.Settings.scrobbling.listenbrainz-token">ListenBrainz token</message>
|
<message id="Lms.Settings.scrobbling.listenbrainz-token">ListenBrainz token</message>
|
||||||
|
<message id="Lms.Settings.settings">Settings</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
|
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
|
||||||
<message id="Lms.Explore.recently-played">Joués récemment</message>
|
<message id="Lms.Explore.recently-played">Joués récemment</message>
|
||||||
<message id="Lms.Explore.releases">Albums</message>
|
<message id="Lms.Explore.releases">Albums</message>
|
||||||
|
<message id="Lms.Explore.search">Rechercher</message>
|
||||||
<message id="Lms.Explore.star">Ajouter aux favoris</message>
|
<message id="Lms.Explore.star">Ajouter aux favoris</message>
|
||||||
<message id="Lms.Explore.starred">Favoris</message>
|
<message id="Lms.Explore.starred">Favoris</message>
|
||||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||||
@@ -207,6 +208,7 @@
|
|||||||
<message id="Lms.Settings.scrobbling.scrobbler.internal">Interne</message>
|
<message id="Lms.Settings.scrobbling.scrobbler.internal">Interne</message>
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
||||||
<message id="Lms.Settings.scrobbling.listenbrainz-token">Jeton ListenBrainz</message>
|
<message id="Lms.Settings.scrobbling.listenbrainz-token">Jeton ListenBrainz</message>
|
||||||
|
<message id="Lms.Settings.settings">Paramètres</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
|
||||||
|
|||||||
@@ -133,9 +133,9 @@
|
|||||||
|
|
||||||
<message id="Lms.Explore.recently-played">Riprodotti di recente</message>
|
<message id="Lms.Explore.recently-played">Riprodotti di recente</message>
|
||||||
<message id="Lms.Explore.releases">Album</message>
|
<message id="Lms.Explore.releases">Album</message>
|
||||||
|
<message id="Lms.Explore.search">Ricerca</message>
|
||||||
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
|
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
|
||||||
<message id="Lms.Explore.starred">Preferiti</message>
|
<message id="Lms.Explore.starred">Preferiti</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.tracks">Tracce</message>
|
<message id="Lms.Explore.tracks">Tracce</message>
|
||||||
<message id="Lms.Explore.type">Tipo</message>
|
<message id="Lms.Explore.type">Tipo</message>
|
||||||
<message id="Lms.Explore.unstar">Rimuovi dai preferiti</message>
|
<message id="Lms.Explore.unstar">Rimuovi dai preferiti</message>
|
||||||
@@ -207,6 +207,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
||||||
|
|||||||
@@ -130,6 +130,7 @@
|
|||||||
<message id="Lms.Explore.recently-modified">最近更改</message>
|
<message id="Lms.Explore.recently-modified">最近更改</message>
|
||||||
<message id="Lms.Explore.recently-played">最近播放</message>
|
<message id="Lms.Explore.recently-played">最近播放</message>
|
||||||
<message id="Lms.Explore.releases">专辑</message>
|
<message id="Lms.Explore.releases">专辑</message>
|
||||||
|
<message id="Lms.Explore.search">搜索</message>
|
||||||
<message id="Lms.Explore.star">收藏</message>
|
<message id="Lms.Explore.star">收藏</message>
|
||||||
<message id="Lms.Explore.starred">已收藏</message>
|
<message id="Lms.Explore.starred">已收藏</message>
|
||||||
<message id="Lms.Explore.tracklists">播放列表</message>
|
<message id="Lms.Explore.tracklists">播放列表</message>
|
||||||
@@ -201,6 +202,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
||||||
|
|||||||
@@ -80,3 +80,6 @@ cover-preferred-file-names = ("cover", "front" );
|
|||||||
|
|
||||||
# Set to true if you want to hide duplicate tracks
|
# Set to true if you want to hide duplicate tracks
|
||||||
scanner-skip-duplicate-recording-mbid = false;
|
scanner-skip-duplicate-recording-mbid = false;
|
||||||
|
|
||||||
|
# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate'
|
||||||
|
scanner-parser-read-style = "accurate";
|
||||||
|
|||||||
+109
-73
@@ -16,58 +16,112 @@ const Mode = {
|
|||||||
Object.freeze(Mode);
|
Object.freeze(Mode);
|
||||||
|
|
||||||
LMS.mediaplayer = function () {
|
LMS.mediaplayer = function () {
|
||||||
|
let _root = {};
|
||||||
|
let _elems = {};
|
||||||
|
let _offset = 0;
|
||||||
|
let _trackId = null;
|
||||||
|
let _duration = 0;
|
||||||
|
let _audioNativeSrc;
|
||||||
|
let _audioTranscodeSrc;
|
||||||
|
let _settings = {};
|
||||||
|
let _playedDuration = 0;
|
||||||
|
let _lastStartPlaying = null;
|
||||||
|
let _audioIsInit = false;
|
||||||
|
let _pendingTrackParameters = null;
|
||||||
|
let _gainNode = null;
|
||||||
|
let _audioCtx = null;
|
||||||
|
|
||||||
var _root = {};
|
let _unlock = function() {
|
||||||
var _elems = {};
|
document.removeEventListener("touchstart", _unlock);
|
||||||
var _offset = 0;
|
document.removeEventListener("touchend", _unlock);
|
||||||
var _trackId = null;
|
document.removeEventListener("click", _unlock);
|
||||||
var _duration = 0;
|
_initAudioCtx();
|
||||||
var _audioNativeSrc;
|
};
|
||||||
var _audioTranscodeSrc;
|
|
||||||
var _settings = {};
|
|
||||||
var _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
||||||
var _gainNode = _audioCtx.createGain();
|
|
||||||
var _playedDuration = 0;
|
|
||||||
var _lastStartPlaying = null;
|
|
||||||
|
|
||||||
var _updateControls = function() {
|
document.addEventListener("touchstart", _unlock);
|
||||||
|
document.addEventListener("touchend", _unlock);
|
||||||
|
document.addEventListener("click", _unlock);
|
||||||
|
|
||||||
|
let _initAudioCtx = function() {
|
||||||
|
if (_audioIsInit) {
|
||||||
|
_audioCtx.resume(); // not sure of this
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_audioIsInit = true;
|
||||||
|
|
||||||
|
_audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
_gainNode = _audioCtx.createGain();
|
||||||
|
source = _audioCtx.createMediaElementSource(_elems.audio);
|
||||||
|
source.connect(_gainNode);
|
||||||
|
_gainNode.connect(_audioCtx.destination);
|
||||||
|
_audioCtx.resume(); // not sure of this
|
||||||
|
|
||||||
|
if ("mediaSession" in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler("play", function() {
|
||||||
|
_playPause();
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler("pause", function() {
|
||||||
|
_playPause();
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler("previoustrack", function() {
|
||||||
|
_playPrevious();
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler("nexttrack", function() {
|
||||||
|
_playNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingTrackParameters != null) {
|
||||||
|
_applyAudioTrackParameters(_pendingTrackParameters);
|
||||||
|
_pendingTrackParameters = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _updateControls = function() {
|
||||||
const pauseClass = "fa-pause";
|
const pauseClass = "fa-pause";
|
||||||
const playClass = "fa-play";
|
const playClass = "fa-play";
|
||||||
|
|
||||||
if (_elems.audio.paused) {
|
if (_elems.audio.paused) {
|
||||||
_elems.playpause.firstElementChild.classList.remove(pauseClass);
|
_elems.playpause.firstElementChild.classList.remove(pauseClass);
|
||||||
_elems.playpause.firstElementChild.classList.add(playClass);
|
_elems.playpause.firstElementChild.classList.add(playClass);
|
||||||
|
if ("mediaSession" in navigator) {
|
||||||
|
navigator.mediaSession.playbackState = "paused";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
_elems.playpause.firstElementChild.classList.remove(playClass);
|
_elems.playpause.firstElementChild.classList.remove(playClass);
|
||||||
_elems.playpause.firstElementChild.classList.add(pauseClass);
|
_elems.playpause.firstElementChild.classList.add(pauseClass);
|
||||||
|
if ("mediaSession" in navigator) {
|
||||||
|
navigator.mediaSession.playbackState = "playing";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _startTimer = function() {
|
let _startTimer = function() {
|
||||||
if (_lastStartPlaying == null)
|
if (_lastStartPlaying == null)
|
||||||
Wt.emit(_root, "scrobbleListenNow", _trackId);
|
Wt.emit(_root, "scrobbleListenNow", _trackId);
|
||||||
_lastStartPlaying = Date.now();
|
_lastStartPlaying = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
var _stopTimer = function() {
|
let _stopTimer = function() {
|
||||||
if (_lastStartPlaying != null) {
|
if (_lastStartPlaying != null) {
|
||||||
_playedDuration += Date.now() - _lastStartPlaying;
|
_playedDuration += Date.now() - _lastStartPlaying;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _resetTimer = function() {
|
let _resetTimer = function() {
|
||||||
if (_lastStartPlaying != null)
|
if (_lastStartPlaying != null)
|
||||||
Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration);
|
Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration);
|
||||||
_playedDuration = 0;
|
_playedDuration = 0;
|
||||||
_lastStartPlaying = null;
|
_lastStartPlaying = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var _durationToString = function (duration) {
|
let _durationToString = function (duration) {
|
||||||
var minutes = parseInt(duration / 60, 10);
|
let minutes = parseInt(duration / 60, 10);
|
||||||
var seconds = parseInt(duration, 10) % 60;
|
let seconds = parseInt(duration, 10) % 60;
|
||||||
|
|
||||||
var res = "";
|
let res = "";
|
||||||
|
|
||||||
res += minutes + ":";
|
res += minutes + ":";
|
||||||
res += (seconds < 10 ? "0" + seconds : seconds);
|
res += (seconds < 10 ? "0" + seconds : seconds);
|
||||||
@@ -75,21 +129,15 @@ LMS.mediaplayer = function () {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
var _playTrack = function() {
|
let _playTrack = function() {
|
||||||
var playPromise = _elems.audio.play();
|
_elems.audio.play()
|
||||||
|
.then(_ => {})
|
||||||
if (playPromise !== undefined) {
|
.catch(error => { console.log("Cannot play audio: " + error); });
|
||||||
playPromise.then(_ => {
|
|
||||||
// Automatic playback started
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// Auto-play was prevented
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _playPause = function() {
|
let _playPause = function() {
|
||||||
_audioCtx.resume();
|
_initAudioCtx();
|
||||||
|
|
||||||
if (_elems.audio.paused && _elems.audio.children.length > 0) {
|
if (_elems.audio.paused && _elems.audio.children.length > 0) {
|
||||||
_playTrack();
|
_playTrack();
|
||||||
}
|
}
|
||||||
@@ -97,25 +145,17 @@ LMS.mediaplayer = function () {
|
|||||||
_elems.audio.pause();
|
_elems.audio.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
var _playPrevious = function() {
|
let _playPrevious = function() {
|
||||||
_audioCtx.resume();
|
_initAudioCtx();
|
||||||
_requestPreviousTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
var _playNext = function() {
|
|
||||||
_audioCtx.resume();
|
|
||||||
_requestNextTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
var _requestPreviousTrack = function() {
|
|
||||||
Wt.emit(_root, "playPrevious");
|
Wt.emit(_root, "playPrevious");
|
||||||
}
|
}
|
||||||
|
|
||||||
var _requestNextTrack = function() {
|
let _playNext = function() {
|
||||||
|
_initAudioCtx();
|
||||||
Wt.emit(_root, "playNext");
|
Wt.emit(_root, "playNext");
|
||||||
}
|
}
|
||||||
|
|
||||||
var _initVolume = function() {
|
let _initVolume = function() {
|
||||||
if (typeof(Storage) !== "undefined" && localStorage.volume) {
|
if (typeof(Storage) !== "undefined" && localStorage.volume) {
|
||||||
_elems.volumeslider.value = Number(localStorage.volume);
|
_elems.volumeslider.value = Number(localStorage.volume);
|
||||||
}
|
}
|
||||||
@@ -123,7 +163,7 @@ LMS.mediaplayer = function () {
|
|||||||
_setVolume(_elems.volumeslider.value);
|
_setVolume(_elems.volumeslider.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
var _initDefaultSettings = function(defaultSettings) {
|
let _initDefaultSettings = function(defaultSettings) {
|
||||||
if (typeof(Storage) !== "undefined" && localStorage.settings) {
|
if (typeof(Storage) !== "undefined" && localStorage.settings) {
|
||||||
_settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
|
_settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
|
||||||
}
|
}
|
||||||
@@ -134,7 +174,7 @@ LMS.mediaplayer = function () {
|
|||||||
Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
|
Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
var _setVolume = function(volume) {
|
let _setVolume = function(volume) {
|
||||||
_elems.lastvolume = _elems.audio.volume;
|
_elems.lastvolume = _elems.audio.volume;
|
||||||
|
|
||||||
_elems.audio.volume = volume;
|
_elems.audio.volume = volume;
|
||||||
@@ -162,11 +202,11 @@ LMS.mediaplayer = function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _setReplayGain = function (replayGain) {
|
let _setReplayGain = function (replayGain) {
|
||||||
_gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20);
|
_gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
var init = function(root, defaultSettings) {
|
let init = function(root, defaultSettings) {
|
||||||
_root = root;
|
_root = root;
|
||||||
|
|
||||||
_elems.audio = document.getElementById("lms-mp-audio");
|
_elems.audio = document.getElementById("lms-mp-audio");
|
||||||
@@ -181,10 +221,6 @@ LMS.mediaplayer = function () {
|
|||||||
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
|
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
|
||||||
_elems.transcodingActive = document.getElementById("lms-transcoding-active");
|
_elems.transcodingActive = document.getElementById("lms-transcoding-active");
|
||||||
|
|
||||||
var source = _audioCtx.createMediaElementSource(_elems.audio);
|
|
||||||
source.connect(_gainNode);
|
|
||||||
_gainNode.connect(_audioCtx.destination);
|
|
||||||
|
|
||||||
_elems.playpause.addEventListener("click", function() {
|
_elems.playpause.addEventListener("click", function() {
|
||||||
_playPause();
|
_playPause();
|
||||||
});
|
});
|
||||||
@@ -196,7 +232,7 @@ LMS.mediaplayer = function () {
|
|||||||
_playNext();
|
_playNext();
|
||||||
});
|
});
|
||||||
_elems.seek.addEventListener("change", function() {
|
_elems.seek.addEventListener("change", function() {
|
||||||
_audioCtx.resume();
|
_initAudioCtx();
|
||||||
let mode = _getAudioMode();
|
let mode = _getAudioMode();
|
||||||
if (!mode)
|
if (!mode)
|
||||||
return;
|
return;
|
||||||
@@ -285,30 +321,21 @@ LMS.mediaplayer = function () {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
if ('mediaSession' in navigator) {
|
|
||||||
navigator.mediaSession.setActionHandler("previoustrack", function() {
|
|
||||||
_requestPreviousTrack();
|
|
||||||
});
|
|
||||||
navigator.mediaSession.setActionHandler("nexttrack", function() {
|
|
||||||
_requestNextTrack();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _removeAudioSources = function() {
|
let _removeAudioSources = function() {
|
||||||
while ( _elems.audio.lastElementChild) {
|
while ( _elems.audio.lastElementChild) {
|
||||||
_elems.audio.removeChild( _elems.audio.lastElementChild);
|
_elems.audio.removeChild( _elems.audio.lastElementChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _addAudioSource = function(audioSrc) {
|
let _addAudioSource = function(audioSrc) {
|
||||||
let source = document.createElement('source');
|
let source = document.createElement('source');
|
||||||
source.src = audioSrc;
|
source.src = audioSrc;
|
||||||
_elems.audio.appendChild(source);
|
_elems.audio.appendChild(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
var _getAudioMode = function() {
|
let _getAudioMode = function() {
|
||||||
if (_elems.audio.currentSrc) {
|
if (_elems.audio.currentSrc) {
|
||||||
if (_elems.audio.currentSrc.includes("format"))
|
if (_elems.audio.currentSrc.includes("format"))
|
||||||
return Mode.Transcode;
|
return Mode.Transcode;
|
||||||
@@ -319,7 +346,7 @@ LMS.mediaplayer = function () {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
var loadTrack = function(params, autoplay) {
|
let loadTrack = function(params, autoplay) {
|
||||||
_stopTimer();
|
_stopTimer();
|
||||||
_resetTimer();
|
_resetTimer();
|
||||||
|
|
||||||
@@ -343,15 +370,24 @@ LMS.mediaplayer = function () {
|
|||||||
}
|
}
|
||||||
_elems.audio.load();
|
_elems.audio.load();
|
||||||
|
|
||||||
_setReplayGain(params.replayGain);
|
|
||||||
|
|
||||||
_elems.curtime.innerHTML = _durationToString(_offset);
|
_elems.curtime.innerHTML = _durationToString(_offset);
|
||||||
_elems.duration.innerHTML = _durationToString(_duration);
|
_elems.duration.innerHTML = _durationToString(_duration);
|
||||||
|
|
||||||
|
if (!_audioIsInit) {
|
||||||
|
_pendingTrackParameters = params;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyAudioTrackParameters(params);
|
||||||
|
|
||||||
if (autoplay && _audioCtx.state == "running")
|
if (autoplay && _audioCtx.state == "running")
|
||||||
_playTrack();
|
_playTrack();
|
||||||
|
}
|
||||||
|
|
||||||
if ('mediaSession' in navigator) {
|
let _applyAudioTrackParameters = function(params)
|
||||||
|
{
|
||||||
|
_setReplayGain(params.replayGain);
|
||||||
|
if ("mediaSession" in navigator) {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: params.title,
|
title: params.title,
|
||||||
artist: params.artist,
|
artist: params.artist,
|
||||||
@@ -361,11 +397,11 @@ LMS.mediaplayer = function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var stop = function() {
|
let stop = function() {
|
||||||
_elems.audio.pause();
|
_elems.audio.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
var setSettings = function(settings) {
|
let setSettings = function(settings) {
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
|
||||||
if (typeof(Storage) !== "undefined") {
|
if (typeof(Storage) !== "undefined") {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ AudioFile::getBestStream() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
AudioFile::hasAttachedPictures(void) const
|
AudioFile::hasAttachedPictures() const
|
||||||
{
|
{
|
||||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,32 +18,48 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "TranscodeResourceHandler.hpp"
|
#include "TranscodeResourceHandler.hpp"
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
namespace Av
|
namespace Av
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::size_t
|
||||||
|
doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
|
||||||
|
{
|
||||||
|
const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::milliseconds>(inputFileParameters.duration).count()) / 1000};
|
||||||
|
return estimatedContentLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<IResourceHandler>
|
std::unique_ptr<IResourceHandler>
|
||||||
createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters)
|
createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
|
||||||
{
|
{
|
||||||
return std::make_unique<TranscodeResourceHandler>(trackPath, parameters);
|
return std::make_unique<TranscodeResourceHandler>(inputFileParameters, transcodeParameters, estimateContentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO set some nice HTTP return code
|
// TODO set some nice HTTP return code
|
||||||
|
|
||||||
TranscodeResourceHandler::TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters)
|
TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
|
||||||
: _transcoder {trackPath, parameters}
|
: _estimatedContentLength {estimateContentLength ? std::make_optional(doEstimateContentLength(inputFileParameters, transcodeParameters)) : std::nullopt}
|
||||||
|
, _transcoder {inputFileParameters, transcodeParameters}
|
||||||
{
|
{
|
||||||
|
if (_estimatedContentLength)
|
||||||
|
LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Http::ResponseContinuation*
|
Wt::Http::ResponseContinuation*
|
||||||
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||||
{
|
{
|
||||||
|
if (_estimatedContentLength)
|
||||||
|
response.setContentLength(*_estimatedContentLength);
|
||||||
response.setMimeType(_transcoder.getOutputMimeType());
|
response.setMimeType(_transcoder.getOutputMimeType());
|
||||||
|
|
||||||
if (_nbBytesReady > 0)
|
if (_bytesReadyCount > 0)
|
||||||
{
|
{
|
||||||
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _nbBytesReady);
|
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
|
||||||
_nbBytesReady = 0;
|
_bytesReadyCount = 0;
|
||||||
|
_totalServedByteCount += _bytesReadyCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_transcoder.finished())
|
if (!_transcoder.finished())
|
||||||
@@ -52,13 +68,30 @@ namespace Av
|
|||||||
continuation->waitForMoreData();
|
continuation->waitForMoreData();
|
||||||
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
||||||
{
|
{
|
||||||
assert(_nbBytesReady == 0);
|
assert(_bytesReadyCount == 0);
|
||||||
_nbBytesReady = nbBytesRead;
|
_bytesReadyCount = nbBytesRead;
|
||||||
continuation->haveMoreData();
|
continuation->haveMoreData();
|
||||||
});
|
});
|
||||||
|
|
||||||
return continuation;
|
return continuation;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// pad with 0 if necessary as duration may not be accurate
|
||||||
|
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
|
||||||
|
{
|
||||||
|
const std::size_t padSize {*_estimatedContentLength - _totalServedByteCount};
|
||||||
|
|
||||||
|
LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes";
|
||||||
|
|
||||||
|
for (std::size_t i {}; i < padSize; ++i)
|
||||||
|
response.out().put(0);
|
||||||
|
|
||||||
|
_totalServedByteCount += padSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "av/TranscodeParameters.hpp"
|
#include "av/TranscodeParameters.hpp"
|
||||||
#include "utils/IResourceHandler.hpp"
|
#include "utils/IResourceHandler.hpp"
|
||||||
@@ -28,19 +29,19 @@
|
|||||||
|
|
||||||
namespace Av
|
namespace Av
|
||||||
{
|
{
|
||||||
|
|
||||||
class TranscodeResourceHandler final : public IResourceHandler
|
class TranscodeResourceHandler final : public IResourceHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
|
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
|
||||||
|
|
||||||
static constexpr std::size_t _chunkSize {32768};
|
static constexpr std::size_t _chunkSize {32768};
|
||||||
|
std::optional<std::size_t> _estimatedContentLength;
|
||||||
std::array<std::byte, _chunkSize> _buffer;
|
std::array<std::byte, _chunkSize> _buffer;
|
||||||
std::size_t _nbBytesReady {};
|
std::size_t _bytesReadyCount {};
|
||||||
const std::filesystem::path _trackPath;
|
std::size_t _totalServedByteCount {};
|
||||||
Transcoder _transcoder;
|
Transcoder _transcoder;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
namespace Av {
|
namespace Av {
|
||||||
|
|
||||||
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - "
|
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - "
|
||||||
|
|
||||||
static std::atomic<size_t> globalId {};
|
static std::atomic<size_t> globalId {};
|
||||||
static std::filesystem::path ffmpegPath;
|
static std::filesystem::path ffmpegPath;
|
||||||
@@ -43,10 +43,10 @@ Transcoder::init()
|
|||||||
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
|
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
|
||||||
}
|
}
|
||||||
|
|
||||||
Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters)
|
Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
|
||||||
: _id {globalId++}
|
: _debugId {globalId++}
|
||||||
, _filePath {filePath}
|
, _inputFileParameters {inputFileParameters}
|
||||||
, _parameters {parameters}
|
, _transcodeParameters {transcodeParameters}
|
||||||
{
|
{
|
||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
@@ -61,17 +61,17 @@ Transcoder::start()
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!std::filesystem::exists(_filePath))
|
if (!std::filesystem::exists(_inputFileParameters.trackPath))
|
||||||
throw Exception {"File '" + _filePath.string() + "' does not exist!"};
|
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"};
|
||||||
else if (!std::filesystem::is_regular_file( _filePath) )
|
else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) )
|
||||||
throw Exception {"File '" + _filePath.string() + "' is not regular!"};
|
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"};
|
||||||
}
|
}
|
||||||
catch (const std::filesystem::filesystem_error& e)
|
catch (const std::filesystem::filesystem_error& e)
|
||||||
{
|
{
|
||||||
throw Exception {"File error '" + _filePath.string() + "': " + e.what()};
|
throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()};
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(INFO) << "Transcoding file '" << _filePath.string() << "'";
|
LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'";
|
||||||
|
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args;
|
||||||
|
|
||||||
@@ -90,22 +90,22 @@ Transcoder::start()
|
|||||||
args.emplace_back("-ss");
|
args.emplace_back("-ss");
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_parameters.offset.count() / float {1000});
|
oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000});
|
||||||
args.emplace_back(oss.str());
|
args.emplace_back(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input file
|
// Input file
|
||||||
args.emplace_back("-i");
|
args.emplace_back("-i");
|
||||||
args.emplace_back(_filePath.string());
|
args.emplace_back(_inputFileParameters.trackPath.string());
|
||||||
|
|
||||||
// Stream mapping, if set
|
// Stream mapping, if set
|
||||||
if (_parameters.stream)
|
if (_transcodeParameters.stream)
|
||||||
{
|
{
|
||||||
args.emplace_back("-map");
|
args.emplace_back("-map");
|
||||||
args.emplace_back("0:" + std::to_string(*_parameters.stream));
|
args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_parameters.stripMetadata)
|
if (_transcodeParameters.stripMetadata)
|
||||||
{
|
{
|
||||||
// Strip metadata
|
// Strip metadata
|
||||||
args.emplace_back("-map_metadata");
|
args.emplace_back("-map_metadata");
|
||||||
@@ -117,10 +117,10 @@ Transcoder::start()
|
|||||||
|
|
||||||
// Output bitrates
|
// Output bitrates
|
||||||
args.emplace_back("-b:a");
|
args.emplace_back("-b:a");
|
||||||
args.emplace_back(std::to_string(_parameters.bitrate));
|
args.emplace_back(std::to_string(_transcodeParameters.bitrate));
|
||||||
|
|
||||||
// Codecs and formats
|
// Codecs and formats
|
||||||
switch (_parameters.format)
|
switch (_transcodeParameters.format)
|
||||||
{
|
{
|
||||||
case Format::MP3:
|
case Format::MP3:
|
||||||
args.emplace_back("-f");
|
args.emplace_back("-f");
|
||||||
@@ -156,10 +156,10 @@ Transcoder::start()
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_parameters.format)) + ")"};
|
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_transcodeParameters.format)) + ")"};
|
||||||
}
|
}
|
||||||
|
|
||||||
_outputMimeType = formatToMimetype(_parameters.format);
|
_outputMimeType = formatToMimetype(_transcodeParameters.format);
|
||||||
|
|
||||||
args.emplace_back("pipe:1");
|
args.emplace_back("pipe:1");
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace Av
|
|||||||
class Transcoder
|
class Transcoder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters);
|
Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters);
|
||||||
~Transcoder();
|
~Transcoder();
|
||||||
|
|
||||||
Transcoder(const Transcoder&) = delete;
|
Transcoder(const Transcoder&) = delete;
|
||||||
@@ -49,7 +49,7 @@ namespace Av
|
|||||||
std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
|
std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
|
||||||
const std::string& getOutputMimeType() const { return _outputMimeType; }
|
const std::string& getOutputMimeType() const { return _outputMimeType; }
|
||||||
const TranscodeParameters& getParameters() const { return _parameters; }
|
const TranscodeParameters& getParameters() const { return _transcodeParameters; }
|
||||||
|
|
||||||
bool finished() const;
|
bool finished() const;
|
||||||
|
|
||||||
@@ -58,9 +58,9 @@ namespace Av
|
|||||||
|
|
||||||
void start();
|
void start();
|
||||||
|
|
||||||
const std::size_t _id {};
|
const std::size_t _debugId {};
|
||||||
const std::filesystem::path _filePath;
|
const InputFileParameters _inputFileParameters;
|
||||||
const TranscodeParameters _parameters;
|
const TranscodeParameters _transcodeParameters;
|
||||||
|
|
||||||
std::unique_ptr<IChildProcess> _childProcess;
|
std::unique_ptr<IChildProcess> _childProcess;
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include "Types.hpp"
|
#include "Types.hpp"
|
||||||
|
|
||||||
namespace Av
|
namespace Av
|
||||||
{
|
{
|
||||||
|
struct InputFileParameters
|
||||||
|
{
|
||||||
|
std::filesystem::path trackPath;
|
||||||
|
std::chrono::milliseconds duration;
|
||||||
|
};
|
||||||
|
|
||||||
struct TranscodeParameters
|
struct TranscodeParameters
|
||||||
{
|
{
|
||||||
Format format;
|
Format format;
|
||||||
|
|||||||
@@ -19,15 +19,15 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "utils/IResourceHandler.hpp"
|
#include "utils/IResourceHandler.hpp"
|
||||||
|
|
||||||
namespace Av
|
namespace Av
|
||||||
{
|
{
|
||||||
|
struct InputFileParameters;
|
||||||
struct TranscodeParameters;
|
struct TranscodeParameters;
|
||||||
|
|
||||||
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ endif()
|
|||||||
|
|
||||||
add_library(lmsmetadata SHARED
|
add_library(lmsmetadata SHARED
|
||||||
impl/AvFormatParser.cpp
|
impl/AvFormatParser.cpp
|
||||||
|
impl/Factory.cpp
|
||||||
impl/TagLibParser.cpp
|
impl/TagLibParser.cpp
|
||||||
impl/Utils.cpp
|
impl/Utils.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "metadata/AvFormatParser.hpp"
|
#include "AvFormatParser.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "metadata/IParser.hpp"
|
||||||
|
|
||||||
|
#include "utils/Exception.hpp"
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
|
#include "AvFormatParser.hpp"
|
||||||
|
#include "TagLibParser.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace MetaData
|
||||||
|
{
|
||||||
|
std::unique_ptr<IParser>
|
||||||
|
createParser(ParserType parserType, ParserReadStyle parserReadStyle)
|
||||||
|
{
|
||||||
|
switch (parserType)
|
||||||
|
{
|
||||||
|
case ParserType::TagLib:
|
||||||
|
LMS_LOG(METADATA, INFO) << "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle);
|
||||||
|
return std::make_unique<TagLibParser>(parserReadStyle);
|
||||||
|
case ParserType::AvFormat:
|
||||||
|
LMS_LOG(METADATA, INFO) << "Creating AvFormat parser";
|
||||||
|
return std::make_unique<AvFormatParser>();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw LmsException {"Unhandled parser type"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "metadata/TagLibParser.hpp"
|
#include "TagLibParser.hpp"
|
||||||
|
|
||||||
#include <taglib/apetag.h>
|
#include <taglib/apetag.h>
|
||||||
#include <taglib/asffile.h>
|
#include <taglib/asffile.h>
|
||||||
@@ -33,7 +33,10 @@
|
|||||||
#include <taglib/vorbisfile.h>
|
#include <taglib/vorbisfile.h>
|
||||||
#include <taglib/wavpackfile.h>
|
#include <taglib/wavpackfile.h>
|
||||||
|
|
||||||
|
#include "utils/IConfig.hpp"
|
||||||
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
@@ -145,6 +148,26 @@ getAlbum(const TagLib::PropertyMap& properties)
|
|||||||
return Album {std::move(albumName.front()), albumMBID.front()};
|
return Album {std::move(albumName.front()), albumMBID.front()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
TagLib::AudioProperties::ReadStyle
|
||||||
|
readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||||
|
{
|
||||||
|
switch (readStyle)
|
||||||
|
{
|
||||||
|
case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
|
||||||
|
case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
|
||||||
|
case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw LmsException {"Cannot convert read style"};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TagLibParser::TagLibParser(ParserReadStyle readStyle)
|
||||||
|
: _readStyle {readStyleToTagLibReadStyle(readStyle)}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug)
|
TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug)
|
||||||
{
|
{
|
||||||
@@ -267,7 +290,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
{
|
{
|
||||||
TagLib::FileRef f {p.string().c_str(),
|
TagLib::FileRef f {p.string().c_str(),
|
||||||
true, // read audio properties
|
true, // read audio properties
|
||||||
TagLib::AudioProperties::Fast}; // TODO parametrize this
|
_readStyle};
|
||||||
|
|
||||||
if (f.isNull())
|
if (f.isNull())
|
||||||
{
|
{
|
||||||
@@ -286,7 +309,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
{
|
{
|
||||||
const TagLib::AudioProperties *properties {f.audioProperties() };
|
const TagLib::AudioProperties *properties {f.audioProperties() };
|
||||||
|
|
||||||
track.duration = std::chrono::milliseconds {properties->length() * 1000};
|
track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()};
|
||||||
|
|
||||||
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
|
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
|
||||||
track.audioStreams = {std::move(audioStream)};
|
track.audioStreams = {std::move(audioStream)};
|
||||||
|
|||||||
+6
-1
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <taglib/audioproperties.h>
|
||||||
#include "metadata/IParser.hpp"
|
#include "metadata/IParser.hpp"
|
||||||
|
|
||||||
namespace TagLib
|
namespace TagLib
|
||||||
@@ -32,10 +33,14 @@ namespace MetaData
|
|||||||
// Parse that makes use of AvFormat
|
// Parse that makes use of AvFormat
|
||||||
class TagLibParser : public IParser
|
class TagLibParser : public IParser
|
||||||
{
|
{
|
||||||
|
public:
|
||||||
|
TagLibParser(ParserReadStyle readStyle);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
|
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
|
||||||
|
|
||||||
void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug);
|
void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug);
|
||||||
|
|
||||||
|
const TagLib::AudioProperties::ReadStyle _readStyle;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace MetaData
|
} // namespace MetaData
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "utils/Exception.hpp"
|
||||||
|
|
||||||
namespace MetaData::Utils
|
namespace MetaData::Utils
|
||||||
{
|
{
|
||||||
Wt::WDate
|
Wt::WDate
|
||||||
@@ -56,5 +58,18 @@ namespace MetaData::Utils
|
|||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string_view
|
||||||
|
readStyleToString(ParserReadStyle readStyle)
|
||||||
|
{
|
||||||
|
switch (readStyle)
|
||||||
|
{
|
||||||
|
case ParserReadStyle::Fast: return "fast";
|
||||||
|
case ParserReadStyle::Average: return "average";
|
||||||
|
case ParserReadStyle::Accurate: return "accurate";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw LmsException {"Unknown read style"};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,11 @@
|
|||||||
|
|
||||||
#include <Wt/WDate.h>
|
#include <Wt/WDate.h>
|
||||||
|
|
||||||
|
#include "metadata/IParser.hpp"
|
||||||
|
|
||||||
namespace MetaData::Utils
|
namespace MetaData::Utils
|
||||||
{
|
{
|
||||||
Wt::WDate parseDate(const std::string& dateStr);
|
Wt::WDate parseDate(const std::string& dateStr);
|
||||||
|
std::string_view readStyleToString(ParserReadStyle readStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,5 +100,18 @@ namespace MetaData
|
|||||||
std::set<std::string> _clusterTypeNames;
|
std::set<std::string> _clusterTypeNames;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class ParserType
|
||||||
|
{
|
||||||
|
TagLib,
|
||||||
|
AvFormat,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ParserReadStyle
|
||||||
|
{
|
||||||
|
Fast,
|
||||||
|
Average,
|
||||||
|
Accurate,
|
||||||
|
};
|
||||||
|
std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
|
||||||
} // namespace MetaData
|
} // namespace MetaData
|
||||||
|
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::si
|
|||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
|
||||||
|
|
||||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||||
for (const Cluster::pointer& cluster : queryRes)
|
for (Cluster::pointer cluster : queryRes)
|
||||||
{
|
{
|
||||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||||
|
|||||||
@@ -591,6 +591,15 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
|||||||
session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
|
session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
void
|
||||||
|
migrateFromV36(Session& session)
|
||||||
|
{
|
||||||
|
// Increased precision for track durations (now in milliseconds instead of secodns)
|
||||||
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
|
ScanSettings::get(session).modify()->incScanVersion();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
doDbMigration(Session& session)
|
doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
@@ -633,6 +642,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
|||||||
{33, migrateFromV33},
|
{33, migrateFromV33},
|
||||||
{34, migrateFromV34},
|
{34, migrateFromV34},
|
||||||
{35, migrateFromV35},
|
{35, migrateFromV35},
|
||||||
|
{36, migrateFromV36},
|
||||||
};
|
};
|
||||||
|
|
||||||
while (1)
|
while (1)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace Database
|
|||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
using Version = std::size_t;
|
using Version = std::size_t;
|
||||||
static constexpr Version LMS_DATABASE_VERSION {36};
|
static constexpr Version LMS_DATABASE_VERSION {37};
|
||||||
class VersionInfo
|
class VersionInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ Release::find(Session& session, const FindParameters& params)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::size_t>
|
std::optional<std::size_t>
|
||||||
Release::getTotalTrack(void) const
|
Release::getTotalTrack() const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ Release::getTotalTrack(void) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::size_t>
|
std::optional<std::size_t>
|
||||||
Release::getTotalDisc(void) const
|
Release::getTotalDisc() const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -287,6 +287,18 @@ Release::getTotalDisc(void) const
|
|||||||
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
|
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Release::getDiscCount() const
|
||||||
|
{
|
||||||
|
assert(session());
|
||||||
|
int res {session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||||
|
.join("release r ON r.id = t.release_id")
|
||||||
|
.where("r.id = ?")
|
||||||
|
.bind(getId())};
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<int>
|
std::optional<int>
|
||||||
Release::getReleaseYear(bool original) const
|
Release::getReleaseYear(bool original) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ WhereClause::Or(const WhereClause& otherClause)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
WhereClause::get(void) const
|
WhereClause::get() const
|
||||||
{
|
{
|
||||||
if (!_clause.empty())
|
if (!_clause.empty())
|
||||||
return "WHERE " + _clause;
|
return "WHERE " + _clause;
|
||||||
@@ -176,7 +176,7 @@ FromClause::get() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
SqlQuery::get(void) const
|
SqlQuery::get() const
|
||||||
{
|
{
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class WhereClause
|
|||||||
WhereClause& bind(const std::string& arg);
|
WhereClause& bind(const std::string& arg);
|
||||||
|
|
||||||
std::string get() const;
|
std::string get() const;
|
||||||
const std::list<std::string>& getBindArgs(void) const {return _bindArgs;}
|
const std::list<std::string>& getBindArgs() const {return _bindArgs;}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string _clause; // WHERE clause
|
std::string _clause; // WHERE clause
|
||||||
@@ -101,15 +101,15 @@ class FromClause
|
|||||||
class SqlQuery
|
class SqlQuery
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SelectStatement& select(void) { return _selectStatement;}
|
SelectStatement& select() { return _selectStatement;}
|
||||||
SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; }
|
SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; }
|
||||||
FromClause& from(void) { return _fromClause; }
|
FromClause& from() { return _fromClause; }
|
||||||
FromClause& from(const std::string& clause) { _whereClause = WhereClause(clause); return _fromClause; }
|
FromClause& from(const std::string& clause) { _whereClause = WhereClause(clause); return _fromClause; }
|
||||||
InnerJoinClause& innerJoin(void) { return _innerJoinClause; }
|
InnerJoinClause& innerJoin() { return _innerJoinClause; }
|
||||||
WhereClause& where(void) { return _whereClause; }
|
WhereClause& where() { return _whereClause; }
|
||||||
const WhereClause& where(void) const { return _whereClause; }
|
const WhereClause& where() const { return _whereClause; }
|
||||||
GroupByStatement& groupBy(void) { return _groupByStatement; }
|
GroupByStatement& groupBy() { return _groupByStatement; }
|
||||||
const GroupByStatement& groupBy(void) const { return _groupByStatement; }
|
const GroupByStatement& groupBy() const { return _groupByStatement; }
|
||||||
|
|
||||||
std::string get() const;
|
std::string get() const;
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class Release : public Object<Release, ReleaseId>
|
|||||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||||
std::optional<std::size_t> getTotalTrack() const;
|
std::optional<std::size_t> getTotalTrack() const;
|
||||||
std::optional<std::size_t> getTotalDisc() const;
|
std::optional<std::size_t> getTotalDisc() const;
|
||||||
|
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||||
std::chrono::milliseconds getDuration() const;
|
std::chrono::milliseconds getDuration() const;
|
||||||
Wt::WDateTime getLastWritten() const;
|
Wt::WDateTime getLastWritten() const;
|
||||||
|
|
||||||
|
|||||||
@@ -207,5 +207,3 @@ TEST_F(DatabaseFixture, Common_subRange)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -464,3 +464,47 @@ TEST_F(DatabaseFixture, Release_artist)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Release_getDiscCount)
|
||||||
|
{
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
track.get().modify()->setDiscNumber(5);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
track2.get().modify()->setRelease(release.get());
|
||||||
|
track2.get().modify()->setDiscNumber(5);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
track2.get().modify()->setDiscNumber(6);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(release.get()->getDiscCount(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "services/database/TrackFeatures.hpp"
|
#include "services/database/TrackFeatures.hpp"
|
||||||
#include "metadata/TagLibParser.hpp"
|
#include "metadata/IParser.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
@@ -255,16 +255,29 @@ createScannerService(Db& db, Recommendation::IRecommendationService& recommendat
|
|||||||
return std::make_unique<ScannerService>(db, recommendationService);
|
return std::make_unique<ScannerService>(db, recommendationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MetaData::ParserReadStyle
|
||||||
|
getParserReadStyle()
|
||||||
|
{
|
||||||
|
std::string_view readStyle {Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate")};
|
||||||
|
|
||||||
|
if (readStyle == "fast")
|
||||||
|
return MetaData::ParserReadStyle::Fast;
|
||||||
|
else if (readStyle == "average")
|
||||||
|
return MetaData::ParserReadStyle::Average;
|
||||||
|
else if (readStyle == "accurate")
|
||||||
|
return MetaData::ParserReadStyle::Accurate;
|
||||||
|
|
||||||
|
throw LmsException {"Invalid value for 'scanner-parser-read-style'"};
|
||||||
|
}
|
||||||
|
|
||||||
ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||||
: _recommendationService {recommendationService}
|
: _recommendationService {recommendationService}
|
||||||
, _skipDuplicateRecordingMBID {Service<IConfig>::get()->getBool("scanner-skip-duplicate-recording-mbid", false)}
|
, _skipDuplicateRecordingMBID {Service<IConfig>::get()->getBool("scanner-skip-duplicate-recording-mbid", false)}
|
||||||
, _dbSession {db}
|
, _dbSession {db}
|
||||||
|
, _metadataParser {MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle())} // For now, always use TagLib
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "skipDuplicateRecordingMBID = " << _skipDuplicateRecordingMBID;
|
LMS_LOG(DBUPDATER, INFO) << "skipDuplicateRecordingMBID = " << _skipDuplicateRecordingMBID;
|
||||||
|
|
||||||
// For now, always use TagLib
|
|
||||||
_metadataParser = std::make_unique<MetaData::TagLibParser>();
|
|
||||||
|
|
||||||
_ioService.setThreadCount(1);
|
_ioService.setThreadCount(1);
|
||||||
|
|
||||||
refreshScanSettings();
|
refreshScanSettings();
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ userTranscodeFormatToAvFormat(AudioFormat format)
|
|||||||
|
|
||||||
struct StreamParameters
|
struct StreamParameters
|
||||||
{
|
{
|
||||||
std::filesystem::path trackPath;
|
Av::InputFileParameters inputFileParameters;
|
||||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||||
|
bool estimateContentLength {};
|
||||||
};
|
};
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -68,9 +69,12 @@ getStreamParameters(RequestContext& context)
|
|||||||
// Optional params
|
// Optional params
|
||||||
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
|
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
|
||||||
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
|
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
|
||||||
|
bool estimateContentLength {getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false)};
|
||||||
|
|
||||||
StreamParameters parameters;
|
StreamParameters parameters;
|
||||||
|
|
||||||
|
parameters.estimateContentLength = estimateContentLength;
|
||||||
|
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
auto transaction {context.dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -78,7 +82,8 @@ getStreamParameters(RequestContext& context)
|
|||||||
if (!track)
|
if (!track)
|
||||||
throw RequestedDataNotFoundError {};
|
throw RequestedDataNotFoundError {};
|
||||||
|
|
||||||
parameters.trackPath = track->getPath();
|
parameters.inputFileParameters.trackPath = track->getPath();
|
||||||
|
parameters.inputFileParameters.duration = track->getDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -155,9 +160,9 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http
|
|||||||
{
|
{
|
||||||
StreamParameters streamParameters {getStreamParameters(context)};
|
StreamParameters streamParameters {getStreamParameters(context)};
|
||||||
if (streamParameters.transcodeParameters)
|
if (streamParameters.transcodeParameters)
|
||||||
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.trackPath, *streamParameters.transcodeParameters);
|
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
||||||
else
|
else
|
||||||
resourceHandler = createFileResourceHandler(streamParameters.trackPath);
|
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -136,17 +136,4 @@ namespace StringUtils
|
|||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
|
||||||
std::optional<bool>
|
|
||||||
readAs(std::string_view str)
|
|
||||||
{
|
|
||||||
if (str == "true")
|
|
||||||
return true;
|
|
||||||
else if (str == "false")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,5 @@ namespace StringUtils
|
|||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackListId>
|
std::optional<Database::TrackListId>
|
||||||
readAs(std::string_view str);
|
readAs(std::string_view str);
|
||||||
|
|
||||||
template<>
|
|
||||||
std::optional<bool>
|
|
||||||
readAs(std::string_view str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -872,6 +872,29 @@ handleGetAlbumRequest(RequestContext& context)
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
Response
|
||||||
|
handleGetSongRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
TrackId id {getMandatoryParameterAs<TrackId>(context.parameters, "id")};
|
||||||
|
|
||||||
|
auto transaction {context.dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
|
const Track::pointer track {Track::find(context.dbSession, id)};
|
||||||
|
if (!track)
|
||||||
|
throw RequestedDataNotFoundError {};
|
||||||
|
|
||||||
|
User::pointer user {User::find(context.dbSession, context.userId)};
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
|
response.addNode("song", trackToResponseNode(track, context.dbSession, user));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
Response
|
Response
|
||||||
handleGetArtistRequest(RequestContext& context)
|
handleGetArtistRequest(RequestContext& context)
|
||||||
@@ -1835,7 +1858,7 @@ static const std::unordered_map<std::string, RequestEntryPointInfo> requestEntry
|
|||||||
{"/getArtists", {handleGetArtistsRequest}},
|
{"/getArtists", {handleGetArtistsRequest}},
|
||||||
{"/getArtist", {handleGetArtistRequest}},
|
{"/getArtist", {handleGetArtistRequest}},
|
||||||
{"/getAlbum", {handleGetAlbumRequest}},
|
{"/getAlbum", {handleGetAlbumRequest}},
|
||||||
{"/getSong", {handleNotImplemented}},
|
{"/getSong", {handleGetSongRequest}},
|
||||||
{"/getVideos", {handleNotImplemented}},
|
{"/getVideos", {handleNotImplemented}},
|
||||||
{"/getArtistInfo", {handleGetArtistInfoRequest}},
|
{"/getArtistInfo", {handleGetArtistInfoRequest}},
|
||||||
{"/getArtistInfo2", {handleGetArtistInfo2Request}},
|
{"/getArtistInfo2", {handleGetArtistInfo2Request}},
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ Response::createOkResponse(ProtocolVersion protocolVersion)
|
|||||||
|
|
||||||
responseNode.setAttribute("status", "ok");
|
responseNode.setAttribute("status", "ok");
|
||||||
responseNode.setVersionAttribute(protocolVersion);
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
|
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -129,6 +130,7 @@ Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& err
|
|||||||
|
|
||||||
responseNode.setAttribute("status", "failed");
|
responseNode.setAttribute("status", "failed");
|
||||||
responseNode.setVersionAttribute(protocolVersion);
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
|
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||||
|
|
||||||
Node& errorNode {responseNode.createChild("error")};
|
Node& errorNode {responseNode.createChild("error")};
|
||||||
errorNode.setAttribute("code", std::to_string(static_cast<int>(error.getCode())));
|
errorNode.setAttribute("code", std::to_string(static_cast<int>(error.getCode())));
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ ChildProcess::wait(bool block)
|
|||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
|
ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
|
||||||
{
|
{
|
||||||
assert(!finished());
|
assert(!finished());
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ readAs(std::string_view str)
|
|||||||
return std::string {str};
|
return std::string {str};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
std::optional<bool>
|
||||||
|
readAs(std::string_view str)
|
||||||
|
{
|
||||||
|
if (str == "1" || str == "true")
|
||||||
|
return true;
|
||||||
|
else if (str == "0" || str == "false")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string>
|
std::vector<std::string>
|
||||||
splitStringCopy(std::string_view string, std::string_view separators)
|
splitStringCopy(std::string_view string, std::string_view separators)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ template<>
|
|||||||
std::optional<std::string>
|
std::optional<std::string>
|
||||||
readAs(std::string_view str);
|
readAs(std::string_view str);
|
||||||
|
|
||||||
|
template<>
|
||||||
|
[[nodiscard]]
|
||||||
|
std::optional<bool>
|
||||||
|
readAs(std::string_view str);
|
||||||
|
|
||||||
[[nodiscard]]
|
[[nodiscard]]
|
||||||
std::string
|
std::string
|
||||||
replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||||
|
|||||||
@@ -109,3 +109,12 @@ TEST(StringUtils, escapeString)
|
|||||||
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
|
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(StringUtils, readAs)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<bool>("false"), false);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<bool>("0"), false);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ add_executable(lms
|
|||||||
ui/explore/DatabaseCollectorBase.cpp
|
ui/explore/DatabaseCollectorBase.cpp
|
||||||
ui/explore/Explore.cpp
|
ui/explore/Explore.cpp
|
||||||
ui/explore/Filters.cpp
|
ui/explore/Filters.cpp
|
||||||
|
ui/explore/PlayQueueController.cpp
|
||||||
ui/explore/ReleaseCollector.cpp
|
ui/explore/ReleaseCollector.cpp
|
||||||
ui/explore/ReleaseListHelpers.cpp
|
ui/explore/ReleaseListHelpers.cpp
|
||||||
ui/explore/ReleasesView.cpp
|
ui/explore/ReleasesView.cpp
|
||||||
|
|||||||
@@ -64,6 +64,48 @@
|
|||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
|
static
|
||||||
|
std::shared_ptr<Wt::WMessageResourceBundle>
|
||||||
|
createMessageResourceBundle()
|
||||||
|
{
|
||||||
|
const std::string appRoot {Wt::WApplication::appRoot()};
|
||||||
|
|
||||||
|
auto res {std::make_shared<Wt::WMessageResourceBundle>()};
|
||||||
|
res->use(appRoot + "admin-database");
|
||||||
|
res->use(appRoot + "admin-initwizard");
|
||||||
|
res->use(appRoot + "admin-scannercontroller");
|
||||||
|
res->use(appRoot + "admin-user");
|
||||||
|
res->use(appRoot + "admin-users");
|
||||||
|
res->use(appRoot + "artist");
|
||||||
|
res->use(appRoot + "artists");
|
||||||
|
res->use(appRoot + "error");
|
||||||
|
res->use(appRoot + "explore");
|
||||||
|
res->use(appRoot + "login");
|
||||||
|
res->use(appRoot + "main");
|
||||||
|
res->use(appRoot + "mediaplayer");
|
||||||
|
res->use(appRoot + "messages");
|
||||||
|
res->use(appRoot + "misc");
|
||||||
|
res->use(appRoot + "notifications");
|
||||||
|
res->use(appRoot + "playqueue");
|
||||||
|
res->use(appRoot + "release");
|
||||||
|
res->use(appRoot + "releases");
|
||||||
|
res->use(appRoot + "search");
|
||||||
|
res->use(appRoot + "settings");
|
||||||
|
res->use(appRoot + "tracklist");
|
||||||
|
res->use(appRoot + "tracklists");
|
||||||
|
res->use(appRoot + "tracks");
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::shared_ptr<Wt::WMessageResourceBundle>
|
||||||
|
getOrCreateMessageBundle()
|
||||||
|
{
|
||||||
|
static std::shared_ptr<Wt::WMessageResourceBundle> res {createMessageResourceBundle()};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
static constexpr const char* defaultPath {"/releases"};
|
static constexpr const char* defaultPath {"/releases"};
|
||||||
|
|
||||||
std::unique_ptr<Wt::WApplication>
|
std::unique_ptr<Wt::WApplication>
|
||||||
@@ -169,32 +211,8 @@ LmsApplication::init()
|
|||||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||||
require("js/mediaplayer.js");
|
require("js/mediaplayer.js");
|
||||||
|
|
||||||
setTitle("LMS");
|
setTitle();
|
||||||
|
setLocalizedStrings(getOrCreateMessageBundle());
|
||||||
// Add a resource bundle
|
|
||||||
messageResourceBundle().use(appRoot() + "admin-database");
|
|
||||||
messageResourceBundle().use(appRoot() + "admin-initwizard");
|
|
||||||
messageResourceBundle().use(appRoot() + "admin-scannercontroller");
|
|
||||||
messageResourceBundle().use(appRoot() + "admin-user");
|
|
||||||
messageResourceBundle().use(appRoot() + "admin-users");
|
|
||||||
messageResourceBundle().use(appRoot() + "artist");
|
|
||||||
messageResourceBundle().use(appRoot() + "artists");
|
|
||||||
messageResourceBundle().use(appRoot() + "error");
|
|
||||||
messageResourceBundle().use(appRoot() + "explore");
|
|
||||||
messageResourceBundle().use(appRoot() + "login");
|
|
||||||
messageResourceBundle().use(appRoot() + "main");
|
|
||||||
messageResourceBundle().use(appRoot() + "mediaplayer");
|
|
||||||
messageResourceBundle().use(appRoot() + "messages");
|
|
||||||
messageResourceBundle().use(appRoot() + "misc");
|
|
||||||
messageResourceBundle().use(appRoot() + "notifications");
|
|
||||||
messageResourceBundle().use(appRoot() + "playqueue");
|
|
||||||
messageResourceBundle().use(appRoot() + "release");
|
|
||||||
messageResourceBundle().use(appRoot() + "releases");
|
|
||||||
messageResourceBundle().use(appRoot() + "search");
|
|
||||||
messageResourceBundle().use(appRoot() + "settings");
|
|
||||||
messageResourceBundle().use(appRoot() + "tracklist");
|
|
||||||
messageResourceBundle().use(appRoot() + "tracklists");
|
|
||||||
messageResourceBundle().use(appRoot() + "tracks");
|
|
||||||
|
|
||||||
// Handle Media Scanner events and other session events
|
// Handle Media Scanner events and other session events
|
||||||
enableUpdates(true);
|
enableUpdates(true);
|
||||||
@@ -360,21 +378,22 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
|||||||
std::string path;
|
std::string path;
|
||||||
int index;
|
int index;
|
||||||
bool admin;
|
bool admin;
|
||||||
|
std::optional<Wt::WString> title;
|
||||||
} views[] =
|
} views[] =
|
||||||
{
|
{
|
||||||
{ "/artists", IdxExplore, false },
|
{ "/artists", IdxExplore, false, Wt::WString::tr("Lms.Explore.artists") },
|
||||||
{ "/artist", IdxExplore, false },
|
{ "/artist", IdxExplore, false, std::nullopt },
|
||||||
{ "/releases", IdxExplore, false },
|
{ "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") },
|
||||||
{ "/release", IdxExplore, false },
|
{ "/release", IdxExplore, false, std::nullopt },
|
||||||
{ "/search", IdxExplore, false },
|
{ "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") },
|
||||||
{ "/tracks", IdxExplore, false },
|
{ "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") },
|
||||||
{ "/tracklists", IdxExplore, false },
|
{ "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") },
|
||||||
{ "/tracklist", IdxExplore, false },
|
{ "/tracklist", IdxExplore, false, std::nullopt },
|
||||||
{ "/playqueue", IdxPlayQueue, false },
|
{ "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") },
|
||||||
{ "/settings", IdxSettings, false },
|
{ "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") },
|
||||||
{ "/admin/database", IdxAdminDatabase, true },
|
{ "/admin/database", IdxAdminDatabase, true, Wt::WString::tr("Lms.Admin.Database.database") },
|
||||||
{ "/admin/users", IdxAdminUsers, true },
|
{ "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") },
|
||||||
{ "/admin/user", IdxAdminUser, true },
|
{ "/admin/user", IdxAdminUser, true, std::nullopt },
|
||||||
};
|
};
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
||||||
@@ -388,6 +407,8 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
stack.setCurrentIndex(view.index);
|
stack.setCurrentIndex(view.index);
|
||||||
|
if (view.title)
|
||||||
|
LmsApp->setTitle(*view.title);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,8 +502,9 @@ LmsApplication::createHome()
|
|||||||
Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")};
|
Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")};
|
||||||
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||||
|
|
||||||
Explore* explore {mainStack->addNew<Explore>(filters)};
|
std::unique_ptr<PlayQueue> playQueue {std::make_unique<PlayQueue>()};
|
||||||
_playQueue = mainStack->addNew<PlayQueue>();
|
Explore* explore {mainStack->addNew<Explore>(*filters, *playQueue)};
|
||||||
|
_playQueue = mainStack->addWidget(std::move(playQueue));
|
||||||
mainStack->addNew<SettingsView>();
|
mainStack->addNew<SettingsView>();
|
||||||
|
|
||||||
searchEdit->enterPressed().connect([=]
|
searchEdit->enterPressed().connect([=]
|
||||||
@@ -504,11 +526,7 @@ LmsApplication::createHome()
|
|||||||
mainStack->addNew<UserView>();
|
mainStack->addNew<UserView>();
|
||||||
}
|
}
|
||||||
|
|
||||||
explore->setMaxTrackCountForAction(_playQueue->getCapacity());
|
explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity());
|
||||||
explore->tracksAction.connect([this] (PlayQueueAction action, const std::vector<Database::TrackId>& trackIds)
|
|
||||||
{
|
|
||||||
_playQueue->processTracks(action, trackIds);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Events from MediaPlayer
|
// Events from MediaPlayer
|
||||||
_mediaPlayer->playNext.connect([this]
|
_mediaPlayer->playNext.connect([this]
|
||||||
@@ -599,6 +617,15 @@ LmsApplication::post(std::function<void()> func)
|
|||||||
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
|
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LmsApplication::setTitle(const Wt::WString& title)
|
||||||
|
{
|
||||||
|
if (title.empty())
|
||||||
|
WApplication::setTitle("LMS");
|
||||||
|
else
|
||||||
|
WApplication::setTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class LmsApplication : public Wt::WApplication
|
|||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
void post(std::function<void()> func);
|
void post(std::function<void()> func);
|
||||||
|
void setTitle(const Wt::WString& title = "");
|
||||||
|
|
||||||
// Used to classify the message sent to the user
|
// Used to classify the message sent to the user
|
||||||
void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000});
|
void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000});
|
||||||
|
|||||||
+34
-26
@@ -416,38 +416,46 @@ PlayQueue::enqueueTracks(const std::vector<Database::TrackId>& trackIds)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::TrackId>& trackIds)
|
PlayQueue::play(const std::vector<Database::TrackId>& trackIds)
|
||||||
{
|
{
|
||||||
std::size_t nbAddedTracks {};
|
playAtIndex(trackIds, 0);
|
||||||
|
}
|
||||||
|
|
||||||
switch (action)
|
void
|
||||||
{
|
PlayQueue::playShuffled(const std::vector<Database::TrackId>& trackIds)
|
||||||
case PlayQueueAction::PlayLast:
|
{
|
||||||
nbAddedTracks = enqueueTracks(trackIds);
|
clearTracks();
|
||||||
if (!_trackPos)
|
std::vector<Database::TrackId> shuffledTrackIds {trackIds};
|
||||||
loadTrack(0, true);
|
Random::shuffleContainer(shuffledTrackIds);
|
||||||
break;
|
const std::size_t nbAddedTracks {enqueueTracks(shuffledTrackIds)};
|
||||||
|
loadTrack(0, true);
|
||||||
|
|
||||||
case PlayQueueAction::Play:
|
notifyAddedTracks(nbAddedTracks);
|
||||||
clearTracks();
|
}
|
||||||
nbAddedTracks = enqueueTracks(trackIds);
|
|
||||||
loadTrack(0, true);
|
|
||||||
|
|
||||||
break;
|
void
|
||||||
|
PlayQueue::playOrAddLast(const std::vector<Database::TrackId>& trackIds)
|
||||||
|
{
|
||||||
|
const std::size_t nbAddedTracks {enqueueTracks(trackIds)};
|
||||||
|
if (!_trackPos)
|
||||||
|
loadTrack(0, true);
|
||||||
|
|
||||||
case PlayQueueAction::PlayShuffled:
|
notifyAddedTracks(nbAddedTracks);
|
||||||
{
|
}
|
||||||
clearTracks();
|
|
||||||
{
|
|
||||||
std::vector<Database::TrackId> shuffledTrackIds {trackIds};
|
|
||||||
Random::shuffleContainer(shuffledTrackIds);
|
|
||||||
nbAddedTracks = enqueueTracks(shuffledTrackIds);
|
|
||||||
}
|
|
||||||
loadTrack(0, true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PlayQueue::playAtIndex(const std::vector<Database::TrackId>& trackIds, std::size_t index)
|
||||||
|
{
|
||||||
|
clearTracks();
|
||||||
|
const std::size_t nbAddedTracks {enqueueTracks(trackIds)};
|
||||||
|
loadTrack(index, true);
|
||||||
|
|
||||||
|
notifyAddedTracks(nbAddedTracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlayQueue::notifyAddedTracks(std::size_t nbAddedTracks) const
|
||||||
|
{
|
||||||
if (nbAddedTracks > 0)
|
if (nbAddedTracks > 0)
|
||||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::seconds {2});
|
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::seconds {2});
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
#include <Wt/WText.h>
|
#include <Wt/WText.h>
|
||||||
|
|
||||||
#include "services/database/Object.hpp"
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
#include "services/database/TrackListId.hpp"
|
#include "services/database/TrackListId.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
|
|
||||||
@@ -53,7 +53,10 @@ class PlayQueue : public Template
|
|||||||
public:
|
public:
|
||||||
PlayQueue();
|
PlayQueue();
|
||||||
|
|
||||||
void processTracks(PlayQueueAction action, const std::vector<Database::TrackId>& trackIds);
|
void play(const std::vector<Database::TrackId>& trackIds);
|
||||||
|
void playShuffled(const std::vector<Database::TrackId>& trackIds);
|
||||||
|
void playOrAddLast(const std::vector<Database::TrackId>& trackIds); // play if queue empty, otherwise just add last
|
||||||
|
void playAtIndex(const std::vector<Database::TrackId>& trackIds, std::size_t index);
|
||||||
|
|
||||||
// play the next track in the queue
|
// play the next track in the queue
|
||||||
void playNext();
|
void playNext();
|
||||||
@@ -70,6 +73,7 @@ class PlayQueue : public Template
|
|||||||
constexpr std::size_t getCapacity() const { return _capacity; }
|
constexpr std::size_t getCapacity() const { return _capacity; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void notifyAddedTracks(std::size_t nbAddedTracks) const;
|
||||||
Database::ObjectPtr<Database::TrackList> getTrackList() const;
|
Database::ObjectPtr<Database::TrackList> getTrackList() const;
|
||||||
bool isFull() const;
|
bool isFull() const;
|
||||||
|
|
||||||
|
|||||||
@@ -215,16 +215,22 @@ UserView::refreshView()
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotFoundException {};
|
throw UserNotFoundException {};
|
||||||
|
|
||||||
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName()), Wt::TextFormat::Plain);
|
const Wt::WString title {Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName())};
|
||||||
|
LmsApp->setTitle(title);
|
||||||
|
|
||||||
|
t->bindString("title", title, Wt::TextFormat::Plain);
|
||||||
t->setCondition("if-has-last-login", true);
|
t->setCondition("if-has-last-login", true);
|
||||||
t->bindString("last-login", user->getLastLogin().toString(), Wt::TextFormat::Plain);
|
t->bindString("last-login", user->getLastLogin().toString(), Wt::TextFormat::Plain);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
const Wt::WString title {Wt::WString::tr("Lms.Admin.User.user-create")};
|
||||||
|
LmsApp->setTitle(title);
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
t->setCondition("if-has-login", true);
|
t->setCondition("if-has-login", true);
|
||||||
t->setFormWidget(UserModel::LoginField, std::make_unique<Wt::WLineEdit>());
|
t->setFormWidget(UserModel::LoginField, std::make_unique<Wt::WLineEdit>());
|
||||||
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create"));
|
t->bindString("title", title);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authPasswordService)
|
if (authPasswordService)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "LmsApplicationException.hpp"
|
#include "LmsApplicationException.hpp"
|
||||||
|
#include "PlayQueueController.hpp"
|
||||||
#include "ReleaseListHelpers.hpp"
|
#include "ReleaseListHelpers.hpp"
|
||||||
#include "TrackListHelpers.hpp"
|
#include "TrackListHelpers.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
@@ -47,9 +48,10 @@ using namespace Database;
|
|||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
Artist::Artist(Filters* filters)
|
Artist::Artist(Filters& filters, PlayQueueController& controller)
|
||||||
: Template {Wt::WString::tr("Lms.Explore.Artist.template")}
|
: Template {Wt::WString::tr("Lms.Explore.Artist.template")}
|
||||||
, _filters {filters}
|
, _filters {filters}
|
||||||
|
, _playQueueController {controller}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
@@ -59,7 +61,7 @@ Artist::Artist(Filters* filters)
|
|||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
|
|
||||||
filters->updated().connect([this]
|
filters.updated().connect([this]
|
||||||
{
|
{
|
||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
@@ -109,6 +111,7 @@ Artist::refreshView()
|
|||||||
if (!artist)
|
if (!artist)
|
||||||
throw ArtistNotFoundException {};
|
throw ArtistNotFoundException {};
|
||||||
|
|
||||||
|
LmsApp->setTitle(artist->getName());
|
||||||
_artistId = *artistId;
|
_artistId = *artistId;
|
||||||
|
|
||||||
std::size_t sectionCount{};
|
std::size_t sectionCount{};
|
||||||
@@ -139,7 +142,7 @@ Artist::refreshView()
|
|||||||
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
|
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
|
||||||
entry->clicked().connect([=]
|
entry->clicked().connect([=]
|
||||||
{
|
{
|
||||||
_filters->add(clusterId);
|
_filters.add(clusterId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,18 +153,18 @@ Artist::refreshView()
|
|||||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
artistsAction.emit(PlayQueueAction::Play, {_artistId});
|
_playQueueController.processCommand(PlayQueueController::Command::Play, {_artistId});
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
artistsAction.emit(PlayQueueAction::PlayShuffled, {_artistId});
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, {_artistId});
|
||||||
});
|
});
|
||||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
artistsAction.emit(PlayQueueAction::PlayLast, {_artistId});
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {_artistId});
|
||||||
});
|
});
|
||||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||||
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(*artistId)});
|
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(*artistId)});
|
||||||
@@ -287,7 +290,7 @@ Artist::addSomeReleases(InfiniteScrollingContainer& releaseContainer, EnumSet<Da
|
|||||||
const Range range {static_cast<std::size_t>(releaseContainer.getCount()), _releasesBatchSize};
|
const Range range {static_cast<std::size_t>(releaseContainer.getCount()), _releasesBatchSize};
|
||||||
|
|
||||||
Release::FindParameters params;
|
Release::FindParameters params;
|
||||||
params.setClusters(_filters->getClusterIds());
|
params.setClusters(_filters.getClusterIds());
|
||||||
params.setArtist(_artistId, linkTypes, excludedLinkTypes);
|
params.setArtist(_artistId, linkTypes, excludedLinkTypes);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
params.setSortMethod(ReleaseSortMethod::DateDesc);
|
params.setSortMethod(ReleaseSortMethod::DateDesc);
|
||||||
@@ -314,7 +317,7 @@ Artist::addSomeNonReleaseTracks()
|
|||||||
const Range range {static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize};
|
const Range range {static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize};
|
||||||
|
|
||||||
Track::FindParameters params;
|
Track::FindParameters params;
|
||||||
params.setClusters(_filters->getClusterIds());
|
params.setClusters(_filters.getClusterIds());
|
||||||
params.setArtist(_artistId);
|
params.setArtist(_artistId);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
params.setSortMethod(TrackSortMethod::Name);
|
params.setSortMethod(TrackSortMethod::Name);
|
||||||
@@ -332,7 +335,7 @@ Artist::addSomeNonReleaseTracks()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||||
_trackContainer->add(TrackListHelpers::createEntry(track, tracksAction));
|
_trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||||
|
|
||||||
areTracksAdded = true;
|
areTracksAdded = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/ArtistId.hpp"
|
||||||
#include "services/database/Object.hpp"
|
#include "services/database/Object.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
|
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
@@ -34,17 +33,14 @@ namespace Database
|
|||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
class Filters;
|
class Filters;
|
||||||
|
class PlayQueueController;
|
||||||
class InfiniteScrollingContainer;
|
class InfiniteScrollingContainer;
|
||||||
|
|
||||||
class Artist : public Template
|
class Artist : public Template
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Artist(Filters* filters);
|
Artist(Filters& filters, PlayQueueController& controller);
|
||||||
|
|
||||||
PlayQueueActionArtistSignal artistsAction;
|
|
||||||
PlayQueueActionTrackSignal tracksAction;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshView();
|
void refreshView();
|
||||||
@@ -60,7 +56,8 @@ namespace UserInterface
|
|||||||
static constexpr std::size_t _tracksBatchSize {6};
|
static constexpr std::size_t _tracksBatchSize {6};
|
||||||
static constexpr std::size_t _tracksMaxCount {160};
|
static constexpr std::size_t _tracksMaxCount {160};
|
||||||
|
|
||||||
Filters* _filters {};
|
Filters& _filters;
|
||||||
|
PlayQueueController& _playQueueController;
|
||||||
InfiniteScrollingContainer* _releaseContainer {};
|
InfiniteScrollingContainer* _releaseContainer {};
|
||||||
InfiniteScrollingContainer* _appearsOnReleaseContainer {};
|
InfiniteScrollingContainer* _appearsOnReleaseContainer {};
|
||||||
InfiniteScrollingContainer* _trackContainer {};
|
InfiniteScrollingContainer* _trackContainer {};
|
||||||
|
|||||||
+81
-209
@@ -19,17 +19,8 @@
|
|||||||
|
|
||||||
#include "Explore.hpp"
|
#include "Explore.hpp"
|
||||||
|
|
||||||
|
#include <Wt/WApplication.h>
|
||||||
#include <Wt/WStackedWidget.h>
|
#include <Wt/WStackedWidget.h>
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
#include <Wt/WText.h>
|
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
|
||||||
#include "services/database/Release.hpp"
|
|
||||||
#include "services/database/Session.hpp"
|
|
||||||
#include "services/database/Track.hpp"
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "ArtistsView.hpp"
|
#include "ArtistsView.hpp"
|
||||||
#include "ArtistView.hpp"
|
#include "ArtistView.hpp"
|
||||||
@@ -43,215 +34,96 @@
|
|||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
void
|
void
|
||||||
handleContentsPathChange(Wt::WStackedWidget* stack)
|
handleContentsPathChange(Wt::WStackedWidget* stack)
|
||||||
{
|
|
||||||
enum Idx
|
|
||||||
{
|
|
||||||
IdxArtists = 0,
|
|
||||||
IdxArtist,
|
|
||||||
IdxTrackLists,
|
|
||||||
IdxTrackList,
|
|
||||||
IdxReleases,
|
|
||||||
IdxRelease,
|
|
||||||
IdxSearch,
|
|
||||||
IdxTracks,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const std::map<std::string, int> indexes =
|
|
||||||
{
|
|
||||||
{ "/artists", IdxArtists },
|
|
||||||
{ "/artist", IdxArtist },
|
|
||||||
{ "/tracklists", IdxTrackLists },
|
|
||||||
{ "/tracklist", IdxTrackList },
|
|
||||||
{ "/releases", IdxReleases },
|
|
||||||
{ "/release", IdxRelease },
|
|
||||||
{ "/search", IdxSearch },
|
|
||||||
{ "/tracks", IdxTracks },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& index : indexes)
|
|
||||||
{
|
|
||||||
if (wApp->internalPathMatches(index.first))
|
|
||||||
{
|
{
|
||||||
stack->setCurrentIndex(index.second);
|
enum Idx
|
||||||
return;
|
{
|
||||||
|
IdxArtists = 0,
|
||||||
|
IdxArtist,
|
||||||
|
IdxTrackLists,
|
||||||
|
IdxTrackList,
|
||||||
|
IdxReleases,
|
||||||
|
IdxRelease,
|
||||||
|
IdxSearch,
|
||||||
|
IdxTracks,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const std::map<std::string, int> indexes =
|
||||||
|
{
|
||||||
|
{ "/artists", IdxArtists },
|
||||||
|
{ "/artist", IdxArtist },
|
||||||
|
{ "/tracklists", IdxTrackLists },
|
||||||
|
{ "/tracklist", IdxTrackList },
|
||||||
|
{ "/releases", IdxReleases },
|
||||||
|
{ "/release", IdxRelease },
|
||||||
|
{ "/search", IdxSearch },
|
||||||
|
{ "/tracks", IdxTracks },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& index : indexes)
|
||||||
|
{
|
||||||
|
if (wApp->internalPathMatches(index.first))
|
||||||
|
{
|
||||||
|
stack->setCurrentIndex(index.second);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Explore::Explore(Filters* filters)
|
Explore::Explore(Filters& filters, PlayQueue& playQueue)
|
||||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")}
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")}
|
||||||
, _filters {filters}
|
, _playQueueController {filters, playQueue}
|
||||||
{
|
|
||||||
addFunction("tr", &Functions::tr);
|
|
||||||
|
|
||||||
// Contents
|
|
||||||
Wt::WStackedWidget* contentsStack {bindNew<Wt::WStackedWidget>("contents")};
|
|
||||||
contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
|
||||||
|
|
||||||
// same order as enum Idx
|
|
||||||
auto artists = std::make_unique<Artists>(*_filters);
|
|
||||||
contentsStack->addWidget(std::move(artists));
|
|
||||||
|
|
||||||
auto artist = std::make_unique<Artist>(_filters);
|
|
||||||
artist->artistsAction.connect(this, &Explore::handleArtistsAction);
|
|
||||||
artist->tracksAction.connect(this, &Explore::handleTracksAction);
|
|
||||||
contentsStack->addWidget(std::move(artist));
|
|
||||||
|
|
||||||
auto trackLists {std::make_unique<TrackLists>(*_filters)};
|
|
||||||
TrackLists* trackListsPtr {trackLists.get()};
|
|
||||||
trackLists->trackListAction.connect(this, &Explore::handleTrackListAction);
|
|
||||||
contentsStack->addWidget(std::move(trackLists));
|
|
||||||
|
|
||||||
auto trackList {std::make_unique<TrackList>(*_filters)};
|
|
||||||
TrackList* trackListPtr {trackList.get()};
|
|
||||||
trackList->trackListAction.connect(this, &Explore::handleTrackListAction);
|
|
||||||
trackList->tracksAction.connect(this, &Explore::handleTracksAction);
|
|
||||||
contentsStack->addWidget(std::move(trackList));
|
|
||||||
|
|
||||||
trackListPtr->trackListDeleted.connect(trackListsPtr, &TrackLists::onTrackListDeleted);
|
|
||||||
|
|
||||||
auto releases = std::make_unique<Releases>(*_filters);
|
|
||||||
releases->releasesAction.connect(this, &Explore::handleReleasesAction);
|
|
||||||
contentsStack->addWidget(std::move(releases));
|
|
||||||
|
|
||||||
auto release = std::make_unique<Release>(_filters);
|
|
||||||
release->releasesAction.connect(this, &Explore::handleReleasesAction);
|
|
||||||
release->tracksAction.connect(this, &Explore::handleTracksAction);
|
|
||||||
contentsStack->addWidget(std::move(release));
|
|
||||||
|
|
||||||
auto search = std::make_unique<SearchView>(_filters);
|
|
||||||
search->tracksAction.connect(this, &Explore::handleTracksAction);
|
|
||||||
_search = search.get();
|
|
||||||
contentsStack->addWidget(std::move(search));
|
|
||||||
|
|
||||||
auto tracks = std::make_unique<Tracks>(*_filters);
|
|
||||||
tracks->tracksAction.connect(this, &Explore::handleTracksAction);
|
|
||||||
contentsStack->addWidget(std::move(tracks));
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(this, [=]
|
|
||||||
{
|
{
|
||||||
|
addFunction("tr", &Functions::tr);
|
||||||
|
|
||||||
|
// Contents
|
||||||
|
Wt::WStackedWidget* contentsStack {bindNew<Wt::WStackedWidget>("contents")};
|
||||||
|
contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||||
|
|
||||||
|
// same order as enum Idx
|
||||||
|
auto artists = std::make_unique<Artists>(filters);
|
||||||
|
contentsStack->addWidget(std::move(artists));
|
||||||
|
|
||||||
|
auto artist = std::make_unique<Artist>(filters, _playQueueController);
|
||||||
|
contentsStack->addWidget(std::move(artist));
|
||||||
|
|
||||||
|
auto trackLists {std::make_unique<TrackLists>(filters)};
|
||||||
|
contentsStack->addWidget(std::move(trackLists));
|
||||||
|
|
||||||
|
auto trackList {std::make_unique<TrackList>(filters, _playQueueController)};
|
||||||
|
trackList->trackListDeleted.connect(trackLists.get(), &TrackLists::onTrackListDeleted);
|
||||||
|
contentsStack->addWidget(std::move(trackList));
|
||||||
|
|
||||||
|
auto releases = std::make_unique<Releases>(filters, _playQueueController);
|
||||||
|
contentsStack->addWidget(std::move(releases));
|
||||||
|
|
||||||
|
auto release = std::make_unique<Release>(filters, _playQueueController);
|
||||||
|
contentsStack->addWidget(std::move(release));
|
||||||
|
|
||||||
|
auto search = std::make_unique<SearchView>(filters, _playQueueController);
|
||||||
|
_search = search.get();
|
||||||
|
contentsStack->addWidget(std::move(search));
|
||||||
|
|
||||||
|
auto tracks = std::make_unique<Tracks>(filters, _playQueueController);
|
||||||
|
contentsStack->addWidget(std::move(tracks));
|
||||||
|
|
||||||
|
wApp->internalPathChanged().connect(this, [=]
|
||||||
|
{
|
||||||
|
handleContentsPathChange(contentsStack);
|
||||||
|
});
|
||||||
|
|
||||||
handleContentsPathChange(contentsStack);
|
handleContentsPathChange(contentsStack);
|
||||||
});
|
|
||||||
|
|
||||||
handleContentsPathChange(contentsStack);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Explore::search(const Wt::WString& searchText)
|
|
||||||
{
|
|
||||||
_search->refreshView(searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
std::vector<Database::TrackId>
|
|
||||||
getArtistsTracks(Database::Session& session, const std::vector<Database::ArtistId>& artistsId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
|
||||||
{
|
|
||||||
assert(maxTrackCount);
|
|
||||||
|
|
||||||
std::vector<Database::TrackId> res;
|
|
||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
|
|
||||||
for (const Database::ArtistId artistId : artistsId)
|
|
||||||
{
|
|
||||||
Database::Track::FindParameters params;
|
|
||||||
params.setArtist(artistId);
|
|
||||||
params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease);
|
|
||||||
params.setClusters(clusters);
|
|
||||||
params.setRange({0, maxTrackCount - res.size()});
|
|
||||||
|
|
||||||
const auto tracks {Database::Track::find(session, params)};
|
|
||||||
|
|
||||||
res.reserve(res.size() + tracks.results.size());
|
|
||||||
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
|
||||||
|
|
||||||
if (res.size() == maxTrackCount)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
void
|
||||||
}
|
Explore::search(const Wt::WString& searchText)
|
||||||
|
|
||||||
static
|
|
||||||
std::vector<Database::TrackId>
|
|
||||||
getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
assert(maxTrackCount);
|
|
||||||
|
|
||||||
std::vector<TrackId> res;
|
|
||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
|
|
||||||
for (const ReleaseId releaseId : releasesId)
|
|
||||||
{
|
{
|
||||||
Database::Track::FindParameters params;
|
_search->refreshView(searchText);
|
||||||
params.setRelease(releaseId);
|
|
||||||
params.setSortMethod(Database::TrackSortMethod::Release);
|
|
||||||
params.setClusters(clusters);
|
|
||||||
params.setRange({0, maxTrackCount - res.size()});
|
|
||||||
|
|
||||||
const auto tracks {Database::Track::find(session, params)};
|
|
||||||
|
|
||||||
res.reserve(res.size() + tracks.results.size());
|
|
||||||
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
|
||||||
|
|
||||||
if (res.size() == maxTrackCount)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
std::vector<Database::TrackId>
|
|
||||||
getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
assert(maxTrackCount);
|
|
||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
|
|
||||||
Database::Track::FindParameters params;
|
|
||||||
params.setTrackList(trackListId);
|
|
||||||
params.setClusters(clusters);
|
|
||||||
params.setRange({0, maxTrackCount});
|
|
||||||
params.setSortMethod(TrackSortMethod::TrackList);
|
|
||||||
params.setDistinct(false);
|
|
||||||
|
|
||||||
return Database::Track::find(session, params).results;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Explore::handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId)
|
|
||||||
{
|
|
||||||
tracksAction.emit(action, getArtistsTracks(LmsApp->getDbSession(), artistsId, _filters->getClusterIds(), _maxTrackCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Explore::handleReleasesAction(PlayQueueAction action, const std::vector<Database::ReleaseId>& releasesId)
|
|
||||||
{
|
|
||||||
tracksAction.emit(action, getReleasesTracks(LmsApp->getDbSession(), releasesId, _filters->getClusterIds(), _maxTrackCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Explore::handleTrackListAction(PlayQueueAction action, Database::TrackListId trackListId)
|
|
||||||
{
|
|
||||||
tracksAction.emit(action, getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters->getClusterIds(), _maxTrackCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Explore::handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId)
|
|
||||||
{
|
|
||||||
// consider things are already filtered here, and _maxTrackCount honored playqueue side...
|
|
||||||
tracksAction.emit(action, tracksId);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -20,34 +20,25 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
#include "PlayQueueController.hpp"
|
||||||
#include "services/database/Types.hpp"
|
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
class Filters;
|
class Filters;
|
||||||
class SearchView;
|
class SearchView;
|
||||||
|
class PlayQueue;
|
||||||
|
|
||||||
class Explore : public Wt::WTemplate
|
class Explore : public Wt::WTemplate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Explore(Filters* filters);
|
Explore(Filters& filters, PlayQueue& playQueue);
|
||||||
|
|
||||||
void search(const Wt::WString& searchText);
|
void search(const Wt::WString& searchText);
|
||||||
|
PlayQueueController& getPlayQueueController() { return _playQueueController; }
|
||||||
PlayQueueActionTrackSignal tracksAction;
|
|
||||||
void setMaxTrackCountForAction(std::size_t maxTrackCount) { _maxTrackCount = maxTrackCount; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId);
|
PlayQueueController _playQueueController;
|
||||||
void handleReleasesAction(PlayQueueAction action, const std::vector<Database::ReleaseId>& releasesId);
|
|
||||||
void handleTrackListAction(PlayQueueAction action, Database::TrackListId trackiListId);
|
|
||||||
void handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId);
|
|
||||||
|
|
||||||
Filters* _filters {};
|
|
||||||
SearchView* _search {};
|
SearchView* _search {};
|
||||||
std::size_t _maxTrackCount {};
|
|
||||||
};
|
};
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ Filters::showDialog()
|
|||||||
{
|
{
|
||||||
const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypesIds.results.front())};
|
const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypesIds.results.front())};
|
||||||
|
|
||||||
for (const Cluster::pointer cluster : clusterType->getClusters())
|
for (const Cluster::pointer& cluster : clusterType->getClusters())
|
||||||
{
|
{
|
||||||
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end())
|
if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end())
|
||||||
valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName()));
|
valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName()));
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "explore/Filters.hpp"
|
||||||
|
#include "explore/PlayQueueController.hpp"
|
||||||
|
#include "PlayQueue.hpp"
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
static
|
||||||
|
std::vector<Database::TrackId>
|
||||||
|
getArtistsTracks(Database::Session& session, const std::vector<Database::ArtistId>& artistsId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||||
|
{
|
||||||
|
assert(maxTrackCount);
|
||||||
|
|
||||||
|
std::vector<Database::TrackId> res;
|
||||||
|
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
for (const Database::ArtistId artistId : artistsId)
|
||||||
|
{
|
||||||
|
Database::Track::FindParameters params;
|
||||||
|
params.setArtist(artistId);
|
||||||
|
params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease);
|
||||||
|
params.setClusters(clusters);
|
||||||
|
params.setRange({0, maxTrackCount - res.size()});
|
||||||
|
|
||||||
|
const auto tracks {Database::Track::find(session, params)};
|
||||||
|
|
||||||
|
res.reserve(res.size() + tracks.results.size());
|
||||||
|
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
||||||
|
|
||||||
|
if (res.size() == maxTrackCount)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::vector<Database::TrackId>
|
||||||
|
getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
assert(maxTrackCount);
|
||||||
|
|
||||||
|
std::vector<TrackId> res;
|
||||||
|
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
for (const ReleaseId releaseId : releasesId)
|
||||||
|
{
|
||||||
|
Database::Track::FindParameters params;
|
||||||
|
params.setRelease(releaseId);
|
||||||
|
params.setSortMethod(Database::TrackSortMethod::Release);
|
||||||
|
params.setClusters(clusters);
|
||||||
|
params.setRange({0, maxTrackCount - res.size()});
|
||||||
|
|
||||||
|
const auto tracks {Database::Track::find(session, params)};
|
||||||
|
|
||||||
|
res.reserve(res.size() + tracks.results.size());
|
||||||
|
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
|
||||||
|
|
||||||
|
if (res.size() == maxTrackCount)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::vector<Database::TrackId>
|
||||||
|
getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
assert(maxTrackCount);
|
||||||
|
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
Database::Track::FindParameters params;
|
||||||
|
params.setTrackList(trackListId);
|
||||||
|
params.setClusters(clusters);
|
||||||
|
params.setRange({0, maxTrackCount});
|
||||||
|
params.setSortMethod(TrackSortMethod::TrackList);
|
||||||
|
params.setDistinct(false);
|
||||||
|
|
||||||
|
return Database::Track::find(session, params).results;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayQueueController::PlayQueueController(Filters& filters, PlayQueue& playQueue)
|
||||||
|
: _filters {filters}
|
||||||
|
, _playQueue {playQueue}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlayQueueController::processCommand(Command command, const std::vector<Database::ArtistId>& artistIds)
|
||||||
|
{
|
||||||
|
const std::vector<Database::TrackId> tracks {getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||||
|
processCommand(command, tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlayQueueController::processCommand(Command command, const std::vector<Database::ReleaseId>& releaseIds)
|
||||||
|
{
|
||||||
|
const std::vector<Database::TrackId> tracks {getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||||
|
processCommand(command, tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlayQueueController::processCommand(Command command, const std::vector<Database::TrackId>& trackIds)
|
||||||
|
{
|
||||||
|
// consider things are already filtered here, and _maxTrackCount honored playqueue side...
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case Command::Play:
|
||||||
|
_playQueue.play(trackIds);
|
||||||
|
break;
|
||||||
|
case Command::PlayShuffled:
|
||||||
|
_playQueue.playShuffled(trackIds);
|
||||||
|
break;
|
||||||
|
case Command::PlayOrAddLast:
|
||||||
|
_playQueue.playOrAddLast(trackIds);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlayQueueController::processCommand(Command command, Database::TrackListId trackListId)
|
||||||
|
{
|
||||||
|
const std::vector<Database::TrackId> tracks {getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||||
|
processCommand(command, tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
PlayQueueController::playTrackInRelease(Database::TrackId trackId)
|
||||||
|
{
|
||||||
|
Database::ReleaseId releaseId;
|
||||||
|
{
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)};
|
||||||
|
if (!track || !track->getRelease())
|
||||||
|
return;
|
||||||
|
|
||||||
|
releaseId = track->getRelease()->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Database::TrackId> tracks {getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters.getClusterIds(), _maxTrackCountToEnqueue)};
|
||||||
|
auto itTrack {std::find(std::cbegin(tracks), std::cend(tracks), trackId)};
|
||||||
|
if (itTrack == std::cend(tracks))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::size_t index {static_cast<std::size_t>(std::distance(std::cbegin(tracks), itTrack))};
|
||||||
|
_playQueue.playAtIndex(tracks, index);
|
||||||
|
}
|
||||||
|
} // namespace UserInterface
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2020 Emeric Poupon
|
* Copyright (C) 2022 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <Wt/WSignal.h>
|
|
||||||
|
|
||||||
#include "services/database/ArtistId.hpp"
|
#include "services/database/ArtistId.hpp"
|
||||||
#include "services/database/ReleaseId.hpp"
|
#include "services/database/ReleaseId.hpp"
|
||||||
@@ -29,16 +28,34 @@
|
|||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
enum class PlayQueueAction
|
class Filters;
|
||||||
{
|
class PlayQueue;
|
||||||
Play,
|
|
||||||
PlayLast,
|
|
||||||
PlayShuffled,
|
|
||||||
};
|
|
||||||
|
|
||||||
using PlayQueueActionArtistSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ArtistId>&>;
|
// Used to interact with the play queue, using the current exploration filters
|
||||||
using PlayQueueActionReleaseSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ReleaseId>&>;
|
class PlayQueueController
|
||||||
using PlayQueueActionTrackSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::TrackId>&>;
|
{
|
||||||
using PlayQueueActionTrackListSignal = Wt::Signal<PlayQueueAction, Database::TrackListId>;
|
public:
|
||||||
|
PlayQueueController(Filters& filters, PlayQueue& playQueue);
|
||||||
|
|
||||||
|
enum class Command
|
||||||
|
{
|
||||||
|
Play,
|
||||||
|
PlayOrAddLast,
|
||||||
|
PlayShuffled,
|
||||||
|
};
|
||||||
|
|
||||||
|
void processCommand(Command command, const std::vector<Database::ArtistId>&);
|
||||||
|
void processCommand(Command command, const std::vector<Database::ReleaseId>&);
|
||||||
|
void processCommand(Command command, const std::vector<Database::TrackId>&);
|
||||||
|
void processCommand(Command command, Database::TrackListId);
|
||||||
|
void playTrackInRelease(Database::TrackId);
|
||||||
|
|
||||||
|
void setMaxTrackCountToEnqueue(std::size_t maxTrackCount) { _maxTrackCountToEnqueue = maxTrackCount; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Filters& _filters;
|
||||||
|
PlayQueue& _playQueue;
|
||||||
|
std::size_t _maxTrackCountToEnqueue {};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,20 +34,22 @@
|
|||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "resource/DownloadResource.hpp"
|
#include "resource/DownloadResource.hpp"
|
||||||
#include "Filters.hpp"
|
#include "explore/Filters.hpp"
|
||||||
|
#include "explore/PlayQueueController.hpp"
|
||||||
|
#include "explore/ReleaseListHelpers.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "LmsApplicationException.hpp"
|
#include "LmsApplicationException.hpp"
|
||||||
#include "MediaPlayer.hpp"
|
#include "MediaPlayer.hpp"
|
||||||
#include "ReleaseListHelpers.hpp"
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
Release::Release(Filters* filters)
|
Release::Release(Filters& filters, PlayQueueController& playQueueController)
|
||||||
: Template {Wt::WString::tr("Lms.Explore.Release.template")}
|
: Template {Wt::WString::tr("Lms.Explore.Release.template")}
|
||||||
, _filters {filters}
|
, _filters {filters}
|
||||||
|
, _playQueueController {playQueueController}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
@@ -57,7 +59,7 @@ Release::Release(Filters* filters)
|
|||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
|
|
||||||
filters->updated().connect([this]
|
_filters.updated().connect([this]
|
||||||
{
|
{
|
||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
@@ -106,6 +108,8 @@ Release::refreshView()
|
|||||||
if (!release)
|
if (!release)
|
||||||
throw ReleaseNotFoundException {};
|
throw ReleaseNotFoundException {};
|
||||||
|
|
||||||
|
LmsApp->setTitle(release->getName());
|
||||||
|
|
||||||
refreshCopyright(release);
|
refreshCopyright(release);
|
||||||
refreshLinks(release);
|
refreshLinks(release);
|
||||||
refreshSimilarReleases(similarReleasesIds);
|
refreshSimilarReleases(similarReleasesIds);
|
||||||
@@ -145,7 +149,7 @@ Release::refreshView()
|
|||||||
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
|
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
|
||||||
entry->clicked().connect([=]
|
entry->clicked().connect([=]
|
||||||
{
|
{
|
||||||
_filters->add(clusterId);
|
_filters.add(clusterId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,19 +158,19 @@ Release::refreshView()
|
|||||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
releasesAction.emit(PlayQueueAction::Play, {*releaseId});
|
_playQueueController.processCommand(PlayQueueController::Command::Play, {*releaseId});
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
releasesAction.emit(PlayQueueAction::PlayShuffled, {*releaseId});
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, {*releaseId});
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
releasesAction.emit(PlayQueueAction::PlayLast, {*releaseId});
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {*releaseId});
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||||
@@ -195,7 +199,8 @@ Release::refreshView()
|
|||||||
|
|
||||||
const bool variousArtists {release->hasVariousArtists()};
|
const bool variousArtists {release->hasVariousArtists()};
|
||||||
const auto totalDisc {release->getTotalDisc()};
|
const auto totalDisc {release->getTotalDisc()};
|
||||||
const bool isReleaseMultiDisc {totalDisc && *totalDisc > 1};
|
const std::size_t discCount {release->getDiscCount()};
|
||||||
|
const bool isReleaseMultiDisc {(discCount > 1) || (totalDisc && *totalDisc > 1)};
|
||||||
|
|
||||||
// Expect to be called in asc order
|
// Expect to be called in asc order
|
||||||
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
||||||
@@ -235,7 +240,7 @@ Release::refreshView()
|
|||||||
Database::Track::FindParameters params;
|
Database::Track::FindParameters params;
|
||||||
params.setRelease(*releaseId);
|
params.setRelease(*releaseId);
|
||||||
params.setSortMethod(Database::TrackSortMethod::Release);
|
params.setSortMethod(Database::TrackSortMethod::Release);
|
||||||
params.setClusters(_filters->getClusterIds());
|
params.setClusters(_filters.getClusterIds());
|
||||||
|
|
||||||
const auto tracks {Database::Track::find(LmsApp->getDbSession(), params)};
|
const auto tracks {Database::Track::find(LmsApp->getDbSession(), params)};
|
||||||
for (const Database::TrackId trackId : tracks.results)
|
for (const Database::TrackId trackId : tracks.results)
|
||||||
@@ -282,7 +287,7 @@ Release::refreshView()
|
|||||||
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
|
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||||
playBtn->clicked().connect([=]
|
playBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksAction.emit(PlayQueueAction::Play, {trackId});
|
_playQueueController.playTrackInRelease(trackId);
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -290,7 +295,7 @@ Release::refreshView()
|
|||||||
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId});
|
||||||
});
|
});
|
||||||
|
|
||||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "services/database/Object.hpp"
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/ReleaseId.hpp"
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -31,15 +31,12 @@ namespace Database
|
|||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
class Filters;
|
class Filters;
|
||||||
|
class PlayQueueController;
|
||||||
|
|
||||||
class Release : public Template
|
class Release : public Template
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Release(Filters* filters);
|
Release(Filters& filters, PlayQueueController& playQueueController);
|
||||||
|
|
||||||
PlayQueueActionReleaseSignal releasesAction;
|
|
||||||
PlayQueueActionTrackSignal tracksAction;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshView();
|
void refreshView();
|
||||||
@@ -48,7 +45,8 @@ namespace UserInterface
|
|||||||
void refreshLinks(const Database::ObjectPtr<Database::Release>& release);
|
void refreshLinks(const Database::ObjectPtr<Database::Release>& release);
|
||||||
void refreshSimilarReleases(const std::vector<Database::ReleaseId>& similarReleasesId);
|
void refreshSimilarReleases(const std::vector<Database::ReleaseId>& similarReleasesId);
|
||||||
|
|
||||||
Filters* _filters {};
|
Filters& _filters;
|
||||||
|
PlayQueueController& _playQueueController;
|
||||||
};
|
};
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -26,16 +26,18 @@
|
|||||||
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "ReleaseListHelpers.hpp"
|
#include "explore/Filters.hpp"
|
||||||
#include "Filters.hpp"
|
#include "explore/PlayQueueController.hpp"
|
||||||
|
#include "explore/ReleaseListHelpers.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
Releases::Releases(Filters& filters)
|
Releases::Releases(Filters& filters, PlayQueueController& playQueueController)
|
||||||
: Template {Wt::WString::tr("Lms.Explore.Releases.template")}
|
: Template {Wt::WString::tr("Lms.Explore.Releases.template")}
|
||||||
|
, _playQueueController {playQueueController}
|
||||||
, _releaseCollector {filters, _defaultMode, _maxCount}
|
, _releaseCollector {filters, _defaultMode, _maxCount}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
@@ -69,18 +71,18 @@ Releases::Releases(Filters& filters)
|
|||||||
Wt::WPushButton* playBtn {bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)};
|
Wt::WPushButton* playBtn {bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)};
|
||||||
playBtn->clicked().connect([this]
|
playBtn->clicked().connect([this]
|
||||||
{
|
{
|
||||||
releasesAction.emit(PlayQueueAction::Play, getAllReleases());
|
_playQueueController.processCommand(PlayQueueController::Command::Play, getAllReleases());
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
releasesAction.emit(PlayQueueAction::PlayShuffled, getAllReleases());
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, getAllReleases());
|
||||||
});
|
});
|
||||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
releasesAction.emit(PlayQueueAction::PlayLast, getAllReleases());
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, getAllReleases());
|
||||||
});
|
});
|
||||||
|
|
||||||
_container = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
|
_container = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
|
||||||
|
|||||||
@@ -22,20 +22,18 @@
|
|||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
#include "ReleaseCollector.hpp"
|
#include "ReleaseCollector.hpp"
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
class Filters;
|
class Filters;
|
||||||
class InfiniteScrollingContainer;
|
class InfiniteScrollingContainer;
|
||||||
|
class PlayQueueController;
|
||||||
|
|
||||||
class Releases : public Template
|
class Releases : public Template
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Releases(Filters& filters);
|
Releases(Filters& filters, PlayQueueController& playQueueController);
|
||||||
|
|
||||||
PlayQueueActionReleaseSignal releasesAction;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshView();
|
void refreshView();
|
||||||
@@ -48,6 +46,7 @@ namespace UserInterface
|
|||||||
static constexpr std::size_t _batchSize {_maxItemsPerLine};
|
static constexpr std::size_t _batchSize {_maxItemsPerLine};
|
||||||
static constexpr std::size_t _maxCount {_maxItemsPerLine * 500};
|
static constexpr std::size_t _maxCount {_maxItemsPerLine * 500};
|
||||||
|
|
||||||
|
PlayQueueController& _playQueueController;
|
||||||
Wt::WWidget* _currentActiveItem {};
|
Wt::WWidget* _currentActiveItem {};
|
||||||
InfiniteScrollingContainer* _container {};
|
InfiniteScrollingContainer* _container {};
|
||||||
ReleaseCollector _releaseCollector;
|
ReleaseCollector _releaseCollector;
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ using namespace Database;
|
|||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
SearchView::SearchView(Filters* filters)
|
SearchView::SearchView(Filters& filters, PlayQueueController& playQueueController)
|
||||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
|
||||||
, _filters {filters}
|
, _playQueueController {playQueueController}
|
||||||
, _artistCollector {*filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)}
|
, _artistCollector {filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)}
|
||||||
, _releaseCollector {*filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)}
|
, _releaseCollector {filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)}
|
||||||
, _trackCollector {*filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)}
|
, _trackCollector {filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ namespace UserInterface
|
|||||||
_tracks = bindNew<InfiniteScrollingContainer>("tracks");
|
_tracks = bindNew<InfiniteScrollingContainer>("tracks");
|
||||||
_tracks->onRequestElements.connect([this] { addSomeTracks(); });
|
_tracks->onRequestElements.connect([this] { addSomeTracks(); });
|
||||||
|
|
||||||
_filters->updated().connect([=]
|
filters.updated().connect([=]
|
||||||
{
|
{
|
||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
@@ -152,7 +152,7 @@ namespace UserInterface
|
|||||||
for (const TrackId trackId : trackIds.results)
|
for (const TrackId trackId : trackIds.results)
|
||||||
{
|
{
|
||||||
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||||
_tracks->add(TrackListHelpers::createEntry(track, tracksAction));
|
_tracks->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,20 +27,17 @@
|
|||||||
#include "ArtistCollector.hpp"
|
#include "ArtistCollector.hpp"
|
||||||
#include "ReleaseCollector.hpp"
|
#include "ReleaseCollector.hpp"
|
||||||
#include "TrackCollector.hpp"
|
#include "TrackCollector.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
class InfiniteScrollingContainer;
|
class InfiniteScrollingContainer;
|
||||||
class Filters;
|
class Filters;
|
||||||
|
class PlayQueueController;
|
||||||
|
|
||||||
class SearchView : public Wt::WTemplate
|
class SearchView : public Wt::WTemplate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SearchView(Filters* filters);
|
SearchView(Filters& filters, PlayQueueController& playQueueController);
|
||||||
|
|
||||||
PlayQueueActionTrackSignal tracksAction;
|
|
||||||
|
|
||||||
void refreshView(const Wt::WString& searchText);
|
void refreshView(const Wt::WString& searchText);
|
||||||
|
|
||||||
@@ -74,7 +71,7 @@ namespace UserInterface
|
|||||||
void addSomeReleases();
|
void addSomeReleases();
|
||||||
void addSomeTracks();
|
void addSomeTracks();
|
||||||
|
|
||||||
Filters* _filters {};
|
PlayQueueController& _playQueueController;
|
||||||
ArtistCollector _artistCollector;
|
ArtistCollector _artistCollector;
|
||||||
ReleaseCollector _releaseCollector;
|
ReleaseCollector _releaseCollector;
|
||||||
TrackCollector _trackCollector;
|
TrackCollector _trackCollector;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#include "TrackListHelpers.hpp"
|
#include "TrackListHelpers.hpp"
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WAnchor.h>
|
||||||
#include <Wt/WContainerWidget.h>
|
|
||||||
#include <Wt/WImage.h>
|
#include <Wt/WImage.h>
|
||||||
#include <Wt/WPushButton.h>
|
#include <Wt/WPushButton.h>
|
||||||
|
|
||||||
@@ -29,10 +28,10 @@
|
|||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
|
#include "explore/PlayQueueController.hpp"
|
||||||
#include "resource/DownloadResource.hpp"
|
#include "resource/DownloadResource.hpp"
|
||||||
#include "resource/CoverResource.hpp"
|
#include "resource/CoverResource.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
@@ -44,7 +43,7 @@ using namespace Database;
|
|||||||
namespace UserInterface::TrackListHelpers
|
namespace UserInterface::TrackListHelpers
|
||||||
{
|
{
|
||||||
std::unique_ptr<Wt::WWidget>
|
std::unique_ptr<Wt::WWidget>
|
||||||
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueActionTrackSignal& tracksAction)
|
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController)
|
||||||
{
|
{
|
||||||
auto entry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
auto entry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
||||||
auto* entryPtr {entry.get()};
|
auto* entryPtr {entry.get()};
|
||||||
@@ -74,16 +73,16 @@ namespace UserInterface::TrackListHelpers
|
|||||||
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
|
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||||
playBtn->clicked().connect([trackId, &tracksAction]
|
playBtn->clicked().connect([trackId, &playQueueController]
|
||||||
{
|
{
|
||||||
tracksAction.emit(PlayQueueAction::Play, {trackId});
|
playQueueController.processCommand(PlayQueueController::Command::Play, {trackId});
|
||||||
});
|
});
|
||||||
|
|
||||||
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
|
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
|
||||||
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
||||||
->clicked().connect([=, &tracksAction]
|
->clicked().connect([=, &playQueueController]
|
||||||
{
|
{
|
||||||
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
|
playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId});
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,15 +23,19 @@
|
|||||||
|
|
||||||
#include <Wt/WWidget.h>
|
#include <Wt/WWidget.h>
|
||||||
#include "services/database/Object.hpp"
|
#include "services/database/Object.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Track;
|
class Track;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace UserInterface
|
||||||
|
{
|
||||||
|
class PlayQueueController;
|
||||||
|
}
|
||||||
|
|
||||||
namespace UserInterface::TrackListHelpers
|
namespace UserInterface::TrackListHelpers
|
||||||
{
|
{
|
||||||
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueActionTrackSignal& tracksAction);
|
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController);
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,13 @@
|
|||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
|
#include "explore/Filters.hpp"
|
||||||
|
#include "explore/PlayQueueController.hpp"
|
||||||
|
#include "explore/TrackListHelpers.hpp"
|
||||||
#include "resource/DownloadResource.hpp"
|
#include "resource/DownloadResource.hpp"
|
||||||
#include "Filters.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "LmsApplicationException.hpp"
|
#include "LmsApplicationException.hpp"
|
||||||
#include "ModalManager.hpp"
|
#include "ModalManager.hpp"
|
||||||
#include "TrackListHelpers.hpp"
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
@@ -52,9 +53,10 @@ namespace
|
|||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
TrackList::TrackList(Filters& filters)
|
TrackList::TrackList(Filters& filters, PlayQueueController& playQueueController)
|
||||||
: Template {Wt::WString::tr("Lms.Explore.TrackList.template")}
|
: Template {Wt::WString::tr("Lms.Explore.TrackList.template")}
|
||||||
, _filters {filters}
|
, _filters {_filters}
|
||||||
|
, _playQueueController {playQueueController}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
@@ -64,7 +66,7 @@ namespace UserInterface
|
|||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
|
|
||||||
_filters.updated().connect([this]
|
filters.updated().connect([this]
|
||||||
{
|
{
|
||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
@@ -88,6 +90,7 @@ namespace UserInterface
|
|||||||
if (!trackList)
|
if (!trackList)
|
||||||
throw TrackListNotFoundException {};
|
throw TrackListNotFoundException {};
|
||||||
|
|
||||||
|
LmsApp->setTitle(std::string {trackList->getName()});
|
||||||
_trackListId = *trackListId;
|
_trackListId = *trackListId;
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
@@ -119,19 +122,19 @@ namespace UserInterface
|
|||||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
trackListAction.emit(PlayQueueAction::Play, *trackListId);
|
_playQueueController.processCommand(PlayQueueController::Command::Play, *trackListId);
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
trackListAction.emit(PlayQueueAction::PlayShuffled, *trackListId);
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, *trackListId);
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
trackListAction.emit(PlayQueueAction::PlayLast, *trackListId);
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, *trackListId);
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||||
@@ -196,7 +199,7 @@ namespace UserInterface
|
|||||||
for (const TrackId trackId : trackIds.results)
|
for (const TrackId trackId : trackIds.results)
|
||||||
{
|
{
|
||||||
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
||||||
_container->add(TrackListHelpers::createEntry(track, tracksAction));
|
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||||
}
|
}
|
||||||
|
|
||||||
_container->setHasMore(trackIds.moreResults && _container->getCount() < _maxCount);
|
_container->setHasMore(trackIds.moreResults && _container->getCount() < _maxCount);
|
||||||
|
|||||||
@@ -23,20 +23,17 @@
|
|||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
class Filters;
|
class Filters;
|
||||||
class InfiniteScrollingContainer;
|
class InfiniteScrollingContainer;
|
||||||
|
class PlayQueueController;
|
||||||
|
|
||||||
class TrackList : public Template
|
class TrackList : public Template
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TrackList(Filters& filters);
|
TrackList(Filters& filters, PlayQueueController& playQueueController);
|
||||||
|
|
||||||
PlayQueueActionTrackListSignal trackListAction;
|
|
||||||
PlayQueueActionTrackSignal tracksAction;
|
|
||||||
|
|
||||||
Wt::Signal<Database::TrackListId> trackListDeleted;
|
Wt::Signal<Database::TrackListId> trackListDeleted;
|
||||||
|
|
||||||
@@ -48,6 +45,7 @@ namespace UserInterface
|
|||||||
static constexpr std::size_t _maxCount {8000};
|
static constexpr std::size_t _maxCount {8000};
|
||||||
|
|
||||||
Filters& _filters;
|
Filters& _filters;
|
||||||
|
PlayQueueController& _playQueueController;
|
||||||
Database::TrackListId _trackListId;
|
Database::TrackListId _trackListId;
|
||||||
InfiniteScrollingContainer* _container {};
|
InfiniteScrollingContainer* _container {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -44,8 +43,6 @@ namespace UserInterface
|
|||||||
public:
|
public:
|
||||||
TrackLists(Filters& filters);
|
TrackLists(Filters& filters);
|
||||||
|
|
||||||
PlayQueueActionTrackListSignal trackListAction;
|
|
||||||
|
|
||||||
void onTrackListDeleted(Database::TrackListId trackListId);
|
void onTrackListDeleted(Database::TrackListId trackListId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -26,17 +26,19 @@
|
|||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
#include "Filters.hpp"
|
#include "explore/Filters.hpp"
|
||||||
|
#include "explore/PlayQueueController.hpp"
|
||||||
|
#include "explore/TrackListHelpers.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "TrackListHelpers.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
Tracks::Tracks(Filters& filters)
|
Tracks::Tracks(Filters& filters, PlayQueueController& playQueueController)
|
||||||
: Template {Wt::WString::tr("Lms.Explore.Tracks.template")},
|
: Template {Wt::WString::tr("Lms.Explore.Tracks.template")}
|
||||||
_trackCollector {filters, _defaultMode, _maxCount}
|
, _playQueueController {playQueueController}
|
||||||
|
, _trackCollector {filters, _defaultMode, _maxCount}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
@@ -69,18 +71,18 @@ _trackCollector {filters, _defaultMode, _maxCount}
|
|||||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksAction.emit(PlayQueueAction::Play, getAllTracks());
|
_playQueueController.processCommand(PlayQueueController::Command::Play, getAllTracks());
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksAction.emit(PlayQueueAction::PlayShuffled, getAllTracks());
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, getAllTracks());
|
||||||
});
|
});
|
||||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||||
->clicked().connect([=]
|
->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksAction.emit(PlayQueueAction::PlayLast, getAllTracks());
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, getAllTracks());
|
||||||
});
|
});
|
||||||
|
|
||||||
_container = bindNew<InfiniteScrollingContainer>("tracks");
|
_container = bindNew<InfiniteScrollingContainer>("tracks");
|
||||||
@@ -122,7 +124,7 @@ Tracks::addSome()
|
|||||||
for (const TrackId trackId : trackIds.results)
|
for (const TrackId trackId : trackIds.results)
|
||||||
{
|
{
|
||||||
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
||||||
_container->add(TrackListHelpers::createEntry(track, tracksAction));
|
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||||
}
|
}
|
||||||
|
|
||||||
_container->setHasMore(trackIds.moreResults);
|
_container->setHasMore(trackIds.moreResults);
|
||||||
|
|||||||
@@ -22,20 +22,18 @@
|
|||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "PlayQueueAction.hpp"
|
|
||||||
#include "TrackCollector.hpp"
|
#include "TrackCollector.hpp"
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
class Filters;
|
class Filters;
|
||||||
class InfiniteScrollingContainer;
|
class InfiniteScrollingContainer;
|
||||||
|
class PlayQueueController;
|
||||||
|
|
||||||
class Tracks : public Template
|
class Tracks : public Template
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Tracks(Filters& filters);
|
Tracks(Filters& filters, PlayQueueController& playQueueController);
|
||||||
|
|
||||||
PlayQueueActionTrackSignal tracksAction;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshView();
|
void refreshView();
|
||||||
@@ -48,6 +46,7 @@ namespace UserInterface
|
|||||||
static constexpr std::size_t _batchSize {6};
|
static constexpr std::size_t _batchSize {6};
|
||||||
static constexpr std::size_t _maxCount {8000};
|
static constexpr std::size_t _maxCount {8000};
|
||||||
|
|
||||||
|
PlayQueueController& _playQueueController;
|
||||||
Wt::WWidget* _currentActiveItem {};
|
Wt::WWidget* _currentActiveItem {};
|
||||||
InfiniteScrollingContainer* _container {};
|
InfiniteScrollingContainer* _container {};
|
||||||
TrackCollector _trackCollector;
|
TrackCollector _trackCollector;
|
||||||
|
|||||||
@@ -118,59 +118,62 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TranscodeParameters
|
namespace
|
||||||
{
|
{
|
||||||
std::filesystem::path file;
|
struct TranscodeParameters
|
||||||
Av::TranscodeParameters transcodeParameters;
|
|
||||||
};
|
|
||||||
|
|
||||||
static
|
|
||||||
std::optional<TranscodeParameters>
|
|
||||||
readTranscodeParameters(const Wt::Http::Request& request)
|
|
||||||
{
|
|
||||||
TranscodeParameters parameters;
|
|
||||||
|
|
||||||
// mandatory parameters
|
|
||||||
const std::optional<Database::TrackId> trackId {readParameterAs<Database::TrackId::ValueType>(request, "trackid")};
|
|
||||||
const auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
|
||||||
const auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
|
||||||
|
|
||||||
if (!trackId || !format || !bitrate)
|
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
|
|
||||||
if (!avFormat)
|
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
// optional parameter
|
|
||||||
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
|
|
||||||
|
|
||||||
std::filesystem::path trackPath;
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
Av::InputFileParameters inputFileParameters;
|
||||||
|
Av::TranscodeParameters transcodeParameters;
|
||||||
|
};
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)};
|
std::optional<TranscodeParameters>
|
||||||
if (!track)
|
readTranscodeParameters(const Wt::Http::Request& request)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Missing track";
|
TranscodeParameters parameters;
|
||||||
|
|
||||||
|
// mandatory parameters
|
||||||
|
const std::optional<Database::TrackId> trackId {readParameterAs<Database::TrackId::ValueType>(request, "trackid")};
|
||||||
|
const auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||||
|
const auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||||
|
|
||||||
|
if (!trackId || !format || !bitrate)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
|
||||||
|
|
||||||
parameters.file = track->getPath();
|
|
||||||
|
|
||||||
if (!Database::isAudioBitrateAllowed(*bitrate))
|
if (!Database::isAudioBitrateAllowed(*bitrate))
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
|
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
|
||||||
|
if (!avFormat)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
// optional parameter
|
||||||
|
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
|
||||||
|
|
||||||
|
std::filesystem::path trackPath;
|
||||||
|
{
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)};
|
||||||
|
if (!track)
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "Missing track";
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.inputFileParameters.trackPath = track->getPath();
|
||||||
|
parameters.inputFileParameters.duration = track->getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.transcodeParameters.stripMetadata = true;
|
||||||
|
parameters.transcodeParameters.format = *avFormat;
|
||||||
|
parameters.transcodeParameters.bitrate = *bitrate;
|
||||||
|
parameters.transcodeParameters.offset = std::chrono::seconds {offset};
|
||||||
|
|
||||||
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
parameters.transcodeParameters.stripMetadata = true;
|
|
||||||
parameters.transcodeParameters.format = *avFormat;
|
|
||||||
parameters.transcodeParameters.bitrate = *bitrate;
|
|
||||||
parameters.transcodeParameters.offset = std::chrono::seconds {offset};
|
|
||||||
|
|
||||||
return parameters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -186,7 +189,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
|
|||||||
{
|
{
|
||||||
const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)};
|
const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)};
|
||||||
if (parameters)
|
if (parameters)
|
||||||
resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters);
|
resourceHandler = Av::createTranscodeResourceHandler(parameters->inputFileParameters, parameters->transcodeParameters, false /* estimate content length */);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
|
|
||||||
#include <Wt/WDate.h>
|
#include <Wt/WDate.h>
|
||||||
|
|
||||||
#include "metadata/AvFormatParser.hpp"
|
#include "metadata/IParser.hpp"
|
||||||
#include "metadata/TagLibParser.hpp"
|
|
||||||
#include "utils/StreamLogger.hpp"
|
#include "utils/StreamLogger.hpp"
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist)
|
std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist)
|
||||||
@@ -69,7 +68,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
|||||||
}
|
}
|
||||||
const auto end {std::chrono::steady_clock::now()};
|
const auto end {std::chrono::steady_clock::now()};
|
||||||
|
|
||||||
std::cout << "Parsing time: " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000. << "ms" << std::endl;
|
std::cout << "Parsing time: " << std::fixed << std::setprecision(2) << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000. << "ms" << std::endl;
|
||||||
|
|
||||||
std::cout << "Track metadata:" << std::endl;
|
std::cout << "Track metadata:" << std::endl;
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Duration: " << track->duration.count() / 1000 << "s" << std::endl;
|
std::cout << "Duration: " << std::fixed << std::setprecision(2) << track->duration.count() / 1000. << "s" << std::endl;
|
||||||
|
|
||||||
if (track->trackNumber)
|
if (track->trackNumber)
|
||||||
std::cout << "Track: " << *track->trackNumber << std::endl;
|
std::cout << "Track: " << *track->trackNumber << std::endl;
|
||||||
@@ -184,14 +183,14 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
{
|
{
|
||||||
std::cout << "Using av:" << std::endl;
|
std::cout << "Using av:" << std::endl;
|
||||||
MetaData::AvFormatParser parser;
|
auto parser {MetaData::createParser(MetaData::ParserType::AvFormat, MetaData::ParserReadStyle::Accurate)};
|
||||||
parse(parser, file);
|
parse(*parser, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::cout << "Using TagLib:" << std::endl;
|
std::cout << "Using TagLib:" << std::endl;
|
||||||
MetaData::TagLibParser parser;
|
auto parser {MetaData::createParser(MetaData::ParserType::TagLib, MetaData::ParserReadStyle::Accurate)};
|
||||||
parse(parser, file);
|
parse(*parser, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user