WIP. new layout on the audio view

This commit is contained in:
emeric
2014-10-17 00:11:53 +02:00
parent 27ea810f9e
commit 49d2475422
19 changed files with 412 additions and 477 deletions
+47 -47
View File
@@ -1,64 +1,64 @@
bin_PROGRAMS = lms bin_PROGRAMS = lms
lms_SOURCES = \ lms_SOURCES = \
$(srcdir)/main/main.cpp \ $(srcdir)/main/main.cpp \
$(srcdir)/av/CodecContext.cpp \ $(srcdir)/av/CodecContext.cpp \
$(srcdir)/av/Common.cpp \ $(srcdir)/av/Common.cpp \
$(srcdir)/av/Dictionary.cpp \ $(srcdir)/av/Dictionary.cpp \
$(srcdir)/av/FormatContext.cpp \ $(srcdir)/av/FormatContext.cpp \
$(srcdir)/av/InputFormatContext.cpp \ $(srcdir)/av/InputFormatContext.cpp \
$(srcdir)/av/Stream.cpp \ $(srcdir)/av/Stream.cpp \
$(srcdir)/config/ConfigReader.cpp \ $(srcdir)/config/ConfigReader.cpp \
$(srcdir)/cover/CoverArt.cpp \ $(srcdir)/cover/CoverArt.cpp \
$(srcdir)/cover/CoverArtGrabber.cpp \ $(srcdir)/cover/CoverArtGrabber.cpp \
$(srcdir)/database/Artist.cpp \ $(srcdir)/database/Artist.cpp \
$(srcdir)/database/Genre.cpp \ $(srcdir)/database/Genre.cpp \
$(srcdir)/database/Release.cpp \ $(srcdir)/database/Release.cpp \
$(srcdir)/database/MediaDirectory.cpp \ $(srcdir)/database/MediaDirectory.cpp \
$(srcdir)/database/Track.cpp \ $(srcdir)/database/Track.cpp \
$(srcdir)/database/DatabaseHandler.cpp \ $(srcdir)/database/DatabaseHandler.cpp \
$(srcdir)/database/SqlQuery.cpp \ $(srcdir)/database/SqlQuery.cpp \
$(srcdir)/database/Video.cpp \ $(srcdir)/database/Video.cpp \
$(srcdir)/database/User.cpp \ $(srcdir)/database/User.cpp \
$(srcdir)/database-updater/DatabaseUpdater.cpp \ $(srcdir)/database-updater/DatabaseUpdater.cpp \
$(srcdir)/database-updater/Checksum.cpp \ $(srcdir)/database-updater/Checksum.cpp \
$(srcdir)/logger/Logger.cpp \ $(srcdir)/logger/Logger.cpp \
$(srcdir)/metadata/AvFormat.cpp \ $(srcdir)/metadata/AvFormat.cpp \
$(srcdir)/metadata/Utils.cpp \ $(srcdir)/metadata/Utils.cpp \
$(srcdir)/remote/server/Connection.cpp \ $(srcdir)/remote/server/Connection.cpp \
$(srcdir)/remote/server/ConnectionManager.cpp \ $(srcdir)/remote/server/ConnectionManager.cpp \
$(srcdir)/remote/server/AudioCollectionRequestHandler.cpp \ $(srcdir)/remote/server/AudioCollectionRequestHandler.cpp \
$(srcdir)/remote/server/AuthRequestHandler.cpp \ $(srcdir)/remote/server/AuthRequestHandler.cpp \
$(srcdir)/remote/server/MediaRequestHandler.cpp \ $(srcdir)/remote/server/MediaRequestHandler.cpp \
$(srcdir)/remote/server/RequestHandler.cpp \ $(srcdir)/remote/server/RequestHandler.cpp \
$(srcdir)/remote/server/Server.cpp \ $(srcdir)/remote/server/Server.cpp \
$(srcdir)/service/ServiceManager.cpp \ $(srcdir)/service/ServiceManager.cpp \
$(srcdir)/service/DatabaseUpdateService.cpp \ $(srcdir)/service/DatabaseUpdateService.cpp \
$(srcdir)/service/UserInterfaceService.cpp \ $(srcdir)/service/UserInterfaceService.cpp \
$(srcdir)/service/RemoteServerService.cpp \ $(srcdir)/service/RemoteServerService.cpp \
$(srcdir)/transcode/AvConvTranscoder.cpp \ $(srcdir)/transcode/AvConvTranscoder.cpp \
$(srcdir)/transcode/Format.cpp \ $(srcdir)/transcode/Format.cpp \
$(srcdir)/transcode/Parameters.cpp \ $(srcdir)/transcode/Parameters.cpp \
$(srcdir)/transcode/InputMediaFile.cpp \ $(srcdir)/transcode/InputMediaFile.cpp \
$(srcdir)/ui/LmsApplication.cpp \ $(srcdir)/ui/LmsApplication.cpp \
$(srcdir)/ui/LmsHome.cpp \ $(srcdir)/ui/LmsHome.cpp \
$(srcdir)/ui/auth/LmsAuth.cpp \ $(srcdir)/ui/auth/LmsAuth.cpp \
$(srcdir)/ui/audio/AudioWidget.cpp \ $(srcdir)/ui/audio/AudioWidget.cpp \
$(srcdir)/ui/audio/AudioDatabaseWidget.cpp \ $(srcdir)/ui/audio/AudioMediaPlayerWidget.cpp \
$(srcdir)/ui/audio/AudioMediaPlayerWidget.cpp \ $(srcdir)/ui/audio/FilterChain.cpp \
$(srcdir)/ui/audio/SearchFilterWidget.cpp \ $(srcdir)/ui/audio/KeywordSearchFilter.cpp \
$(srcdir)/ui/audio/TableFilterWidget.cpp \ $(srcdir)/ui/audio/TableFilter.cpp \
$(srcdir)/ui/audio/TrackWidget.cpp \ $(srcdir)/ui/audio/TrackView.cpp \
$(srcdir)/ui/common/DirectoryValidator.cpp \ $(srcdir)/ui/common/DirectoryValidator.cpp \
$(srcdir)/ui/common/SessionData.cpp \ $(srcdir)/ui/common/SessionData.cpp \
$(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \ $(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \
$(srcdir)/ui/video/VideoWidget.cpp \ $(srcdir)/ui/video/VideoWidget.cpp \
$(srcdir)/ui/video/VideoDatabaseWidget.cpp \ $(srcdir)/ui/video/VideoDatabaseWidget.cpp \
$(srcdir)/ui/video/VideoMediaPlayerWidget.cpp \ $(srcdir)/ui/video/VideoMediaPlayerWidget.cpp \
$(srcdir)/ui/video/VideoParametersDialog.cpp \ $(srcdir)/ui/video/VideoParametersDialog.cpp \
$(srcdir)/ui/settings/Settings.cpp \ $(srcdir)/ui/settings/Settings.cpp \
$(srcdir)/ui/settings/SettingsAccountFormView.cpp \ $(srcdir)/ui/settings/SettingsAccountFormView.cpp \
$(srcdir)/ui/settings/SettingsAudioFormView.cpp \ $(srcdir)/ui/settings/SettingsAudioFormView.cpp \
$(srcdir)/ui/settings/SettingsDatabaseFormView.cpp \ $(srcdir)/ui/settings/SettingsDatabaseFormView.cpp \
$(srcdir)/ui/settings/SettingsFirstConnectionFormView.cpp \ $(srcdir)/ui/settings/SettingsFirstConnectionFormView.cpp \
$(srcdir)/ui/settings/SettingsMediaDirectories.cpp \ $(srcdir)/ui/settings/SettingsMediaDirectories.cpp \
+3
View File
@@ -70,6 +70,7 @@ AuthRequestHandler::processPassword(const AuthRequest::Password& request, AuthRe
{ {
case Wt::Auth::PasswordInvalid: case Wt::Auth::PasswordInvalid:
response.set_type(AuthResponse::PasswordResult::TypePasswordInvalid); response.set_type(AuthResponse::PasswordResult::TypePasswordInvalid);
LMS_LOG(MOD_REMOTE, SEV_NOTICE) << "User '" << request.user_login() << "': invalid password";
res = true; res = true;
break; break;
case Wt::Auth::LoginThrottling: case Wt::Auth::LoginThrottling:
@@ -81,9 +82,11 @@ AuthRequestHandler::processPassword(const AuthRequest::Password& request, AuthRe
response.set_type(AuthResponse::PasswordResult::TypePasswordValid); response.set_type(AuthResponse::PasswordResult::TypePasswordValid);
// Log the user in // Log the user in
_db.getLogin().login( user ); _db.getLogin().login( user );
LMS_LOG(MOD_REMOTE, SEV_NOTICE) << "User '" << request.user_login() << "' successfully logged in";
res = true; res = true;
break; break;
default: default:
LMS_LOG(MOD_REMOTE, SEV_ERROR) << "Cannot handle password result!";
break; break;
} }
+50 -47
View File
@@ -18,8 +18,24 @@
*/ */
#include <Wt/WBootstrapTheme> #include <Wt/WBootstrapTheme>
#include <Wt/WVBoxLayout>
#include <Wt/WHBoxLayout>
#include <Wt/WNavigationBar>
#include <Wt/WStackedWidget>
#include <Wt/WMenu>
#include <Wt/WNavigationBar>
#include <Wt/WPopupMenu>
#include <Wt/WPopupMenuItem>
#include <Wt/WVBoxLayout>
#include <Wt/Auth/Identity>
#include "settings/Settings.hpp"
#include "auth/LmsAuth.hpp" #include "auth/LmsAuth.hpp"
#include "logger/Logger.hpp"
#include "LmsHome.hpp" #include "LmsHome.hpp"
#include "settings/SettingsFirstConnectionFormView.hpp" #include "settings/SettingsFirstConnectionFormView.hpp"
@@ -64,65 +80,52 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, boost::filesystem::p
setTitle("LMS"); // application title setTitle("LMS"); // application title
bool firstConnection; // Create a Vertical layout: top is the nav bar, bottom is the contents
{ Wt::WVBoxLayout *layout = new Wt::WVBoxLayout(this->root());
Wt::Dbo::Transaction transaction(_sessionData.getDatabaseHandler().getSession()); // Create a navigation bar with a link to a web page.
Wt::WNavigationBar *navigation = new Wt::WNavigationBar();
navigation->setTitle("LMS");
navigation->setResponsive(true);
navigation->addStyleClass("main-nav");
firstConnection = (Database::User::getAll(_sessionData.getDatabaseHandler().getSession()).size() == 0); Wt::WStackedWidget *contentsStack = new Wt::WStackedWidget();
}
// If here is no account in the database, launch the first connection wizard // Setup a Left-aligned menu.
if (firstConnection) Wt::WMenu *leftMenu = new Wt::WMenu(contentsStack);
{ navigation->addMenu(leftMenu);
// Hack, use the auth widget builtin strings
builtinLocalizedStrings().useBuiltin(skeletons::AuthStrings_xml1);
root()->addWidget( new Settings::FirstConnectionFormView(_sessionData)); _audioWidget = new AudioWidget(_sessionData);
} _videoWidget = new VideoWidget(_sessionData);
else
{
_sessionData.getDatabaseHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
LmsAuth *authWidget = new LmsAuth(_sessionData.getDatabaseHandler()); leftMenu->addItem("Audio", _audioWidget);
leftMenu->addItem("Video", _videoWidget);
// leftMenu->addItem("Settings", new Settings::Settings(_sessionData));
authWidget->model()->addPasswordAuth(&Database::Handler::getPasswordService()); // Setup a Right-aligned menu.
authWidget->setRegistrationEnabled(false); Wt::WMenu *rightMenu = new Wt::WMenu();
authWidget->processEnvironment(); navigation->addMenu(rightMenu, Wt::AlignRight);
root()->addWidget(authWidget); Wt::WPopupMenu *popup = new Wt::WPopupMenu();
} popup->addItem("Logout");
}
// popup->itemSelected().connect(this, &LmsHome::handleUserMenuSelected);
/* Wt::WMenuItem *item = new Wt::WMenuItem( user.identity(Wt::Auth::Identity::LoginName) );
item->setMenu(popup);
rightMenu->addItem(item);*/
void // Add a Search control.
LmsApplication::handleAuthEvent(void) /* _searchEdit = new Wt::WLineEdit();
{ _searchEdit->setEmptyText("Search...");
if (_sessionData.getDatabaseHandler().getLogin().loggedIn())
{
if (_home == nullptr) {
_home = new LmsHome(_sessionData, root() );
}
else
std::cerr << "Already logged in??" << std::endl;
}
else
{
std::cerr << "user log out" << std::endl;
if (_home != nullptr) {
delete _home;
_home = nullptr;
// Hack: quit/redirect in order to avoid 'signal not exposed' problems _searchEdit->enterPressed().connect(this, &LmsHome::handleSearch);
// TODO, investigate/remove? */
quit(); // navigation->addSearch(_searchEdit, Wt::AlignLeft);
redirect("/");
} layout->addWidget(navigation);
else layout->addWidget(contentsStack, 1);
std::cerr << "Already logged out??" << std::endl; layout->setContentsMargins(0, 0, 0, 0);
}
} }
} // namespace UserInterface } // namespace UserInterface
+2
View File
@@ -44,6 +44,8 @@ class LmsApplication : public Wt::WApplication
SessionData _sessionData; SessionData _sessionData;
LmsHome* _home; LmsHome* _home;
AudioWidget* _audioWidget;
VideoWidget* _videoWidget;
}; };
+1 -41
View File
@@ -22,6 +22,7 @@
#include <Wt/WNavigationBar> #include <Wt/WNavigationBar>
#include <Wt/WPopupMenu> #include <Wt/WPopupMenu>
#include <Wt/WPopupMenuItem> #include <Wt/WPopupMenuItem>
#include <Wt/WVBoxLayout>
#include <Wt/Auth/Identity> #include <Wt/Auth/Identity>
#include "settings/Settings.hpp" #include "settings/Settings.hpp"
@@ -36,48 +37,7 @@ LmsHome::LmsHome(SessionData& sessionData, Wt::WContainerWidget* parent)
{ {
const Wt::Auth::User& user = sessionData.getDatabaseHandler().getLogin().user(); const Wt::Auth::User& user = sessionData.getDatabaseHandler().getLogin().user();
// Create a navigation bar with a link to a web page.
Wt::WNavigationBar *navigation = new Wt::WNavigationBar(this);
navigation->setTitle("LMS");
navigation->setResponsive(true);
navigation->addStyleClass("main-nav");
Wt::WStackedWidget *contentsStack = new Wt::WStackedWidget(this);
// Setup a Left-aligned menu.
Wt::WMenu *leftMenu = new Wt::WMenu(contentsStack);
navigation->addMenu(leftMenu);
_audioWidget = new AudioWidget(_sessionData);
_videoWidget = new VideoWidget(_sessionData);
leftMenu->addItem("Audio", _audioWidget);
leftMenu->addItem("Video", _videoWidget);
leftMenu->addItem("Settings", new Settings::Settings(_sessionData));
// Setup a Right-aligned menu.
Wt::WMenu *rightMenu = new Wt::WMenu();
navigation->addMenu(rightMenu, Wt::AlignRight);
Wt::WPopupMenu *popup = new Wt::WPopupMenu();
popup->addItem("Logout");
popup->itemSelected().connect(this, &LmsHome::handleUserMenuSelected);
Wt::WMenuItem *item = new Wt::WMenuItem( user.identity(Wt::Auth::Identity::LoginName) );
item->setMenu(popup);
rightMenu->addItem(item);
// Add a Search control.
_searchEdit = new Wt::WLineEdit();
_searchEdit->setEmptyText("Search...");
_searchEdit->enterPressed().connect(this, &LmsHome::handleSearch);
navigation->addSearch(_searchEdit, Wt::AlignLeft);
addWidget(contentsStack);
} }
-126
View File
@@ -1,126 +0,0 @@
/*
* Copyright (C) 2013 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 <boost/foreach.hpp>
#include <Wt/WTable> // TODO
#include <Wt/WBreak> // TODO
#include "AudioDatabaseWidget.hpp"
#include "TableFilterWidget.hpp"
#include "SearchFilterWidget.hpp"
#include "TrackWidget.hpp"
namespace UserInterface {
AudioDatabaseWidget::AudioDatabaseWidget( Database::Handler& db, Wt::WContainerWidget *parent)
: Wt::WContainerWidget(parent),
_refreshingFilters(false)
{
std::size_t idFilter (0);
{
SearchFilterWidget* search = new SearchFilterWidget(this);
_filters.push_back( search );
search->update().connect( boost::bind(&AudioDatabaseWidget::handleFilterUpdated, this, idFilter++) );
}
Wt::WTable* table = new Wt::WTable(this);
{
TableFilterWidget* filterTable = new TableFilterWidget(db, "genre", "name", table->elementAt(0,0));
_filters.push_back( filterTable );
filterTable->update().connect( boost::bind(&AudioDatabaseWidget::handleFilterUpdated, this, idFilter++) );
}
{
TableFilterWidget* filterTable = new TableFilterWidget(db, "artist", "name", table->elementAt(0,1));
_filters.push_back( filterTable );
filterTable->update().connect( boost::bind(&AudioDatabaseWidget::handleFilterUpdated, this, idFilter++) );
}
{
TableFilterWidget* filterTable = new TableFilterWidget(db, "release", "name", table->elementAt(0,2));
_filters.push_back( filterTable );
filterTable->update().connect( boost::bind(&AudioDatabaseWidget::handleFilterUpdated, this, idFilter++) );
}
{
TrackWidget* track = new TrackWidget(db, this);
_filters.push_back( track );
track->trackSelected().connect(this, &AudioDatabaseWidget::handleTrackSelected);
}
}
void
AudioDatabaseWidget::search(const std::string& text)
{
SearchFilterWidget* searchWidget ( dynamic_cast<SearchFilterWidget*>(_filters.front() ) );
searchWidget->setText(text);
}
void
AudioDatabaseWidget::handleTrackSelected(boost::filesystem::path p)
{
_trackSelected.emit(p);
}
void
AudioDatabaseWidget::handleFilterUpdated(std::size_t idFilterUpdated)
{
// TODO disconnect from event!
if (_refreshingFilters)
return;
_refreshingFilters = true;
FilterWidget::Constraint currentConstraint;
currentConstraint.where.And( WhereClause( "track.artist_id = artist.id and track.release_id = release.id and track_genre.track_id = track.id and genre.id = track_genre.genre_id"));
for (std::size_t idFilter = 0; idFilter < _filters.size(); ++idFilter)
{
FilterWidget* filter = _filters.at(idFilter);
// Apply contraints created by previous filters
if (idFilter > idFilterUpdated) {
filter->refresh(currentConstraint);
}
// Get constraints generated by this filter
// (Note: adding accross successive calls)
filter->getConstraint(currentConstraint);
}
_refreshingFilters = false;
}
void
AudioDatabaseWidget::selectNextTrack(void)
{
TrackWidget* trackWidget ( dynamic_cast<TrackWidget*>(_filters.back() ) );
trackWidget->selectNextTrack();
}
} // namespace UserInterface
-62
View File
@@ -1,62 +0,0 @@
/*
* Copyright (C) 2013 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/>.
*/
#ifndef AUDIO_DB_WIDGET_HPP
#define AUDIO_DB_WIDGET_HPP
#include <boost/filesystem/path.hpp>
#include <Wt/WContainerWidget>
#include <Wt/WSignal>
#include "common/SessionData.hpp"
#include "FilterWidget.hpp"
namespace UserInterface {
class AudioDatabaseWidget : public Wt::WContainerWidget
{
public:
AudioDatabaseWidget( Database::Handler& db, Wt::WContainerWidget *parent = 0);
void search(const std::string& text);
void selectNextTrack(void); // Will later emit the next selected track
// Signals
Wt::Signal< boost::filesystem::path >& trackSelected() { return _trackSelected; }
private:
Wt::Signal< boost::filesystem::path > _trackSelected;
void handleTrackSelected(boost::filesystem::path p);
void handleFilterUpdated(std::size_t idFilter);
std::vector<FilterWidget*> _filters; // Free Search, Genre, Artist, Release, etc.
bool _refreshingFilters;
};
} // namespace UserInterface
#endif
+76 -41
View File
@@ -17,10 +17,20 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <Wt/WBreak> #include <boost/bind.hpp>
#include <Wt/WVBoxLayout>
#include <Wt/WHBoxLayout>
#include <Wt/WGridLayout>
#include <Wt/WComboBox>
#include <Wt/WPushButton>
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "TableFilter.hpp"
#include "KeywordSearchFilter.hpp"
#include "TrackView.hpp"
#include "AudioWidget.hpp" #include "AudioWidget.hpp"
namespace UserInterface { namespace UserInterface {
@@ -28,33 +38,80 @@ namespace UserInterface {
AudioWidget::AudioWidget(SessionData& sessionData, Wt::WContainerWidget* parent) AudioWidget::AudioWidget(SessionData& sessionData, Wt::WContainerWidget* parent)
: Wt::WContainerWidget(parent), : Wt::WContainerWidget(parent),
_db(sessionData.getDatabaseHandler()), _db(sessionData.getDatabaseHandler()),
_audioDbWidget(nullptr), _mediaPlayer(nullptr)
_mediaPlayer(nullptr),
_imgResource(nullptr),
_img(nullptr)
{ {
_audioDbWidget = new AudioDatabaseWidget(sessionData.getDatabaseHandler(), this);
_audioDbWidget->trackSelected().connect(this, &AudioWidget::playTrack); Wt::WGridLayout *mainLayout = new Wt::WGridLayout();
this->setLayout(mainLayout);
// Filters
Wt::WHBoxLayout *filterLayout = new Wt::WHBoxLayout();
{
TableFilter *filter = new TableFilter(_db, "genre", "name");
filterLayout->addWidget(filter);
_filterChain.addFilter(filter);
}
{
TableFilter *filter = new TableFilter(_db, "artist", "name");
filterLayout->addWidget(filter);
_filterChain.addFilter(filter);
}
{
TableFilter *filter = new TableFilter(_db, "release", "name");
filterLayout->addWidget(filter);
_filterChain.addFilter(filter);
}
mainLayout->addLayout(filterLayout, 0, 0);
// ENDOF(Filters)
TrackView* trackView = new TrackView(_db);
mainLayout->addWidget( trackView, 1, 0);
_filterChain.addFilter(trackView);
// TODO Playlist here
{
Wt::WVBoxLayout* playlist = new Wt::WVBoxLayout();
Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout();
Wt::WComboBox *cb = new Wt::WComboBox();
cb->addItem("metal");
cb->addItem("rock");
cb->addItem("top50");
cb->setWidth(100);
playlistControls->addWidget(cb, 1);
playlistControls->addWidget(new Wt::WPushButton("Rename"));
playlistControls->addWidget(new Wt::WPushButton("+"));
playlistControls->addWidget(new Wt::WPushButton("-"));
playlist->addLayout(playlistControls);
playlist->addWidget( new TableFilter(_db, "release", "name"), 1);
mainLayout->addLayout(playlist, 0, 1, 2, 1);
}
_mediaPlayer = new AudioMediaPlayerWidget();
mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 2);
mainLayout->setColumnStretch(0, 1);
mainLayout->setRowStretch(1, 1);
mainLayout->setRowResizable(0, true, Wt::WLength(200, Wt::WLength::Pixel));
mainLayout->setColumnResizable(0, true);
_mediaPlayer = new AudioMediaPlayerWidget(this);
_mediaPlayer->playbackEnded().connect(this, &AudioWidget::handleTrackEnded); _mediaPlayer->playbackEnded().connect(this, &AudioWidget::handleTrackEnded);
this->addWidget(new Wt::WBreak()); trackView->trackSelected().connect(boost::bind(&AudioWidget::playTrack, this, _1));
// Image
_imgResource = new Wt::WMemoryResource(this);
_imgLink.setResource( _imgResource);
_img = new Wt::WImage(_imgLink, this);
} }
void void
AudioWidget::search(const std::string& searchText) AudioWidget::search(const std::string& searchText)
{ {
_audioDbWidget->search(searchText); _filterChain.searchKeyword(searchText);
} }
void void
AudioWidget::playTrack(boost::filesystem::path p) AudioWidget::playTrack(boost::filesystem::path p)
{ {
@@ -71,7 +128,7 @@ AudioWidget::playTrack(boost::filesystem::path p)
bitrate = user->getAudioBitrate(); bitrate = user->getAudioBitrate();
else else
{ {
LMS_LOG(MOD_UI, SEV_ERROR) << "Can't play video: user does not exists!"; LMS_LOG(MOD_UI, SEV_ERROR) << "Can't play: user does not exists!";
return; // TODO logout? return; // TODO logout?
} }
} }
@@ -83,27 +140,6 @@ AudioWidget::playTrack(boost::filesystem::path p)
parameters.setBitrate(Transcode::Stream::Audio, bitrate); parameters.setBitrate(Transcode::Stream::Audio, bitrate);
_mediaPlayer->load( parameters ); _mediaPlayer->load( parameters );
// Refresh cover
{
std::vector<CoverArt::CoverArt> covers = inputFile.getCovers();
if (!covers.empty())
{
LMS_LOG(MOD_UI, SEV_DEBUG) << "Cover found!";
if (!covers.front().scale(256)) // TODO
LMS_LOG(MOD_UI, SEV_ERROR) << "Cannot resize!";
//_imgResource->setMimeType(covers.front().getMimeType());
_imgResource->setData(covers.front().getData());
}
else {
LMS_LOG(MOD_UI, SEV_DEBUG) << "No cover found!";
_imgResource->setData( std::vector<unsigned char>());
}
_imgResource->setChanged();
}
} }
catch( std::exception &e) catch( std::exception &e)
{ {
@@ -111,11 +147,10 @@ AudioWidget::playTrack(boost::filesystem::path p)
} }
} }
void void
AudioWidget::handleTrackEnded(void) AudioWidget::handleTrackEnded(void)
{ {
LMS_LOG(MOD_UI, SEV_DEBUG) << "Track playback ended!"; LMS_LOG(MOD_UI, SEV_DEBUG) << "Track playback ended!";
_audioDbWidget->selectNextTrack();
} }
} // namespace UserInterface } // namespace UserInterface
+6 -13
View File
@@ -22,13 +22,13 @@
#include <string> #include <string>
#include <Wt/WLink>
#include <Wt/WImage>
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
#include <Wt/WMemoryResource>
#include "audio/AudioMediaPlayerWidget.hpp" #include "common/SessionData.hpp"
#include "audio/AudioDatabaseWidget.hpp"
#include "AudioMediaPlayerWidget.hpp"
#include "FilterChain.hpp"
namespace UserInterface { namespace UserInterface {
@@ -49,16 +49,9 @@ class AudioWidget : public Wt::WContainerWidget
Database::Handler& _db; Database::Handler& _db;
AudioDatabaseWidget* _audioDbWidget;
AudioMediaPlayerWidget* _mediaPlayer; AudioMediaPlayerWidget* _mediaPlayer;
// Image FilterChain _filterChain;
Wt::WMemoryResource *_imgResource;
Wt::WLink _imgLink;
Wt::WImage *_img;
}; };
@@ -17,35 +17,30 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef FILTER_WIDGET_HPP #ifndef FILTER_HPP
#define FILTER_WIDGET_HPP #define FILTER_HPP
#include <boost/foreach.hpp>
#include <string>
#include <list>
#include <Wt/WSignal> #include <Wt/WSignal>
#include <Wt/WContainerWidget>
#include "database/SqlQuery.hpp" #include "database/SqlQuery.hpp"
namespace UserInterface { namespace UserInterface {
class FilterWidget : public Wt::WContainerWidget { class Filter
{
public: public:
struct Constraint { struct Constraint {
WhereClause where; // WHERE SQL clause WhereClause where; // WHERE SQL clause
}; };
FilterWidget(Wt::WContainerWidget* parent = 0) : Wt::WContainerWidget(parent) {} Filter() {}
virtual ~FilterWidget() {} virtual ~Filter() {}
// Refresh filter Widget using constraints created by parent filters // Refresh filter using constraints created by parent filters
virtual void refresh(const Constraint& constraint) = 0; virtual void refresh(const Constraint& constraint) = 0;
// Update constraints for child filters // Update constraints for next filters
virtual void getConstraint(Constraint& constraint) = 0; virtual void getConstraint(Constraint& constraint) = 0;
// Emitted when a constraint has changed // Emitted when a constraint has changed
+76
View File
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2014 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 "FilterChain.hpp"
namespace UserInterface {
FilterChain::FilterChain()
: _refreshingFilters(false)
{
addFilter(&_keywordSearchFilter);
}
void
FilterChain::addFilter(Filter* filter)
{
filter->update().connect(boost::bind(&FilterChain::updateFilters, this, _filters.size()));
_filters.push_back(filter);
}
void
FilterChain::searchKeyword(const std::string& text)
{
_keywordSearchFilter.setText(text);
}
void
FilterChain::updateFilters(std::size_t startIdx)
{
// Prevent loops
if (_refreshingFilters)
return;
_refreshingFilters = true;
Filter::Constraint currentConstraint;
// TODO: replace with inner joins?
currentConstraint.where.And( WhereClause( "track.artist_id = artist.id and track.release_id = release.id and track_genre.track_id = track.id and genre.id = track_genre.genre_id"));
for (std::size_t idFilter = 0; idFilter < _filters.size(); ++idFilter)
{
Filter* filter = _filters.at(idFilter);
// Apply contraints created by previous filters
if (idFilter > startIdx) {
filter->refresh(currentConstraint);
}
// Get constraints generated by this filter
// (Note: adding accross successive calls)
filter->getConstraint(currentConstraint);
}
_refreshingFilters = false;
}
} // namespace UserInterface
+57
View File
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2014 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/>.
*/
#ifndef FILTER_CHAIN_HPP
#define FILTER_CHAIN_HPP
#include <Wt/WSignal>
#include "Filter.hpp"
#include "KeywordSearchFilter.hpp"
namespace UserInterface {
// FilterChain
class FilterChain
{
public:
FilterChain();
void addFilter(Filter* filter);
// First filter is a keywork search
void searchKeyword(const std::string& text);
// Update filters from filter @ startIdx
void updateFilters(std::size_t startIdx);
private:
KeywordSearchFilter _keywordSearchFilter;
// No ownership
std::vector<Filter*> _filters;
bool _refreshingFilters;
};
} // namespace UserInterface
#endif
@@ -17,19 +17,16 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <Wt/WLabel> #include "KeywordSearchFilter.hpp"
#include "SearchFilterWidget.hpp"
namespace UserInterface { namespace UserInterface {
SearchFilterWidget::SearchFilterWidget(Wt::WContainerWidget* parent) KeywordSearchFilter::KeywordSearchFilter()
: FilterWidget( parent )
{ {
} }
void void
SearchFilterWidget::setText(const std::string& text) KeywordSearchFilter::setText(const std::string& text)
{ {
_lastEmittedText = text; _lastEmittedText = text;
emitUpdate(); emitUpdate();
@@ -37,7 +34,7 @@ SearchFilterWidget::setText(const std::string& text)
// Get constraints created by this filter // Get constraints created by this filter
void void
SearchFilterWidget::getConstraint(Constraint& constraint) KeywordSearchFilter::getConstraint(Constraint& constraint)
{ {
// No active search means no constaint! // No active search means no constaint!
if (!_lastEmittedText.empty()) { if (!_lastEmittedText.empty()) {
@@ -17,18 +17,19 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef SEARCH_FILTER_WIDGET_PP #ifndef KEYWORD_SEARCH_FILTER_HPP
#define SEARCH_FILTER_WIDGET_PP #define KEYWORD_SEARCH_FILTER_HPP
#include <Wt/WLineEdit> #include <string>
#include "FilterWidget.hpp" #include "Filter.hpp"
namespace UserInterface { namespace UserInterface {
class SearchFilterWidget : public FilterWidget { class KeywordSearchFilter : public Filter
{
public: public:
SearchFilterWidget(Wt::WContainerWidget* parent = 0); KeywordSearchFilter();
void setText(const std::string& text); void setText(const std::string& text);
@@ -21,36 +21,49 @@
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "TableFilterWidget.hpp" #include "TableFilter.hpp"
namespace UserInterface { namespace UserInterface {
TableFilterWidget::TableFilterWidget(Database::Handler& db, std::string table, std::string field, Wt::WContainerWidget* parent) TableFilter::TableFilter(Database::Handler& db, std::string table, std::string field, Wt::WContainerWidget* parent)
: FilterWidget( parent ), : Wt::WTableView( parent ),
Filter(),
_db(db), _db(db),
_table(table), _table(table),
_field(field), _field(field)
_tableView(nullptr)
{ {
_queryModel.setQuery( _db.getSession().query< ResultType >("select " + _table + "." + _field + ",count(DISTINCT track.id),0 as ORDERBY from track,artist,release,genre,track_genre WHERE track.artist_id = artist.id and track.release_id = release.id and track_genre.track_id = track.id and genre.id = track_genre.genre_id GROUP BY " + _table + "." + _field + " UNION select '<All>',0,1 AS ORDERBY").orderBy("ORDERBY DESC," + _table + "." + _field)); _queryModel.setQuery( _db.getSession().query< ResultType >("select " + _table + "." + _field + ",count(DISTINCT track.id),0 as ORDERBY from track,artist,release,genre,track_genre WHERE track.artist_id = artist.id and track.release_id = release.id and track_genre.track_id = track.id and genre.id = track_genre.genre_id GROUP BY " + _table + "." + _field + " UNION select '<All>',0,1 AS ORDERBY").orderBy("ORDERBY DESC," + _table + "." + _field));
_queryModel.addColumn( _table + "." + _field, table); _queryModel.addColumn( _table + "." + _field, table);
_queryModel.addColumn( "count(DISTINCT track.id)", "Tracks"); _queryModel.addColumn( "count(DISTINCT track.id)", "Tracks");
_tableView = new Wt::WTableView( this ); this->setSelectionMode(Wt::ExtendedSelection);
_tableView->resize(250, 200); // TODO this->setSortingEnabled(false);
_tableView->setSelectionMode(Wt::ExtendedSelection); this->setAlternatingRowColors(true);
_tableView->setSortingEnabled(false); this->setModel(&_queryModel);
_tableView->setAlternatingRowColors(true);
_tableView->setModel(&_queryModel);
_tableView->selectionChanged().connect(this, &TableFilterWidget::emitUpdate); this->selectionChanged().connect(this, &TableFilter::emitUpdate);
setLayoutSizeAware(true);
_queryModel.setBatchSize(100); _queryModel.setBatchSize(100);
} }
void
TableFilter::layoutSizeChanged (int width, int height)
{
LMS_LOG(MOD_UI, SEV_DEBUG) << "LAYOUT CHANGED!";
/* TODO
std::size_t trackColumnSize = this->columnWidth(1).toPixels() + 30 ;
// Set the remaining size for the name column
this->setColumnWidth(0, width - 7 - trackColumnSize);
*/
}
// Set constraints on this filter // Set constraints on this filter
void void
TableFilterWidget::refresh(const Constraint& constraint) TableFilter::refresh(const Constraint& constraint)
{ {
SqlQuery sqlQuery; SqlQuery sqlQuery;
@@ -73,16 +86,16 @@ TableFilterWidget::refresh(const Constraint& constraint)
query.bind(bindArg); query.bind(bindArg);
} }
_queryModel.setQuery( query, true /* Keep columns */); _queryModel.setQuery( query, true );
LMS_LOG(MOD_UI, SEV_DEBUG) << "Finish !"; LMS_LOG(MOD_UI, SEV_DEBUG) << "Finish !";
} }
// Get constraint created by this filter // Get constraint created by this filter
void void
TableFilterWidget::getConstraint(Constraint& constraint) TableFilter::getConstraint(Constraint& constraint)
{ {
Wt::WModelIndexSet indexSet = _tableView->selectedIndexes(); Wt::WModelIndexSet indexSet = this->selectedIndexes();
// WHERE statement // WHERE statement
WhereClause clause; WhereClause clause;
@@ -17,29 +17,31 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef TABLE_VIEW_FILTER_WIDGET_HPP #ifndef TABLE_FILTER_HPP
#define TABLE_VIEW_FILTER_WIDGET_HPP #define TABLE_FILTER_HPP
#include <Wt/Dbo/QueryModel> #include <Wt/Dbo/QueryModel>
#include <Wt/WTableView> #include <Wt/WTableView>
#include "FilterWidget.hpp" #include "Filter.hpp"
#include "database/DatabaseHandler.hpp" #include "database/DatabaseHandler.hpp"
namespace UserInterface { namespace UserInterface {
class TableFilterWidget : public FilterWidget class TableFilter : public Wt::WTableView, public Filter
{ {
public: public:
TableFilterWidget(Database::Handler& db, std::string table, std::string field, Wt::WContainerWidget* parent = 0); TableFilter(Database::Handler& db, std::string table, std::string field, Wt::WContainerWidget* parent = 0);
// Set constraints on this filter // Set constraints on this filter
virtual void refresh(const Constraint& constraint); void refresh(const Constraint& constraint);
// Get constraints created by this filter // Get constraints created by this filter
virtual void getConstraint(Constraint& constraint); void getConstraint(Constraint& constraint);
void layoutSizeChanged (int width, int height);
protected: protected:
@@ -51,7 +53,6 @@ class TableFilterWidget : public FilterWidget
typedef boost::tuple<std::string, int, int> ResultType; typedef boost::tuple<std::string, int, int> ResultType;
Wt::Dbo::QueryModel< ResultType > _queryModel; Wt::Dbo::QueryModel< ResultType > _queryModel;
Wt::WTableView* _tableView;
}; };
} // namespace UserInterface } // namespace UserInterface
@@ -25,18 +25,14 @@
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "TrackWidget.hpp" #include "TrackView.hpp"
namespace UserInterface { namespace UserInterface {
TrackWidget::TrackWidget( Database::Handler& db, Wt::WContainerWidget* parent) TrackView::TrackView( Database::Handler& db, Wt::WContainerWidget* parent)
: FilterWidget( parent ), : Wt::WTableView( parent ),
_db(db), _db(db)
_tableView(nullptr),
_trackStats(nullptr)
{ {
// ----- TRACK -----
_queryModel.setQuery(_db.getSession().query<ResultType>("select track,release,artist from track,release,artist where track.release_id = release.id and track.artist_id = artist.id" ).orderBy("artist.name,release.name,track.disc_number,track.track_number")); _queryModel.setQuery(_db.getSession().query<ResultType>("select track,release,artist from track,release,artist where track.release_id = release.id and track.artist_id = artist.id" ).orderBy("artist.name,release.name,track.disc_number,track.track_number"));
_queryModel.addColumn( "artist.name", "Artist" ); _queryModel.addColumn( "artist.name", "Artist" );
_queryModel.addColumn( "release.name", "Album" ); _queryModel.addColumn( "release.name", "Album" );
@@ -48,56 +44,52 @@ _trackStats(nullptr)
_queryModel.addColumn( "track.original_date", "Original Date" ); _queryModel.addColumn( "track.original_date", "Original Date" );
_queryModel.addColumn( "track.genre_list", "Genres" ); _queryModel.addColumn( "track.genre_list", "Genres" );
_queryModel.setBatchSize(1000); _queryModel.setBatchSize(250);
_tableView = new Wt::WTableView( this ); this->setSortingEnabled(true);
_tableView->resize(Wt::WLength::Auto, 400); this->setSelectionMode(Wt::SingleSelection);
this->setAlternatingRowColors(true);
this->setModel(&_queryModel);
_tableView->setSortingEnabled(true); this->setColumnWidth(0, 180); // Artist
_tableView->setSelectionMode(Wt::SingleSelection); this->setColumnWidth(1, 180); // Album
_tableView->setAlternatingRowColors(true); this->setColumnWidth(2, 70); // Disc Number
_tableView->setModel(&_queryModel); this->setColumnWidth(3, 70); // Track Number
this->setColumnWidth(4, 180); // Track
this->setColumnWidth(5, 70); // Duration
this->setColumnWidth(6, 70); // Date
this->setColumnWidth(7, 70); // Original Date
this->setColumnWidth(8, 180); // Genres
// this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
// Duration display // Duration display
{ {
// TODO better handle 1 hour+ files! // TODO better handle 1 hour+ files!
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this); Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
delegate->setTextFormat("mm:ss"); delegate->setTextFormat("mm:ss");
_tableView->setItemDelegateForColumn(5, delegate); this->setItemDelegateForColumn(5, delegate);
} }
// Date display, just the year // Date display, just the year
{ {
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this); Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
delegate->setTextFormat("yyyy"); delegate->setTextFormat("yyyy");
_tableView->setItemDelegateForColumn(6, delegate); this->setItemDelegateForColumn(6, delegate);
} }
{ {
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this); Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
delegate->setTextFormat("yyyy"); delegate->setTextFormat("yyyy");
_tableView->setItemDelegateForColumn(7, delegate); this->setItemDelegateForColumn(7, delegate);
} }
// TODO other event! // TODO other event!
_tableView->selectionChanged().connect(this, &TrackWidget::handleTrackSelected); this->selectionChanged().connect(this, &TrackView::handleTrackSelected);
new Wt::WBreak(this);
_trackStats = new Wt::WText(this);
updateStats();
}
void
TrackWidget::updateStats(void)
{
std::ostringstream oss; oss << "Files :" << _tableView->model()->rowCount();
// TODO from UTF-8 ?
_trackStats->setText(oss.str());
} }
// Set constraints created by parent filters // Set constraints created by parent filters
void void
TrackWidget::refresh(const Constraint& constraint) TrackView::refresh(const Constraint& constraint)
{ {
SqlQuery sqlQuery; SqlQuery sqlQuery;
@@ -119,13 +111,12 @@ TrackWidget::refresh(const Constraint& constraint)
_queryModel.setQuery( query, true ); _queryModel.setQuery( query, true );
updateStats();
} }
void void
TrackWidget::handleTrackSelected(void) TrackView::handleTrackSelected(void)
{ {
Wt::WModelIndexSet indexSet = _tableView->selectedIndexes(); Wt::WModelIndexSet indexSet = this->selectedIndexes();
if (!indexSet.empty()) { if (!indexSet.empty()) {
Wt::WModelIndex currentIndex( *indexSet.begin() ); Wt::WModelIndex currentIndex( *indexSet.begin() );
@@ -144,15 +135,15 @@ TrackWidget::handleTrackSelected(void)
} }
void void
TrackWidget::selectNextTrack(void) TrackView::selectNextTrack(void)
{ {
Wt::WModelIndexSet indexSet = _tableView->selectedIndexes(); Wt::WModelIndexSet indexSet = this->selectedIndexes();
if (!indexSet.empty()) { if (!indexSet.empty()) {
Wt::WModelIndex currentIndex( *indexSet.begin() ); Wt::WModelIndex currentIndex( *indexSet.begin() );
// Check there are remainin tracks! // Check there are remainin tracks!
if (currentIndex.isValid() && _tableView->model()->rowCount() > currentIndex.row() + 1) if (currentIndex.isValid() && this->model()->rowCount() > currentIndex.row() + 1)
{ {
_tableView->select( _tableView->model()->index( currentIndex.row() + 1, currentIndex.column())); this->select( this->model()->index( currentIndex.row() + 1, currentIndex.column()));
ResultType result = _queryModel.resultRow( currentIndex.row() + 1 ); ResultType result = _queryModel.resultRow( currentIndex.row() + 1 );
@@ -17,8 +17,8 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef TRACK_WIDGET_HPP #ifndef TRACK_HPP
#define TRACK_WIDGET_HPP #define TRACK_HPP
#include <Wt/WTableView> #include <Wt/WTableView>
#include <Wt/Dbo/QueryModel> #include <Wt/Dbo/QueryModel>
@@ -26,15 +26,15 @@
#include "database/DatabaseHandler.hpp" #include "database/DatabaseHandler.hpp"
#include "database/AudioTypes.hpp" #include "database/AudioTypes.hpp"
#include "FilterWidget.hpp" #include "Filter.hpp"
namespace UserInterface { namespace UserInterface {
class TrackWidget : public FilterWidget class TrackView : public Wt::WTableView, public Filter
{ {
public: public:
TrackWidget( Database::Handler& db, Wt::WContainerWidget* parent = 0); TrackView( Database::Handler& db, Wt::WContainerWidget* parent = 0);
// Set constraints created by parent filters // Set constraints created by parent filters
void refresh(const Constraint& constraint); void refresh(const Constraint& constraint);
@@ -59,10 +59,6 @@ class TrackWidget : public FilterWidget
Wt::Dbo::QueryModel< ResultType > _queryModel; Wt::Dbo::QueryModel< ResultType > _queryModel;
Wt::WTableView* _tableView; Wt::WTableView* _tableView;
void updateStats(void);
Wt::WText* _trackStats;
}; };
} // namespace UserInterface } // namespace UserInterface
+1 -1
View File
@@ -750,7 +750,7 @@ std::set<Test> tests = {
Test::ReleaseFilterGenre, Test::ReleaseFilterGenre,
// Test::TrackFilters, // Test::TrackFilters,
// Test::Cover, // Test::Cover,
// Test::Transcode, Test::Transcode,
}; };
bool test(Test t) bool test(Test t)