1 /*
2  *  Copyright (C) 2005-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "PartyModeManager.h"
10 
11 #include "Application.h"
12 #include "FileItem.h"
13 #include "GUIUserMessages.h"
14 #include "PlayListPlayer.h"
15 #include "ServiceBroker.h"
16 #include "dialogs/GUIDialogProgress.h"
17 #include "guilib/GUIComponent.h"
18 #include "guilib/GUIWindowManager.h"
19 #include "interfaces/AnnouncementManager.h"
20 #include "messaging/helpers/DialogOKHelper.h"
21 #include "music/MusicDatabase.h"
22 #include "music/tags/MusicInfoTag.h"
23 #include "playlists/PlayList.h"
24 #include "playlists/SmartPlayList.h"
25 #include "profiles/ProfileManager.h"
26 #include "settings/SettingsComponent.h"
27 #include "threads/SystemClock.h"
28 #include "utils/Random.h"
29 #include "utils/StringUtils.h"
30 #include "utils/Variant.h"
31 #include "utils/log.h"
32 #include "video/VideoDatabase.h"
33 #include "video/VideoInfoTag.h"
34 
35 #include <algorithm>
36 
37 using namespace KODI::MESSAGING;
38 using namespace PLAYLIST;
39 
40 #define QUEUE_DEPTH       10
41 
CPartyModeManager(void)42 CPartyModeManager::CPartyModeManager(void)
43 {
44   m_bIsVideo = false;
45   m_bEnabled = false;
46   ClearState();
47 }
48 
Enable(PartyModeContext context,const std::string & strXspPath)49 bool CPartyModeManager::Enable(PartyModeContext context /*= PARTYMODECONTEXT_MUSIC*/, const std::string& strXspPath /*= ""*/)
50 {
51   // Filter using our PartyMode xml file
52   CSmartPlaylist playlist;
53   std::string partyModePath;
54   bool playlistLoaded;
55 
56   m_bIsVideo = context == PARTYMODECONTEXT_VIDEO;
57 
58   const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
59 
60   if (!strXspPath.empty()) //if a path to a smartplaylist is supplied use it
61     partyModePath = strXspPath;
62   else if (m_bIsVideo)
63     partyModePath = profileManager->GetUserDataItem("PartyMode-Video.xsp");
64   else
65     partyModePath = profileManager->GetUserDataItem("PartyMode.xsp");
66 
67   playlistLoaded=playlist.Load(partyModePath);
68 
69   if (playlistLoaded)
70   {
71     m_type = playlist.GetType();
72     if (context == PARTYMODECONTEXT_UNKNOWN)
73     {
74       //get it from the xsp file
75       m_bIsVideo = (StringUtils::EqualsNoCase(m_type, "video") ||
76         StringUtils::EqualsNoCase(m_type, "musicvideos") ||
77         StringUtils::EqualsNoCase(m_type, "mixed"));
78     }
79   }
80   else if (m_bIsVideo)
81     m_type = "musicvideos";
82   else
83     m_type = "songs";
84 
85   CGUIDialogProgress* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
86   int iHeading = (m_bIsVideo ? 20250 : 20121);
87   int iLine0 = (m_bIsVideo ? 20251 : 20123);
88   pDialog->SetHeading(CVariant{iHeading});
89   pDialog->SetLine(0, CVariant{iLine0});
90   pDialog->SetLine(1, CVariant{""});
91   pDialog->SetLine(2, CVariant{""});
92   pDialog->Open();
93 
94   ClearState();
95   std::string strCurrentFilterMusic;
96   std::string strCurrentFilterVideo;
97   unsigned int songcount = 0;
98   unsigned int videocount = 0;
99   unsigned int time = XbmcThreads::SystemClockMillis();
100 
101   if (StringUtils::EqualsNoCase(m_type, "songs") ||
102       StringUtils::EqualsNoCase(m_type, "mixed"))
103   {
104     CMusicDatabase db;
105     if (db.Open())
106     {
107       std::set<std::string> playlists;
108       if (playlistLoaded)
109       {
110         playlist.SetType("songs");
111         strCurrentFilterMusic = playlist.GetWhereClause(db, playlists);
112       }
113 
114       CLog::Log(LOGINFO, "PARTY MODE MANAGER: Registering filter:[%s]", strCurrentFilterMusic.c_str());
115       songcount = db.GetRandomSongIDs(CDatabase::Filter(strCurrentFilterMusic), m_songIDCache);
116       m_iMatchingSongs = static_cast<int>(songcount);
117       if (m_iMatchingSongs < 1 && StringUtils::EqualsNoCase(m_type, "songs"))
118       {
119         pDialog->Close();
120         db.Close();
121         OnError(16031, "Party mode found no matching songs. Aborting.");
122         return false;
123       }
124     }
125     else
126     {
127       pDialog->Close();
128       OnError(16033, "Party mode could not open database. Aborting.");
129       return false;
130     }
131     db.Close();
132   }
133 
134   if (StringUtils::EqualsNoCase(m_type, "musicvideos") ||
135       StringUtils::EqualsNoCase(m_type, "mixed"))
136   {
137     std::vector< std::pair<int,int> > songIDs2;
138     CVideoDatabase db;
139     if (db.Open())
140     {
141       std::set<std::string> playlists;
142       if (playlistLoaded)
143       {
144         playlist.SetType("musicvideos");
145         strCurrentFilterVideo = playlist.GetWhereClause(db, playlists);
146       }
147 
148       CLog::Log(LOGINFO, "PARTY MODE MANAGER: Registering filter:[%s]", strCurrentFilterVideo.c_str());
149       videocount = db.GetRandomMusicVideoIDs(strCurrentFilterVideo, songIDs2);
150       m_iMatchingSongs += static_cast<int>(videocount);
151       if (m_iMatchingSongs < 1)
152       {
153         pDialog->Close();
154         db.Close();
155         OnError(16031, "Party mode found no matching songs. Aborting.");
156         return false;
157       }
158     }
159     else
160     {
161       pDialog->Close();
162       OnError(16033, "Party mode could not open database. Aborting.");
163       return false;
164     }
165     db.Close();
166     m_songIDCache.insert(m_songIDCache.end(), songIDs2.begin(), songIDs2.end());
167   }
168 
169   // Songs and music videos are random from query, but need mixing together when have both
170   if (songcount > 0 && videocount > 0 )
171     KODI::UTILS::RandomShuffle(m_songIDCache.begin(), m_songIDCache.end());
172 
173   CLog::Log(LOGINFO,"PARTY MODE MANAGER: Matching songs = {0}", m_iMatchingSongs);
174   CLog::Log(LOGINFO,"PARTY MODE MANAGER: Party mode enabled!");
175 
176   int iPlaylist = m_bIsVideo ? PLAYLIST_VIDEO : PLAYLIST_MUSIC;
177 
178   CServiceBroker::GetPlaylistPlayer().ClearPlaylist(iPlaylist);
179   CServiceBroker::GetPlaylistPlayer().SetShuffle(iPlaylist, false);
180   CServiceBroker::GetPlaylistPlayer().SetRepeat(iPlaylist, PLAYLIST::REPEAT_NONE);
181 
182   pDialog->SetLine(0, CVariant{m_bIsVideo ? 20252 : 20124});
183   pDialog->Progress();
184   // add initial songs
185   if (!AddRandomSongs())
186   {
187     pDialog->Close();
188     return false;
189   }
190   CLog::Log(LOGDEBUG, "%s time for song fetch: %u",
191             __FUNCTION__, XbmcThreads::SystemClockMillis() - time);
192 
193   // start playing
194   CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(iPlaylist);
195   Play(0);
196 
197   pDialog->Close();
198   // open now playing window
199   if (StringUtils::EqualsNoCase(m_type, "songs"))
200   {
201     if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_MUSIC_PLAYLIST)
202       CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
203   }
204 
205   // done
206   m_bEnabled = true;
207   Announce();
208   return true;
209 }
210 
Disable()211 void CPartyModeManager::Disable()
212 {
213   if (!IsEnabled())
214     return;
215   m_bEnabled = false;
216   Announce();
217   CLog::Log(LOGINFO,"PARTY MODE MANAGER: Party mode disabled.");
218 }
219 
OnSongChange(bool bUpdatePlayed)220 void CPartyModeManager::OnSongChange(bool bUpdatePlayed /* = false */)
221 {
222   if (!IsEnabled())
223     return;
224   Process();
225   if (bUpdatePlayed)
226     m_iSongsPlayed++;
227 }
228 
AddUserSongs(CPlayList & tempList,bool bPlay)229 void CPartyModeManager::AddUserSongs(CPlayList& tempList, bool bPlay /* = false */)
230 {
231   if (!IsEnabled())
232     return;
233 
234   // where do we add?
235   int iAddAt = -1;
236   if (m_iLastUserSong < 0 || bPlay)
237     iAddAt = 1; // under the currently playing song
238   else
239     iAddAt = m_iLastUserSong + 1; // under the last user added song
240 
241   int iNewUserSongs = tempList.size();
242   CLog::Log(LOGINFO,"PARTY MODE MANAGER: Adding %i user selected songs at %i", iNewUserSongs, iAddAt);
243 
244   int iPlaylist = PLAYLIST_MUSIC;
245   if (m_bIsVideo)
246     iPlaylist = PLAYLIST_VIDEO;
247   CServiceBroker::GetPlaylistPlayer().GetPlaylist(iPlaylist).Insert(tempList, iAddAt);
248 
249   // update last user added song location
250   if (m_iLastUserSong < 0)
251     m_iLastUserSong = 0;
252   m_iLastUserSong += iNewUserSongs;
253 
254   if (bPlay)
255     Play(1);
256 }
257 
AddUserSongs(CFileItemList & tempList,bool bPlay)258 void CPartyModeManager::AddUserSongs(CFileItemList& tempList, bool bPlay /* = false */)
259 {
260   if (!IsEnabled())
261     return;
262 
263   // where do we add?
264   int iAddAt = -1;
265   if (m_iLastUserSong < 0 || bPlay)
266     iAddAt = 1; // under the currently playing song
267   else
268     iAddAt = m_iLastUserSong + 1; // under the last user added song
269 
270   int iNewUserSongs = tempList.Size();
271   CLog::Log(LOGINFO,"PARTY MODE MANAGER: Adding %i user selected songs at %i", iNewUserSongs, iAddAt);
272 
273   int iPlaylist = PLAYLIST_MUSIC;
274   if (m_bIsVideo)
275     iPlaylist = PLAYLIST_VIDEO;
276 
277   CServiceBroker::GetPlaylistPlayer().GetPlaylist(iPlaylist).Insert(tempList, iAddAt);
278 
279   // update last user added song location
280   if (m_iLastUserSong < 0)
281     m_iLastUserSong = 0;
282   m_iLastUserSong += iNewUserSongs;
283 
284   if (bPlay)
285     Play(1);
286 }
287 
Process()288 void CPartyModeManager::Process()
289 {
290   ReapSongs();
291   MovePlaying();
292   AddRandomSongs();
293   UpdateStats();
294   SendUpdateMessage();
295 }
296 
AddRandomSongs()297 bool CPartyModeManager::AddRandomSongs()
298 {
299   // All songs have been picked, no more to add
300   if (static_cast<int>(m_songIDCache.size()) == m_iMatchingSongsPicked)
301     return false;
302 
303   int iPlaylist = PLAYLIST_MUSIC;
304   if (m_bIsVideo)
305     iPlaylist = PLAYLIST_VIDEO;
306 
307   CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(iPlaylist);
308   int iMissingSongs = QUEUE_DEPTH - playlist.size();
309 
310   if (iMissingSongs > 0)
311   {
312     // Limit songs fetched to remainder of songID cache
313     iMissingSongs = std::min(iMissingSongs, static_cast<int>(m_songIDCache.size()) - m_iMatchingSongsPicked);
314 
315     // Pick iMissingSongs from remaining songID cache
316     std::string sqlWhereMusic = "songview.idSong IN (";
317     std::string sqlWhereVideo = "idMVideo IN (";
318 
319     bool bSongs = false;
320     bool bMusicVideos = false;
321     for (int i = m_iMatchingSongsPicked; i < m_iMatchingSongsPicked + iMissingSongs; i++)
322     {
323       std::string song = StringUtils::Format("%i,", m_songIDCache[i].second);
324       if (m_songIDCache[i].first == 1)
325       {
326         sqlWhereMusic += song;
327         bSongs = true;
328       }
329       else if (m_songIDCache[i].first == 2)
330       {
331         sqlWhereVideo += song;
332         bMusicVideos = true;
333       }
334     }
335     CFileItemList items;
336 
337     if (bSongs)
338     {
339       sqlWhereMusic.back() = ')'; // replace the last comma with closing bracket
340       // Apply random sort (and limit) at db query for efficiency
341       SortDescription SortDescription;
342       SortDescription.sortBy = SortByRandom;
343       SortDescription.limitEnd = QUEUE_DEPTH;
344       CMusicDatabase database;
345       if (database.Open())
346       {
347         database.GetSongsFullByWhere("musicdb://songs/", CDatabase::Filter(sqlWhereMusic),
348           items, SortDescription, true);
349 
350         // Get artist and album properties for songs
351         for (auto& item : items)
352           database.SetPropertiesForFileItem(*item);
353         database.Close();
354       }
355       else
356       {
357         OnError(16033, "Party mode could not open database. Aborting.");
358         return false;
359       }
360     }
361     if (bMusicVideos)
362     {
363       sqlWhereVideo.back() = ')'; // replace the last comma with closing bracket
364       CVideoDatabase database;
365       if (database.Open())
366       {
367         database.GetMusicVideosByWhere("videodb://musicvideos/titles/",
368           CDatabase::Filter(sqlWhereVideo), items);
369         database.Close();
370       }
371       else
372       {
373         OnError(16033, "Party mode could not open database. Aborting.");
374         return false;
375       }
376     }
377 
378     // Randomize if the list has music videos or they will be in db order
379     // Songs only are already random.
380     if (bMusicVideos)
381       items.Randomize();
382     for (const auto& item : items)
383     {
384       // Update songID cache with order items in playlist
385       if (item->HasMusicInfoTag())
386       {
387         m_songIDCache[m_iMatchingSongsPicked].first = 1;
388         m_songIDCache[m_iMatchingSongsPicked].second = item->GetMusicInfoTag()->GetDatabaseId();
389       }
390       else if (item->HasVideoInfoTag())
391       {
392         m_songIDCache[m_iMatchingSongsPicked].first = 2;
393         m_songIDCache[m_iMatchingSongsPicked].second = item->GetVideoInfoTag()->m_iDbId;
394       }
395       CFileItemPtr pItem(item);
396       Add(pItem); // inc m_iMatchingSongsPicked
397     }
398   }
399   return true;
400 }
401 
Add(CFileItemPtr & pItem)402 void CPartyModeManager::Add(CFileItemPtr &pItem)
403 {
404   int iPlaylist = m_bIsVideo ? PLAYLIST_VIDEO : PLAYLIST_MUSIC;
405 
406   CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(iPlaylist);
407   playlist.Add(pItem);
408   CLog::Log(LOGINFO,"PARTY MODE MANAGER: Adding randomly selected song at %i:[%s]", playlist.size() - 1, pItem->GetPath().c_str());
409   m_iMatchingSongsPicked++;
410 }
411 
ReapSongs()412 bool CPartyModeManager::ReapSongs()
413 {
414   int iPlaylist = m_bIsVideo ? PLAYLIST_VIDEO : PLAYLIST_MUSIC;
415 
416   // reap any played songs
417   int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
418   int i=0;
419   while (i < CServiceBroker::GetPlaylistPlayer().GetPlaylist(iPlaylist).size())
420   {
421     if (i < iCurrentSong)
422     {
423       CServiceBroker::GetPlaylistPlayer().GetPlaylist(iPlaylist).Remove(i);
424       iCurrentSong--;
425       if (i <= m_iLastUserSong)
426         m_iLastUserSong--;
427     }
428     else
429       i++;
430   }
431 
432   CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong);
433   return true;
434 }
435 
MovePlaying()436 bool CPartyModeManager::MovePlaying()
437 {
438   // move current song to the top if its not there
439   int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
440   int iPlaylist = m_bIsVideo ? PLAYLIST_MUSIC : PLAYLIST_VIDEO;
441 
442   if (iCurrentSong > 0)
443   {
444     CLog::Log(LOGINFO,"PARTY MODE MANAGER: Moving currently playing song from %i to 0", iCurrentSong);
445     CPlayList &playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(iPlaylist);
446     CPlayList playlistTemp;
447     playlistTemp.Add(playlist[iCurrentSong]);
448     playlist.Remove(iCurrentSong);
449     for (int i=0; i<playlist.size(); i++)
450       playlistTemp.Add(playlist[i]);
451     playlist.Clear();
452     for (int i=0; i<playlistTemp.size(); i++)
453       playlist.Add(playlistTemp[i]);
454   }
455   CServiceBroker::GetPlaylistPlayer().SetCurrentSong(0);
456   return true;
457 }
458 
SendUpdateMessage()459 void CPartyModeManager::SendUpdateMessage()
460 {
461   CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
462   CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
463 }
464 
Play(int iPos)465 void CPartyModeManager::Play(int iPos)
466 {
467   // Move current song to the top if its not there. Playlist filled up below by
468   // OnSongChange call from application GUI_MSG_PLAYBACK_STARTED processing
469   CServiceBroker::GetPlaylistPlayer().Play(iPos, "");
470   CLog::Log(LOGINFO,"PARTY MODE MANAGER: Playing song at %i", iPos);
471 }
472 
OnError(int iError,const std::string & strLogMessage)473 void CPartyModeManager::OnError(int iError, const std::string&  strLogMessage)
474 {
475   // open error dialog
476   HELPERS::ShowOKDialogLines(CVariant{257}, CVariant{16030}, CVariant{iError}, CVariant{0});
477   CLog::Log(LOGERROR, "PARTY MODE MANAGER: %s", strLogMessage.c_str());
478   m_bEnabled = false;
479   SendUpdateMessage();
480 }
481 
GetSongsPlayed()482 int CPartyModeManager::GetSongsPlayed()
483 {
484   if (!IsEnabled())
485     return -1;
486   return m_iSongsPlayed;
487 }
488 
GetMatchingSongs()489 int CPartyModeManager::GetMatchingSongs()
490 {
491   if (!IsEnabled())
492     return -1;
493   return m_iMatchingSongs;
494 }
495 
GetMatchingSongsPicked()496 int CPartyModeManager::GetMatchingSongsPicked()
497 {
498   if (!IsEnabled())
499     return -1;
500   return m_iMatchingSongsPicked;
501 }
502 
GetMatchingSongsLeft()503 int CPartyModeManager::GetMatchingSongsLeft()
504 {
505   if (!IsEnabled())
506     return -1;
507   return m_iMatchingSongsLeft;
508 }
509 
GetRelaxedSongs()510 int CPartyModeManager::GetRelaxedSongs()
511 {
512   if (!IsEnabled())
513     return -1;
514   return m_iRelaxedSongs;
515 }
516 
GetRandomSongs()517 int CPartyModeManager::GetRandomSongs()
518 {
519   if (!IsEnabled())
520     return -1;
521   return m_iRandomSongs;
522 }
523 
GetType() const524 PartyModeContext CPartyModeManager::GetType() const
525 {
526   if (!IsEnabled())
527     return PARTYMODECONTEXT_UNKNOWN;
528 
529   if (m_bIsVideo)
530     return PARTYMODECONTEXT_VIDEO;
531 
532   return PARTYMODECONTEXT_MUSIC;
533 }
534 
ClearState()535 void CPartyModeManager::ClearState()
536 {
537   m_iLastUserSong = -1;
538   m_iSongsPlayed = 0;
539   m_iMatchingSongs = 0;
540   m_iMatchingSongsPicked = 0;
541   m_iMatchingSongsLeft = 0;
542   m_iRelaxedSongs = 0;
543   m_iRandomSongs = 0;
544 
545   m_songIDCache.clear();
546 }
547 
UpdateStats()548 void CPartyModeManager::UpdateStats()
549 {
550   m_iMatchingSongsLeft = m_iMatchingSongs - m_iMatchingSongsPicked;
551   m_iRandomSongs = m_iMatchingSongsPicked;
552   m_iRelaxedSongs = 0;  // unsupported at this stage
553 }
554 
IsEnabled(PartyModeContext context) const555 bool CPartyModeManager::IsEnabled(PartyModeContext context /* = PARTYMODECONTEXT_UNKNOWN */) const
556 {
557   if (!m_bEnabled) return false;
558   if (context == PARTYMODECONTEXT_VIDEO)
559     return m_bIsVideo;
560   if (context == PARTYMODECONTEXT_MUSIC)
561     return !m_bIsVideo;
562   return true; // unknown, but we're enabled
563 }
564 
Announce()565 void CPartyModeManager::Announce()
566 {
567   if (g_application.GetAppPlayer().IsPlaying())
568   {
569     CVariant data;
570 
571     data["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
572     data["property"]["partymode"] = m_bEnabled;
573     CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPropertyChanged",
574                                                        data);
575   }
576 }
577