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