2 * Copyright (C) 2022 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "VideoGUIUtils.h"
12 #include "FileItemList.h"
13 #include "GUIPassword.h"
14 #include "PartyModeManager.h"
15 #include "PlayListPlayer.h"
16 #include "ServiceBroker.h"
18 #include "application/ApplicationComponents.h"
19 #include "application/ApplicationPlayer.h"
20 #include "dialogs/GUIDialogBusy.h"
21 #include "filesystem/Directory.h"
22 #include "filesystem/VideoDatabaseDirectory.h"
23 #include "filesystem/VideoDatabaseDirectory/DirectoryNode.h"
24 #include "guilib/GUIComponent.h"
25 #include "guilib/GUIWindowManager.h"
26 #include "guilib/LocalizeStrings.h"
27 #include "music/MusicFileItemClassify.h"
28 #include "network/NetworkFileItemClassify.h"
29 #include "playlists/PlayList.h"
30 #include "playlists/PlayListFactory.h"
31 #include "playlists/PlayListFileItemClassify.h"
32 #include "profiles/ProfileManager.h"
33 #include "settings/MediaSettings.h"
34 #include "settings/SettingUtils.h"
35 #include "settings/Settings.h"
36 #include "settings/SettingsComponent.h"
37 #include "threads/IRunnable.h"
38 #include "utils/FileUtils.h"
39 #include "utils/StringUtils.h"
40 #include "utils/URIUtils.h"
41 #include "utils/log.h"
42 #include "video/VideoDatabase.h"
43 #include "video/VideoFileItemClassify.h"
44 #include "video/VideoInfoTag.h"
45 #include "video/VideoUtils.h"
46 #include "view/GUIViewState.h"
53 class CAsyncGetItemsForPlaylist
: public IRunnable
56 CAsyncGetItemsForPlaylist(const std::shared_ptr
<CFileItem
>& item
, CFileItemList
& queuedItems
)
58 m_resume((item
->GetStartOffset() == STARTOFFSET_RESUME
) &&
59 VIDEO::UTILS::GetItemResumeInformation(*item
).isResumable
),
60 m_queuedItems(queuedItems
)
64 ~CAsyncGetItemsForPlaylist() override
= default;
68 // fast lookup is needed here
69 m_queuedItems
.SetFastLookup(true);
71 GetItemsForPlaylist(m_item
);
75 void GetItemsForPlaylist(const std::shared_ptr
<CFileItem
>& item
);
77 const std::shared_ptr
<CFileItem
> m_item
;
78 const bool m_resume
{false};
79 CFileItemList
& m_queuedItems
;
82 SortDescription
GetSortDescription(const CGUIViewState
& state
, const CFileItemList
& items
)
84 SortDescription sortDescDate
;
86 auto sortDescriptions
= state
.GetSortDescriptions();
87 for (auto& sortDescription
: sortDescriptions
)
89 if (sortDescription
.sortBy
== SortByEpisodeNumber
)
91 // check whether at least one item has actually an episode number set
92 for (const auto& item
: items
)
94 if (item
->HasVideoInfoTag() && item
->GetVideoInfoTag()->m_iEpisode
> 0)
96 // first choice for folders containig episodes
97 sortDescription
.sortOrder
= SortOrderAscending
;
98 return sortDescription
;
103 else if (sortDescription
.sortBy
== SortByYear
)
105 // check whether at least one item has actually a year set
106 for (const auto& item
: items
)
108 if (item
->HasVideoInfoTag() && item
->GetVideoInfoTag()->HasYear())
110 // first choice for folders containing movies
111 sortDescription
.sortOrder
= SortOrderAscending
;
112 return sortDescription
;
116 else if (sortDescription
.sortBy
== SortByDate
)
118 // check whether at least one item has actually a valid date set
119 for (const auto& item
: items
)
121 if (item
->m_dateTime
.IsValid())
123 // fallback, if neither ByEpisode nor ByYear is available
124 sortDescDate
= sortDescription
;
125 sortDescDate
.sortOrder
= SortOrderAscending
;
126 break; // leave items loop. we can still find ByEpisode or ByYear. so, no return here.
132 if (sortDescDate
.sortBy
!= SortByNone
)
135 return state
.GetSortMethod(); // last resort
138 void CAsyncGetItemsForPlaylist::GetItemsForPlaylist(const std::shared_ptr
<CFileItem
>& item
)
140 if (item
->IsParentFolder() || !item
->CanQueue() || item
->IsRAR() || item
->IsZIP())
143 if (item
->m_bIsFolder
)
145 if (!item
->IsPlugin())
147 // check if it's a folder with dvd or bluray files, then just add the relevant file
148 const std::string mediapath
= VIDEO::UTILS::GetOpticalMediaPath(*item
);
149 if (!mediapath
.empty())
151 m_queuedItems
.Add(std::make_shared
<CFileItem
>(mediapath
, false));
156 // Check if we add a locked share
157 if (!item
->IsPVR() && item
->m_bIsShareOrDrive
)
159 if (!g_passwordManager
.IsItemUnlocked(item
.get(), "video"))
164 XFILE::CDirectory::GetDirectory(item
->GetPath(), items
, "", XFILE::DIR_FLAG_DEFAULTS
);
166 int viewStateWindowId
= WINDOW_VIDEO_NAV
;
167 if (URIUtils::IsPVRRadioRecordingFileOrFolder(item
->GetPath()))
168 viewStateWindowId
= WINDOW_RADIO_RECORDINGS
;
169 else if (URIUtils::IsPVRTVRecordingFileOrFolder(item
->GetPath()))
170 viewStateWindowId
= WINDOW_TV_RECORDINGS
;
172 const std::unique_ptr
<CGUIViewState
> state(
173 CGUIViewState::GetViewState(viewStateWindowId
, items
));
176 LABEL_MASKS labelMasks
;
177 state
->GetSortMethodLabelMasks(labelMasks
);
179 const CLabelFormatter
fileFormatter(labelMasks
.m_strLabelFile
, labelMasks
.m_strLabel2File
);
180 const CLabelFormatter
folderFormatter(labelMasks
.m_strLabelFolder
,
181 labelMasks
.m_strLabel2Folder
);
182 for (const auto& i
: items
)
184 if (i
->IsLabelPreformatted())
188 folderFormatter
.FormatLabels(i
.get());
190 fileFormatter
.FormatLabels(i
.get());
193 SortDescription sortDesc
;
194 if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == viewStateWindowId
)
196 sortDesc
= state
->GetSortMethod();
198 // It makes no sense to play from younger to older.
199 if (sortDesc
.sortBy
== SortByDate
|| sortDesc
.sortBy
== SortByYear
||
200 sortDesc
.sortBy
== SortByEpisodeNumber
)
201 sortDesc
.sortOrder
= SortOrderAscending
;
204 sortDesc
= GetSortDescription(*state
, items
);
206 if (sortDesc
.sortBy
== SortByLabel
)
207 items
.ClearSortState();
209 items
.Sort(sortDesc
);
212 if (items
.GetContent().empty() && !VIDEO::IsVideoDb(items
) && !items
.IsVirtualDirectoryRoot() &&
213 !items
.IsSourcesPath() && !items
.IsLibraryFolder())
218 std::string content
= db
.GetContentForPath(items
.GetPath());
219 if (content
.empty() && !items
.IsPlugin())
222 items
.SetContent(content
);
224 // Get play counts and resume bookmarks for the items.
225 db
.GetPlayCounts(items
.GetPath(), items
);
231 // put last played item at the begin of the playlist; add start offsets for videos
232 std::shared_ptr
<CFileItem
> lastPlayedItem
;
233 CDateTime lastPlayed
;
234 for (const auto& i
: items
)
236 if (!i
->HasVideoInfoTag())
239 const auto videoTag
= i
->GetVideoInfoTag();
241 const CBookmark
& bookmark
= videoTag
->GetResumePoint();
242 if (bookmark
.IsSet())
244 i
->SetStartOffset(CUtil::ConvertSecsToMilliSecs(bookmark
.timeInSeconds
));
246 const CDateTime
& currLastPlayed
= videoTag
->m_lastPlayed
;
247 if (currLastPlayed
.IsValid() && (!lastPlayed
.IsValid() || (lastPlayed
< currLastPlayed
)))
250 lastPlayed
= currLastPlayed
;
257 items
.Remove(lastPlayedItem
.get());
258 items
.AddFront(lastPlayedItem
, 0);
264 watchedMode
= WatchedModeUnwatched
;
266 watchedMode
= CMediaSettings::GetInstance().GetWatchedMode(items
.GetContent());
268 const bool unwatchedOnly
= watchedMode
== WatchedModeUnwatched
;
269 const bool watchedOnly
= watchedMode
== WatchedModeWatched
;
270 bool fetchedPlayCounts
= false;
271 for (const auto& i
: items
)
275 std::string path
= i
->GetPath();
276 URIUtils::RemoveSlashAtEnd(path
);
277 if (StringUtils::EndsWithNoCase(path
, "sample")) // skip sample folders
282 if (!fetchedPlayCounts
&&
283 (!i
->HasVideoInfoTag() || !i
->GetVideoInfoTag()->IsPlayCountSet()))
288 fetchedPlayCounts
= true;
289 db
.GetPlayCounts(items
.GetPath(), items
);
292 if (i
->HasVideoInfoTag() && i
->GetVideoInfoTag()->IsPlayCountSet())
294 const int playCount
= i
->GetVideoInfoTag()->GetPlayCount();
295 if ((unwatchedOnly
&& playCount
> 0) || (watchedOnly
&& playCount
<= 0))
299 GetItemsForPlaylist(i
);
302 else if (PLAYLIST::IsPlayList(*item
))
304 // just queue the playlist, it will be expanded on play
305 m_queuedItems
.Add(item
);
307 else if (NETWORK::IsInternetStream(*item
))
309 // just queue the internet stream, it will be expanded on play
310 m_queuedItems
.Add(item
);
312 else if (item
->IsPlugin() && item
->GetProperty("isplayable").asBoolean())
314 // a playable python files
315 m_queuedItems
.Add(item
);
317 else if (VIDEO::IsVideoDb(*item
))
319 // this case is needed unless we allow IsVideo() to return true for videodb items,
320 // but then we have issues with playlists of videodb items
321 const auto itemCopy
= std::make_shared
<CFileItem
>(*item
->GetVideoInfoTag());
322 itemCopy
->SetStartOffset(item
->GetStartOffset());
323 m_queuedItems
.Add(itemCopy
);
325 else if (!item
->IsNFO() && VIDEO::IsVideo(*item
))
327 m_queuedItems
.Add(item
);
331 std::string
GetVideoDbItemPath(const CFileItem
& item
)
333 std::string path
= item
.GetPath();
334 if (!URIUtils::IsVideoDb(path
))
335 path
= item
.GetProperty("original_listitem_url").asString();
337 if (URIUtils::IsVideoDb(path
))
343 void AddItemToPlayListAndPlay(const std::shared_ptr
<CFileItem
>& itemToQueue
,
344 const std::shared_ptr
<CFileItem
>& itemToPlay
,
345 const std::string
& player
)
347 // recursively add items to list
348 CFileItemList queuedItems
;
349 VIDEO::UTILS::GetItemsForPlayList(itemToQueue
, queuedItems
);
351 auto& playlistPlayer
= CServiceBroker::GetPlaylistPlayer();
352 playlistPlayer
.ClearPlaylist(PLAYLIST::Id::TYPE_VIDEO
);
353 playlistPlayer
.Reset();
354 playlistPlayer
.Add(PLAYLIST::Id::TYPE_VIDEO
, queuedItems
);
356 // figure out where to start playback
357 PLAYLIST::CPlayList
& playList
= playlistPlayer
.GetPlaylist(PLAYLIST::Id::TYPE_VIDEO
);
361 for (const std::shared_ptr
<CFileItem
>& queuedItem
: queuedItems
)
363 if (queuedItem
->IsSamePath(itemToPlay
.get()))
365 queuedItem
->m_lStartPartNumber
= itemToPlay
->m_lStartPartNumber
;
372 if (playlistPlayer
.IsShuffled(PLAYLIST::Id::TYPE_VIDEO
))
374 playList
.Swap(0, playList
.FindOrder(pos
));
378 playlistPlayer
.SetCurrentPlaylist(PLAYLIST::Id::TYPE_VIDEO
);
379 playlistPlayer
.Play(pos
, player
);
382 } // unnamed namespace
386 namespace KODI::VIDEO::UTILS
389 const std::shared_ptr
<CFileItem
>& itemIn
,
390 const std::string
& player
,
391 ContentUtils::PlayMode mode
/* = ContentUtils::PlayMode::CHECK_AUTO_PLAY_NEXT_VIDEO */)
395 // Allow queuing of unqueueable items
396 // when we try to queue them directly
397 if (!itemIn
->CanQueue())
399 // make a copy to not alter the original item
400 item
= std::make_shared
<CFileItem
>(*itemIn
);
401 item
->SetCanQueue(true);
404 if (item
->m_bIsFolder
&& !item
->IsPlugin())
406 AddItemToPlayListAndPlay(item
, nullptr, player
);
408 else if (item
->HasVideoInfoTag())
410 if (mode
== ContentUtils::PlayMode::PLAY_FROM_HERE
||
411 (mode
== ContentUtils::PlayMode::CHECK_AUTO_PLAY_NEXT_ITEM
&& IsAutoPlayNextItem(*item
)))
413 // Add item and all its siblings to the playlist and play. Prefer videodb path if available,
414 // because it provides more information than just a plain file system path for example.
415 std::string parentPath
= item
->GetProperty("ParentPath").asString();
416 if (parentPath
.empty())
418 std::string path
= GetVideoDbItemPath(*item
);
420 path
= item
->GetPath();
422 URIUtils::GetParentPath(path
, parentPath
);
424 if (parentPath
.empty())
426 CLog::LogF(LOGERROR
, "Unable to obtain parent path for '{}'", item
->GetPath());
431 const auto parentItem
= std::make_shared
<CFileItem
>(parentPath
, true);
432 parentItem
->SetProperty("IsVideoFolder", true);
433 parentItem
->LoadDetails();
434 if (item
->GetStartOffset() == STARTOFFSET_RESUME
)
435 parentItem
->SetStartOffset(STARTOFFSET_RESUME
);
437 AddItemToPlayListAndPlay(parentItem
, item
, player
);
439 else // mode == PlayMode::PLAY_ONLY_THIS
441 // single item, play it
442 auto& playlistPlayer
= CServiceBroker::GetPlaylistPlayer();
443 playlistPlayer
.Reset();
444 playlistPlayer
.SetCurrentPlaylist(PLAYLIST::Id::TYPE_NONE
);
445 playlistPlayer
.Play(item
, player
);
450 void QueueItem(const std::shared_ptr
<CFileItem
>& itemIn
, QueuePosition pos
)
454 // Allow queuing of unqueueable items
455 // when we try to queue them directly
456 if (!itemIn
->CanQueue())
458 // make a copy to not alter the original item
459 item
= std::make_shared
<CFileItem
>(*itemIn
);
460 item
->SetCanQueue(true);
463 auto& player
= CServiceBroker::GetPlaylistPlayer();
464 const auto& components
= CServiceBroker::GetAppComponents();
466 // Determine the proper list to queue this element
467 PLAYLIST::Id playlistId
= player
.GetCurrentPlaylist();
468 if (playlistId
== PLAYLIST::Id::TYPE_NONE
)
469 playlistId
= components
.GetComponent
<CApplicationPlayer
>()->GetPreferredPlaylist();
471 if (playlistId
== PLAYLIST::Id::TYPE_NONE
)
472 playlistId
= PLAYLIST::Id::TYPE_VIDEO
;
474 CFileItemList queuedItems
;
475 GetItemsForPlayList(item
, queuedItems
);
477 // if party mode, add items but DONT start playing
478 if (g_partyModeManager
.IsEnabled(PartyModeContext::VIDEO
))
480 g_partyModeManager
.AddUserSongs(queuedItems
, false);
484 if (pos
== QueuePosition::POSITION_BEGIN
&&
485 components
.GetComponent
<CApplicationPlayer
>()->IsPlaying())
486 player
.Insert(playlistId
, queuedItems
, player
.GetCurrentItemIdx() + 1);
488 player
.Add(playlistId
, queuedItems
);
490 player
.SetCurrentPlaylist(playlistId
);
492 // Note: video does not auto play on queue like music
495 bool GetItemsForPlayList(const std::shared_ptr
<CFileItem
>& item
, CFileItemList
& queuedItems
)
497 CAsyncGetItemsForPlaylist
getItems(item
, queuedItems
);
498 return CGUIDialogBusy::Wait(&getItems
,
499 500, // 500ms before busy dialog appears
500 true); // can be cancelled
505 bool IsNonExistingUserPartyModePlaylist(const CFileItem
& item
)
507 if (!PLAYLIST::IsSmartPlayList(item
))
510 const std::string
& path
{item
.GetPath()};
511 const auto profileManager
{CServiceBroker::GetSettingsComponent()->GetProfileManager()};
512 return ((profileManager
->GetUserDataItem("PartyMode-Video.xsp") == path
) &&
513 !CFileUtils::Exists(path
));
516 bool IsEmptyVideoItem(const CFileItem
& item
)
518 return item
.HasVideoInfoTag() && item
.GetVideoInfoTag()->IsEmpty();
520 } // unnamed namespace
522 bool IsItemPlayable(const CFileItem
& item
)
524 if (item
.IsParentFolder())
527 if (item
.IsDeleted())
530 // Include all PVR recordings and recordings folders
531 if (URIUtils::IsPVRRecordingFileOrFolder(item
.GetPath()))
535 if (!item
.m_bIsFolder
&& (item
.IsLiveTV() || item
.IsEPG()))
538 // Exclude all music library items
539 if (MUSIC::IsMusicDb(item
) || StringUtils::StartsWithNoCase(item
.GetPath(), "library://music/"))
543 if (item
.IsAddonsPath())
546 // Exclude special items
547 if (StringUtils::StartsWithNoCase(item
.GetPath(), "newsmartplaylist://") ||
548 StringUtils::StartsWithNoCase(item
.GetPath(), "newplaylist://") ||
549 StringUtils::StartsWithNoCase(item
.GetPath(), "newtag://"))
552 // Include playlists located at one of the possible video/mixed playlist locations
553 if (PLAYLIST::IsPlayList(item
))
555 if (StringUtils::StartsWithNoCase(item
.GetMimeType(), "video/"))
558 if (StringUtils::StartsWithNoCase(item
.GetPath(), "special://videoplaylists/") ||
559 StringUtils::StartsWithNoCase(item
.GetPath(), "special://profile/playlists/video/") ||
560 StringUtils::StartsWithNoCase(item
.GetPath(), "special://profile/playlists/mixed/"))
563 // Has user changed default playlists location and the list is located there?
564 const auto settings
= CServiceBroker::GetSettingsComponent()->GetSettings();
565 std::string path
= settings
->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH
);
566 StringUtils::TrimRight(path
, "/");
567 if (StringUtils::StartsWith(item
.GetPath(), StringUtils::Format("{}/video/", path
)) ||
568 StringUtils::StartsWith(item
.GetPath(), StringUtils::Format("{}/mixed/", path
)))
571 if (!item
.m_bIsFolder
&& !item
.HasVideoInfoTag())
573 // Unknown location. Type cannot be determined for non-folder items.
578 if (IsNonExistingUserPartyModePlaylist(item
))
581 if (item
.m_bIsFolder
&&
582 (IsVideoDb(item
) || StringUtils::StartsWithNoCase(item
.GetPath(), "library://video/")))
584 // Exclude top level nodes - eg can't play 'genres' just a specific genre etc
585 const auto node
= XFILE::CVideoDatabaseDirectory::GetDirectoryParentType(item
.GetPath());
586 if (node
== XFILE::VIDEODATABASEDIRECTORY::NodeType::OVERVIEW
||
587 node
== XFILE::VIDEODATABASEDIRECTORY::NodeType::MOVIES_OVERVIEW
||
588 node
== XFILE::VIDEODATABASEDIRECTORY::NodeType::TVSHOWS_OVERVIEW
||
589 node
== XFILE::VIDEODATABASEDIRECTORY::NodeType::MUSICVIDEOS_OVERVIEW
)
595 if (item
.IsPlugin() && IsVideo(item
) && !IsEmptyVideoItem(item
) &&
596 item
.GetProperty("isplayable").asBoolean(false))
600 else if (item
.HasVideoInfoTag() && item
.CanQueue() && !item
.IsPlugin() && !item
.IsScript())
604 else if ((!item
.m_bIsFolder
&& IsVideo(item
) && !IsEmptyVideoItem(item
)) || item
.IsDVD() ||
609 else if (item
.m_bIsFolder
&& !item
.IsPlugin() && !item
.IsScript())
611 // Not a video-specific folder (like file:// or nfs://). Allow play if context is Video window.
612 if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV
&&
613 item
.GetPath() != "add") // Exclude "Add video source" item
620 bool HasItemVideoDbInformation(const CFileItem
& item
)
625 CLog::LogF(LOGERROR
, "Cannot open VideoDatabase");
629 return db
.HasMovieInfo(item
.GetDynPath()) ||
630 db
.HasTvShowInfo(URIUtils::GetDirectory(item
.GetPath())) ||
631 db
.HasEpisodeInfo(item
.GetDynPath()) || db
.HasMusicVideoInfo(item
.GetDynPath());
634 std::string
GetResumeString(const CFileItem
& item
)
636 const ResumeInformation resumeInfo
= GetItemResumeInformation(item
);
637 if (resumeInfo
.isResumable
)
639 return GetResumeString(resumeInfo
.startOffset
, resumeInfo
.partNumber
);
647 std::string
GetResumeString(int64_t startOffset
, unsigned int partNumber
)
651 std::string resumeString
{
652 StringUtils::Format(g_localizeStrings
.Get(12022),
653 StringUtils::SecondsToTimeString(
654 static_cast<long>(CUtil::ConvertMilliSecsToSecsInt(startOffset
)),
655 TIME_FORMAT_HH_MM_SS
))};
658 const std::string partString
{StringUtils::Format(g_localizeStrings
.Get(23051), partNumber
)};
659 resumeString
+= " (" + partString
+ ")";
665 return g_localizeStrings
.Get(13362); // Continue watching
669 } // namespace KODI::VIDEO::UTILS