/* * Copyright (C) 2017 Kevin Whitaker * * 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 #include #include #include "WebInterface.h" #include #include #include #include #include GroovePlayerMgr::GroovePlayerMgr (std::string dbFile) : sqliteConnection(dbFile), connectionPool(&sqliteConnection, 3) { musicScanDir = std::filesystem::current_path()/"music"; Wt::Dbo::Session sqlSession; sqlSession.setConnectionPool(connectionPool); sqlSession.mapClass("user"); sqlSession.mapClass("tracks"); sqlSession.mapClass("actions"); try { sqlSession.createTables(); } catch(Wt::Dbo::Exception e) { Wt::log("info") << "Using Existing DB."; } groove_init(); /*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(Wt::Dbo::Session* session) { /** * 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. */ std::list selectedTracks; Wt::Dbo::Transaction transaction(*session); //First make sure there are at least 3 tracks to suggest. 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. return selectedTracks; } //Determine first track 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 = 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 = session->find().where("artist = ?").limit(1).offset(rand() % trackArtistCount).bind(getCurrentTrack(session)->trackArtistName); selectedTracks.push_back(eligibleTrack.get()); } //Determine second track 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 = session->find().where("genre = ?").limit(1).offset(rand() % trackGenreCount).bind(getCurrentTrack(session)->trackGenre); selectedTracks.push_back(eligibleTrack.get()); } //Determine third track 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. selectedTracks.push_back(requestQueue.front()); } else if(trackNotGenreCount > 0) { //Pick from other genre 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 = session->find().limit(1).offset(rand() % trackCount); selectedTracks.push_back(eligibleTrack.get()); } return selectedTracks; } 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 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::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(sqlSession.query("select count(fingerprint) from tracks") < 1) { while(sqlSession.query("select count(fingerprint) from tracks") < 1){} } //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 = 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 = sqlSession.query("select count(fingerprint) from tracks"); selectedTrack = sqlSession.find().limit(1).offset(rand() % trackCount).resultValue().get(); } struct GroovePlaylist* playlist = groove_playlist_create(); currentPlaylist = playlist; struct GroovePlayer* player = groove_player_create(); if(!player) {return;} currentPlayer = player; groove_playlist_insert(playlist, groove_file_open(selectedTrack->trackPath.c_str()),1.0,1.0,nullptr); //Now boostrap player with initial data groove_player_attach(player, playlist); groove_playlist_play(playlist); Wt::WServer::instance()->postAll([&]() { Wt::WApplication* app = Wt::WApplication::instance(); if(app != nullptr) { static_cast(app)->songChangedFromServer(getCurrentTrack(&sqlSession).get()); } } ); //Now start loop while(getInstance()->continueEventLoop) { 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 } currentPlaylist = nullptr; currentPlayer = nullptr; groove_player_detach(player); groove_player_destroy(player); groove_playlist_destroy(playlist); } void GroovePlayerMgr::shutdown() { continueEventLoop = false; } 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; } Wt::Dbo::ptr GroovePlayerMgr::getCurrentTrack(Wt::Dbo::Session* session) { Wt::Dbo::Transaction transaction(*session); GroovePlaylistItem* currentItem; 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(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()); 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. groove_file_close(gfile); return false; } //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); 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. groove_file_close(gfile); return false; } //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(); session->add(newTrack); groove_file_close(gfile); return true; } void GroovePlayerMgr::removeOrphanedTracks(Wt::Dbo::Session* session) { 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 = session->find().where("fingerprint = ?").bind((*i)->trackFingerprint); Wt::log("info") << (*i)->trackPath + " not found in filesystem. Removed from DB."; item.remove(); } } } 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); 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."; } 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)); } if(extensionLowered == ".mp3") //TODO:think about supporting more than mp3s. { if(addFileToTrackDBIfTagged(&sqlSession, p.path())) { Wt::log("info") << p.path().string() << " was added to DB"; } } else { Wt::log("info") << p.path().string() + " was not an accepted audio file."; } } //Now check for tracks in DB without a file. removeOrphanedTracks(&sqlSession); finishedLaunchScan = true; }