diff --git a/src/GroovePlayer.cpp b/src/GroovePlayer.cpp index 3454a2301e274291030b55e8ca22f5d1ea82c997..693b71dac613f64ea072ab6f29041421eebaa14a 100644 --- a/src/GroovePlayer.cpp +++ b/src/GroovePlayer.cpp @@ -29,12 +29,11 @@ #include #include -std::filesystem::path GroovePlayerMgr::musicScanDir = ""; - -GroovePlayerMgr::GroovePlayerMgr (std::string dbFile) : sqliteConnection(dbFile) +GroovePlayerMgr::GroovePlayerMgr (std::string dbFile) : sqliteConnection(dbFile), connectionPool(&sqliteConnection, 3) { musicScanDir = std::filesystem::current_path()/"music"; - sqlSession.setConnection(this->sqliteConnection); + Wt::Dbo::Session sqlSession; + sqlSession.setConnectionPool(connectionPool); sqlSession.mapClass("user"); sqlSession.mapClass("tracks"); sqlSession.mapClass("actions"); @@ -47,12 +46,25 @@ GroovePlayerMgr::GroovePlayerMgr (std::string dbFile) : sqliteConnection(dbFile) } groove_init(); - - grooveAudioScanner = new std::thread(grooveAudioScannerLoop); - grooveEvents = new std::thread(grooveEventLoop); + /*bool emptyTrackListing = false; + { + Wt::Dbo::Transaction transaction(sqlSession); + int trackCount = sqlSession.query("select count(fingerprint) from tracks"); + emptyTrackListing = trackCount==0; + } + if(emptyTrackListing) + { + grooveAudioScanner = new std::thread([this](){grooveAudioScannerLoop();}); + grooveEvents = new std::thread([this](){while(!this->finishedLaunchScan){std::this_thread::sleep_for(std::chrono::milliseconds(200));}[this](){grooveEventLoop();};}); + } + else + {*/ + grooveAudioScanner = new std::thread([this](){grooveAudioScannerLoop();}); + grooveEvents = new std::thread([this](){grooveEventLoop();}); + //} } -std::list GroovePlayerMgr::getNextVoteBatch() +std::list GroovePlayerMgr::getNextVoteBatch(Wt::Dbo::Session* session) { /** * This method will attempt to pick 3 tracks that will be up for selection next. @@ -68,9 +80,9 @@ std::list GroovePlayerMgr::getNextVoteBatch() std::list selectedTracks; - Wt::Dbo::Transaction transaction(getInstance()->sqlSession); + Wt::Dbo::Transaction transaction(*session); //First make sure there are at least 3 tracks to suggest. - int trackCount = getInstance()->sqlSession.query("select count(fingerprint) from tracks"); + int trackCount = session->query("select count(fingerprint) from tracks"); if(trackCount < 3) { //Return empty list to show there are not enough tracks to allow voting. @@ -78,33 +90,33 @@ std::list GroovePlayerMgr::getNextVoteBatch() } //Determine first track - int trackAlbumCount = getInstance()->sqlSession.query("select count(fingerprint) from tracks").where("album = ?").bind(getCurrentTrack()->trackAlbumName); - int trackArtistCount = getInstance()->sqlSession.query("select count(fingerprint) from tracks").where("artist = ?").bind(getCurrentTrack()->trackArtistName); + int trackAlbumCount = session->query("select count(fingerprint) from tracks").where("album = ?").bind(getCurrentTrack(session)->trackAlbumName); + int trackArtistCount = session->query("select count(fingerprint) from tracks").where("artist = ?").bind(getCurrentTrack(session)->trackArtistName); int computerSlightOfHand = rand() % 10 + 1; if(trackAlbumCount > 0 && computerSlightOfHand > 3) { //Pick item from album - Wt::Dbo::ptr eligibleTrack = getInstance()->sqlSession.find().where("album = ?").limit(1).offset(rand() % trackAlbumCount).bind(getCurrentTrack()->trackAlbumName); + Wt::Dbo::ptr eligibleTrack = session->find().where("album = ?").limit(1).offset(rand() % trackAlbumCount).bind(getCurrentTrack(session)->trackAlbumName); selectedTracks.push_back(eligibleTrack.get()); } else if(trackArtistCount > 0) { //Pick item from artist - Wt::Dbo::ptr eligibleTrack = getInstance()->sqlSession.find().where("artist = ?").limit(1).offset(rand() % trackArtistCount).bind(getCurrentTrack()->trackArtistName); + Wt::Dbo::ptr eligibleTrack = session->find().where("artist = ?").limit(1).offset(rand() % trackArtistCount).bind(getCurrentTrack(session)->trackArtistName); selectedTracks.push_back(eligibleTrack.get()); } //Determine second track - int trackGenreCount = getInstance()->sqlSession.query("select count(fingerprint) from tracks").where("genre = ?").bind(getCurrentTrack()->trackGenre); + int trackGenreCount = session->query("select count(fingerprint) from tracks").where("genre = ?").bind(getCurrentTrack(session)->trackGenre); if(trackGenreCount > 0) { //Pick item from genre - Wt::Dbo::ptr eligibleTrack = getInstance()->sqlSession.find().where("genre = ?").limit(1).offset(rand() % trackGenreCount).bind(getCurrentTrack()->trackGenre); + Wt::Dbo::ptr eligibleTrack = session->find().where("genre = ?").limit(1).offset(rand() % trackGenreCount).bind(getCurrentTrack(session)->trackGenre); selectedTracks.push_back(eligibleTrack.get()); } //Determine third track - int trackNotGenreCount = getInstance()->sqlSession.query("select count(fingerprint) from tracks").where("genre != ?").bind(getCurrentTrack()->trackGenre); + int trackNotGenreCount = session->query("select count(fingerprint) from tracks").where("genre != ?").bind(getCurrentTrack(session)->trackGenre); if(requestQueue.size() > 0) { //There's a request. Pick one up front and put as third item. @@ -113,16 +125,15 @@ std::list GroovePlayerMgr::getNextVoteBatch() else if(trackNotGenreCount > 0) { //Pick from other genre - Wt::Dbo::ptr eligibleTrack = getInstance()->sqlSession.find().where("genre != ?").limit(1).offset(rand() % trackNotGenreCount).bind(getCurrentTrack()->trackGenre); + Wt::Dbo::ptr eligibleTrack = session->find().where("genre != ?").limit(1).offset(rand() % trackNotGenreCount).bind(getCurrentTrack(session)->trackGenre); selectedTracks.push_back(eligibleTrack.get()); } else { //Pick randomly - Wt::Dbo::ptr eligibleTrack = getInstance()->sqlSession.find().limit(1).offset(rand() % trackCount); + Wt::Dbo::ptr eligibleTrack = session->find().limit(1).offset(rand() % trackCount); selectedTracks.push_back(eligibleTrack.get()); } - return selectedTracks; } @@ -131,63 +142,200 @@ void GroovePlayerMgr::grooveEventLoop() /** * 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 60% of the time with second being 30% of the time and third being picked 10% of the time. - * Exception is if third track is a request, then it is picked. If another song is voted over this, it will be put in back of request queue. + * 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. */ - Wt::Dbo::Transaction transaction(getInstance()->sqlSession); + Wt::Dbo::Session sqlSession; + sqlSession.setConnectionPool(connectionPool); + sqlSession.mapClass("user"); + sqlSession.mapClass("tracks"); + sqlSession.mapClass("actions"); + Wt::Dbo::Transaction transaction(sqlSession); //Wait until at least one track is in DB. - if(getInstance()->sqlSession.query("select count(fingerprint) from tracks") < 1) + if(sqlSession.query("select count(fingerprint) from tracks") < 1) { - while(getInstance()->sqlSession.query("select count(fingerprint) from tracks") < 1){} + while(sqlSession.query("select count(fingerprint) from tracks") < 1){} } - //Pick initial track and boostrap player. - int tracksPlayedBefore = getInstance()->sqlSession.query("select count(fingerprint) from tracks as t join actions on (actions.action = 3 or actions.action = 2) and actions.track_id = t.id"); + //Pick initial track to bootstrap. + int tracksPlayedBefore = sqlSession.query("select count(fingerprint) from tracks as t join actions on (actions.action = 3 or actions.action = 2) and actions.track_id = t.id"); const AudioTrack* selectedTrack; if (tracksPlayedBefore > 0) { - selectedTrack = getInstance()->sqlSession.query>("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().get(); + selectedTrack = sqlSession.query>("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().get(); } else { - int trackCount = getInstance()->sqlSession.query("select count(fingerprint) from tracks"); - selectedTrack = getInstance()->sqlSession.find().limit(1).offset(rand() % trackCount).resultValue().get(); + int trackCount = sqlSession.query("select count(fingerprint) from tracks"); + selectedTrack = sqlSession.find().limit(1).offset(rand() % trackCount).resultValue().get(); } struct GroovePlaylist* playlist = groove_playlist_create(); - getInstance()->currentPlaylist = playlist; + currentPlaylist = playlist; struct GroovePlayer* player = groove_player_create(); if(!player) {return;} - getInstance()->currentPlayer = player; + currentPlayer = player; groove_playlist_insert(playlist, groove_file_open(selectedTrack->trackPath.c_str()),1.0,1.0,nullptr); - //Now start loop + //Now boostrap player with initial data groove_player_attach(player, playlist); - getInstance()->currentVoteBatch = getInstance()->getNextVoteBatch(); groove_playlist_play(playlist); - Wt::WServer::instance()->postAll([]() { + Wt::WServer::instance()->postAll([&]() { Wt::WApplication* app = Wt::WApplication::instance(); - if(app != nullptr) - { - static_cast(app)->songChangedFromServer(getInstance()->getCurrentTrack().get()); - static_cast(app)->voteTracksUpdatedFromServer(getInstance()->currentVoteBatch); - } + if(app != nullptr) + { + static_cast(app)->songChangedFromServer(getCurrentTrack(&sqlSession).get()); } + } ); + //Now start loop while(getInstance()->continueEventLoop) { - PlayerEvents event = getInstance()->getNextPlayerEvent(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + PlayerEvents event = getNextPlayerEvent(&sqlSession); if(event == NOTHING) { continue; } + else if(event == GROOVE_NOWPLAYING) + { + //Pick new batch of tracks to vote on and inform UI of update. + currentVoteBatch = getNextVoteBatch(&sqlSession); + currentVoteStatus = std::map(); + std::for_each(currentVoteBatch.begin(),currentVoteBatch.end(),[&](const AudioTrack* item){ + currentVoteStatus.insert(std::pair(item,0)); + }); + Wt::WServer::instance()->postAll([&]() { + Wt::WApplication* app = Wt::WApplication::instance(); + if(app != nullptr) + { + static_cast(app)->songChangedFromServer(getCurrentTrack(&sqlSession).get()); + static_cast(app)->voteTracksUpdatedFromServer(currentVoteBatch); + } + } + ); + Wt::log("info") << "Track playing changed to: " << getCurrentTrack(&sqlSession).get()->trackName; + } + else if(event == VOTING_ENDED) + { + //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. + + std::list> trackWinners; + for(std::map::iterator track = currentVoteStatus.begin(); track != currentVoteStatus.end(); track++) + { + //There nothing yet and there was at least a vote is an initial winner. + if(trackWinners.size() == 0 && track->second > 0) + { + trackWinners.push_back(std::pair(track->first,track->second)); + 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) + { + if(trackWinners.front().second < track->second) + { + trackWinners.clear(); + trackWinners.push_back(std::pair(track->first, track->second)); + } + else if(trackWinners.front().second == track->second) + { + trackWinners.push_back(std::pair(track->first, track->second)); + } + } + } + + const AudioTrack* winner; + //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. + if(requestQueue.front()->trackFingerprint == winner->trackFingerprint) + { + 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; + if(pick <= 6) + { + //First track wins + winner = currentVoteBatch.front(); + } + else if(pick <= 9) + { + //Second track wins. + winner = (*currentVoteBatch.begin()++); + } + else + { + //Third track wins + winner = (*currentVoteBatch.begin()+=2); + } + } + } + groove_playlist_insert(currentPlaylist,groove_file_open(winner->trackPath.c_str()),1.0,1.0,nullptr); + Wt::log("info")<< "Voting has ended. Next track is: " << winner->trackName; + } + else if(event == VOTE_CAST) + { + // + } + else if(event == PLAYING_PAUSED) + { + // + } + else if(event == PLAYING_RESUMED) + { + // + } + else if(event == SKIP_REQUESTED) + { + // + } + else if(event == SKIP_VOTE_CAST) + { + // + } + else if(event == SKIP_VOTING_ENDED) + { + // + } + else if(event == ADMIN_FORCE_SKIP) + { + // + } //TODO } - getInstance()->currentPlaylist = nullptr; - getInstance()->currentPlayer = nullptr; + currentPlaylist = nullptr; + currentPlayer = nullptr; groove_player_detach(player); groove_player_destroy(player); groove_playlist_destroy(playlist); @@ -198,24 +346,46 @@ void GroovePlayerMgr::shutdown() continueEventLoop = false; } -GroovePlayerMgr::PlayerEvents GroovePlayerMgr::getNextPlayerEvent() +GroovePlayerMgr::PlayerEvents GroovePlayerMgr::getNextPlayerEvent(Wt::Dbo::Session* session) { + double timeIntoTrack; + groove_player_position(currentPlayer,nullptr,&timeIntoTrack); + if(getCurrentTrack(session).get() != nullptr && getCurrentTrack(session)->trackLengthSeconds-timeIntoTrack < 3) + { + return VOTING_ENDED; + } + GroovePlayerEvent event; + if(groove_player_event_get(this->currentPlayer,&event, 0) > 0) + { + if(event.type == GROOVE_EVENT_NOWPLAYING) + { + return GROOVE_NOWPLAYING; + } + } + if(lastInternalEvents.size() > 0) + { + PlayerEvents event = lastInternalEvents.front(); + lastInternalEvents.pop_front(); + return event; + } return NOTHING; - //TODO } -Wt::Dbo::ptr GroovePlayerMgr::getCurrentTrack() +Wt::Dbo::ptr GroovePlayerMgr::getCurrentTrack(Wt::Dbo::Session* session) { - Wt::Dbo::Transaction transaction(this->sqlSession); + Wt::Dbo::Transaction transaction(*session); GroovePlaylistItem* currentItem; - double timeInto; - groove_player_position(this->currentPlayer,¤tItem,&timeInto); - Wt::Dbo::ptr track = this->sqlSession.find().where("path = ?").bind(currentItem->file->filename); - return track; + groove_player_position(this->currentPlayer,¤tItem,nullptr); + if(currentItem != nullptr) + { + Wt::Dbo::ptr track = session->find().where("path = ?").bind(currentItem->file->filename); + return track; + } + return Wt::Dbo::ptr(); } -bool GroovePlayerMgr::addFileToTrackDBIfTagged(std::filesystem::path file) +bool GroovePlayerMgr::addFileToTrackDBIfTagged(Wt::Dbo::Session* session, std::filesystem::path file) { //Now check if tags exist and put into DB. struct GrooveFile* gfile = groove_file_open(file.c_str()); @@ -247,8 +417,8 @@ bool GroovePlayerMgr::addFileToTrackDBIfTagged(std::filesystem::path file) groove_fingerprinter_destroy(fingerprinter); groove_playlist_destroy(playlist); - Wt::Dbo::Transaction transaction(getInstance()->sqlSession); - int existing = getInstance()->sqlSession.query("select count(fingerprint) from tracks").where("fingerprint = ?").bind(fingerprint); + Wt::Dbo::Transaction transaction(*session); + int existing = session->query("select count(fingerprint) from tracks").where("fingerprint = ?").bind(fingerprint); if(existing >0) { //This track has a duplicate already. @@ -265,20 +435,20 @@ bool GroovePlayerMgr::addFileToTrackDBIfTagged(std::filesystem::path file) newTrack->trackLengthSeconds = trackLen; newTrack->trackFingerprint = fingerprint; newTrack->trackPath = file.string(); - getInstance()->sqlSession.add(newTrack); + session->add(newTrack); groove_file_close(gfile); return true; } -void GroovePlayerMgr::removeOrphanedTracks() +void GroovePlayerMgr::removeOrphanedTracks(Wt::Dbo::Session* session) { - Wt::Dbo::Transaction transaction(getInstance()->sqlSession); - Wt::Dbo::collection> tracks = getInstance()->sqlSession.find(); + Wt::Dbo::Transaction transaction(*session); + Wt::Dbo::collection> tracks = session->find(); for(Wt::Dbo::collection>::const_iterator i = tracks.begin(); i != tracks.end(); ++i) { if(!std::filesystem::exists(std::filesystem::path((*i)->trackPath))) { - Wt::Dbo::ptr item = getInstance()->sqlSession.find().where("fingerprint = ?").bind((*i)->trackFingerprint); + Wt::Dbo::ptr item = session->find().where("fingerprint = ?").bind((*i)->trackFingerprint); Wt::log("info") << (*i)->trackPath + " not found in filesystem. Removed from DB."; item.remove(); } @@ -289,6 +459,11 @@ void GroovePlayerMgr::removeOrphanedTracks() void GroovePlayerMgr::grooveAudioScannerLoop() { + Wt::Dbo::Session sqlSession; + sqlSession.setConnectionPool(connectionPool); + sqlSession.mapClass("user"); + sqlSession.mapClass("tracks"); + sqlSession.mapClass("actions"); if(!std::filesystem::exists(musicScanDir)) { std::filesystem::create_directory(musicScanDir); @@ -306,7 +481,7 @@ void GroovePlayerMgr::grooveAudioScannerLoop() } if(extensionLowered == ".mp3") //TODO:think about supporting more than mp3s. { - if(addFileToTrackDBIfTagged(p.path())) + if(addFileToTrackDBIfTagged(&sqlSession, p.path())) { Wt::log("info") << p.path().string() << " was added to DB"; } @@ -317,6 +492,7 @@ void GroovePlayerMgr::grooveAudioScannerLoop() } } //Now check for tracks in DB without a file. - removeOrphanedTracks(); + removeOrphanedTracks(&sqlSession); + finishedLaunchScan = true; } diff --git a/src/GroovePlayer.h b/src/GroovePlayer.h index a975c150758b8647811fb4e625da725d01067ae5..e41f86e2d0ed702000fdc11e83a2b3f04d82c960 100644 --- a/src/GroovePlayer.h +++ b/src/GroovePlayer.h @@ -22,7 +22,9 @@ #include #include +#include #include +#include #include #include "db/User.h" #include "db/AudioTrack.h" @@ -35,7 +37,7 @@ namespace std { class GroovePlayerMgr { public: - static std::filesystem::path musicScanDir; + std::filesystem::path musicScanDir; static GroovePlayerMgr* getInstance() { static GroovePlayerMgr instance("music.db"); return &instance; @@ -47,28 +49,29 @@ public: struct GroovePlaylist* currentPlaylist; struct GroovePlayer* currentPlayer; std::list currentVoteBatch; + std::map currentVoteStatus; private: enum PlayerEvents {NOTHING, GROOVE_NOWPLAYING, VOTING_ENDED, VOTE_CAST, PLAYING_PAUSED, PLAYING_RESUMED, SKIP_REQUESTED, SKIP_VOTE_CAST, SKIP_VOTING_ENDED, ADMIN_FORCE_SKIP}; GroovePlayerMgr (std::string dbFile); Wt::Dbo::backend::Sqlite3 sqliteConnection; - Wt::Dbo::Session sqlSession; + Wt::Dbo::FixedSqlConnectionPool connectionPool; bool continueEventLoop = true; + bool finishedLaunchScan = false; std::list requestQueue; std::list lastInternalEvents; - std::thread* grooveEvents; - static void grooveEventLoop(); - std::list getNextVoteBatch(); + void grooveEventLoop(); + std::list getNextVoteBatch(Wt::Dbo::Session* session); std::thread* grooveAudioScanner; - static void grooveAudioScannerLoop(); - PlayerEvents getNextPlayerEvent(); - static bool addFileToTrackDBIfTagged(std::filesystem::path file); - static void removeOrphanedTracks(); + void grooveAudioScannerLoop(); + PlayerEvents getNextPlayerEvent(Wt::Dbo::Session* session); + bool addFileToTrackDBIfTagged(Wt::Dbo::Session* session, std::filesystem::path file); + void removeOrphanedTracks(Wt::Dbo::Session* session); - Wt::Dbo::ptr getCurrentTrack(); + Wt::Dbo::ptr getCurrentTrack(Wt::Dbo::Session* session); };