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 "guilib/GUIComponent.h"
24 #include "guilib/GUIWindowManager.h"
25 #include "guilib/LocalizeStrings.h"
26 #include "music/MusicFileItemClassify.h"
27 #include "network/NetworkFileItemClassify.h"
28 #include "playlists/PlayList.h"
29 #include "playlists/PlayListFactory.h"
30 #include "playlists/PlayListFileItemClassify.h"
31 #include "profiles/ProfileManager.h"
32 #include "settings/MediaSettings.h"
33 #include "settings/SettingUtils.h"
34 #include "settings/Settings.h"
35 #include "settings/SettingsComponent.h"
36 #include "threads/IRunnable.h"
37 #include "utils/FileUtils.h"
38 #include "utils/StringUtils.h"
39 #include "utils/URIUtils.h"
40 #include "utils/log.h"
41 #include "video/VideoDatabase.h"
42 #include "video/VideoFileItemClassify.h"
43 #include "video/VideoInfoTag.h"
44 #include "video/VideoUtils.h"
45 #include "view/GUIViewState.h"
52 class CAsyncGetItemsForPlaylist
: public IRunnable
55 CAsyncGetItemsForPlaylist(const std::shared_ptr
<CFileItem
>& item
, CFileItemList
& queuedItems
)
57 m_resume((item
->GetStartOffset() == STARTOFFSET_RESUME
) &&
58 VIDEO::UTILS::GetItemResumeInformation(*item
).isResumable
),
59 m_queuedItems(queuedItems
)
63 ~CAsyncGetItemsForPlaylist() override
= default;
67 // fast lookup is needed here
68 m_queuedItems
.SetFastLookup(true);
70 GetItemsForPlaylist(m_item
);
74 void GetItemsForPlaylist(const std::shared_ptr
<CFileItem
>& item
);
76 const std::shared_ptr
<CFileItem
> m_item
;
77 const bool m_resume
{false};
78 CFileItemList
& m_queuedItems
;
81 SortDescription
GetSortDescription(const CGUIViewState
& state
, const CFileItemList
& items
)
83 SortDescription sortDescDate
;
85 auto sortDescriptions
= state
.GetSortDescriptions();
86 for (auto& sortDescription
: sortDescriptions
)
88 if (sortDescription
.sortBy
== SortByEpisodeNumber
)
90 // check whether at least one item has actually an episode number set
91 for (const auto& item
: items
)
93 if (item
->HasVideoInfoTag() && item
->GetVideoInfoTag()->m_iEpisode
> 0)
95 // first choice for folders containig episodes
96 sortDescription
.sortOrder
= SortOrderAscending
;
97 return sortDescription
;
102 else if (sortDescription
.sortBy
== SortByYear
)
104 // check whether at least one item has actually a year set
105 for (const auto& item
: items
)
107 if (item
->HasVideoInfoTag() && item
->GetVideoInfoTag()->HasYear())
109 // first choice for folders containing movies
110 sortDescription
.sortOrder
= SortOrderAscending
;
111 return sortDescription
;
115 else if (sortDescription
.sortBy
== SortByDate
)
117 // check whether at least one item has actually a valid date set
118 for (const auto& item
: items
)
120 if (item
->m_dateTime
.IsValid())
122 // fallback, if neither ByEpisode nor ByYear is available
123 sortDescDate
= sortDescription
;
124 sortDescDate
.sortOrder
= SortOrderAscending
;
125 break; // leave items loop. we can still find ByEpisode or ByYear. so, no return here.
131 if (sortDescDate
.sortBy
!= SortByNone
)
134 return state
.GetSortMethod(); // last resort
137 void CAsyncGetItemsForPlaylist::GetItemsForPlaylist(const std::shared_ptr
<CFileItem
>& item
)
139 if (item
->IsParentFolder() || !item
->CanQueue() || item
->IsRAR() || item
->IsZIP())
142 if (item
->m_bIsFolder
)
144 if (!item
->IsPlugin())
146 // check if it's a folder with dvd or bluray files, then just add the relevant file
147 const std::string mediapath
= VIDEO::UTILS::GetOpticalMediaPath(*item
);
148 if (!mediapath
.empty())
150 m_queuedItems
.Add(std::make_shared
<CFileItem
>(mediapath
, false));
155 // Check if we add a locked share
156 if (!item
->IsPVR() && item
->m_bIsShareOrDrive
)
158 if (!g_passwordManager
.IsItemUnlocked(item
.get(), "video"))
163 XFILE::CDirectory::GetDirectory(item
->GetPath(), items
, "", XFILE::DIR_FLAG_DEFAULTS
);
165 int viewStateWindowId
= WINDOW_VIDEO_NAV
;
166 if (URIUtils::IsPVRRadioRecordingFileOrFolder(item
->GetPath()))
167 viewStateWindowId
= WINDOW_RADIO_RECORDINGS
;
168 else if (URIUtils::IsPVRTVRecordingFileOrFolder(item
->GetPath()))
169 viewStateWindowId
= WINDOW_TV_RECORDINGS
;
171 const std::unique_ptr
<CGUIViewState
> state(
172 CGUIViewState::GetViewState(viewStateWindowId
, items
));
175 LABEL_MASKS labelMasks
;
176 state
->GetSortMethodLabelMasks(labelMasks
);
178 const CLabelFormatter
fileFormatter(labelMasks
.m_strLabelFile
, labelMasks
.m_strLabel2File
);
179 const CLabelFormatter
folderFormatter(labelMasks
.m_strLabelFolder
,
180 labelMasks
.m_strLabel2Folder
);
181 for (const auto& i
: items
)
183 if (i
->IsLabelPreformatted())
187 folderFormatter
.FormatLabels(i
.get());
189 fileFormatter
.FormatLabels(i
.get());
192 SortDescription sortDesc
;
193 if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == viewStateWindowId
)
195 sortDesc
= state
->GetSortMethod();
197 // It makes no sense to play from younger to older.
198 if (sortDesc
.sortBy
== SortByDate
|| sortDesc
.sortBy
== SortByYear
||
199 sortDesc
.sortBy
== SortByEpisodeNumber
)
200 sortDesc
.sortOrder
= SortOrderAscending
;
203 sortDesc
= GetSortDescription(*state
, items
);
205 if (sortDesc
.sortBy
== SortByLabel
)
206 items
.ClearSortState();
208 items
.Sort(sortDesc
);
211 if (items
.GetContent().empty() && !VIDEO::IsVideoDb(items
) && !items
.IsVirtualDirectoryRoot() &&
212 !items
.IsSourcesPath() && !items
.IsLibraryFolder())
217 std::string content
= db
.GetContentForPath(items
.GetPath());
218 if (content
.empty() && !items
.IsPlugin())
221 items
.SetContent(content
);
227 // put last played item at the begin of the playlist; add start offsets for videos
228 std::shared_ptr
<CFileItem
> lastPlayedItem
;
229 CDateTime lastPlayed
;
230 for (const auto& i
: items
)
232 if (!i
->HasVideoInfoTag())
235 const auto videoTag
= i
->GetVideoInfoTag();
237 const CBookmark
& bookmark
= videoTag
->GetResumePoint();
238 if (bookmark
.IsSet())
240 i
->SetStartOffset(CUtil::ConvertSecsToMilliSecs(bookmark
.timeInSeconds
));
242 const CDateTime
& currLastPlayed
= videoTag
->m_lastPlayed
;
243 if (currLastPlayed
.IsValid() && (!lastPlayed
.IsValid() || (lastPlayed
< currLastPlayed
)))
246 lastPlayed
= currLastPlayed
;
253 items
.Remove(lastPlayedItem
.get());
254 items
.AddFront(lastPlayedItem
, 0);
260 watchedMode
= WatchedModeUnwatched
;
262 watchedMode
= CMediaSettings::GetInstance().GetWatchedMode(items
.GetContent());
264 const bool unwatchedOnly
= watchedMode
== WatchedModeUnwatched
;
265 const bool watchedOnly
= watchedMode
== WatchedModeWatched
;
266 bool fetchedPlayCounts
= false;
267 for (const auto& i
: items
)
271 std::string path
= i
->GetPath();
272 URIUtils::RemoveSlashAtEnd(path
);
273 if (StringUtils::EndsWithNoCase(path
, "sample")) // skip sample folders
278 if (!fetchedPlayCounts
&&
279 (!i
->HasVideoInfoTag() || !i
->GetVideoInfoTag()->IsPlayCountSet()))
284 fetchedPlayCounts
= true;
285 db
.GetPlayCounts(items
.GetPath(), items
);
288 if (i
->HasVideoInfoTag() && i
->GetVideoInfoTag()->IsPlayCountSet())
290 const int playCount
= i
->GetVideoInfoTag()->GetPlayCount();
291 if ((unwatchedOnly
&& playCount
> 0) || (watchedOnly
&& playCount
<= 0))
295 GetItemsForPlaylist(i
);
298 else if (PLAYLIST::IsPlayList(*item
))
300 // just queue the playlist, it will be expanded on play
301 m_queuedItems
.Add(item
);
303 else if (NETWORK::IsInternetStream(*item
))
305 // just queue the internet stream, it will be expanded on play
306 m_queuedItems
.Add(item
);
308 else if (item
->IsPlugin() && item
->GetProperty("isplayable").asBoolean())
310 // a playable python files
311 m_queuedItems
.Add(item
);
313 else if (VIDEO::IsVideoDb(*item
))
315 // this case is needed unless we allow IsVideo() to return true for videodb items,
316 // but then we have issues with playlists of videodb items
317 const auto itemCopy
= std::make_shared
<CFileItem
>(*item
->GetVideoInfoTag());
318 itemCopy
->SetStartOffset(item
->GetStartOffset());
319 m_queuedItems
.Add(itemCopy
);
321 else if (!item
->IsNFO() && VIDEO::IsVideo(*item
))
323 m_queuedItems
.Add(item
);
327 std::string
GetVideoDbItemPath(const CFileItem
& item
)
329 std::string path
= item
.GetPath();
330 if (!URIUtils::IsVideoDb(path
))
331 path
= item
.GetProperty("original_listitem_url").asString();
333 if (URIUtils::IsVideoDb(path
))
339 void AddItemToPlayListAndPlay(const std::shared_ptr
<CFileItem
>& itemToQueue
,
340 const std::shared_ptr
<CFileItem
>& itemToPlay
,
341 const std::string
& player
)
343 // recursively add items to list
344 CFileItemList queuedItems
;
345 VIDEO::UTILS::GetItemsForPlayList(itemToQueue
, queuedItems
);
347 auto& playlistPlayer
= CServiceBroker::GetPlaylistPlayer();
348 playlistPlayer
.ClearPlaylist(PLAYLIST::Id::TYPE_VIDEO
);
349 playlistPlayer
.Reset();
350 playlistPlayer
.Add(PLAYLIST::Id::TYPE_VIDEO
, queuedItems
);
352 // figure out where to start playback
353 PLAYLIST::CPlayList
& playList
= playlistPlayer
.GetPlaylist(PLAYLIST::Id::TYPE_VIDEO
);
357 for (const std::shared_ptr
<CFileItem
>& queuedItem
: queuedItems
)
359 if (queuedItem
->IsSamePath(itemToPlay
.get()))
366 if (playlistPlayer
.IsShuffled(PLAYLIST::Id::TYPE_VIDEO
))
368 playList
.Swap(0, playList
.FindOrder(pos
));
372 playlistPlayer
.SetCurrentPlaylist(PLAYLIST::Id::TYPE_VIDEO
);
373 playlistPlayer
.Play(pos
, player
);
376 } // unnamed namespace
380 namespace KODI::VIDEO::UTILS
383 const std::shared_ptr
<CFileItem
>& itemIn
,
384 const std::string
& player
,
385 ContentUtils::PlayMode mode
/* = ContentUtils::PlayMode::CHECK_AUTO_PLAY_NEXT_VIDEO */)
389 // Allow queuing of unqueueable items
390 // when we try to queue them directly
391 if (!itemIn
->CanQueue())
393 // make a copy to not alter the original item
394 item
= std::make_shared
<CFileItem
>(*itemIn
);
395 item
->SetCanQueue(true);
398 if (item
->m_bIsFolder
&& !item
->IsPlugin())
400 AddItemToPlayListAndPlay(item
, nullptr, player
);
402 else if (item
->HasVideoInfoTag())
404 if (mode
== ContentUtils::PlayMode::PLAY_FROM_HERE
||
405 (mode
== ContentUtils::PlayMode::CHECK_AUTO_PLAY_NEXT_ITEM
&& IsAutoPlayNextItem(*item
)))
407 // Add item and all its siblings to the playlist and play. Prefer videodb path if available,
408 // because it provides more information than just a plain file system path for example.
409 std::string parentPath
= item
->GetProperty("ParentPath").asString();
410 if (parentPath
.empty())
412 std::string path
= GetVideoDbItemPath(*item
);
414 path
= item
->GetPath();
416 URIUtils::GetParentPath(path
, parentPath
);
418 if (parentPath
.empty())
420 CLog::LogF(LOGERROR
, "Unable to obtain parent path for '{}'", item
->GetPath());
425 const auto parentItem
= std::make_shared
<CFileItem
>(parentPath
, true);
426 if (item
->GetStartOffset() == STARTOFFSET_RESUME
)
427 parentItem
->SetStartOffset(STARTOFFSET_RESUME
);
429 AddItemToPlayListAndPlay(parentItem
, item
, player
);
431 else // mode == PlayMode::PLAY_ONLY_THIS
433 // single item, play it
434 auto& playlistPlayer
= CServiceBroker::GetPlaylistPlayer();
435 playlistPlayer
.Reset();
436 playlistPlayer
.SetCurrentPlaylist(PLAYLIST::Id::TYPE_NONE
);
437 playlistPlayer
.Play(item
, player
);
442 void QueueItem(const std::shared_ptr
<CFileItem
>& itemIn
, QueuePosition pos
)
446 // Allow queuing of unqueueable items
447 // when we try to queue them directly
448 if (!itemIn
->CanQueue())
450 // make a copy to not alter the original item
451 item
= std::make_shared
<CFileItem
>(*itemIn
);
452 item
->SetCanQueue(true);
455 auto& player
= CServiceBroker::GetPlaylistPlayer();
456 const auto& components
= CServiceBroker::GetAppComponents();
458 // Determine the proper list to queue this element
459 PLAYLIST::Id playlistId
= player
.GetCurrentPlaylist();
460 if (playlistId
== PLAYLIST::Id::TYPE_NONE
)
461 playlistId
= components
.GetComponent
<CApplicationPlayer
>()->GetPreferredPlaylist();
463 if (playlistId
== PLAYLIST::Id::TYPE_NONE
)
464 playlistId
= PLAYLIST::Id::TYPE_VIDEO
;
466 CFileItemList queuedItems
;
467 GetItemsForPlayList(item
, queuedItems
);
469 // if party mode, add items but DONT start playing
470 if (g_partyModeManager
.IsEnabled(PARTYMODECONTEXT_VIDEO
))
472 g_partyModeManager
.AddUserSongs(queuedItems
, false);
476 if (pos
== QueuePosition::POSITION_BEGIN
&&
477 components
.GetComponent
<CApplicationPlayer
>()->IsPlaying())
478 player
.Insert(playlistId
, queuedItems
, player
.GetCurrentItemIdx() + 1);
480 player
.Add(playlistId
, queuedItems
);
482 player
.SetCurrentPlaylist(playlistId
);
484 // Note: video does not auto play on queue like music
487 bool GetItemsForPlayList(const std::shared_ptr
<CFileItem
>& item
, CFileItemList
& queuedItems
)
489 CAsyncGetItemsForPlaylist
getItems(item
, queuedItems
);
490 return CGUIDialogBusy::Wait(&getItems
,
491 500, // 500ms before busy dialog appears
492 true); // can be cancelled
497 bool IsNonExistingUserPartyModePlaylist(const CFileItem
& item
)
499 if (!PLAYLIST::IsSmartPlayList(item
))
502 const std::string
& path
{item
.GetPath()};
503 const auto profileManager
{CServiceBroker::GetSettingsComponent()->GetProfileManager()};
504 return ((profileManager
->GetUserDataItem("PartyMode-Video.xsp") == path
) &&
505 !CFileUtils::Exists(path
));
507 } // unnamed namespace
509 bool IsItemPlayable(const CFileItem
& item
)
511 if (item
.IsParentFolder())
514 if (item
.IsDeleted())
517 // Include all PVR recordings and recordings folders
518 if (URIUtils::IsPVRRecordingFileOrFolder(item
.GetPath()))
522 if (!item
.m_bIsFolder
&& (item
.IsLiveTV() || item
.IsEPG()))
525 // Exclude all music library items
526 if (MUSIC::IsMusicDb(item
) || StringUtils::StartsWithNoCase(item
.GetPath(), "library://music/"))
529 // Exclude other components
530 if (item
.IsPlugin() || item
.IsScript() || item
.IsAddonsPath())
533 // Exclude special items
534 if (StringUtils::StartsWithNoCase(item
.GetPath(), "newsmartplaylist://") ||
535 StringUtils::StartsWithNoCase(item
.GetPath(), "newplaylist://") ||
536 StringUtils::StartsWithNoCase(item
.GetPath(), "newtag://"))
539 // Include playlists located at one of the possible video/mixed playlist locations
540 if (PLAYLIST::IsPlayList(item
))
542 if (StringUtils::StartsWithNoCase(item
.GetMimeType(), "video/"))
545 if (StringUtils::StartsWithNoCase(item
.GetPath(), "special://videoplaylists/") ||
546 StringUtils::StartsWithNoCase(item
.GetPath(), "special://profile/playlists/video/") ||
547 StringUtils::StartsWithNoCase(item
.GetPath(), "special://profile/playlists/mixed/"))
550 // Has user changed default playlists location and the list is located there?
551 const auto settings
= CServiceBroker::GetSettingsComponent()->GetSettings();
552 std::string path
= settings
->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH
);
553 StringUtils::TrimRight(path
, "/");
554 if (StringUtils::StartsWith(item
.GetPath(), StringUtils::Format("{}/video/", path
)) ||
555 StringUtils::StartsWith(item
.GetPath(), StringUtils::Format("{}/mixed/", path
)))
558 if (!item
.m_bIsFolder
&& !item
.HasVideoInfoTag())
560 // Unknown location. Type cannot be determined for non-folder items.
565 if (IsNonExistingUserPartyModePlaylist(item
))
568 if (item
.m_bIsFolder
&&
569 (IsVideoDb(item
) || StringUtils::StartsWithNoCase(item
.GetPath(), "library://video/")))
571 // Exclude top level nodes - eg can't play 'genres' just a specific genre etc
572 const XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE node
=
573 XFILE::CVideoDatabaseDirectory::GetDirectoryParentType(item
.GetPath());
574 if (node
== XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_OVERVIEW
||
575 node
== XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_MOVIES_OVERVIEW
||
576 node
== XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_TVSHOWS_OVERVIEW
||
577 node
== XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_OVERVIEW
)
583 if (item
.HasVideoInfoTag() && item
.CanQueue())
587 else if ((!item
.m_bIsFolder
&& IsVideo(item
)) || item
.IsDVD() || MUSIC::IsCDDA(item
))
591 else if (item
.m_bIsFolder
)
593 // Not a video-specific folder (like file:// or nfs://). Allow play if context is Video window.
594 if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV
&&
595 item
.GetPath() != "add") // Exclude "Add video source" item
602 bool HasItemVideoDbInformation(const CFileItem
& item
)
607 CLog::LogF(LOGERROR
, "Cannot open VideoDatabase");
611 return db
.HasMovieInfo(item
.GetDynPath()) ||
612 db
.HasTvShowInfo(URIUtils::GetDirectory(item
.GetPath())) ||
613 db
.HasEpisodeInfo(item
.GetDynPath()) || db
.HasMusicVideoInfo(item
.GetDynPath());
616 std::string
GetResumeString(const CFileItem
& item
)
618 const ResumeInformation resumeInfo
= GetItemResumeInformation(item
);
619 if (resumeInfo
.isResumable
)
621 if (resumeInfo
.startOffset
> 0)
623 std::string resumeString
= StringUtils::Format(
624 g_localizeStrings
.Get(12022),
625 StringUtils::SecondsToTimeString(
626 static_cast<long>(CUtil::ConvertMilliSecsToSecsInt(resumeInfo
.startOffset
)),
627 TIME_FORMAT_HH_MM_SS
));
628 if (resumeInfo
.partNumber
> 0)
630 const std::string partString
=
631 StringUtils::Format(g_localizeStrings
.Get(23051), resumeInfo
.partNumber
);
632 resumeString
+= " (" + partString
+ ")";
638 return g_localizeStrings
.Get(13362); // Continue watching
644 } // namespace KODI::VIDEO::UTILS