diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
commit | c04dcc2e7d834218ef2d4194331e383402495ae1 (patch) | |
tree | 7333e38d10d75386e60f336b80c2443c1166031d /xbmc/video/VideoUtils.cpp | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/video/VideoUtils.cpp')
-rw-r--r-- | xbmc/video/VideoUtils.cpp | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/xbmc/video/VideoUtils.cpp b/xbmc/video/VideoUtils.cpp new file mode 100644 index 0000000..44609aa --- /dev/null +++ b/xbmc/video/VideoUtils.cpp @@ -0,0 +1,702 @@ +/* + * Copyright (C) 2022 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 "VideoUtils.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "PartyModeManager.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogBusy.h" +#include "filesystem/Directory.h" +#include "filesystem/VideoDatabaseDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/IRunnable.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" +#include "video/VideoInfoTag.h" +#include "view/GUIViewState.h" + +namespace +{ +class CAsyncGetItemsForPlaylist : public IRunnable +{ +public: + CAsyncGetItemsForPlaylist(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems) + : m_item(item), + m_resume(item->GetStartOffset() == STARTOFFSET_RESUME), + m_queuedItems(queuedItems) + { + } + + ~CAsyncGetItemsForPlaylist() override = default; + + void Run() override + { + // fast lookup is needed here + m_queuedItems.SetFastLookup(true); + + GetItemsForPlaylist(m_item); + } + +private: + void GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item); + + const std::shared_ptr<CFileItem> m_item; + const bool m_resume{false}; + CFileItemList& m_queuedItems; +}; + +SortDescription GetSortDescription(const CGUIViewState& state, const CFileItemList& items) +{ + SortDescription sortDescDate; + + auto sortDescriptions = state.GetSortDescriptions(); + for (auto& sortDescription : sortDescriptions) + { + if (sortDescription.sortBy == SortByEpisodeNumber) + { + // check whether at least one item has actually an episode number set + for (const auto& item : items) + { + if (item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_iEpisode > 0) + { + // first choice for folders containig episodes + sortDescription.sortOrder = SortOrderAscending; + return sortDescription; + } + } + continue; + } + else if (sortDescription.sortBy == SortByYear) + { + // check whether at least one item has actually a year set + for (const auto& item : items) + { + if (item->HasVideoInfoTag() && item->GetVideoInfoTag()->HasYear()) + { + // first choice for folders containing movies + sortDescription.sortOrder = SortOrderAscending; + return sortDescription; + } + } + } + else if (sortDescription.sortBy == SortByDate) + { + // check whether at least one item has actually a valid date set + for (const auto& item : items) + { + if (item->m_dateTime.IsValid()) + { + // fallback, if neither ByEpisode nor ByYear is available + sortDescDate = sortDescription; + sortDescDate.sortOrder = SortOrderAscending; + break; // leave items loop. we can still find ByEpisode or ByYear. so, no return here. + } + } + } + } + + if (sortDescDate.sortBy != SortByNone) + return sortDescDate; + else + return state.GetSortMethod(); // last resort +} + +void CAsyncGetItemsForPlaylist::GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item) +{ + if (item->IsParentFolder() || !item->CanQueue() || item->IsRAR() || item->IsZIP()) + return; + + if (item->m_bIsFolder) + { + // check if it's a folder with dvd or bluray files, then just add the relevant file + const std::string mediapath = item->GetOpticalMediaPath(); + if (!mediapath.empty()) + { + m_queuedItems.Add(std::make_shared<CFileItem>(mediapath, false)); + return; + } + + // Check if we add a locked share + if (!item->IsPVR() && item->m_bIsShareOrDrive) + { + if (!g_passwordManager.IsItemUnlocked(item.get(), "video")) + return; + } + + CFileItemList items; + XFILE::CDirectory::GetDirectory(item->GetPath(), items, "", XFILE::DIR_FLAG_DEFAULTS); + + int viewStateWindowId = WINDOW_VIDEO_NAV; + if (URIUtils::IsPVRRadioRecordingFileOrFolder(item->GetPath())) + viewStateWindowId = WINDOW_RADIO_RECORDINGS; + else if (URIUtils::IsPVRTVRecordingFileOrFolder(item->GetPath())) + viewStateWindowId = WINDOW_TV_RECORDINGS; + + const std::unique_ptr<CGUIViewState> state( + CGUIViewState::GetViewState(viewStateWindowId, items)); + if (state) + { + LABEL_MASKS labelMasks; + state->GetSortMethodLabelMasks(labelMasks); + + const CLabelFormatter fileFormatter(labelMasks.m_strLabelFile, labelMasks.m_strLabel2File); + const CLabelFormatter folderFormatter(labelMasks.m_strLabelFolder, + labelMasks.m_strLabel2Folder); + for (const auto& i : items) + { + if (i->IsLabelPreformatted()) + continue; + + if (i->m_bIsFolder) + folderFormatter.FormatLabels(i.get()); + else + fileFormatter.FormatLabels(i.get()); + } + + SortDescription sortDesc; + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == viewStateWindowId) + sortDesc = state->GetSortMethod(); + else + sortDesc = GetSortDescription(*state, items); + + if (sortDesc.sortBy == SortByLabel) + items.ClearSortState(); + + items.Sort(sortDesc); + } + + if (items.GetContent().empty() && !items.IsVideoDb() && !items.IsVirtualDirectoryRoot() && + !items.IsSourcesPath() && !items.IsLibraryFolder()) + { + CVideoDatabase db; + if (db.Open()) + { + std::string content = db.GetContentForPath(items.GetPath()); + if (content.empty() && !items.IsPlugin()) + content = "files"; + + items.SetContent(content); + } + } + + if (m_resume) + { + // put last played item at the begin of the playlist; add start offsets for videos + std::shared_ptr<CFileItem> lastPlayedItem; + CDateTime lastPlayed; + for (const auto& i : items) + { + if (!i->HasVideoInfoTag()) + continue; + + const auto videoTag = i->GetVideoInfoTag(); + + const CBookmark& bookmark = videoTag->GetResumePoint(); + if (bookmark.IsSet()) + { + i->SetStartOffset(CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds)); + + const CDateTime& currLastPlayed = videoTag->m_lastPlayed; + if (currLastPlayed.IsValid() && (!lastPlayed.IsValid() || (lastPlayed < currLastPlayed))) + { + lastPlayedItem = i; + lastPlayed = currLastPlayed; + } + } + } + + if (lastPlayedItem) + { + items.Remove(lastPlayedItem.get()); + items.AddFront(lastPlayedItem, 0); + } + } + + int watchedMode; + if (m_resume) + watchedMode = WatchedModeUnwatched; + else + watchedMode = CMediaSettings::GetInstance().GetWatchedMode(items.GetContent()); + + const bool unwatchedOnly = watchedMode == WatchedModeUnwatched; + const bool watchedOnly = watchedMode == WatchedModeWatched; + bool fetchedPlayCounts = false; + for (const auto& i : items) + { + if (i->m_bIsFolder) + { + std::string path = i->GetPath(); + URIUtils::RemoveSlashAtEnd(path); + if (StringUtils::EndsWithNoCase(path, "sample")) // skip sample folders + continue; + } + else + { + if (!fetchedPlayCounts && + (!i->HasVideoInfoTag() || !i->GetVideoInfoTag()->IsPlayCountSet())) + { + CVideoDatabase db; + if (db.Open()) + { + fetchedPlayCounts = true; + db.GetPlayCounts(items.GetPath(), items); + } + } + if (i->HasVideoInfoTag() && i->GetVideoInfoTag()->IsPlayCountSet()) + { + const int playCount = i->GetVideoInfoTag()->GetPlayCount(); + if ((unwatchedOnly && playCount > 0) || (watchedOnly && playCount <= 0)) + continue; + } + } + GetItemsForPlaylist(i); + } + } + else if (item->IsPlayList()) + { + const std::unique_ptr<PLAYLIST::CPlayList> playList(PLAYLIST::CPlayListFactory::Create(*item)); + if (!playList) + { + CLog::LogF(LOGERROR, "Failed to create playlist {}", item->GetPath()); + return; + } + + if (!playList->Load(item->GetPath())) + { + CLog::LogF(LOGERROR, "Failed to load playlist {}", item->GetPath()); + return; + } + + for (int i = 0; i < playList->size(); ++i) + { + GetItemsForPlaylist((*playList)[i]); + } + } + else if (item->IsInternetStream()) + { + // just queue the internet stream, it will be expanded on play + m_queuedItems.Add(item); + } + else if (item->IsPlugin() && item->GetProperty("isplayable").asBoolean()) + { + // a playable python files + m_queuedItems.Add(item); + } + else if (item->IsVideoDb()) + { + // this case is needed unless we allow IsVideo() to return true for videodb items, + // but then we have issues with playlists of videodb items + const auto itemCopy = std::make_shared<CFileItem>(*item->GetVideoInfoTag()); + itemCopy->SetStartOffset(item->GetStartOffset()); + m_queuedItems.Add(itemCopy); + } + else if (!item->IsNFO() && item->IsVideo()) + { + m_queuedItems.Add(item); + } +} + +} // unnamed namespace + +namespace VIDEO_UTILS +{ +void PlayItem(const std::shared_ptr<CFileItem>& itemIn) +{ + auto item = itemIn; + + // Allow queuing of unqueueable items + // when we try to queue them directly + if (!itemIn->CanQueue()) + { + // make a copy to not alter the original item + item = std::make_shared<CFileItem>(*itemIn); + item->SetCanQueue(true); + } + + if (item->m_bIsFolder && !item->IsPlugin()) + { + // recursively add items to list + CFileItemList queuedItems; + GetItemsForPlayList(item, queuedItems); + + auto& player = CServiceBroker::GetPlaylistPlayer(); + player.ClearPlaylist(PLAYLIST::TYPE_VIDEO); + player.Reset(); + player.Add(PLAYLIST::TYPE_VIDEO, queuedItems); + player.SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO); + player.Play(); + } + else if (item->HasVideoInfoTag()) + { + // single item, play it + CServiceBroker::GetPlaylistPlayer().Play(item, ""); + } +} + +void QueueItem(const std::shared_ptr<CFileItem>& itemIn, QueuePosition pos) +{ + auto item = itemIn; + + // Allow queuing of unqueueable items + // when we try to queue them directly + if (!itemIn->CanQueue()) + { + // make a copy to not alter the original item + item = std::make_shared<CFileItem>(*itemIn); + item->SetCanQueue(true); + } + + auto& player = CServiceBroker::GetPlaylistPlayer(); + const auto& components = CServiceBroker::GetAppComponents(); + + // Determine the proper list to queue this element + PLAYLIST::Id playlistId = player.GetCurrentPlaylist(); + if (playlistId == PLAYLIST::TYPE_NONE) + playlistId = components.GetComponent<CApplicationPlayer>()->GetPreferredPlaylist(); + + if (playlistId == PLAYLIST::TYPE_NONE) + playlistId = PLAYLIST::TYPE_VIDEO; + + CFileItemList queuedItems; + GetItemsForPlayList(item, queuedItems); + + // if party mode, add items but DONT start playing + if (g_partyModeManager.IsEnabled(PARTYMODECONTEXT_VIDEO)) + { + g_partyModeManager.AddUserSongs(queuedItems, false); + return; + } + + if (pos == QueuePosition::POSITION_BEGIN && + components.GetComponent<CApplicationPlayer>()->IsPlaying()) + player.Insert(playlistId, queuedItems, player.GetCurrentSong() + 1); + else + player.Add(playlistId, queuedItems); + + player.SetCurrentPlaylist(playlistId); + + // Note: video does not auto play on queue like music +} + +bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems) +{ + CAsyncGetItemsForPlaylist getItems(item, queuedItems); + return CGUIDialogBusy::Wait(&getItems, + 500, // 500ms before busy dialog appears + true); // can be cancelled +} + +bool IsItemPlayable(const CFileItem& item) +{ + if (item.IsParentFolder()) + return false; + + if (item.IsDeleted()) + return false; + + // Include all PVR recordings and recordings folders + if (URIUtils::IsPVRRecordingFileOrFolder(item.GetPath())) + return true; + + // Include Live TV + if (!item.m_bIsFolder && (item.IsLiveTV() || item.IsEPG())) + return true; + + // Exclude all music library items + if (item.IsMusicDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://music/")) + return false; + + // Exclude other components + if (item.IsPlugin() || item.IsScript() || item.IsAddonsPath()) + return false; + + // Exclude unwanted windows + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST) + return false; + + // Exclude special items + if (StringUtils::StartsWithNoCase(item.GetPath(), "newsmartplaylist://") || + StringUtils::StartsWithNoCase(item.GetPath(), "newplaylist://") || + StringUtils::StartsWithNoCase(item.GetPath(), "newtag://")) + return false; + + // Include playlists located at one of the possible video/mixed playlist locations + if (item.IsPlayList()) + { + if (StringUtils::StartsWithNoCase(item.GetMimeType(), "video/")) + return true; + + if (StringUtils::StartsWithNoCase(item.GetPath(), "special://videoplaylists/") || + StringUtils::StartsWithNoCase(item.GetPath(), "special://profile/playlists/video/") || + StringUtils::StartsWithNoCase(item.GetPath(), "special://profile/playlists/mixed/")) + return true; + + // Has user changed default playlists location and the list is located there? + const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + std::string path = settings->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH); + StringUtils::TrimRight(path, "/"); + if (StringUtils::StartsWith(item.GetPath(), StringUtils::Format("{}/video/", path)) || + StringUtils::StartsWith(item.GetPath(), StringUtils::Format("{}/mixed/", path))) + return true; + + if (!item.m_bIsFolder) + { + // Unknown location. Type cannot be determined for non-folder items. + return false; + } + } + + if (item.m_bIsFolder && + (item.IsVideoDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://video/"))) + { + // Exclude top level nodes - eg can't play 'genres' just a specific genre etc + const XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE node = + XFILE::CVideoDatabaseDirectory::GetDirectoryParentType(item.GetPath()); + if (node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_OVERVIEW || + node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_MOVIES_OVERVIEW || + node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_TVSHOWS_OVERVIEW || + node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_OVERVIEW) + return false; + + return true; + } + + if (item.HasVideoInfoTag() && item.CanQueue()) + { + return true; + } + else if ((!item.m_bIsFolder && item.IsVideo()) || item.IsDVD() || item.IsCDDA()) + { + return true; + } + else if (item.m_bIsFolder) + { + // Not a video-specific folder (like file:// or nfs://). Allow play if context is Video window. + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV && + item.GetPath() != "add") // Exclude "Add video source" item + return true; + } + + return false; +} + +namespace +{ +bool HasInProgressVideo(const std::string& path, CVideoDatabase& db) +{ + //! @todo this function is really very expensive and should be optimized (at db level). + + CFileItemList items; + CUtil::GetRecursiveListing(path, items, {}, XFILE::DIR_FLAG_DEFAULTS); + + if (items.IsEmpty()) + return false; + + for (const auto& item : items) + { + const auto videoTag = item->GetVideoInfoTag(); + if (!item->HasVideoInfoTag()) + continue; + + if (videoTag->GetPlayCount() > 0) + continue; + + // get resume point + CBookmark bookmark(videoTag->GetResumePoint()); + if (!bookmark.IsSet() && db.GetResumeBookMark(videoTag->m_strFileNameAndPath, bookmark)) + videoTag->SetResumePoint(bookmark); + + if (bookmark.IsSet()) + return true; + } + + return false; +} + +ResumeInformation GetFolderItemResumeInformation(const CFileItem& item) +{ + if (!item.m_bIsFolder) + return {}; + + bool hasInProgressVideo = false; + + CFileItem folderItem(item); + if ((!folderItem.HasProperty("watchedepisodes") || // season/show + (folderItem.GetProperty("watchedepisodes").asInteger() == 0)) && + (!folderItem.HasProperty("watched") || // movie set + (folderItem.GetProperty("watched").asInteger() == 0))) + { + CVideoDatabase db; + if (db.Open()) + { + if (!folderItem.HasProperty("watchedepisodes") && !folderItem.HasProperty("watched")) + { + XFILE::VIDEODATABASEDIRECTORY::CQueryParams params; + XFILE::VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(item.GetPath(), params); + + if (params.GetTvShowId() >= 0) + { + if (params.GetSeason() >= 0) + { + const int idSeason = db.GetSeasonId(static_cast<int>(params.GetTvShowId()), + static_cast<int>(params.GetSeason())); + if (idSeason >= 0) + { + CVideoInfoTag details; + db.GetSeasonInfo(idSeason, details, &folderItem); + } + } + else + { + CVideoInfoTag details; + db.GetTvShowInfo(item.GetPath(), details, static_cast<int>(params.GetTvShowId()), + &folderItem); + } + } + else if (params.GetSetId() >= 0) + { + CVideoInfoTag details; + db.GetSetInfo(static_cast<int>(params.GetSetId()), details, &folderItem); + } + } + + // no episodes/movies watched completely, but there could be some or more we have + // started watching + if ((folderItem.HasProperty("watchedepisodes") && // season/show + folderItem.GetProperty("watchedepisodes").asInteger() == 0) || + (folderItem.HasProperty("watched") && // movie set + folderItem.GetProperty("watched").asInteger() == 0)) + hasInProgressVideo = HasInProgressVideo(item.GetPath(), db); + + db.Close(); + } + } + + if (hasInProgressVideo || + (folderItem.GetProperty("watchedepisodes").asInteger() > 0 && + folderItem.GetProperty("unwatchedepisodes").asInteger() > 0) || + (folderItem.GetProperty("watched").asInteger() > 0 && + folderItem.GetProperty("unwatched").asInteger() > 0)) + { + ResumeInformation resumeInfo; + resumeInfo.isResumable = true; + return resumeInfo; + } + return {}; +} + +ResumeInformation GetNonFolderItemResumeInformation(const CFileItem& item) +{ + if (!item.IsResumable()) + return {}; + + // do not resume Live TV and 'deleted' items (e.g. trashed pvr recordings) + if (item.IsLiveTV() || item.IsDeleted()) + return {}; + + ResumeInformation resumeInfo; + + if (item.GetCurrentResumeTimeAndPartNumber(resumeInfo.startOffset, resumeInfo.partNumber)) + { + if (resumeInfo.startOffset > 0) + { + resumeInfo.startOffset = CUtil::ConvertSecsToMilliSecs(resumeInfo.startOffset); + resumeInfo.isResumable = true; + } + } + else + { + // Obtain the resume bookmark from video db... + + CVideoDatabase db; + if (!db.Open()) + { + CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); + return {}; + } + + std::string path = item.GetPath(); + if (item.IsVideoDb() || item.IsDVD()) + { + if (item.HasVideoInfoTag()) + { + path = item.GetVideoInfoTag()->m_strFileNameAndPath; + } + else if (item.IsVideoDb()) + { + // Obtain path+filename from video db + XFILE::VIDEODATABASEDIRECTORY::CQueryParams params; + XFILE::VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(item.GetPath(), params); + + long id = -1; + VideoDbContentType content_type; + if ((id = params.GetMovieId()) >= 0) + content_type = VideoDbContentType::MOVIES; + else if ((id = params.GetEpisodeId()) >= 0) + content_type = VideoDbContentType::EPISODES; + else if ((id = params.GetMVideoId()) >= 0) + content_type = VideoDbContentType::MUSICVIDEOS; + else + { + CLog::LogF(LOGERROR, "Cannot obtain video content type"); + db.Close(); + return {}; + } + + db.GetFilePathById(static_cast<int>(id), path, content_type); + } + else + { + // DVD + CLog::LogF(LOGERROR, "Cannot obtain bookmark for DVD"); + db.Close(); + return {}; + } + } + + CBookmark bookmark; + db.GetResumeBookMark(path, bookmark); + db.Close(); + + if (bookmark.IsSet()) + { + resumeInfo.isResumable = bookmark.IsPartWay(); + resumeInfo.startOffset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds); + resumeInfo.partNumber = static_cast<int>(bookmark.partNumber); + } + } + return resumeInfo; +} + +} // unnamed namespace + +ResumeInformation GetItemResumeInformation(const CFileItem& item) +{ + ResumeInformation info = GetNonFolderItemResumeInformation(item); + if (info.isResumable) + return info; + + return GetFolderItemResumeInformation(item); +} + +} // namespace VIDEO_UTILS |