/* * 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 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 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(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 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(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 > songIDs2; CVideoDatabase db; if (db.Open()) { std::set 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(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(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(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(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; iGetWindowManager().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(); 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; }