Newer
Older
Kevin Whitaker
committed
/*
* Copyright (C) 2017 Kevin Whitaker <eyecreate@gmail.com>
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
#include "GroovePlayer.h"
#include "db/UserAction.h"
#include <Wt/WLogger>
#include <Wt/WServer>
#include <Wt/WApplication>
#include "WebInterface.h"
Kevin Whitaker
committed
#include <locale>
#include <Wt/WLogger>
#include <Wt/Dbo/Transaction>
Kevin Whitaker
committed
#include <Wt/WDateTime>
#include <groovefingerprinter/fingerprinter.h>
Kevin Whitaker
committed
#include <grooveplayer/player.h>
Kevin Whitaker
committed
#include <taglib/fileref.h>
#include <taglib/mpegfile.h>
#include <taglib/id3v2tag.h>
#include <taglib/attachedpictureframe.h>
Kevin Whitaker
committed
Kevin Whitaker
committed
GroovePlayerMgr::GroovePlayerMgr (std::string dbFile)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
sqliteConnection = new Wt::Dbo::backend::Sqlite3(dbFile);
connectionPool = new Wt::Dbo::FixedSqlConnectionPool(sqliteConnection, 3);
Kevin Whitaker
committed
musicScanDir = std::filesystem::current_path()/"music";
Kevin Whitaker
committed
Wt::Dbo::Session sqlSession;
Kevin Whitaker
committed
sqlSession.setConnectionPool(*connectionPool);
sqlSession.mapClass<User>("user");
sqlSession.mapClass<AudioTrack>("tracks");
sqlSession.mapClass<UserAction>("actions");
try
{
sqlSession.createTables();
} catch(Wt::Dbo::Exception e)
{
Wt::log("info") << "Using Existing DB.";
}
groove_init();
grooveAudioScanner = new std::thread([this](){grooveAudioScannerLoop();});
grooveEvents = new std::thread([this](){grooveEventLoop();});
Kevin Whitaker
committed
}
Kevin Whitaker
committed
GroovePlayerMgr::~GroovePlayerMgr()
{
delete connectionPool;
}
Kevin Whitaker
committed
std::list<AudioTrack> GroovePlayerMgr::getNextVoteBatch(Wt::Dbo::Session* session)
Kevin Whitaker
committed
/**
* This method will attempt to pick 3 tracks that will be up for selection next.
*
* The first track should be from the same album or artist.
* The second track should be from the same genre.
* The third track should be from something in a different genre.
*
* What is picked inside those requirements is a mix of random and TODO:based on past user actions(33% of time).
*
* The third track will be replaced by a recent request item if one exists.
*/
Kevin Whitaker
committed
std::list<AudioTrack> selectedTracks;
Kevin Whitaker
committed
Kevin Whitaker
committed
Wt::Dbo::Transaction transaction(*session);
Kevin Whitaker
committed
//First make sure there are at least 3 tracks to suggest.
Kevin Whitaker
committed
int trackCount = session->query<int>("select count(fingerprint) from tracks");
Kevin Whitaker
committed
if(trackCount < 3)
{
//Return empty list to show there are not enough tracks to allow voting.
return selectedTracks;
}
//Determine first track
Kevin Whitaker
committed
int trackAlbumCount = session->query<int>("select count(fingerprint) from tracks").where("album = ?").bind(currentTrack.trackAlbumName);
int trackArtistCount = session->query<int>("select count(fingerprint) from tracks").where("artist = ?").bind(currentTrack.trackArtistName);
Kevin Whitaker
committed
int computerSlightOfHand = rand() % 10 + 1;
Kevin Whitaker
committed
if(trackAlbumCount > 0 && ((computerSlightOfHand > 3 && trackAlbumCount > 1) || (computerSlightOfHand > 3 && trackAlbumCount == 1 && trackArtistCount == 1) || !(trackArtistCount > 1 && trackAlbumCount == 1)))
Kevin Whitaker
committed
{
//Pick item from album
Kevin Whitaker
committed
Wt::Dbo::ptr<AudioTrack> eligibleTrack = session->find<AudioTrack>().where("album = ?").limit(1).offset(rand() % trackAlbumCount).bind(currentTrack.trackAlbumName);
//Try to make sure the same track isn't up to vote again.
if((*eligibleTrack).trackFingerprint == currentTrack.trackFingerprint && trackAlbumCount > 1)
{
while((*eligibleTrack).trackFingerprint == currentTrack.trackFingerprint)
{
eligibleTrack = session->find<AudioTrack>().where("album = ?").limit(1).offset(rand() % trackAlbumCount).bind(currentTrack.trackAlbumName);
}
}
Kevin Whitaker
committed
AudioTrack selected = *eligibleTrack;
getPictureFromTrack(&selected);
selectedTracks.push_back(selected);
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else
Kevin Whitaker
committed
{
//Pick item from artist
Kevin Whitaker
committed
Wt::Dbo::ptr<AudioTrack> eligibleTrack = session->find<AudioTrack>().where("artist = ?").limit(1).offset(rand() % trackArtistCount).bind(currentTrack.trackArtistName);
//Try to make sure the same track isn't up to vote again.
if((*eligibleTrack).trackFingerprint == currentTrack.trackFingerprint && trackArtistCount > 1)
{
while((*eligibleTrack).trackFingerprint == currentTrack.trackFingerprint)
{
eligibleTrack = session->find<AudioTrack>().where("artist = ?").limit(1).offset(rand() % trackArtistCount).bind(currentTrack.trackArtistName);
}
}
Kevin Whitaker
committed
AudioTrack selected = *eligibleTrack;
getPictureFromTrack(&selected);
selectedTracks.push_back(selected);
Kevin Whitaker
committed
}
//Determine second track
Kevin Whitaker
committed
int trackGenreCount = session->query<int>("select count(fingerprint) from tracks").where("genre = ?").bind(currentTrack.trackGenre);
Kevin Whitaker
committed
if(trackGenreCount > 0)
{
//Pick item from genre
Kevin Whitaker
committed
Wt::Dbo::ptr<AudioTrack> eligibleTrack = session->find<AudioTrack>().where("genre = ?").limit(1).offset(rand() % trackGenreCount).bind(currentTrack.trackGenre);
Kevin Whitaker
committed
//Try to make sure same track isn't up to vote again and isn't the same as first votable track.
if(((*eligibleTrack).trackFingerprint == currentTrack.trackFingerprint || (*eligibleTrack).trackFingerprint == selectedTracks.front().trackFingerprint) && trackGenreCount > 2)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
while((*eligibleTrack).trackFingerprint == currentTrack.trackFingerprint || (*eligibleTrack).trackFingerprint == selectedTracks.front().trackFingerprint)
Kevin Whitaker
committed
{
eligibleTrack = session->find<AudioTrack>().where("genre = ?").limit(1).offset(rand() % trackGenreCount).bind(currentTrack.trackGenre);
}
}
Kevin Whitaker
committed
AudioTrack selected = *eligibleTrack;
getPictureFromTrack(&selected);
selectedTracks.push_back(selected);
Kevin Whitaker
committed
}
//Determine third track
Kevin Whitaker
committed
int trackNotGenreCount = session->query<int>("select count(fingerprint) from tracks").where("genre != ?").bind(currentTrack.trackGenre);
Kevin Whitaker
committed
if(requestQueue.size() > 0)
{
//There's a request. Pick one up front and put as third item.
Kevin Whitaker
committed
AudioTrack selected = requestQueue.front();
getPictureFromTrack(&selected);
selectedTracks.push_back(selected);
Kevin Whitaker
committed
}
else if(trackNotGenreCount > 0)
{
//Pick from other genre
Kevin Whitaker
committed
Wt::Dbo::ptr<AudioTrack> eligibleTrack = session->find<AudioTrack>().where("genre != ?").limit(1).offset(rand() % trackNotGenreCount).bind(currentTrack.trackGenre);
Kevin Whitaker
committed
AudioTrack selected = *eligibleTrack;
getPictureFromTrack(&selected);
selectedTracks.push_back(selected);
Kevin Whitaker
committed
}
else
{
//Pick randomly
Kevin Whitaker
committed
Wt::Dbo::ptr<AudioTrack> eligibleTrack = session->find<AudioTrack>().limit(1).offset(rand() % trackCount);
Kevin Whitaker
committed
AudioTrack selected = *eligibleTrack;
getPictureFromTrack(&selected);
selectedTracks.push_back(selected);
Kevin Whitaker
committed
}
Kevin Whitaker
committed
transaction.commit();
Wt::log("info") << "Next set of tracks to vote picked: 1=>[" << (*selectedTracks.begin()).trackName << "] 2=>[" << (*std::next(selectedTracks.begin(),1)).trackName << "] 3=>[" << (*std::next(selectedTracks.begin(),2)).trackName << "]";
Kevin Whitaker
committed
return selectedTracks;
Kevin Whitaker
committed
void GroovePlayerMgr::grooveEventLoop()
Kevin Whitaker
committed
{
/**
Kevin Whitaker
committed
* On first boot, random track that has been voted before will start(or random if no track has been played before).
* When voting ends, if no vote was cast, the first will be picked 50% of the time with second being 30% of the time and third being picked 20% of the time.
Kevin Whitaker
committed
* Exception is if third track is the top request, then it is picked. If another song is voted over this, it will be put in back of request queue.
Kevin Whitaker
committed
*/
Kevin Whitaker
committed
Kevin Whitaker
committed
Wt::Dbo::Session sqlSession;
Kevin Whitaker
committed
sqlSession.setConnectionPool(*connectionPool);
Kevin Whitaker
committed
sqlSession.mapClass<User>("user");
sqlSession.mapClass<AudioTrack>("tracks");
sqlSession.mapClass<UserAction>("actions");
Kevin Whitaker
committed
Kevin Whitaker
committed
Wt::Dbo::Transaction transaction(sqlSession);
Kevin Whitaker
committed
int trackCount = sqlSession.query<int>("select count(fingerprint) from tracks");
transaction.commit();
Kevin Whitaker
committed
Kevin Whitaker
committed
//Wait until at least 3 tracks are in DB to prevent nothing to vote on.
if(trackCount < 3)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
while(trackCount < 3)
{
Wt::Dbo::Transaction transaction(sqlSession);
trackCount = sqlSession.query<int>("select count(fingerprint) from tracks");
transaction.commit();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
Kevin Whitaker
committed
}
Kevin Whitaker
committed
Wt::Dbo::Transaction bootTransaction(sqlSession);
Kevin Whitaker
committed
Kevin Whitaker
committed
//Pick initial track to bootstrap.
int tracksPlayedBefore = sqlSession.query<int>("select count(fingerprint) from tracks as t join actions on (actions.action = 3 or actions.action = 2) and actions.track_id = t.id");
Kevin Whitaker
committed
AudioTrack selectedTrack;
Kevin Whitaker
committed
if (tracksPlayedBefore > 0)
{
Kevin Whitaker
committed
selectedTrack = *sqlSession.query<Wt::Dbo::ptr<AudioTrack>>("select t from tracks as t join actions on (actions.action = 3 or actions.action = 2) and actions.track_id = t.id").limit(1).offset(rand() % tracksPlayedBefore).resultValue();
Kevin Whitaker
committed
}
else
{
Kevin Whitaker
committed
int trackCount = sqlSession.query<int>("select count(fingerprint) from tracks");
Kevin Whitaker
committed
selectedTrack = *sqlSession.find<AudioTrack>().limit(1).offset(rand() % trackCount).resultValue();
Kevin Whitaker
committed
}
Kevin Whitaker
committed
bootTransaction.commit();
Kevin Whitaker
committed
struct GroovePlaylist* playlist = groove_playlist_create();
Kevin Whitaker
committed
currentPlaylist = playlist;
Kevin Whitaker
committed
struct GroovePlayer* player = groove_player_create();
if(!player) {return;}
Kevin Whitaker
committed
currentPlayer = player;
Kevin Whitaker
committed
Kevin Whitaker
committed
groove_playlist_insert(playlist, groove_file_open(selectedTrack.trackPath.c_str()),1.0,1.0,nullptr);
Kevin Whitaker
committed
Kevin Whitaker
committed
//Now boostrap player with initial data
Kevin Whitaker
committed
groove_player_attach(player, playlist);
Kevin Whitaker
committed
groove_playlist_play(playlist);
Kevin Whitaker
committed
Kevin Whitaker
committed
//Now start loop
Kevin Whitaker
committed
while(getInstance()->continueEventLoop)
{
Kevin Whitaker
committed
std::this_thread::sleep_for(std::chrono::milliseconds(200));
Kevin Whitaker
committed
PlayerEvent event = getNextPlayerEvent(&sqlSession);
if(event.eventType == NOTHING)
Kevin Whitaker
committed
{
continue;
}
Kevin Whitaker
committed
else if(event.eventType == GROOVE_NOWPLAYING)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
currentSkipRequester.clear();
Kevin Whitaker
committed
currentTrack = getCurrentTrackDB(&sqlSession);
Kevin Whitaker
committed
getPictureFromTrack(¤tTrack);
Kevin Whitaker
committed
//Pick new batch of tracks to vote on and inform UI of update.
currentVoteBatch = getNextVoteBatch(&sqlSession);
Kevin Whitaker
committed
currentVoteStatus = std::list<std::pair<AudioTrack, int>>();
for(auto track : currentVoteBatch)
{
currentVoteStatus.push_back(std::pair<AudioTrack, int>(track,0));
}
Kevin Whitaker
committed
Wt::WServer::instance()->postAll([&]() {
Wt::WApplication* app = Wt::WApplication::instance();
if(app != nullptr)
{
Kevin Whitaker
committed
static_cast<WebInterface*>(app)->songChangedFromServer(currentTrack);
Kevin Whitaker
committed
static_cast<WebInterface*>(app)->voteTracksUpdatedFromServer(currentVoteBatch);
}
}
);
Kevin Whitaker
committed
Wt::log("info") << "Track playing changed to: " << currentTrack.trackName;
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == VOTING_ENDED)
Kevin Whitaker
committed
{
//Look at votes and add correct track to end of playlist.
//If more than one track has highest votes, pick random. TODO:maybe base off of other play data instead.
Kevin Whitaker
committed
std::list<std::pair<AudioTrack,int>> trackWinners;
for(auto track : currentVoteStatus)
Kevin Whitaker
committed
{
//There nothing yet and there was at least a vote is an initial winner.
Kevin Whitaker
committed
if(trackWinners.size() == 0 && track.second > 0)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
trackWinners.push_back(track);
Kevin Whitaker
committed
continue;
}
//If there is a winner. Check if higher. If so, clear the list and add this one. If equal, add to list.
if(trackWinners.size() > 0)
{
Kevin Whitaker
committed
if(trackWinners.front().second < track.second)
Kevin Whitaker
committed
{
trackWinners.clear();
Kevin Whitaker
committed
trackWinners.push_back(std::pair<AudioTrack,int>(track.first, track.second));
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(trackWinners.front().second == track.second)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
trackWinners.push_back(std::pair<AudioTrack, int>(track.first, track.second));
Kevin Whitaker
committed
}
}
}
Kevin Whitaker
committed
AudioTrack winner;
Kevin Whitaker
committed
//Randomly pick from winners(in case of tie), if there are any.
if(trackWinners.size() > 0)
{
auto iter = trackWinners.begin();
std::advance(iter,rand() % trackWinners.size());
winner = (*iter).first;
//Deal with requests if there are any.
if(requestQueue.size() > 0)
{
//If winning track was a request, now you can remove it from request queue.
Kevin Whitaker
committed
if(requestQueue.front().trackFingerprint == winner.trackFingerprint)
Kevin Whitaker
committed
{
requestQueue.pop_front();
}
//Request didn't win. Put request at bottom of queue.
else
{
requestQueue.push_back(requestQueue.front());
requestQueue.pop_front();
}
}
}
else
{
//Pick request song if there are any.
if(requestQueue.size() > 0)
{
winner = requestQueue.front();
requestQueue.pop_front();
}
else
{
//Pick based on percentages.
int pick = rand() % 10 +1;
Kevin Whitaker
committed
if(pick <= 5 && currentVoteBatch.front().trackFingerprint != currentTrack.trackFingerprint)
Kevin Whitaker
committed
{
//First track wins
winner = currentVoteBatch.front();
}
Kevin Whitaker
committed
else if(pick <= 8 && (*std::next(currentVoteBatch.begin(),1)).trackFingerprint != currentTrack.trackFingerprint)
Kevin Whitaker
committed
{
//Second track wins.
winner = (*std::next(currentVoteBatch.begin(),1));
Kevin Whitaker
committed
}
else
{
//Third track wins
winner = (*std::next(currentVoteBatch.begin(),2));
Kevin Whitaker
committed
}
}
}
Kevin Whitaker
committed
groove_playlist_insert(currentPlaylist,groove_file_open(winner.trackPath.c_str()),1.0,1.0,nullptr);
Kevin Whitaker
committed
Wt::WServer::instance()->postAll([winner]() {
Kevin Whitaker
committed
Wt::WApplication* app = Wt::WApplication::instance();
if(app != nullptr)
{
Kevin Whitaker
committed
static_cast<WebInterface*>(app)->voteNextPollClosedFromServer(winner);
Kevin Whitaker
committed
}
}
);
Kevin Whitaker
committed
Wt::log("info")<< "Voting has ended. Next track is: " << winner.trackName;
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == VOTE_CAST)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
if(!voteEndedButNotNextTrackYet)
{
Kevin Whitaker
committed
//Add vote to DB
Wt::Dbo::Transaction voteTransaction(sqlSession);
UserAction* action = new UserAction();
action->action = UserAction::UAction::VoteTrack;
action->user = sqlSession.find<User>().where("username = ?").bind(event.userInvolved.username);
action->trackInvolved = sqlSession.find<AudioTrack>().where("fingerprint = ?").bind(event.tracksInvolved.front().trackFingerprint);
Kevin Whitaker
committed
action->datetime = Wt::WDateTime::currentDateTime();
sqlSession.add(action);
voteTransaction.commit();
//Add vote to vote status
for(std::pair<AudioTrack, int> voteItem: currentVoteStatus)
{
if(voteItem.first.trackFingerprint == event.tracksInvolved.front().trackFingerprint)
{
++(voteItem.second);
}
}
//Update vote display on all clients.
Wt::WServer::instance()->postAll([event]() {
Wt::WApplication* app = Wt::WApplication::instance();
if(app != nullptr)
{
static_cast<WebInterface*>(app)->voteNextSongFromServer(event.userInvolved, event.tracksInvolved.front());
}
}
);
Kevin Whitaker
committed
}
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == PLAYING_PAUSED)
Kevin Whitaker
committed
{
//Add action to DB
Wt::Dbo::Transaction pauseTransaction(sqlSession);
UserAction* action = new UserAction();
action->action = UserAction::UAction::PlayPause;
action->user = sqlSession.find<User>().where("username = ?").bind(event.userInvolved.username);
action->trackInvolved = sqlSession.find<AudioTrack>().where("fingerprint = ?").bind(event.tracksInvolved.front().trackFingerprint);
action->datetime = Wt::WDateTime::currentDateTime();
sqlSession.add(action);
pauseTransaction.commit();
Kevin Whitaker
committed
Kevin Whitaker
committed
groove_playlist_pause(currentPlaylist);
//Update vote display on all clients.
Wt::WServer::instance()->postAll([event]() {
Kevin Whitaker
committed
Wt::WApplication* app = Wt::WApplication::instance();
if(app != nullptr)
Kevin Whitaker
committed
{
static_cast<WebInterface*>(app)->playPauseActionFromServer(event.userInvolved, true);
}
}
);
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == PLAYING_RESUMED)
Kevin Whitaker
committed
{
//Add action to DB
Wt::Dbo::Transaction playTransaction(sqlSession);
UserAction* action = new UserAction();
action->action = UserAction::UAction::PlayPause;
action->user = sqlSession.find<User>().where("username = ?").bind(event.userInvolved.username);
action->trackInvolved = sqlSession.find<AudioTrack>().where("fingerprint = ?").bind(event.tracksInvolved.front().trackFingerprint);
action->datetime = Wt::WDateTime::currentDateTime();
sqlSession.add(action);
playTransaction.commit();
Kevin Whitaker
committed
Kevin Whitaker
committed
groove_playlist_play(currentPlaylist);
//Update vote display on all clients.
Wt::WServer::instance()->postAll([event]() {
Kevin Whitaker
committed
Wt::WApplication* app = Wt::WApplication::instance();
if(app != nullptr)
Kevin Whitaker
committed
{
static_cast<WebInterface*>(app)->playPauseActionFromServer(event.userInvolved, false);
}
}
);
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == SKIP_REQUESTED)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
if(!voteEndedButNotNextTrackYet)
{
currentSkipRequester.push_back(event.userInvolved);
skipRequestedAt = std::chrono::steady_clock::now();
Wt::Dbo::Transaction skipTransaction(sqlSession);
UserAction* action = new UserAction();
action->action = UserAction::UAction::RequestSkip;
action->user = sqlSession.find<User>().where("username = ?").bind(event.userInvolved.username);
action->trackInvolved = sqlSession.find<AudioTrack>().where("fingerprint = ?").bind(event.tracksInvolved.front().trackFingerprint);
Kevin Whitaker
committed
action->datetime = Wt::WDateTime::currentDateTime();
sqlSession.add(action);
skipTransaction.commit();
//Display skip request on all clients.
Wt::WServer::instance()->postAll([event]() {
Wt::WApplication* app = Wt::WApplication::instance();
if(app != nullptr)
{
static_cast<WebInterface*>(app)->skipVotedFromServer(event.userInvolved);
}
}
);
}
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == SKIP_VOTE_CAST_AGAINST)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
if(!voteEndedButNotNextTrackYet)
{
currentSkipRequester.clear();
//Display skip request on all clients.
Wt::WServer::instance()->postAll([event]() {
Wt::WApplication* app = Wt::WApplication::instance();
if(app != nullptr)
{
static_cast<WebInterface*>(app)->skipVoteUpdateFromServer(event.userInvolved, false);
}
}
);
}
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == SKIP_VOTING_ENDED)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
//Don't skip without first determining next track
if(!voteEndedButNotNextTrackYet)
{
voteEndedButNotNextTrackYet = true;
Kevin Whitaker
committed
lastInternalEvents.push_front(PlayerEvent() = {PlayerEventType::SKIP_VOTING_ENDED});
Kevin Whitaker
committed
lastInternalEvents.push_front(PlayerEvent() = {PlayerEventType::VOTING_ENDED});
}
else
{
GroovePlaylistItem* item;
groove_player_position(currentPlayer,&item,nullptr);
groove_playlist_seek(currentPlaylist, item->next, 0.0);
}
Kevin Whitaker
committed
}
Kevin Whitaker
committed
else if(event.eventType == ADMIN_FORCE_SKIP)
{
if(!voteEndedButNotNextTrackYet)
{
lastInternalEvents.push_front(PlayerEvent() = {PlayerEventType::SKIP_VOTING_ENDED});
}
}
Kevin Whitaker
committed
}
Kevin Whitaker
committed
currentPlaylist = nullptr;
currentPlayer = nullptr;
Kevin Whitaker
committed
groove_player_detach(player);
groove_player_destroy(player);
groove_playlist_destroy(playlist);
}
void GroovePlayerMgr::shutdown()
{
continueEventLoop = false;
Kevin Whitaker
committed
}
Kevin Whitaker
committed
GroovePlayerMgr::PlayerEvent GroovePlayerMgr::getNextPlayerEvent(Wt::Dbo::Session* session)
Kevin Whitaker
committed
double timeIntoTrack;
groove_player_position(currentPlayer,nullptr,&timeIntoTrack);
Kevin Whitaker
committed
if(!voteEndedButNotNextTrackYet && currentTrack.trackName != std::string() && currentTrack.trackLengthSeconds-timeIntoTrack < 3)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
voteEndedButNotNextTrackYet = true;
Kevin Whitaker
committed
return PlayerEvent() = {PlayerEventType::VOTING_ENDED};
Kevin Whitaker
committed
}
GroovePlayerEvent event;
if(groove_player_event_get(this->currentPlayer,&event, 0) > 0)
{
if(event.type == GROOVE_EVENT_NOWPLAYING)
{
Kevin Whitaker
committed
voteEndedButNotNextTrackYet = false;
Kevin Whitaker
committed
return PlayerEvent() = {PlayerEventType::GROOVE_NOWPLAYING};
Kevin Whitaker
committed
}
}
if(!voteEndedButNotNextTrackYet && currentSkipRequester.size() > 0 && skipRequestedAt+std::chrono::seconds(10) < std::chrono::steady_clock::now())
Kevin Whitaker
committed
{
//Skip succeeded. Time to skip song.
lastInternalEvents.push_front(PlayerEvent() = {PlayerEventType::SKIP_VOTING_ENDED});
}
Kevin Whitaker
committed
if(lastInternalEvents.size() > 0)
{
Kevin Whitaker
committed
PlayerEvent event = lastInternalEvents.front();
Kevin Whitaker
committed
lastInternalEvents.pop_front();
return event;
}
Kevin Whitaker
committed
return PlayerEvent() = {PlayerEventType::NOTHING};
Kevin Whitaker
committed
AudioTrack GroovePlayerMgr::getCurrentTrackDB(Wt::Dbo::Session* session)
Kevin Whitaker
committed
{
Kevin Whitaker
committed
Wt::Dbo::Transaction transaction(*session);
Kevin Whitaker
committed
GroovePlaylistItem* currentItem;
Kevin Whitaker
committed
groove_player_position(this->currentPlayer,¤tItem,nullptr);
if(currentItem != nullptr)
{
Kevin Whitaker
committed
AudioTrack track = *session->find<AudioTrack>().where("path = ?").bind(currentItem->file->filename).resultValue();
transaction.commit();
Kevin Whitaker
committed
return track;
}
Kevin Whitaker
committed
transaction.commit();
return AudioTrack();
Kevin Whitaker
committed
}
Kevin Whitaker
committed
GroovePlayerMgr::ScanResults GroovePlayerMgr::addFileToTrackDBIfTagged(Wt::Dbo::Session* session, std::filesystem::path file)
Kevin Whitaker
committed
{
//Now check if tags exist and put into DB.
struct GrooveFile* gfile = groove_file_open(file.c_str());
struct GrooveTag* artist_tag = groove_file_metadata_get(gfile, "artist", nullptr, 0);
struct GrooveTag* album_tag = groove_file_metadata_get(gfile, "album", nullptr, 0);
struct GrooveTag* name_tag = groove_file_metadata_get(gfile, "title", nullptr, 0);
struct GrooveTag* genre_tag = groove_file_metadata_get(gfile, "genre", nullptr, 0);
if(artist_tag == nullptr || album_tag == nullptr || name_tag == nullptr || genre_tag == nullptr)
{
//Only accept song with all metadata for DB.
Kevin Whitaker
committed
Wt::log("info") << "Audio track " << file << " did not have all required tags.";
groove_file_close(gfile);
Kevin Whitaker
committed
if(artist_tag == nullptr && album_tag != nullptr && name_tag != nullptr && genre_tag != nullptr) return ScanResults::MISSING_ARTIST_TAG;
if(artist_tag != nullptr && album_tag == nullptr && name_tag == nullptr && genre_tag == nullptr) return ScanResults::MISSING_ALBUM_TAG;
if(artist_tag != nullptr && album_tag != nullptr && name_tag == nullptr && genre_tag != nullptr) return ScanResults::MISSING_TITLE_TAG;
if(artist_tag != nullptr && album_tag != nullptr && name_tag != nullptr && genre_tag == nullptr) return ScanResults::MISSING_GENRE_TAG;
return ScanResults::MISSING_MULTIPLE_TAGS;
}
//Take fingerprint and compare to DB.
struct GrooveFingerprinter* fingerprinter = groove_fingerprinter_create();
struct GrooveFingerprinterInfo info;
struct GroovePlaylist* playlist = groove_playlist_create();
groove_playlist_insert(playlist,gfile,1.0,1.0,nullptr);
groove_fingerprinter_attach(fingerprinter, playlist);
groove_fingerprinter_info_get(fingerprinter, &info, 1);
double trackLen = info.duration;
char* encodedFP;
groove_fingerprinter_encode(info.fingerprint,info.fingerprint_size,&encodedFP);
std::string fingerprint = std::string(encodedFP);
groove_fingerprinter_dealloc(encodedFP);
groove_fingerprinter_free_info(&info);
groove_fingerprinter_detach(fingerprinter);
groove_fingerprinter_destroy(fingerprinter);
groove_playlist_destroy(playlist);
Kevin Whitaker
committed
Wt::Dbo::Transaction transaction(*session);
int existing = session->query<int>("select count(fingerprint) from tracks").where("fingerprint = ?").bind(fingerprint);
if(existing >0)
{
//This track has a duplicate already.
groove_file_close(gfile);
Kevin Whitaker
committed
return ScanResults::DUPLICATE_TRACK;
}
//Add to DB.
AudioTrack* newTrack = new AudioTrack();
newTrack->trackName = groove_tag_value(name_tag);
newTrack->trackAlbumName = groove_tag_value(album_tag);
newTrack->trackArtistName = groove_tag_value(artist_tag);
newTrack->trackGenre = groove_tag_value(genre_tag);
newTrack->trackLengthSeconds = trackLen;
newTrack->trackFingerprint = fingerprint;
newTrack->trackPath = file.string();
Kevin Whitaker
committed
//Quickly see if there is cover artist
getPictureFromTrack(newTrack);
if(newTrack->coverArt.size() == 0)
{
Wt::log("info") << "Audio track " << newTrack->trackPath << " did not have cover art in the file.";
groove_file_close(gfile);
Kevin Whitaker
committed
return ScanResults::MISSING_COVERART_TAG;
Kevin Whitaker
committed
}
Kevin Whitaker
committed
session->add(newTrack);
Kevin Whitaker
committed
transaction.commit();
groove_file_close(gfile);
Kevin Whitaker
committed
return ScanResults::ACCEPTED;
Kevin Whitaker
committed
}
Kevin Whitaker
committed
void GroovePlayerMgr::removeOrphanedTracks(Wt::Dbo::Session* session)
{
Kevin Whitaker
committed
Wt::Dbo::Transaction transaction(*session);
Wt::Dbo::collection<Wt::Dbo::ptr<AudioTrack>> tracks = session->find<AudioTrack>();
for(Wt::Dbo::collection<Wt::Dbo::ptr<AudioTrack>>::const_iterator i = tracks.begin(); i != tracks.end(); ++i)
{
if(!std::filesystem::exists(std::filesystem::path((*i)->trackPath)))
{
Kevin Whitaker
committed
Wt::Dbo::ptr<AudioTrack> item = session->find<AudioTrack>().where("fingerprint = ?").bind((*i)->trackFingerprint);
Wt::log("info") << (*i)->trackPath + " not found in filesystem. Removed from DB.";
item.remove();
}
}
Kevin Whitaker
committed
transaction.commit();
}
Kevin Whitaker
committed
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
void GroovePlayerMgr::getPictureFromTrack(AudioTrack* trackToFill)
{
if(trackToFill->trackPath != "")
{
TagLib::String file = TagLib::String(trackToFill->trackPath);
if(file.substr(file.size()-3).upper() == "MP3" && TagLib::MPEG::File(trackToFill->trackPath.c_str()).hasID3v2Tag())
{
TagLib::MPEG::File file(trackToFill->trackPath.c_str());
TagLib::ID3v2::Tag* tag = file.ID3v2Tag();
if(!tag->frameListMap()["APIC"].isEmpty() && dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(tag->frameListMap()["APIC"].front()) != nullptr)
{
TagLib::ID3v2::AttachedPictureFrame* pic = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(tag->frameListMap()["APIC"].front());
trackToFill->coverArt = TagLib::ByteVector(pic->picture().data(),pic->picture().size());
trackToFill->coverMimeType = std::string(pic->mimeType().toCString(true));
}
}
// else if(dynamic_cast<TagLib::Ogg::XiphComment*>(tagTrack.tag()) != nullptr && !dynamic_cast<TagLib::Ogg::XiphComment*>(tagTrack.tag())->isEmpty())
// {
// TagLib::Ogg::XiphComment* tag = dynamic_cast<TagLib::Ogg::XiphComment*>(tagTrack.tag());
// if(tag->fieldListMap()["METADATA_BLOCK_PICTURE"].isEmpty())
// {
// tag->fieldListMap()["METADATA_BLOCK_PICTURE"];//TODO?
// }
// }
}
}
Kevin Whitaker
committed
Kevin Whitaker
committed
void GroovePlayerMgr::grooveAudioScannerLoop()
Kevin Whitaker
committed
Wt::Dbo::Session sqlSession;
Kevin Whitaker
committed
sqlSession.setConnectionPool(*connectionPool);
Kevin Whitaker
committed
sqlSession.mapClass<User>("user");
sqlSession.mapClass<AudioTrack>("tracks");
sqlSession.mapClass<UserAction>("actions");
if(!std::filesystem::exists(musicScanDir))
{
std::filesystem::create_directory(musicScanDir);
Wt::log("info") << "Directory "+std::filesystem::canonical(musicScanDir).string()+" didn't exist, so created.";
} else
{
Wt::log("info") << "Directory "+std::filesystem::canonical(musicScanDir).string()+" exists.";
}
Kevin Whitaker
committed
for(std::filesystem::directory_entry p: std::filesystem::directory_iterator(musicScanDir))
{
std::string extensionLowered;
for(auto elem : p.path().extension().string())
{
extensionLowered.push_back(std::tolower(elem));
}
Kevin Whitaker
committed
if(extensionLowered == ".mp3") //TODO:think about supporting more than mp3s.
Kevin Whitaker
committed
{
Kevin Whitaker
committed
if(addFileToTrackDBIfTagged(&sqlSession, p.path()))
Kevin Whitaker
committed
{
Wt::log("info") << p.path().string() << " was added to DB";
}
}
else
{
Wt::log("info") << p.path().string() + " was not an accepted audio file.";
}
Kevin Whitaker
committed
}
//Now check for tracks in DB without a file.
Kevin Whitaker
committed
removeOrphanedTracks(&sqlSession);