diff options
Diffstat (limited to 'xbmc/PartyModeManager.cpp')
-rw-r--r-- | xbmc/PartyModeManager.cpp | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/xbmc/PartyModeManager.cpp b/xbmc/PartyModeManager.cpp new file mode 100644 index 0000000..2f02c23 --- /dev/null +++ b/xbmc/PartyModeManager.cpp @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PartyModeManager.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogProgress.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "interfaces/AnnouncementManager.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/MusicDatabase.h" +#include "music/tags/MusicInfoTag.h" +#include "playlists/PlayList.h" +#include "playlists/SmartPlayList.h" +#include "profiles/ProfileManager.h" +#include "settings/SettingsComponent.h" +#include "utils/Random.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" +#include "video/VideoInfoTag.h" + +#include <algorithm> + +using namespace KODI::MESSAGING; + +#define QUEUE_DEPTH 10 + +CPartyModeManager::CPartyModeManager(void) +{ + m_bIsVideo = false; + m_bEnabled = false; + ClearState(); +} + +bool CPartyModeManager::Enable(PartyModeContext context /*= PARTYMODECONTEXT_MUSIC*/, const std::string& strXspPath /*= ""*/) +{ + // Filter using our PartyMode xml file + CSmartPlaylist playlist; + std::string partyModePath; + bool playlistLoaded; + + m_bIsVideo = context == PARTYMODECONTEXT_VIDEO; + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (!strXspPath.empty()) //if a path to a smartplaylist is supplied use it + partyModePath = strXspPath; + else if (m_bIsVideo) + partyModePath = profileManager->GetUserDataItem("PartyMode-Video.xsp"); + else + partyModePath = profileManager->GetUserDataItem("PartyMode.xsp"); + + playlistLoaded=playlist.Load(partyModePath); + + if (playlistLoaded) + { + m_type = playlist.GetType(); + if (context == PARTYMODECONTEXT_UNKNOWN) + { + //get it from the xsp file + m_bIsVideo = (StringUtils::EqualsNoCase(m_type, "video") || + StringUtils::EqualsNoCase(m_type, "musicvideos") || + StringUtils::EqualsNoCase(m_type, "mixed")); + } + } + else if (m_bIsVideo) + m_type = "musicvideos"; + else + m_type = "songs"; + + CGUIDialogProgress* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + int iHeading = (m_bIsVideo ? 20250 : 20121); + int iLine0 = (m_bIsVideo ? 20251 : 20123); + pDialog->SetHeading(CVariant{iHeading}); + pDialog->SetLine(0, CVariant{iLine0}); + pDialog->SetLine(1, CVariant{""}); + pDialog->SetLine(2, CVariant{""}); + pDialog->Open(); + + ClearState(); + std::string strCurrentFilterMusic; + std::string strCurrentFilterVideo; + unsigned int songcount = 0; + unsigned int videocount = 0; + auto start = std::chrono::steady_clock::now(); + + if (StringUtils::EqualsNoCase(m_type, "songs") || + StringUtils::EqualsNoCase(m_type, "mixed")) + { + CMusicDatabase db; + if (db.Open()) + { + std::set<std::string> playlists; + if (playlistLoaded) + { + playlist.SetType("songs"); + strCurrentFilterMusic = playlist.GetWhereClause(db, playlists); + } + + CLog::Log(LOGINFO, "PARTY MODE MANAGER: Registering filter:[{}]", strCurrentFilterMusic); + songcount = db.GetRandomSongIDs(CDatabase::Filter(strCurrentFilterMusic), m_songIDCache); + m_iMatchingSongs = static_cast<int>(songcount); + if (m_iMatchingSongs < 1 && StringUtils::EqualsNoCase(m_type, "songs")) + { + pDialog->Close(); + db.Close(); + OnError(16031, "Party mode found no matching songs. Aborting."); + return false; + } + } + else + { + pDialog->Close(); + OnError(16033, "Party mode could not open database. Aborting."); + return false; + } + db.Close(); + } + + if (StringUtils::EqualsNoCase(m_type, "musicvideos") || + StringUtils::EqualsNoCase(m_type, "mixed")) + { + std::vector< std::pair<int,int> > songIDs2; + CVideoDatabase db; + if (db.Open()) + { + std::set<std::string> playlists; + if (playlistLoaded) + { + playlist.SetType("musicvideos"); + strCurrentFilterVideo = playlist.GetWhereClause(db, playlists); + } + + CLog::Log(LOGINFO, "PARTY MODE MANAGER: Registering filter:[{}]", strCurrentFilterVideo); + videocount = db.GetRandomMusicVideoIDs(strCurrentFilterVideo, songIDs2); + m_iMatchingSongs += static_cast<int>(videocount); + if (m_iMatchingSongs < 1) + { + pDialog->Close(); + db.Close(); + OnError(16031, "Party mode found no matching songs. Aborting."); + return false; + } + } + else + { + pDialog->Close(); + OnError(16033, "Party mode could not open database. Aborting."); + return false; + } + db.Close(); + m_songIDCache.insert(m_songIDCache.end(), songIDs2.begin(), songIDs2.end()); + } + + // Songs and music videos are random from query, but need mixing together when have both + if (songcount > 0 && videocount > 0 ) + KODI::UTILS::RandomShuffle(m_songIDCache.begin(), m_songIDCache.end()); + + CLog::Log(LOGINFO,"PARTY MODE MANAGER: Matching songs = {0}", m_iMatchingSongs); + CLog::Log(LOGINFO,"PARTY MODE MANAGER: Party mode enabled!"); + + PLAYLIST::Id playlistId = GetPlaylistId(); + + CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId); + CServiceBroker::GetPlaylistPlayer().SetShuffle(playlistId, false); + CServiceBroker::GetPlaylistPlayer().SetRepeat(playlistId, PLAYLIST::RepeatState::NONE); + + pDialog->SetLine(0, CVariant{m_bIsVideo ? 20252 : 20124}); + pDialog->Progress(); + // add initial songs + if (!AddRandomSongs()) + { + pDialog->Close(); + return false; + } + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + CLog::Log(LOGDEBUG, "{} time for song fetch: {} ms", __FUNCTION__, duration.count()); + + // start playing + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId); + Play(0); + + pDialog->Close(); + // open now playing window + if (StringUtils::EqualsNoCase(m_type, "songs")) + { + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_MUSIC_PLAYLIST) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST); + } + + // done + m_bEnabled = true; + Announce(); + return true; +} + +void CPartyModeManager::Disable() +{ + if (!IsEnabled()) + return; + m_bEnabled = false; + Announce(); + CLog::Log(LOGINFO,"PARTY MODE MANAGER: Party mode disabled."); +} + +void CPartyModeManager::OnSongChange(bool bUpdatePlayed /* = false */) +{ + if (!IsEnabled()) + return; + Process(); + if (bUpdatePlayed) + m_iSongsPlayed++; +} + +void CPartyModeManager::AddUserSongs(PLAYLIST::CPlayList& tempList, bool bPlay /* = false */) +{ + if (!IsEnabled()) + return; + + // where do we add? + int iAddAt = -1; + if (m_iLastUserSong < 0 || bPlay) + iAddAt = 1; // under the currently playing song + else + iAddAt = m_iLastUserSong + 1; // under the last user added song + + int iNewUserSongs = tempList.size(); + CLog::Log(LOGINFO, "PARTY MODE MANAGER: Adding {} user selected songs at {}", iNewUserSongs, + iAddAt); + + CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId()).Insert(tempList, iAddAt); + + // update last user added song location + if (m_iLastUserSong < 0) + m_iLastUserSong = 0; + m_iLastUserSong += iNewUserSongs; + + if (bPlay) + Play(1); +} + +void CPartyModeManager::AddUserSongs(CFileItemList& tempList, bool bPlay /* = false */) +{ + if (!IsEnabled()) + return; + + // where do we add? + int iAddAt = -1; + if (m_iLastUserSong < 0 || bPlay) + iAddAt = 1; // under the currently playing song + else + iAddAt = m_iLastUserSong + 1; // under the last user added song + + int iNewUserSongs = tempList.Size(); + CLog::Log(LOGINFO, "PARTY MODE MANAGER: Adding {} user selected songs at {}", iNewUserSongs, + iAddAt); + + CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId()).Insert(tempList, iAddAt); + + // update last user added song location + if (m_iLastUserSong < 0) + m_iLastUserSong = 0; + m_iLastUserSong += iNewUserSongs; + + if (bPlay) + Play(1); +} + +void CPartyModeManager::Process() +{ + ReapSongs(); + MovePlaying(); + AddRandomSongs(); + UpdateStats(); + SendUpdateMessage(); +} + +bool CPartyModeManager::AddRandomSongs() +{ + // All songs have been picked, no more to add + if (static_cast<int>(m_songIDCache.size()) == m_iMatchingSongsPicked) + return false; + + PLAYLIST::CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId()); + int iMissingSongs = QUEUE_DEPTH - playlist.size(); + + if (iMissingSongs > 0) + { + // Limit songs fetched to remainder of songID cache + iMissingSongs = std::min(iMissingSongs, static_cast<int>(m_songIDCache.size()) - m_iMatchingSongsPicked); + + // Pick iMissingSongs from remaining songID cache + std::string sqlWhereMusic = "songview.idSong IN ("; + std::string sqlWhereVideo = "idMVideo IN ("; + + bool bSongs = false; + bool bMusicVideos = false; + for (int i = m_iMatchingSongsPicked; i < m_iMatchingSongsPicked + iMissingSongs; i++) + { + std::string song = StringUtils::Format("{},", m_songIDCache[i].second); + if (m_songIDCache[i].first == 1) + { + sqlWhereMusic += song; + bSongs = true; + } + else if (m_songIDCache[i].first == 2) + { + sqlWhereVideo += song; + bMusicVideos = true; + } + } + CFileItemList items; + + if (bSongs) + { + sqlWhereMusic.back() = ')'; // replace the last comma with closing bracket + // Apply random sort (and limit) at db query for efficiency + SortDescription SortDescription; + SortDescription.sortBy = SortByRandom; + SortDescription.limitEnd = QUEUE_DEPTH; + CMusicDatabase database; + if (database.Open()) + { + database.GetSongsFullByWhere("musicdb://songs/", CDatabase::Filter(sqlWhereMusic), + items, SortDescription, true); + + // Get artist and album properties for songs + for (auto& item : items) + database.SetPropertiesForFileItem(*item); + database.Close(); + } + else + { + OnError(16033, "Party mode could not open database. Aborting."); + return false; + } + } + if (bMusicVideos) + { + sqlWhereVideo.back() = ')'; // replace the last comma with closing bracket + CVideoDatabase database; + if (database.Open()) + { + database.GetMusicVideosByWhere("videodb://musicvideos/titles/", + CDatabase::Filter(sqlWhereVideo), items); + database.Close(); + } + else + { + OnError(16033, "Party mode could not open database. Aborting."); + return false; + } + } + + // Randomize if the list has music videos or they will be in db order + // Songs only are already random. + if (bMusicVideos) + items.Randomize(); + for (const auto& item : items) + { + // Update songID cache with order items in playlist + if (item->HasMusicInfoTag()) + { + m_songIDCache[m_iMatchingSongsPicked].first = 1; + m_songIDCache[m_iMatchingSongsPicked].second = item->GetMusicInfoTag()->GetDatabaseId(); + } + else if (item->HasVideoInfoTag()) + { + m_songIDCache[m_iMatchingSongsPicked].first = 2; + m_songIDCache[m_iMatchingSongsPicked].second = item->GetVideoInfoTag()->m_iDbId; + } + CFileItemPtr pItem(item); + Add(pItem); // inc m_iMatchingSongsPicked + } + } + return true; +} + +void CPartyModeManager::Add(CFileItemPtr &pItem) +{ + PLAYLIST::CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId()); + playlist.Add(pItem); + CLog::Log(LOGINFO, "PARTY MODE MANAGER: Adding randomly selected song at {}:[{}]", + playlist.size() - 1, pItem->GetPath()); + m_iMatchingSongsPicked++; +} + +bool CPartyModeManager::ReapSongs() +{ + const PLAYLIST::Id playlistId = GetPlaylistId(); + + // reap any played songs + int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + int i=0; + while (i < CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).size()) + { + if (i < iCurrentSong) + { + CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).Remove(i); + iCurrentSong--; + if (i <= m_iLastUserSong) + m_iLastUserSong--; + } + else + i++; + } + + CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong); + return true; +} + +bool CPartyModeManager::MovePlaying() +{ + // move current song to the top if its not there + int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + + if (iCurrentSong > 0) + { + CLog::Log(LOGINFO, "PARTY MODE MANAGER: Moving currently playing song from {} to 0", + iCurrentSong); + PLAYLIST::CPlayList& playlist = + CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId()); + PLAYLIST::CPlayList playlistTemp; + playlistTemp.Add(playlist[iCurrentSong]); + playlist.Remove(iCurrentSong); + for (int i=0; i<playlist.size(); i++) + playlistTemp.Add(playlist[i]); + playlist.Clear(); + for (int i=0; i<playlistTemp.size(); i++) + playlist.Add(playlistTemp[i]); + } + CServiceBroker::GetPlaylistPlayer().SetCurrentSong(0); + return true; +} + +void CPartyModeManager::SendUpdateMessage() +{ + CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +void CPartyModeManager::Play(int iPos) +{ + // Move current song to the top if its not there. Playlist filled up below by + // OnSongChange call from application GUI_MSG_PLAYBACK_STARTED processing + CServiceBroker::GetPlaylistPlayer().Play(iPos, ""); + CLog::Log(LOGINFO, "PARTY MODE MANAGER: Playing song at {}", iPos); +} + +void CPartyModeManager::OnError(int iError, const std::string& strLogMessage) +{ + // open error dialog + HELPERS::ShowOKDialogLines(CVariant{257}, CVariant{16030}, CVariant{iError}, CVariant{0}); + CLog::Log(LOGERROR, "PARTY MODE MANAGER: {}", strLogMessage); + m_bEnabled = false; + SendUpdateMessage(); +} + +int CPartyModeManager::GetSongsPlayed() +{ + if (!IsEnabled()) + return -1; + return m_iSongsPlayed; +} + +int CPartyModeManager::GetMatchingSongs() +{ + if (!IsEnabled()) + return -1; + return m_iMatchingSongs; +} + +int CPartyModeManager::GetMatchingSongsPicked() +{ + if (!IsEnabled()) + return -1; + return m_iMatchingSongsPicked; +} + +int CPartyModeManager::GetMatchingSongsLeft() +{ + if (!IsEnabled()) + return -1; + return m_iMatchingSongsLeft; +} + +int CPartyModeManager::GetRelaxedSongs() +{ + if (!IsEnabled()) + return -1; + return m_iRelaxedSongs; +} + +int CPartyModeManager::GetRandomSongs() +{ + if (!IsEnabled()) + return -1; + return m_iRandomSongs; +} + +PartyModeContext CPartyModeManager::GetType() const +{ + if (!IsEnabled()) + return PARTYMODECONTEXT_UNKNOWN; + + if (m_bIsVideo) + return PARTYMODECONTEXT_VIDEO; + + return PARTYMODECONTEXT_MUSIC; +} + +void CPartyModeManager::ClearState() +{ + m_iLastUserSong = -1; + m_iSongsPlayed = 0; + m_iMatchingSongs = 0; + m_iMatchingSongsPicked = 0; + m_iMatchingSongsLeft = 0; + m_iRelaxedSongs = 0; + m_iRandomSongs = 0; + + m_songIDCache.clear(); +} + +void CPartyModeManager::UpdateStats() +{ + m_iMatchingSongsLeft = m_iMatchingSongs - m_iMatchingSongsPicked; + m_iRandomSongs = m_iMatchingSongsPicked; + m_iRelaxedSongs = 0; // unsupported at this stage +} + +bool CPartyModeManager::IsEnabled(PartyModeContext context /* = PARTYMODECONTEXT_UNKNOWN */) const +{ + if (!m_bEnabled) return false; + if (context == PARTYMODECONTEXT_VIDEO) + return m_bIsVideo; + if (context == PARTYMODECONTEXT_MUSIC) + return !m_bIsVideo; + return true; // unknown, but we're enabled +} + +void CPartyModeManager::Announce() +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlaying()) + { + CVariant data; + + data["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist(); + data["property"]["partymode"] = m_bEnabled; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPropertyChanged", + data); + } +} + +PLAYLIST::Id CPartyModeManager::GetPlaylistId() const +{ + return m_bIsVideo ? PLAYLIST::TYPE_VIDEO : PLAYLIST::TYPE_MUSIC; +} |