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 | |
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')
99 files changed, 39571 insertions, 0 deletions
diff --git a/xbmc/video/Bookmark.cpp b/xbmc/video/Bookmark.cpp new file mode 100644 index 0000000..d18c00e --- /dev/null +++ b/xbmc/video/Bookmark.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2018 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 "Bookmark.h" + +CBookmark::CBookmark() +{ + Reset(); +} + +void CBookmark::Reset() +{ + episodeNumber = 0; + seasonNumber = 0; + timeInSeconds = 0.0; + totalTimeInSeconds = 0.0; + partNumber = 0; + type = STANDARD; +} + +bool CBookmark::IsSet() const +{ + return totalTimeInSeconds > 0.0; +} + +bool CBookmark::IsPartWay() const +{ + return totalTimeInSeconds > 0.0 && timeInSeconds > 0.0; +} + +bool CBookmark::HasSavedPlayerState() const +{ + return !playerState.empty(); +} diff --git a/xbmc/video/Bookmark.h b/xbmc/video/Bookmark.h new file mode 100644 index 0000000..6c118b1 --- /dev/null +++ b/xbmc/video/Bookmark.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include <string> +#include <vector> + +class CBookmark +{ +public: + CBookmark(); + void Reset(); + + /*! \brief returns true if this bookmark has been set. + \return true if totalTimeInSeconds is positive. + */ + bool IsSet() const; + + /*! \brief returns true if this bookmark is part way through the video file + \return true if both totalTimeInSeconds and timeInSeconds are positive. + */ + bool IsPartWay() const; + + /*! \brief returns true if this bookmark has a stored serialized player state + \return true if playerState is not empty. + */ + bool HasSavedPlayerState() const; + + double timeInSeconds; + double totalTimeInSeconds; + long partNumber; + std::string thumbNailImage; + std::string playerState; + std::string player; + long seasonNumber; + long episodeNumber; + + enum EType + { + STANDARD = 0, + RESUME = 1, + EPISODE = 2 + } type; +}; + +typedef std::vector<CBookmark> VECBOOKMARKS; + diff --git a/xbmc/video/CMakeLists.txt b/xbmc/video/CMakeLists.txt new file mode 100644 index 0000000..00f9baf --- /dev/null +++ b/xbmc/video/CMakeLists.txt @@ -0,0 +1,33 @@ +set(SOURCES Bookmark.cpp + ContextMenus.cpp + GUIViewStateVideo.cpp + PlayerController.cpp + Teletext.cpp + VideoDatabase.cpp + VideoDbUrl.cpp + VideoInfoDownloader.cpp + VideoInfoScanner.cpp + VideoInfoTag.cpp + VideoLibraryQueue.cpp + VideoThumbLoader.cpp + VideoUtils.cpp + ViewModeSettings.cpp) + +set(HEADERS Bookmark.h + ContextMenus.h + Episode.h + GUIViewStateVideo.h + PlayerController.h + Teletext.h + TeletextDefines.h + VideoDatabase.h + VideoDbUrl.h + VideoInfoDownloader.h + VideoInfoScanner.h + VideoInfoTag.h + VideoLibraryQueue.h + VideoThumbLoader.h + VideoUtils.h + ViewModeSettings.h) + +core_add_library(video) diff --git a/xbmc/video/ContextMenus.cpp b/xbmc/video/ContextMenus.cpp new file mode 100644 index 0000000..c1f4c43 --- /dev/null +++ b/xbmc/video/ContextMenus.cpp @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2016-2018 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 "ContextMenus.h" + +#include "Autorun.h" +#include "GUIUserMessages.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "filesystem/Directory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "playlists/PlayList.h" +#include "settings/MediaSettings.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "video/VideoUtils.h" +#include "video/dialogs/GUIDialogVideoInfo.h" +#include "video/windows/GUIWindowVideoBase.h" +#include "view/GUIViewState.h" + +#include <utility> + +namespace CONTEXTMENU +{ + +CVideoInfo::CVideoInfo(MediaType mediaType) + : CStaticContextMenuAction(19033), m_mediaType(std::move(mediaType)) +{ +} + +bool CVideoInfo::IsVisible(const CFileItem& item) const +{ + if (!item.HasVideoInfoTag()) + return false; + + if (item.IsPVRRecording()) + return false; // pvr recordings have its own implementation for this + + return item.GetVideoInfoTag()->m_type == m_mediaType; +} + +bool CVideoInfo::Execute(const std::shared_ptr<CFileItem>& item) const +{ + CGUIDialogVideoInfo::ShowFor(*item); + return true; +} + +bool CVideoRemoveResumePoint::IsVisible(const CFileItem& itemIn) const +{ + CFileItem item(itemIn.GetItemToPlay()); + if (item.IsDeleted()) // e.g. trashed pvr recording + return false; + + // Folders don't have a resume point + return !item.m_bIsFolder && VIDEO_UTILS::GetItemResumeInformation(item).isResumable; +} + +bool CVideoRemoveResumePoint::Execute(const std::shared_ptr<CFileItem>& item) const +{ + CVideoLibraryQueue::GetInstance().ResetResumePoint(item); + return true; +} + +bool CVideoMarkWatched::IsVisible(const CFileItem& item) const +{ + if (item.IsDeleted()) // e.g. trashed pvr recording + return false; + + if (item.m_bIsFolder && item.IsPlugin()) // we cannot manage plugin folder's watched state + return false; + + if (item.m_bIsFolder) // Only allow video db content, video and recording folders to be updated recursively + { + if (item.HasVideoInfoTag()) + return item.IsVideoDb(); + else if (item.GetProperty("IsVideoFolder").asBoolean()) + return true; + else + return !item.IsParentFolder() && URIUtils::IsPVRRecordingFileOrFolder(item.GetPath()); + } + else if (!item.HasVideoInfoTag()) + return false; + + return item.GetVideoInfoTag()->GetPlayCount() == 0; +} + +bool CVideoMarkWatched::Execute(const std::shared_ptr<CFileItem>& item) const +{ + CVideoLibraryQueue::GetInstance().MarkAsWatched(item, true); + return true; +} + +bool CVideoMarkUnWatched::IsVisible(const CFileItem& item) const +{ + if (item.IsDeleted()) // e.g. trashed pvr recording + return false; + + if (item.m_bIsFolder && item.IsPlugin()) // we cannot manage plugin folder's watched state + return false; + + if (item.m_bIsFolder) // Only allow video db content, video and recording folders to be updated recursively + { + if (item.HasVideoInfoTag()) + return item.IsVideoDb(); + else if (item.GetProperty("IsVideoFolder").asBoolean()) + return true; + else + return !item.IsParentFolder() && URIUtils::IsPVRRecordingFileOrFolder(item.GetPath()); + } + else if (!item.HasVideoInfoTag()) + return false; + + return item.GetVideoInfoTag()->GetPlayCount() > 0; +} + +bool CVideoMarkUnWatched::Execute(const std::shared_ptr<CFileItem>& item) const +{ + CVideoLibraryQueue::GetInstance().MarkAsWatched(item, false); + return true; +} + +bool CVideoBrowse::IsVisible(const CFileItem& item) const +{ + if (item.IsFileFolder(EFILEFOLDER_MASK_ONBROWSE)) + return false; // handled by CMediaWindow + + return item.m_bIsFolder && VIDEO_UTILS::IsItemPlayable(item); +} + +bool CVideoBrowse::Execute(const std::shared_ptr<CFileItem>& item) const +{ + int target = WINDOW_INVALID; + if (URIUtils::IsPVRRadioRecordingFileOrFolder(item->GetPath())) + target = WINDOW_RADIO_RECORDINGS; + else if (URIUtils::IsPVRTVRecordingFileOrFolder(item->GetPath())) + target = WINDOW_TV_RECORDINGS; + else + target = WINDOW_VIDEO_NAV; + + auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager(); + + if (target == windowMgr.GetActiveWindow()) + { + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, target, 0, GUI_MSG_UPDATE); + msg.SetStringParam(item->GetPath()); + windowMgr.SendMessage(msg); + } + else + { + windowMgr.ActivateWindow(target, {item->GetPath(), "return"}); + } + return true; +} + +std::string CVideoResume::GetLabel(const CFileItem& item) const +{ + return CGUIWindowVideoBase::GetResumeString(item.GetItemToPlay()); +} + +bool CVideoResume::IsVisible(const CFileItem& itemIn) const +{ + CFileItem item(itemIn.GetItemToPlay()); + if (item.IsDeleted()) // e.g. trashed pvr recording + return false; + + return VIDEO_UTILS::GetItemResumeInformation(item).isResumable; +} + +namespace +{ + +void AddRecordingsToPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems) +{ + if (item->m_bIsFolder) + { + CFileItemList items; + XFILE::CDirectory::GetDirectory(item->GetPath(), items, "", XFILE::DIR_FLAG_DEFAULTS); + + const int watchedMode = CMediaSettings::GetInstance().GetWatchedMode("recordings"); + const bool unwatchedOnly = watchedMode == WatchedModeUnwatched; + const bool watchedOnly = watchedMode == WatchedModeWatched; + for (const auto& currItem : items) + { + if (currItem->HasVideoInfoTag() && + ((unwatchedOnly && currItem->GetVideoInfoTag()->GetPlayCount() > 0) || + (watchedOnly && currItem->GetVideoInfoTag()->GetPlayCount() <= 0))) + continue; + + AddRecordingsToPlayList(currItem, queuedItems); + } + } + else + { + queuedItems.Add(item); + } +} + +void AddRecordingsToPlayListAndSort(const std::shared_ptr<CFileItem>& item, + CFileItemList& queuedItems) +{ + queuedItems.SetPath(item->GetPath()); + AddRecordingsToPlayList(item, queuedItems); + + if (!queuedItems.IsEmpty()) + { + const int windowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + if (windowId == WINDOW_TV_RECORDINGS || windowId == WINDOW_RADIO_RECORDINGS) + { + std::unique_ptr<CGUIViewState> viewState(CGUIViewState::GetViewState(windowId, queuedItems)); + if (viewState) + queuedItems.Sort(viewState->GetSortMethod()); + } + } +} + +void PlayAndQueueRecordings(const std::shared_ptr<CFileItem>& item, int windowId) +{ + const std::shared_ptr<CFileItem> parentFolderItem = + std::make_shared<CFileItem>(URIUtils::GetParentPath(item->GetPath()), true); + + // add all items of given item's directory to a temporary playlist, start playback of given item + CFileItemList queuedItems; + AddRecordingsToPlayListAndSort(parentFolderItem, queuedItems); + + PLAYLIST::CPlayListPlayer& player = CServiceBroker::GetPlaylistPlayer(); + + player.ClearPlaylist(PLAYLIST::TYPE_VIDEO); + player.Reset(); + player.Add(PLAYLIST::TYPE_VIDEO, queuedItems); + + // figure out where to start playback + PLAYLIST::CPlayList& playList = player.GetPlaylist(PLAYLIST::TYPE_VIDEO); + int itemToPlay = 0; + + for (int i = 0; i < queuedItems.Size(); ++i) + { + if (item->IsSamePath(queuedItems.Get(i).get())) + { + itemToPlay = i; + break; + } + } + + if (player.IsShuffled(PLAYLIST::TYPE_VIDEO)) + { + playList.Swap(0, playList.FindOrder(itemToPlay)); + itemToPlay = 0; + } + + player.SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO); + player.Play(itemToPlay, ""); +} + +void SetPathAndPlay(CFileItem& item) +{ + if (!item.m_bIsFolder && item.IsVideoDb()) + { + item.SetProperty("original_listitem_url", item.GetPath()); + item.SetPath(item.GetVideoInfoTag()->m_strFileNameAndPath); + } + item.SetProperty("check_resume", false); + + if (item.IsLiveTV()) // pvr tv or pvr radio? + { + g_application.PlayMedia(item, "", PLAYLIST::TYPE_VIDEO); + } + else + { + item.SetProperty("playlist_type_hint", PLAYLIST::TYPE_VIDEO); + VIDEO_UTILS::PlayItem(std::make_shared<CFileItem>(item)); + } +} + +} // unnamed namespace + +bool CVideoResume::Execute(const std::shared_ptr<CFileItem>& itemIn) const +{ + CFileItem item(itemIn->GetItemToPlay()); +#ifdef HAS_DVD_DRIVE + if (item.IsDVD() || item.IsCDDA()) + return MEDIA_DETECT::CAutorun::PlayDisc(item.GetPath(), true, false); +#endif + + item.SetStartOffset(STARTOFFSET_RESUME); + SetPathAndPlay(item); + return true; +}; + +std::string CVideoPlay::GetLabel(const CFileItem& itemIn) const +{ + CFileItem item(itemIn.GetItemToPlay()); + if (item.IsLiveTV()) + return g_localizeStrings.Get(19000); // Switch to channel + if (VIDEO_UTILS::GetItemResumeInformation(item).isResumable) + return g_localizeStrings.Get(12021); // Play from beginning + return g_localizeStrings.Get(208); // Play +} + +bool CVideoPlay::IsVisible(const CFileItem& item) const +{ + return VIDEO_UTILS::IsItemPlayable(item); +} + +bool CVideoPlay::Execute(const std::shared_ptr<CFileItem>& itemIn) const +{ + CFileItem item(itemIn->GetItemToPlay()); +#ifdef HAS_DVD_DRIVE + if (item.IsDVD() || item.IsCDDA()) + return MEDIA_DETECT::CAutorun::PlayDisc(item.GetPath(), true, true); +#endif + SetPathAndPlay(item); + return true; +}; + +namespace +{ +void SelectNextItem(int windowID) +{ + auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager(); + CGUIWindow* window = windowMgr.GetWindow(windowID); + if (window) + { + const int viewContainerID = window->GetViewContainerID(); + if (viewContainerID > 0) + { + CGUIMessage msg1(GUI_MSG_ITEM_SELECTED, windowID, viewContainerID); + windowMgr.SendMessage(msg1, windowID); + + CGUIMessage msg2(GUI_MSG_ITEM_SELECT, windowID, viewContainerID, msg1.GetParam1() + 1); + windowMgr.SendMessage(msg2, windowID); + } + } +} +} // unnamed namespace + +bool CVideoQueue::IsVisible(const CFileItem& item) const +{ + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST) + return false; // Already queued + + if (!item.CanQueue()) + return false; + + return VIDEO_UTILS::IsItemPlayable(item); +} + +bool CVideoQueue::Execute(const std::shared_ptr<CFileItem>& item) const +{ + const int windowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + if (windowID == WINDOW_VIDEO_PLAYLIST) + return false; // Already queued + + VIDEO_UTILS::QueueItem(item, VIDEO_UTILS::QueuePosition::POSITION_END); + + // Set selection to next item in active window's view. + SelectNextItem(windowID); + + return true; +}; + +bool CVideoPlayNext::IsVisible(const CFileItem& item) const +{ + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST) + return false; // Already queued + + if (!item.CanQueue()) + return false; + + return VIDEO_UTILS::IsItemPlayable(item); +} + +bool CVideoPlayNext::Execute(const std::shared_ptr<CFileItem>& item) const +{ + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST) + return false; // Already queued + + VIDEO_UTILS::QueueItem(item, VIDEO_UTILS::QueuePosition::POSITION_BEGIN); + return true; +}; + +bool CVideoPlayAndQueue::IsVisible(const CFileItem& item) const +{ + const int windowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + if (windowId == WINDOW_VIDEO_PLAYLIST) + return false; // Already queued + + if ((windowId == WINDOW_TV_RECORDINGS || windowId == WINDOW_RADIO_RECORDINGS) && + item.IsUsablePVRRecording()) + return true; + + return false; //! @todo implement +} + +bool CVideoPlayAndQueue::Execute(const std::shared_ptr<CFileItem>& item) const +{ + const int windowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + if (windowId == WINDOW_VIDEO_PLAYLIST) + return false; // Already queued + + if ((windowId == WINDOW_TV_RECORDINGS || windowId == WINDOW_RADIO_RECORDINGS) && + item->IsUsablePVRRecording()) + { + // recursively add items located in the same folder as item to play list, starting with item + PlayAndQueueRecordings(item, windowId); + return true; + } + + return true; //! @todo implement +}; + +} diff --git a/xbmc/video/ContextMenus.h b/xbmc/video/ContextMenus.h new file mode 100644 index 0000000..1cbffe4 --- /dev/null +++ b/xbmc/video/ContextMenus.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016-2018 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. + */ + +#pragma once + +#include "ContextMenuItem.h" +#include "VideoLibraryQueue.h" +#include "media/MediaType.h" + +#include <memory> + +namespace CONTEXTMENU +{ + +class CVideoInfo : public CStaticContextMenuAction +{ +public: + explicit CVideoInfo(MediaType mediaType); + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; + +private: + const MediaType m_mediaType; +}; + +struct CTVShowInfo : CVideoInfo +{ + CTVShowInfo() : CVideoInfo(MediaTypeTvShow) {} +}; + +struct CEpisodeInfo : CVideoInfo +{ + CEpisodeInfo() : CVideoInfo(MediaTypeEpisode) {} +}; + +struct CMusicVideoInfo : CVideoInfo +{ + CMusicVideoInfo() : CVideoInfo(MediaTypeMusicVideo) {} +}; + +struct CMovieInfo : CVideoInfo +{ + CMovieInfo() : CVideoInfo(MediaTypeMovie) {} +}; + +struct CVideoRemoveResumePoint : CStaticContextMenuAction +{ + CVideoRemoveResumePoint() : CStaticContextMenuAction(38209) {} + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CVideoMarkWatched : CStaticContextMenuAction +{ + CVideoMarkWatched() : CStaticContextMenuAction(16103) {} + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CVideoMarkUnWatched : CStaticContextMenuAction +{ + CVideoMarkUnWatched() : CStaticContextMenuAction(16104) {} + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CVideoBrowse : CStaticContextMenuAction +{ + CVideoBrowse() : CStaticContextMenuAction(37015) {} // Browse into + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CVideoResume : IContextMenuItem +{ + std::string GetLabel(const CFileItem& item) const override; + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& _item) const override; +}; + +struct CVideoPlay : IContextMenuItem +{ + std::string GetLabel(const CFileItem& item) const override; + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& _item) const override; +}; + +struct CVideoQueue : CStaticContextMenuAction +{ + CVideoQueue() : CStaticContextMenuAction(13347) {} // Queue item + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CVideoPlayNext : CStaticContextMenuAction +{ + CVideoPlayNext() : CStaticContextMenuAction(10008) {} // Play next + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +struct CVideoPlayAndQueue : CStaticContextMenuAction +{ + CVideoPlayAndQueue() : CStaticContextMenuAction(13412) {} // Play from here + bool IsVisible(const CFileItem& item) const override; + bool Execute(const std::shared_ptr<CFileItem>& item) const override; +}; + +} diff --git a/xbmc/video/Episode.h b/xbmc/video/Episode.h new file mode 100644 index 0000000..833f9e3 --- /dev/null +++ b/xbmc/video/Episode.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "XBDateTime.h" +#include "utils/ScraperUrl.h" + +#include <memory> +#include <string> +#include <vector> + +class CFileItem; + +// single episode information +namespace VIDEO +{ + struct EPISODE + { + bool isFolder; + int iSeason; + int iEpisode; + int iSubepisode; + std::string strPath; + std::string strTitle; + CDateTime cDate; + CScraperUrl cScraperUrl; + std::shared_ptr<CFileItem> item; + EPISODE(int Season = -1, int Episode = -1, int Subepisode = 0, bool Folder = false) + { + iSeason = Season; + iEpisode = Episode; + iSubepisode = Subepisode; + isFolder = Folder; + } + bool operator==(const struct EPISODE& rhs) const + { + return (iSeason == rhs.iSeason && + iEpisode == rhs.iEpisode && + iSubepisode == rhs.iSubepisode); + } + }; + + typedef std::vector<EPISODE> EPISODELIST; +} + diff --git a/xbmc/video/GUIViewStateVideo.cpp b/xbmc/video/GUIViewStateVideo.cpp new file mode 100644 index 0000000..3065272 --- /dev/null +++ b/xbmc/video/GUIViewStateVideo.cpp @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2016-2018 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 "GUIViewStateVideo.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "VideoDatabase.h" +#include "filesystem/Directory.h" +#include "filesystem/VideoDatabaseDirectory.h" +#include "guilib/WindowIDs.h" +#include "playlists/PlayListTypes.h" +#include "settings/MediaSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileExtensionProvider.h" +#include "utils/SortUtils.h" +#include "view/ViewStateSettings.h" + +using namespace XFILE; +using namespace VIDEODATABASEDIRECTORY; + +std::string CGUIViewStateWindowVideo::GetLockType() +{ + return "video"; +} + +std::string CGUIViewStateWindowVideo::GetExtensions() +{ + return CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(); +} + +PLAYLIST::Id CGUIViewStateWindowVideo::GetPlaylist() const +{ + return PLAYLIST::TYPE_VIDEO; +} + +VECSOURCES& CGUIViewStateWindowVideo::GetSources() +{ + AddLiveTVSources(); + return CGUIViewState::GetSources(); +} + +bool CGUIViewStateWindowVideo::AutoPlayNextItem() +{ + return AutoPlayNextVideoItem(); +} + +/***************************/ + +CGUIViewStateWindowVideoNav::CGUIViewStateWindowVideoNav(const CFileItemList& items) : CGUIViewStateWindowVideo(items) +{ + SortAttribute sortAttributes = SortAttributeNone; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttributes = SortAttributeIgnoreArticle; + + if (items.IsVirtualDirectoryRoot()) + { + AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + } + else if (items.IsVideoDb()) + { + NODE_TYPE NodeType=CVideoDatabaseDirectory::GetDirectoryChildType(items.GetPath()); + CQueryParams params; + CVideoDatabaseDirectory::GetQueryParams(items.GetPath(),params); + + switch (NodeType) + { + case NODE_TYPE_MOVIES_OVERVIEW: + case NODE_TYPE_TVSHOWS_OVERVIEW: + case NODE_TYPE_MUSICVIDEOS_OVERVIEW: + case NODE_TYPE_OVERVIEW: + { + AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty + + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_DIRECTOR: + case NODE_TYPE_ACTOR: + { + AddSortMethod(SortByLabel, 551, LABEL_MASKS("%T", "%R", "%L", "")); // Title, Rating | Label, empty + AddSortMethod(SortByRelevance, 38026, LABEL_MASKS("%T", "%c", "%L", "%c")); // Title, Actor's appearances (Relevance) | Label, Actor's appearances (Relevance) + SetSortMethod(SortByLabel); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavactors"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_YEAR: + { + AddSortMethod(SortByLabel, 562, LABEL_MASKS("%T", "%R", "%L", "")); // Title, Rating | Label, empty + SetSortMethod(SortByLabel); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavyears"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_SEASONS: + { + AddSortMethod(SortBySortTitle, 556, LABEL_MASKS("%L", "","%L","")); // Label, empty | Label, empty + SetSortMethod(SortBySortTitle); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavseasons"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_TITLE_TVSHOWS: + case NODE_TYPE_INPROGRESS_TVSHOWS: + { + AddSortMethod(SortBySortTitle, sortAttributes, 556, LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes + AddSortMethod(SortByOriginalTitle, sortAttributes, 20376, + LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes + + AddSortMethod(SortByNumberOfEpisodes, sortAttributes, 20360, + LABEL_MASKS("%L", "%M", "%L", "%M")); // Label, #Episodes | Label, #Episodes + AddSortMethod( + SortByLastPlayed, sortAttributes, 568, + LABEL_MASKS("%T", "%p", "%T", "%p")); // Title, #Last played | Title, #Last played + AddSortMethod(SortByDateAdded, sortAttributes, 570, + LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded + AddSortMethod(SortByYear, sortAttributes, 562, + LABEL_MASKS("%L", "%Y", "%L", "%Y")); // Label, Year | Label, Year + AddSortMethod(SortByRating, sortAttributes, 563, + LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByUserRating, sortAttributes, 38018, + LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating + SetSortMethod(SortByLabel); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtvshows"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_MUSICVIDEOS_ALBUM: + case NODE_TYPE_GENRE: + case NODE_TYPE_COUNTRY: + case NODE_TYPE_STUDIO: + { + AddSortMethod(SortByLabel, 551, LABEL_MASKS("%T", "%R", "%L", "")); // Title, Rating | Label, empty + SetSortMethod(SortByLabel); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavgenres"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_SETS: + { + AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T","%R", "%T","%R")); // Title, Rating | Title, Rating + + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year + AddSortMethod(SortByRating, 563, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded + AddSortMethod(SortByPlaycount, 567, + LABEL_MASKS("%T", "%V", "%T", "%V")); // Title, Playcount | Title, Playcount + + SetSortMethod(SortByLabel); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavgenres"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_TAGS: + { + AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T","", "%T","")); // Title, empty | Title, empty + SetSortMethod(SortByLabel); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavgenres"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_EPISODES: + { + if (params.GetSeason() > -1) + { + AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS("%E. %T","%R")); // Episode. Title, Rating | empty, empty + AddSortMethod(SortByRating, 563, LABEL_MASKS("%E. %T", "%R")); // Episode. Title, Rating | empty, empty + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%E. %T", "%r")); // Episode. Title, Userrating | empty, empty + AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%E. %T", "%O")); // Episode. Title, MPAA | empty, empty + AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS("%E. %T","%P", "%E. %T","%P")); // Episode. Title, ProductionCode | Episode. Title, ProductionCode + AddSortMethod(SortByDate, 552, LABEL_MASKS("%E. %T","%J","%E. %T","%J")); // Episode. Title, Date | Episode. Title, Date + AddSortMethod(SortByPlaycount, 567, + LABEL_MASKS("%E. %T", "%V")); // Episode. Title, Playcount | empty, empty + } + else + { + AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS("%H. %T","%R")); // Order. Title, Rating | empty, empty + AddSortMethod(SortByRating, 563, LABEL_MASKS("%H. %T", "%R")); // Order. Title, Rating | empty, empty + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%H. %T", "%r")); // Order. Title, Userrating | empty, empty + AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%H. %T", "%O")); // Order. Title, MPAA | empty, empty + AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS("%H. %T","%P", "%H. %T","%P")); // Order. Title, ProductionCode | Episode. Title, ProductionCode + AddSortMethod(SortByDate, 552, LABEL_MASKS("%H. %T","%J","%H. %T","%J")); // Order. Title, Date | Episode. Title, Date + AddSortMethod(SortByPlaycount, 567, + LABEL_MASKS("%H. %T", "%V")); // Order. Title, Playcount | empty, empty + } + AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T","%R")); // Title, Rating | empty, empty + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavepisodes"); + SetSortMethod(viewState->m_sortDescription); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + break; + } + case NODE_TYPE_RECENTLY_ADDED_EPISODES: + { + AddSortMethod(SortByNone, 552, LABEL_MASKS("%Z - %H. %T", "%R")); // TvShow - Order. Title, Rating | empty, empty + SetSortMethod(SortByNone); + + SetViewAsControl(CViewStateSettings::GetInstance().Get("videonavepisodes")->m_viewMode); + SetSortOrder(SortOrderNone); + + break; + } + case NODE_TYPE_TITLE_MOVIES: + { + if (params.GetSetId() > -1) // Is this a listing within a set? + { + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y")); // Title, Year | empty, empty + AddSortMethod(SortBySortTitle, sortAttributes, 556, LABEL_MASKS("%T", "%R")); // Title, Rating | empty, empty + AddSortMethod(SortByOriginalTitle, sortAttributes, 20376, + LABEL_MASKS("%T", "%R")); // Title, Rating | empty, empty + } + else + { + AddSortMethod(SortBySortTitle, sortAttributes, 556, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByOriginalTitle, sortAttributes, 20376, + LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year + } + AddSortMethod(SortByRating, 563, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating + AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T", "%O")); // Title, MPAA | empty, empty + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T", "%D")); // Title, Duration | empty, empty + AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded + AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%T", "%p", "%T", "%p")); // Title, #Last played | Title, #Last played + AddSortMethod(SortByPlaycount, 567, + LABEL_MASKS("%T", "%V", "%T", "%V")); // Title, Playcount | Title, Playcount + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtitles"); + if (params.GetSetId() > -1) + { + SetSortMethod(SortByYear); + SetSortOrder(SortOrderAscending); + } + else + { + SetSortMethod(viewState->m_sortDescription); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + + SetViewAsControl(viewState->m_viewMode); + } + break; + case NODE_TYPE_TITLE_MUSICVIDEOS: + { + AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year | empty, empty + AddSortMethod(SortByAlbum, sortAttributes, 558, LABEL_MASKS("%B - %T - %A", "%Y")); // Album, Title, Artist, Year | empty, empty + AddSortMethod(SortByArtist, sortAttributes, 557, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty + AddSortMethod(SortByArtistThenYear, sortAttributes, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year| empty, empty + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount + AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T - %A", "%O")); // Title - Artist, MPAARating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating + + std::string strTrack=CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%N")); // Userdefined, Track Number | empty, empty + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavmusicvideos"); + SetSortMethod(viewState->m_sortDescription); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + break; + case NODE_TYPE_RECENTLY_ADDED_MOVIES: + { + AddSortMethod(SortByNone, 552, LABEL_MASKS("%T", "%R")); // Title, Rating | empty, empty + SetSortMethod(SortByNone); + + SetViewAsControl(CViewStateSettings::GetInstance().Get("videonavtitles")->m_viewMode); + + SetSortOrder(SortOrderNone); + } + break; + case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: + { + AddSortMethod(SortByNone, 552, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty + SetSortMethod(SortByNone); + + SetViewAsControl(CViewStateSettings::GetInstance().Get("videonavmusicvideos")->m_viewMode); + + SetSortOrder(SortOrderNone); + } + break; + default: + break; + } + } + else + { + AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty + AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // Label, Size | Label, Size + AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // Label, Date | Label, Date + AddSortMethod(SortByFile, 561, LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videofiles"); + SetSortMethod(viewState->m_sortDescription); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV); +} + +void CGUIViewStateWindowVideoNav::SaveViewState() +{ + if (m_items.IsVideoDb()) + { + NODE_TYPE NodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_items.GetPath()); + CQueryParams params; + CVideoDatabaseDirectory::GetQueryParams(m_items.GetPath(),params); + switch (NodeType) + { + case NODE_TYPE_ACTOR: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavactors")); + break; + case NODE_TYPE_YEAR: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavyears")); + break; + case NODE_TYPE_GENRE: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavgenres")); + break; + case NODE_TYPE_TITLE_MOVIES: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, params.GetSetId() > -1 ? NULL : CViewStateSettings::GetInstance().Get("videonavtitles")); + break; + case NODE_TYPE_EPISODES: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavepisodes")); + break; + case NODE_TYPE_TITLE_TVSHOWS: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavtvshows")); + break; + case NODE_TYPE_SEASONS: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavseasons")); + break; + case NODE_TYPE_TITLE_MUSICVIDEOS: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavmusicvideos")); + break; + default: + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV); + break; + } + } + else + { + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videofiles")); + } +} + +VECSOURCES& CGUIViewStateWindowVideoNav::GetSources() +{ + // Setup shares we want to have + m_sources.clear(); + CFileItemList items; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + CDirectory::GetDirectory("library://video_flat/", items, "", DIR_FLAG_DEFAULTS); + else + CDirectory::GetDirectory("library://video/", items, "", DIR_FLAG_DEFAULTS); + for (int i=0; i<items.Size(); ++i) + { + CFileItemPtr item=items[i]; + CMediaSource share; + share.strName=item->GetLabel(); + share.strPath = item->GetPath(); + share.m_strThumbnailImage = item->GetArt("icon"); + share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + m_sources.push_back(share); + } + return CGUIViewStateWindowVideo::GetSources(); +} + +bool CGUIViewStateWindowVideoNav::AutoPlayNextItem() +{ + CQueryParams params; + CVideoDatabaseDirectory::GetQueryParams(m_items.GetPath(),params); + if (static_cast<VideoDbContentType>(params.GetContentType()) == VideoDbContentType::MUSICVIDEOS) + return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_AUTOPLAYNEXTITEM); + + return CGUIViewStateWindowVideo::AutoPlayNextItem(); +} + +CGUIViewStateWindowVideoPlaylist::CGUIViewStateWindowVideoPlaylist(const CFileItemList& items) : CGUIViewStateWindowVideo(items) +{ + AddSortMethod(SortByNone, 551, LABEL_MASKS("%L", "", "%L", "")); // Label, empty | Label, empty + SetSortMethod(SortByNone); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderNone); + + LoadViewState(items.GetPath(), WINDOW_VIDEO_PLAYLIST); +} + +void CGUIViewStateWindowVideoPlaylist::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_PLAYLIST); +} + +bool CGUIViewStateWindowVideoPlaylist::HideExtensions() +{ + return true; +} + +bool CGUIViewStateWindowVideoPlaylist::HideParentDirItems() +{ + return true; +} + +VECSOURCES& CGUIViewStateWindowVideoPlaylist::GetSources() +{ + m_sources.clear(); + // Playlist share + CMediaSource share; + share.strPath= "playlistvideo://"; + share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + m_sources.push_back(share); + + // no plugins in playlist window + return m_sources; +} + +CGUIViewStateVideoMovies::CGUIViewStateVideoMovies(const CFileItemList& items) : CGUIViewStateWindowVideo(items) +{ + SortAttribute sortAttributes = SortAttributeNone; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttributes = SortAttributeIgnoreArticle; + + AddSortMethod(SortBySortTitle, sortAttributes, 556, + LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByOriginalTitle, sortAttributes, 20376, + LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year + AddSortMethod(SortByRating, 563, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating + AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T", "%O")); // Title, MPAA | empty, empty + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T", "%D")); // Title, Duration | empty, empty + AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded + AddSortMethod(SortByPlaycount, 567, + LABEL_MASKS("%T", "%V", "%T", "%V")); // Title, Playcount | Title, Playcount + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtitles"); + if (items.IsSmartPlayList() || items.IsLibraryFolder()) + AddPlaylistOrder(items, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating + else + { + SetSortMethod(viewState->m_sortDescription); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + + SetViewAsControl(viewState->m_viewMode); + + LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV); +} + +void CGUIViewStateVideoMovies::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavtitles")); +} + +CGUIViewStateVideoMusicVideos::CGUIViewStateVideoMusicVideos(const CFileItemList& items) : CGUIViewStateWindowVideo(items) +{ + SortAttribute sortAttributes = SortAttributeNone; + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttributes = SortAttributeIgnoreArticle; + + AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year | empty, empty + AddSortMethod(SortByAlbum, sortAttributes, 558, LABEL_MASKS("%B - %T - %A", "%Y")); // Album, Title, Artist, Year | empty, empty + AddSortMethod(SortByArtist, sortAttributes, 557, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty + AddSortMethod(SortByArtistThenYear, sortAttributes, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty + AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year| empty, empty + AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty + AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount + AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T - %A", "%O")); // Title - Artist, MPAARating + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating + + std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%N")); // Userdefined, Track Number | empty, empty + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavmusicvideos"); + if (items.IsSmartPlayList() || items.IsLibraryFolder()) + AddPlaylistOrder(items, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty + else + { + SetSortMethod(viewState->m_sortDescription); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + + SetViewAsControl(viewState->m_viewMode); + + LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV); +} + +void CGUIViewStateVideoMusicVideos::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavmusicvideos")); +} + +CGUIViewStateVideoTVShows::CGUIViewStateVideoTVShows(const CFileItemList& items) : CGUIViewStateWindowVideo(items) +{ + SortAttribute sortAttributes = SortAttributeNone; + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttributes = SortAttributeIgnoreArticle; + + AddSortMethod(SortBySortTitle, sortAttributes, 556, + LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes + AddSortMethod(SortByNumberOfEpisodes, sortAttributes, 20360, + LABEL_MASKS("%L", "%M", "%L", "%M")); // Label, #Episodes | Label, #Episodes + AddSortMethod(SortByLastPlayed, sortAttributes, 568, + LABEL_MASKS("%T", "%p", "%T", "%p")); // Title, #Last played | Title, #Last played + AddSortMethod(SortByDateAdded, sortAttributes, 570, + LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded + AddSortMethod(SortByYear, sortAttributes, 562, + LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year + AddSortMethod(SortByUserRating, sortAttributes, 38018, + LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtvshows"); + if (items.IsSmartPlayList() || items.IsLibraryFolder()) + AddPlaylistOrder(items, LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes + else + { + SetSortMethod(viewState->m_sortDescription); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + + SetViewAsControl(viewState->m_viewMode); + + LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV); +} + +void CGUIViewStateVideoTVShows::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavtvshows")); +} + +CGUIViewStateVideoEpisodes::CGUIViewStateVideoEpisodes(const CFileItemList& items) : CGUIViewStateWindowVideo(items) +{ + // TvShow - Order. Title, Rating | empty, empty + AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS("%Z - %H. %T","%R")); + // TvShow - Order. Title, Rating | empty, empty + AddSortMethod(SortByRating, 563, LABEL_MASKS("%Z - %H. %T", "%R")); + // TvShow - Order. Title, Userrating | empty, empty + AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%Z - %H. %T", "%r")); + // TvShow - Order. Title, MPAA | empty, empty + AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%Z - %H. %T", "%O")); + // TvShow - Order. Title, Production Code | empty, empty + AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS("%Z - %H. %T","%P")); + // TvShow - Order. Title, Date | empty, empty + AddSortMethod(SortByDate, 552, LABEL_MASKS("%Z - %H. %T","%J")); + // TvShow - Order. Title, Playcount | empty, empty + AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%H. %T", "%V")); + + AddSortMethod(SortByLabel, 551, LABEL_MASKS("%Z - %H. %T","%R"), // TvShow - Order. Title, Rating | empty, empty + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavepisodes"); + if (items.IsSmartPlayList() || items.IsLibraryFolder()) + AddPlaylistOrder(items, LABEL_MASKS("%Z - %H. %T", "%R")); // TvShow - Order. Title, Rating | empty, empty + else + { + SetSortMethod(viewState->m_sortDescription); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + + SetViewAsControl(viewState->m_viewMode); + + LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV); +} + +void CGUIViewStateVideoEpisodes::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavepisodes")); +} + +CGUIViewStateVideoPlaylist::CGUIViewStateVideoPlaylist(const CFileItemList& items) + : CGUIViewStateWindowVideo(items) +{ + SortAttribute sortAttributes = SortAttributeNone; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sortAttributes = SortAttributeIgnoreArticle; + + AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS("%L", "")); // Label, empty + AddSortMethod(SortByLabel, sortAttributes, 551, + LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty + AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // Label, Size | Label, Size + AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // Label, Date | Label, Date + AddSortMethod(SortByFile, 561, LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty + + SetSortMethod(SortByPlaylistOrder); + + const CViewState* viewState = CViewStateSettings::GetInstance().Get("videofiles"); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + + LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV); +} + +void CGUIViewStateVideoPlaylist::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV); +} diff --git a/xbmc/video/GUIViewStateVideo.h b/xbmc/video/GUIViewStateVideo.h new file mode 100644 index 0000000..4a7a8e6 --- /dev/null +++ b/xbmc/video/GUIViewStateVideo.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "view/GUIViewState.h" + +class CGUIViewStateWindowVideo : public CGUIViewState +{ +public: + explicit CGUIViewStateWindowVideo(const CFileItemList& items) : CGUIViewState(items) {} + +protected: + VECSOURCES& GetSources() override; + std::string GetLockType() override; + PLAYLIST::Id GetPlaylist() const override; + std::string GetExtensions() override; + bool AutoPlayNextItem() override; +}; + +class CGUIViewStateVideoPlaylist : public CGUIViewStateWindowVideo +{ +public: + explicit CGUIViewStateVideoPlaylist(const CFileItemList& items); + +protected: + void SaveViewState() override; +}; + +class CGUIViewStateWindowVideoNav : public CGUIViewStateWindowVideo +{ +public: + explicit CGUIViewStateWindowVideoNav(const CFileItemList& items); + bool AutoPlayNextItem() override; + +protected: + void SaveViewState() override; + VECSOURCES& GetSources() override; +}; + +class CGUIViewStateWindowVideoPlaylist : public CGUIViewStateWindowVideo +{ +public: + explicit CGUIViewStateWindowVideoPlaylist(const CFileItemList& items); + +protected: + void SaveViewState() override; + bool HideExtensions() override; + bool HideParentDirItems() override; + VECSOURCES& GetSources() override; + bool AutoPlayNextItem() override { return false; } +}; + +class CGUIViewStateVideoMovies : public CGUIViewStateWindowVideo +{ +public: + explicit CGUIViewStateVideoMovies(const CFileItemList& items); +protected: + void SaveViewState() override; +}; + +class CGUIViewStateVideoMusicVideos : public CGUIViewStateWindowVideo +{ +public: + explicit CGUIViewStateVideoMusicVideos(const CFileItemList& items); +protected: + void SaveViewState() override; +}; + +class CGUIViewStateVideoTVShows : public CGUIViewStateWindowVideo +{ +public: + explicit CGUIViewStateVideoTVShows(const CFileItemList& items); +protected: + void SaveViewState() override; +}; + +class CGUIViewStateVideoEpisodes : public CGUIViewStateWindowVideo +{ +public: + explicit CGUIViewStateVideoEpisodes(const CFileItemList& items); +protected: + void SaveViewState() override; +}; + diff --git a/xbmc/video/PlayerController.cpp b/xbmc/video/PlayerController.cpp new file mode 100644 index 0000000..036fbae --- /dev/null +++ b/xbmc/video/PlayerController.cpp @@ -0,0 +1,582 @@ +/* + * Copyright (C) 2012-2018 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 "PlayerController.h" + +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "cores/IPlayer.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogSlider.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUISliderControl.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "settings/AdvancedSettings.h" +#include "settings/DisplaySettings.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/SubtitlesSettings.h" +#include "utils/LangCodeExpander.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "video/dialogs/GUIDialogAudioSettings.h" + +using namespace KODI; +using namespace UTILS; + +CPlayerController::CPlayerController() +{ + MOVING_SPEED::EventCfg eventCfg{100.0f, 300.0f, 200}; + m_movingSpeed.AddEventConfig(ACTION_SUBTITLE_VSHIFT_UP, eventCfg); + m_movingSpeed.AddEventConfig(ACTION_SUBTITLE_VSHIFT_DOWN, eventCfg); +} + +CPlayerController::~CPlayerController() = default; + +CPlayerController& CPlayerController::GetInstance() +{ + static CPlayerController instance; + return instance; +} + +bool CPlayerController::OnAction(const CAction &action) +{ + const unsigned int MsgTime = 300; + const unsigned int DisplTime = 2000; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + if (appPlayer->IsPlayingVideo()) + { + switch (action.GetID()) + { + case ACTION_SHOW_SUBTITLES: + { + if (appPlayer->GetSubtitleCount() == 0) + { + CGUIDialogKaiToast::QueueNotification( + CGUIDialogKaiToast::Info, g_localizeStrings.Get(287), g_localizeStrings.Get(10005), + DisplTime, false, MsgTime); + return true; + } + + bool subsOn = !appPlayer->GetSubtitleVisible(); + appPlayer->SetSubtitleVisible(subsOn); + std::string sub; + if (subsOn) + { + std::string lang; + SubtitleStreamInfo info; + appPlayer->GetSubtitleStreamInfo(CURRENT_STREAM, info); + if (!g_LangCodeExpander.Lookup(info.language, lang)) + lang = g_localizeStrings.Get(13205); // Unknown + + if (info.name.length() == 0) + sub = lang; + else + sub = StringUtils::Format("{} - {}", lang, info.name); + } + else + sub = g_localizeStrings.Get(1223); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, + g_localizeStrings.Get(287), sub, DisplTime, false, MsgTime); + return true; + } + + case ACTION_NEXT_SUBTITLE: + case ACTION_CYCLE_SUBTITLE: + { + if (appPlayer->GetSubtitleCount() == 0) + return true; + + int currentSub = appPlayer->GetSubtitle(); + bool currentSubVisible = true; + + if (appPlayer->GetSubtitleVisible()) + { + if (++currentSub >= appPlayer->GetSubtitleCount()) + { + currentSub = 0; + if (action.GetID() == ACTION_NEXT_SUBTITLE) + { + appPlayer->SetSubtitleVisible(false); + currentSubVisible = false; + } + } + appPlayer->SetSubtitle(currentSub); + } + else if (action.GetID() == ACTION_NEXT_SUBTITLE) + { + appPlayer->SetSubtitleVisible(true); + } + + std::string sub, lang; + if (currentSubVisible) + { + SubtitleStreamInfo info; + appPlayer->GetSubtitleStreamInfo(currentSub, info); + if (!g_LangCodeExpander.Lookup(info.language, lang)) + lang = g_localizeStrings.Get(13205); // Unknown + + if (info.name.length() == 0) + sub = lang; + else + sub = StringUtils::Format("{} - {}", lang, info.name); + } + else + sub = g_localizeStrings.Get(1223); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(287), sub, DisplTime, false, MsgTime); + return true; + } + + case ACTION_SUBTITLE_DELAY_MIN: + { + float videoSubsDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange; + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_SubtitleDelay -= 0.1f; + if (vs.m_SubtitleDelay < -videoSubsDelayRange) + vs.m_SubtitleDelay = -videoSubsDelayRange; + appPlayer->SetSubTitleDelay(vs.m_SubtitleDelay); + + ShowSlider(action.GetID(), 22006, appPlayer->GetVideoSettings().m_SubtitleDelay, + -videoSubsDelayRange, 0.1f, videoSubsDelayRange); + return true; + } + + case ACTION_SUBTITLE_DELAY_PLUS: + { + float videoSubsDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange; + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_SubtitleDelay += 0.1f; + if (vs.m_SubtitleDelay > videoSubsDelayRange) + vs.m_SubtitleDelay = videoSubsDelayRange; + appPlayer->SetSubTitleDelay(vs.m_SubtitleDelay); + + ShowSlider(action.GetID(), 22006, appPlayer->GetVideoSettings().m_SubtitleDelay, + -videoSubsDelayRange, 0.1f, videoSubsDelayRange); + return true; + } + + case ACTION_SUBTITLE_DELAY: + { + float videoSubsDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange; + ShowSlider(action.GetID(), 22006, appPlayer->GetVideoSettings().m_SubtitleDelay, + -videoSubsDelayRange, 0.1f, videoSubsDelayRange, true); + return true; + } + + case ACTION_AUDIO_DELAY: + { + float videoAudioDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange; + ShowSlider(action.GetID(), 297, appPlayer->GetVideoSettings().m_AudioDelay, + -videoAudioDelayRange, AUDIO_DELAY_STEP, videoAudioDelayRange, true); + return true; + } + + case ACTION_AUDIO_DELAY_MIN: + { + float videoAudioDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange; + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_AudioDelay -= AUDIO_DELAY_STEP; + if (vs.m_AudioDelay < -videoAudioDelayRange) + vs.m_AudioDelay = -videoAudioDelayRange; + appPlayer->SetAVDelay(vs.m_AudioDelay); + + ShowSlider(action.GetID(), 297, appPlayer->GetVideoSettings().m_AudioDelay, + -videoAudioDelayRange, AUDIO_DELAY_STEP, videoAudioDelayRange); + return true; + } + + case ACTION_AUDIO_DELAY_PLUS: + { + float videoAudioDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange; + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_AudioDelay += AUDIO_DELAY_STEP; + if (vs.m_AudioDelay > videoAudioDelayRange) + vs.m_AudioDelay = videoAudioDelayRange; + appPlayer->SetAVDelay(vs.m_AudioDelay); + + ShowSlider(action.GetID(), 297, appPlayer->GetVideoSettings().m_AudioDelay, + -videoAudioDelayRange, AUDIO_DELAY_STEP, videoAudioDelayRange); + return true; + } + + case ACTION_AUDIO_NEXT_LANGUAGE: + { + if (appPlayer->GetAudioStreamCount() == 1) + return true; + + int currentAudio = appPlayer->GetAudioStream(); + int audioStreamCount = appPlayer->GetAudioStreamCount(); + + if (++currentAudio >= audioStreamCount) + currentAudio = 0; + appPlayer->SetAudioStream(currentAudio); // Set the audio stream to the one selected + std::string aud; + std::string lan; + AudioStreamInfo info; + appPlayer->GetAudioStreamInfo(currentAudio, info); + if (!g_LangCodeExpander.Lookup(info.language, lan)) + lan = g_localizeStrings.Get(13205); // Unknown + if (info.name.empty()) + aud = lan; + else + aud = StringUtils::Format("{} - {}", lan, info.name); + std::string caption = g_localizeStrings.Get(460); + caption += StringUtils::Format(" ({}/{})", currentAudio + 1, audioStreamCount); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, caption, aud, DisplTime, false, MsgTime); + return true; + } + + case ACTION_VIDEO_NEXT_STREAM: + { + if (appPlayer->GetVideoStreamCount() == 1) + return true; + + int currentVideo = appPlayer->GetVideoStream(); + int videoStreamCount = appPlayer->GetVideoStreamCount(); + + if (++currentVideo >= videoStreamCount) + currentVideo = 0; + appPlayer->SetVideoStream(currentVideo); + VideoStreamInfo info; + appPlayer->GetVideoStreamInfo(currentVideo, info); + std::string caption = g_localizeStrings.Get(38031); + caption += StringUtils::Format(" ({}/{})", currentVideo + 1, videoStreamCount); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, caption, info.name, DisplTime, false, MsgTime); + return true; + } + + case ACTION_ZOOM_IN: + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_CustomZoomAmount += 0.01f; + if (vs.m_CustomZoomAmount > 2.f) + vs.m_CustomZoomAmount = 2.f; + vs.m_ViewMode = ViewModeCustom; + appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + ShowSlider(action.GetID(), 216, vs.m_CustomZoomAmount, 0.5f, 0.1f, 2.0f); + return true; + } + + case ACTION_ZOOM_OUT: + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_CustomZoomAmount -= 0.01f; + if (vs.m_CustomZoomAmount < 0.5f) + vs.m_CustomZoomAmount = 0.5f; + vs.m_ViewMode = ViewModeCustom; + appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + ShowSlider(action.GetID(), 216, vs.m_CustomZoomAmount, 0.5f, 0.1f, 2.0f); + return true; + } + + case ACTION_INCREASE_PAR: + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_CustomPixelRatio += 0.01f; + if (vs.m_CustomPixelRatio > 2.f) + vs.m_CustomPixelRatio = 2.f; + vs.m_ViewMode = ViewModeCustom; + appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + ShowSlider(action.GetID(), 217, vs.m_CustomPixelRatio, 0.5f, 0.1f, 2.0f); + return true; + } + + case ACTION_DECREASE_PAR: + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_CustomPixelRatio -= 0.01f; + if (vs.m_CustomPixelRatio < 0.5f) + vs.m_CustomPixelRatio = 0.5f; + vs.m_ViewMode = ViewModeCustom; + appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + ShowSlider(action.GetID(), 217, vs.m_CustomPixelRatio, 0.5f, 0.1f, 2.0f); + return true; + } + + case ACTION_VSHIFT_UP: + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_CustomVerticalShift -= 0.01f; + if (vs.m_CustomVerticalShift < -2.0f) + vs.m_CustomVerticalShift = -2.0f; + vs.m_ViewMode = ViewModeCustom; + appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + ShowSlider(action.GetID(), 225, vs.m_CustomVerticalShift, -2.0f, 0.1f, 2.0f); + return true; + } + + case ACTION_VSHIFT_DOWN: + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_CustomVerticalShift += 0.01f; + if (vs.m_CustomVerticalShift > 2.0f) + vs.m_CustomVerticalShift = 2.0f; + vs.m_ViewMode = ViewModeCustom; + appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + ShowSlider(action.GetID(), 225, vs.m_CustomVerticalShift, -2.0f, 0.1f, 2.0f); + return true; + } + + case ACTION_SUBTITLE_VSHIFT_UP: + { + const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()}; + SUBTITLES::Align subAlign{settings->GetAlignment()}; + if (subAlign != SUBTITLES::Align::BOTTOM_OUTSIDE && subAlign != SUBTITLES::Align::MANUAL) + return true; + + RESOLUTION_INFO resInfo = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(); + CVideoSettings vs = appPlayer->GetVideoSettings(); + + int maxPos = resInfo.Overscan.bottom; + if (subAlign == SUBTITLES::Align::BOTTOM_OUTSIDE) + { + maxPos = + resInfo.Overscan.bottom + static_cast<int>(static_cast<float>(resInfo.iHeight) / 100 * + settings->GetVerticalMarginPerc()); + } + + vs.m_subtitleVerticalPosition -= + static_cast<int>(m_movingSpeed.GetUpdatedDistance(ACTION_SUBTITLE_VSHIFT_UP)); + if (vs.m_subtitleVerticalPosition < resInfo.Overscan.top) + vs.m_subtitleVerticalPosition = resInfo.Overscan.top; + appPlayer->SetSubtitleVerticalPosition(vs.m_subtitleVerticalPosition, + action.GetText() == "save"); + + ShowSlider(action.GetID(), 277, static_cast<float>(vs.m_subtitleVerticalPosition), + static_cast<float>(resInfo.Overscan.top), 1.0f, static_cast<float>(maxPos)); + return true; + } + + case ACTION_SUBTITLE_VSHIFT_DOWN: + { + const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()}; + SUBTITLES::Align subAlign{settings->GetAlignment()}; + if (subAlign != SUBTITLES::Align::BOTTOM_OUTSIDE && subAlign != SUBTITLES::Align::MANUAL) + return true; + + RESOLUTION_INFO resInfo = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(); + CVideoSettings vs = appPlayer->GetVideoSettings(); + + int maxPos = resInfo.Overscan.bottom; + if (subAlign == SUBTITLES::Align::BOTTOM_OUTSIDE) + { + // In this case the position not includes the vertical margin, + // so to be able to move the text to the bottom of the screen + // we must extend the maximum position with the vertical margin. + // Note that the text may go also slightly off-screen, this is + // caused by Libass see "displacement compensation" on OverlayRenderer + maxPos = + resInfo.Overscan.bottom + static_cast<int>(static_cast<float>(resInfo.iHeight) / 100 * + settings->GetVerticalMarginPerc()); + } + + vs.m_subtitleVerticalPosition += + static_cast<int>(m_movingSpeed.GetUpdatedDistance(ACTION_SUBTITLE_VSHIFT_DOWN)); + if (vs.m_subtitleVerticalPosition > maxPos) + vs.m_subtitleVerticalPosition = maxPos; + appPlayer->SetSubtitleVerticalPosition(vs.m_subtitleVerticalPosition, + action.GetText() == "save"); + + ShowSlider(action.GetID(), 277, static_cast<float>(vs.m_subtitleVerticalPosition), + static_cast<float>(resInfo.Overscan.top), 1.0f, static_cast<float>(maxPos)); + return true; + } + + case ACTION_SUBTITLE_ALIGN: + { + const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()}; + SUBTITLES::Align align{settings->GetAlignment()}; + + align = static_cast<SUBTITLES::Align>(static_cast<int>(align) + 1); + + if (align != SUBTITLES::Align::MANUAL && align != SUBTITLES::Align::BOTTOM_INSIDE && + align != SUBTITLES::Align::BOTTOM_OUTSIDE && align != SUBTITLES::Align::TOP_INSIDE && + align != SUBTITLES::Align::TOP_OUTSIDE) + { + align = SUBTITLES::Align::MANUAL; + } + + settings->SetAlignment(align); + CGUIDialogKaiToast::QueueNotification( + CGUIDialogKaiToast::Info, g_localizeStrings.Get(21460), + g_localizeStrings.Get(21461 + static_cast<int>(align)), TOAST_DISPLAY_TIME, false); + return true; + } + + case ACTION_VOLAMP_UP: + case ACTION_VOLAMP_DOWN: + { + // Don't allow change with passthrough audio + if (appPlayer->IsPassthrough()) + { + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, + g_localizeStrings.Get(660), + g_localizeStrings.Get(29802), + TOAST_DISPLAY_TIME, false); + return false; + } + + float sliderMax = VOLUME_DRC_MAXIMUM / 100.0f; + float sliderMin = VOLUME_DRC_MINIMUM / 100.0f; + + CVideoSettings vs = appPlayer->GetVideoSettings(); + if (action.GetID() == ACTION_VOLAMP_UP) + vs.m_VolumeAmplification += 1.0f; + else + vs.m_VolumeAmplification -= 1.0f; + + vs.m_VolumeAmplification = + std::max(std::min(vs.m_VolumeAmplification, sliderMax), sliderMin); + + appPlayer->SetDynamicRangeCompression((long)(vs.m_VolumeAmplification * 100)); + + ShowSlider(action.GetID(), 660, vs.m_VolumeAmplification, sliderMin, 1.0f, sliderMax); + return true; + } + + case ACTION_VOLAMP: + { + float sliderMax = VOLUME_DRC_MAXIMUM / 100.0f; + float sliderMin = VOLUME_DRC_MINIMUM / 100.0f; + ShowSlider(action.GetID(), 660, appPlayer->GetVideoSettings().m_VolumeAmplification, + sliderMin, 1.0f, sliderMax, true); + return true; + } + + case ACTION_PLAYER_PROGRAM_SELECT: + { + std::vector<ProgramInfo> programs; + appPlayer->GetPrograms(programs); + CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (dialog) + { + int playing = 0; + int idx = 0; + for (const auto& prog : programs) + { + dialog->Add(prog.name); + if (prog.playing) + playing = idx; + idx++; + } + dialog->SetHeading(CVariant{g_localizeStrings.Get(39109)}); + dialog->SetSelected(playing); + dialog->Open(); + idx = dialog->GetSelectedItem(); + if (idx > 0) + appPlayer->SetProgram(programs[idx].id); + } + return true; + } + + case ACTION_PLAYER_RESOLUTION_SELECT: + { + std::vector<CVariant> indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(CSettings::SETTING_VIDEOSCREEN_WHITELIST); + + CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (dialog) + { + int current = 0; + int idx = 0; + auto currentRes = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + for (const CVariant &mode : indexList) + { + auto res = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res); + dialog->Add(info.strMode); + if (res == currentRes) + current = idx; + idx++; + } + dialog->SetHeading(CVariant{g_localizeStrings.Get(39110)}); + dialog->SetSelected(current); + dialog->Open(); + idx = dialog->GetSelectedItem(); + if (idx >= 0) + { + auto res = CDisplaySettings::GetInstance().GetResFromString(indexList[idx].asString()); + CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(res, false); + } + } + return true; + } + + default: + break; + } + } + return false; +} + +void CPlayerController::ShowSlider(int action, int label, float value, float min, float delta, float max, bool modal) +{ + m_sliderAction = action; + if (modal) + CGUIDialogSlider::ShowAndGetInput(g_localizeStrings.Get(label), value, min, delta, max, this); + else + CGUIDialogSlider::Display(label, value, min, delta, max, this); +} + +void CPlayerController::OnSliderChange(void *data, CGUISliderControl *slider) +{ + if (!slider) + return; + + if (m_sliderAction == ACTION_ZOOM_OUT || m_sliderAction == ACTION_ZOOM_IN || + m_sliderAction == ACTION_INCREASE_PAR || m_sliderAction == ACTION_DECREASE_PAR || + m_sliderAction == ACTION_VSHIFT_UP || m_sliderAction == ACTION_VSHIFT_DOWN) + { + std::string strValue = StringUtils::Format("{:1.2f}", slider->GetFloatValue()); + slider->SetTextValue(strValue); + } + else if (m_sliderAction == ACTION_SUBTITLE_VSHIFT_UP || + m_sliderAction == ACTION_SUBTITLE_VSHIFT_DOWN) + { + std::string strValue = StringUtils::Format("{:.0f}px", slider->GetFloatValue()); + slider->SetTextValue(strValue); + } + else if (m_sliderAction == ACTION_VOLAMP_UP || + m_sliderAction == ACTION_VOLAMP_DOWN || + m_sliderAction == ACTION_VOLAMP) + slider->SetTextValue(CGUIDialogAudioSettings::FormatDecibel(slider->GetFloatValue())); + else + slider->SetTextValue( + CGUIDialogAudioSettings::FormatDelay(slider->GetFloatValue(), AUDIO_DELAY_STEP)); + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + if (appPlayer->HasPlayer()) + { + if (m_sliderAction == ACTION_AUDIO_DELAY) + { + appPlayer->SetAVDelay(slider->GetFloatValue()); + } + else if (m_sliderAction == ACTION_SUBTITLE_DELAY) + { + appPlayer->SetSubTitleDelay(slider->GetFloatValue()); + } + else if (m_sliderAction == ACTION_VOLAMP) + { + appPlayer->SetDynamicRangeCompression((long)(slider->GetFloatValue() * 100)); + } + } +} diff --git a/xbmc/video/PlayerController.h b/xbmc/video/PlayerController.h new file mode 100644 index 0000000..688c46c --- /dev/null +++ b/xbmc/video/PlayerController.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012-2018 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. + */ + +#pragma once + +#include "guilib/ISliderCallback.h" +#include "input/Key.h" +#include "interfaces/IActionListener.h" +#include "utils/MovingSpeed.h" + +/*! \brief Player controller class to handle user actions. + + Handles actions that are normally suited to fullscreen playback, such as + altering subtitles and audio tracks, changing aspect ratio, subtitle placement, + and placement of the video on screen. + */ +class CPlayerController : public ISliderCallback, public IActionListener +{ +public: + static CPlayerController& GetInstance(); + + /*! \brief Perform a player control action if appropriate. + \param action the action to perform. + \return true if the action is considered handled, false if it should be handled elsewhere. + */ + bool OnAction(const CAction &action) override; + + /*! \brief Callback from the slider dialog. + \sa CGUIDialogSlider + */ + void OnSliderChange(void *data, CGUISliderControl *slider) override; + +protected: + CPlayerController(); + CPlayerController(const CPlayerController&) = delete; + CPlayerController& operator=(CPlayerController const&) = delete; + ~CPlayerController() override; + +private: + /*! \brief pop up a slider dialog for a particular action + \param action id of the action the slider responds to + \param label id of the label to display + \param value value to set on the slider + \param min minimum value the slider may take + \param delta change value to advance the slider by with each click + \param max maximal value the slider may take + \param modal true if we should wait for the slider to finish. Defaults to false + */ + void ShowSlider(int action, int label, float value, float min, float delta, float max, bool modal = false); + + int m_sliderAction = 0; ///< \brief set to the action id for a slider being displayed \sa ShowSlider + UTILS::MOVING_SPEED::CMovingSpeed m_movingSpeed; +}; diff --git a/xbmc/video/Teletext.cpp b/xbmc/video/Teletext.cpp new file mode 100644 index 0000000..3cbff16 --- /dev/null +++ b/xbmc/video/Teletext.cpp @@ -0,0 +1,4163 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +/* + * Most Codeparts are taken from the TuxBox Teletext plugin which is based + * upon videotext-0.6.19991029 and written by Thomas Loewe (LazyT), + * Roland Meier and DBLuelle. See http://www.tuxtxt.net/ for more information. + * Many thanks to the TuxBox Teletext Team for this great work. + */ + +#include "Teletext.h" + +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "filesystem/SpecialProtocol.h" +#include "input/Key.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include <harfbuzz/hb-ft.h> + +using namespace std::chrono_literals; + +static inline void SDL_memset4(uint32_t* dst, uint32_t val, size_t len) +{ + for (; len > 0; --len) + *dst++ = val; +} +#define SDL_memcpy4(dst, src, len) memcpy(dst, src, (len) << 2) + +static const char *TeletextFont = "special://xbmc/media/Fonts/teletext.ttf"; + +/* spacing attributes */ +#define alpha_black 0x00 +#define alpha_red 0x01 +#define alpha_green 0x02 +#define alpha_yellow 0x03 +#define alpha_blue 0x04 +#define alpha_magenta 0x05 +#define alpha_cyan 0x06 +#define alpha_white 0x07 +#define flash 0x08 +#define steady 0x09 +#define end_box 0x0A +#define start_box 0x0B +#define normal_size 0x0C +#define double_height 0x0D +#define double_width 0x0E +#define double_size 0x0F +#define mosaic_black 0x10 +#define mosaic_red 0x11 +#define mosaic_green 0x12 +#define mosaic_yellow 0x13 +#define mosaic_blue 0x14 +#define mosaic_magenta 0x15 +#define mosaic_cyan 0x16 +#define mosaic_white 0x17 +#define conceal 0x18 +#define contiguous_mosaic 0x19 +#define separated_mosaic 0x1A +#define esc 0x1B +#define black_background 0x1C +#define new_background 0x1D +#define hold_mosaic 0x1E +#define release_mosaic 0x1F + +#define RowAddress2Row(row) ((row == 40) ? 24 : (row - 40)) + +// G2 Set as defined in ETS 300 706 +const unsigned short int G2table[5][6*16] = +{ + // Latin G2 Supplementary Set + { 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x0024, 0x00A5, 0x0023, 0x00A7, 0x00A4, 0x2018, 0x201C, 0x00AB, 0x2190, 0x2191, 0x2192, 0x2193, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00D7, 0x00B5, 0x00B6, 0x00B7, 0x00F7, 0x2019, 0x201D, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x0020, 0x0300, 0x0301, 0x02C6, 0x0303, 0x02C9, 0x02D8, 0x02D9, 0x00A8, 0x002E, 0x02DA, 0x00B8, 0x005F, 0x02DD, 0x02DB, 0x02C7, + 0x2014, 0x00B9, 0x00AE, 0x00A9, 0x2122, 0x266A, 0x20AC, 0x2030, 0x03B1, 0x0020, 0x0020, 0x0020, 0x215B, 0x215C, 0x215D, 0x215E, + 0x2126, 0x00C6, 0x00D0, 0x00AA, 0x0126, 0x0020, 0x0132, 0x013F, 0x0141, 0x00D8, 0x0152, 0x00BA, 0x00DE, 0x0166, 0x014A, 0x0149, + 0x0138, 0x00E6, 0x0111, 0x00F0, 0x0127, 0x0131, 0x0133, 0x0140, 0x0142, 0x00F8, 0x0153, 0x00DF, 0x00FE, 0x0167, 0x014B, 0x25A0}, + // Cyrillic G2 Supplementary Set + { 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x0024, 0x00A5, 0x0020, 0x00A7, 0x0020, 0x2018, 0x201C, 0x00AB, 0x2190, 0x2191, 0x2192, 0x2193, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00D7, 0x00B5, 0x00B6, 0x00B7, 0x00F7, 0x2019, 0x201D, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x0020, 0x0300, 0x0301, 0x02C6, 0x02DC, 0x02C9, 0x02D8, 0x02D9, 0x00A8, 0x002E, 0x02DA, 0x00B8, 0x005F, 0x02DD, 0x02DB, 0x02C7, + 0x2014, 0x00B9, 0x00AE, 0x00A9, 0x2122, 0x266A, 0x20AC, 0x2030, 0x03B1, 0x0141, 0x0142, 0x00DF, 0x215B, 0x215C, 0x215D, 0x215E, + 0x0044, 0x0045, 0x0046, 0x0047, 0x0049, 0x004A, 0x004B, 0x004C, 0x004E, 0x0051, 0x0052, 0x0053, 0x0055, 0x0056, 0x0057, 0x005A, + 0x0064, 0x0065, 0x0066, 0x0067, 0x0069, 0x006A, 0x006B, 0x006C, 0x006E, 0x0071, 0x0072, 0x0073, 0x0075, 0x0076, 0x0077, 0x007A}, + // Greek G2 Supplementary Set + { 0x0020, 0x0061, 0x0062, 0x00A3, 0x0065, 0x0068, 0x0069, 0x00A7, 0x003A, 0x2018, 0x201C, 0x006B, 0x2190, 0x2191, 0x2192, 0x2193, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00D7, 0x006D, 0x006E, 0x0070, 0x00F7, 0x2019, 0x201D, 0x0074, 0x00BC, 0x00BD, 0x00BE, 0x0078, + 0x0020, 0x0300, 0x0301, 0x02C6, 0x02DC, 0x02C9, 0x02D8, 0x02D9, 0x00A8, 0x002E, 0x02DA, 0x00B8, 0x005F, 0x02DD, 0x02DB, 0x02C7, + 0x003F, 0x00B9, 0x00AE, 0x00A9, 0x2122, 0x266A, 0x20AC, 0x2030, 0x03B1, 0x038A, 0x038E, 0x038F, 0x215B, 0x215C, 0x215D, 0x215E, + 0x0043, 0x0044, 0x0046, 0x0047, 0x004A, 0x004C, 0x0051, 0x0052, 0x0053, 0x0055, 0x0056, 0x0057, 0x0059, 0x005A, 0x0386, 0x0389, + 0x0063, 0x0064, 0x0066, 0x0067, 0x006A, 0x006C, 0x0071, 0x0072, 0x0073, 0x0075, 0x0076, 0x0077, 0x0079, 0x007A, 0x0388, 0x25A0}, + // Arabic G2 Set + { 0x0020, 0x0639, 0xFEC9, 0xFE83, 0xFE85, 0xFE87, 0xFE8B, 0xFE89, 0xFB7C, 0xFB7D, 0xFB7A, 0xFB58, 0xFB59, 0xFB56, 0xFB6D, 0xFB8E, + 0x0660, 0x0661, 0x0662, 0x0663, 0x0664, 0x0665, 0x0666, 0x0667, 0x0668, 0x0669, 0xFECE, 0xFECD, 0xFEFC, 0xFEEC, 0xFEEA, 0xFEE9, + 0x00E0, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, + 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x00EB, 0x00EA, 0x00F9, 0x00EE, 0xFECA, + 0x00E9, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, + 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007A, 0x00E2, 0x00F4, 0x00FB, 0x00E7, 0x25A0} +}; + +//const (avoid warnings :<) +TextPageAttr_t Text_AtrTable[] = +{ + { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_WB */ + { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 1 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_PassiveDefault */ + { TXT_ColorWhite , TXT_ColorRed , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L250 */ + { TXT_ColorBlack , TXT_ColorGreen , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L251 */ + { TXT_ColorBlack , TXT_ColorYellow, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L252 */ + { TXT_ColorWhite , TXT_ColorBlue , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L253 */ + { TXT_ColorMagenta, TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU0 */ + { TXT_ColorGreen , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU1 */ + { TXT_ColorYellow , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU2 */ + { TXT_ColorCyan , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU3 */ + { TXT_ColorMenu2 , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG0 */ + { TXT_ColorYellow , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG1 */ + { TXT_ColorMenu2 , TXT_ColorTransp, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG2 */ + { TXT_ColorWhite , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG3 */ + { TXT_ColorMenu2 , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM0 */ + { TXT_ColorYellow , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM1 */ + { TXT_ColorMenu2 , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM2 */ + { TXT_ColorWhite , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM3 */ + { TXT_ColorMenu1 , TXT_ColorBlue , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENUHIL0 5a Z */ + { TXT_ColorWhite , TXT_ColorBlue , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENUHIL1 58 X */ + { TXT_ColorMenu2 , TXT_ColorTransp, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENUHIL2 9b õ */ + { TXT_ColorMenu2 , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU0 ab ´ */ + { TXT_ColorYellow , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU1 a4 § */ + { TXT_ColorMenu2 , TXT_ColorTransp, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU2 9b õ */ + { TXT_ColorMenu2 , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU3 cb À */ + { TXT_ColorCyan , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU4 c7 « */ + { TXT_ColorWhite , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU5 c8 » */ + { TXT_ColorWhite , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU6 a8 ® */ + { TXT_ColorYellow , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_CATCHMENU0 a4 § */ + { TXT_ColorWhite , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f} /* ATR_CATCHMENU1 a8 ® */ +}; + +/* shapes */ +enum +{ + S_END = 0, + S_FHL, /* full horizontal line: y-offset */ + S_FVL, /* full vertical line: x-offset */ + S_BOX, /* rectangle: x-offset, y-offset, width, height */ + S_TRA, /* trapez: x0, y0, l0, x1, y1, l1 */ + S_BTR, /* trapez in bgcolor: x0, y0, l0, x1, y1, l1 */ + S_INV, /* invert */ + S_LNK, /* call other shape: shapenumber */ + S_CHR, /* Character from freetype hibyte, lowbyte */ + S_ADT, /* Character 2F alternating raster */ + S_FLH, /* flip horizontal */ + S_FLV /* flip vertical */ +}; + +/* shape coordinates */ +enum +{ + S_W13 = 5, /* width*1/3 */ + S_W12, /* width*1/2 */ + S_W23, /* width*2/3 */ + S_W11, /* width */ + S_WM3, /* width-3 */ + S_H13, /* height*1/3 */ + S_H12, /* height*1/2 */ + S_H23, /* height*2/3 */ + S_H11, /* height */ + S_NrShCoord +}; + +/* G3 characters */ +unsigned char aG3_20[] = { S_TRA, 0, S_H23, 1, 0, S_H11, S_W12, S_END }; +unsigned char aG3_21[] = { S_TRA, 0, S_H23, 1, 0, S_H11, S_W11, S_END }; +unsigned char aG3_22[] = { S_TRA, 0, S_H12, 1, 0, S_H11, S_W12, S_END }; +unsigned char aG3_23[] = { S_TRA, 0, S_H12, 1, 0, S_H11, S_W11, S_END }; +unsigned char aG3_24[] = { S_TRA, 0, 0, 1, 0, S_H11, S_W12, S_END }; +unsigned char aG3_25[] = { S_TRA, 0, 0, 1, 0, S_H11, S_W11, S_END }; +unsigned char aG3_26[] = { S_INV, S_LNK, 0x66, S_END }; +unsigned char aG3_27[] = { S_INV, S_LNK, 0x67, S_END }; +unsigned char aG3_28[] = { S_INV, S_LNK, 0x68, S_END }; +unsigned char aG3_29[] = { S_INV, S_LNK, 0x69, S_END }; +unsigned char aG3_2a[] = { S_INV, S_LNK, 0x6a, S_END }; +unsigned char aG3_2b[] = { S_INV, S_LNK, 0x6b, S_END }; +unsigned char aG3_2c[] = { S_INV, S_LNK, 0x6c, S_END }; +unsigned char aG3_2d[] = { S_INV, S_LNK, 0x6d, S_END }; +unsigned char aG3_2e[] = { S_BOX, 2, 0, 3, S_H11, S_END }; +unsigned char aG3_2f[] = { S_ADT }; +unsigned char aG3_30[] = { S_LNK, 0x20, S_FLH, S_END }; +unsigned char aG3_31[] = { S_LNK, 0x21, S_FLH, S_END }; +unsigned char aG3_32[] = { S_LNK, 0x22, S_FLH, S_END }; +unsigned char aG3_33[] = { S_LNK, 0x23, S_FLH, S_END }; +unsigned char aG3_34[] = { S_LNK, 0x24, S_FLH, S_END }; +unsigned char aG3_35[] = { S_LNK, 0x25, S_FLH, S_END }; +unsigned char aG3_36[] = { S_INV, S_LNK, 0x76, S_END }; +unsigned char aG3_37[] = { S_INV, S_LNK, 0x77, S_END }; +unsigned char aG3_38[] = { S_INV, S_LNK, 0x78, S_END }; +unsigned char aG3_39[] = { S_INV, S_LNK, 0x79, S_END }; +unsigned char aG3_3a[] = { S_INV, S_LNK, 0x7a, S_END }; +unsigned char aG3_3b[] = { S_INV, S_LNK, 0x7b, S_END }; +unsigned char aG3_3c[] = { S_INV, S_LNK, 0x7c, S_END }; +unsigned char aG3_3d[] = { S_INV, S_LNK, 0x7d, S_END }; +unsigned char aG3_3e[] = { S_LNK, 0x2e, S_FLH, S_END }; +unsigned char aG3_3f[] = { S_BOX, 0, 0, S_W11, S_H11, S_END }; +unsigned char aG3_40[] = { S_BOX, 0, S_H13, S_W11, S_H13, S_LNK, 0x7e, S_END }; +unsigned char aG3_41[] = { S_BOX, 0, S_H13, S_W11, S_H13, S_LNK, 0x7e, S_FLV, S_END }; +unsigned char aG3_42[] = { S_LNK, 0x50, S_BOX, S_W12, S_H13, S_W12, S_H13, S_END }; +unsigned char aG3_43[] = { S_LNK, 0x50, S_BOX, 0, S_H13, S_W12, S_H13, S_END }; +unsigned char aG3_44[] = { S_LNK, 0x48, S_FLV, S_LNK, 0x48, S_END }; +unsigned char aG3_45[] = { S_LNK, 0x44, S_FLH, S_END }; +unsigned char aG3_46[] = { S_LNK, 0x47, S_FLV, S_END }; +unsigned char aG3_47[] = { S_LNK, 0x48, S_FLH, S_LNK, 0x48, S_END }; +unsigned char aG3_48[] = { S_TRA, 0, 0, S_W23, 0, S_H23, 0, S_BTR, 0, 0, S_W13, 0, S_H13, 0, S_END }; +unsigned char aG3_49[] = { S_LNK, 0x48, S_FLH, S_END }; +unsigned char aG3_4a[] = { S_LNK, 0x48, S_FLV, S_END }; +unsigned char aG3_4b[] = { S_LNK, 0x48, S_FLH, S_FLV, S_END }; +unsigned char aG3_4c[] = { S_LNK, 0x50, S_BOX, 0, S_H13, S_W11, S_H13, S_END }; +unsigned char aG3_4d[] = { S_CHR, 0x25, 0xE6 }; +unsigned char aG3_4e[] = { S_CHR, 0x25, 0xCF }; +unsigned char aG3_4f[] = { S_CHR, 0x25, 0xCB }; +unsigned char aG3_50[] = { S_BOX, S_W12, 0, 2, S_H11, S_FLH, S_BOX, S_W12, 0, 2, S_H11,S_END }; +unsigned char aG3_51[] = { S_BOX, 0, S_H12, S_W11, 2, S_FLV, S_BOX, 0, S_H12, S_W11, 2,S_END }; +unsigned char aG3_52[] = { S_LNK, 0x55, S_FLH, S_FLV, S_END }; +unsigned char aG3_53[] = { S_LNK, 0x55, S_FLV, S_END }; +unsigned char aG3_54[] = { S_LNK, 0x55, S_FLH, S_END }; +unsigned char aG3_55[] = { S_LNK, 0x7e, S_FLV, S_BOX, 0, S_H12, S_W12, 2, S_FLV, S_BOX, 0, S_H12, S_W12, 2, S_END }; +unsigned char aG3_56[] = { S_LNK, 0x57, S_FLH, S_END}; +unsigned char aG3_57[] = { S_LNK, 0x55, S_LNK, 0x50 , S_END}; +unsigned char aG3_58[] = { S_LNK, 0x59, S_FLV, S_END}; +unsigned char aG3_59[] = { S_LNK, 0x7e, S_LNK, 0x51 , S_END}; +unsigned char aG3_5a[] = { S_LNK, 0x50, S_LNK, 0x51 , S_END}; +unsigned char aG3_5b[] = { S_CHR, 0x21, 0x92}; +unsigned char aG3_5c[] = { S_CHR, 0x21, 0x90}; +unsigned char aG3_5d[] = { S_CHR, 0x21, 0x91}; +unsigned char aG3_5e[] = { S_CHR, 0x21, 0x93}; +unsigned char aG3_5f[] = { S_CHR, 0x00, 0x20}; +unsigned char aG3_60[] = { S_INV, S_LNK, 0x20, S_END }; +unsigned char aG3_61[] = { S_INV, S_LNK, 0x21, S_END }; +unsigned char aG3_62[] = { S_INV, S_LNK, 0x22, S_END }; +unsigned char aG3_63[] = { S_INV, S_LNK, 0x23, S_END }; +unsigned char aG3_64[] = { S_INV, S_LNK, 0x24, S_END }; +unsigned char aG3_65[] = { S_INV, S_LNK, 0x25, S_END }; +unsigned char aG3_66[] = { S_LNK, 0x20, S_FLV, S_END }; +unsigned char aG3_67[] = { S_LNK, 0x21, S_FLV, S_END }; +unsigned char aG3_68[] = { S_LNK, 0x22, S_FLV, S_END }; +unsigned char aG3_69[] = { S_LNK, 0x23, S_FLV, S_END }; +unsigned char aG3_6a[] = { S_LNK, 0x24, S_FLV, S_END }; +unsigned char aG3_6b[] = { S_BOX, 0, 0, S_W11, S_H13, S_TRA, 0, S_H13, S_W11, 0, S_H23, 1, S_END }; +unsigned char aG3_6c[] = { S_TRA, 0, 0, 1, 0, S_H12, S_W12, S_FLV, S_TRA, 0, 0, 1, 0, S_H12, S_W12, S_BOX, 0, S_H12, S_W12,1, S_END }; +unsigned char aG3_6d[] = { S_TRA, 0, 0, S_W12, S_W12, S_H12, 0, S_FLH, S_TRA, 0, 0, S_W12, S_W12, S_H12, 0, S_END }; +unsigned char aG3_6e[] = { S_CHR, 0x00, 0x20}; +unsigned char aG3_6f[] = { S_CHR, 0x00, 0x20}; +unsigned char aG3_70[] = { S_INV, S_LNK, 0x30, S_END }; +unsigned char aG3_71[] = { S_INV, S_LNK, 0x31, S_END }; +unsigned char aG3_72[] = { S_INV, S_LNK, 0x32, S_END }; +unsigned char aG3_73[] = { S_INV, S_LNK, 0x33, S_END }; +unsigned char aG3_74[] = { S_INV, S_LNK, 0x34, S_END }; +unsigned char aG3_75[] = { S_INV, S_LNK, 0x35, S_END }; +unsigned char aG3_76[] = { S_LNK, 0x66, S_FLH, S_END }; +unsigned char aG3_77[] = { S_LNK, 0x67, S_FLH, S_END }; +unsigned char aG3_78[] = { S_LNK, 0x68, S_FLH, S_END }; +unsigned char aG3_79[] = { S_LNK, 0x69, S_FLH, S_END }; +unsigned char aG3_7a[] = { S_LNK, 0x6a, S_FLH, S_END }; +unsigned char aG3_7b[] = { S_LNK, 0x6b, S_FLH, S_END }; +unsigned char aG3_7c[] = { S_LNK, 0x6c, S_FLH, S_END }; +unsigned char aG3_7d[] = { S_LNK, 0x6d, S_FLV, S_END }; +unsigned char aG3_7e[] = { S_BOX, S_W12, 0, 2, S_H12, S_FLH, S_BOX, S_W12, 0, 2, S_H12, S_END };// help char, not printed directly (only by S_LNK) + +unsigned char *aShapes[] = +{ + aG3_20, aG3_21, aG3_22, aG3_23, aG3_24, aG3_25, aG3_26, aG3_27, aG3_28, aG3_29, aG3_2a, aG3_2b, aG3_2c, aG3_2d, aG3_2e, aG3_2f, + aG3_30, aG3_31, aG3_32, aG3_33, aG3_34, aG3_35, aG3_36, aG3_37, aG3_38, aG3_39, aG3_3a, aG3_3b, aG3_3c, aG3_3d, aG3_3e, aG3_3f, + aG3_40, aG3_41, aG3_42, aG3_43, aG3_44, aG3_45, aG3_46, aG3_47, aG3_48, aG3_49, aG3_4a, aG3_4b, aG3_4c, aG3_4d, aG3_4e, aG3_4f, + aG3_50, aG3_51, aG3_52, aG3_53, aG3_54, aG3_55, aG3_56, aG3_57, aG3_58, aG3_59, aG3_5a, aG3_5b, aG3_5c, aG3_5d, aG3_5e, aG3_5f, + aG3_60, aG3_61, aG3_62, aG3_63, aG3_64, aG3_65, aG3_66, aG3_67, aG3_68, aG3_69, aG3_6a, aG3_6b, aG3_6c, aG3_6d, aG3_6e, aG3_6f, + aG3_70, aG3_71, aG3_72, aG3_73, aG3_74, aG3_75, aG3_76, aG3_77, aG3_78, aG3_79, aG3_7a, aG3_7b, aG3_7c, aG3_7d, aG3_7e +}; + +// G0 Table as defined in ETS 300 706 +// cyrillic G0 Charset (0 = Serbian/Croatian, 1 = Russian/Bulgarian, 2 = Ukrainian) +const unsigned short int G0table[6][6*16] = +{ + // Cyrillic G0 Set - Option 1 - Serbian/Croatian + { ' ', '!', '\"', '#', '$', '%', '&', '\'', '(' , ')' , '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + 0x0427, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0408, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, + 0x041F, 0x040C, 0x0420, 0x0421, 0x0422, 0x0423, 0x0412, 0x0403, 0x0409, 0x040A, 0x0417, 0x040B, 0x0416, 0x0402, 0x0428, 0x040F, + 0x0447, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0458, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, + 0x043F, 0x045C, 0x0440, 0x0441, 0x0442, 0x0443, 0x0432, 0x0453, 0x0459, 0x045A, 0x0437, 0x045B, 0x0436, 0x0452, 0x0448, 0x25A0}, + // Cyrillic G0 Set - Option 2 - Russian/Bulgarian + { ' ', '!', '\"', '#', '$', '%', 0x044B, '\'', '(' , ')' , '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, + 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x042A, 0x0417, 0x0428, 0x042D, 0x0429, 0x0427, 0x042B, + 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, + 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x044A, 0x0437, 0x0448, 0x044D, 0x0449, 0x0447, 0x25A0}, + // Cyrillic G0 Set - Option 3 - Ukrainian + { ' ', '!', '\"', '#', '$', '%', 0x0457, '\'', '(' , ')' , '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, + 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x0406, 0x0417, 0x0428, 0x0404, 0x0429, 0x0427, 0x0407, + 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, + 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x0456, 0x0437, 0x0448, 0x0454, 0x0449, 0x0447, 0x25A0}, + // Greek G0 Set + { ' ', '!', '\"', '#', '$', '%', '&', '\'', '(' , ')' , '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', 0x00AB, '=', 0x00BB, '?', + 0x0390, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, + 0x03A0, 0x03A1, 0x0384, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03AA, 0x03AB, 0x03AC, 0x03AD, 0x03AE, 0x03AF, + 0x03B0, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, + 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03CC, 0x03CD, 0x03CE, 0x25A0}, + // Hebrew G0 Set + { ' ', '!', 0x05F2, 0x00A3, '$', '%', '&', '\'', '(' , ')' , '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 0x2190, 0x00BD, 0x2192, 0x2191, '#', + 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF, + 0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0x20AA, 0x2551, 0x00BE, 0x00F7, 0x25A0}, + // Arabic G0 Set - Thanks to Habib2006(fannansat) + { ' ', '!', 0x05F2, 0x00A3, '$', 0x066A, 0xFEF0, 0xFEF2, 0xFD3F, 0xFD3E, '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', 0x061B, '>', '=', '<', 0x061F, + 0xFE94, 0x0621, 0xFE92, 0x0628, 0xFE98, 0x062A, 0xFE8E, 0xFE8D, 0xFE91, 0xFE93, 0xFE97, 0xFE9B, 0xFE9F, 0xFEA3, 0xFEA7, 0xFEA9, + 0x0630, 0xFEAD, 0xFEAF, 0xFEB3, 0xFEB7, 0xFEBB, 0xFEBF, 0xFEC1, 0xFEC5, 0xFECB, 0xFECF, 0xFE9C, 0xFEA0, 0xFEA4, 0xFEA8, 0x0023, + 0x0640, 0xFED3, 0xFED7, 0xFEDB, 0xFEDF, 0xFEE3, 0xFEE7, 0xFEEB, 0xFEED, 0xFEEF, 0xFEF3, 0xFE99, 0xFE9D, 0xFEA1, 0xFEA5, 0xFEF4, + 0xFEF0, 0xFECC, 0xFED0, 0xFED4, 0xFED1, 0xFED8, 0xFED5, 0xFED9, 0xFEE0, 0xFEDD, 0xFEE4, 0xFEE1, 0xFEE8, 0xFEE5, 0xFEFB, 0x25A0} +}; + +const unsigned short int nationaltable23[14][2] = +{ + { '#', 0x00A4 }, /* 0 */ + { '#', 0x016F }, /* 1 CS/SK */ + { 0x00A3, '$' }, /* 2 EN */ + { '#', 0x00F5 }, /* 3 ET */ + { 0x00E9, 0x0457 }, /* 4 FR */ + { '#', '$' }, /* 5 DE */ + { 0x00A3, '$' }, /* 6 IT */ + { '#', '$' }, /* 7 LV/LT */ + { '#', 0x0144 }, /* 8 PL */ + { 0x00E7, '$' }, /* 9 PT/ES */ + { '#', 0x00A4 }, /* A RO */ + { '#', 0x00CB }, /* B SR/HR/SL */ + { '#', 0x00A4 }, /* C SV/FI/HU */ + { 0x20A4, 0x011F }, /* D TR */ +}; +const unsigned short int nationaltable40[14] = +{ + '@', /* 0 */ + 0x010D, /* 1 CS/SK */ + '@', /* 2 EN */ + 0x0161, /* 3 ET */ + 0x00E0, /* 4 FR */ + 0x00A7, /* 5 DE */ + 0x00E9, /* 6 IT */ + 0x0161, /* 7 LV/LT */ + 0x0105, /* 8 PL */ + 0x00A1, /* 9 PT/ES */ + 0x0162, /* A RO */ + 0x010C, /* B SR/HR/SL */ + 0x00C9, /* C SV/FI/HU */ + 0x0130, /* D TR */ +}; +const unsigned short int nationaltable5b[14][6] = +{ + { '[', '\\', ']', '^', '_', '`' }, /* 0 */ + { 0x0165, 0x017E, 0x00FD, 0x00ED, 0x0159, 0x00E9 }, /* 1 CS/SK */ + { 0x2190, 0x00BD, 0x2192, 0x2191, '#', 0x00AD }, /* 2 EN */ + { 0x00C4, 0x00D6, 0x017D, 0x00DC, 0x00D5, 0x0161 }, /* 3 ET */ + { 0x0451, 0x00EA, 0x00F9, 0x00EE, '#', 0x00E8 }, /* 4 FR */ + { 0x00C4, 0x00D6, 0x00DC, '^', '_', 0x00B0 }, /* 5 DE */ + { 0x00B0, 0x00E7, 0x2192, 0x2191, '#', 0x00F9 }, /* 6 IT */ + { 0x0117, 0x0119, 0x017D, 0x010D, 0x016B, 0x0161 }, /* 7 LV/LT */ + { 0x017B, 0x015A, 0x0141, 0x0107, 0x00F3, 0x0119 }, /* 8 PL */ + { 0x00E1, 0x00E9, 0x00ED, 0x00F3, 0x00FA, 0x00BF }, /* 9 PT/ES */ + { 0x00C2, 0x015E, 0x01CD, 0x01CF, 0x0131, 0x0163 }, /* A RO */ + { 0x0106, 0x017D, 0x00D0, 0x0160, 0x0451, 0x010D }, /* B SR/HR/SL */ + { 0x00C4, 0x00D6, 0x00C5, 0x00DC, '_', 0x00E9 }, /* C SV/FI/HU */ + { 0x015E, 0x00D6, 0x00C7, 0x00DC, 0x011E, 0x0131 }, /* D TR */ +}; +const unsigned short int nationaltable7b[14][4] = +{ + { '{', '|', '}', '~' }, /* 0 */ + { 0x00E1, 0x011B, 0x00FA, 0x0161 }, /* 1 CS/SK */ + { 0x00BC, 0x2551, 0x00BE, 0x00F7 }, /* 2 EN */ + { 0x00E4, 0x00F6, 0x017E, 0x00FC }, /* 3 ET */ + { 0x00E2, 0x00F4, 0x00FB, 0x00E7 }, /* 4 FR */ + { 0x00E4, 0x00F6, 0x00FC, 0x00DF }, /* 5 DE */ + { 0x00E0, 0x00F3, 0x00E8, 0x00EC }, /* 6 IT */ + { 0x0105, 0x0173, 0x017E, 0x012F }, /* 7 LV/LT */ + { 0x017C, 0x015B, 0x0142, 0x017A }, /* 8 PL */ + { 0x00FC, 0x00F1, 0x00E8, 0x00E0 }, /* 9 PT/ES */ + { 0x00E2, 0x015F, 0x01CE, 0x00EE }, /* A RO */ + { 0x0107, 0x017E, 0x0111, 0x0161 }, /* B SR/HR/SL */ + { 0x00E4, 0x00F6, 0x00E5, 0x00FC }, /* C SV/FI/HU */ + { 0x015F, 0x00F6, 0x00E7, 0x00FC }, /* D TR */ +}; +const unsigned short int arrowtable[] = +{ + 8592, 8594, 8593, 8595, 'O', 'K', 8592, 8592 +}; + +CTeletextDecoder::CTeletextDecoder() +{ + memset(&m_RenderInfo, 0, sizeof(TextRenderInfo_t)); + + m_teletextFont = CSpecialProtocol::TranslatePath(TeletextFont); + m_TextureBuffer = NULL; + m_txtCache = NULL; + m_Manager = NULL; + m_Library = NULL; + m_RenderInfo.ShowFlof = true; + m_RenderInfo.Show39 = false; + m_RenderInfo.Showl25 = true; + m_RenderInfo.Prev_100 = 0x100; + m_RenderInfo.Prev_10 = 0x100; + m_RenderInfo.Next_100 = 0x100; + m_RenderInfo.Next_10 = 0x100; + m_RenderInfo.InputCounter = 2; + + unsigned short rd0[] = {0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0x00<<8, 0x00<<8, 0x00<<8, 0, 0 }; + unsigned short gn0[] = {0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0x20<<8, 0x10<<8, 0x20<<8, 0, 0 }; + unsigned short bl0[] = {0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0x40<<8, 0x20<<8, 0x40<<8, 0, 0 }; + unsigned short tr0[] = {0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF, + 0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF, + 0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF, + 0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF, + 0x0000 , 0x0000 , 0x0A0A , 0xFFFF, 0x3030 }; + + memcpy(m_RenderInfo.rd0,rd0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short)); + memcpy(m_RenderInfo.gn0,gn0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short)); + memcpy(m_RenderInfo.bl0,bl0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short)); + memcpy(m_RenderInfo.tr0,tr0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short)); + + m_LastPage = 0; + m_TempPage = 0; + m_Ascender = 0; + m_PCOldCol = 0; + m_PCOldRow = 0; + m_CatchedPage = 0; + m_CatchCol = 0; + m_CatchRow = 0; + prevTimeSec = 0; + prevHeaderPage = 0; + m_updateTexture = false; + m_YOffset = 0; +} + +CTeletextDecoder::~CTeletextDecoder() = default; + +bool CTeletextDecoder::HandleAction(const CAction &action) +{ + if (m_txtCache == NULL) + { + CLog::Log(LOGERROR, "CTeletextDecoder::HandleAction called without teletext cache"); + return false; + } + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + if (action.GetID() == ACTION_MOVE_UP) + { + if (m_RenderInfo.PageCatching) + CatchNextPage(-1, -1); + else + GetNextPageOne(true); + return true; + } + else if (action.GetID() == ACTION_MOVE_DOWN) + { + if (m_RenderInfo.PageCatching) + CatchNextPage(1, 1); + else + GetNextPageOne(false); + return true; + } + else if (action.GetID() == ACTION_MOVE_RIGHT) + { + if (m_RenderInfo.PageCatching) + CatchNextPage(0, 1); + else if (m_RenderInfo.Boxed) + { + m_RenderInfo.SubtitleDelay++; + // display SubtitleDelay + m_RenderInfo.PosY = 0; + char ns[10]; + SetPosX(1); + sprintf(ns,"+%d ", m_RenderInfo.SubtitleDelay); + RenderCharFB(ns[0], &Text_AtrTable[ATR_WB]); + RenderCharFB(ns[1], &Text_AtrTable[ATR_WB]); + RenderCharFB(ns[2], &Text_AtrTable[ATR_WB]); + RenderCharFB(ns[4], &Text_AtrTable[ATR_WB]); + } + else + { + GetNextSubPage(1); + } + return true; + } + else if (action.GetID() == ACTION_MOVE_LEFT) + { + if (m_RenderInfo.PageCatching) + CatchNextPage(0, -1); + else if (m_RenderInfo.Boxed) + { + m_RenderInfo.SubtitleDelay--; + + // display subtitledelay + m_RenderInfo.PosY = 0; + char ns[10]; + SetPosX(1); + sprintf(ns,"+%d ", m_RenderInfo.SubtitleDelay); + RenderCharFB(ns[0], &Text_AtrTable[ATR_WB]); + RenderCharFB(ns[1], &Text_AtrTable[ATR_WB]); + RenderCharFB(ns[2], &Text_AtrTable[ATR_WB]); + RenderCharFB(ns[4], &Text_AtrTable[ATR_WB]); + } + else + { + GetNextSubPage(-1); + } + return true; + } + else if (action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9) + { + PageInput(action.GetID() - REMOTE_0); + return true; + } + else if (action.GetID() == KEY_UNICODE) + { // input from the keyboard + if (action.GetUnicode() >= 48 && action.GetUnicode() < 58) + { + PageInput(action.GetUnicode() - 48); + return true; + } + return false; + } + else if (action.GetID() == ACTION_PAGE_UP) + { + SwitchZoomMode(); + return true; + } + else if (action.GetID() == ACTION_PAGE_DOWN) + { + SwitchTranspMode(); + return true; + } + else if (action.GetID() == ACTION_SELECT_ITEM) + { + if (m_txtCache->SubPageTable[m_txtCache->Page] == 0xFF) + return false; + + if (!m_RenderInfo.PageCatching) + StartPageCatching(); + else + StopPageCatching(); + + return true; + } + + if (m_RenderInfo.PageCatching) + { + m_txtCache->PageUpdate = true; + m_RenderInfo.PageCatching = false; + return true; + } + + if (action.GetID() == ACTION_SHOW_INFO) + { + SwitchHintMode(); + return true; + } + else if (action.GetID() == ACTION_TELETEXT_RED) + { + ColorKey(m_RenderInfo.Prev_100); + return true; + } + else if (action.GetID() == ACTION_TELETEXT_GREEN) + { + ColorKey(m_RenderInfo.Prev_10); + return true; + } + else if (action.GetID() == ACTION_TELETEXT_YELLOW) + { + ColorKey(m_RenderInfo.Next_10); + return true; + } + else if (action.GetID() == ACTION_TELETEXT_BLUE) + { + ColorKey(m_RenderInfo.Next_100); + return true; + } + + return false; +} + +bool CTeletextDecoder::InitDecoder() +{ + int error; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + m_txtCache = appPlayer->GetTeletextCache(); + if (m_txtCache == nullptr) + { + CLog::Log(LOGERROR, "{}: called without teletext cache", __FUNCTION__); + return false; + } + + /* init fontlibrary */ + if ((error = FT_Init_FreeType(&m_Library))) + { + CLog::Log(LOGERROR, "{}: <FT_Init_FreeType: {:#2X}>", __FUNCTION__, error); + m_Library = NULL; + return false; + } + + if ((error = FTC_Manager_New(m_Library, 7, 2, 0, &MyFaceRequester, NULL, &m_Manager))) + { + FT_Done_FreeType(m_Library); + m_Library = NULL; + m_Manager = NULL; + CLog::Log(LOGERROR, "{}: <FTC_Manager_New: {:#2X}>", __FUNCTION__, error); + return false; + } + + if ((error = FTC_SBitCache_New(m_Manager, &m_Cache))) + { + FTC_Manager_Done(m_Manager); + FT_Done_FreeType(m_Library); + m_Manager = NULL; + m_Library = NULL; + CLog::Log(LOGERROR, "{}: <FTC_SBit_Cache_New: {:#2X}>", __FUNCTION__, error); + return false; + } + + /* calculate font dimensions */ + m_RenderInfo.Width = (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()*CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIScaleX()); + m_RenderInfo.Height = (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()*CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIScaleY()); + m_RenderInfo.FontHeight = m_RenderInfo.Height / 25; + m_RenderInfo.FontWidth_Normal = m_RenderInfo.Width / (m_RenderInfo.Show39 ? 39 : 40); + SetFontWidth(m_RenderInfo.FontWidth_Normal); + for (int i = 0; i <= 10; i++) + m_RenderInfo.axdrcs[i+12+1] = (m_RenderInfo.FontHeight * i + 6) / 10; + + /* center screen */ + m_TypeTTF.face_id = (FTC_FaceID) const_cast<char*>(m_teletextFont.c_str()); + m_TypeTTF.height = (FT_UShort) m_RenderInfo.FontHeight; + m_TypeTTF.flags = FT_LOAD_MONOCHROME; + if (FTC_Manager_LookupFace(m_Manager, m_TypeTTF.face_id, &m_Face)) + { + m_TypeTTF.face_id = (FTC_FaceID) const_cast<char*>(m_teletextFont.c_str()); + if ((error = FTC_Manager_LookupFace(m_Manager, m_TypeTTF.face_id, &m_Face))) + { + CLog::Log(LOGERROR, "{}: <FTC_Manager_Lookup_Face failed with Errorcode {:#2X}>", + __FUNCTION__, error); + FTC_Manager_Done(m_Manager); + FT_Done_FreeType(m_Library); + m_Manager = NULL; + m_Library = NULL; + return false; + } + } + m_Ascender = m_RenderInfo.FontHeight * m_Face->ascender / m_Face->units_per_EM; + + /* set variable screeninfo for double buffering */ + m_YOffset = 0; + m_TextureBuffer = new UTILS::COLOR::Color[4 * m_RenderInfo.Height * m_RenderInfo.Width]; + + ClearFB(GetColorRGB(TXT_ColorTransp)); + ClearBB(GetColorRGB(TXT_ColorTransp)); /* initialize backbuffer */ + /* set new colormap */ + SetColors(DefaultColors, 0, TXT_Color_SIZECOLTABLE); + + for (int i = 0; i < 40 * 25; i++) + { + m_RenderInfo.PageChar[i] = ' '; + m_RenderInfo.PageAtrb[i].fg = TXT_ColorTransp; + m_RenderInfo.PageAtrb[i].bg = TXT_ColorTransp; + m_RenderInfo.PageAtrb[i].charset = C_G0P; + m_RenderInfo.PageAtrb[i].doubleh = 0; + m_RenderInfo.PageAtrb[i].doublew = 0; + m_RenderInfo.PageAtrb[i].IgnoreAtBlackBgSubst = 0; + } + + m_RenderInfo.TranspMode = false; + m_LastPage = 0x100; + + return true; +} + +void CTeletextDecoder::EndDecoder() +{ + /* clear SubtitleCache */ + for (TextSubtitleCache_t*& subtitleCache : m_RenderInfo.SubtitleCache) + { + if (subtitleCache != NULL) + { + delete subtitleCache; + subtitleCache = NULL; + } + } + + if (m_TextureBuffer) + { + delete[] m_TextureBuffer; + m_TextureBuffer = NULL; + } + + /* close freetype */ + if (m_Manager) + { + FTC_Node_Unref(m_anode, m_Manager); + FTC_Manager_Done(m_Manager); + } + if (m_Library) + { + FT_Done_FreeType(m_Library); + } + + m_Manager = NULL; + m_Library = NULL; + + if (!m_txtCache) + { + CLog::Log(LOGINFO, "{}: called without cache", __FUNCTION__); + } + else + { + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + m_txtCache->PageUpdate = true; + CLog::Log(LOGDEBUG, "Teletext: Rendering ended"); + } +} + +void CTeletextDecoder::PageInput(int Number) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + m_updateTexture = true; + + /* clear m_TempPage */ + if (m_RenderInfo.InputCounter == 2) + m_TempPage = 0; + + /* check for 0 & 9 on first position */ + if (Number == 0 && m_RenderInfo.InputCounter == 2) + { + /* set page */ + m_TempPage = m_LastPage; /* 0 toggles to last page as in program switching */ + m_RenderInfo.InputCounter = -1; + } + else if (Number == 9 && m_RenderInfo.InputCounter == 2) + { + return; + } + + /* show pageinput */ + if (m_RenderInfo.ZoomMode == 2) + { + m_RenderInfo.ZoomMode = 1; + CopyBB2FB(); + } + + m_RenderInfo.PosY = 0; + + switch (m_RenderInfo.InputCounter) + { + case 2: + SetPosX(1); + RenderCharFB(Number | '0', &Text_AtrTable[ATR_WB]); + RenderCharFB('-', &Text_AtrTable[ATR_WB]); + RenderCharFB('-', &Text_AtrTable[ATR_WB]); + break; + + case 1: + SetPosX(2); + RenderCharFB(Number | '0', &Text_AtrTable[ATR_WB]); + break; + + case 0: + SetPosX(3); + RenderCharFB(Number | '0', &Text_AtrTable[ATR_WB]); + break; + } + + /* generate pagenumber */ + m_TempPage |= Number << (m_RenderInfo.InputCounter*4); + + m_RenderInfo.InputCounter--; + + if (m_RenderInfo.InputCounter < 0) + { + /* disable SubPage zapping */ + m_txtCache->ZapSubpageManual = false; + + /* reset input */ + m_RenderInfo.InputCounter = 2; + + /* set new page */ + m_LastPage = m_txtCache->Page; + + m_txtCache->Page = m_TempPage; + m_RenderInfo.HintMode = false; + + /* check cache */ + int subp = m_txtCache->SubPageTable[m_txtCache->Page]; + if (subp != 0xFF) + { + m_txtCache->SubPage = subp; + m_txtCache->PageUpdate = true; + } + else + { + m_txtCache->SubPage = 0; +// RenderMessage(PageNotFound); + } + } +} + +void CTeletextDecoder::GetNextPageOne(bool up) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* disable subpage zapping */ + m_txtCache->ZapSubpageManual = false; + + /* abort pageinput */ + m_RenderInfo.InputCounter = 2; + + /* find next cached page */ + m_LastPage = m_txtCache->Page; + + int subp; + do { + if (up) + CDVDTeletextTools::NextDec(&m_txtCache->Page); + else + CDVDTeletextTools::PrevDec(&m_txtCache->Page); + subp = m_txtCache->SubPageTable[m_txtCache->Page]; + } while (subp == 0xFF && m_txtCache->Page != m_LastPage); + + /* update Page */ + if (m_txtCache->Page != m_LastPage) + { + if (m_RenderInfo.ZoomMode == 2) + m_RenderInfo.ZoomMode = 1; + + m_txtCache->SubPage = subp; + m_RenderInfo.HintMode = false; + m_txtCache->PageUpdate = true; + } +} + +void CTeletextDecoder::GetNextSubPage(int offset) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* abort pageinput */ + m_RenderInfo.InputCounter = 2; + + for (int loop = m_txtCache->SubPage + offset; loop != m_txtCache->SubPage; loop += offset) + { + if (loop < 0) + loop = 0x79; + else if (loop > 0x79) + loop = 0; + if (loop == m_txtCache->SubPage) + break; + + if (m_txtCache->astCachetable[m_txtCache->Page][loop]) + { + /* enable manual SubPage zapping */ + m_txtCache->ZapSubpageManual = true; + + /* update page */ + if (m_RenderInfo.ZoomMode == 2) /* if zoomed to lower half */ + m_RenderInfo.ZoomMode = 1; /* activate upper half */ + + m_txtCache->SubPage = loop; + m_RenderInfo.HintMode = false; + m_txtCache->PageUpdate = true; + + return; + } + } +} + +void CTeletextDecoder::SwitchZoomMode() +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + if (m_txtCache->SubPageTable[m_txtCache->Page] != 0xFF) + { + /* toggle mode */ + m_RenderInfo.ZoomMode++; + + if (m_RenderInfo.ZoomMode == 3) + m_RenderInfo.ZoomMode = 0; + + /* update page */ + m_txtCache->PageUpdate = true; + } +} + +void CTeletextDecoder::SwitchTranspMode() +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* toggle mode */ + if (!m_RenderInfo.TranspMode) + m_RenderInfo.TranspMode = true; + else + m_RenderInfo.TranspMode = false; /* backward to immediately switch to TV-screen */ + + /* set mode */ + if (!m_RenderInfo.TranspMode) /* normal text-only */ + { + ClearBB(m_txtCache->FullScrColor); + m_txtCache->PageUpdate = true; + } + else /* semi-transparent BG with FG text */ + { + ClearBB(TXT_ColorTransp); + m_txtCache->PageUpdate = true; + } +} + +void CTeletextDecoder::SwitchHintMode() +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* toggle mode */ + m_RenderInfo.HintMode ^= true; + + if (!m_RenderInfo.HintMode) /* toggle evaluation of level 2.5 information by explicitly switching off HintMode */ + { + m_RenderInfo.Showl25 ^= true; + } + /* update page */ + m_txtCache->PageUpdate = true; +} + +void CTeletextDecoder::ColorKey(int target) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + if (!target) + return; + + if (m_RenderInfo.ZoomMode == 2) + m_RenderInfo.ZoomMode = 1; + + m_LastPage = m_txtCache->Page; + m_txtCache->Page = target; + m_txtCache->SubPage = m_txtCache->SubPageTable[m_txtCache->Page]; + m_RenderInfo.InputCounter = 2; + m_RenderInfo.HintMode = false; + m_txtCache->PageUpdate = true; +} + +void CTeletextDecoder::StartPageCatching() +{ + m_RenderInfo.PageCatching = true; + + /* abort pageinput */ + m_RenderInfo.InputCounter = 2; + + /* show info line */ + m_RenderInfo.ZoomMode = 0; + m_RenderInfo.PosX = 0; + m_RenderInfo.PosY = 24*m_RenderInfo.FontHeight; + + /* check for pagenumber(s) */ + m_CatchRow = 1; + m_CatchCol = 0; + m_CatchedPage = 0; + m_PCOldRow = 0; + m_PCOldCol = 0; /* no inverted page number to restore yet */ + CatchNextPage(0, 1); + + if (!m_CatchedPage) + { + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + m_RenderInfo.PageCatching = false; + m_txtCache->PageUpdate = true; + return; + } +} + +void CTeletextDecoder::StopPageCatching() +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* set new page */ + if (m_RenderInfo.ZoomMode == 2) + m_RenderInfo.ZoomMode = 1; + + m_LastPage = m_txtCache->Page; + m_txtCache->Page = m_CatchedPage; + m_RenderInfo.HintMode = false; + m_txtCache->PageUpdate = true; + m_RenderInfo.PageCatching = false; + + int subp = m_txtCache->SubPageTable[m_txtCache->Page]; + if (subp != 0xFF) + m_txtCache->SubPage = subp; + else + m_txtCache->SubPage = 0; +} + +void CTeletextDecoder::CatchNextPage(int firstlineinc, int inc) +{ + int tmp_page, allowwrap = 1; /* allow first wrap around */ + + /* catch next page */ + for(;;) + { + unsigned char *p = &(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol]); + TextPageAttr_t a = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol]; + + if (!(a.charset == C_G1C || a.charset == C_G1S) && /* no mosaic */ + (a.fg != a.bg) && /* not hidden */ + (*p >= '1' && *p <= '8' && /* valid page number */ + *(p+1) >= '0' && *(p+1) <= '9' && + *(p+2) >= '0' && *(p+2) <= '9') && + (m_CatchRow == 0 || (*(p-1) < '0' || *(p-1) > '9')) && /* non-numeric char before and behind */ + (m_CatchRow == 37 || (*(p+3) < '0' || *(p+3) > '9'))) + { + tmp_page = ((*p - '0')<<8) | ((*(p+1) - '0')<<4) | (*(p+2) - '0'); + +#if 0 + if (tmp_page != m_CatchedPage) /* confusing to skip identical page numbers - I want to reach what I aim to */ +#endif + { + m_CatchedPage = tmp_page; + RenderCatchedPage(); + m_CatchCol += inc; /* FIXME: limit */ + return; + } + } + + if (firstlineinc > 0) + { + m_CatchRow++; + m_CatchCol = 0; + firstlineinc = 0; + } + else if (firstlineinc < 0) + { + m_CatchRow--; + m_CatchCol = 37; + firstlineinc = 0; + } + else + m_CatchCol += inc; + + if (m_CatchCol > 37) + { + m_CatchRow++; + m_CatchCol = 0; + } + else if (m_CatchCol < 0) + { + m_CatchRow--; + m_CatchCol = 37; + } + + if (m_CatchRow > 23) + { + if (allowwrap) + { + allowwrap = 0; + m_CatchRow = 1; + m_CatchCol = 0; + } + else + { + return; + } + } + else if (m_CatchRow < 1) + { + if (allowwrap) + { + allowwrap = 0; + m_CatchRow = 23; + m_CatchCol =37; + } + else + { + return; + } + } + } +} + +void CTeletextDecoder::RenderCatchedPage() +{ + int zoom = 0; + m_updateTexture = true; + + /* handle zoom */ + if (m_RenderInfo.ZoomMode) + zoom = 1<<10; + + if (m_PCOldRow || m_PCOldCol) /* not at first call */ + { + /* restore pagenumber */ + SetPosX(m_PCOldCol); + + if (m_RenderInfo.ZoomMode == 2) + m_RenderInfo.PosY = (m_PCOldRow-12)*m_RenderInfo.FontHeight*((zoom>>10)+1); + else + m_RenderInfo.PosY = m_PCOldRow*m_RenderInfo.FontHeight*((zoom>>10)+1); + + RenderCharFB(m_RenderInfo.PageChar[m_PCOldRow*40 + m_PCOldCol ], &m_RenderInfo.PageAtrb[m_PCOldRow*40 + m_PCOldCol ]); + RenderCharFB(m_RenderInfo.PageChar[m_PCOldRow*40 + m_PCOldCol + 1], &m_RenderInfo.PageAtrb[m_PCOldRow*40 + m_PCOldCol + 1]); + RenderCharFB(m_RenderInfo.PageChar[m_PCOldRow*40 + m_PCOldCol + 2], &m_RenderInfo.PageAtrb[m_PCOldRow*40 + m_PCOldCol + 2]); + } + + m_PCOldRow = m_CatchRow; + m_PCOldCol = m_CatchCol; + + /* mark pagenumber */ + if (m_RenderInfo.ZoomMode == 1 && m_CatchRow > 11) + { + m_RenderInfo.ZoomMode = 2; + CopyBB2FB(); + } + else if (m_RenderInfo.ZoomMode == 2 && m_CatchRow < 12) + { + m_RenderInfo.ZoomMode = 1; + CopyBB2FB(); + } + SetPosX(m_CatchCol); + + if (m_RenderInfo.ZoomMode == 2) + m_RenderInfo.PosY = (m_CatchRow-12)*m_RenderInfo.FontHeight*((zoom>>10)+1); + else + m_RenderInfo.PosY = m_CatchRow*m_RenderInfo.FontHeight*((zoom>>10)+1); + + TextPageAttr_t a0 = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol ]; + TextPageAttr_t a1 = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol + 1]; + TextPageAttr_t a2 = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol + 2]; + int t; + + /* exchange colors */ + t = a0.fg; a0.fg = a0.bg; a0.bg = t; + t = a1.fg; a1.fg = a1.bg; a1.bg = t; + t = a2.fg; a2.fg = a2.bg; a2.bg = t; + + RenderCharFB(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol ], &a0); + RenderCharFB(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol + 1], &a1); + RenderCharFB(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol + 2], &a2); +} + +void CTeletextDecoder::RenderPage() +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + int StartRow = 0; + int national_subset_bak = m_txtCache->NationalSubset; + + if (m_txtCache->PageUpdate) + m_updateTexture = true; + + /* update page or timestring */ + if (m_txtCache->PageUpdate && m_txtCache->PageReceiving != m_txtCache->Page && m_RenderInfo.InputCounter == 2) + { + /* reset update flag */ + m_txtCache->PageUpdate = false; + if (m_RenderInfo.Boxed && m_RenderInfo.SubtitleDelay) + { + TextSubtitleCache_t* c = NULL; + int j = -1; + for (int i = 0; i < SUBTITLE_CACHESIZE; i++) + { + if (j == -1 && !m_RenderInfo.SubtitleCache[i]) + j = i; + if (m_RenderInfo.SubtitleCache[i] && !m_RenderInfo.SubtitleCache[i]->Valid) + { + c = m_RenderInfo.SubtitleCache[i]; + break; + } + } + if (c == NULL) + { + if (j == -1) // no more space in SubtitleCache + return; + + c = new TextSubtitleCache_t; + if (c == NULL) + return; + + *c = {}; + m_RenderInfo.SubtitleCache[j] = c; + } + c->Valid = true; + c->Timestamp = std::chrono::steady_clock::now(); + + if (m_txtCache->SubPageTable[m_txtCache->Page] != 0xFF) + { + TextPageinfo_t * p = DecodePage(m_RenderInfo.Showl25, c->PageChar, c->PageAtrb, m_RenderInfo.HintMode, m_RenderInfo.ShowFlof); + if (p) + { + m_RenderInfo.Boxed = p->boxed; + } + } + m_RenderInfo.DelayStarted = true; + return; + } + m_RenderInfo.DelayStarted = false; + /* decode page */ + if (m_txtCache->SubPageTable[m_txtCache->Page] != 0xFF) + { + TextPageinfo_t * p = DecodePage(m_RenderInfo.Showl25, m_RenderInfo.PageChar, m_RenderInfo.PageAtrb, m_RenderInfo.HintMode, m_RenderInfo.ShowFlof); + if (p) + { + m_RenderInfo.PageInfo = p; + m_RenderInfo.Boxed = p->boxed; + } + if (m_RenderInfo.Boxed || m_RenderInfo.TranspMode) + FillBorder(GetColorRGB(TXT_ColorTransp)); + else + FillBorder(GetColorRGB((enumTeletextColor)m_txtCache->FullScrColor)); + + if (m_txtCache->ColorTable) /* as late as possible to shorten the time the old page is displayed with the new colors */ + SetColors(m_txtCache->ColorTable, 16, 16); /* set colors for CLUTs 2+3 */ + } + else + StartRow = 1; + + DoRenderPage(StartRow, national_subset_bak); + } + else + { + if (m_RenderInfo.DelayStarted) + { + auto now = std::chrono::steady_clock::now(); + for (TextSubtitleCache_t* const subtitleCache : m_RenderInfo.SubtitleCache) + { + if (subtitleCache && subtitleCache->Valid && + std::chrono::duration_cast<std::chrono::seconds>(now - subtitleCache->Timestamp) + .count() >= m_RenderInfo.SubtitleDelay) + { + memcpy(m_RenderInfo.PageChar, subtitleCache->PageChar, 40 * 25); + memcpy(m_RenderInfo.PageAtrb, subtitleCache->PageAtrb, 40 * 25 * sizeof(TextPageAttr_t)); + DoRenderPage(StartRow, national_subset_bak); + subtitleCache->Valid = false; + return; + } + } + } + if (m_RenderInfo.ZoomMode != 2) + { + m_RenderInfo.PosY = 0; + if (m_txtCache->SubPageTable[m_txtCache->Page] == 0xff) + { + m_RenderInfo.PageAtrb[32].fg = TXT_ColorYellow; + m_RenderInfo.PageAtrb[32].bg = TXT_ColorMenu1; + int showpage = m_txtCache->PageReceiving; + int showsubpage; + + // Verify that showpage is positive before any access to the array + if (showpage >= 0 && (showsubpage = m_txtCache->SubPageTable[showpage]) != 0xff) + { + TextCachedPage_t *pCachedPage; + pCachedPage = m_txtCache->astCachetable[showpage][showsubpage]; + if (pCachedPage && IsDec(showpage)) + { + m_RenderInfo.PosX = 0; + if (m_RenderInfo.InputCounter == 2) + { + if (m_txtCache->BTTok && !m_txtCache->BasicTop[m_txtCache->Page]) /* page non-existent according to TOP (continue search anyway) */ + { + m_RenderInfo.PageAtrb[0].fg = TXT_ColorWhite; + m_RenderInfo.PageAtrb[0].bg = TXT_ColorRed; + } + else + { + m_RenderInfo.PageAtrb[0].fg = TXT_ColorYellow; + m_RenderInfo.PageAtrb[0].bg = TXT_ColorMenu1; + } + CDVDTeletextTools::Hex2Str((char*)m_RenderInfo.PageChar+3, m_txtCache->Page); + + int col; + for (col = m_RenderInfo.nofirst; col < 7; col++) // selected page + { + RenderCharFB(m_RenderInfo.PageChar[col], &m_RenderInfo.PageAtrb[0]); + } + RenderCharFB(m_RenderInfo.PageChar[col], &m_RenderInfo.PageAtrb[32]); + } + else + SetPosX(8); + + memcpy(&m_RenderInfo.PageChar[8], pCachedPage->p0, 24); /* header line without timestring */ + for (unsigned char i : pCachedPage->p0) + { + RenderCharFB(i, &m_RenderInfo.PageAtrb[32]); + } + + /* Update on every Header number change */ + if (pCachedPage->p0[2] != prevHeaderPage) + { + prevHeaderPage = pCachedPage->p0[2]; + m_updateTexture = true; + } + } + } + } + + /* update timestring */ + SetPosX(32); + for (int i = 0; i < 8; i++) + { + if (!m_RenderInfo.PageAtrb[32+i].flashing) + RenderCharFB(m_txtCache->TimeString[i], &m_RenderInfo.PageAtrb[32]); + else + { + SetPosX(33+i); + } + } + + if (!IsSubtitlePage(m_txtCache->Page)) + { + /* Update on every changed second */ + if (m_txtCache->TimeString[7] != prevTimeSec) + { + prevTimeSec = m_txtCache->TimeString[7]; + m_updateTexture = true; + } + } + else + { + m_updateTexture = true; + } + } + DoFlashing(StartRow); + m_txtCache->NationalSubset = national_subset_bak; + } +} + +bool CTeletextDecoder::IsSubtitlePage(int pageNumber) const +{ + if (!m_txtCache) + return false; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + for (const auto subPage : m_txtCache->SubtitlePages) + { + if (subPage.page == pageNumber) + return true; + } + + return false; +} + +void CTeletextDecoder::DoFlashing(int startrow) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + TextCachedPage_t* textCachepage = + m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage]; + + // Verify that the page is not deleted by the other thread: CDVDTeletextData::ResetTeletextCache() + if (!textCachepage || m_RenderInfo.PageInfo != &textCachepage->pageinfo) + m_RenderInfo.PageInfo = nullptr; + + /* get national subset */ + if (m_txtCache->NationalSubset <= NAT_MAX_FROM_HEADER && /* not for GR/RU as long as line28 is not evaluated */ + m_RenderInfo.PageInfo && m_RenderInfo.PageInfo->nationalvalid) /* individual subset according to page header */ + { + m_txtCache->NationalSubset = CountryConversionTable[m_RenderInfo.PageInfo->national]; + } + + /* Flashing */ + TextPageAttr_t flashattr; + char flashchar; + std::chrono::milliseconds flashphase = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now().time_since_epoch()) % + 1000; + + int srow = startrow; + int erow = 24; + int factor=1; + + switch (m_RenderInfo.ZoomMode) + { + case 1: erow = 12; factor=2;break; + case 2: srow = 12; factor=2;break; + } + + m_RenderInfo.PosY = startrow*m_RenderInfo.FontHeight*factor; + for (int row = srow; row < erow; row++) + { + int index = row * 40; + int dhset = 0; + int incflash = 3; + int decflash = 2; + + m_RenderInfo.PosX = 0; + for (int col = m_RenderInfo.nofirst; col < 40; col++) + { + if (m_RenderInfo.PageAtrb[index + col].flashing && m_RenderInfo.PageChar[index + col] > 0x20 && m_RenderInfo.PageChar[index + col] != 0xff ) + { + SetPosX(col); + flashchar = m_RenderInfo.PageChar[index + col]; + bool doflash = false; + memcpy(&flashattr, &m_RenderInfo.PageAtrb[index + col], sizeof(TextPageAttr_t)); + switch (flashattr.flashing &0x1c) // Flash Rate + { + case 0x00 : // 1 Hz + if (flashphase > 500ms) + doflash = true; + break; + case 0x04 : // 2 Hz Phase 1 + if (flashphase < 250ms) + doflash = true; + break; + case 0x08 : // 2 Hz Phase 2 + if (flashphase >= 250ms && flashphase < 500ms) + doflash = true; + break; + case 0x0c : // 2 Hz Phase 3 + if (flashphase >= 500ms && flashphase < 750ms) + doflash = true; + break; + case 0x10 : // incremental flash + incflash++; + if (incflash>3) incflash = 1; + switch (incflash) + { + case 1: + if (flashphase < 250ms) + doflash = true; + break; + case 2: + if (flashphase >= 250ms && flashphase < 500ms) + doflash = true; + break; + case 3: + if (flashphase >= 500ms && flashphase < 750ms) + doflash = true; + break; + } + break; + case 0x14 : // decremental flash + decflash--; + if (decflash<1) decflash = 3; + switch (decflash) + { + case 1: + if (flashphase < 250ms) + doflash = true; + break; + case 2: + if (flashphase >= 250ms && flashphase < 500ms) + doflash = true; + break; + case 3: + if (flashphase >= 500ms && flashphase < 750ms) + doflash = true; + break; + } + break; + + } + + switch (flashattr.flashing &0x03) // Flash Mode + { + case 0x01 : // normal Flashing + if (doflash) flashattr.fg = flashattr.bg; + break; + case 0x02 : // inverted Flashing + doflash = !doflash; + if (doflash) flashattr.fg = flashattr.bg; + break; + case 0x03 : // color Flashing + if (doflash) flashattr.fg = flashattr.fg + (flashattr.fg > 7 ? (-8) : 8); + break; + + } + RenderCharFB(flashchar, &flashattr); + if (flashattr.doublew) col++; + if (flashattr.doubleh) dhset = 1; + + m_updateTexture = true; + } + } + if (dhset) + { + row++; + m_RenderInfo.PosY += m_RenderInfo.FontHeight*factor; + } + m_RenderInfo.PosY += m_RenderInfo.FontHeight*factor; + } +} + +void CTeletextDecoder::DoRenderPage(int startrow, int national_subset_bak) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* display first column? */ + m_RenderInfo.nofirst = m_RenderInfo.Show39; + for (int row = 1; row < 24; row++) + { + int Byte = m_RenderInfo.PageChar[row*40]; + if (Byte != ' ' && Byte != 0x00 && Byte != 0xFF && m_RenderInfo.PageAtrb[row*40].fg != m_RenderInfo.PageAtrb[row*40].bg) + { + m_RenderInfo.nofirst = 0; + break; + } + } + m_RenderInfo.FontWidth_Normal = m_RenderInfo.Width / (m_RenderInfo.nofirst ? 39 : 40); + SetFontWidth(m_RenderInfo.FontWidth_Normal); + + if (m_RenderInfo.TranspMode || m_RenderInfo.Boxed) + { + FillBorder(GetColorRGB(TXT_ColorTransp));//ClearBB(transp); + m_RenderInfo.ClearBBColor = TXT_ColorTransp; + } + + /* get national subset */ + if (m_txtCache->NationalSubset <= NAT_MAX_FROM_HEADER && /* not for GR/RU as long as line28 is not evaluated */ + m_RenderInfo.PageInfo && m_RenderInfo.PageInfo->nationalvalid) /* individual subset according to page header */ + { + m_txtCache->NationalSubset = CountryConversionTable[m_RenderInfo.PageInfo->national]; + } + /* render page */ + if (m_RenderInfo.PageInfo && (m_RenderInfo.PageInfo->function == FUNC_GDRCS || m_RenderInfo.PageInfo->function == FUNC_DRCS)) /* character definitions */ + { + #define DRCSROWS 8 + #define DRCSCOLS (48/DRCSROWS) + #define DRCSZOOMX 3 + #define DRCSZOOMY 5 + #define DRCSXSPC (12*DRCSZOOMX + 2) + #define DRCSYSPC (10*DRCSZOOMY + 2) + + unsigned char ax[] = { /* array[0..12] of x-offsets, array[0..10] of y-offsets for each pixel */ + DRCSZOOMX * 0, + DRCSZOOMX * 1, + DRCSZOOMX * 2, + DRCSZOOMX * 3, + DRCSZOOMX * 4, + DRCSZOOMX * 5, + DRCSZOOMX * 6, + DRCSZOOMX * 7, + DRCSZOOMX * 8, + DRCSZOOMX * 9, + DRCSZOOMX * 10, + DRCSZOOMX * 11, + DRCSZOOMX * 12, + DRCSZOOMY * 0, + DRCSZOOMY * 1, + DRCSZOOMY * 2, + DRCSZOOMY * 3, + DRCSZOOMY * 4, + DRCSZOOMY * 5, + DRCSZOOMY * 6, + DRCSZOOMY * 7, + DRCSZOOMY * 8, + DRCSZOOMY * 9, + DRCSZOOMY * 10 + }; + + ClearBB(TXT_ColorBlack); + for (int col = 0; col < 24*40; col++) + m_RenderInfo.PageAtrb[col] = Text_AtrTable[ATR_WB]; + + for (int row = 0; row < DRCSROWS; row++) + { + for (int col = 0; col < DRCSCOLS; col++) + { + RenderDRCS(m_RenderInfo.Width, + m_RenderInfo.PageChar + 20 * (DRCSCOLS * row + col + 2), + m_TextureBuffer + + (m_RenderInfo.FontHeight + DRCSYSPC * row + m_RenderInfo.Height) * m_RenderInfo.Width + + DRCSXSPC * col, + ax, GetColorRGB(TXT_ColorWhite), GetColorRGB(TXT_ColorBlack)); + } + } + memset(m_RenderInfo.PageChar + 40, 0xff, 24*40); /* don't render any char below row 0 */ + } + m_RenderInfo.PosY = startrow*m_RenderInfo.FontHeight; + for (int row = startrow; row < 24; row++) + { + int index = row * 40; + + m_RenderInfo.PosX = 0; + for (int col = m_RenderInfo.nofirst; col < 40; col++) + { + RenderCharBB(m_RenderInfo.PageChar[index + col], &m_RenderInfo.PageAtrb[index + col]); + + if (m_RenderInfo.PageAtrb[index + col].doubleh && m_RenderInfo.PageChar[index + col] != 0xff && row < 24-1) /* disable lower char in case of doubleh setting in l25 objects */ + m_RenderInfo.PageChar[index + col + 40] = 0xff; + if (m_RenderInfo.PageAtrb[index + col].doublew && col < 40-1) /* skip next column if double width */ + { + col++; + if (m_RenderInfo.PageAtrb[index + col - 1].doubleh && m_RenderInfo.PageChar[index + col] != 0xff && row < 24-1) /* disable lower char in case of doubleh setting in l25 objects */ + m_RenderInfo.PageChar[index + col + 40] = 0xff; + } + } + m_RenderInfo.PosY += m_RenderInfo.FontHeight; + } + DoFlashing(startrow); + + /* update framebuffer */ + CopyBB2FB(); + m_txtCache->NationalSubset = national_subset_bak; +} + +void CTeletextDecoder::Decode_BTT() +{ + /* basic top table */ + int current, b1, b2, b3, b4; + unsigned char btt[23*40]; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + if (m_txtCache->SubPageTable[0x1f0] == 0xff || 0 == m_txtCache->astCachetable[0x1f0][m_txtCache->SubPageTable[0x1f0]]) /* not yet received */ + return; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + appPlayer->LoadPage(0x1f0, m_txtCache->SubPageTable[0x1f0], btt); + if (btt[799] == ' ') /* not completely received or error */ + return; + + current = 0x100; + for (int i = 0; i < 800; i++) + { + b1 = btt[i]; + if (b1 == ' ') + b1 = 0; + else + { + b1 = dehamming[b1]; + if (b1 == 0xFF) /* hamming error in btt */ + { + btt[799] = ' '; /* mark btt as not received */ + return; + } + } + m_txtCache->BasicTop[current] = b1; + CDVDTeletextTools::NextDec(¤t); + } + /* page linking table */ + m_txtCache->ADIP_PgMax = -1; /* rebuild table of adip pages */ + for (int i = 0; i < 10; i++) + { + b1 = dehamming[btt[800 + 8*i +0]]; + + if (b1 == 0xE) + continue; /* unused */ + else if (b1 == 0xF) + break; /* end */ + + b4 = dehamming[btt[800 + 8*i +7]]; + + if (b4 != 2) /* only adip, ignore multipage (1) */ + continue; + + b2 = dehamming[btt[800 + 8*i +1]]; + b3 = dehamming[btt[800 + 8*i +2]]; + + if (b1 == 0xFF || b2 == 0xFF || b3 == 0xFF) + { + CLog::Log(LOGERROR, "CTeletextDecoder::Decode_BTT <Biterror in btt/plt index {}>", i); + btt[799] = ' '; /* mark btt as not received */ + return; + } + + b1 = b1<<8 | b2<<4 | b3; /* page number */ + m_txtCache->ADIP_Pg[++m_txtCache->ADIP_PgMax] = b1; + } + + m_txtCache->BTTok = true; +} + +void CTeletextDecoder::Decode_ADIP() /* additional information table */ +{ + int i, p, j, b1, b2, b3, charfound; + unsigned char padip[23*40]; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + for (i = 0; i <= m_txtCache->ADIP_PgMax; i++) + { + p = m_txtCache->ADIP_Pg[i]; + if (!p || m_txtCache->SubPageTable[p] == 0xff || 0 == m_txtCache->astCachetable[p][m_txtCache->SubPageTable[p]]) /* not cached (avoid segfault) */ + continue; + + appPlayer->LoadPage(p, m_txtCache->SubPageTable[p], padip); + for (j = 0; j < 44; j++) + { + b1 = dehamming[padip[20*j+0]]; + if (b1 == 0xE) + continue; /* unused */ + + if (b1 == 0xF) + break; /* end */ + + b2 = dehamming[padip[20*j+1]]; + b3 = dehamming[padip[20*j+2]]; + + if (b1 == 0xFF || b2 == 0xFF || b3 == 0xFF) + { + CLog::Log(LOGERROR, + "CTeletextDecoder::Decode_BTT <Biterror in ait {:03x} {} {:02x} {:02x} {:02x} " + "{:02x} {:02x} {:02x}>", + p, j, padip[20 * j + 0], padip[20 * j + 1], padip[20 * j + 2], b1, b2, b3); + return; + } + + if (b1>8 || b2>9 || b3>9) /* ignore entries with invalid or hex page numbers */ + { + continue; + } + + b1 = b1<<8 | b2<<4 | b3; /* page number */ + charfound = 0; /* flag: no printable char found */ + + for (b2 = 11; b2 >= 0; b2--) + { + b3 = deparity[padip[20*j + 8 + b2]]; + if (b3 < ' ') + b3 = ' '; + + if (b3 == ' ' && !charfound) + m_txtCache->ADIPTable[b1][b2] = '\0'; + else + { + m_txtCache->ADIPTable[b1][b2] = b3; + charfound = 1; + } + } + } /* next link j */ + + m_txtCache->ADIP_Pg[i] = 0; /* completely decoded: clear entry */ + } /* next adip page i */ + + while ((m_txtCache->ADIP_PgMax >= 0) && !m_txtCache->ADIP_Pg[m_txtCache->ADIP_PgMax]) /* and shrink table */ + m_txtCache->ADIP_PgMax--; +} + +int CTeletextDecoder::TopText_GetNext(int startpage, int up, int findgroup) +{ + int current, nextgrp, nextblk; + + int stoppage = (IsDec(startpage) ? startpage : startpage & 0xF00); // avoid endless loop in hexmode + nextgrp = nextblk = 0; + current = startpage; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + do { + if (up) + CDVDTeletextTools::NextDec(¤t); + else + CDVDTeletextTools::PrevDec(¤t); + + if (!m_txtCache->BTTok || m_txtCache->BasicTop[current]) /* only if existent */ + { + if (findgroup) + { + if (m_txtCache->BasicTop[current] >= 6 && m_txtCache->BasicTop[current] <= 7) + return current; + if (!nextgrp && (current&0x00F) == 0) + nextgrp = current; + } + if (m_txtCache->BasicTop[current] >= 2 && m_txtCache->BasicTop[current] <= 5) /* always find block */ + return current; + + if (!nextblk && (current&0x0FF) == 0) + nextblk = current; + } + } while (current != stoppage); + + if (nextgrp) + return nextgrp; + else if (nextblk) + return nextblk; + else + return current; +} + +void CTeletextDecoder::Showlink(int column, int linkpage) +{ + unsigned char line[] = " >??? "; + int oldfontwidth = m_RenderInfo.FontWidth; + int yoffset; + + if (m_YOffset) + yoffset = 0; + else + yoffset = m_RenderInfo.Height; + + int abx = ((m_RenderInfo.Width)%(40-m_RenderInfo.nofirst) == 0 ? m_RenderInfo.Width+1 : (m_RenderInfo.Width)/(((m_RenderInfo.Width)%(40-m_RenderInfo.nofirst)))+1);// distance between 'inserted' pixels + int width = m_RenderInfo.Width /4; + + m_RenderInfo.PosY = 24*m_RenderInfo.FontHeight; + + if (m_RenderInfo.Boxed) + { + m_RenderInfo.PosX = column*width; + FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY+yoffset, m_RenderInfo.Width, m_RenderInfo.FontHeight, GetColorRGB(TXT_ColorTransp)); + return; + } + + if (m_txtCache->ADIPTable[linkpage][0]) + { + m_RenderInfo.PosX = column*width; + int l = strlen(m_txtCache->ADIPTable[linkpage]); + + if (l > 9) /* smaller font, if no space for one half space at front and end */ + SetFontWidth(oldfontwidth * 10 / (l+1)); + + FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY+yoffset, width+(m_RenderInfo.Width%4), m_RenderInfo.FontHeight, GetColorRGB((enumTeletextColor)Text_AtrTable[ATR_L250 + column].bg)); + m_RenderInfo.PosX += ((width) - (l*m_RenderInfo.FontWidth+l*m_RenderInfo.FontWidth/abx))/2; /* center */ + + for (char *p = m_txtCache->ADIPTable[linkpage]; *p; p++) + RenderCharBB(*p, &Text_AtrTable[ATR_L250 + column]); + + SetFontWidth(oldfontwidth); + } + else /* display number */ + { + m_RenderInfo.PosX = column*width; + FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY+yoffset, m_RenderInfo.Width-m_RenderInfo.PosX, m_RenderInfo.FontHeight, GetColorRGB((enumTeletextColor)Text_AtrTable[ATR_L250 + column].bg)); + if (linkpage < m_txtCache->Page) + { + line[6] = '<'; + CDVDTeletextTools::Hex2Str((char*)line + 5, linkpage); + } + else + CDVDTeletextTools::Hex2Str((char*)line + 6, linkpage); + + for (unsigned char *p = line; p < line+9; p++) + RenderCharBB(*p, &Text_AtrTable[ATR_L250 + column]); + } +} + +void CTeletextDecoder::CreateLine25() +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* btt completely received and not yet decoded */ + if (!m_txtCache->BTTok) + Decode_BTT(); + + if (m_txtCache->ADIP_PgMax >= 0) + Decode_ADIP(); + + if (!m_RenderInfo.ShowHex && m_RenderInfo.ShowFlof && + (m_txtCache->FlofPages[m_txtCache->Page][0] || m_txtCache->FlofPages[m_txtCache->Page][1] || m_txtCache->FlofPages[m_txtCache->Page][2] || m_txtCache->FlofPages[m_txtCache->Page][3])) // FLOF-Navigation present + { + m_RenderInfo.Prev_100 = m_txtCache->FlofPages[m_txtCache->Page][0]; + m_RenderInfo.Prev_10 = m_txtCache->FlofPages[m_txtCache->Page][1]; + m_RenderInfo.Next_10 = m_txtCache->FlofPages[m_txtCache->Page][2]; + m_RenderInfo.Next_100 = m_txtCache->FlofPages[m_txtCache->Page][3]; + + m_RenderInfo.PosY = 24*m_RenderInfo.FontHeight; + m_RenderInfo.PosX = 0; + for (int i=m_RenderInfo.nofirst; i<40; i++) + RenderCharBB(m_RenderInfo.PageChar[24*40 + i], &m_RenderInfo.PageAtrb[24*40 + i]); + } + else + { + /* normal: blk-1, grp+1, grp+2, blk+1 */ + /* hex: hex+1, blk-1, grp+1, blk+1 */ + if (m_RenderInfo.ShowHex) + { + /* arguments: startpage, up, findgroup */ + m_RenderInfo.Prev_100 = NextHex(m_txtCache->Page); + m_RenderInfo.Prev_10 = TopText_GetNext(m_txtCache->Page, 0, 0); + m_RenderInfo.Next_10 = TopText_GetNext(m_txtCache->Page, 1, 1); + } + else + { + m_RenderInfo.Prev_100 = TopText_GetNext(m_txtCache->Page, 0, 0); + m_RenderInfo.Prev_10 = TopText_GetNext(m_txtCache->Page, 1, 1); + m_RenderInfo.Next_10 = TopText_GetNext(m_RenderInfo.Prev_10, 1, 1); + } + m_RenderInfo.Next_100 = TopText_GetNext(m_RenderInfo.Next_10, 1, 0); + Showlink(0, m_RenderInfo.Prev_100); + Showlink(1, m_RenderInfo.Prev_10); + Showlink(2, m_RenderInfo.Next_10); + Showlink(3, m_RenderInfo.Next_100); + } +} + +void CTeletextDecoder::RenderCharFB(int Char, TextPageAttr_t *Attribute) +{ + RenderCharIntern(&m_RenderInfo, Char, Attribute, m_RenderInfo.ZoomMode, m_YOffset); +} + +void CTeletextDecoder::RenderCharBB(int Char, TextPageAttr_t *Attribute) +{ + RenderCharIntern(&m_RenderInfo, Char, Attribute, 0, m_RenderInfo.Height-m_YOffset); +} + +void CTeletextDecoder::CopyBB2FB() +{ + UTILS::COLOR::Color *src, *dst, *topsrc; + int screenwidth; + UTILS::COLOR::Color fillcolor; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* line 25 */ + if (!m_RenderInfo.PageCatching) + CreateLine25(); + + /* copy backbuffer to framebuffer */ + if (!m_RenderInfo.ZoomMode) + { + if (m_YOffset) + m_YOffset = 0; + else + m_YOffset = m_RenderInfo.Height; + + if (m_RenderInfo.ClearBBColor >= 0) + { + m_RenderInfo.ClearBBColor = -1; + } + return; + } + + src = dst = topsrc = m_TextureBuffer + m_RenderInfo.Width; + + if (m_YOffset) + { + dst += m_RenderInfo.Width * m_RenderInfo.Height; + } + else + { + src += m_RenderInfo.Width * m_RenderInfo.Height; + topsrc += m_RenderInfo.Width * m_RenderInfo.Height; + } + + if (!m_RenderInfo.PageCatching) + SDL_memcpy4(dst+(24*m_RenderInfo.FontHeight)*m_RenderInfo.Width, src + (24*m_RenderInfo.FontHeight)*m_RenderInfo.Width, m_RenderInfo.Width*m_RenderInfo.FontHeight); /* copy line25 in normal height */ + + if (m_RenderInfo.TranspMode) + fillcolor = GetColorRGB(TXT_ColorTransp); + else + fillcolor = GetColorRGB((enumTeletextColor)m_txtCache->FullScrColor); + + if (m_RenderInfo.ZoomMode == 2) + src += 12*m_RenderInfo.FontHeight*m_RenderInfo.Width; + + screenwidth = m_RenderInfo.Width; + + for (int i = 12*m_RenderInfo.FontHeight; i; i--) + { + SDL_memcpy4(dst, src, screenwidth); + dst += m_RenderInfo.Width; + SDL_memcpy4(dst, src, screenwidth); + dst += m_RenderInfo.Width; + src += m_RenderInfo.Width; + } + + for (int i = m_RenderInfo.Height - 25*m_RenderInfo.FontHeight; i >= 0;i--) + { + SDL_memset4(dst + m_RenderInfo.Width*(m_RenderInfo.FontHeight+i), fillcolor, screenwidth); + } +} + +FT_Error CTeletextDecoder::MyFaceRequester(FTC_FaceID face_id, FT_Library library, FT_Pointer request_data, FT_Face *aface) +{ + FT_Error result = FT_New_Face(library, (const char*)face_id, 0, aface); + + if (!result) + CLog::Log(LOGINFO, "Teletext font {} loaded", (char*)face_id); + else + CLog::Log(LOGERROR, "Opening of Teletext font {} failed", (char*)face_id); + + return result; +} + +void CTeletextDecoder::SetFontWidth(int newWidth) +{ + if (m_RenderInfo.FontWidth != newWidth) + { + m_RenderInfo.FontWidth = newWidth; + m_TypeTTF.width = (FT_UShort) m_RenderInfo.FontWidth; + + for (int i = 0; i <= 12; i++) + m_RenderInfo.axdrcs[i] = (m_RenderInfo.FontWidth * i + 6) / 12; + } +} + +int CTeletextDecoder::GetCurFontWidth() +{ + int mx = (m_RenderInfo.Width)%(40-m_RenderInfo.nofirst); // # of unused pixels + int abx = (mx == 0 ? m_RenderInfo.Width+1 : (m_RenderInfo.Width)/(mx+1)); // distance between 'inserted' pixels + int nx = abx+1-(m_RenderInfo.PosX % (abx+1)); // # of pixels to next insert + return m_RenderInfo.FontWidth+(((m_RenderInfo.PosX+m_RenderInfo.FontWidth+1) <= m_RenderInfo.Width && nx <= m_RenderInfo.FontWidth+1) ? 1 : 0); +} + +void CTeletextDecoder::SetPosX(int column) +{ + m_RenderInfo.PosX = 0; + + for (int i = 0; i < column-m_RenderInfo.nofirst; i++) + m_RenderInfo.PosX += GetCurFontWidth(); +} + +void CTeletextDecoder::ClearBB(UTILS::COLOR::Color Color) +{ + SDL_memset4(m_TextureBuffer + (m_RenderInfo.Height-m_YOffset)*m_RenderInfo.Width, Color, m_RenderInfo.Width*m_RenderInfo.Height); +} + +void CTeletextDecoder::ClearFB(UTILS::COLOR::Color Color) +{ + SDL_memset4(m_TextureBuffer + m_RenderInfo.Width*m_YOffset, Color, m_RenderInfo.Width*m_RenderInfo.Height); +} + +void CTeletextDecoder::FillBorder(UTILS::COLOR::Color Color) +{ + FillRect(m_TextureBuffer + (m_RenderInfo.Height-m_YOffset)*m_RenderInfo.Width, m_RenderInfo.Width, 0, 25*m_RenderInfo.FontHeight, m_RenderInfo.Width, m_RenderInfo.Height-(25*m_RenderInfo.FontHeight), Color); + FillRect(m_TextureBuffer + m_RenderInfo.Width*m_YOffset, m_RenderInfo.Width, 0, 25*m_RenderInfo.FontHeight, m_RenderInfo.Width, m_RenderInfo.Height-(25*m_RenderInfo.FontHeight), Color); +} + +void CTeletextDecoder::FillRect( + UTILS::COLOR::Color* buffer, int xres, int x, int y, int w, int h, UTILS::COLOR::Color Color) +{ + if (!buffer) return; + + UTILS::COLOR::Color* p = buffer + x + y * xres; + + if (w > 0) + { + for ( ; h > 0 ; h--) + { + SDL_memset4(p, Color, w); + p += xres; + } + } +} + +void CTeletextDecoder::DrawVLine( + UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color) +{ + if (!lfb) return; + UTILS::COLOR::Color* p = lfb + x + y * xres; + + for ( ; l > 0 ; l--) + { + *p = color; + p += xres; + } +} + +void CTeletextDecoder::DrawHLine( + UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color) +{ + if (!lfb) return; + if (l > 0) + SDL_memset4(lfb + x + y * xres, color, l); +} + +void CTeletextDecoder::RenderDRCS( + int xres, + unsigned char* s, /* pointer to char data, parity undecoded */ + UTILS::COLOR::Color* d, /* pointer to frame buffer of top left pixel */ + unsigned char* ax, /* array[0..12] of x-offsets, array[0..10] of y-offsets for each pixel */ + UTILS::COLOR::Color fgcolor, + UTILS::COLOR::Color bgcolor) +{ + if (d == NULL) return; + + unsigned char *ay = ax + 13; /* array[0..10] of y-offsets for each pixel */ + + for (int y = 0; y < 10; y++) /* 10*2 bytes a 6 pixels per char definition */ + { + unsigned char c1 = deparity[*s++]; + unsigned char c2 = deparity[*s++]; + int h = ay[y+1] - ay[y]; + + if (!h) + continue; + if (((c1 == ' ') && (*(s-2) != ' ')) || ((c2 == ' ') && (*(s-1) != ' '))) /* parity error: stop decoding FIXME */ + return; + for (int bit = 0x20, x = 0; + bit; + bit >>= 1, x++) /* bit mask (MSB left), column counter */ + { + UTILS::COLOR::Color f1 = (c1 & bit) ? fgcolor : bgcolor; + UTILS::COLOR::Color f2 = (c2 & bit) ? fgcolor : bgcolor; + for (int i = 0; i < h; i++) + { + if (ax[x+1] > ax[x]) + SDL_memset4(d + ax[x], f1, ax[x+1] - ax[x]); + if (ax[x+7] > ax[x+6]) + SDL_memset4(d + ax[x+6], f2, ax[x+7] - ax[x+6]); /* 2nd byte 6 pixels to the right */ + d += xres; + } + d -= h * xres; + } + d += h * xres; + } +} + +void CTeletextDecoder::FillRectMosaicSeparated(UTILS::COLOR::Color* lfb, + int xres, + int x, + int y, + int w, + int h, + UTILS::COLOR::Color fgcolor, + UTILS::COLOR::Color bgcolor, + int set) +{ + if (!lfb) return; + FillRect(lfb,xres,x, y, w, h, bgcolor); + if (set) + { + FillRect(lfb,xres,x+1, y+1, w-2, h-2, fgcolor); + } +} + +void CTeletextDecoder::FillTrapez(UTILS::COLOR::Color* lfb, + int xres, + int x0, + int y0, + int l0, + int xoffset1, + int h, + int l1, + UTILS::COLOR::Color color) +{ + UTILS::COLOR::Color* p = lfb + x0 + y0 * xres; + int xoffset, l; + + for (int yoffset = 0; yoffset < h; yoffset++) + { + l = l0 + ((l1-l0) * yoffset + h/2) / h; + xoffset = (xoffset1 * yoffset + h/2) / h; + if (l > 0) + SDL_memset4(p + xoffset, color, l); + p += xres; + } +} + +void CTeletextDecoder::FlipHorz(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h) +{ + UTILS::COLOR::Color buf[2048]; + UTILS::COLOR::Color* p = lfb + x + y * xres; + int w1,h1; + + for (h1 = 0 ; h1 < h ; h1++) + { + SDL_memcpy4(buf,p,w); + for (w1 = 0 ; w1 < w ; w1++) + { + *(p+w1) = buf[w-(w1+1)]; + } + p += xres; + } +} + +void CTeletextDecoder::FlipVert(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h) +{ + UTILS::COLOR::Color buf[2048]; + UTILS::COLOR::Color *p = lfb + x + y * xres, *p1, *p2; + int h1; + + for (h1 = 0 ; h1 < h/2 ; h1++) + { + p1 = (p+(h1*xres)); + p2 = (p+(h-(h1+1))*xres); + SDL_memcpy4(buf, p1, w); + SDL_memcpy4(p1, p2, w); + SDL_memcpy4(p2, buf, w); + } +} + +int CTeletextDecoder::ShapeCoord(int param, int curfontwidth, int curFontHeight) +{ + switch (param) + { + case S_W13: + return curfontwidth/3; + case S_W12: + return curfontwidth/2; + case S_W23: + return curfontwidth*2/3; + case S_W11: + return curfontwidth; + case S_WM3: + return curfontwidth-3; + case S_H13: + return curFontHeight/3; + case S_H12: + return curFontHeight/2; + case S_H23: + return curFontHeight*2/3; + case S_H11: + return curFontHeight; + default: + return param; + } +} + +void CTeletextDecoder::DrawShape(UTILS::COLOR::Color* lfb, + int xres, + int x, + int y, + int shapenumber, + int curfontwidth, + int FontHeight, + int curFontHeight, + UTILS::COLOR::Color fgcolor, + UTILS::COLOR::Color bgcolor, + bool clear) +{ + if (!lfb || shapenumber < 0x20 || shapenumber > 0x7e || (shapenumber == 0x7e && clear)) + return; + + unsigned char *p = aShapes[shapenumber - 0x20]; + + if (*p == S_INV) + { + int t = fgcolor; + fgcolor = bgcolor; + bgcolor = t; + p++; + } + + if (clear) + FillRect(lfb, xres, x, y, curfontwidth, FontHeight, bgcolor); + + while (*p != S_END) + { + switch (*p++) + { + case S_FHL: + { + int offset = ShapeCoord(*p++, curfontwidth, curFontHeight); + DrawHLine(lfb, xres, x, y + offset, curfontwidth, fgcolor); + break; + } + case S_FVL: + { + int offset = ShapeCoord(*p++, curfontwidth, curFontHeight); + DrawVLine(lfb,xres,x + offset, y, FontHeight, fgcolor); + break; + } + case S_FLH: + FlipHorz(lfb,xres,x,y,curfontwidth, FontHeight); + break; + case S_FLV: + FlipVert(lfb,xres,x,y,curfontwidth, FontHeight); + break; + case S_BOX: + { + int xo = ShapeCoord(*p++, curfontwidth, curFontHeight); + int yo = ShapeCoord(*p++, curfontwidth, curFontHeight); + int w = ShapeCoord(*p++, curfontwidth, curFontHeight); + int h = ShapeCoord(*p++, curfontwidth, curFontHeight); + FillRect(lfb,xres,x + xo, y + yo, w, h, fgcolor); + break; + } + case S_TRA: + { + int x0 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int y0 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int l0 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int x1 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int y1 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int l1 = ShapeCoord(*p++, curfontwidth, curFontHeight); + FillTrapez(lfb, xres,x + x0, y + y0, l0, x1-x0, y1-y0, l1, fgcolor); + break; + } + case S_BTR: + { + int x0 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int y0 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int l0 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int x1 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int y1 = ShapeCoord(*p++, curfontwidth, curFontHeight); + int l1 = ShapeCoord(*p++, curfontwidth, curFontHeight); + FillTrapez(lfb, xres, x + x0, y + y0, l0, x1-x0, y1-y0, l1, bgcolor); + break; + } + case S_LNK: + { + DrawShape(lfb,xres,x, y, ShapeCoord(*p, curfontwidth, curFontHeight), curfontwidth, FontHeight, curFontHeight, fgcolor, bgcolor, false); + break; + } + default: + break; + } + } +} + +void CTeletextDecoder::RenderCharIntern(TextRenderInfo_t* RenderInfo, int Char, TextPageAttr_t *Attribute, int zoom, int yoffset) +{ + int Row, Pitch; + int glyph; + UTILS::COLOR::Color bgcolor, fgcolor; + int factor, xfactor; + unsigned char *sbitbuffer; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + int national_subset_local = m_txtCache->NationalSubset; + int curfontwidth = GetCurFontWidth(); + int t = curfontwidth; + m_RenderInfo.PosX += t; + int curfontwidth2 = GetCurFontWidth(); + m_RenderInfo.PosX -= t; + int alphachar = RenderChar(m_TextureBuffer+(yoffset)*m_RenderInfo.Width, m_RenderInfo.Width, Char, &m_RenderInfo.PosX, m_RenderInfo.PosY, Attribute, zoom > 0, curfontwidth, curfontwidth2, m_RenderInfo.FontHeight, m_RenderInfo.TranspMode, m_RenderInfo.axdrcs, m_Ascender); + if (alphachar <= 0) return; + + if (zoom && Attribute->doubleh) + factor = 4; + else if (zoom || Attribute->doubleh) + factor = 2; + else + factor = 1; + + fgcolor = GetColorRGB((enumTeletextColor)Attribute->fg); + if (m_RenderInfo.TranspMode && m_RenderInfo.PosY < 24*m_RenderInfo.FontHeight) + { + bgcolor = GetColorRGB(TXT_ColorTransp); + } + else + { + bgcolor = GetColorRGB((enumTeletextColor)Attribute->bg); + } + + if (Attribute->doublew) + { + curfontwidth += curfontwidth2; + xfactor = 2; + } + else + xfactor = 1; + + // Check if the alphanumeric char has diacritical marks (or results from composing chars) or + // on the other hand it is just a simple alphanumeric char + if (!Attribute->diacrit) + { + Char = alphachar; + } + else + { + if ((national_subset_local == NAT_SC) || (national_subset_local == NAT_RB) || + (national_subset_local == NAT_UA)) + Char = G2table[1][0x20 + Attribute->diacrit]; + else if (national_subset_local == NAT_GR) + Char = G2table[2][0x20 + Attribute->diacrit]; + else if (national_subset_local == NAT_HB) + Char = G2table[3][0x20 + Attribute->diacrit]; + else if (national_subset_local == NAT_AR) + Char = G2table[4][0x20 + Attribute->diacrit]; + else + Char = G2table[0][0x20 + Attribute->diacrit]; + + // use harfbuzz to combine the diacritical mark with the alphanumeric char + // fallback to the alphanumeric char if composition fails + hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_get_default(); + hb_codepoint_t composedChar; + const hb_bool_t isComposed = hb_unicode_compose(ufuncs, alphachar, Char, &composedChar); + Char = isComposed ? composedChar : alphachar; + } + + /* render char */ + if (!(glyph = FT_Get_Char_Index(m_Face, Char))) + { + CLog::Log(LOGERROR, "{}: <FT_Get_Char_Index for Char {:x} \"{}\" failed", __FUNCTION__, + alphachar, alphachar); + + FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY + yoffset, curfontwidth, factor*m_RenderInfo.FontHeight, bgcolor); + m_RenderInfo.PosX += curfontwidth; + return; + } + + if (FTC_SBitCache_Lookup(m_Cache, &m_TypeTTF, glyph, &m_sBit, &m_anode) != 0) + { + FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY + yoffset, curfontwidth, m_RenderInfo.FontHeight, bgcolor); + m_RenderInfo.PosX += curfontwidth; + return; + } + + sbitbuffer = m_sBit->buffer; + + int backupTTFshiftY = m_RenderInfo.TTFShiftY; + if (national_subset_local == NAT_AR) + m_RenderInfo.TTFShiftY = backupTTFshiftY - 2; // for arabic TTF font should be shifted up slightly + + UTILS::COLOR::Color* p; + int f; /* running counter for zoom factor */ + int he = m_sBit->height; // sbit->height should not be altered, I guess + Row = factor * (m_Ascender - m_sBit->top + m_RenderInfo.TTFShiftY); + if (Row < 0) + { + sbitbuffer -= m_sBit->pitch*Row; + he += Row; + Row = 0; + } + else + { + FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY + yoffset, curfontwidth, Row, bgcolor); /* fill upper margin */ + } + + if (m_Ascender - m_sBit->top + m_RenderInfo.TTFShiftY + he > m_RenderInfo.FontHeight) + he = m_RenderInfo.FontHeight - m_Ascender + m_sBit->top - m_RenderInfo.TTFShiftY; /* limit char height to defined/calculated FontHeight */ + if (he < 0) he = m_RenderInfo.FontHeight; + + p = m_TextureBuffer + m_RenderInfo.PosX + (yoffset + m_RenderInfo.PosY + Row) * m_RenderInfo.Width; /* running pointer into framebuffer */ + for (Row = he; Row; Row--) /* row counts up, but down may be a little faster :) */ + { + int pixtodo = m_sBit->width; + UTILS::COLOR::Color* pstart = p; + + for (int Bit = xfactor * (m_sBit->left + m_RenderInfo.TTFShiftX); Bit > 0; Bit--) /* fill left margin */ + { + for (f = factor-1; f >= 0; f--) + *(p + f*m_RenderInfo.Width) = bgcolor; + p++; + } + + for (Pitch = m_sBit->pitch; Pitch; Pitch--) + { + for (int Bit = 0x80; Bit; Bit >>= 1) + { + UTILS::COLOR::Color color; + + if (--pixtodo < 0) + break; + + if (*sbitbuffer & Bit) /* bit set -> foreground */ + color = fgcolor; + else /* bit not set -> background */ + color = bgcolor; + + for (f = factor-1; f >= 0; f--) + *(p + f*m_RenderInfo.Width) = color; + p++; + + if (xfactor > 1) /* double width */ + { + for (f = factor-1; f >= 0; f--) + *(p + f*m_RenderInfo.Width) = color; + p++; + } + } + sbitbuffer++; + } + for (int Bit = (curfontwidth - xfactor*(m_sBit->width + m_sBit->left + m_RenderInfo.TTFShiftX)); + Bit > 0; Bit--) /* fill rest of char width */ + { + for (f = factor-1; f >= 0; f--) + *(p + f*m_RenderInfo.Width) = bgcolor; + p++; + } + + p = pstart + factor*m_RenderInfo.Width; + } + + Row = m_Ascender - m_sBit->top + he + m_RenderInfo.TTFShiftY; + FillRect(m_TextureBuffer, + m_RenderInfo.Width, + m_RenderInfo.PosX, + m_RenderInfo.PosY + yoffset + Row * factor, + curfontwidth, + (m_RenderInfo.FontHeight - Row) * factor, + bgcolor); /* fill lower margin */ + + if (Attribute->underline) + FillRect(m_TextureBuffer, + m_RenderInfo.Width, + m_RenderInfo.PosX, + m_RenderInfo.PosY + yoffset + (m_RenderInfo.FontHeight-2)* factor, + curfontwidth, + 2*factor, + fgcolor); /* underline char */ + + m_RenderInfo.PosX += curfontwidth; + m_RenderInfo.TTFShiftY = backupTTFshiftY; // restore TTFShiftY +} + +int CTeletextDecoder::RenderChar( + UTILS::COLOR::Color* buffer, // pointer to render buffer, min. FontHeight*2*xres + int xres, // length of 1 line in render buffer + int Char, // character to render + int* + pPosX, // left border for rendering relative to *buffer, will be set to right border after rendering + int PosY, // vertical position of char in *buffer + TextPageAttr_t* Attribute, // Attributes of Char + bool zoom, // 1= character will be rendered in double height + int curfontwidth, // rendering width of character + int curfontwidth2, // rendering width of next character (needed for doublewidth) + int FontHeight, // height of character + bool transpmode, // 1= transparent display + unsigned char* axdrcs, // width and height of DRCS-chars + int Ascender) // Ascender of font +{ + UTILS::COLOR::Color bgcolor, fgcolor; + int factor, xfactor; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + int national_subset_local = m_txtCache->NationalSubset; + int ymosaic[4]; + ymosaic[0] = 0; /* y-offsets for 2*3 mosaic */ + ymosaic[1] = (FontHeight + 1) / 3; + ymosaic[2] = (FontHeight * 2 + 1) / 3; + ymosaic[3] = FontHeight; + + if (Attribute->setX26) + { + national_subset_local = 0; // no national subset + } + + // G0+G2 set designation + if (Attribute->setG0G2 != 0x3f) + { + switch (Attribute->setG0G2) + { + case 0x20 : + national_subset_local = NAT_SC; + break; + case 0x24 : + national_subset_local = NAT_RB; + break; + case 0x25 : + national_subset_local = NAT_UA; + break; + case 0x37: + national_subset_local = NAT_GR; + break; + case 0x55: + national_subset_local = NAT_HB; + break; + case 0x47: + case 0x57: + national_subset_local = NAT_AR; + break; + default: + national_subset_local = CountryConversionTable[Attribute->setG0G2 & 0x07]; + break; + } + } + + if (Attribute->charset == C_G0S) // use secondary charset + national_subset_local = m_txtCache->NationalSubsetSecondary; + if (zoom && Attribute->doubleh) + factor = 4; + else if (zoom || Attribute->doubleh) + factor = 2; + else + factor = 1; + + if (Attribute->doublew) + { + curfontwidth += curfontwidth2; + xfactor = 2; + } + else + xfactor = 1; + + if (Char == 0xFF) /* skip doubleheight chars in lower line */ + { + *pPosX += curfontwidth; + return -1; + } + + /* get colors */ + if (Attribute->inverted) + { + int t = Attribute->fg; + Attribute->fg = Attribute->bg; + Attribute->bg = t; + } + fgcolor = GetColorRGB((enumTeletextColor)Attribute->fg); + if (transpmode == true && PosY < 24*FontHeight) + { + bgcolor = GetColorRGB(TXT_ColorTransp); + } + else + { + bgcolor = GetColorRGB((enumTeletextColor)Attribute->bg); + } + + /* handle mosaic */ + if ((Attribute->charset == C_G1C || Attribute->charset == C_G1S) && + ((Char&0xA0) == 0x20)) + { + int w1 = (curfontwidth / 2 ) *xfactor; + int w2 = (curfontwidth - w1) *xfactor; + + Char = (Char & 0x1f) | ((Char & 0x40) >> 1); + if (Attribute->charset == C_G1S) /* separated mosaic */ + { + for (int y = 0; y < 3; y++) + { + FillRectMosaicSeparated(buffer, xres,*pPosX, PosY + ymosaic[y]*factor, w1, (ymosaic[y+1] - ymosaic[y])*factor, fgcolor, bgcolor, Char & 0x01); + FillRectMosaicSeparated(buffer, xres,*pPosX + w1, PosY + ymosaic[y]*factor, w2, (ymosaic[y+1] - ymosaic[y])*factor, fgcolor, bgcolor, Char & 0x02); + Char >>= 2; + } + } + else + { + for (int y = 0; y < 3; y++) + { + FillRect(buffer, xres, *pPosX, PosY + ymosaic[y]*factor, w1, (ymosaic[y+1] - ymosaic[y])*factor, (Char & 0x01) ? fgcolor : bgcolor); + FillRect(buffer, xres, *pPosX + w1, PosY + ymosaic[y]*factor, w2, (ymosaic[y+1] - ymosaic[y])*factor, (Char & 0x02) ? fgcolor : bgcolor); + Char >>= 2; + } + } + + *pPosX += curfontwidth; + return 0; + } + + if (Attribute->charset == C_G3) + { + if (Char < 0x20 || Char > 0x7d) + { + Char = 0x20; + } + else + { + if (*aShapes[Char - 0x20] == S_CHR) + { + unsigned char *p = aShapes[Char - 0x20]; + Char = (*(p+1) <<8) + (*(p+2)); + } + else if (*aShapes[Char - 0x20] == S_ADT) + { + if (buffer) + { + int x,y,f,c; + UTILS::COLOR::Color* p = buffer + *pPosX + PosY * xres; + for (y=0; y<FontHeight;y++) + { + for (f=0; f<factor; f++) + { + for (x=0; x<curfontwidth*xfactor;x++) + { + c = (y&4 ? (x/3)&1 :((x+3)/3)&1); + *(p+x) = (c ? fgcolor : bgcolor); + } + p += xres; + } + } + } + *pPosX += curfontwidth; + return 0; + } + else + { + DrawShape(buffer, xres,*pPosX, PosY, Char, curfontwidth, FontHeight, factor*FontHeight, fgcolor, bgcolor, true); + *pPosX += curfontwidth; + return 0; + } + } + } + else if (Attribute->charset >= C_OFFSET_DRCS) + { + TextCachedPage_t *pcache = m_txtCache->astCachetable[(Attribute->charset & 0x10) ? m_txtCache->drcs : m_txtCache->gdrcs][Attribute->charset & 0x0f]; + if (pcache) + { + unsigned char drcs_data[23*40]; + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->LoadPage((Attribute->charset & 0x10) ? m_txtCache->drcs : m_txtCache->gdrcs, + Attribute->charset & 0x0f, drcs_data); + unsigned char *p; + if (Char < 23*2) + p = drcs_data + 20*Char; + else if (pcache->pageinfo.p24) + p = pcache->pageinfo.p24 + 20*(Char - 23*2); + else + { + FillRect(buffer, xres,*pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor); + *pPosX += curfontwidth; + return 0; + } + axdrcs[12] = curfontwidth; /* adjust last x-offset according to position, FIXME: double width */ + RenderDRCS(xres, p, buffer + *pPosX + PosY * xres, axdrcs, fgcolor, bgcolor); + } + else + { + FillRect(buffer,xres,*pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor); + } + *pPosX += curfontwidth; + return 0; + } + else if (Attribute->charset == C_G2 && Char >= 0x20 && Char <= 0x7F) + { + if ((national_subset_local == NAT_SC) || (national_subset_local == NAT_RB) || (national_subset_local == NAT_UA)) + Char = G2table[1][Char-0x20]; + else if (national_subset_local == NAT_GR) + Char = G2table[2][Char-0x20]; + else if (national_subset_local == NAT_AR) + Char = G2table[3][Char-0x20]; + else + Char = G2table[0][Char-0x20]; + + //if (Char == 0x7F) + //{ + // FillRect(buffer,xres,*pPosX, PosY, curfontwidth, factor*Ascender, fgcolor); + // FillRect(buffer,xres,*pPosX, PosY + factor*Ascender, curfontwidth, factor*(FontHeight-Ascender), bgcolor); + // *pPosX += curfontwidth; + // return 0; + //} + } + else if (national_subset_local == NAT_SC && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for serbian/croatian */ + Char = G0table[0][Char-0x20]; + else if (national_subset_local == NAT_RB && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for russian/bulgarian */ + Char = G0table[1][Char-0x20]; + else if (national_subset_local == NAT_UA && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for ukrainian */ + Char = G0table[2][Char-0x20]; + else if (national_subset_local == NAT_GR && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for greek */ + Char = G0table[3][Char-0x20]; + else if (national_subset_local == NAT_HB && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for hebrew */ + Char = G0table[4][Char-0x20]; + else if (national_subset_local == NAT_AR && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for arabic */ + Char = G0table[5][Char-0x20]; + else + { + /* load char */ + switch (Char) + { + case 0x00: + case 0x20: + FillRect(buffer, xres, *pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor); + *pPosX += curfontwidth; + return -3; + case 0x23: + case 0x24: + Char = nationaltable23[national_subset_local][Char-0x23]; + break; + case 0x40: + Char = nationaltable40[national_subset_local]; + break; + case 0x5B: + case 0x5C: + case 0x5D: + case 0x5E: + case 0x5F: + case 0x60: + Char = nationaltable5b[national_subset_local][Char-0x5B]; + break; + case 0x7B: + case 0x7C: + case 0x7D: + case 0x7E: + Char = nationaltable7b[national_subset_local][Char-0x7B]; + break; + case 0x7F: + FillRect(buffer,xres,*pPosX, PosY , curfontwidth, factor*Ascender, fgcolor); + FillRect(buffer,xres,*pPosX, PosY + factor*Ascender, curfontwidth, factor*(FontHeight-Ascender), bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE0: /* |- */ + DrawHLine(buffer,xres,*pPosX, PosY, curfontwidth, fgcolor); + DrawVLine(buffer,xres,*pPosX, PosY +1, FontHeight -1, fgcolor); + FillRect(buffer,xres,*pPosX +1, PosY +1, curfontwidth-1, FontHeight-1, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE1: /* - */ + DrawHLine(buffer,xres,*pPosX, PosY, curfontwidth, fgcolor); + FillRect(buffer,xres,*pPosX, PosY +1, curfontwidth, FontHeight-1, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE2: /* -| */ + DrawHLine(buffer,xres,*pPosX, PosY, curfontwidth, fgcolor); + DrawVLine(buffer,xres,*pPosX + curfontwidth -1, PosY +1, FontHeight -1, fgcolor); + FillRect(buffer,xres,*pPosX, PosY +1, curfontwidth-1, FontHeight-1, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE3: /* | */ + DrawVLine(buffer,xres,*pPosX, PosY, FontHeight, fgcolor); + FillRect(buffer,xres,*pPosX +1, PosY, curfontwidth -1, FontHeight, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE4: /* | */ + DrawVLine(buffer,xres,*pPosX + curfontwidth -1, PosY, FontHeight, fgcolor); + FillRect(buffer,xres,*pPosX, PosY, curfontwidth -1, FontHeight, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE5: /* |_ */ + DrawHLine(buffer,xres,*pPosX, PosY + FontHeight -1, curfontwidth, fgcolor); + DrawVLine(buffer,xres,*pPosX, PosY, FontHeight -1, fgcolor); + FillRect(buffer,xres,*pPosX +1, PosY, curfontwidth-1, FontHeight-1, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE6: /* _ */ + DrawHLine(buffer,xres,*pPosX, PosY + FontHeight -1, curfontwidth, fgcolor); + FillRect(buffer,xres,*pPosX, PosY, curfontwidth, FontHeight-1, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE7: /* _| */ + DrawHLine(buffer,xres,*pPosX, PosY + FontHeight -1, curfontwidth, fgcolor); + DrawVLine(buffer,xres,*pPosX + curfontwidth -1, PosY, FontHeight -1, fgcolor); + FillRect(buffer,xres,*pPosX, PosY, curfontwidth-1, FontHeight-1, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE8: /* Ii */ + FillRect(buffer,xres,*pPosX +1, PosY, curfontwidth -1, FontHeight, bgcolor); + for (int Row=0; Row < curfontwidth/2; Row++) + DrawVLine(buffer,xres,*pPosX + Row, PosY + Row, FontHeight - Row, fgcolor); + *pPosX += curfontwidth; + return 0; + case 0xE9: /* II */ + FillRect(buffer,xres,*pPosX, PosY, curfontwidth/2, FontHeight, fgcolor); + FillRect(buffer,xres,*pPosX + curfontwidth/2, PosY, (curfontwidth+1)/2, FontHeight, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xEA: /* ∞ */ + FillRect(buffer,xres,*pPosX, PosY, curfontwidth, FontHeight, bgcolor); + FillRect(buffer,xres,*pPosX, PosY, curfontwidth/2, curfontwidth/2, fgcolor); + *pPosX += curfontwidth; + return 0; + case 0xEB: /* ¨ */ + FillRect(buffer,xres,*pPosX, PosY +1, curfontwidth, FontHeight -1, bgcolor); + for (int Row=0; Row < curfontwidth/2; Row++) + DrawHLine(buffer,xres,*pPosX + Row, PosY + Row, curfontwidth - Row, fgcolor); + *pPosX += curfontwidth; + return 0; + case 0xEC: /* -- */ + FillRect(buffer, xres,*pPosX, PosY, curfontwidth, curfontwidth/2, fgcolor); + FillRect(buffer, xres,*pPosX, PosY + curfontwidth/2, curfontwidth, FontHeight - curfontwidth/2, bgcolor); + *pPosX += curfontwidth; + return 0; + case 0xED: + case 0xEE: + case 0xEF: + case 0xF0: + case 0xF1: + case 0xF2: + case 0xF3: + case 0xF4: + Char = arrowtable[Char - 0xED]; + break; + default: + break; + } + } + if (Char <= 0x20) + { + FillRect(buffer, xres, *pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor); + *pPosX += curfontwidth; + return -2; + } + return Char; // Char is an alphanumeric unicode character +} + +TextPageinfo_t* CTeletextDecoder::DecodePage(bool showl25, // 1=decode Level2.5-graphics + unsigned char* PageChar, // page buffer, min. 25*40 + TextPageAttr_t *PageAtrb, // attribute buffer, min 25*40 + bool HintMode, // 1=show hidden information + bool showflof) // 1=decode FLOF-line +{ + int col; + int hold, dhset; + int foreground, background, doubleheight, doublewidth, charset, previous_charset, mosaictype, IgnoreAtBlackBgSubst, concealed, flashmode, boxwin; + unsigned char held_mosaic, *p; + TextCachedPage_t *pCachedPage; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + /* copy page to decode buffer */ + if (m_txtCache->SubPageTable[m_txtCache->Page] == 0xff) /* not cached: do nothing */ + return NULL; + + if (m_txtCache->ZapSubpageManual) + pCachedPage = m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage]; + else + pCachedPage = m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPageTable[m_txtCache->Page]]; + if (!pCachedPage) /* not cached: do nothing */ + return nullptr; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->LoadPage(m_txtCache->Page, m_txtCache->SubPage, &PageChar[40]); + + memcpy(&PageChar[8], pCachedPage->p0, 24); /* header line without TimeString */ + + TextPageinfo_t* PageInfo = &(pCachedPage->pageinfo); + if (PageInfo->p24) + memcpy(&PageChar[24*40], PageInfo->p24, 40); /* line 25 for FLOF */ + + /* copy TimeString */ + memcpy(&PageChar[32], &m_txtCache->TimeString, 8); + + bool boxed; + /* check for newsflash & subtitle */ + if (PageInfo->boxed && IsDec(m_txtCache->Page)) + boxed = true; + else + boxed = false; + + + /* modify header */ + if (boxed) + { + memset(PageChar, ' ', 40); + } + else + { + memset(PageChar, ' ', 8); + CDVDTeletextTools::Hex2Str((char*)PageChar+3, m_txtCache->Page); + if (m_txtCache->SubPage) + { + *(PageChar+4) ='/'; + *(PageChar+5) ='0'; + CDVDTeletextTools::Hex2Str((char*)PageChar+6, m_txtCache->SubPage); + } + } + + if (!IsDec(m_txtCache->Page)) + { + TextPageAttr_t atr = { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}; + if (PageInfo->function == FUNC_MOT) /* magazine organization table */ + { + for (col = 0; col < 24*40; col++) + PageAtrb[col] = atr; + for (col = 40; col < 24*40; col++) + PageChar[col] = number2char(PageChar[col]); + return PageInfo; /* don't interpret irregular pages */ + } + else if (PageInfo->function == FUNC_GPOP || PageInfo->function == FUNC_POP) /* object definitions */ + { + for (int col = 0; col < 24*40; col++) + PageAtrb[col] = atr; + + p = PageChar + 40; + for (int row = 1; row < 12; row++) + { + *p++ = number2char(row); /* first column: number (0-9, A-..) */ + for (int col = 1; col < 40; col += 3) + { + int d = CDVDTeletextTools::deh24(p); + if (d < 0) + { + memcpy(p, "???", 3); + p += 3; + } + else + { + *p++ = number2char((d >> 6) & 0x1f); /* mode */ + *p++ = number2char(d & 0x3f); /* address */ + *p++ = number2char((d >> 11) & 0x7f); /* data */ + } + } + } + return PageInfo; /* don't interpret irregular pages */ + } + else if (PageInfo->function == FUNC_GDRCS || PageInfo->function == FUNC_DRCS) /* character definitions */ + { + return PageInfo; /* don't interpret irregular pages */ + } + else + { + int h, parityerror = 0; + + for (int i = 0; i < 8; i++) + PageAtrb[i] = atr; + + /* decode parity/hamming */ + for (unsigned int i = 40; i < TELETEXT_PAGE_SIZE; i++) + { + PageAtrb[i] = atr; + p = PageChar + i; + h = dehamming[*p]; + if (parityerror && h != 0xFF) /* if no regular page (after any parity error) */ + CDVDTeletextTools::Hex2Str((char*)p, h); /* first try dehamming */ + else + { + if (*p == ' ' || deparity[*p] != ' ') /* correct parity */ + *p &= 127; + else + { + parityerror = 1; + if (h != 0xFF) /* first parity error: try dehamming */ + CDVDTeletextTools::Hex2Str((char*)p, h); + else + *p = ' '; + } + } + } + if (parityerror) + { + return PageInfo; /* don't interpret irregular pages */ + } + } + } + int mosaic_pending,esc_pending; + /* decode */ + for (int row = 0; row < ((showflof && PageInfo->p24) ? 25 : 24); row++) + { + /* start-of-row default conditions */ + foreground = TXT_ColorWhite; + background = TXT_ColorBlack; + doubleheight = 0; + doublewidth = 0; + charset = previous_charset = C_G0P; // remember charset for switching back after mosaic charset was used + mosaictype = 0; + concealed = 0; + flashmode = 0; + hold = 0; + boxwin = 0; + held_mosaic = ' '; + dhset = 0; + IgnoreAtBlackBgSubst = 0; + mosaic_pending = esc_pending = 0; // we need to render at least one mosaic char if 'esc' is received immediately after mosaic charset switch on + + if (boxed && memchr(&PageChar[row*40], start_box, 40) == 0) + { + foreground = TXT_ColorTransp; + background = TXT_ColorTransp; + } + + for (int col = 0; col < 40; col++) + { + int index = row*40 + col; + + PageAtrb[index].fg = foreground; + PageAtrb[index].bg = background; + PageAtrb[index].charset = charset; + PageAtrb[index].doubleh = doubleheight; + PageAtrb[index].doublew = (col < 39 ? doublewidth : 0); + PageAtrb[index].IgnoreAtBlackBgSubst = IgnoreAtBlackBgSubst; + PageAtrb[index].concealed = concealed; + PageAtrb[index].flashing = flashmode; + PageAtrb[index].boxwin = boxwin; + PageAtrb[index].inverted = 0; // only relevant for Level 2.5 + PageAtrb[index].underline = 0; // only relevant for Level 2.5 + PageAtrb[index].diacrit = 0; // only relevant for Level 2.5 + PageAtrb[index].setX26 = 0; // only relevant for Level 2.5 + PageAtrb[index].setG0G2 = 0x3f; // only relevant for Level 2.5 + + if (PageChar[index] < ' ') + { + if (esc_pending) { // mosaic char has been rendered and we can switch charsets + charset = previous_charset; + if (charset == C_G0P) + charset = previous_charset = C_G0S; + else if (charset == C_G0S) + charset = previous_charset = C_G0P; + esc_pending = 0; + } + switch (PageChar[index]) + { + case alpha_black: + case alpha_red: + case alpha_green: + case alpha_yellow: + case alpha_blue: + case alpha_magenta: + case alpha_cyan: + case alpha_white: + concealed = 0; + foreground = PageChar[index] - alpha_black + TXT_ColorBlack; + if (col == 0 && PageChar[index] == alpha_white) + PageAtrb[index].fg = TXT_ColorBlack; // indicate level 1 color change on column 0; (hack) + if ((charset!=C_G0P) && (charset!=C_G0S)) // we need to change charset to state it was before mosaic + charset = previous_charset; + break; + + case flash: + flashmode = 1; + break; + + case steady: + flashmode = 0; + PageAtrb[index].flashing = 0; + break; + + case end_box: + boxwin = 0; + IgnoreAtBlackBgSubst = 0; + break; + + case start_box: + if (!boxwin) + boxwin = 1; + break; + + case normal_size: + doubleheight = 0; + doublewidth = 0; + PageAtrb[index].doubleh = doubleheight; + PageAtrb[index].doublew = doublewidth; + break; + + case double_height: + if (row < 23) + { + doubleheight = 1; + dhset = 1; + } + doublewidth = 0; + + break; + + case double_width: + if (col < 39) + doublewidth = 1; + doubleheight = 0; + break; + + case double_size: + if (row < 23) + { + doubleheight = 1; + dhset = 1; + } + if (col < 39) + doublewidth = 1; + break; + + case mosaic_black: + case mosaic_red: + case mosaic_green: + case mosaic_yellow: + case mosaic_blue: + case mosaic_magenta: + case mosaic_cyan: + case mosaic_white: + concealed = 0; + foreground = PageChar[index] - mosaic_black + TXT_ColorBlack; + if ((charset==C_G0P) || (charset==C_G0S)) + previous_charset=charset; + charset = mosaictype ? C_G1S : C_G1C; + mosaic_pending = 1; + break; + + case conceal: + PageAtrb[index].concealed = 1; + concealed = 1; + if (!HintMode) + { + foreground = background; + PageAtrb[index].fg = foreground; + } + break; + + case contiguous_mosaic: + mosaictype = 0; + if (charset == C_G1S) + { + charset = C_G1C; + PageAtrb[index].charset = charset; + } + break; + + case separated_mosaic: + mosaictype = 1; + if (charset == C_G1C) + { + charset = C_G1S; + PageAtrb[index].charset = charset; + } + break; + + case esc: + if (!mosaic_pending) { // if mosaic is pending we need to wait before mosaic arrives + if ((charset != C_G0P) && (charset != C_G0S)) // we need to switch to charset which was active before mosaic + charset = previous_charset; + if (charset == C_G0P) + charset = previous_charset = C_G0S; + else if (charset == C_G0S) + charset = previous_charset = C_G0P; + } else esc_pending = 1; + break; + + case black_background: + background = TXT_ColorBlack; + IgnoreAtBlackBgSubst = 0; + PageAtrb[index].bg = background; + PageAtrb[index].IgnoreAtBlackBgSubst = IgnoreAtBlackBgSubst; + break; + + case new_background: + background = foreground; + if (background == TXT_ColorBlack) + IgnoreAtBlackBgSubst = 1; + else + IgnoreAtBlackBgSubst = 0; + PageAtrb[index].bg = background; + PageAtrb[index].IgnoreAtBlackBgSubst = IgnoreAtBlackBgSubst; + break; + + case hold_mosaic: + hold = 1; + break; + + case release_mosaic: + hold = 2; + break; + } + + /* handle spacing attributes */ + if (hold && (PageAtrb[index].charset == C_G1C || PageAtrb[index].charset == C_G1S)) + PageChar[index] = held_mosaic; + else + PageChar[index] = ' '; + + if (hold == 2) + hold = 0; + } + else /* char >= ' ' */ + { + mosaic_pending = 0; // charset will be switched next if esc_pending + /* set new held-mosaic char */ + if ((charset == C_G1C || charset == C_G1S) && + ((PageChar[index]&0xA0) == 0x20)) + held_mosaic = PageChar[index]; + if (PageAtrb[index].doubleh) + PageChar[index + 40] = 0xFF; + + } + if (!(charset == C_G1C || charset == C_G1S)) + held_mosaic = ' '; /* forget if outside mosaic */ + + } /* for col */ + + /* skip row if doubleheight */ + if (row < 23 && dhset) + { + for (int col = 0; col < 40; col++) + { + int index = row*40 + col; + PageAtrb[index+40].bg = PageAtrb[index].bg; + PageAtrb[index+40].fg = TXT_ColorWhite; + if (!PageAtrb[index].doubleh) + PageChar[index+40] = ' '; + PageAtrb[index+40].flashing = 0; + PageAtrb[index+40].charset = C_G0P; + PageAtrb[index+40].doubleh = 0; + PageAtrb[index+40].doublew = 0; + PageAtrb[index+40].IgnoreAtBlackBgSubst = 0; + PageAtrb[index+40].concealed = 0; + PageAtrb[index+40].flashing = 0; + PageAtrb[index+40].boxwin = PageAtrb[index].boxwin; + } + row++; + } + } /* for row */ + m_txtCache->FullScrColor = TXT_ColorBlack; + + if (showl25) + Eval_l25(PageChar, PageAtrb, HintMode); + + /* handle Black Background Color Substitution and transparency (CLUT1#0) */ + { + int o = 0; + char bitmask ; + + for (unsigned char row : m_txtCache->FullRowColor) + { + for (int c = 0; c < 40; c++) + { + bitmask = (PageAtrb[o].bg == 0x08 ? 0x08 : 0x00) | (row == 0x08 ? 0x04 : 0x00) | (PageAtrb[o].boxwin <<1) | (int)boxed; + switch (bitmask) + { + case 0x08: + case 0x0b: + if (row == 0x08) + PageAtrb[o].bg = m_txtCache->FullScrColor; + else + PageAtrb[o].bg = row; + break; + case 0x01: + case 0x05: + case 0x09: + case 0x0a: + case 0x0c: + case 0x0d: + case 0x0e: + case 0x0f: + PageAtrb[o].bg = TXT_ColorTransp; + break; + } + bitmask = (PageAtrb[o].fg == 0x08 ? 0x08 : 0x00) | (row == 0x08 ? 0x04 : 0x00) | (PageAtrb[o].boxwin <<1) | (int)boxed; + switch (bitmask) + { + case 0x08: + case 0x0b: + if (row == 0x08) + PageAtrb[o].fg = m_txtCache->FullScrColor; + else + PageAtrb[o].fg = row; + break; + case 0x01: + case 0x05: + case 0x09: + case 0x0a: + case 0x0c: + case 0x0d: + case 0x0e: + case 0x0f: + PageAtrb[o].fg = TXT_ColorTransp; + break; + } + o++; + } + } + } + return PageInfo; +} + +void CTeletextDecoder::Eval_l25(unsigned char* PageChar, TextPageAttr_t *PageAtrb, bool HintMode) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + memset(m_txtCache->FullRowColor, 0, sizeof(m_txtCache->FullRowColor)); + m_txtCache->FullScrColor = TXT_ColorBlack; + m_txtCache->ColorTable = NULL; + + if (!m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage]) + return; + + /* normal page */ + if (IsDec(m_txtCache->Page)) + { + unsigned char APx0, APy0, APx, APy; + TextPageinfo_t *pi = &(m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage]->pageinfo); + TextCachedPage_t *pmot = m_txtCache->astCachetable[(m_txtCache->Page & 0xf00) | 0xfe][0]; + int p26Received = 0; + int BlackBgSubst = 0; + int ColorTableRemapping = 0; + + m_txtCache->pop = m_txtCache->gpop = m_txtCache->drcs = m_txtCache->gdrcs = 0; + + if (pi->ext) + { + TextExtData_t *e = pi->ext; + + if (e->p26[0]) + p26Received = 1; + + if (e->p27) + { + Textp27_t *p27 = e->p27; + if (p27[0].l25) + m_txtCache->gpop = p27[0].page; + if (p27[1].l25) + m_txtCache->pop = p27[1].page; + if (p27[2].l25) + m_txtCache->gdrcs = p27[2].page; + if (p27[3].l25) + m_txtCache->drcs = p27[3].page; + } + + if (e->p28Received) + { + m_txtCache->ColorTable = e->bgr; + BlackBgSubst = e->BlackBgSubst; + ColorTableRemapping = e->ColorTableRemapping; + memset(m_txtCache->FullRowColor, e->DefRowColor, sizeof(m_txtCache->FullRowColor)); + m_txtCache->FullScrColor = e->DefScreenColor; + m_txtCache->NationalSubset = SetNational(e->DefaultCharset); + m_txtCache->NationalSubsetSecondary = SetNational(e->SecondCharset); + } /* e->p28Received */ + } + + if (!m_txtCache->ColorTable && m_txtCache->astP29[m_txtCache->Page >> 8]) + { + TextExtData_t *e = m_txtCache->astP29[m_txtCache->Page >> 8]; + m_txtCache->ColorTable = e->bgr; + BlackBgSubst = e->BlackBgSubst; + ColorTableRemapping = e->ColorTableRemapping; + memset(m_txtCache->FullRowColor, e->DefRowColor, sizeof(m_txtCache->FullRowColor)); + m_txtCache->FullScrColor = e->DefScreenColor; + m_txtCache->NationalSubset = SetNational(e->DefaultCharset); + m_txtCache->NationalSubsetSecondary = SetNational(e->SecondCharset); + } + + if (ColorTableRemapping) + { + for (int i = 0; i < 25*40; i++) + { + PageAtrb[i].fg += MapTblFG[ColorTableRemapping - 1]; + if (!BlackBgSubst || PageAtrb[i].bg != TXT_ColorBlack || PageAtrb[i].IgnoreAtBlackBgSubst) + PageAtrb[i].bg += MapTblBG[ColorTableRemapping - 1]; + } + } + + /* determine ?pop/?drcs from MOT */ + if (pmot) + { + unsigned char pmot_data[23*40]; + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->LoadPage((m_txtCache->Page & 0xf00) | 0xfe, 0, pmot_data); + + unsigned char *p = pmot_data; /* start of link data */ + int o = 2 * (((m_txtCache->Page & 0xf0) >> 4) * 10 + (m_txtCache->Page & 0x0f)); /* offset of links for current page */ + int opop = p[o] & 0x07; /* index of POP link */ + int odrcs = p[o+1] & 0x07; /* index of DRCS link */ + unsigned char obj[3*4*4]; /* types* objects * (triplet,packet,subp,high) */ + unsigned char type,ct, tstart = 4*4; + memset(obj,0,sizeof(obj)); + + if (p[o] & 0x08) /* GPOP data used */ + { + if (!m_txtCache->gpop || !(p[18*40] & 0x08)) /* no p27 data or higher prio of MOT link */ + { + m_txtCache->gpop = ((p[18*40] << 8) | (p[18*40+1] << 4) | p[18*40+2]) & 0x7ff; + if ((m_txtCache->gpop & 0xff) == 0xff) + m_txtCache->gpop = 0; + else + { + if (m_txtCache->gpop < 0x100) + m_txtCache->gpop += 0x800; + if (!p26Received) + { + ct = 2; + while (ct) + { + ct--; + type = (p[18*40+5] >> 2*ct) & 0x03; + + if (type == 0) continue; + obj[(type-1)*(tstart)+ct*4 ] = 3 * ((p[18*40+7+ct*2] >> 1) & 0x03) + type; //triplet + obj[(type-1)*(tstart)+ct*4+1] = ((p[18*40+7+ct*2] & 0x08) >> 3) + 1 ; //packet + obj[(type-1)*(tstart)+ct*4+2] = p[18*40+6+ct*2] & 0x0f ; //subp + obj[(type-1)*(tstart)+ct*4+3] = p[18*40+7+ct*2] & 0x01 ; //high + } + } + } + } + } + if (opop) /* POP data used */ + { + opop = 18*40 + 10*opop; /* offset to POP link */ + if (!m_txtCache->pop || !(p[opop] & 0x08)) /* no p27 data or higher prio of MOT link */ + { + m_txtCache->pop = ((p[opop] << 8) | (p[opop+1] << 4) | p[opop+2]) & 0x7ff; + if ((m_txtCache->pop & 0xff) == 0xff) + m_txtCache->pop = 0; + else + { + if (m_txtCache->pop < 0x100) + m_txtCache->pop += 0x800; + if (!p26Received) + { + ct = 2; + while (ct) + { + ct--; + type = (p[opop+5] >> 2*ct) & 0x03; + + if (type == 0) continue; + obj[(type-1)*(tstart)+(ct+2)*4 ] = 3 * ((p[opop+7+ct*2] >> 1) & 0x03) + type; //triplet + obj[(type-1)*(tstart)+(ct+2)*4+1] = ((p[opop+7+ct*2] & 0x08) >> 3) + 1 ; //packet + obj[(type-1)*(tstart)+(ct+2)*4+2] = p[opop+6+ct*2] ; //subp + obj[(type-1)*(tstart)+(ct+2)*4+3] = p[opop+7+ct*2] & 0x01 ; //high + } + } + } + } + } + // eval default objects in correct order + for (int i = 0; i < 12; i++) + { + if (obj[i*4] != 0) + { + APx0 = APy0 = APx = APy = m_txtCache->tAPx = m_txtCache->tAPy = 0; + Eval_NumberedObject(i % 4 > 1 ? m_txtCache->pop : m_txtCache->gpop, obj[i*4+2], obj[i*4+1], obj[i*4], obj[i*4+3], &APx, &APy, &APx0, &APy0, PageChar, PageAtrb); + } + } + + if (p[o+1] & 0x08) /* GDRCS data used */ + { + if (!m_txtCache->gdrcs || !(p[20*40] & 0x08)) /* no p27 data or higher prio of MOT link */ + { + m_txtCache->gdrcs = ((p[20*40] << 8) | (p[20*40+1] << 4) | p[20*40+2]) & 0x7ff; + if ((m_txtCache->gdrcs & 0xff) == 0xff) + m_txtCache->gdrcs = 0; + else if (m_txtCache->gdrcs < 0x100) + m_txtCache->gdrcs += 0x800; + } + } + if (odrcs) /* DRCS data used */ + { + odrcs = 20*40 + 4*odrcs; /* offset to DRCS link */ + if (!m_txtCache->drcs || !(p[odrcs] & 0x08)) /* no p27 data or higher prio of MOT link */ + { + m_txtCache->drcs = ((p[odrcs] << 8) | (p[odrcs+1] << 4) | p[odrcs+2]) & 0x7ff; + if ((m_txtCache->drcs & 0xff) == 0xff) + m_txtCache->drcs = 0; + else if (m_txtCache->drcs < 0x100) + m_txtCache->drcs += 0x800; + } + } + if (m_txtCache->astCachetable[m_txtCache->gpop][0]) + m_txtCache->astCachetable[m_txtCache->gpop][0]->pageinfo.function = FUNC_GPOP; + if (m_txtCache->astCachetable[m_txtCache->pop][0]) + m_txtCache->astCachetable[m_txtCache->pop][0]->pageinfo.function = FUNC_POP; + if (m_txtCache->astCachetable[m_txtCache->gdrcs][0]) + m_txtCache->astCachetable[m_txtCache->gdrcs][0]->pageinfo.function = FUNC_GDRCS; + if (m_txtCache->astCachetable[m_txtCache->drcs][0]) + m_txtCache->astCachetable[m_txtCache->drcs][0]->pageinfo.function = FUNC_DRCS; + } /* if mot */ + + /* evaluate local extension data from p26 */ + if (p26Received) + { + APx0 = APy0 = APx = APy = m_txtCache->tAPx = m_txtCache->tAPy = 0; + Eval_Object(13 * (23-2 + 2), m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage], &APx, &APy, &APx0, &APy0, OBJ_ACTIVE, &PageChar[40], PageChar, PageAtrb); /* 1st triplet p26/0 */ + } + + { + int o = 0; + for (unsigned char row : m_txtCache->FullRowColor) + { + for (int c = 0; c < 40; c++) + { + if (BlackBgSubst && PageAtrb[o].bg == TXT_ColorBlack && !(PageAtrb[o].IgnoreAtBlackBgSubst)) + { + if (row == 0x08) + PageAtrb[o].bg = m_txtCache->FullScrColor; + else + PageAtrb[o].bg = row; + } + o++; + } + } + } + + if (!HintMode) + { + for (int i = 0; i < 25*40; i++) + { + if (PageAtrb[i].concealed) PageAtrb[i].fg = PageAtrb[i].bg; + } + } + } /* is_dec(page) */ +} + +/* dump interpreted object data to stdout */ +/* in: 18 bit object data */ +/* out: termination info, >0 if end of object */ +void CTeletextDecoder::Eval_Object(int iONr, TextCachedPage_t *pstCachedPage, + unsigned char *pAPx, unsigned char *pAPy, + unsigned char *pAPx0, unsigned char *pAPy0, + tObjType ObjType, unsigned char* pagedata, unsigned char* PageChar, TextPageAttr_t* PageAtrb) +{ + int iOData; + int iONr1 = iONr + 1; /* don't terminate after first triplet */ + unsigned char drcssubp=0, gdrcssubp=0; + signed char endcol = -1; /* last column to which to extend attribute changes */ + TextPageAttr_t attrPassive = { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 1 ,0, 0, 0, 0, 0, 0, 0, 0x3f}; /* current attribute for passive objects */ + + do + { + iOData = iTripletNumber2Data(iONr, pstCachedPage, pagedata); /* get triplet data, next triplet */ + if (iOData < 0) /* invalid number, not cached, or hamming error: terminate */ + break; + + if (endcol < 0) + { + if (ObjType == OBJ_ACTIVE) + { + endcol = 40; + } + else if (ObjType == OBJ_ADAPTIVE) /* search end of line */ + { + for (int i = iONr; i <= 506; i++) + { + int iTempOData = iTripletNumber2Data(i, pstCachedPage, pagedata); /* get triplet data, next triplet */ + int iAddress = (iTempOData ) & 0x3f; + int iMode = (iTempOData >> 6) & 0x1f; + //int iData = (iTempOData >> 11) & 0x7f; + if (iTempOData < 0 || /* invalid number, not cached, or hamming error: terminate */ + (iAddress >= 40 /* new row: row address and */ + && (iMode == 0x01 || /* Full Row Color or */ + iMode == 0x04 || /* Set Active Position */ + (iMode >= 0x15 && iMode <= 0x17) || /* Object Definition */ + iMode == 0x17))) /* Object Termination */ + break; + if (iAddress < 40 && iMode != 0x06) + endcol = iAddress; + } + } + } + iONr++; + } + while (0 == Eval_Triplet(iOData, pstCachedPage, pAPx, pAPy, pAPx0, pAPy0, &drcssubp, &gdrcssubp, &endcol, &attrPassive, pagedata, PageChar, PageAtrb) || iONr1 == iONr); /* repeat until termination reached */ +} + +void CTeletextDecoder::Eval_NumberedObject(int p, int s, int packet, int triplet, int high, + unsigned char *pAPx, unsigned char *pAPy, + unsigned char *pAPx0, unsigned char *pAPy0, unsigned char* PageChar, TextPageAttr_t* PageAtrb) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + if (!packet || 0 == m_txtCache->astCachetable[p][s]) + return; + + unsigned char pagedata[23*40]; + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->LoadPage(p, s, pagedata); + + int idata = CDVDTeletextTools::deh24(pagedata + 40*(packet-1) + 1 + 3*triplet); + int iONr; + + if (idata < 0) /* hamming error: ignore triplet */ + return; + if (high) + iONr = idata >> 9; /* triplet number of odd object data */ + else + iONr = idata & 0x1ff; /* triplet number of even object data */ + if (iONr <= 506) + { + Eval_Object(iONr, m_txtCache->astCachetable[p][s], pAPx, pAPy, pAPx0, pAPy0, (tObjType)(triplet % 3),pagedata, PageChar, PageAtrb); + } +} + +int CTeletextDecoder::Eval_Triplet(int iOData, TextCachedPage_t *pstCachedPage, + unsigned char *pAPx, unsigned char *pAPy, + unsigned char *pAPx0, unsigned char *pAPy0, + unsigned char *drcssubp, unsigned char *gdrcssubp, + signed char *endcol, TextPageAttr_t *attrPassive, unsigned char* pagedata, unsigned char* PageChar, TextPageAttr_t* PageAtrb) +{ + int iAddress = (iOData ) & 0x3f; + int iMode = (iOData >> 6) & 0x1f; + int iData = (iOData >> 11) & 0x7f; + + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + if (iAddress < 40) /* column addresses */ + { + int offset; /* offset to PageChar and PageAtrb */ + + if (iMode != 0x06) + *pAPx = iAddress; /* new Active Column */ + offset = (*pAPy0 + *pAPy) * 40 + *pAPx0 + *pAPx; /* offset to PageChar and PageAtrb */ + + switch (iMode) + { + case 0x00: + if (0 == (iData>>5)) + { + int newcolor = iData & 0x1f; + if (*endcol < 0) /* passive object */ + attrPassive->fg = newcolor; + else if (*endcol == 40) /* active object */ + { + TextPageAttr_t *p = &PageAtrb[offset]; + int oldcolor = (p)->fg; /* current color (set-after) */ + int c = *pAPx0 + *pAPx; /* current column absolute */ + do + { + p->fg = newcolor; + p++; + c++; + } while (c < 40 && p->fg == oldcolor); /* stop at change by level 1 page */ + } + else /* adaptive object */ + { + TextPageAttr_t *p = &PageAtrb[offset]; + int c = *pAPx; /* current column relative to object origin */ + do + { + p->fg = newcolor; + p++; + c++; + } while (c <= *endcol); + } + } + break; + case 0x01: + if (iData >= 0x20) + { + PageChar[offset] = iData; + if (*endcol < 0) /* passive object */ + { + attrPassive->charset = C_G1C; /* FIXME: separated? */ + PageAtrb[offset] = *attrPassive; + } + else if (PageAtrb[offset].charset != C_G1S) + PageAtrb[offset].charset = C_G1C; /* FIXME: separated? */ + } + break; + case 0x02: + case 0x0b: + PageChar[offset] = iData; + if (*endcol < 0) /* passive object */ + { + attrPassive->charset = C_G3; + PageAtrb[offset] = *attrPassive; + } + else + PageAtrb[offset].charset = C_G3; + break; + case 0x03: + if (0 == (iData>>5)) + { + int newcolor = iData & 0x1f; + if (*endcol < 0) /* passive object */ + attrPassive->bg = newcolor; + else if (*endcol == 40) /* active object */ + { + TextPageAttr_t *p = &PageAtrb[offset]; + int oldcolor = (p)->bg; /* current color (set-after) */ + int c = *pAPx0 + *pAPx; /* current column absolute */ + do + { + p->bg = newcolor; + if (newcolor == TXT_ColorBlack) + p->IgnoreAtBlackBgSubst = 1; + p++; + c++; + } while (c < 40 && p->bg == oldcolor); /* stop at change by level 1 page */ + } + else /* adaptive object */ + { + TextPageAttr_t *p = &PageAtrb[offset]; + int c = *pAPx; /* current column relative to object origin */ + do + { + p->bg = newcolor; + if (newcolor == TXT_ColorBlack) + p->IgnoreAtBlackBgSubst = 1; + p++; + c++; + } while (c <= *endcol); + } + } + break; + case 0x06: + /* ignore */ + break; + case 0x07: + if ((iData & 0x60) != 0) break; // reserved data field + if (*endcol < 0) /* passive object */ + { + attrPassive->flashing=iData & 0x1f; + PageAtrb[offset] = *attrPassive; + } + else + PageAtrb[offset].flashing=iData & 0x1f; + break; + case 0x08: + if (*endcol < 0) /* passive object */ + { + attrPassive->setG0G2=iData & 0x3f; + PageAtrb[offset] = *attrPassive; + } + else + PageAtrb[offset].setG0G2=iData & 0x3f; + break; + case 0x09: + PageChar[offset] = iData; + if (*endcol < 0) /* passive object */ + { + attrPassive->charset = C_G0P; /* FIXME: secondary? */ + attrPassive->setX26 = 1; + PageAtrb[offset] = *attrPassive; + } + else + { + PageAtrb[offset].charset = C_G0P; /* FIXME: secondary? */ + PageAtrb[offset].setX26 = 1; + } + break; +// case 0x0b: (see 0x02) + case 0x0c: + { + int conc = (iData & 0x04); + int inv = (iData & 0x10); + int dw = (iData & 0x40 ?1:0); + int dh = (iData & 0x01 ?1:0); + int sep = (iData & 0x20); + int bw = (iData & 0x02 ?1:0); + if (*endcol < 0) /* passive object */ + { + if (conc) + { + attrPassive->concealed = 1; + attrPassive->fg = attrPassive->bg; + } + attrPassive->inverted = (inv ? 1- attrPassive->inverted : 0); + attrPassive->doubleh = dh; + attrPassive->doublew = dw; + attrPassive->boxwin = bw; + if (bw) attrPassive->IgnoreAtBlackBgSubst = 0; + if (sep) + { + if (attrPassive->charset == C_G1C) + attrPassive->charset = C_G1S; + else + attrPassive->underline = 1; + } + else + { + if (attrPassive->charset == C_G1S) + attrPassive->charset = C_G1C; + else + attrPassive->underline = 0; + } + } + else + { + + int c = *pAPx0 + (*endcol == 40 ? *pAPx : 0); /* current column */ + int c1 = offset; + TextPageAttr_t *p = &PageAtrb[offset]; + do + { + p->inverted = (inv ? 1- p->inverted : 0); + if (conc) + { + p->concealed = 1; + p->fg = p->bg; + } + if (sep) + { + if (p->charset == C_G1C) + p->charset = C_G1S; + else + p->underline = 1; + } + else + { + if (p->charset == C_G1S) + p->charset = C_G1C; + else + p->underline = 0; + } + p->doublew = dw; + p->doubleh = dh; + p->boxwin = bw; + if (bw) p->IgnoreAtBlackBgSubst = 0; + p++; + c++; + c1++; + } while (c < *endcol); + } + break; + } + case 0x0d: + PageChar[offset] = iData & 0x3f; + if (*endcol < 0) /* passive object */ + { + attrPassive->charset = C_OFFSET_DRCS + ((iData & 0x40) ? (0x10 + *drcssubp) : *gdrcssubp); + PageAtrb[offset] = *attrPassive; + } + else + PageAtrb[offset].charset = C_OFFSET_DRCS + ((iData & 0x40) ? (0x10 + *drcssubp) : *gdrcssubp); + break; + case 0x0f: + PageChar[offset] = iData; + if (*endcol < 0) /* passive object */ + { + attrPassive->charset = C_G2; + PageAtrb[offset] = *attrPassive; + } + else + PageAtrb[offset].charset = C_G2; + break; + default: + if (iMode == 0x10 && iData == 0x2a) + iData = '@'; + if (iMode >= 0x10) + { + PageChar[offset] = iData; + if (*endcol < 0) /* passive object */ + { + attrPassive->charset = C_G0P; + attrPassive->diacrit = iMode & 0x0f; + attrPassive->setX26 = 1; + PageAtrb[offset] = *attrPassive; + } + else + { + PageAtrb[offset].charset = C_G0P; + PageAtrb[offset].diacrit = iMode & 0x0f; + PageAtrb[offset].setX26 = 1; + } + } + break; /* unsupported or not yet implemented mode: ignore */ + } /* switch (iMode) */ + } + else /* ================= (iAddress >= 40): row addresses ====================== */ + { + switch (iMode) + { + case 0x00: + if (0 == (iData>>5)) + { + m_txtCache->FullScrColor = iData & 0x1f; + } + break; + case 0x01: + if (*endcol == 40) /* active object */ + { + *pAPy = RowAddress2Row(iAddress); /* new Active Row */ + + int color = iData & 0x1f; + int row = *pAPy0 + *pAPy; + int maxrow; + + if (row <= 24 && 0 == (iData>>5)) + maxrow = row; + else if (3 == (iData>>5)) + maxrow = 24; + else + maxrow = -1; + for (; row <= maxrow; row++) + m_txtCache->FullRowColor[row] = color; + *endcol = -1; + } + break; + case 0x04: + *pAPy = RowAddress2Row(iAddress); /* new Active Row */ + if (iData < 40) + *pAPx = iData; /* new Active Column */ + *endcol = -1; /* FIXME: check if row changed? */ + break; + case 0x07: + if (iAddress == 0x3f) + { + *pAPx = *pAPy = 0; /* new Active Position 0,0 */ + if (*endcol == 40) /* active object */ + { + int color = iData & 0x1f; + int row = *pAPy0; // + *pAPy; + int maxrow; + + if (row <= 24 && 0 == (iData>>5)) + maxrow = row; + else if (3 == (iData>>5)) + maxrow = 24; + else + maxrow = -1; + for (; row <= maxrow; row++) + m_txtCache->FullRowColor[row] = color; + } + *endcol = -1; + } + break; + case 0x08: + case 0x09: + case 0x0a: + case 0x0b: + case 0x0c: + case 0x0d: + case 0x0e: + case 0x0f: + /* ignore */ + break; + case 0x10: + m_txtCache->tAPy = iAddress - 40; + m_txtCache->tAPx = iData; + break; + case 0x11: + case 0x12: + case 0x13: + if (iAddress & 0x10) /* POP or GPOP */ + { + unsigned char APx = 0, APy = 0; + unsigned char APx0 = *pAPx0 + *pAPx + m_txtCache->tAPx, APy0 = *pAPy0 + *pAPy + m_txtCache->tAPy; + int triplet = 3 * ((iData >> 5) & 0x03) + (iMode & 0x03); + int packet = (iAddress & 0x03) + 1; + int subp = iData & 0x0f; + int high = (iData >> 4) & 0x01; + + + if (APx0 < 40) /* not in side panel */ + { + Eval_NumberedObject((iAddress & 0x08) ? m_txtCache->gpop : m_txtCache->pop, subp, packet, triplet, high, &APx, &APy, &APx0, &APy0, PageChar,PageAtrb); + } + } + else if (iAddress & 0x08) /* local: eval invoked object */ + { + unsigned char APx = 0, APy = 0; + unsigned char APx0 = *pAPx0 + *pAPx + m_txtCache->tAPx, APy0 = *pAPy0 + *pAPy + m_txtCache->tAPy; + int descode = ((iAddress & 0x01) << 3) | (iData >> 4); + int triplet = iData & 0x0f; + + if (APx0 < 40) /* not in side panel */ + { + Eval_Object(13 * 23 + 13 * descode + triplet, pstCachedPage, &APx, &APy, &APx0, &APy0, (tObjType)(triplet % 3), pagedata, PageChar, PageAtrb); + } + } + break; + case 0x15: + case 0x16: + case 0x17: + if (0 == (iAddress & 0x08)) /* Object Definition illegal or only level 3.5 */ + break; /* ignore */ + + m_txtCache->tAPx = m_txtCache->tAPy = 0; + *endcol = -1; + return 0xFF; /* termination by object definition */ + break; + case 0x18: + if (0 == (iData & 0x10)) /* DRCS Mode reserved or only level 3.5 */ + break; /* ignore */ + + if (iData & 0x40) + *drcssubp = iData & 0x0f; + else + *gdrcssubp = iData & 0x0f; + break; + case 0x1f: + m_txtCache->tAPx = m_txtCache->tAPy = 0; + *endcol = -1; + return 0x80 | iData; /* explicit termination */ + break; + default: + break; /* unsupported or not yet implemented mode: ignore */ + } /* switch (iMode) */ + } /* (iAddress >= 40): row addresses */ + + if (iAddress < 40 || iMode != 0x10) /* leave temp. AP-Offset unchanged only immediately after definition */ + m_txtCache->tAPx = m_txtCache->tAPy = 0; + + return 0; /* normal exit, no termination */ +} + +/* get object data */ +/* in: absolute triplet number (0..506, start at packet 3 byte 1) */ +/* in: pointer to cache struct of page data */ +/* out: 18 bit triplet data, <0 if invalid number, not cached, or hamming error */ +int CTeletextDecoder::iTripletNumber2Data(int iONr, TextCachedPage_t *pstCachedPage, unsigned char* pagedata) +{ + if (iONr > 506 || 0 == pstCachedPage) + return -1; + + unsigned char *p; + int packet = (iONr / 13) + 3; + int packetoffset = 3 * (iONr % 13); + + if (packet <= 23) + p = pagedata + 40*(packet-1) + packetoffset + 1; + else if (packet <= 25) + { + if (0 == pstCachedPage->pageinfo.p24) + return -1; + p = pstCachedPage->pageinfo.p24 + 40*(packet-24) + packetoffset + 1; + } + else + { + int descode = packet - 26; + if (0 == pstCachedPage->pageinfo.ext) + return -1; + if (0 == pstCachedPage->pageinfo.ext->p26[descode]) + return -1; + p = pstCachedPage->pageinfo.ext->p26[descode] + packetoffset; /* first byte (=designation code) is not cached */ + } + return CDVDTeletextTools::deh24(p); +} + +int CTeletextDecoder::SetNational(unsigned char sec) +{ + std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection); + + switch (sec) + { + case 0x08: + return NAT_PL; //polish + case 0x16: + case 0x36: + return NAT_TR; //turkish + case 0x1d: + return NAT_SR; //serbian, croatian, slovenian + case 0x20: + return NAT_SC; // serbian, croatian + case 0x24: + return NAT_RB; // russian, bulgarian + case 0x25: + return NAT_UA; // ukrainian + case 0x22: + return NAT_ET; // estonian + case 0x23: + return NAT_LV; // latvian, lithuanian + case 0x37: + return NAT_GR; // greek + case 0x55: + return NAT_HB; // hebrew + case 0x47: + case 0x57: + return NAT_AR; // arabic + } + return CountryConversionTable[sec & 0x07]; +} + +int CTeletextDecoder::NextHex(int i) /* return next existing non-decimal page number */ +{ + int startpage = i; + if (startpage < 0x100) + startpage = 0x100; + + do + { + i++; + if (i > 0x8FF) + i = 0x100; + if (i == startpage) + break; + } while ((m_txtCache->SubPageTable[i] == 0xFF) || IsDec(i)); + return i; +} + +void CTeletextDecoder::SetColors(const unsigned short *pcolormap, int offset, int number) +{ + int j = offset; /* index in global color table */ + + for (int i = 0; i < number; i++) + { + int r = ((pcolormap[i] >> 8) & 0xf) << 4; + int g = ((pcolormap[i] >> 4) & 0xf) << 4; + int b = ((pcolormap[i]) & 0xf) << 4; + + if (m_RenderInfo.rd0[j] != r) + { + m_RenderInfo.rd0[j] = r; + } + if (m_RenderInfo.gn0[j] != g) + { + m_RenderInfo.gn0[j] = g; + } + if (m_RenderInfo.bl0[j] != b) + { + m_RenderInfo.bl0[j] = b; + } + j++; + } +} + +UTILS::COLOR::Color CTeletextDecoder::GetColorRGB(enumTeletextColor ttc) +{ + switch (ttc) + { + case TXT_ColorBlack: return 0xFF000000; + case TXT_ColorRed: return 0xFFFC1414; + case TXT_ColorGreen: return 0xFF24FC24; + case TXT_ColorYellow: return 0xFFFCC024; + case TXT_ColorBlue: return 0xFF0000FC; + case TXT_ColorMagenta: return 0xFFB000FC; + case TXT_ColorCyan: return 0xFF00FCFC; + case TXT_ColorWhite: return 0xFFFCFCFC; + case TXT_ColorTransp: return 0x00000000; + default: break; + } + + /* Get colors for CLUTs 2+3 */ + int index = (int)ttc; + UTILS::COLOR::Color color = (m_RenderInfo.tr0[index] << 24) | (m_RenderInfo.bl0[index] << 16) | + (m_RenderInfo.gn0[index] << 8) | m_RenderInfo.rd0[index]; + return color; +} + diff --git a/xbmc/video/Teletext.h b/xbmc/video/Teletext.h new file mode 100644 index 0000000..105d842 --- /dev/null +++ b/xbmc/video/Teletext.h @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "TeletextDefines.h" +#include "guilib/GUITexture.h" +#include "utils/ColorUtils.h" + +// stuff for freetype +#include <ft2build.h> + +#ifdef TARGET_WINDOWS_STORE +#define generic GenericFromFreeTypeLibrary +#endif + +#include FT_FREETYPE_H +#include FT_CACHE_H +#include FT_CACHE_SMALL_BITMAPS_H + +class CAction; + +typedef enum /* object type */ +{ + OBJ_PASSIVE, + OBJ_ACTIVE, + OBJ_ADAPTIVE +} tObjType; + +class CTeletextDecoder +{ +public: + CTeletextDecoder(); + virtual ~CTeletextDecoder(void); + + bool NeedRendering() { return m_updateTexture; } + void RenderingDone() { m_updateTexture = false; } + UTILS::COLOR::Color* GetTextureBuffer() + { + return m_TextureBuffer + (m_RenderInfo.Width * m_YOffset); + } + int GetHeight() { return m_RenderInfo.Height; } + int GetWidth() { return m_RenderInfo.Width; } + bool InitDecoder(); + void EndDecoder(); + void RenderPage(); + bool HandleAction(const CAction &action); + +private: + void PageInput(int Number); + void GetNextPageOne(bool up); + void GetNextSubPage(int offset); + bool IsSubtitlePage(int pageNumber) const; + void SwitchZoomMode(); + void SwitchTranspMode(); + void SwitchHintMode(); + void ColorKey(int target); + void StartPageCatching(); + void StopPageCatching(); + void CatchNextPage(int firstlineinc, int inc); + void RenderCatchedPage(); + void DoFlashing(int startrow); + void DoRenderPage(int startrow, int national_subset_bak); + void Decode_BTT(); + void Decode_ADIP(); + int TopText_GetNext(int startpage, int up, int findgroup); + void Showlink(int column, int linkpage); + void CreateLine25(); + void RenderCharFB(int Char, TextPageAttr_t *Attribute); + void RenderCharBB(int Char, TextPageAttr_t *Attribute); + void CopyBB2FB(); + void SetFontWidth(int newWidth); + int GetCurFontWidth(); + void SetPosX(int column); + void ClearBB(UTILS::COLOR::Color Color); + void ClearFB(UTILS::COLOR::Color Color); + void FillBorder(UTILS::COLOR::Color Color); + void FillRect( + UTILS::COLOR::Color* buffer, int xres, int x, int y, int w, int h, UTILS::COLOR::Color Color); + void DrawVLine( + UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color); + void DrawHLine( + UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color); + void FillRectMosaicSeparated(UTILS::COLOR::Color* lfb, + int xres, + int x, + int y, + int w, + int h, + UTILS::COLOR::Color fgcolor, + UTILS::COLOR::Color bgcolor, + int set); + void FillTrapez(UTILS::COLOR::Color* lfb, + int xres, + int x0, + int y0, + int l0, + int xoffset1, + int h, + int l1, + UTILS::COLOR::Color color); + void FlipHorz(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h); + void FlipVert(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h); + int ShapeCoord(int param, int curfontwidth, int curfontheight); + void DrawShape(UTILS::COLOR::Color* lfb, + int xres, + int x, + int y, + int shapenumber, + int curfontwidth, + int fontheight, + int curfontheight, + UTILS::COLOR::Color fgcolor, + UTILS::COLOR::Color bgcolor, + bool clear); + void RenderDRCS( + int xres, + unsigned char* s, /* pointer to char data, parity undecoded */ + UTILS::COLOR::Color* d, /* pointer to frame buffer of top left pixel */ + unsigned char* ax, /* array[0..12] of x-offsets, array[0..10] of y-offsets for each pixel */ + UTILS::COLOR::Color fgcolor, + UTILS::COLOR::Color bgcolor); + void RenderCharIntern(TextRenderInfo_t* RenderInfo, int Char, TextPageAttr_t *Attribute, int zoom, int yoffset); + int RenderChar( + UTILS::COLOR::Color* buffer, // pointer to render buffer, min. fontheight*2*xres + int xres, // length of 1 line in render buffer + int Char, // character to render + int* + pPosX, // left border for rendering relative to *buffer, will be set to right border after rendering + int PosY, // vertical position of char in *buffer + TextPageAttr_t* Attribute, // Attributes of Char + bool zoom, // 1= character will be rendered in double height + int curfontwidth, // rendering width of character + int curfontwidth2, // rendering width of next character (needed for doublewidth) + int fontheight, // height of character + bool transpmode, // 1= transparent display + unsigned char* axdrcs, // width and height of DRCS-chars + int Ascender); + TextPageinfo_t* DecodePage(bool showl25, // 1=decode Level2.5-graphics + unsigned char* PageChar, // page buffer, min. 25*40 + TextPageAttr_t *PageAtrb, // attribute buffer, min 25*40 + bool HintMode, // 1=show hidden information + bool showflof); // 1=decode FLOF-line + void Eval_l25(unsigned char* page_char, TextPageAttr_t *PageAtrb, bool HintMode); + void Eval_Object(int iONr, TextCachedPage_t *pstCachedPage, + unsigned char *pAPx, unsigned char *pAPy, + unsigned char *pAPx0, unsigned char *pAPy0, + tObjType ObjType, unsigned char* pagedata, unsigned char* page_char, TextPageAttr_t* PageAtrb); + void Eval_NumberedObject(int p, int s, int packet, int triplet, int high, + unsigned char *pAPx, unsigned char *pAPy, + unsigned char *pAPx0, unsigned char *pAPy0, unsigned char* page_char, TextPageAttr_t* PageAtrb); + int Eval_Triplet(int iOData, TextCachedPage_t *pstCachedPage, + unsigned char *pAPx, unsigned char *pAPy, + unsigned char *pAPx0, unsigned char *pAPy0, + unsigned char *drcssubp, unsigned char *gdrcssubp, + signed char *endcol, TextPageAttr_t *attrPassive, unsigned char* pagedata, unsigned char* page_char, TextPageAttr_t* PageAtrb); + int iTripletNumber2Data(int iONr, TextCachedPage_t *pstCachedPage, unsigned char* pagedata); + int SetNational(unsigned char sec); + int NextHex(int i); + void SetColors(const unsigned short *pcolormap, int offset, int number); + UTILS::COLOR::Color GetColorRGB(enumTeletextColor ttc); + + static FT_Error MyFaceRequester(FTC_FaceID face_id, FT_Library library, FT_Pointer request_data, FT_Face *aface); + + std::string m_teletextFont; /* Path to teletext font */ + int m_YOffset; /* Swap position for Front buffer and Back buffer */ + UTILS::COLOR::Color* m_TextureBuffer; /* Texture buffer to hold generated data */ + bool m_updateTexture; /* Update the texture if set */ + char prevHeaderPage; /* Needed for texture update if header is changed */ + char prevTimeSec; /* Needed for Time string update */ + + int m_CatchRow; /* for page catching */ + int m_CatchCol; /* " " " */ + int m_CatchedPage; /* " " " */ + int m_PCOldRow; /* " " " */ + int m_PCOldCol; /* " " " */ + + FT_Library m_Library; /* FreeType 2 data */ + FTC_Manager m_Manager; /* " " " */ + FTC_SBitCache m_Cache; /* " " " */ + FTC_SBit m_sBit; /* " " " */ + FT_Face m_Face; /* " " " */ + /*! An opaque handle to a cache node object. Each cache node is reference-counted. */ + FTC_Node m_anode; + FTC_ImageTypeRec m_TypeTTF; /* " " " */ + int m_Ascender; /* " " " */ + + int m_TempPage; /* Temporary page number for number input */ + int m_LastPage; /* Last selected Page */ + std::shared_ptr<TextCacheStruct_t> m_txtCache; /* Text cache generated by the VideoPlayer if Teletext present */ + TextRenderInfo_t m_RenderInfo; /* Rendering information of displayed Teletext page */ +}; diff --git a/xbmc/video/TeletextDefines.h b/xbmc/video/TeletextDefines.h new file mode 100644 index 0000000..1c4884e --- /dev/null +++ b/xbmc/video/TeletextDefines.h @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <chrono> +#include <string> + +#define FLOFSIZE 4 +#define SUBTITLE_CACHESIZE 50 +#define TELETEXT_PAGE_SIZE (40 * 25) + +#define number2char(c) ((c) + (((c) <= 9) ? '0' : ('A' - 10))) + +enum /* indices in atrtable */ +{ + ATR_WB, /* white on black */ + ATR_PassiveDefault, /* Default for passive objects: white on black, ignore at Black Background Color Substitution */ + ATR_L250, /* line25 */ + ATR_L251, /* line25 */ + ATR_L252, /* line25 */ + ATR_L253, /* line25 */ + ATR_TOPMENU0, /* topmenu */ + ATR_TOPMENU1, /* topmenu */ + ATR_TOPMENU2, /* topmenu */ + ATR_TOPMENU3, /* topmenu */ + ATR_MSG0, /* message */ + ATR_MSG1, /* message */ + ATR_MSG2, /* message */ + ATR_MSG3, /* message */ + ATR_MSGDRM0, /* message */ + ATR_MSGDRM1, /* message */ + ATR_MSGDRM2, /* message */ + ATR_MSGDRM3, /* message */ + ATR_MENUHIL0, /* highlight menu line */ + ATR_MENUHIL1, /* highlight menu line */ + ATR_MENUHIL2, /* highlight menu line */ + ATR_MENU0, /* menu line */ + ATR_MENU1, /* menu line */ + ATR_MENU2, /* menu line */ + ATR_MENU3, /* menu line */ + ATR_MENU4, /* menu line */ + ATR_MENU5, /* menu line */ + ATR_MENU6, /* menu line */ + ATR_CATCHMENU0, /* catch menu line */ + ATR_CATCHMENU1 /* catch menu line */ +}; + +/* colortable */ +enum enumTeletextColor +{ + TXT_ColorBlack = 0, + TXT_ColorRed, /* 1 */ + TXT_ColorGreen, /* 2 */ + TXT_ColorYellow, /* 3 */ + TXT_ColorBlue, /* 4 */ + TXT_ColorMagenta, /* 5 */ + TXT_ColorCyan, /* 6 */ + TXT_ColorWhite, /* 7 */ + TXT_ColorMenu1 = (4*8), + TXT_ColorMenu2, + TXT_ColorMenu3, + TXT_ColorTransp, + TXT_ColorTransp2, + TXT_Color_SIZECOLTABLE +}; + +enum /* options for charset */ +{ + C_G0P = 0, /* primary G0 */ + C_G0S, /* secondary G0 */ + C_G1C, /* G1 contiguous */ + C_G1S, /* G1 separate */ + C_G2, + C_G3, + C_OFFSET_DRCS = 32 + /* 32..47: 32+subpage# GDRCS (offset/20 in PageChar) */ + /* 48..63: 48+subpage# DRCS (offset/20 in PageChar) */ +}; + +enum /* page function */ +{ + FUNC_LOP = 0, /* Basic Level 1 Teletext page (LOP) */ + FUNC_DATA, /* Data broadcasting page coded according to EN 300 708 [2] clause 4 */ + FUNC_GPOP, /* Global Object definition page (GPOP) - (see clause 10.5.1) */ + FUNC_POP, /* Normal Object definition page (POP) - (see clause 10.5.1) */ + FUNC_GDRCS, /* Global DRCS downloading page (GDRCS) - (see clause 10.5.2) */ + FUNC_DRCS, /* Normal DRCS downloading page (DRCS) - (see clause 10.5.2) */ + FUNC_MOT, /* Magazine Organization table (MOT) - (see clause 10.6) */ + FUNC_MIP, /* Magazine Inventory page (MIP) - (see clause 11.3) */ + FUNC_BTT, /* Basic TOP table (BTT) } */ + FUNC_AIT, /* Additional Information Table (AIT) } (see clause 11.2) */ + FUNC_MPT, /* Multi-page table (MPT) } */ + FUNC_MPTEX, /* Multi-page extension table (MPT-EX) } */ + FUNC_TRIGGER /* Page contain trigger messages defined according to [8] */ +}; + +enum +{ + NAT_DEFAULT = 0, + NAT_CZ = 1, + NAT_UK = 2, + NAT_ET = 3, + NAT_FR = 4, + NAT_DE = 5, + NAT_IT = 6, + NAT_LV = 7, + NAT_PL = 8, + NAT_SP = 9, + NAT_RO = 10, + NAT_SR = 11, + NAT_SW = 12, + NAT_TR = 13, + NAT_MAX_FROM_HEADER = 13, + NAT_SC = 14, + NAT_RB = 15, + NAT_UA = 16, + NAT_GR = 17, + NAT_HB = 18, + NAT_AR = 19 +}; + +const unsigned char CountryConversionTable[] = { NAT_UK, NAT_DE, NAT_SW, NAT_IT, NAT_FR, NAT_SP, NAT_CZ, NAT_RO}; +const unsigned char MapTblFG[] = { 0, 0, 8, 8, 16, 16, 16 }; +const unsigned char MapTblBG[] = { 8, 16, 8, 16, 8, 16, 24 }; +const unsigned short DefaultColors[] = /* 0x0bgr */ +{ + 0x000, 0x00f, 0x0f0, 0x0ff, 0xf00, 0xf0f, 0xff0, 0xfff, + 0x000, 0x007, 0x070, 0x077, 0x700, 0x707, 0x770, 0x777, + 0x50f, 0x07f, 0x7f0, 0xbff, 0xac0, 0x005, 0x256, 0x77c, + 0x333, 0x77f, 0x7f7, 0x7ff, 0xf77, 0xf7f, 0xff7, 0xddd, + 0x420, 0x210, 0x420, 0x000, 0x000 +}; + +/* hamming table */ +const unsigned char dehamming[] = +{ + 0x01, 0xFF, 0x01, 0x01, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0x02, 0x01, 0xFF, 0x0A, 0xFF, 0xFF, 0x07, + 0xFF, 0x00, 0x01, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x06, 0xFF, 0xFF, 0x0B, 0xFF, 0x00, 0x03, 0xFF, + 0xFF, 0x0C, 0x01, 0xFF, 0x04, 0xFF, 0xFF, 0x07, 0x06, 0xFF, 0xFF, 0x07, 0xFF, 0x07, 0x07, 0x07, + 0x06, 0xFF, 0xFF, 0x05, 0xFF, 0x00, 0x0D, 0xFF, 0x06, 0x06, 0x06, 0xFF, 0x06, 0xFF, 0xFF, 0x07, + 0xFF, 0x02, 0x01, 0xFF, 0x04, 0xFF, 0xFF, 0x09, 0x02, 0x02, 0xFF, 0x02, 0xFF, 0x02, 0x03, 0xFF, + 0x08, 0xFF, 0xFF, 0x05, 0xFF, 0x00, 0x03, 0xFF, 0xFF, 0x02, 0x03, 0xFF, 0x03, 0xFF, 0x03, 0x03, + 0x04, 0xFF, 0xFF, 0x05, 0x04, 0x04, 0x04, 0xFF, 0xFF, 0x02, 0x0F, 0xFF, 0x04, 0xFF, 0xFF, 0x07, + 0xFF, 0x05, 0x05, 0x05, 0x04, 0xFF, 0xFF, 0x05, 0x06, 0xFF, 0xFF, 0x05, 0xFF, 0x0E, 0x03, 0xFF, + 0xFF, 0x0C, 0x01, 0xFF, 0x0A, 0xFF, 0xFF, 0x09, 0x0A, 0xFF, 0xFF, 0x0B, 0x0A, 0x0A, 0x0A, 0xFF, + 0x08, 0xFF, 0xFF, 0x0B, 0xFF, 0x00, 0x0D, 0xFF, 0xFF, 0x0B, 0x0B, 0x0B, 0x0A, 0xFF, 0xFF, 0x0B, + 0x0C, 0x0C, 0xFF, 0x0C, 0xFF, 0x0C, 0x0D, 0xFF, 0xFF, 0x0C, 0x0F, 0xFF, 0x0A, 0xFF, 0xFF, 0x07, + 0xFF, 0x0C, 0x0D, 0xFF, 0x0D, 0xFF, 0x0D, 0x0D, 0x06, 0xFF, 0xFF, 0x0B, 0xFF, 0x0E, 0x0D, 0xFF, + 0x08, 0xFF, 0xFF, 0x09, 0xFF, 0x09, 0x09, 0x09, 0xFF, 0x02, 0x0F, 0xFF, 0x0A, 0xFF, 0xFF, 0x09, + 0x08, 0x08, 0x08, 0xFF, 0x08, 0xFF, 0xFF, 0x09, 0x08, 0xFF, 0xFF, 0x0B, 0xFF, 0x0E, 0x03, 0xFF, + 0xFF, 0x0C, 0x0F, 0xFF, 0x04, 0xFF, 0xFF, 0x09, 0x0F, 0xFF, 0x0F, 0x0F, 0xFF, 0x0E, 0x0F, 0xFF, + 0x08, 0xFF, 0xFF, 0x05, 0xFF, 0x0E, 0x0D, 0xFF, 0xFF, 0x0E, 0x0F, 0xFF, 0x0E, 0x0E, 0xFF, 0x0E +}; + +/* odd parity table, error=0x20 (space) */ +const unsigned char deparity[] = +{ + ' ' , 0x01, 0x02, ' ' , 0x04, ' ' , ' ' , 0x07, 0x08, ' ' , ' ' , 0x0b, ' ' , 0x0d, 0x0e, ' ' , + 0x10, ' ' , ' ' , 0x13, ' ' , 0x15, 0x16, ' ' , ' ' , 0x19, 0x1a, ' ' , 0x1c, ' ' , ' ' , 0x1f, + 0x20, ' ' , ' ' , 0x23, ' ' , 0x25, 0x26, ' ' , ' ' , 0x29, 0x2a, ' ' , 0x2c, ' ' , ' ' , 0x2f, + ' ' , 0x31, 0x32, ' ' , 0x34, ' ' , ' ' , 0x37, 0x38, ' ' , ' ' , 0x3b, ' ' , 0x3d, 0x3e, ' ' , + 0x40, ' ' , ' ' , 0x43, ' ' , 0x45, 0x46, ' ' , ' ' , 0x49, 0x4a, ' ' , 0x4c, ' ' , ' ' , 0x4f, + ' ' , 0x51, 0x52, ' ' , 0x54, ' ' , ' ' , 0x57, 0x58, ' ' , ' ' , 0x5b, ' ' , 0x5d, 0x5e, ' ' , + ' ' , 0x61, 0x62, ' ' , 0x64, ' ' , ' ' , 0x67, 0x68, ' ' , ' ' , 0x6b, ' ' , 0x6d, 0x6e, ' ' , + 0x70, ' ' , ' ' , 0x73, ' ' , 0x75, 0x76, ' ' , ' ' , 0x79, 0x7a, ' ' , 0x7c, ' ' , ' ' , 0x7f, + 0x00, ' ' , ' ' , 0x03, ' ' , 0x05, 0x06, ' ' , ' ' , 0x09, 0x0a, ' ' , 0x0c, ' ' , ' ' , 0x0f, + ' ' , 0x11, 0x12, ' ' , 0x14, ' ' , ' ' , 0x17, 0x18, ' ' , ' ' , 0x1b, ' ' , 0x1d, 0x1e, ' ' , + ' ' , 0x21, 0x22, ' ' , 0x24, ' ' , ' ' , 0x27, 0x28, ' ' , ' ' , 0x2b, ' ' , 0x2d, 0x2e, ' ' , + 0x30, ' ' , ' ' , 0x33, ' ' , 0x35, 0x36, ' ' , ' ' , 0x39, 0x3a, ' ' , 0x3c, ' ' , ' ' , 0x3f, + ' ' , 0x41, 0x42, ' ' , 0x44, ' ' , ' ' , 0x47, 0x48, ' ' , ' ' , 0x4b, ' ' , 0x4d, 0x4e, ' ' , + 0x50, ' ' , ' ' , 0x53, ' ' , 0x55, 0x56, ' ' , ' ' , 0x59, 0x5a, ' ' , 0x5c, ' ' , ' ' , 0x5f, + 0x60, ' ' , ' ' , 0x63, ' ' , 0x65, 0x66, ' ' , ' ' , 0x69, 0x6a, ' ' , 0x6c, ' ' , ' ' , 0x6f, + ' ' , 0x71, 0x72, ' ' , 0x74, ' ' , ' ' , 0x77, 0x78, ' ' , ' ' , 0x7b, ' ' , 0x7d, 0x7e, ' ' , +}; + +/* + * [AleVT] + * + * This table generates the parity checks for hamm24/18 decoding. + * Bit 0 is for test A, 1 for B, ... + * + * Thanks to R. Gancarz for this fine table *g* + */ +const unsigned char hamm24par[3][256] = +{ + { + /* Parities of first byte */ + 0, 33, 34, 3, 35, 2, 1, 32, 36, 5, 6, 39, 7, 38, 37, 4, + 37, 4, 7, 38, 6, 39, 36, 5, 1, 32, 35, 2, 34, 3, 0, 33, + 38, 7, 4, 37, 5, 36, 39, 6, 2, 35, 32, 1, 33, 0, 3, 34, + 3, 34, 33, 0, 32, 1, 2, 35, 39, 6, 5, 36, 4, 37, 38, 7, + 39, 6, 5, 36, 4, 37, 38, 7, 3, 34, 33, 0, 32, 1, 2, 35, + 2, 35, 32, 1, 33, 0, 3, 34, 38, 7, 4, 37, 5, 36, 39, 6, + 1, 32, 35, 2, 34, 3, 0, 33, 37, 4, 7, 38, 6, 39, 36, 5, + 36, 5, 6, 39, 7, 38, 37, 4, 0, 33, 34, 3, 35, 2, 1, 32, + 40, 9, 10, 43, 11, 42, 41, 8, 12, 45, 46, 15, 47, 14, 13, 44, + 13, 44, 47, 14, 46, 15, 12, 45, 41, 8, 11, 42, 10, 43, 40, 9, + 14, 47, 44, 13, 45, 12, 15, 46, 42, 11, 8, 41, 9, 40, 43, 10, + 43, 10, 9, 40, 8, 41, 42, 11, 15, 46, 45, 12, 44, 13, 14, 47, + 15, 46, 45, 12, 44, 13, 14, 47, 43, 10, 9, 40, 8, 41, 42, 11, + 42, 11, 8, 41, 9, 40, 43, 10, 14, 47, 44, 13, 45, 12, 15, 46, + 41, 8, 11, 42, 10, 43, 40, 9, 13, 44, 47, 14, 46, 15, 12, 45, + 12, 45, 46, 15, 47, 14, 13, 44, 40, 9, 10, 43, 11, 42, 41, 8 + }, { + /* Parities of second byte */ + 0, 41, 42, 3, 43, 2, 1, 40, 44, 5, 6, 47, 7, 46, 45, 4, + 45, 4, 7, 46, 6, 47, 44, 5, 1, 40, 43, 2, 42, 3, 0, 41, + 46, 7, 4, 45, 5, 44, 47, 6, 2, 43, 40, 1, 41, 0, 3, 42, + 3, 42, 41, 0, 40, 1, 2, 43, 47, 6, 5, 44, 4, 45, 46, 7, + 47, 6, 5, 44, 4, 45, 46, 7, 3, 42, 41, 0, 40, 1, 2, 43, + 2, 43, 40, 1, 41, 0, 3, 42, 46, 7, 4, 45, 5, 44, 47, 6, + 1, 40, 43, 2, 42, 3, 0, 41, 45, 4, 7, 46, 6, 47, 44, 5, + 44, 5, 6, 47, 7, 46, 45, 4, 0, 41, 42, 3, 43, 2, 1, 40, + 48, 25, 26, 51, 27, 50, 49, 24, 28, 53, 54, 31, 55, 30, 29, 52, + 29, 52, 55, 30, 54, 31, 28, 53, 49, 24, 27, 50, 26, 51, 48, 25, + 30, 55, 52, 29, 53, 28, 31, 54, 50, 27, 24, 49, 25, 48, 51, 26, + 51, 26, 25, 48, 24, 49, 50, 27, 31, 54, 53, 28, 52, 29, 30, 55, + 31, 54, 53, 28, 52, 29, 30, 55, 51, 26, 25, 48, 24, 49, 50, 27, + 50, 27, 24, 49, 25, 48, 51, 26, 30, 55, 52, 29, 53, 28, 31, 54, + 49, 24, 27, 50, 26, 51, 48, 25, 29, 52, 55, 30, 54, 31, 28, 53, + 28, 53, 54, 31, 55, 30, 29, 52, 48, 25, 26, 51, 27, 50, 49, 24 + }, { + /* Parities of third byte */ + 63, 14, 13, 60, 12, 61, 62, 15, 11, 58, 57, 8, 56, 9, 10, 59, + 10, 59, 56, 9, 57, 8, 11, 58, 62, 15, 12, 61, 13, 60, 63, 14, + 9, 56, 59, 10, 58, 11, 8, 57, 61, 12, 15, 62, 14, 63, 60, 13, + 60, 13, 14, 63, 15, 62, 61, 12, 8, 57, 58, 11, 59, 10, 9, 56, + 8, 57, 58, 11, 59, 10, 9, 56, 60, 13, 14, 63, 15, 62, 61, 12, + 61, 12, 15, 62, 14, 63, 60, 13, 9, 56, 59, 10, 58, 11, 8, 57, + 62, 15, 12, 61, 13, 60, 63, 14, 10, 59, 56, 9, 57, 8, 11, 58, + 11, 58, 57, 8, 56, 9, 10, 59, 63, 14, 13, 60, 12, 61, 62, 15, + 31, 46, 45, 28, 44, 29, 30, 47, 43, 26, 25, 40, 24, 41, 42, 27, + 42, 27, 24, 41, 25, 40, 43, 26, 30, 47, 44, 29, 45, 28, 31, 46, + 41, 24, 27, 42, 26, 43, 40, 25, 29, 44, 47, 30, 46, 31, 28, 45, + 28, 45, 46, 31, 47, 30, 29, 44, 40, 25, 26, 43, 27, 42, 41, 24, + 40, 25, 26, 43, 27, 42, 41, 24, 28, 45, 46, 31, 47, 30, 29, 44, + 29, 44, 47, 30, 46, 31, 28, 45, 41, 24, 27, 42, 26, 43, 40, 25, + 30, 47, 44, 29, 45, 28, 31, 46, 42, 27, 24, 41, 25, 40, 43, 26, + 43, 26, 25, 40, 24, 41, 42, 27, 31, 46, 45, 28, 44, 29, 30, 47 + } +}; + +/* + * [AleVT] + * + * Table to extract the lower 4 bit from hamm24/18 encoded bytes + */ +const unsigned char hamm24val[256] = +{ + 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, + 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 5, 5, 5, 5, 4, 4, 4, 4, 5, 5, 5, 5, + 6, 6, 6, 6, 7, 7, 7, 7, 6, 6, 6, 6, 7, 7, 7, 7, + 8, 8, 8, 8, 9, 9, 9, 9, 8, 8, 8, 8, 9, 9, 9, 9, + 10, 10, 10, 10, 11, 11, 11, 11, 10, 10, 10, 10, 11, 11, 11, 11, + 12, 12, 12, 12, 13, 13, 13, 13, 12, 12, 12, 12, 13, 13, 13, 13, + 14, 14, 14, 14, 15, 15, 15, 15, 14, 14, 14, 14, 15, 15, 15, 15, + 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, + 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 5, 5, 5, 5, 4, 4, 4, 4, 5, 5, 5, 5, + 6, 6, 6, 6, 7, 7, 7, 7, 6, 6, 6, 6, 7, 7, 7, 7, + 8, 8, 8, 8, 9, 9, 9, 9, 8, 8, 8, 8, 9, 9, 9, 9, + 10, 10, 10, 10, 11, 11, 11, 11, 10, 10, 10, 10, 11, 11, 11, 11, + 12, 12, 12, 12, 13, 13, 13, 13, 12, 12, 12, 12, 13, 13, 13, 13, + 14, 14, 14, 14, 15, 15, 15, 15, 14, 14, 14, 14, 15, 15, 15, 15 +}; + +const signed char hamm24err[64] = +{ + 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, +}; + +/* + * [AleVT] + * + * Mapping from parity checks made by table hamm24par to faulty bit + * in the decoded 18 bit word. + */ +const unsigned int hamm24cor[64] = +{ + 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, + 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, + 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, + 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, + 0x00000, 0x00000, 0x00000, 0x00001, 0x00000, 0x00002, 0x00004, 0x00008, + 0x00000, 0x00010, 0x00020, 0x00040, 0x00080, 0x00100, 0x00200, 0x00400, + 0x00000, 0x00800, 0x01000, 0x02000, 0x04000, 0x08000, 0x10000, 0x20000, + 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, +}; + +inline int IsDec(int i) +{ + return ((i & 0x00F) <= 9) && ((i & 0x0F0) <= 0x90); +} + +/* struct for page attributes */ +typedef struct +{ + unsigned char fg :6; /* foreground color */ + unsigned char bg :6; /* background color */ + unsigned char charset :6; /* see enum above */ + unsigned char doubleh :1; /* double height */ + unsigned char doublew :1; /* double width */ + /* ignore at Black Background Color Substitution */ + /* black background set by New Background ($1d) instead of start-of-row default or Black Backgr. ($1c) */ + /* or black background set by level 2.5 extensions */ + unsigned char IgnoreAtBlackBgSubst:1; + unsigned char concealed:1; /* concealed information */ + unsigned char inverted :1; /* colors inverted */ + unsigned char flashing :5; /* flash mode */ + unsigned char diacrit :4; /* diacritical mark */ + unsigned char underline:1; /* Text underlined */ + unsigned char boxwin :1; /* Text boxed/windowed */ + unsigned char setX26 :1; /* Char is set by packet X/26 (no national subset used) */ + unsigned char setG0G2 :7; /* G0+G2 set designation */ +} TextPageAttr_t; + +/* struct for (G)POP/(G)DRCS links for level 2.5, allocated at reception of p27/4 or /5, initialized with 0 after allocation */ +typedef struct +{ + unsigned short page; /* linked page number */ + unsigned short subpage; /* 1 bit for each needed (1) subpage */ + unsigned char l25:1; /* 1: page required at level 2.5 */ + unsigned char l35:1; /* 1: page required at level 3.5 */ + unsigned char drcs:1; /* 1: link to (G)DRCS, 0: (G)POP */ + unsigned char local:1; /* 1: global (G*), 0: local */ +} Textp27_t; + +/* struct for extension data for level 2.5, allocated at reception, initialized with 0 after allocation */ +typedef struct +{ + unsigned char *p26[16]; /* array of pointers to max. 16 designation codes of packet 26 */ + Textp27_t *p27; /* array of 4 structs for (G)POP/(G)DRCS links for level 2.5 */ + unsigned short bgr[16]; /* CLUT 2+3, 2*8 colors, 0x0bgr */ + unsigned char DefaultCharset:7; /* default G0/G2 charset + national option */ + unsigned char LSP:1; /* 1: left side panel to be displayed */ + unsigned char SecondCharset:7; /* second G0 charset */ + unsigned char RSP:1; /* 1: right side panel to be displayed */ + unsigned char DefScreenColor:5; /* default screen color (above and below lines 0..24) */ + unsigned char ColorTableRemapping:3; /* 1: index in table of CLUTs to use */ + unsigned char DefRowColor:5; /* default row color (left and right to lines 0..24) */ + unsigned char BlackBgSubst:1; /* 1: substitute black background (as result of start-of-line or 1c, not 00/10+1d) */ + unsigned char SPL25:1; /* 1: side panel required at level 2.5 */ + unsigned char p28Received:1; /* 1: extension data valid (p28/0 received) */ + unsigned char LSPColumns:4; /* number of columns in left side panel, 0->16, rsp=16-lsp */ +} TextExtData_t; + +/* struct for pageinfo, max. 16 Bytes, at beginning of each cached page buffer, initialized with 0 after allocation */ +typedef struct +{ + unsigned char *p24; /* pointer to lines 25+26 (packets 24+25) (2*40 bytes) for FLOF or level 2.5 data */ + TextExtData_t *ext; /* pointer to array[16] of data for level 2.5 */ + unsigned char boxed :1; /* p0: boxed (newsflash or subtitle) */ + unsigned char nationalvalid :1; /* p0: national option character subset is valid (no biterror detected) */ + unsigned char national :3; /* p0: national option character subset */ + unsigned char function :3; /* p28/0: page function */ +} TextPageinfo_t; + +/* one cached page: struct for pageinfo, 24 lines page data */ +typedef struct +{ + TextPageinfo_t pageinfo; + unsigned char p0[24]; /* packet 0: center of headline */ + unsigned char data[23*40]; /* packet 1-23 */ +} TextCachedPage_t; + +typedef struct +{ + short page; + short language; +} TextSubtitle_t; + +typedef struct +{ + bool Valid; + std::chrono::time_point<std::chrono::steady_clock> Timestamp; + unsigned char PageChar[TELETEXT_PAGE_SIZE]; + TextPageAttr_t PageAtrb[TELETEXT_PAGE_SIZE]; +} TextSubtitleCache_t; + +/* main data structure */ +typedef struct TextCacheStruct_t +{ + int CurrentPage[9]; + int CurrentSubPage[9]; + TextExtData_t *astP29[9]; + TextCachedPage_t *astCachetable[0x900][0x80]; + unsigned char SubPageTable[0x900]; + unsigned char BasicTop[0x900]; + short FlofPages[0x900][FLOFSIZE]; + char ADIPTable[0x900][13]; + int ADIP_PgMax; + int ADIP_Pg[10]; + bool BTTok; + int CachedPages; + int PageReceiving; + int Page; + int SubPage; + bool PageUpdate; + int NationalSubset; + int NationalSubsetSecondary; + bool ZapSubpageManual; + TextSubtitle_t SubtitlePages[8]; + unsigned char TimeString[8]; + int vtxtpid; + + /* cachetable for packets 29 (one for each magazine) */ + /* cachetable */ + unsigned char FullRowColor[25]; + unsigned char FullScrColor; + unsigned char tAPx, tAPy; /* temporary offset to Active Position for objects */ + short pop, gpop, drcs, gdrcs; + unsigned short *ColorTable; + + std::string line30; + + // TODO: We should get rid of this public mutex. Here are the details: https://github.com/xbmc/xbmc/pull/22226 + CCriticalSection m_critSection; +} TextCacheStruct_t; + +/* struct for all Information needed for Page Rendering */ +typedef struct +{ + bool PageCatching; + bool TranspMode; + bool HintMode; + bool ShowFlof; + bool Show39; + bool Showl25; + bool ShowHex; + int ZoomMode; + + int InputCounter; + int ClearBBColor; + int Prev_100, Prev_10, Next_10, Next_100; + int Height; + int Width; + int FontHeight; + int FontWidth; + int FontWidth_Normal; + unsigned short rd0[TXT_Color_SIZECOLTABLE]; + unsigned short gn0[TXT_Color_SIZECOLTABLE]; + unsigned short bl0[TXT_Color_SIZECOLTABLE]; + unsigned short tr0[TXT_Color_SIZECOLTABLE]; + TextSubtitleCache_t *SubtitleCache[SUBTITLE_CACHESIZE]; + unsigned char PageChar[25*40]; + TextPageAttr_t PageAtrb[25*40]; + TextPageinfo_t *PageInfo; + int PosX; + int PosY; + int nofirst; + unsigned char axdrcs[12+1+10+1]; + int TTFShiftX, TTFShiftY; /* parameters for adapting to various TTF fonts */ + bool Boxed; + int ScreenMode, PrevScreenMode; + bool DelayStarted; + unsigned int SubtitleDelay; +} TextRenderInfo_t; + +class CDVDTeletextTools +{ +public: + static void NextDec(int *i); + static void PrevDec(int *i); + static void Hex2Str(char *s, unsigned int n); + static signed int deh24(unsigned char *p); +}; diff --git a/xbmc/video/VideoDatabase.cpp b/xbmc/video/VideoDatabase.cpp new file mode 100644 index 0000000..c000d68 --- /dev/null +++ b/xbmc/video/VideoDatabase.cpp @@ -0,0 +1,11356 @@ +/* + * Copyright (C) 2016-2018 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 "VideoDatabase.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "GUIPassword.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "URL.h" +#include "Util.h" +#include "VideoInfoScanner.h" +#include "XBDateTime.h" +#include "addons/AddonManager.h" +#include "dbwrappers/dataset.h" +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/Directory.h" +#include "filesystem/File.h" +#include "filesystem/MultiPathDirectory.h" +#include "filesystem/PluginDirectory.h" +#include "filesystem/StackDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "interfaces/AnnouncementManager.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/Artist.h" +#include "playlists/SmartPlayList.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "storage/MediaManager.h" +#include "utils/FileUtils.h" +#include "utils/GroupUtils.h" +#include "utils/LabelFormatter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" +#include "video/VideoDbUrl.h" +#include "video/VideoInfoTag.h" +#include "video/VideoLibraryQueue.h" +#include "video/windows/GUIWindowVideoBase.h" + +#include <algorithm> +#include <map> +#include <memory> +#include <string> +#include <unordered_set> +#include <vector> + +using namespace dbiplus; +using namespace XFILE; +using namespace VIDEO; +using namespace ADDON; +using namespace KODI::MESSAGING; +using namespace KODI::GUILIB; + +//******************************************************************************************************************************** +CVideoDatabase::CVideoDatabase(void) = default; + +//******************************************************************************************************************************** +CVideoDatabase::~CVideoDatabase(void) = default; + +//******************************************************************************************************************************** +bool CVideoDatabase::Open() +{ + return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseVideo); +} + +void CVideoDatabase::CreateTables() +{ + CLog::Log(LOGINFO, "create bookmark table"); + m_pDS->exec("CREATE TABLE bookmark ( idBookmark integer primary key, idFile integer, timeInSeconds double, totalTimeInSeconds double, thumbNailImage text, player text, playerState text, type integer)\n"); + + CLog::Log(LOGINFO, "create settings table"); + m_pDS->exec("CREATE TABLE settings ( idFile integer, Deinterlace bool," + "ViewMode integer,ZoomAmount float, PixelRatio float, VerticalShift float, AudioStream integer, SubtitleStream integer," + "SubtitleDelay float, SubtitlesOn bool, Brightness float, Contrast float, Gamma float," + "VolumeAmplification float, AudioDelay float, ResumeTime integer," + "Sharpness float, NoiseReduction float, NonLinStretch bool, PostProcess bool," + "ScalingMethod integer, DeinterlaceMode integer, StereoMode integer, StereoInvert bool, VideoStream integer," + "TonemapMethod integer, TonemapParam float, Orientation integer, CenterMixLevel integer)\n"); + + CLog::Log(LOGINFO, "create stacktimes table"); + m_pDS->exec("CREATE TABLE stacktimes (idFile integer, times text)\n"); + + CLog::Log(LOGINFO, "create genre table"); + m_pDS->exec("CREATE TABLE genre ( genre_id integer primary key, name TEXT)\n"); + m_pDS->exec("CREATE TABLE genre_link (genre_id integer, media_id integer, media_type TEXT)"); + + CLog::Log(LOGINFO, "create country table"); + m_pDS->exec("CREATE TABLE country ( country_id integer primary key, name TEXT)"); + m_pDS->exec("CREATE TABLE country_link (country_id integer, media_id integer, media_type TEXT)"); + + CLog::Log(LOGINFO, "create movie table"); + std::string columns = "CREATE TABLE movie ( idMovie integer primary key, idFile integer"; + + for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++) + columns += StringUtils::Format(",c{:02} text", i); + + columns += ", idSet integer, userrating integer, premiered text)"; + m_pDS->exec(columns); + + CLog::Log(LOGINFO, "create actor table"); + m_pDS->exec("CREATE TABLE actor ( actor_id INTEGER PRIMARY KEY, name TEXT, art_urls TEXT )"); + m_pDS->exec("CREATE TABLE actor_link(actor_id INTEGER, media_id INTEGER, media_type TEXT, role TEXT, cast_order INTEGER)"); + m_pDS->exec("CREATE TABLE director_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)"); + m_pDS->exec("CREATE TABLE writer_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)"); + + CLog::Log(LOGINFO, "create path table"); + m_pDS->exec( + "CREATE TABLE path ( idPath integer primary key, strPath text, strContent text, strScraper " + "text, strHash text, scanRecursive integer, useFolderNames bool, strSettings text, noUpdate " + "bool, exclude bool, allAudio bool, dateAdded text, idParentPath integer)"); + + CLog::Log(LOGINFO, "create files table"); + m_pDS->exec("CREATE TABLE files ( idFile integer primary key, idPath integer, strFilename text, playCount integer, lastPlayed text, dateAdded text)"); + + CLog::Log(LOGINFO, "create tvshow table"); + columns = "CREATE TABLE tvshow ( idShow integer primary key"; + + for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++) + columns += StringUtils::Format(",c{:02} text", i); + + columns += ", userrating integer, duration INTEGER)"; + m_pDS->exec(columns); + + CLog::Log(LOGINFO, "create episode table"); + columns = "CREATE TABLE episode ( idEpisode integer primary key, idFile integer"; + for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++) + { + std::string column; + if ( i == VIDEODB_ID_EPISODE_SEASON || i == VIDEODB_ID_EPISODE_EPISODE || i == VIDEODB_ID_EPISODE_BOOKMARK) + column = StringUtils::Format(",c{:02} varchar(24)", i); + else + column = StringUtils::Format(",c{:02} text", i); + + columns += column; + } + columns += ", idShow integer, userrating integer, idSeason integer)"; + m_pDS->exec(columns); + + CLog::Log(LOGINFO, "create tvshowlinkpath table"); + m_pDS->exec("CREATE TABLE tvshowlinkpath (idShow integer, idPath integer)\n"); + + CLog::Log(LOGINFO, "create movielinktvshow table"); + m_pDS->exec("CREATE TABLE movielinktvshow ( idMovie integer, IdShow integer)\n"); + + CLog::Log(LOGINFO, "create studio table"); + m_pDS->exec("CREATE TABLE studio ( studio_id integer primary key, name TEXT)\n"); + m_pDS->exec("CREATE TABLE studio_link (studio_id integer, media_id integer, media_type TEXT)"); + + CLog::Log(LOGINFO, "create musicvideo table"); + columns = "CREATE TABLE musicvideo ( idMVideo integer primary key, idFile integer"; + for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++) + columns += StringUtils::Format(",c{:02} text", i); + + columns += ", userrating integer, premiered text)"; + m_pDS->exec(columns); + + CLog::Log(LOGINFO, "create streaminfo table"); + m_pDS->exec("CREATE TABLE streamdetails (idFile integer, iStreamType integer, " + "strVideoCodec text, fVideoAspect float, iVideoWidth integer, iVideoHeight integer, " + "strAudioCodec text, iAudioChannels integer, strAudioLanguage text, " + "strSubtitleLanguage text, iVideoDuration integer, strStereoMode text, strVideoLanguage text, " + "strHdrType text)"); + + CLog::Log(LOGINFO, "create sets table"); + m_pDS->exec("CREATE TABLE sets ( idSet integer primary key, strSet text, strOverview text)"); + + CLog::Log(LOGINFO, "create seasons table"); + m_pDS->exec("CREATE TABLE seasons ( idSeason integer primary key, idShow integer, season integer, name text, userrating integer)"); + + CLog::Log(LOGINFO, "create art table"); + m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, type TEXT, url TEXT)"); + + CLog::Log(LOGINFO, "create tag table"); + m_pDS->exec("CREATE TABLE tag (tag_id integer primary key, name TEXT)"); + m_pDS->exec("CREATE TABLE tag_link (tag_id integer, media_id integer, media_type TEXT)"); + + CLog::Log(LOGINFO, "create rating table"); + m_pDS->exec("CREATE TABLE rating (rating_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, rating_type TEXT, rating FLOAT, votes INTEGER)"); + + CLog::Log(LOGINFO, "create uniqueid table"); + m_pDS->exec("CREATE TABLE uniqueid (uniqueid_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, value TEXT, type TEXT)"); +} + +void CVideoDatabase::CreateLinkIndex(const char *table) +{ + m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_1 ON %s (name(255))", table, table)); + m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_1 ON %s_link (%s_id, media_type(20), media_id)", table, table, table)); + m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_2 ON %s_link (media_id, media_type(20), %s_id)", table, table, table)); + m_pDS->exec(PrepareSQL("CREATE INDEX ix_%s_link_3 ON %s_link (media_type(20))", table, table)); +} + +void CVideoDatabase::CreateForeignLinkIndex(const char *table, const char *foreignkey) +{ + m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_1 ON %s_link (%s_id, media_type(20), media_id)", table, table, foreignkey)); + m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_2 ON %s_link (media_id, media_type(20), %s_id)", table, table, foreignkey)); + m_pDS->exec(PrepareSQL("CREATE INDEX ix_%s_link_3 ON %s_link (media_type(20))", table, table)); +} + +void CVideoDatabase::CreateAnalytics() +{ + /* indexes should be added on any columns that are used in */ + /* a where or a join. primary key on a column is the same as a */ + /* unique index on that column, so there is no need to add any */ + /* index if no other columns are referred */ + + /* order of indexes are important, for an index to be considered all */ + /* columns up to the column in question have to have been specified */ + /* select * from foolink where foo_id = 1, can not take */ + /* advantage of a index that has been created on ( bar_id, foo_id ) */ + /* however an index on ( foo_id, bar_id ) will be considered for use */ + + CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__); + m_pDS->exec("CREATE INDEX ix_bookmark ON bookmark (idFile, type)"); + m_pDS->exec("CREATE UNIQUE INDEX ix_settings ON settings ( idFile )\n"); + m_pDS->exec("CREATE UNIQUE INDEX ix_stacktimes ON stacktimes ( idFile )\n"); + m_pDS->exec("CREATE INDEX ix_path ON path ( strPath(255) )"); + m_pDS->exec("CREATE INDEX ix_path2 ON path ( idParentPath )"); + m_pDS->exec("CREATE INDEX ix_files ON files ( idPath, strFilename(255) )"); + + m_pDS->exec("CREATE UNIQUE INDEX ix_movie_file_1 ON movie (idFile, idMovie)"); + m_pDS->exec("CREATE UNIQUE INDEX ix_movie_file_2 ON movie (idMovie, idFile)"); + + m_pDS->exec("CREATE UNIQUE INDEX ix_tvshowlinkpath_1 ON tvshowlinkpath ( idShow, idPath )\n"); + m_pDS->exec("CREATE UNIQUE INDEX ix_tvshowlinkpath_2 ON tvshowlinkpath ( idPath, idShow )\n"); + m_pDS->exec("CREATE UNIQUE INDEX ix_movielinktvshow_1 ON movielinktvshow ( idShow, idMovie)\n"); + m_pDS->exec("CREATE UNIQUE INDEX ix_movielinktvshow_2 ON movielinktvshow ( idMovie, idShow)\n"); + + m_pDS->exec("CREATE UNIQUE INDEX ix_episode_file_1 on episode (idEpisode, idFile)"); + m_pDS->exec("CREATE UNIQUE INDEX id_episode_file_2 on episode (idFile, idEpisode)"); + std::string createColIndex = + StringUtils::Format("CREATE INDEX ix_episode_season_episode on episode (c{:02}, c{:02})", + VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_EPISODE_EPISODE); + m_pDS->exec(createColIndex); + createColIndex = StringUtils::Format("CREATE INDEX ix_episode_bookmark on episode (c{:02})", + VIDEODB_ID_EPISODE_BOOKMARK); + m_pDS->exec(createColIndex); + m_pDS->exec("CREATE INDEX ix_episode_show1 on episode(idEpisode,idShow)"); + m_pDS->exec("CREATE INDEX ix_episode_show2 on episode(idShow,idEpisode)"); + + m_pDS->exec("CREATE UNIQUE INDEX ix_musicvideo_file_1 on musicvideo (idMVideo, idFile)"); + m_pDS->exec("CREATE UNIQUE INDEX ix_musicvideo_file_2 on musicvideo (idFile, idMVideo)"); + + m_pDS->exec("CREATE INDEX ixMovieBasePath ON movie ( c23(12) )"); + m_pDS->exec("CREATE INDEX ixMusicVideoBasePath ON musicvideo ( c14(12) )"); + m_pDS->exec("CREATE INDEX ixEpisodeBasePath ON episode ( c19(12) )"); + + m_pDS->exec("CREATE INDEX ix_streamdetails ON streamdetails (idFile)"); + m_pDS->exec("CREATE INDEX ix_seasons ON seasons (idShow, season)"); + m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))"); + + m_pDS->exec("CREATE INDEX ix_rating ON rating(media_id, media_type(20))"); + + m_pDS->exec("CREATE INDEX ix_uniqueid1 ON uniqueid(media_id, media_type(20), type(20))"); + m_pDS->exec("CREATE INDEX ix_uniqueid2 ON uniqueid(media_type(20), value(20))"); + + m_pDS->exec("CREATE UNIQUE INDEX ix_actor_1 ON actor (name(255))"); + m_pDS->exec("CREATE UNIQUE INDEX ix_actor_link_1 ON " + "actor_link (actor_id, media_type(20), media_id, role(255))"); + m_pDS->exec("CREATE INDEX ix_actor_link_2 ON " + "actor_link (media_id, media_type(20), actor_id)"); + m_pDS->exec("CREATE INDEX ix_actor_link_3 ON actor_link (media_type(20))"); + + CreateLinkIndex("tag"); + CreateForeignLinkIndex("director", "actor"); + CreateForeignLinkIndex("writer", "actor"); + CreateLinkIndex("studio"); + CreateLinkIndex("genre"); + CreateLinkIndex("country"); + + CLog::Log(LOGINFO, "{} - creating triggers", __FUNCTION__); + m_pDS->exec("CREATE TRIGGER delete_movie AFTER DELETE ON movie FOR EACH ROW BEGIN " + "DELETE FROM genre_link WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM actor_link WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM director_link WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM studio_link WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM country_link WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM writer_link WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM movielinktvshow WHERE idMovie=old.idMovie; " + "DELETE FROM art WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM tag_link WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM rating WHERE media_id=old.idMovie AND media_type='movie'; " + "DELETE FROM uniqueid WHERE media_id=old.idMovie AND media_type='movie'; " + "END"); + m_pDS->exec("CREATE TRIGGER delete_tvshow AFTER DELETE ON tvshow FOR EACH ROW BEGIN " + "DELETE FROM actor_link WHERE media_id=old.idShow AND media_type='tvshow'; " + "DELETE FROM director_link WHERE media_id=old.idShow AND media_type='tvshow'; " + "DELETE FROM studio_link WHERE media_id=old.idShow AND media_type='tvshow'; " + "DELETE FROM tvshowlinkpath WHERE idShow=old.idShow; " + "DELETE FROM genre_link WHERE media_id=old.idShow AND media_type='tvshow'; " + "DELETE FROM movielinktvshow WHERE idShow=old.idShow; " + "DELETE FROM seasons WHERE idShow=old.idShow; " + "DELETE FROM art WHERE media_id=old.idShow AND media_type='tvshow'; " + "DELETE FROM tag_link WHERE media_id=old.idShow AND media_type='tvshow'; " + "DELETE FROM rating WHERE media_id=old.idShow AND media_type='tvshow'; " + "DELETE FROM uniqueid WHERE media_id=old.idShow AND media_type='tvshow'; " + "END"); + m_pDS->exec("CREATE TRIGGER delete_musicvideo AFTER DELETE ON musicvideo FOR EACH ROW BEGIN " + "DELETE FROM actor_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; " + "DELETE FROM director_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; " + "DELETE FROM genre_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; " + "DELETE FROM studio_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; " + "DELETE FROM art WHERE media_id=old.idMVideo AND media_type='musicvideo'; " + "DELETE FROM tag_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; " + "DELETE FROM uniqueid WHERE media_id=old.idMVideo AND media_type='musicvideo'; " + "END"); + m_pDS->exec("CREATE TRIGGER delete_episode AFTER DELETE ON episode FOR EACH ROW BEGIN " + "DELETE FROM actor_link WHERE media_id=old.idEpisode AND media_type='episode'; " + "DELETE FROM director_link WHERE media_id=old.idEpisode AND media_type='episode'; " + "DELETE FROM writer_link WHERE media_id=old.idEpisode AND media_type='episode'; " + "DELETE FROM art WHERE media_id=old.idEpisode AND media_type='episode'; " + "DELETE FROM rating WHERE media_id=old.idEpisode AND media_type='episode'; " + "DELETE FROM uniqueid WHERE media_id=old.idEpisode AND media_type='episode'; " + "END"); + m_pDS->exec("CREATE TRIGGER delete_season AFTER DELETE ON seasons FOR EACH ROW BEGIN " + "DELETE FROM art WHERE media_id=old.idSeason AND media_type='season'; " + "END"); + m_pDS->exec("CREATE TRIGGER delete_set AFTER DELETE ON sets FOR EACH ROW BEGIN " + "DELETE FROM art WHERE media_id=old.idSet AND media_type='set'; " + "END"); + m_pDS->exec("CREATE TRIGGER delete_person AFTER DELETE ON actor FOR EACH ROW BEGIN " + "DELETE FROM art WHERE media_id=old.actor_id AND media_type IN ('actor','artist','writer','director'); " + "END"); + m_pDS->exec("CREATE TRIGGER delete_tag AFTER DELETE ON tag_link FOR EACH ROW BEGIN " + "DELETE FROM tag WHERE tag_id=old.tag_id AND tag_id NOT IN (SELECT DISTINCT tag_id FROM tag_link); " + "END"); + m_pDS->exec("CREATE TRIGGER delete_file AFTER DELETE ON files FOR EACH ROW BEGIN " + "DELETE FROM bookmark WHERE idFile=old.idFile; " + "DELETE FROM settings WHERE idFile=old.idFile; " + "DELETE FROM stacktimes WHERE idFile=old.idFile; " + "DELETE FROM streamdetails WHERE idFile=old.idFile; " + "END"); + + CreateViews(); +} + +void CVideoDatabase::CreateViews() +{ + CLog::Log(LOGINFO, "create episode_view"); + std::string episodeview = PrepareSQL("CREATE VIEW episode_view AS SELECT " + " episode.*," + " files.strFileName AS strFileName," + " path.strPath AS strPath," + " files.playCount AS playCount," + " files.lastPlayed AS lastPlayed," + " files.dateAdded AS dateAdded," + " tvshow.c%02d AS strTitle," + " tvshow.c%02d AS genre," + " tvshow.c%02d AS studio," + " tvshow.c%02d AS premiered," + " tvshow.c%02d AS mpaa," + " bookmark.timeInSeconds AS resumeTimeInSeconds, " + " bookmark.totalTimeInSeconds AS totalTimeInSeconds, " + " bookmark.playerState AS playerState, " + " rating.rating AS rating, " + " rating.votes AS votes, " + " rating.rating_type AS rating_type, " + " uniqueid.value AS uniqueid_value, " + " uniqueid.type AS uniqueid_type " + "FROM episode" + " JOIN files ON" + " files.idFile=episode.idFile" + " JOIN tvshow ON" + " tvshow.idShow=episode.idShow" + " JOIN seasons ON" + " seasons.idSeason=episode.idSeason" + " JOIN path ON" + " files.idPath=path.idPath" + " LEFT JOIN bookmark ON" + " bookmark.idFile=episode.idFile AND bookmark.type=1" + " LEFT JOIN rating ON" + " rating.rating_id=episode.c%02d" + " LEFT JOIN uniqueid ON" + " uniqueid.uniqueid_id=episode.c%02d", + VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_GENRE, + VIDEODB_ID_TV_STUDIOS, VIDEODB_ID_TV_PREMIERED, + VIDEODB_ID_TV_MPAA, VIDEODB_ID_EPISODE_RATING_ID, + VIDEODB_ID_EPISODE_IDENT_ID); + m_pDS->exec(episodeview); + + CLog::Log(LOGINFO, "create tvshowcounts"); + std::string tvshowcounts = PrepareSQL("CREATE VIEW tvshowcounts AS SELECT " + " tvshow.idShow AS idShow," + " MAX(files.lastPlayed) AS lastPlayed," + " NULLIF(COUNT(episode.c12), 0) AS totalCount," + " COUNT(files.playCount) AS watchedcount," + " NULLIF(COUNT(DISTINCT(episode.c12)), 0) AS totalSeasons, " + " MAX(files.dateAdded) as dateAdded " + " FROM tvshow" + " LEFT JOIN episode ON" + " episode.idShow=tvshow.idShow" + " LEFT JOIN files ON" + " files.idFile=episode.idFile " + "GROUP BY tvshow.idShow"); + m_pDS->exec(tvshowcounts); + + CLog::Log(LOGINFO, "create tvshowlinkpath_minview"); + // This view only exists to workaround a limitation in MySQL <5.7 which is not able to + // perform subqueries in joins. + // Also, the correct solution is to remove the path information altogether, since a + // TV series can always have multiple paths. It is used in the GUI at the moment, but + // such usage should be removed together with this view and the path columns in tvshow_view. + //!@todo Remove the hacky selection of a semi-random path for tvshows from the queries and UI + std::string tvshowlinkpathview = PrepareSQL("CREATE VIEW tvshowlinkpath_minview AS SELECT " + " idShow, " + " min(idPath) AS idPath " + "FROM tvshowlinkpath " + "GROUP BY idShow"); + m_pDS->exec(tvshowlinkpathview); + + CLog::Log(LOGINFO, "create tvshow_view"); + std::string tvshowview = PrepareSQL("CREATE VIEW tvshow_view AS SELECT " + " tvshow.*," + " path.idParentPath AS idParentPath," + " path.strPath AS strPath," + " tvshowcounts.dateAdded AS dateAdded," + " lastPlayed, totalCount, watchedcount, totalSeasons, " + " rating.rating AS rating, " + " rating.votes AS votes, " + " rating.rating_type AS rating_type, " + " uniqueid.value AS uniqueid_value, " + " uniqueid.type AS uniqueid_type " + "FROM tvshow" + " LEFT JOIN tvshowlinkpath_minview ON " + " tvshowlinkpath_minview.idShow=tvshow.idShow" + " LEFT JOIN path ON" + " path.idPath=tvshowlinkpath_minview.idPath" + " INNER JOIN tvshowcounts ON" + " tvshow.idShow = tvshowcounts.idShow " + " LEFT JOIN rating ON" + " rating.rating_id=tvshow.c%02d " + " LEFT JOIN uniqueid ON" + " uniqueid.uniqueid_id=tvshow.c%02d ", + VIDEODB_ID_TV_RATING_ID, VIDEODB_ID_TV_IDENT_ID); + m_pDS->exec(tvshowview); + + CLog::Log(LOGINFO, "create season_view"); + std::string seasonview = PrepareSQL("CREATE VIEW season_view AS SELECT " + " seasons.idSeason AS idSeason," + " seasons.idShow AS idShow," + " seasons.season AS season," + " seasons.name AS name," + " seasons.userrating AS userrating," + " tvshow_view.strPath AS strPath," + " tvshow_view.c%02d AS showTitle," + " tvshow_view.c%02d AS plot," + " tvshow_view.c%02d AS premiered," + " tvshow_view.c%02d AS genre," + " tvshow_view.c%02d AS studio," + " tvshow_view.c%02d AS mpaa," + " count(DISTINCT episode.idEpisode) AS episodes," + " count(files.playCount) AS playCount," + " min(episode.c%02d) AS aired " + "FROM seasons" + " JOIN tvshow_view ON" + " tvshow_view.idShow = seasons.idShow" + " JOIN episode ON" + " episode.idShow = seasons.idShow AND episode.c%02d = seasons.season" + " JOIN files ON" + " files.idFile = episode.idFile " + "GROUP BY seasons.idSeason," + " seasons.idShow," + " seasons.season," + " seasons.name," + " seasons.userrating," + " tvshow_view.strPath," + " tvshow_view.c%02d," + " tvshow_view.c%02d," + " tvshow_view.c%02d," + " tvshow_view.c%02d," + " tvshow_view.c%02d," + " tvshow_view.c%02d ", + VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_PLOT, VIDEODB_ID_TV_PREMIERED, + VIDEODB_ID_TV_GENRE, VIDEODB_ID_TV_STUDIOS, VIDEODB_ID_TV_MPAA, + VIDEODB_ID_EPISODE_AIRED, VIDEODB_ID_EPISODE_SEASON, + VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_PLOT, VIDEODB_ID_TV_PREMIERED, + VIDEODB_ID_TV_GENRE, VIDEODB_ID_TV_STUDIOS, VIDEODB_ID_TV_MPAA); + m_pDS->exec(seasonview); + + CLog::Log(LOGINFO, "create musicvideo_view"); + m_pDS->exec(PrepareSQL( + "CREATE VIEW musicvideo_view AS SELECT" + " musicvideo.*," + " files.strFileName as strFileName," + " path.strPath as strPath," + " files.playCount as playCount," + " files.lastPlayed as lastPlayed," + " files.dateAdded as dateAdded, " + " bookmark.timeInSeconds AS resumeTimeInSeconds, " + " bookmark.totalTimeInSeconds AS totalTimeInSeconds, " + " bookmark.playerState AS playerState, " + " uniqueid.value AS uniqueid_value, " + " uniqueid.type AS uniqueid_type " + "FROM musicvideo" + " JOIN files ON" + " files.idFile=musicvideo.idFile" + " JOIN path ON" + " path.idPath=files.idPath" + " LEFT JOIN bookmark ON" + " bookmark.idFile=musicvideo.idFile AND bookmark.type=1" + " LEFT JOIN uniqueid ON" + " uniqueid.uniqueid_id=musicvideo.c%02d", + VIDEODB_ID_MUSICVIDEO_IDENT_ID)); + + CLog::Log(LOGINFO, "create movie_view"); + + std::string movieview = PrepareSQL("CREATE VIEW movie_view AS SELECT" + " movie.*," + " sets.strSet AS strSet," + " sets.strOverview AS strSetOverview," + " files.strFileName AS strFileName," + " path.strPath AS strPath," + " files.playCount AS playCount," + " files.lastPlayed AS lastPlayed, " + " files.dateAdded AS dateAdded, " + " bookmark.timeInSeconds AS resumeTimeInSeconds, " + " bookmark.totalTimeInSeconds AS totalTimeInSeconds, " + " bookmark.playerState AS playerState, " + " rating.rating AS rating, " + " rating.votes AS votes, " + " rating.rating_type AS rating_type, " + " uniqueid.value AS uniqueid_value, " + " uniqueid.type AS uniqueid_type " + "FROM movie" + " LEFT JOIN sets ON" + " sets.idSet = movie.idSet" + " JOIN files ON" + " files.idFile=movie.idFile" + " JOIN path ON" + " path.idPath=files.idPath" + " LEFT JOIN bookmark ON" + " bookmark.idFile=movie.idFile AND bookmark.type=1" + " LEFT JOIN rating ON" + " rating.rating_id=movie.c%02d" + " LEFT JOIN uniqueid ON" + " uniqueid.uniqueid_id=movie.c%02d", + VIDEODB_ID_RATING_ID, VIDEODB_ID_IDENT_ID); + m_pDS->exec(movieview); +} + +//******************************************************************************************************************************** +int CVideoDatabase::GetPathId(const std::string& strPath) +{ + std::string strSQL; + try + { + int idPath=-1; + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string strPath1(strPath); + if (URIUtils::IsStack(strPath) || StringUtils::StartsWithNoCase(strPath, "rar://") || StringUtils::StartsWithNoCase(strPath, "zip://")) + URIUtils::GetParentPath(strPath,strPath1); + + URIUtils::AddSlashAtEnd(strPath1); + + strSQL=PrepareSQL("select idPath from path where strPath='%s'",strPath1.c_str()); + m_pDS->query(strSQL); + if (!m_pDS->eof()) + idPath = m_pDS->fv("path.idPath").get_asInt(); + + m_pDS->close(); + return idPath; + } + catch (...) + { + CLog::Log(LOGERROR, "{} unable to getpath ({})", __FUNCTION__, strSQL); + } + return -1; +} + +bool CVideoDatabase::GetPaths(std::set<std::string> &paths) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + paths.clear(); + + // grab all paths with movie content set + if (!m_pDS->query("select strPath,noUpdate from path" + " where (strContent = 'movies' or strContent = 'musicvideos')" + " and strPath NOT like 'multipath://%%'" + " order by strPath")) + return false; + + while (!m_pDS->eof()) + { + if (!m_pDS->fv("noUpdate").get_asBool()) + paths.insert(m_pDS->fv("strPath").get_asString()); + m_pDS->next(); + } + m_pDS->close(); + + // then grab all tvshow paths + if (!m_pDS->query("select strPath,noUpdate from path" + " where ( strContent = 'tvshows'" + " or idPath in (select idPath from tvshowlinkpath))" + " and strPath NOT like 'multipath://%%'" + " order by strPath")) + return false; + + while (!m_pDS->eof()) + { + if (!m_pDS->fv("noUpdate").get_asBool()) + paths.insert(m_pDS->fv("strPath").get_asString()); + m_pDS->next(); + } + m_pDS->close(); + + // finally grab all other paths holding a movie which is not a stack or a rar archive + // - this isnt perfect but it should do fine in most situations. + // reason we need it to hold a movie is stacks from different directories (cdx folders for instance) + // not making mistakes must take priority + if (!m_pDS->query("select strPath,noUpdate from path" + " where idPath in (select idPath from files join movie on movie.idFile=files.idFile)" + " and idPath NOT in (select idPath from tvshowlinkpath)" + " and idPath NOT in (select idPath from files where strFileName like 'video_ts.ifo')" // dvd folders get stacked to a single item in parent folder + " and idPath NOT in (select idPath from files where strFileName like 'index.bdmv')" // bluray folders get stacked to a single item in parent folder + " and strPath NOT like 'multipath://%%'" + " and strContent NOT in ('movies', 'tvshows', 'None')" // these have been added above + " order by strPath")) + + return false; + while (!m_pDS->eof()) + { + if (!m_pDS->fv("noUpdate").get_asBool()) + paths.insert(m_pDS->fv("strPath").get_asString()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetPathsLinkedToTvShow(int idShow, std::vector<std::string> &paths) +{ + std::string sql; + try + { + sql = PrepareSQL("SELECT strPath FROM path JOIN tvshowlinkpath ON tvshowlinkpath.idPath=path.idPath WHERE idShow=%i", idShow); + m_pDS->query(sql); + while (!m_pDS->eof()) + { + paths.emplace_back(m_pDS->fv(0).get_asString()); + m_pDS->next(); + } + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} error during query: {}", __FUNCTION__, sql); + } + return false; +} + +bool CVideoDatabase::GetPathsForTvShow(int idShow, std::set<int>& paths) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // add base path + strSQL = PrepareSQL("SELECT strPath FROM tvshow_view WHERE idShow=%i", idShow); + if (m_pDS->query(strSQL)) + paths.insert(GetPathId(m_pDS->fv(0).get_asString())); + + // add all other known paths + strSQL = PrepareSQL("SELECT DISTINCT idPath FROM files JOIN episode ON episode.idFile=files.idFile WHERE episode.idShow=%i",idShow); + m_pDS->query(strSQL); + while (!m_pDS->eof()) + { + paths.insert(m_pDS->fv(0).get_asInt()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} error during query: {}", __FUNCTION__, strSQL); + } + return false; +} + +int CVideoDatabase::RunQuery(const std::string &sql) +{ + auto start = std::chrono::steady_clock::now(); + + int rows = -1; + if (m_pDS->query(sql)) + { + rows = m_pDS->num_rows(); + if (rows == 0) + m_pDS->close(); + } + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{} took {} ms for {} items query: {}", __FUNCTION__, + duration.count(), rows, sql); + + return rows; +} + +bool CVideoDatabase::GetSubPaths(const std::string &basepath, std::vector<std::pair<int, std::string>>& subpaths) +{ + std::string sql; + try + { + if (!m_pDB || !m_pDS) + return false; + + std::string path(basepath); + URIUtils::AddSlashAtEnd(path); + sql = PrepareSQL("SELECT idPath,strPath FROM path WHERE SUBSTR(strPath,1,%i)='%s'" + " AND idPath NOT IN (SELECT idPath FROM files WHERE strFileName LIKE 'video_ts.ifo')" + " AND idPath NOT IN (SELECT idPath FROM files WHERE strFileName LIKE 'index.bdmv')" + , StringUtils::utf8_strlen(path.c_str()), path.c_str()); + + m_pDS->query(sql); + while (!m_pDS->eof()) + { + subpaths.emplace_back(m_pDS->fv(0).get_asInt(), m_pDS->fv(1).get_asString()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} error during query: {}", __FUNCTION__, sql); + } + return false; +} + +int CVideoDatabase::AddPath(const std::string& strPath, const std::string &parentPath /*= "" */, const CDateTime& dateAdded /* = CDateTime() */) +{ + std::string strSQL; + try + { + int idPath = GetPathId(strPath); + if (idPath >= 0) + return idPath; // already have the path + + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string strPath1(strPath); + if (URIUtils::IsStack(strPath) || StringUtils::StartsWithNoCase(strPath, "rar://") || StringUtils::StartsWithNoCase(strPath, "zip://")) + URIUtils::GetParentPath(strPath,strPath1); + + URIUtils::AddSlashAtEnd(strPath1); + + int idParentPath = GetPathId(parentPath.empty() ? URIUtils::GetParentPath(strPath1) : parentPath); + + // add the path + if (idParentPath < 0) + { + if (dateAdded.IsValid()) + strSQL=PrepareSQL("insert into path (idPath, strPath, dateAdded) values (NULL, '%s', '%s')", strPath1.c_str(), dateAdded.GetAsDBDateTime().c_str()); + else + strSQL=PrepareSQL("insert into path (idPath, strPath) values (NULL, '%s')", strPath1.c_str()); + } + else + { + if (dateAdded.IsValid()) + strSQL = PrepareSQL("insert into path (idPath, strPath, dateAdded, idParentPath) values (NULL, '%s', '%s', %i)", strPath1.c_str(), dateAdded.GetAsDBDateTime().c_str(), idParentPath); + else + strSQL=PrepareSQL("insert into path (idPath, strPath, idParentPath) values (NULL, '%s', %i)", strPath1.c_str(), idParentPath); + } + m_pDS->exec(strSQL); + idPath = (int)m_pDS->lastinsertid(); + return idPath; + } + catch (...) + { + CLog::Log(LOGERROR, "{} unable to addpath ({})", __FUNCTION__, strSQL); + } + return -1; +} + +bool CVideoDatabase::GetPathHash(const std::string &path, std::string &hash) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL=PrepareSQL("select strHash from path where strPath='%s'", path.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + return false; + hash = m_pDS->fv("strHash").get_asString(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path); + } + + return false; +} + +bool CVideoDatabase::GetSourcePath(const std::string &path, std::string &sourcePath) +{ + SScanSettings dummy; + return GetSourcePath(path, sourcePath, dummy); +} + +bool CVideoDatabase::GetSourcePath(const std::string &path, std::string &sourcePath, SScanSettings& settings) +{ + try + { + if (path.empty() || m_pDB == nullptr || m_pDS == nullptr) + return false; + + std::string strPath2; + + if (URIUtils::IsMultiPath(path)) + strPath2 = CMultiPathDirectory::GetFirstPath(path); + else + strPath2 = path; + + std::string strPath1 = URIUtils::GetDirectory(strPath2); + int idPath = GetPathId(strPath1); + + if (idPath > -1) + { + // check if the given path already is a source itself + std::string strSQL = PrepareSQL("SELECT path.useFolderNames, path.scanRecursive, path.noUpdate, path.exclude FROM path WHERE " + "path.idPath = %i AND " + "path.strContent IS NOT NULL AND path.strContent != '' AND " + "path.strScraper IS NOT NULL AND path.strScraper != ''", idPath); + if (m_pDS->query(strSQL) && !m_pDS->eof()) + { + settings.parent_name_root = settings.parent_name = m_pDS->fv(0).get_asBool(); + settings.recurse = m_pDS->fv(1).get_asInt(); + settings.noupdate = m_pDS->fv(2).get_asBool(); + settings.exclude = m_pDS->fv(3).get_asBool(); + + m_pDS->close(); + sourcePath = path; + return true; + } + } + + // look for parent paths until there is one which is a source + std::string strParent; + bool found = false; + while (URIUtils::GetParentPath(strPath1, strParent)) + { + std::string strSQL = PrepareSQL("SELECT path.strContent, path.strScraper, path.scanRecursive, path.useFolderNames, path.noUpdate, path.exclude FROM path WHERE strPath = '%s'", strParent.c_str()); + if (m_pDS->query(strSQL) && !m_pDS->eof()) + { + std::string strContent = m_pDS->fv(0).get_asString(); + std::string strScraper = m_pDS->fv(1).get_asString(); + if (!strContent.empty() && !strScraper.empty()) + { + settings.parent_name_root = settings.parent_name = m_pDS->fv(2).get_asBool(); + settings.recurse = m_pDS->fv(3).get_asInt(); + settings.noupdate = m_pDS->fv(4).get_asBool(); + settings.exclude = m_pDS->fv(5).get_asBool(); + found = true; + break; + } + } + + strPath1 = strParent; + } + m_pDS->close(); + + if (found) + { + sourcePath = strParent; + return true; + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +//******************************************************************************************************************************** +int CVideoDatabase::AddFile(const std::string& strFileNameAndPath, + const std::string& parentPath /* = "" */, + const CDateTime& dateAdded /* = CDateTime() */, + int playcount /* = 0 */, + const CDateTime& lastPlayed /* = CDateTime() */) +{ + std::string strSQL = ""; + try + { + int idFile; + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + const auto finalDateAdded = GetDateAdded(strFileNameAndPath, dateAdded); + + std::string strFileName, strPath; + SplitPath(strFileNameAndPath,strPath,strFileName); + + int idPath = AddPath(strPath, parentPath, finalDateAdded); + if (idPath < 0) + return -1; + + std::string strSQL=PrepareSQL("select idFile from files where strFileName='%s' and idPath=%i", strFileName.c_str(),idPath); + + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + { + idFile = m_pDS->fv("idFile").get_asInt() ; + m_pDS->close(); + return idFile; + } + m_pDS->close(); + + std::string strPlaycount = "NULL"; + if (playcount > 0) + strPlaycount = std::to_string(playcount); + std::string strLastPlayed = "NULL"; + if (lastPlayed.IsValid()) + strLastPlayed = "'" + lastPlayed.GetAsDBDateTime() + "'"; + + strSQL = PrepareSQL("INSERT INTO files (idFile, idPath, strFileName, playCount, lastPlayed, dateAdded) " + "VALUES(NULL, %i, '%s', " + strPlaycount + ", " + strLastPlayed + ", '%s')", + idPath, strFileName.c_str(), finalDateAdded.GetAsDBDateTime().c_str()); + m_pDS->exec(strSQL); + idFile = (int)m_pDS->lastinsertid(); + return idFile; + } + catch (...) + { + CLog::Log(LOGERROR, "{} unable to addfile ({})", __FUNCTION__, strSQL); + } + return -1; +} + +int CVideoDatabase::AddFile(const CFileItem& item) +{ + if (item.IsVideoDb() && item.HasVideoInfoTag()) + { + const auto videoInfoTag = item.GetVideoInfoTag(); + if (videoInfoTag->m_iFileId != -1) + return videoInfoTag->m_iFileId; + else + return AddFile(*videoInfoTag); + } + return AddFile(item.GetPath()); +} + +int CVideoDatabase::AddFile(const CVideoInfoTag& details, const std::string& parentPath /* = "" */) +{ + return AddFile(details.GetPath(), parentPath, details.m_dateAdded, details.GetPlayCount(), + details.m_lastPlayed); +} + +void CVideoDatabase::UpdateFileDateAdded(CVideoInfoTag& details) +{ + if (details.GetPath().empty() || GetAndFillFileId(details) <= 0) + return; + + CDateTime finalDateAdded; + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + finalDateAdded = GetDateAdded(details.GetPath(), details.m_dateAdded); + + m_pDS->exec(PrepareSQL("UPDATE files SET dateAdded='%s' WHERE idFile=%d", + finalDateAdded.GetAsDBDateTime().c_str(), details.m_iFileId)); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}, {}) failed", __FUNCTION__, CURL::GetRedacted(details.GetPath()), + finalDateAdded.GetAsDBDateTime()); + } +} + +bool CVideoDatabase::SetPathHash(const std::string &path, const std::string &hash) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + int idPath = AddPath(path); + if (idPath < 0) return false; + + std::string strSQL=PrepareSQL("update path set strHash='%s' where idPath=%ld", hash.c_str(), idPath); + m_pDS->exec(strSQL); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, path, hash); + } + + return false; +} + +bool CVideoDatabase::LinkMovieToTvshow(int idMovie, int idShow, bool bRemove) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (bRemove) // delete link + { + std::string strSQL=PrepareSQL("delete from movielinktvshow where idMovie=%i and idShow=%i", idMovie, idShow); + m_pDS->exec(strSQL); + return true; + } + + std::string strSQL=PrepareSQL("insert into movielinktvshow (idShow,idMovie) values (%i,%i)", idShow,idMovie); + m_pDS->exec(strSQL); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, idMovie, idShow); + } + + return false; +} + +bool CVideoDatabase::IsLinkedToTvshow(int idMovie) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL=PrepareSQL("select * from movielinktvshow where idMovie=%i", idMovie); + m_pDS->query(strSQL); + if (m_pDS->eof()) + { + m_pDS->close(); + return false; + } + + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idMovie); + } + + return false; +} + +bool CVideoDatabase::GetLinksToTvShow(int idMovie, std::vector<int>& ids) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL=PrepareSQL("select * from movielinktvshow where idMovie=%i", idMovie); + m_pDS2->query(strSQL); + while (!m_pDS2->eof()) + { + ids.push_back(m_pDS2->fv(1).get_asInt()); + m_pDS2->next(); + } + + m_pDS2->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idMovie); + } + + return false; +} + + +//******************************************************************************************************************************** +int CVideoDatabase::GetFileId(const std::string& strFilenameAndPath) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + std::string strPath, strFileName; + SplitPath(strFilenameAndPath,strPath,strFileName); + + int idPath = GetPathId(strPath); + if (idPath >= 0) + { + std::string strSQL; + strSQL=PrepareSQL("select idFile from files where strFileName='%s' and idPath=%i", strFileName.c_str(),idPath); + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + { + int idFile = m_pDS->fv("files.idFile").get_asInt(); + m_pDS->close(); + return idFile; + } + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return -1; +} + +int CVideoDatabase::GetFileId(const CFileItem &item) +{ + int fileId = -1; + if (item.HasVideoInfoTag()) + fileId = GetFileId(*item.GetVideoInfoTag()); + + if (fileId == -1) + fileId = GetFileId(item.GetPath()); + + return fileId; +} + +int CVideoDatabase::GetFileId(const CVideoInfoTag& details) +{ + if (details.m_iFileId > 0) + return details.m_iFileId; + + const auto& filePath = details.GetPath(); + if (filePath.empty()) + return -1; + + return GetFileId(filePath); +} + +int CVideoDatabase::GetAndFillFileId(CVideoInfoTag& details) +{ + details.m_iFileId = GetFileId(details); + return details.m_iFileId; +} + +//******************************************************************************************************************************** +int CVideoDatabase::GetMovieId(const std::string& strFilenameAndPath) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + int idMovie = -1; + + // needed for query parameters + int idFile = GetFileId(strFilenameAndPath); + int idPath=-1; + std::string strPath; + if (idFile < 0) + { + std::string strFile; + SplitPath(strFilenameAndPath,strPath,strFile); + + // have to join movieinfo table for correct results + idPath = GetPathId(strPath); + if (idPath < 0 && strPath != strFilenameAndPath) + return -1; + } + + if (idFile == -1 && strPath != strFilenameAndPath) + return -1; + + std::string strSQL; + if (idFile == -1) + strSQL=PrepareSQL("select idMovie from movie join files on files.idFile=movie.idFile where files.idPath=%i",idPath); + else + strSQL=PrepareSQL("select idMovie from movie where idFile=%i", idFile); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{} ({}), query = {}", __FUNCTION__, + CURL::GetRedacted(strFilenameAndPath), strSQL); + m_pDS->query(strSQL); + if (m_pDS->num_rows() > 0) + idMovie = m_pDS->fv("idMovie").get_asInt(); + m_pDS->close(); + + return idMovie; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return -1; +} + +int CVideoDatabase::GetTvShowId(const std::string& strPath) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + int idTvShow = -1; + + // have to join movieinfo table for correct results + int idPath = GetPathId(strPath); + if (idPath < 0) + return -1; + + std::string strSQL; + std::string strPath1=strPath; + std::string strParent; + int iFound=0; + + strSQL=PrepareSQL("select idShow from tvshowlinkpath where tvshowlinkpath.idPath=%i",idPath); + m_pDS->query(strSQL); + if (!m_pDS->eof()) + iFound = 1; + + while (iFound == 0 && URIUtils::GetParentPath(strPath1, strParent)) + { + strSQL=PrepareSQL("SELECT idShow FROM path INNER JOIN tvshowlinkpath ON tvshowlinkpath.idPath=path.idPath WHERE strPath='%s'",strParent.c_str()); + m_pDS->query(strSQL); + if (!m_pDS->eof()) + { + int idShow = m_pDS->fv("idShow").get_asInt(); + if (idShow != -1) + iFound = 2; + } + strPath1 = strParent; + } + + if (m_pDS->num_rows() > 0) + idTvShow = m_pDS->fv("idShow").get_asInt(); + m_pDS->close(); + + return idTvShow; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath); + } + return -1; +} + +int CVideoDatabase::GetEpisodeId(const std::string& strFilenameAndPath, int idEpisode, int idSeason) // input value is episode/season number hint - for multiparters +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + // need this due to the nested GetEpisodeInfo query + std::unique_ptr<Dataset> pDS; + pDS.reset(m_pDB->CreateDataset()); + if (nullptr == pDS) + return -1; + + int idFile = GetFileId(strFilenameAndPath); + if (idFile < 0) + return -1; + + std::string strSQL=PrepareSQL("select idEpisode from episode where idFile=%i", idFile); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{} ({}), query = {}", __FUNCTION__, + CURL::GetRedacted(strFilenameAndPath), strSQL); + pDS->query(strSQL); + if (pDS->num_rows() > 0) + { + if (idEpisode == -1) + idEpisode = pDS->fv("episode.idEpisode").get_asInt(); + else // use the hint! + { + while (!pDS->eof()) + { + CVideoInfoTag tag; + int idTmpEpisode = pDS->fv("episode.idEpisode").get_asInt(); + GetEpisodeBasicInfo(strFilenameAndPath, tag, idTmpEpisode); + if (tag.m_iEpisode == idEpisode && (idSeason == -1 || tag.m_iSeason == idSeason)) { + // match on the episode hint, and there's no season hint or a season hint match + idEpisode = idTmpEpisode; + break; + } + pDS->next(); + } + if (pDS->eof()) + idEpisode = -1; + } + } + else + idEpisode = -1; + + pDS->close(); + + return idEpisode; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return -1; +} + +int CVideoDatabase::GetMusicVideoId(const std::string& strFilenameAndPath) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + int idFile = GetFileId(strFilenameAndPath); + if (idFile < 0) + return -1; + + std::string strSQL=PrepareSQL("select idMVideo from musicvideo where idFile=%i", idFile); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{} ({}), query = {}", __FUNCTION__, + CURL::GetRedacted(strFilenameAndPath), strSQL); + m_pDS->query(strSQL); + int idMVideo=-1; + if (m_pDS->num_rows() > 0) + idMVideo = m_pDS->fv("idMVideo").get_asInt(); + m_pDS->close(); + + return idMVideo; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return -1; +} + +//******************************************************************************************************************************** +int CVideoDatabase::AddNewMovie(CVideoInfoTag& details) +{ + const auto filePath = details.GetPath(); + + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + if (details.m_iFileId <= 0) + { + details.m_iFileId = AddFile(details); + if (details.m_iFileId <= 0) + return -1; + } + + std::string strSQL = + PrepareSQL("INSERT INTO movie (idMovie, idFile) VALUES (NULL, %i)", details.m_iFileId); + m_pDS->exec(strSQL); + details.m_iDbId = static_cast<int>(m_pDS->lastinsertid()); + + return details.m_iDbId; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } + return -1; +} + +bool CVideoDatabase::AddPathToTvShow(int idShow, const std::string &path, const std::string &parentPath, const CDateTime& dateAdded /* = CDateTime() */) +{ + // Check if this path is already added + int idPath = GetPathId(path); + if (idPath < 0) + idPath = AddPath(path, parentPath, GetDateAdded(path, dateAdded)); + + return ExecuteQuery(PrepareSQL("REPLACE INTO tvshowlinkpath(idShow, idPath) VALUES (%i,%i)", idShow, idPath)); +} + +int CVideoDatabase::AddTvShow() +{ + if (ExecuteQuery("INSERT INTO tvshow(idShow) VALUES(NULL)")) + return (int)m_pDS->lastinsertid(); + return -1; +} + +//******************************************************************************************************************************** +int CVideoDatabase::AddNewEpisode(int idShow, CVideoInfoTag& details) +{ + const auto filePath = details.GetPath(); + + try + { + if (nullptr == m_pDB || nullptr == m_pDS) + return -1; + + if (details.m_iFileId <= 0) + { + details.m_iFileId = AddFile(details); + if (details.m_iFileId <= 0) + return -1; + } + + std::string strSQL = + PrepareSQL("INSERT INTO episode (idEpisode, idFile, idShow) VALUES (NULL, %i, %i)", + details.m_iFileId, idShow); + m_pDS->exec(strSQL); + details.m_iDbId = static_cast<int>(m_pDS->lastinsertid()); + + return details.m_iDbId; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } + return -1; +} + +int CVideoDatabase::AddNewMusicVideo(CVideoInfoTag& details) +{ + const auto filePath = details.GetPath(); + + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + if (details.m_iFileId <= 0) + { + details.m_iFileId = AddFile(details); + if (details.m_iFileId <= 0) + return -1; + } + + std::string strSQL = PrepareSQL("INSERT INTO musicvideo (idMVideo, idFile) VALUES (NULL, %i)", + details.m_iFileId); + m_pDS->exec(strSQL); + details.m_iDbId = static_cast<int>(m_pDS->lastinsertid()); + + return details.m_iDbId; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } + return -1; +} + +//******************************************************************************************************************************** +int CVideoDatabase::AddToTable(const std::string& table, const std::string& firstField, const std::string& secondField, const std::string& value) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string strSQL = PrepareSQL("select %s from %s where %s like '%s'", firstField.c_str(), table.c_str(), secondField.c_str(), value.substr(0, 255).c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + // doesn't exists, add it + strSQL = PrepareSQL("insert into %s (%s, %s) values(NULL, '%s')", table.c_str(), firstField.c_str(), secondField.c_str(), value.substr(0, 255).c_str()); + m_pDS->exec(strSQL); + int id = (int)m_pDS->lastinsertid(); + return id; + } + else + { + int id = m_pDS->fv(firstField.c_str()).get_asInt(); + m_pDS->close(); + return id; + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, value); + } + + return -1; +} + +int CVideoDatabase::UpdateRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string sql = PrepareSQL("DELETE FROM rating WHERE media_id=%i AND media_type='%s'", mediaId, mediaType); + m_pDS->exec(sql); + + return AddRatings(mediaId, mediaType, values, defaultRating); + } + catch (...) + { + CLog::Log(LOGERROR, "{} unable to update ratings of ({})", __FUNCTION__, mediaType); + } + return -1; +} + +int CVideoDatabase::AddRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating) +{ + int ratingid = -1; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + for (const auto& i : values) + { + int id; + std::string strSQL = PrepareSQL("SELECT rating_id FROM rating WHERE media_id=%i AND media_type='%s' AND rating_type = '%s'", mediaId, mediaType, i.first.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + // doesn't exists, add it + strSQL = PrepareSQL("INSERT INTO rating (media_id, media_type, rating_type, rating, votes) " + "VALUES (%i, '%s', '%s', %f, %i)", + mediaId, mediaType, i.first.c_str(), + static_cast<double>(i.second.rating), i.second.votes); + m_pDS->exec(strSQL); + id = (int)m_pDS->lastinsertid(); + } + else + { + id = m_pDS->fv(0).get_asInt(); + m_pDS->close(); + strSQL = PrepareSQL("UPDATE rating SET rating = %f, votes = %i WHERE rating_id = %i", + static_cast<double>(i.second.rating), i.second.votes, id); + m_pDS->exec(strSQL); + } + if (i.first == defaultRating) + ratingid = id; + } + return ratingid; + + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({} - {}) failed", __FUNCTION__, mediaId, mediaType); + } + + return ratingid; +} + +int CVideoDatabase::UpdateUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string sql = PrepareSQL("DELETE FROM uniqueid WHERE media_id=%i AND media_type='%s'", mediaId, mediaType); + m_pDS->exec(sql); + + return AddUniqueIDs(mediaId, mediaType, details); + } + catch (...) + { + CLog::Log(LOGERROR, "{} unable to update unique ids of ({})", __FUNCTION__, mediaType); + } + return -1; +} + +int CVideoDatabase::AddUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details) +{ + int uniqueid = -1; + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + for (const auto& i : details.GetUniqueIDs()) + { + int id; + std::string strSQL = PrepareSQL("SELECT uniqueid_id FROM uniqueid WHERE media_id=%i AND media_type='%s' AND type = '%s'", mediaId, mediaType, i.first.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + // doesn't exists, add it + strSQL = PrepareSQL("INSERT INTO uniqueid (media_id, media_type, value, type) VALUES (%i, '%s', '%s', '%s')", mediaId, mediaType, i.second.c_str(), i.first.c_str()); + m_pDS->exec(strSQL); + id = (int)m_pDS->lastinsertid(); + } + else + { + id = m_pDS->fv(0).get_asInt(); + m_pDS->close(); + strSQL = PrepareSQL("UPDATE uniqueid SET value = '%s', type = '%s' WHERE uniqueid_id = %i", i.second.c_str(), i.first.c_str(), id); + m_pDS->exec(strSQL); + } + if (i.first == details.GetDefaultUniqueID()) + uniqueid = id; + } + return uniqueid; + + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({} - {}) failed", __FUNCTION__, mediaId, mediaType); + } + + return uniqueid; +} + +int CVideoDatabase::AddSet(const std::string& strSet, const std::string& strOverview /* = "" */) +{ + if (strSet.empty()) + return -1; + + try + { + if (m_pDB == nullptr || m_pDS == nullptr) + return -1; + + std::string strSQL = PrepareSQL("SELECT idSet FROM sets WHERE strSet LIKE '%s'", strSet.c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + strSQL = PrepareSQL("INSERT INTO sets (idSet, strSet, strOverview) VALUES(NULL, '%s', '%s')", strSet.c_str(), strOverview.c_str()); + m_pDS->exec(strSQL); + int id = static_cast<int>(m_pDS->lastinsertid()); + return id; + } + else + { + int id = m_pDS->fv("idSet").get_asInt(); + m_pDS->close(); + return id; + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSet); + } + + return -1; +} + +int CVideoDatabase::AddTag(const std::string& name) +{ + if (name.empty()) + return -1; + + return AddToTable("tag", "tag_id", "name", name); +} + +int CVideoDatabase::AddActor(const std::string& name, const std::string& thumbURLs, const std::string &thumb) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + int idActor = -1; + + // ATTENTION: the trimming of actor names should really not be done here but after the scraping / NFO-parsing + std::string trimmedName = name.c_str(); + StringUtils::Trim(trimmedName); + + std::string strSQL=PrepareSQL("select actor_id from actor where name like '%s'", trimmedName.substr(0, 255).c_str()); + m_pDS->query(strSQL); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + // doesn't exists, add it + strSQL=PrepareSQL("insert into actor (actor_id, name, art_urls) values(NULL, '%s', '%s')", trimmedName.substr(0,255).c_str(), thumbURLs.c_str()); + m_pDS->exec(strSQL); + idActor = (int)m_pDS->lastinsertid(); + } + else + { + idActor = m_pDS->fv(0).get_asInt(); + m_pDS->close(); + // update the thumb url's + if (!thumbURLs.empty()) + { + strSQL=PrepareSQL("update actor set art_urls = '%s' where actor_id = %i", thumbURLs.c_str(), idActor); + m_pDS->exec(strSQL); + } + } + // add artwork + if (!thumb.empty()) + SetArtForItem(idActor, "actor", "thumb", thumb); + return idActor; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, name); + } + return -1; +} + + + +void CVideoDatabase::AddLinkToActor(int mediaId, const char *mediaType, int actorId, const std::string &role, int order) +{ + std::string sql = PrepareSQL("SELECT 1 FROM actor_link WHERE actor_id=%i AND " + "media_id=%i AND media_type='%s' AND role='%s'", + actorId, mediaId, mediaType, role.c_str()); + + if (GetSingleValue(sql).empty()) + { // doesn't exists, add it + sql = PrepareSQL("INSERT INTO actor_link (actor_id, media_id, media_type, role, cast_order) VALUES(%i,%i,'%s','%s',%i)", actorId, mediaId, mediaType, role.c_str(), order); + ExecuteQuery(sql); + } +} + +void CVideoDatabase::AddToLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey) +{ + const char *key = foreignKey ? foreignKey : table.c_str(); + std::string sql = PrepareSQL("SELECT 1 FROM %s_link WHERE %s_id=%i AND media_id=%i AND media_type='%s'", table.c_str(), key, valueId, mediaId, mediaType.c_str()); + + if (GetSingleValue(sql).empty()) + { // doesn't exists, add it + sql = PrepareSQL("INSERT INTO %s_link (%s_id,media_id,media_type) VALUES(%i,%i,'%s')", table.c_str(), key, valueId, mediaId, mediaType.c_str()); + ExecuteQuery(sql); + } +} + +void CVideoDatabase::RemoveFromLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey) +{ + const char *key = foreignKey ? foreignKey : table.c_str(); + std::string sql = PrepareSQL("DELETE FROM %s_link WHERE %s_id=%i AND media_id=%i AND media_type='%s'", table.c_str(), key, valueId, mediaId, mediaType.c_str()); + + ExecuteQuery(sql); +} + +void CVideoDatabase::AddLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values) +{ + for (const auto &i : values) + { + if (!i.empty()) + { + int idValue = AddToTable(field, field + "_id", "name", i); + if (idValue > -1) + AddToLinkTable(mediaId, mediaType, field, idValue); + } + } +} + +void CVideoDatabase::UpdateLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values) +{ + std::string sql = PrepareSQL("DELETE FROM %s_link WHERE media_id=%i AND media_type='%s'", field.c_str(), mediaId, mediaType.c_str()); + m_pDS->exec(sql); + + AddLinksToItem(mediaId, mediaType, field, values); +} + +void CVideoDatabase::AddActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values) +{ + for (const auto &i : values) + { + if (!i.empty()) + { + int idValue = AddActor(i, ""); + if (idValue > -1) + AddToLinkTable(mediaId, mediaType, field, idValue, "actor"); + } + } +} + +void CVideoDatabase::UpdateActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values) +{ + std::string sql = PrepareSQL("DELETE FROM %s_link WHERE media_id=%i AND media_type='%s'", field.c_str(), mediaId, mediaType.c_str()); + m_pDS->exec(sql); + + AddActorLinksToItem(mediaId, mediaType, field, values); +} + +//****Tags**** +void CVideoDatabase::AddTagToItem(int media_id, int tag_id, const std::string &type) +{ + if (type.empty()) + return; + + AddToLinkTable(media_id, type, "tag", tag_id); +} + +void CVideoDatabase::RemoveTagFromItem(int media_id, int tag_id, const std::string &type) +{ + if (type.empty()) + return; + + RemoveFromLinkTable(media_id, type, "tag", tag_id); +} + +void CVideoDatabase::RemoveTagsFromItem(int media_id, const std::string &type) +{ + if (type.empty()) + return; + + m_pDS2->exec(PrepareSQL("DELETE FROM tag_link WHERE media_id=%d AND media_type='%s'", media_id, type.c_str())); +} + +//****Actors**** +void CVideoDatabase::AddCast(int mediaId, const char *mediaType, const std::vector< SActorInfo > &cast) +{ + if (cast.empty()) + return; + + int order = std::max_element(cast.begin(), cast.end())->order; + for (const auto &i : cast) + { + int idActor = AddActor(i.strName, i.thumbUrl.GetData(), i.thumb); + AddLinkToActor(mediaId, mediaType, idActor, i.strRole, i.order >= 0 ? i.order : ++order); + } +} + +//******************************************************************************************************************************** +bool CVideoDatabase::LoadVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int getDetails /* = VideoDbDetailsAll */) +{ + if (GetMovieInfo(strFilenameAndPath, details)) + return true; + if (GetEpisodeInfo(strFilenameAndPath, details)) + return true; + if (GetMusicVideoInfo(strFilenameAndPath, details)) + return true; + if (GetFileInfo(strFilenameAndPath, details)) + return true; + + return false; +} + +bool CVideoDatabase::HasMovieInfo(const std::string& strFilenameAndPath) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + int idMovie = GetMovieId(strFilenameAndPath); + return (idMovie > 0); // index of zero is also invalid + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +bool CVideoDatabase::HasTvShowInfo(const std::string& strPath) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + int idTvShow = GetTvShowId(strPath); + return (idTvShow > 0); // index of zero is also invalid + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath); + } + return false; +} + +bool CVideoDatabase::HasEpisodeInfo(const std::string& strFilenameAndPath) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + int idEpisode = GetEpisodeId(strFilenameAndPath); + return (idEpisode > 0); // index of zero is also invalid + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +bool CVideoDatabase::HasMusicVideoInfo(const std::string& strFilenameAndPath) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + int idMVideo = GetMusicVideoId(strFilenameAndPath); + return (idMVideo > 0); // index of zero is also invalid + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +void CVideoDatabase::DeleteDetailsForTvShow(int idTvShow) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + std::string strSQL; + strSQL=PrepareSQL("DELETE from genre_link WHERE media_id=%i AND media_type='tvshow'", idTvShow); + m_pDS->exec(strSQL); + + strSQL=PrepareSQL("DELETE FROM actor_link WHERE media_id=%i AND media_type='tvshow'", idTvShow); + m_pDS->exec(strSQL); + + strSQL=PrepareSQL("DELETE FROM director_link WHERE media_id=%i AND media_type='tvshow'", idTvShow); + m_pDS->exec(strSQL); + + strSQL=PrepareSQL("DELETE FROM studio_link WHERE media_id=%i AND media_type='tvshow'", idTvShow); + m_pDS->exec(strSQL); + + strSQL = PrepareSQL("DELETE FROM rating WHERE media_id=%i AND media_type='tvshow'", idTvShow); + m_pDS->exec(strSQL); + + strSQL = PrepareSQL("DELETE FROM uniqueid WHERE media_id=%i AND media_type='tvshow'", idTvShow); + m_pDS->exec(strSQL); + + // remove all info other than the id + // we do this due to the way we have the link between the file + movie tables. + + std::vector<std::string> ids; + for (int iType = VIDEODB_ID_TV_MIN + 1; iType < VIDEODB_ID_TV_MAX; iType++) + ids.emplace_back(StringUtils::Format("c{:02}=NULL", iType)); + + strSQL = "update tvshow set "; + strSQL += StringUtils::Join(ids, ", "); + strSQL += PrepareSQL(" where idShow=%i", idTvShow); + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idTvShow); + } +} + +//******************************************************************************************************************************** +void CVideoDatabase::GetMoviesByActor(const std::string& name, CFileItemList& items) +{ + Filter filter; + filter.join = "LEFT JOIN actor_link ON actor_link.media_id=movie_view.idMovie AND actor_link.media_type='movie' " + "LEFT JOIN actor a ON a.actor_id=actor_link.actor_id " + "LEFT JOIN director_link ON director_link.media_id=movie_view.idMovie AND director_link.media_type='movie' " + "LEFT JOIN actor d ON d.actor_id=director_link.actor_id"; + filter.where = PrepareSQL("a.name='%s' OR d.name='%s'", name.c_str(), name.c_str()); + filter.group = "movie_view.idMovie"; + GetMoviesByWhere("videodb://movies/titles/", filter, items); +} + +void CVideoDatabase::GetTvShowsByActor(const std::string& name, CFileItemList& items) +{ + Filter filter; + filter.join = "LEFT JOIN actor_link ON actor_link.media_id=tvshow_view.idShow AND actor_link.media_type='tvshow' " + "LEFT JOIN actor a ON a.actor_id=actor_link.actor_id " + "LEFT JOIN director_link ON director_link.media_id=tvshow_view.idShow AND director_link.media_type='tvshow' " + "LEFT JOIN actor d ON d.actor_id=director_link.actor_id"; + filter.where = PrepareSQL("a.name='%s' OR d.name='%s'", name.c_str(), name.c_str()); + GetTvShowsByWhere("videodb://tvshows/titles/", filter, items); +} + +void CVideoDatabase::GetEpisodesByActor(const std::string& name, CFileItemList& items) +{ + Filter filter; + filter.join = "LEFT JOIN actor_link ON actor_link.media_id=episode_view.idEpisode AND actor_link.media_type='episode' " + "LEFT JOIN actor a ON a.actor_id=actor_link.actor_id " + "LEFT JOIN director_link ON director_link.media_id=episode_view.idEpisode AND director_link.media_type='episode' " + "LEFT JOIN actor d ON d.actor_id=director_link.actor_id"; + filter.where = PrepareSQL("a.name='%s' OR d.name='%s'", name.c_str(), name.c_str()); + filter.group = "episode_view.idEpisode"; + GetEpisodesByWhere("videodb://tvshows/titles/", filter, items); +} + +void CVideoDatabase::GetMusicVideosByArtist(const std::string& strArtist, CFileItemList& items) +{ + try + { + items.Clear(); + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + std::string strSQL; + if (strArtist.empty()) //! @todo SMARTPLAYLISTS what is this here for??? + strSQL=PrepareSQL("select distinct * from musicvideo_view join actor_link on actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo' join actor on actor.actor_id=actor_link.actor_id"); + else // same artist OR same director + strSQL = PrepareSQL( + "select * from musicvideo_view join actor_link on " + "actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo' " + "join actor on actor.actor_id=actor_link.actor_id where actor.name='%s' OR " + "musicvideo_view.c05='%s' GROUP BY idMVideo", + strArtist.c_str(), strArtist.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + CVideoInfoTag tag = GetDetailsForMusicVideo(m_pDS); + CFileItemPtr pItem(new CFileItem(tag)); + pItem->SetLabel(StringUtils::Join(tag.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator)); + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strArtist); + } +} + +//******************************************************************************************************************************** +bool CVideoDatabase::GetMovieInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMovie /* = -1 */, int getDetails /* = VideoDbDetailsAll */) +{ + try + { + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + if (idMovie < 0) + idMovie = GetMovieId(strFilenameAndPath); + if (idMovie < 0) return false; + + std::string sql = PrepareSQL("select * from movie_view where idMovie=%i", idMovie); + if (!m_pDS->query(sql)) + return false; + details = GetDetailsForMovie(m_pDS, getDetails); + return !details.IsEmpty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +//******************************************************************************************************************************** +bool CVideoDatabase::GetTvShowInfo(const std::string& strPath, CVideoInfoTag& details, int idTvShow /* = -1 */, CFileItem *item /* = NULL */, int getDetails /* = VideoDbDetailsAll */) +{ + try + { + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + if (idTvShow < 0) + idTvShow = GetTvShowId(strPath); + if (idTvShow < 0) return false; + + std::string sql = PrepareSQL("SELECT * FROM tvshow_view WHERE idShow=%i GROUP BY idShow", idTvShow); + if (!m_pDS->query(sql)) + return false; + details = GetDetailsForTvShow(m_pDS, getDetails, item); + return !details.IsEmpty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath); + } + return false; +} + +bool CVideoDatabase::GetSeasonInfo(int idSeason, CVideoInfoTag& details, bool allDetails /* = true */) +{ + return GetSeasonInfo(idSeason, details, allDetails, nullptr); +} + +bool CVideoDatabase::GetSeasonInfo(int idSeason, CVideoInfoTag& details, CFileItem* item) +{ + return GetSeasonInfo(idSeason, details, true, item); +} + +bool CVideoDatabase::GetSeasonInfo(int idSeason, + CVideoInfoTag& details, + bool allDetails, + CFileItem* item) +{ + if (idSeason < 0) + return false; + + try + { + if (!m_pDB || !m_pDS) + return false; + + std::string sql = PrepareSQL("SELECT idSeason, idShow, season, name, userrating FROM seasons WHERE idSeason=%i", idSeason); + if (!m_pDS->query(sql)) + return false; + + if (m_pDS->num_rows() != 1) + return false; + + if (allDetails) + { + int idShow = m_pDS->fv(1).get_asInt(); + + // close the current result because we are going to query the season view for all details + m_pDS->close(); + + if (idShow < 0) + return false; + + CFileItemList seasons; + if (!GetSeasonsNav(StringUtils::Format("videodb://tvshows/titles/{}/", idShow), seasons, -1, + -1, -1, -1, idShow, false) || + seasons.Size() <= 0) + return false; + + for (int index = 0; index < seasons.Size(); index++) + { + const CFileItemPtr season = seasons.Get(index); + if (season->HasVideoInfoTag() && season->GetVideoInfoTag()->m_iDbId == idSeason && season->GetVideoInfoTag()->m_iIdShow == idShow) + { + details = *season->GetVideoInfoTag(); + if (item) + *item = *season; + return true; + } + } + + return false; + } + + const int season = m_pDS->fv(2).get_asInt(); + std::string name = m_pDS->fv(3).get_asString(); + + if (name.empty()) + { + if (season == 0) + name = g_localizeStrings.Get(20381); + else + name = StringUtils::Format(g_localizeStrings.Get(20358), season); + } + + details.m_strTitle = name; + if (!name.empty()) + details.m_strSortTitle = name; + details.m_iSeason = season; + details.m_iDbId = m_pDS->fv(0).get_asInt(); + details.m_iIdSeason = details.m_iDbId; + details.m_type = MediaTypeSeason; + details.m_iUserRating = m_pDS->fv(4).get_asInt(); + details.m_iIdShow = m_pDS->fv(1).get_asInt(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSeason); + } + return false; +} + +bool CVideoDatabase::GetEpisodeBasicInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode /* = -1 */) +{ + try + { + if (idEpisode < 0) + idEpisode = GetEpisodeId(strFilenameAndPath); + + if (idEpisode < 0) + return false; + + std::string sql = PrepareSQL("select * from episode where idEpisode=%i",idEpisode); + if (!m_pDS->query(sql)) + return false; + details = GetBasicDetailsForEpisode(m_pDS); + return !details.IsEmpty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +bool CVideoDatabase::GetEpisodeInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode /* = -1 */, int getDetails /* = VideoDbDetailsAll */) +{ + try + { + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + if (idEpisode < 0) + idEpisode = GetEpisodeId(strFilenameAndPath, details.m_iEpisode, details.m_iSeason); + if (idEpisode < 0) return false; + + std::string sql = PrepareSQL("select * from episode_view where idEpisode=%i",idEpisode); + if (!m_pDS->query(sql)) + return false; + details = GetDetailsForEpisode(m_pDS, getDetails); + return !details.IsEmpty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +bool CVideoDatabase::GetMusicVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMVideo /* = -1 */, int getDetails /* = VideoDbDetailsAll */) +{ + try + { + if (m_pDB == nullptr || m_pDS == nullptr) + return false; + + if (idMVideo < 0) + idMVideo = GetMusicVideoId(strFilenameAndPath); + if (idMVideo < 0) return false; + + std::string sql = PrepareSQL("select * from musicvideo_view where idMVideo=%i", idMVideo); + if (!m_pDS->query(sql)) + return false; + details = GetDetailsForMusicVideo(m_pDS, getDetails); + return !details.IsEmpty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +bool CVideoDatabase::GetSetInfo(int idSet, CVideoInfoTag& details, CFileItem* item /* = nullptr */) +{ + try + { + if (idSet < 0) + return false; + + Filter filter; + filter.where = PrepareSQL("sets.idSet=%d", idSet); + CFileItemList items; + if (!GetSetsByWhere("videodb://movies/sets/", filter, items) || + items.Size() != 1 || + !items[0]->HasVideoInfoTag()) + return false; + + details = *(items[0]->GetVideoInfoTag()); + if (item) + *item = *items[0]; + return !details.IsEmpty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSet); + } + return false; +} + +bool CVideoDatabase::GetFileInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idFile /* = -1 */) +{ + try + { + if (idFile < 0) + idFile = GetFileId(strFilenameAndPath); + if (idFile < 0) + return false; + + std::string sql = PrepareSQL("SELECT * FROM files " + "JOIN path ON path.idPath = files.idPath " + "LEFT JOIN bookmark ON bookmark.idFile = files.idFile AND bookmark.type = %i " + "WHERE files.idFile = %i", CBookmark::RESUME, idFile); + if (!m_pDS->query(sql)) + return false; + + details.m_iFileId = m_pDS->fv("files.idFile").get_asInt(); + details.m_strPath = m_pDS->fv("path.strPath").get_asString(); + std::string strFileName = m_pDS->fv("files.strFilename").get_asString(); + ConstructPath(details.m_strFileNameAndPath, details.m_strPath, strFileName); + details.SetPlayCount(std::max(details.GetPlayCount(), m_pDS->fv("files.playCount").get_asInt())); + if (!details.m_lastPlayed.IsValid()) + details.m_lastPlayed.SetFromDBDateTime(m_pDS->fv("files.lastPlayed").get_asString()); + if (!details.m_dateAdded.IsValid()) + details.m_dateAdded.SetFromDBDateTime(m_pDS->fv("files.dateAdded").get_asString()); + if (!details.GetResumePoint().IsSet() || + (!details.GetResumePoint().HasSavedPlayerState() && + !m_pDS->fv("bookmark.playerState").get_asString().empty())) + { + details.SetResumePoint(m_pDS->fv("bookmark.timeInSeconds").get_asDouble(), + m_pDS->fv("bookmark.totalTimeInSeconds").get_asDouble(), + m_pDS->fv("bookmark.playerState").get_asString()); + } + + // get streamdetails + GetStreamDetails(details); + + return !details.IsEmpty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } + return false; +} + +std::string CVideoDatabase::GetValueString(const CVideoInfoTag &details, int min, int max, const SDbTableOffsets *offsets) const +{ + std::vector<std::string> conditions; + for (int i = min + 1; i < max; ++i) + { + switch (offsets[i].type) + { + case VIDEODB_TYPE_STRING: + conditions.emplace_back(PrepareSQL("c%02d='%s'", i, ((const std::string*)(((const char*)&details)+offsets[i].offset))->c_str())); + break; + case VIDEODB_TYPE_INT: + conditions.emplace_back(PrepareSQL("c%02d='%i'", i, *(const int*)(((const char*)&details)+offsets[i].offset))); + break; + case VIDEODB_TYPE_COUNT: + { + int value = *(const int*)(((const char*)&details)+offsets[i].offset); + if (value) + conditions.emplace_back(PrepareSQL("c%02d=%i", i, value)); + else + conditions.emplace_back(PrepareSQL("c%02d=NULL", i)); + } + break; + case VIDEODB_TYPE_BOOL: + conditions.emplace_back(PrepareSQL("c%02d='%s'", i, *(const bool*)(((const char*)&details)+offsets[i].offset)?"true":"false")); + break; + case VIDEODB_TYPE_FLOAT: + conditions.emplace_back(PrepareSQL( + "c%02d='%f'", i, *(const double*)(((const char*)&details) + offsets[i].offset))); + break; + case VIDEODB_TYPE_STRINGARRAY: + conditions.emplace_back(PrepareSQL("c%02d='%s'", i, StringUtils::Join(*((const std::vector<std::string>*)(((const char*)&details)+offsets[i].offset)), + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator).c_str())); + break; + case VIDEODB_TYPE_DATE: + conditions.emplace_back(PrepareSQL("c%02d='%s'", i, ((const CDateTime*)(((const char*)&details)+offsets[i].offset))->GetAsDBDate().c_str())); + break; + case VIDEODB_TYPE_DATETIME: + conditions.emplace_back(PrepareSQL("c%02d='%s'", i, ((const CDateTime*)(((const char*)&details)+offsets[i].offset))->GetAsDBDateTime().c_str())); + break; + case VIDEODB_TYPE_UNUSED: // Skip the unused field to avoid populating unused data + continue; + } + } + return StringUtils::Join(conditions, ","); +} + +//******************************************************************************************************************************** +int CVideoDatabase::SetDetailsForItem(CVideoInfoTag& details, const std::map<std::string, std::string> &artwork) +{ + return SetDetailsForItem(details.m_iDbId, details.m_type, details, artwork); +} + +int CVideoDatabase::SetDetailsForItem(int id, const MediaType& mediaType, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork) +{ + if (mediaType == MediaTypeNone) + return -1; + + if (mediaType == MediaTypeMovie) + return SetDetailsForMovie(details, artwork, id); + else if (mediaType == MediaTypeVideoCollection) + return SetDetailsForMovieSet(details, artwork, id); + else if (mediaType == MediaTypeTvShow) + { + std::map<int, std::map<std::string, std::string> > seasonArtwork; + if (!UpdateDetailsForTvShow(id, details, artwork, seasonArtwork)) + return -1; + + return id; + } + else if (mediaType == MediaTypeSeason) + return SetDetailsForSeason(details, artwork, details.m_iIdShow, id); + else if (mediaType == MediaTypeEpisode) + return SetDetailsForEpisode(details, artwork, details.m_iIdShow, id); + else if (mediaType == MediaTypeMusicVideo) + return SetDetailsForMusicVideo(details, artwork, id); + + return -1; +} + +int CVideoDatabase::SetDetailsForMovie(CVideoInfoTag& details, + const std::map<std::string, std::string>& artwork, + int idMovie /* = -1 */) +{ + const auto filePath = details.GetPath(); + + try + { + BeginTransaction(); + + if (idMovie < 0) + idMovie = GetMovieId(filePath); + + if (idMovie > -1) + DeleteMovie(idMovie, true); // true to keep the table entry + else + { + // only add a new movie if we don't already have a valid idMovie + // (DeleteMovie is called with bKeepId == true so the movie won't + // be removed from the movie table) + idMovie = AddNewMovie(details); + if (idMovie < 0) + { + RollbackTransaction(); + return idMovie; + } + } + + // update dateadded if it's set + if (details.m_dateAdded.IsValid()) + UpdateFileDateAdded(details); + + AddCast(idMovie, "movie", details.m_cast); + AddLinksToItem(idMovie, MediaTypeMovie, "genre", details.m_genre); + AddLinksToItem(idMovie, MediaTypeMovie, "studio", details.m_studio); + AddLinksToItem(idMovie, MediaTypeMovie, "country", details.m_country); + AddLinksToItem(idMovie, MediaTypeMovie, "tag", details.m_tags); + AddActorLinksToItem(idMovie, MediaTypeMovie, "director", details.m_director); + AddActorLinksToItem(idMovie, MediaTypeMovie, "writer", details.m_writingCredits); + + // add ratingsu + details.m_iIdRating = AddRatings(idMovie, MediaTypeMovie, details.m_ratings, details.GetDefaultRating()); + + // add unique ids + details.m_iIdUniqueID = AddUniqueIDs(idMovie, MediaTypeMovie, details); + + // add set... + int idSet = -1; + if (!details.m_set.title.empty()) + { + idSet = AddSet(details.m_set.title, details.m_set.overview); + // add art if not available + if (!HasArtForItem(idSet, MediaTypeVideoCollection)) + { + for (const auto &it : artwork) + { + if (StringUtils::StartsWith(it.first, "set.")) + SetArtForItem(idSet, MediaTypeVideoCollection, it.first.substr(4), it.second); + } + } + } + + if (details.HasStreamDetails()) + SetStreamDetailsForFileId(details.m_streamDetails, GetAndFillFileId(details)); + + SetArtForItem(idMovie, MediaTypeMovie, artwork); + + if (!details.HasUniqueID() && details.HasYear()) + { // query DB for any movies matching online id and year + std::string strSQL = PrepareSQL("SELECT files.playCount, files.lastPlayed " + "FROM movie " + " INNER JOIN files " + " ON files.idFile=movie.idFile " + " JOIN uniqueid " + " ON movie.idMovie=uniqueid.media_id AND uniqueid.media_type='movie' AND uniqueid.value='%s'" + "WHERE movie.premiered LIKE '%i%%' AND movie.idMovie!=%i AND files.playCount > 0", + details.GetUniqueID().c_str(), details.GetYear(), idMovie); + m_pDS->query(strSQL); + + if (!m_pDS->eof()) + { + int playCount = m_pDS->fv("files.playCount").get_asInt(); + + CDateTime lastPlayed; + lastPlayed.SetFromDBDateTime(m_pDS->fv("files.lastPlayed").get_asString()); + + // update with playCount and lastPlayed + strSQL = + PrepareSQL("update files set playCount=%i,lastPlayed='%s' where idFile=%i", playCount, + lastPlayed.GetAsDBDateTime().c_str(), GetAndFillFileId(details)); + m_pDS->exec(strSQL); + } + + m_pDS->close(); + } + // update our movie table (we know it was added already above) + // and insert the new row + std::string sql = "UPDATE movie SET " + GetValueString(details, VIDEODB_ID_MIN, VIDEODB_ID_MAX, DbMovieOffsets); + if (idSet > 0) + sql += PrepareSQL(", idSet = %i", idSet); + else + sql += ", idSet = NULL"; + if (details.m_iUserRating > 0 && details.m_iUserRating < 11) + sql += PrepareSQL(", userrating = %i", details.m_iUserRating); + else + sql += ", userrating = NULL"; + if (details.HasPremiered()) + sql += PrepareSQL(", premiered = '%s'", details.GetPremiered().GetAsDBDate().c_str()); + else + sql += PrepareSQL(", premiered = '%i'", details.GetYear()); + sql += PrepareSQL(" where idMovie=%i", idMovie); + m_pDS->exec(sql); + CommitTransaction(); + + return idMovie; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } + RollbackTransaction(); + return -1; +} + +int CVideoDatabase::UpdateDetailsForMovie(int idMovie, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, const std::set<std::string> &updatedDetails) +{ + if (idMovie < 0) + return idMovie; + + try + { + CLog::Log(LOGINFO, "{}: Starting updates for movie {}", __FUNCTION__, idMovie); + + BeginTransaction(); + + // process the link table updates + if (updatedDetails.find("genre") != updatedDetails.end()) + UpdateLinksToItem(idMovie, MediaTypeMovie, "genre", details.m_genre); + if (updatedDetails.find("studio") != updatedDetails.end()) + UpdateLinksToItem(idMovie, MediaTypeMovie, "studio", details.m_studio); + if (updatedDetails.find("country") != updatedDetails.end()) + UpdateLinksToItem(idMovie, MediaTypeMovie, "country", details.m_country); + if (updatedDetails.find("tag") != updatedDetails.end()) + UpdateLinksToItem(idMovie, MediaTypeMovie, "tag", details.m_tags); + if (updatedDetails.find("director") != updatedDetails.end()) + UpdateActorLinksToItem(idMovie, MediaTypeMovie, "director", details.m_director); + if (updatedDetails.find("writer") != updatedDetails.end()) + UpdateActorLinksToItem(idMovie, MediaTypeMovie, "writer", details.m_writingCredits); + if (updatedDetails.find("art.altered") != updatedDetails.end()) + SetArtForItem(idMovie, MediaTypeMovie, artwork); + if (updatedDetails.find("ratings") != updatedDetails.end()) + details.m_iIdRating = UpdateRatings(idMovie, MediaTypeMovie, details.m_ratings, details.GetDefaultRating()); + if (updatedDetails.find("uniqueid") != updatedDetails.end()) + details.m_iIdUniqueID = UpdateUniqueIDs(idMovie, MediaTypeMovie, details); + if (updatedDetails.find("dateadded") != updatedDetails.end() && details.m_dateAdded.IsValid()) + UpdateFileDateAdded(details); + + // track if the set was updated + int idSet = 0; + if (updatedDetails.find("set") != updatedDetails.end()) + { // set + idSet = -1; + if (!details.m_set.title.empty()) + { + idSet = AddSet(details.m_set.title, details.m_set.overview); + } + } + + if (updatedDetails.find("showlink") != updatedDetails.end()) + { + // remove existing links + std::vector<int> tvShowIds; + GetLinksToTvShow(idMovie, tvShowIds); + for (const auto& idTVShow : tvShowIds) + LinkMovieToTvshow(idMovie, idTVShow, true); + + // setup links to shows if the linked shows are in the db + for (const auto& showLink : details.m_showLink) + { + CFileItemList items; + GetTvShowsByName(showLink, items); + if (!items.IsEmpty()) + LinkMovieToTvshow(idMovie, items[0]->GetVideoInfoTag()->m_iDbId, false); + else + CLog::Log(LOGWARNING, "{}: Failed to link movie {} to show {}", __FUNCTION__, + details.m_strTitle, showLink); + } + } + + // and update the movie table + std::string sql = "UPDATE movie SET " + GetValueString(details, VIDEODB_ID_MIN, VIDEODB_ID_MAX, DbMovieOffsets); + if (idSet > 0) + sql += PrepareSQL(", idSet = %i", idSet); + else if (idSet < 0) + sql += ", idSet = NULL"; + if (details.m_iUserRating > 0 && details.m_iUserRating < 11) + sql += PrepareSQL(", userrating = %i", details.m_iUserRating); + else + sql += ", userrating = NULL"; + if (details.HasPremiered()) + sql += PrepareSQL(", premiered = '%s'", details.GetPremiered().GetAsDBDate().c_str()); + else + sql += PrepareSQL(", premiered = '%i'", details.GetYear()); + sql += PrepareSQL(" where idMovie=%i", idMovie); + m_pDS->exec(sql); + + CommitTransaction(); + + CLog::Log(LOGINFO, "{}: Finished updates for movie {}", __FUNCTION__, idMovie); + + return idMovie; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idMovie); + } + RollbackTransaction(); + return -1; +} + +int CVideoDatabase::SetDetailsForMovieSet(const CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, int idSet /* = -1 */) +{ + if (details.m_strTitle.empty()) + return -1; + + try + { + BeginTransaction(); + if (idSet < 0) + { + idSet = AddSet(details.m_strTitle, details.m_strPlot); + if (idSet < 0) + { + RollbackTransaction(); + return -1; + } + } + + SetArtForItem(idSet, MediaTypeVideoCollection, artwork); + + // and insert the new row + std::string sql = PrepareSQL("UPDATE sets SET strSet='%s', strOverview='%s' WHERE idSet=%i", details.m_strTitle.c_str(), details.m_strPlot.c_str(), idSet); + m_pDS->exec(sql); + CommitTransaction(); + + return idSet; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSet); + } + RollbackTransaction(); + return -1; +} + +int CVideoDatabase::GetMatchingTvShow(const CVideoInfoTag &details) +{ + // first try matching on uniqueid, then on title + year + int id = -1; + if (!details.HasUniqueID()) + id = GetDbId(PrepareSQL("SELECT idShow FROM tvshow JOIN uniqueid ON uniqueid.media_id=tvshow.idShow AND uniqueid.media_type='tvshow' WHERE uniqueid.value='%s'", details.GetUniqueID().c_str())); + if (id < 0) + id = GetDbId(PrepareSQL("SELECT idShow FROM tvshow WHERE c%02d='%s' AND c%02d='%s'", VIDEODB_ID_TV_TITLE, details.m_strTitle.c_str(), VIDEODB_ID_TV_PREMIERED, details.GetPremiered().GetAsDBDate().c_str())); + return id; +} + +int CVideoDatabase::SetDetailsForTvShow(const std::vector<std::pair<std::string, std::string> > &paths, + CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, + const std::map<int, std::map<std::string, std::string> > &seasonArt, int idTvShow /*= -1 */) +{ + + /* + The steps are as follows. + 1. Check if the tvshow is found on any of the given paths. If found, we have the show id. + 2. Search for a matching show. If found, we have the show id. + 3. If we don't have the id, add a new show. + 4. Add the paths to the show. + 5. Add details for the show. + */ + + if (idTvShow < 0) + { + for (const auto &i : paths) + { + idTvShow = GetTvShowId(i.first); + if (idTvShow > -1) + break; + } + } + if (idTvShow < 0) + idTvShow = GetMatchingTvShow(details); + if (idTvShow < 0) + { + idTvShow = AddTvShow(); + if (idTvShow < 0) + return -1; + } + + // add any paths to the tvshow + for (const auto &i : paths) + AddPathToTvShow(idTvShow, i.first, i.second, details.m_dateAdded); + + UpdateDetailsForTvShow(idTvShow, details, artwork, seasonArt); + + return idTvShow; +} + +bool CVideoDatabase::UpdateDetailsForTvShow(int idTvShow, CVideoInfoTag &details, + const std::map<std::string, std::string> &artwork, const std::map<int, std::map<std::string, std::string>> &seasonArt) +{ + BeginTransaction(); + + DeleteDetailsForTvShow(idTvShow); + + AddCast(idTvShow, "tvshow", details.m_cast); + AddLinksToItem(idTvShow, MediaTypeTvShow, "genre", details.m_genre); + AddLinksToItem(idTvShow, MediaTypeTvShow, "studio", details.m_studio); + AddLinksToItem(idTvShow, MediaTypeTvShow, "tag", details.m_tags); + AddActorLinksToItem(idTvShow, MediaTypeTvShow, "director", details.m_director); + + // add ratings + details.m_iIdRating = AddRatings(idTvShow, MediaTypeTvShow, details.m_ratings, details.GetDefaultRating()); + + // add unique ids + details.m_iIdUniqueID = AddUniqueIDs(idTvShow, MediaTypeTvShow, details); + + // add "all seasons" - the rest are added in SetDetailsForEpisode + AddSeason(idTvShow, -1); + + // add any named seasons + for (const auto& namedSeason : details.m_namedSeasons) + { + // make sure the named season exists + int seasonId = AddSeason(idTvShow, namedSeason.first, namedSeason.second); + + // get any existing details for the named season + CVideoInfoTag season; + if (!GetSeasonInfo(seasonId, season, false) || season.m_strSortTitle == namedSeason.second) + continue; + + season.SetSortTitle(namedSeason.second); + SetDetailsForSeason(season, std::map<std::string, std::string>(), idTvShow, seasonId); + } + + SetArtForItem(idTvShow, MediaTypeTvShow, artwork); + for (const auto &i : seasonArt) + { + int idSeason = AddSeason(idTvShow, i.first); + if (idSeason > -1) + SetArtForItem(idSeason, MediaTypeSeason, i.second); + } + + // and insert the new row + std::string sql = "UPDATE tvshow SET " + GetValueString(details, VIDEODB_ID_TV_MIN, VIDEODB_ID_TV_MAX, DbTvShowOffsets); + if (details.m_iUserRating > 0 && details.m_iUserRating < 11) + sql += PrepareSQL(", userrating = %i", details.m_iUserRating); + else + sql += ", userrating = NULL"; + if (details.GetDuration() > 0) + sql += PrepareSQL(", duration = %i", details.GetDuration()); + else + sql += ", duration = NULL"; + sql += PrepareSQL(" WHERE idShow=%i", idTvShow); + if (ExecuteQuery(sql)) + { + CommitTransaction(); + return true; + } + RollbackTransaction(); + return false; +} + +int CVideoDatabase::SetDetailsForSeason(const CVideoInfoTag& details, const std::map<std::string, + std::string> &artwork, int idShow, int idSeason /* = -1 */) +{ + if (idShow < 0 || details.m_iSeason < -1) + return -1; + + try + { + BeginTransaction(); + if (idSeason < 0) + { + idSeason = AddSeason(idShow, details.m_iSeason); + if (idSeason < 0) + { + RollbackTransaction(); + return -1; + } + } + + SetArtForItem(idSeason, MediaTypeSeason, artwork); + + // and insert the new row + std::string sql = PrepareSQL("UPDATE seasons SET season=%i", details.m_iSeason); + if (!details.m_strSortTitle.empty()) + sql += PrepareSQL(", name='%s'", details.m_strSortTitle.c_str()); + if (details.m_iUserRating > 0 && details.m_iUserRating < 11) + sql += PrepareSQL(", userrating = %i", details.m_iUserRating); + else + sql += ", userrating = NULL"; + sql += PrepareSQL(" WHERE idSeason=%i", idSeason); + m_pDS->exec(sql.c_str()); + CommitTransaction(); + + return idSeason; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSeason); + } + RollbackTransaction(); + return -1; +} + +int CVideoDatabase::SetDetailsForEpisode(CVideoInfoTag& details, + const std::map<std::string, std::string>& artwork, + int idShow, + int idEpisode /* = -1 */) +{ + const auto filePath = details.GetPath(); + + try + { + BeginTransaction(); + if (idEpisode < 0) + idEpisode = GetEpisodeId(filePath); + + if (idEpisode > 0) + DeleteEpisode(idEpisode, true); // true to keep the table entry + else + { + // only add a new episode if we don't already have a valid idEpisode + // (DeleteEpisode is called with bKeepId == true so the episode won't + // be removed from the episode table) + idEpisode = AddNewEpisode(idShow, details); + if (idEpisode < 0) + { + RollbackTransaction(); + return -1; + } + } + + // update dateadded if it's set + if (details.m_dateAdded.IsValid()) + UpdateFileDateAdded(details); + + AddCast(idEpisode, "episode", details.m_cast); + AddActorLinksToItem(idEpisode, MediaTypeEpisode, "director", details.m_director); + AddActorLinksToItem(idEpisode, MediaTypeEpisode, "writer", details.m_writingCredits); + + // add ratings + details.m_iIdRating = AddRatings(idEpisode, MediaTypeEpisode, details.m_ratings, details.GetDefaultRating()); + + // add unique ids + details.m_iIdUniqueID = AddUniqueIDs(idEpisode, MediaTypeEpisode, details); + + if (details.HasStreamDetails()) + SetStreamDetailsForFileId(details.m_streamDetails, GetAndFillFileId(details)); + + // ensure we have this season already added + int idSeason = AddSeason(idShow, details.m_iSeason); + + SetArtForItem(idEpisode, MediaTypeEpisode, artwork); + + if (details.m_iEpisode != -1 && details.m_iSeason != -1) + { // query DB for any episodes matching idShow, Season and Episode + std::string strSQL = PrepareSQL("SELECT files.playCount, files.lastPlayed " + "FROM episode INNER JOIN files ON files.idFile=episode.idFile " + "WHERE episode.c%02d=%i AND episode.c%02d=%i AND episode.idShow=%i " + "AND episode.idEpisode!=%i AND files.playCount > 0", + VIDEODB_ID_EPISODE_SEASON, details.m_iSeason, VIDEODB_ID_EPISODE_EPISODE, + details.m_iEpisode, idShow, idEpisode); + m_pDS->query(strSQL); + + if (!m_pDS->eof()) + { + int playCount = m_pDS->fv("files.playCount").get_asInt(); + + CDateTime lastPlayed; + lastPlayed.SetFromDBDateTime(m_pDS->fv("files.lastPlayed").get_asString()); + + // update with playCount and lastPlayed + strSQL = + PrepareSQL("update files set playCount=%i,lastPlayed='%s' where idFile=%i", playCount, + lastPlayed.GetAsDBDateTime().c_str(), GetAndFillFileId(details)); + m_pDS->exec(strSQL); + } + + m_pDS->close(); + } + // and insert the new row + std::string sql = "UPDATE episode SET " + GetValueString(details, VIDEODB_ID_EPISODE_MIN, VIDEODB_ID_EPISODE_MAX, DbEpisodeOffsets); + if (details.m_iUserRating > 0 && details.m_iUserRating < 11) + sql += PrepareSQL(", userrating = %i", details.m_iUserRating); + else + sql += ", userrating = NULL"; + sql += PrepareSQL(", idSeason = %i", idSeason); + sql += PrepareSQL(" where idEpisode=%i", idEpisode); + m_pDS->exec(sql); + CommitTransaction(); + + return idEpisode; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } + RollbackTransaction(); + return -1; +} + +int CVideoDatabase::GetSeasonId(int showID, int season) +{ + std::string sql = PrepareSQL("idShow=%i AND season=%i", showID, season); + std::string id = GetSingleValue("seasons", "idSeason", sql); + if (id.empty()) + return -1; + return strtol(id.c_str(), NULL, 10); +} + +int CVideoDatabase::AddSeason(int showID, int season, const std::string& name /* = "" */) +{ + int seasonId = GetSeasonId(showID, season); + if (seasonId < 0) + { + if (ExecuteQuery(PrepareSQL("INSERT INTO seasons (idShow, season, name) VALUES(%i, %i, '%s')", showID, season, name.c_str()))) + seasonId = (int)m_pDS->lastinsertid(); + } + return seasonId; +} + +int CVideoDatabase::SetDetailsForMusicVideo(CVideoInfoTag& details, + const std::map<std::string, std::string>& artwork, + int idMVideo /* = -1 */) +{ + const auto filePath = details.GetPath(); + + try + { + BeginTransaction(); + + if (idMVideo < 0) + idMVideo = GetMusicVideoId(filePath); + + if (idMVideo > -1) + DeleteMusicVideo(idMVideo, true); // Keep id + else + { + // only add a new musicvideo if we don't already have a valid idMVideo + // (DeleteMusicVideo is called with bKeepId == true so the musicvideo won't + // be removed from the musicvideo table) + idMVideo = AddNewMusicVideo(details); + if (idMVideo < 0) + { + RollbackTransaction(); + return -1; + } + } + + // update dateadded if it's set + if (details.m_dateAdded.IsValid()) + UpdateFileDateAdded(details); + + AddCast(idMVideo, MediaTypeMusicVideo, details.m_cast); + AddActorLinksToItem(idMVideo, MediaTypeMusicVideo, "actor", details.m_artist); + AddActorLinksToItem(idMVideo, MediaTypeMusicVideo, "director", details.m_director); + AddLinksToItem(idMVideo, MediaTypeMusicVideo, "genre", details.m_genre); + AddLinksToItem(idMVideo, MediaTypeMusicVideo, "studio", details.m_studio); + AddLinksToItem(idMVideo, MediaTypeMusicVideo, "tag", details.m_tags); + + // add unique ids + details.m_iIdUniqueID = UpdateUniqueIDs(idMVideo, MediaTypeMusicVideo, details); + + if (details.HasStreamDetails()) + SetStreamDetailsForFileId(details.m_streamDetails, GetAndFillFileId(details)); + + SetArtForItem(idMVideo, MediaTypeMusicVideo, artwork); + + // update our movie table (we know it was added already above) + // and insert the new row + std::string sql = "UPDATE musicvideo SET " + GetValueString(details, VIDEODB_ID_MUSICVIDEO_MIN, VIDEODB_ID_MUSICVIDEO_MAX, DbMusicVideoOffsets); + if (details.m_iUserRating > 0 && details.m_iUserRating < 11) + sql += PrepareSQL(", userrating = %i", details.m_iUserRating); + else + sql += ", userrating = NULL"; + if (details.HasPremiered()) + sql += PrepareSQL(", premiered = '%s'", details.GetPremiered().GetAsDBDate().c_str()); + else + sql += PrepareSQL(", premiered = '%i'", details.GetYear()); + sql += PrepareSQL(" where idMVideo=%i", idMVideo); + m_pDS->exec(sql); + CommitTransaction(); + + return idMVideo; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } + RollbackTransaction(); + return -1; +} + +void CVideoDatabase::SetStreamDetailsForFile(const CStreamDetails& details, const std::string &strFileNameAndPath) +{ + // AddFile checks to make sure the file isn't already in the DB first + int idFile = AddFile(strFileNameAndPath); + if (idFile < 0) + return; + SetStreamDetailsForFileId(details, idFile); +} + +void CVideoDatabase::SetStreamDetailsForFileId(const CStreamDetails& details, int idFile) +{ + if (idFile < 0) + return; + + try + { + m_pDS->exec(PrepareSQL("DELETE FROM streamdetails WHERE idFile = %i", idFile)); + + for (int i=1; i<=details.GetVideoStreamCount(); i++) + { + m_pDS->exec(PrepareSQL("INSERT INTO streamdetails " + "(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth, " + "iVideoHeight, iVideoDuration, strStereoMode, strVideoLanguage, " + "strHdrType)" + "VALUES (%i,%i,'%s',%f,%i,%i,%i,'%s','%s','%s')", + idFile, (int)CStreamDetail::VIDEO, details.GetVideoCodec(i).c_str(), + static_cast<double>(details.GetVideoAspect(i)), + details.GetVideoWidth(i), details.GetVideoHeight(i), + details.GetVideoDuration(i), details.GetStereoMode(i).c_str(), + details.GetVideoLanguage(i).c_str(), + details.GetVideoHdrType(i).c_str())); + } + for (int i=1; i<=details.GetAudioStreamCount(); i++) + { + m_pDS->exec(PrepareSQL("INSERT INTO streamdetails " + "(idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) " + "VALUES (%i,%i,'%s',%i,'%s')", + idFile, (int)CStreamDetail::AUDIO, + details.GetAudioCodec(i).c_str(), details.GetAudioChannels(i), + details.GetAudioLanguage(i).c_str())); + } + for (int i=1; i<=details.GetSubtitleStreamCount(); i++) + { + m_pDS->exec(PrepareSQL("INSERT INTO streamdetails " + "(idFile, iStreamType, strSubtitleLanguage) " + "VALUES (%i,%i,'%s')", + idFile, (int)CStreamDetail::SUBTITLE, + details.GetSubtitleLanguage(i).c_str())); + } + + // update the runtime information, if empty + if (details.GetVideoDuration()) + { + std::vector<std::pair<std::string, int> > tables; + tables.emplace_back("movie", VIDEODB_ID_RUNTIME); + tables.emplace_back("episode", VIDEODB_ID_EPISODE_RUNTIME); + tables.emplace_back("musicvideo", VIDEODB_ID_MUSICVIDEO_RUNTIME); + for (const auto &i : tables) + { + std::string sql = PrepareSQL("update %s set c%02d=%d where idFile=%d and c%02d=''", + i.first.c_str(), i.second, details.GetVideoDuration(), idFile, i.second); + m_pDS->exec(sql); + } + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idFile); + } +} + +//******************************************************************************************************************************** +void CVideoDatabase::GetFilePathById(int idMovie, std::string& filePath, VideoDbContentType iType) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (idMovie < 0) return ; + + std::string strSQL; + if (iType == VideoDbContentType::MOVIES) + strSQL=PrepareSQL("SELECT path.strPath, files.strFileName FROM path INNER JOIN files ON path.idPath=files.idPath INNER JOIN movie ON files.idFile=movie.idFile WHERE movie.idMovie=%i ORDER BY strFilename", idMovie ); + if (iType == VideoDbContentType::EPISODES) + strSQL=PrepareSQL("SELECT path.strPath, files.strFileName FROM path INNER JOIN files ON path.idPath=files.idPath INNER JOIN episode ON files.idFile=episode.idFile WHERE episode.idEpisode=%i ORDER BY strFilename", idMovie ); + if (iType == VideoDbContentType::TVSHOWS) + strSQL=PrepareSQL("SELECT path.strPath FROM path INNER JOIN tvshowlinkpath ON path.idPath=tvshowlinkpath.idPath WHERE tvshowlinkpath.idShow=%i", idMovie ); + if (iType == VideoDbContentType::MUSICVIDEOS) + strSQL=PrepareSQL("SELECT path.strPath, files.strFileName FROM path INNER JOIN files ON path.idPath=files.idPath INNER JOIN musicvideo ON files.idFile=musicvideo.idFile WHERE musicvideo.idMVideo=%i ORDER BY strFilename", idMovie ); + + m_pDS->query( strSQL ); + if (!m_pDS->eof()) + { + if (iType != VideoDbContentType::TVSHOWS) + { + std::string fileName = m_pDS->fv("files.strFilename").get_asString(); + ConstructPath(filePath,m_pDS->fv("path.strPath").get_asString(),fileName); + } + else + filePath = m_pDS->fv("path.strPath").get_asString(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } +} + +//******************************************************************************************************************************** +void CVideoDatabase::GetBookMarksForFile(const std::string& strFilenameAndPath, VECBOOKMARKS& bookmarks, CBookmark::EType type /*= CBookmark::STANDARD*/, bool bAppend, long partNumber) +{ + try + { + if (URIUtils::IsStack(strFilenameAndPath) && CFileItem(CStackDirectory::GetFirstStackedFile(strFilenameAndPath),false).IsDiscImage()) + { + CStackDirectory dir; + CFileItemList fileList; + const CURL pathToUrl(strFilenameAndPath); + dir.GetDirectory(pathToUrl, fileList); + if (!bAppend) + bookmarks.clear(); + for (int i = fileList.Size() - 1; i >= 0; i--) // put the bookmarks of the highest part first in the list + GetBookMarksForFile(fileList[i]->GetPath(), bookmarks, type, true, (i+1)); + } + else + { + int idFile = GetFileId(strFilenameAndPath); + if (idFile < 0) return ; + if (!bAppend) + bookmarks.erase(bookmarks.begin(), bookmarks.end()); + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + std::string strSQL=PrepareSQL("select * from bookmark where idFile=%i and type=%i order by timeInSeconds", idFile, (int)type); + m_pDS->query( strSQL ); + while (!m_pDS->eof()) + { + CBookmark bookmark; + bookmark.timeInSeconds = m_pDS->fv("timeInSeconds").get_asDouble(); + bookmark.partNumber = partNumber; + bookmark.totalTimeInSeconds = m_pDS->fv("totalTimeInSeconds").get_asDouble(); + bookmark.thumbNailImage = m_pDS->fv("thumbNailImage").get_asString(); + bookmark.playerState = m_pDS->fv("playerState").get_asString(); + bookmark.player = m_pDS->fv("player").get_asString(); + bookmark.type = type; + if (type == CBookmark::EPISODE) + { + std::string strSQL2=PrepareSQL("select c%02d, c%02d from episode where c%02d=%i order by c%02d, c%02d", VIDEODB_ID_EPISODE_EPISODE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_EPISODE_BOOKMARK, m_pDS->fv("idBookmark").get_asInt(), VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTEPISODE); + m_pDS2->query(strSQL2); + bookmark.episodeNumber = m_pDS2->fv(0).get_asInt(); + bookmark.seasonNumber = m_pDS2->fv(1).get_asInt(); + m_pDS2->close(); + } + bookmarks.push_back(bookmark); + m_pDS->next(); + } + //sort(bookmarks.begin(), bookmarks.end(), SortBookmarks); + m_pDS->close(); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } +} + +bool CVideoDatabase::GetResumeBookMark(const std::string& strFilenameAndPath, CBookmark &bookmark) +{ + VECBOOKMARKS bookmarks; + GetBookMarksForFile(strFilenameAndPath, bookmarks, CBookmark::RESUME); + if (!bookmarks.empty()) + { + bookmark = bookmarks[0]; + return true; + } + return false; +} + +void CVideoDatabase::DeleteResumeBookMark(const CFileItem& item) +{ + if (!m_pDB || !m_pDS) + return; + + int fileID = item.GetVideoInfoTag()->m_iFileId; + if (fileID < 0) + { + fileID = GetFileId(item.GetPath()); + if (fileID < 0) + return; + } + + try + { + std::string sql = PrepareSQL("delete from bookmark where idFile=%i and type=%i", fileID, CBookmark::RESUME); + m_pDS->exec(sql); + + VideoDbContentType iType = static_cast<VideoDbContentType>(item.GetVideoContentType()); + std::string content; + switch (iType) + { + case VideoDbContentType::MOVIES: + content = MediaTypeMovie; + break; + case VideoDbContentType::EPISODES: + content = MediaTypeEpisode; + break; + case VideoDbContentType::TVSHOWS: + content = MediaTypeTvShow; + break; + case VideoDbContentType::MUSICVIDEOS: + content = MediaTypeMusicVideo; + break; + default: + break; + } + + if (!content.empty()) + { + AnnounceUpdate(content, item.GetVideoInfoTag()->m_iDbId); + } + + } + catch(...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, + item.GetVideoInfoTag()->m_strFileNameAndPath); + } +} + +void CVideoDatabase::GetEpisodesByFile(const std::string& strFilenameAndPath, std::vector<CVideoInfoTag>& episodes) +{ + try + { + std::string strSQL = PrepareSQL("select * from episode_view where idFile=%i order by c%02d, c%02d asc", GetFileId(strFilenameAndPath), VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTEPISODE); + m_pDS->query(strSQL); + while (!m_pDS->eof()) + { + episodes.emplace_back(GetDetailsForEpisode(m_pDS)); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } +} + +//******************************************************************************************************************************** +void CVideoDatabase::AddBookMarkToFile(const std::string& strFilenameAndPath, const CBookmark &bookmark, CBookmark::EType type /*= CBookmark::STANDARD*/) +{ + try + { + int idFile = AddFile(strFilenameAndPath); + if (idFile < 0) + return; + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + std::string strSQL; + int idBookmark=-1; + if (type == CBookmark::RESUME) // get the same resume mark bookmark each time type + { + strSQL=PrepareSQL("select idBookmark from bookmark where idFile=%i and type=1", idFile); + } + else if (type == CBookmark::STANDARD) // get the same bookmark again, and update. not sure here as a dvd can have same time in multiple places, state will differ thou + { + /* get a bookmark within the same time as previous */ + double mintime = bookmark.timeInSeconds - 0.5; + double maxtime = bookmark.timeInSeconds + 0.5; + strSQL=PrepareSQL("select idBookmark from bookmark where idFile=%i and type=%i and (timeInSeconds between %f and %f) and playerState='%s'", idFile, (int)type, mintime, maxtime, bookmark.playerState.c_str()); + } + + if (type != CBookmark::EPISODE) + { + // get current id + m_pDS->query( strSQL ); + if (m_pDS->num_rows() != 0) + idBookmark = m_pDS->get_field_value("idBookmark").get_asInt(); + m_pDS->close(); + } + // update or insert depending if it existed before + if (idBookmark >= 0 ) + strSQL=PrepareSQL("update bookmark set timeInSeconds = %f, totalTimeInSeconds = %f, thumbNailImage = '%s', player = '%s', playerState = '%s' where idBookmark = %i", bookmark.timeInSeconds, bookmark.totalTimeInSeconds, bookmark.thumbNailImage.c_str(), bookmark.player.c_str(), bookmark.playerState.c_str(), idBookmark); + else + strSQL=PrepareSQL("insert into bookmark (idBookmark, idFile, timeInSeconds, totalTimeInSeconds, thumbNailImage, player, playerState, type) values(NULL,%i,%f,%f,'%s','%s','%s', %i)", idFile, bookmark.timeInSeconds, bookmark.totalTimeInSeconds, bookmark.thumbNailImage.c_str(), bookmark.player.c_str(), bookmark.playerState.c_str(), (int)type); + + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } +} + +void CVideoDatabase::ClearBookMarkOfFile(const std::string& strFilenameAndPath, CBookmark& bookmark, CBookmark::EType type /*= CBookmark::STANDARD*/) +{ + try + { + int idFile = GetFileId(strFilenameAndPath); + if (idFile < 0) return ; + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + /* a little bit ugly, we clear first bookmark that is within one second of given */ + /* should be no problem since we never add bookmarks that are closer than that */ + double mintime = bookmark.timeInSeconds - 0.5; + double maxtime = bookmark.timeInSeconds + 0.5; + std::string strSQL = PrepareSQL("select idBookmark from bookmark where idFile=%i and type=%i and playerState like '%s' and player like '%s' and (timeInSeconds between %f and %f)", idFile, type, bookmark.playerState.c_str(), bookmark.player.c_str(), mintime, maxtime); + + m_pDS->query( strSQL ); + if (m_pDS->num_rows() != 0) + { + int idBookmark = m_pDS->get_field_value("idBookmark").get_asInt(); + strSQL=PrepareSQL("delete from bookmark where idBookmark=%i",idBookmark); + m_pDS->exec(strSQL); + if (type == CBookmark::EPISODE) + { + strSQL=PrepareSQL("update episode set c%02d=-1 where idFile=%i and c%02d=%i", VIDEODB_ID_EPISODE_BOOKMARK, idFile, VIDEODB_ID_EPISODE_BOOKMARK, idBookmark); + m_pDS->exec(strSQL); + } + } + + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath); + } +} + +//******************************************************************************************************************************** +void CVideoDatabase::ClearBookMarksOfFile(const std::string& strFilenameAndPath, CBookmark::EType type /*= CBookmark::STANDARD*/) +{ + int idFile = GetFileId(strFilenameAndPath); + if (idFile >= 0) + return ClearBookMarksOfFile(idFile, type); +} + +void CVideoDatabase::ClearBookMarksOfFile(int idFile, CBookmark::EType type /*= CBookmark::STANDARD*/) +{ + if (idFile < 0) + return; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + std::string strSQL=PrepareSQL("delete from bookmark where idFile=%i and type=%i", idFile, (int)type); + m_pDS->exec(strSQL); + if (type == CBookmark::EPISODE) + { + strSQL=PrepareSQL("update episode set c%02d=-1 where idFile=%i", VIDEODB_ID_EPISODE_BOOKMARK, idFile); + m_pDS->exec(strSQL); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idFile); + } +} + + +bool CVideoDatabase::GetBookMarkForEpisode(const CVideoInfoTag& tag, CBookmark& bookmark) +{ + try + { + std::string strSQL = PrepareSQL("select bookmark.* from bookmark join episode on episode.c%02d=bookmark.idBookmark where episode.idEpisode=%i", VIDEODB_ID_EPISODE_BOOKMARK, tag.m_iDbId); + m_pDS2->query( strSQL ); + if (!m_pDS2->eof()) + { + bookmark.timeInSeconds = m_pDS2->fv("timeInSeconds").get_asDouble(); + bookmark.totalTimeInSeconds = m_pDS2->fv("totalTimeInSeconds").get_asDouble(); + bookmark.thumbNailImage = m_pDS2->fv("thumbNailImage").get_asString(); + bookmark.playerState = m_pDS2->fv("playerState").get_asString(); + bookmark.player = m_pDS2->fv("player").get_asString(); + bookmark.type = (CBookmark::EType)m_pDS2->fv("type").get_asInt(); + } + else + { + m_pDS2->close(); + return false; + } + m_pDS2->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + return false; + } + return true; +} + +void CVideoDatabase::AddBookMarkForEpisode(const CVideoInfoTag& tag, const CBookmark& bookmark) +{ + try + { + int idFile = GetFileId(tag.m_strFileNameAndPath); + // delete the current episode for the selected episode number + std::string strSQL = PrepareSQL("delete from bookmark where idBookmark in (select c%02d from episode where c%02d=%i and c%02d=%i and idFile=%i)", VIDEODB_ID_EPISODE_BOOKMARK, VIDEODB_ID_EPISODE_SEASON, tag.m_iSeason, VIDEODB_ID_EPISODE_EPISODE, tag.m_iEpisode, idFile); + m_pDS->exec(strSQL); + + AddBookMarkToFile(tag.m_strFileNameAndPath, bookmark, CBookmark::EPISODE); + int idBookmark = (int)m_pDS->lastinsertid(); + strSQL = PrepareSQL("update episode set c%02d=%i where c%02d=%i and c%02d=%i and idFile=%i", VIDEODB_ID_EPISODE_BOOKMARK, idBookmark, VIDEODB_ID_EPISODE_SEASON, tag.m_iSeason, VIDEODB_ID_EPISODE_EPISODE, tag.m_iEpisode, idFile); + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, tag.m_iDbId); + } +} + +void CVideoDatabase::DeleteBookMarkForEpisode(const CVideoInfoTag& tag) +{ + try + { + std::string strSQL = PrepareSQL("delete from bookmark where idBookmark in (select c%02d from episode where idEpisode=%i)", VIDEODB_ID_EPISODE_BOOKMARK, tag.m_iDbId); + m_pDS->exec(strSQL); + strSQL = PrepareSQL("update episode set c%02d=-1 where idEpisode=%i", VIDEODB_ID_EPISODE_BOOKMARK, tag.m_iDbId); + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, tag.m_iDbId); + } +} + +//******************************************************************************************************************************** +void CVideoDatabase::DeleteMovie(int idMovie, bool bKeepId /* = false */) +{ + if (idMovie < 0) + return; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + BeginTransaction(); + + int idFile = GetDbId(PrepareSQL("SELECT idFile FROM movie WHERE idMovie=%i", idMovie)); + DeleteStreamDetails(idFile); + + // keep the movie table entry, linking to tv shows, and bookmarks + // so we can update the data in place + // the ancillary tables are still purged + if (!bKeepId) + { + std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path JOIN files ON files.idPath=path.idPath WHERE files.idFile=%i", idFile)); + if (!path.empty()) + InvalidatePathHash(path); + + std::string strSQL = PrepareSQL("delete from movie where idMovie=%i", idMovie); + m_pDS->exec(strSQL); + } + + //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore + if (!bKeepId) + AnnounceRemove(MediaTypeMovie, idMovie); + + CommitTransaction(); + + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + RollbackTransaction(); + } +} + +void CVideoDatabase::DeleteTvShow(const std::string& strPath) +{ + int idTvShow = GetTvShowId(strPath); + if (idTvShow >= 0) + DeleteTvShow(idTvShow); +} + +void CVideoDatabase::DeleteTvShow(int idTvShow, bool bKeepId /* = false */) +{ + if (idTvShow < 0) + return; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + BeginTransaction(); + + std::set<int> paths; + GetPathsForTvShow(idTvShow, paths); + + std::string strSQL=PrepareSQL("SELECT episode.idEpisode FROM episode WHERE episode.idShow=%i",idTvShow); + m_pDS2->query(strSQL); + while (!m_pDS2->eof()) + { + DeleteEpisode(m_pDS2->fv(0).get_asInt(), bKeepId); + m_pDS2->next(); + } + + DeleteDetailsForTvShow(idTvShow); + + strSQL=PrepareSQL("delete from seasons where idShow=%i", idTvShow); + m_pDS->exec(strSQL); + + // keep tvshow table and movielink table so we can update data in place + if (!bKeepId) + { + strSQL=PrepareSQL("delete from tvshow where idShow=%i", idTvShow); + m_pDS->exec(strSQL); + + for (const auto &i : paths) + { + std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path WHERE idPath=%i", i)); + if (!path.empty()) + InvalidatePathHash(path); + } + } + + //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore + if (!bKeepId) + AnnounceRemove(MediaTypeTvShow, idTvShow); + + CommitTransaction(); + + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idTvShow); + RollbackTransaction(); + } +} + +void CVideoDatabase::DeleteSeason(int idSeason, bool bKeepId /* = false */) +{ + if (idSeason < 0) + return; + + try + { + if (m_pDB == nullptr || m_pDS == nullptr || m_pDS2 == nullptr) + return; + + BeginTransaction(); + + std::string strSQL = PrepareSQL("SELECT episode.idEpisode FROM episode " + "JOIN seasons ON seasons.idSeason = %d AND episode.idShow = seasons.idShow AND episode.c%02d = seasons.season ", + idSeason, VIDEODB_ID_EPISODE_SEASON); + m_pDS2->query(strSQL); + while (!m_pDS2->eof()) + { + DeleteEpisode(m_pDS2->fv(0).get_asInt(), bKeepId); + m_pDS2->next(); + } + + ExecuteQuery(PrepareSQL("DELETE FROM seasons WHERE idSeason = %i", idSeason)); + + CommitTransaction(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSeason); + RollbackTransaction(); + } +} + +void CVideoDatabase::DeleteEpisode(int idEpisode, bool bKeepId /* = false */) +{ + if (idEpisode < 0) + return; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore + if (!bKeepId) + AnnounceRemove(MediaTypeEpisode, idEpisode); + + int idFile = GetDbId(PrepareSQL("SELECT idFile FROM episode WHERE idEpisode=%i", idEpisode)); + DeleteStreamDetails(idFile); + + // keep episode table entry and bookmarks so we can update the data in place + // the ancillary tables are still purged + if (!bKeepId) + { + std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path JOIN files ON files.idPath=path.idPath WHERE files.idFile=%i", idFile)); + if (!path.empty()) + InvalidatePathHash(path); + + std::string strSQL = PrepareSQL("delete from episode where idEpisode=%i", idEpisode); + m_pDS->exec(strSQL); + } + + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idEpisode); + } +} + +void CVideoDatabase::DeleteMusicVideo(int idMVideo, bool bKeepId /* = false */) +{ + if (idMVideo < 0) + return; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + BeginTransaction(); + + int idFile = GetDbId(PrepareSQL("SELECT idFile FROM musicvideo WHERE idMVideo=%i", idMVideo)); + DeleteStreamDetails(idFile); + + // keep the music video table entry and bookmarks so we can update data in place + // the ancillary tables are still purged + if (!bKeepId) + { + std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path JOIN files ON files.idPath=path.idPath WHERE files.idFile=%i", idFile)); + if (!path.empty()) + InvalidatePathHash(path); + + std::string strSQL = PrepareSQL("delete from musicvideo where idMVideo=%i", idMVideo); + m_pDS->exec(strSQL); + } + + //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore + if (!bKeepId) + AnnounceRemove(MediaTypeMusicVideo, idMVideo); + + CommitTransaction(); + + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + RollbackTransaction(); + } +} + +int CVideoDatabase::GetDbId(const std::string &query) +{ + std::string result = GetSingleValue(query); + if (!result.empty()) + { + int idDb = strtol(result.c_str(), NULL, 10); + if (idDb > 0) + return idDb; + } + return -1; +} + +void CVideoDatabase::DeleteStreamDetails(int idFile) +{ + m_pDS->exec(PrepareSQL("DELETE FROM streamdetails WHERE idFile = %i", idFile)); +} + +void CVideoDatabase::DeleteSet(int idSet) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + std::string strSQL; + strSQL=PrepareSQL("delete from sets where idSet = %i", idSet); + m_pDS->exec(strSQL); + strSQL=PrepareSQL("update movie set idSet = null where idSet = %i", idSet); + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSet); + } +} + +void CVideoDatabase::ClearMovieSet(int idMovie) +{ + SetMovieSet(idMovie, -1); +} + +void CVideoDatabase::SetMovieSet(int idMovie, int idSet) +{ + if (idSet >= 0) + ExecuteQuery(PrepareSQL("update movie set idSet = %i where idMovie = %i", idSet, idMovie)); + else + ExecuteQuery(PrepareSQL("update movie set idSet = null where idMovie = %i", idMovie)); +} + +void CVideoDatabase::DeleteTag(int idTag, VideoDbContentType mediaType) +{ + try + { + if (m_pDB == nullptr || m_pDS == nullptr) + return; + + std::string type; + if (mediaType == VideoDbContentType::MOVIES) + type = MediaTypeMovie; + else if (mediaType == VideoDbContentType::TVSHOWS) + type = MediaTypeTvShow; + else if (mediaType == VideoDbContentType::MUSICVIDEOS) + type = MediaTypeMusicVideo; + else + return; + + std::string strSQL = PrepareSQL("DELETE FROM tag_link WHERE tag_id = %i AND media_type = '%s'", idTag, type.c_str()); + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idTag); + } +} + +void CVideoDatabase::GetDetailsFromDB(std::unique_ptr<Dataset> &pDS, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset) +{ + GetDetailsFromDB(pDS->get_sql_record(), min, max, offsets, details, idxOffset); +} + +void CVideoDatabase::GetDetailsFromDB(const dbiplus::sql_record* const record, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset) +{ + for (int i = min + 1; i < max; i++) + { + switch (offsets[i].type) + { + case VIDEODB_TYPE_STRING: + *(std::string*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asString(); + break; + case VIDEODB_TYPE_INT: + case VIDEODB_TYPE_COUNT: + *(int*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asInt(); + break; + case VIDEODB_TYPE_BOOL: + *(bool*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asBool(); + break; + case VIDEODB_TYPE_FLOAT: + *(float*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asFloat(); + break; + case VIDEODB_TYPE_STRINGARRAY: + { + std::string value = record->at(i+idxOffset).get_asString(); + if (!value.empty()) + *(std::vector<std::string>*)(((char*)&details)+offsets[i].offset) = StringUtils::Split(value, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + break; + } + case VIDEODB_TYPE_DATE: + ((CDateTime*)(((char*)&details)+offsets[i].offset))->SetFromDBDate(record->at(i+idxOffset).get_asString()); + break; + case VIDEODB_TYPE_DATETIME: + ((CDateTime*)(((char*)&details)+offsets[i].offset))->SetFromDBDateTime(record->at(i+idxOffset).get_asString()); + break; + case VIDEODB_TYPE_UNUSED: // Skip the unused field to avoid populating unused data + continue; + } + } +} + +bool CVideoDatabase::GetDetailsByTypeAndId(CFileItem& item, VideoDbContentType type, int id) +{ + CVideoInfoTag details; + details.Reset(); + + switch (type) + { + case VideoDbContentType::MOVIES: + GetMovieInfo("", details, id); + break; + case VideoDbContentType::TVSHOWS: + GetTvShowInfo("", details, id, &item); + break; + case VideoDbContentType::EPISODES: + GetEpisodeInfo("", details, id); + break; + case VideoDbContentType::MUSICVIDEOS: + GetMusicVideoInfo("", details, id); + break; + default: + return false; + } + + item.SetFromVideoInfoTag(details); + return true; +} + +CVideoInfoTag CVideoDatabase::GetDetailsByTypeAndId(VideoDbContentType type, int id) +{ + CFileItem item; + if (GetDetailsByTypeAndId(item, type, id)) + return CVideoInfoTag(*item.GetVideoInfoTag()); + + return {}; +} + +bool CVideoDatabase::GetStreamDetails(CFileItem& item) +{ + // Note that this function (possibly) creates VideoInfoTags for items that don't have one yet! + int fileId = -1; + + if (item.HasVideoInfoTag()) + fileId = item.GetVideoInfoTag()->m_iFileId; + + if (fileId < 0) + fileId = GetFileId(item); + + if (fileId < 0) + return false; + + // Have a file id, get stream details if available (creates tag either way) + item.GetVideoInfoTag()->m_iFileId = fileId; + return GetStreamDetails(*item.GetVideoInfoTag()); +} + +bool CVideoDatabase::GetStreamDetails(CVideoInfoTag& tag) const +{ + if (tag.m_iFileId < 0) + return false; + + bool retVal = false; + + CStreamDetails& details = tag.m_streamDetails; + details.Reset(); + + std::unique_ptr<Dataset> pDS(m_pDB->CreateDataset()); + try + { + std::string strSQL = PrepareSQL("SELECT * FROM streamdetails WHERE idFile = %i", tag.m_iFileId); + pDS->query(strSQL); + + while (!pDS->eof()) + { + CStreamDetail::StreamType e = (CStreamDetail::StreamType)pDS->fv(1).get_asInt(); + switch (e) + { + case CStreamDetail::VIDEO: + { + CStreamDetailVideo *p = new CStreamDetailVideo(); + p->m_strCodec = pDS->fv(2).get_asString(); + p->m_fAspect = pDS->fv(3).get_asFloat(); + p->m_iWidth = pDS->fv(4).get_asInt(); + p->m_iHeight = pDS->fv(5).get_asInt(); + p->m_iDuration = pDS->fv(10).get_asInt(); + p->m_strStereoMode = pDS->fv(11).get_asString(); + p->m_strLanguage = pDS->fv(12).get_asString(); + p->m_strHdrType = pDS->fv(13).get_asString(); + details.AddStream(p); + retVal = true; + break; + } + case CStreamDetail::AUDIO: + { + CStreamDetailAudio *p = new CStreamDetailAudio(); + p->m_strCodec = pDS->fv(6).get_asString(); + if (pDS->fv(7).get_isNull()) + p->m_iChannels = -1; + else + p->m_iChannels = pDS->fv(7).get_asInt(); + p->m_strLanguage = pDS->fv(8).get_asString(); + details.AddStream(p); + retVal = true; + break; + } + case CStreamDetail::SUBTITLE: + { + CStreamDetailSubtitle *p = new CStreamDetailSubtitle(); + p->m_strLanguage = pDS->fv(9).get_asString(); + details.AddStream(p); + retVal = true; + break; + } + } + + pDS->next(); + } + + pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, tag.m_iFileId); + } + details.DetermineBestStreams(); + + if (details.GetVideoDuration() > 0) + tag.SetDuration(details.GetVideoDuration()); + + return retVal; +} + +bool CVideoDatabase::GetResumePoint(CVideoInfoTag& tag) +{ + if (tag.m_iFileId < 0) + return false; + + bool match = false; + + try + { + if (URIUtils::IsStack(tag.m_strFileNameAndPath) && CFileItem(CStackDirectory::GetFirstStackedFile(tag.m_strFileNameAndPath),false).IsDiscImage()) + { + CStackDirectory dir; + CFileItemList fileList; + const CURL pathToUrl(tag.m_strFileNameAndPath); + dir.GetDirectory(pathToUrl, fileList); + tag.SetResumePoint(CBookmark()); + for (int i = fileList.Size() - 1; i >= 0; i--) + { + CBookmark bookmark; + if (GetResumeBookMark(fileList[i]->GetPath(), bookmark)) + { + bookmark.partNumber = (i+1); /* store part number in here */ + tag.SetResumePoint(bookmark); + match = true; + break; + } + } + } + else + { + std::string strSQL=PrepareSQL("select timeInSeconds, totalTimeInSeconds from bookmark where idFile=%i and type=%i order by timeInSeconds", tag.m_iFileId, CBookmark::RESUME); + m_pDS2->query( strSQL ); + if (!m_pDS2->eof()) + { + tag.SetResumePoint(m_pDS2->fv(0).get_asDouble(), m_pDS2->fv(1).get_asDouble(), ""); + match = true; + } + m_pDS2->close(); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, tag.m_strFileNameAndPath); + } + + return match; +} + +CVideoInfoTag CVideoDatabase::GetDetailsForMovie(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */) +{ + return GetDetailsForMovie(pDS->get_sql_record(), getDetails); +} + +CVideoInfoTag CVideoDatabase::GetDetailsForMovie(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */) +{ + CVideoInfoTag details; + + if (record == NULL) + return details; + + int idMovie = record->at(0).get_asInt(); + + GetDetailsFromDB(record, VIDEODB_ID_MIN, VIDEODB_ID_MAX, DbMovieOffsets, details); + + details.m_iDbId = idMovie; + details.m_type = MediaTypeMovie; + + details.m_set.id = record->at(VIDEODB_DETAILS_MOVIE_SET_ID).get_asInt(); + details.m_set.title = record->at(VIDEODB_DETAILS_MOVIE_SET_NAME).get_asString(); + details.m_set.overview = record->at(VIDEODB_DETAILS_MOVIE_SET_OVERVIEW).get_asString(); + details.m_iFileId = record->at(VIDEODB_DETAILS_FILEID).get_asInt(); + details.m_strPath = record->at(VIDEODB_DETAILS_MOVIE_PATH).get_asString(); + std::string strFileName = record->at(VIDEODB_DETAILS_MOVIE_FILE).get_asString(); + ConstructPath(details.m_strFileNameAndPath,details.m_strPath,strFileName); + details.SetPlayCount(record->at(VIDEODB_DETAILS_MOVIE_PLAYCOUNT).get_asInt()); + details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MOVIE_LASTPLAYED).get_asString()); + details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MOVIE_DATEADDED).get_asString()); + details.SetResumePoint(record->at(VIDEODB_DETAILS_MOVIE_RESUME_TIME).get_asInt(), + record->at(VIDEODB_DETAILS_MOVIE_TOTAL_TIME).get_asInt(), + record->at(VIDEODB_DETAILS_MOVIE_PLAYER_STATE).get_asString()); + details.m_iUserRating = record->at(VIDEODB_DETAILS_MOVIE_USER_RATING).get_asInt(); + details.SetRating(record->at(VIDEODB_DETAILS_MOVIE_RATING).get_asFloat(), + record->at(VIDEODB_DETAILS_MOVIE_VOTES).get_asInt(), + record->at(VIDEODB_DETAILS_MOVIE_RATING_TYPE).get_asString(), true); + details.SetUniqueID(record->at(VIDEODB_DETAILS_MOVIE_UNIQUEID_VALUE).get_asString(), record->at(VIDEODB_DETAILS_MOVIE_UNIQUEID_TYPE).get_asString() ,true); + std::string premieredString = record->at(VIDEODB_DETAILS_MOVIE_PREMIERED).get_asString(); + if (premieredString.size() == 4) + details.SetYear(record->at(VIDEODB_DETAILS_MOVIE_PREMIERED).get_asInt()); + else + details.SetPremieredFromDBDate(premieredString); + + if (getDetails) + { + GetCast(details.m_iDbId, MediaTypeMovie, details.m_cast); + + if (getDetails & VideoDbDetailsTag) + GetTags(details.m_iDbId, MediaTypeMovie, details.m_tags); + + if (getDetails & VideoDbDetailsRating) + GetRatings(details.m_iDbId, MediaTypeMovie, details.m_ratings); + + if (getDetails & VideoDbDetailsUniqueID) + GetUniqueIDs(details.m_iDbId, MediaTypeMovie, details); + + if (getDetails & VideoDbDetailsShowLink) + { + // create tvshowlink string + std::vector<int> links; + GetLinksToTvShow(idMovie, links); + for (unsigned int i = 0; i < links.size(); ++i) + { + std::string strSQL = PrepareSQL("select c%02d from tvshow where idShow=%i", + VIDEODB_ID_TV_TITLE, links[i]); + m_pDS2->query(strSQL); + if (!m_pDS2->eof()) + details.m_showLink.emplace_back(m_pDS2->fv(0).get_asString()); + } + m_pDS2->close(); + } + + if (getDetails & VideoDbDetailsStream) + GetStreamDetails(details); + + details.m_parsedDetails = getDetails; + } + return details; +} + +CVideoInfoTag CVideoDatabase::GetDetailsForTvShow(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */, CFileItem* item /* = NULL */) +{ + return GetDetailsForTvShow(pDS->get_sql_record(), getDetails, item); +} + +CVideoInfoTag CVideoDatabase::GetDetailsForTvShow(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */, CFileItem* item /* = NULL */) +{ + CVideoInfoTag details; + + if (record == NULL) + return details; + + int idTvShow = record->at(0).get_asInt(); + + GetDetailsFromDB(record, VIDEODB_ID_TV_MIN, VIDEODB_ID_TV_MAX, DbTvShowOffsets, details, 1); + details.m_bHasPremiered = details.m_premiered.IsValid(); + details.m_iDbId = idTvShow; + details.m_type = MediaTypeTvShow; + details.m_strPath = record->at(VIDEODB_DETAILS_TVSHOW_PATH).get_asString(); + details.m_basePath = details.m_strPath; + details.m_parentPathID = record->at(VIDEODB_DETAILS_TVSHOW_PARENTPATHID).get_asInt(); + details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_TVSHOW_DATEADDED).get_asString()); + details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_TVSHOW_LASTPLAYED).get_asString()); + details.m_iSeason = record->at(VIDEODB_DETAILS_TVSHOW_NUM_SEASONS).get_asInt(); + details.m_iEpisode = record->at(VIDEODB_DETAILS_TVSHOW_NUM_EPISODES).get_asInt(); + details.SetPlayCount(record->at(VIDEODB_DETAILS_TVSHOW_NUM_WATCHED).get_asInt()); + details.m_strShowTitle = details.m_strTitle; + details.m_iUserRating = record->at(VIDEODB_DETAILS_TVSHOW_USER_RATING).get_asInt(); + details.SetRating(record->at(VIDEODB_DETAILS_TVSHOW_RATING).get_asFloat(), + record->at(VIDEODB_DETAILS_TVSHOW_VOTES).get_asInt(), + record->at(VIDEODB_DETAILS_TVSHOW_RATING_TYPE).get_asString(), true); + details.SetUniqueID(record->at(VIDEODB_DETAILS_TVSHOW_UNIQUEID_VALUE).get_asString(), record->at(VIDEODB_DETAILS_TVSHOW_UNIQUEID_TYPE).get_asString(), true); + details.SetDuration(record->at(VIDEODB_DETAILS_TVSHOW_DURATION).get_asInt()); + + if (getDetails) + { + if (getDetails & VideoDbDetailsCast) + { + GetCast(details.m_iDbId, "tvshow", details.m_cast); + } + + if (getDetails & VideoDbDetailsTag) + GetTags(details.m_iDbId, MediaTypeTvShow, details.m_tags); + + if (getDetails & VideoDbDetailsRating) + GetRatings(details.m_iDbId, MediaTypeTvShow, details.m_ratings); + + if (getDetails & VideoDbDetailsUniqueID) + GetUniqueIDs(details.m_iDbId, MediaTypeTvShow, details); + + details.m_parsedDetails = getDetails; + } + + if (item != NULL) + { + item->m_dateTime = details.GetPremiered(); + item->SetProperty("totalseasons", details.m_iSeason); + item->SetProperty("totalepisodes", details.m_iEpisode); + item->SetProperty("numepisodes", details.m_iEpisode); // will be changed later to reflect watchmode setting + item->SetProperty("watchedepisodes", details.GetPlayCount()); + item->SetProperty("unwatchedepisodes", details.m_iEpisode - details.GetPlayCount()); + item->SetProperty("watchedepisodepercent", + details.m_iEpisode > 0 ? (details.GetPlayCount() * 100 / details.m_iEpisode) + : 0); + } + details.SetPlayCount((details.m_iEpisode <= details.GetPlayCount()) ? 1 : 0); + + return details; +} + +CVideoInfoTag CVideoDatabase::GetBasicDetailsForEpisode(std::unique_ptr<Dataset> &pDS) +{ + return GetBasicDetailsForEpisode(pDS->get_sql_record()); +} + +CVideoInfoTag CVideoDatabase::GetBasicDetailsForEpisode(const dbiplus::sql_record* const record) +{ + CVideoInfoTag details; + + if (record == nullptr) + return details; + + int idEpisode = record->at(0).get_asInt(); + + GetDetailsFromDB(record, VIDEODB_ID_EPISODE_MIN, VIDEODB_ID_EPISODE_MAX, DbEpisodeOffsets, details); + details.m_iDbId = idEpisode; + details.m_type = MediaTypeEpisode; + details.m_iFileId = record->at(VIDEODB_DETAILS_FILEID).get_asInt(); + details.m_iIdShow = record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_ID).get_asInt(); + details.m_iIdSeason = record->at(VIDEODB_DETAILS_EPISODE_SEASON_ID).get_asInt(); + details.m_iUserRating = record->at(VIDEODB_DETAILS_EPISODE_USER_RATING).get_asInt(); + + return details; +} + +CVideoInfoTag CVideoDatabase::GetDetailsForEpisode(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */) +{ + return GetDetailsForEpisode(pDS->get_sql_record(), getDetails); +} + +CVideoInfoTag CVideoDatabase::GetDetailsForEpisode(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */) +{ + CVideoInfoTag details; + + if (record == nullptr) + return details; + + details = GetBasicDetailsForEpisode(record); + + details.m_strPath = record->at(VIDEODB_DETAILS_EPISODE_PATH).get_asString(); + std::string strFileName = record->at(VIDEODB_DETAILS_EPISODE_FILE).get_asString(); + ConstructPath(details.m_strFileNameAndPath,details.m_strPath,strFileName); + details.SetPlayCount(record->at(VIDEODB_DETAILS_EPISODE_PLAYCOUNT).get_asInt()); + details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_EPISODE_LASTPLAYED).get_asString()); + details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_EPISODE_DATEADDED).get_asString()); + details.m_strMPAARating = record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_MPAA).get_asString(); + details.m_strShowTitle = record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_NAME).get_asString(); + details.m_genre = StringUtils::Split(record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_GENRE).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + details.m_studio = StringUtils::Split(record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_STUDIO).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + details.SetPremieredFromDBDate(record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_AIRED).get_asString()); + + details.SetResumePoint(record->at(VIDEODB_DETAILS_EPISODE_RESUME_TIME).get_asInt(), + record->at(VIDEODB_DETAILS_EPISODE_TOTAL_TIME).get_asInt(), + record->at(VIDEODB_DETAILS_EPISODE_PLAYER_STATE).get_asString()); + + details.SetRating(record->at(VIDEODB_DETAILS_EPISODE_RATING).get_asFloat(), + record->at(VIDEODB_DETAILS_EPISODE_VOTES).get_asInt(), + record->at(VIDEODB_DETAILS_EPISODE_RATING_TYPE).get_asString(), true); + details.SetUniqueID(record->at(VIDEODB_DETAILS_EPISODE_UNIQUEID_VALUE).get_asString(), record->at(VIDEODB_DETAILS_EPISODE_UNIQUEID_TYPE).get_asString(), true); + + if (getDetails) + { + if (getDetails & VideoDbDetailsCast) + { + GetCast(details.m_iDbId, MediaTypeEpisode, details.m_cast); + GetCast(details.m_iIdShow, MediaTypeTvShow, details.m_cast); + } + + if (getDetails & VideoDbDetailsRating) + GetRatings(details.m_iDbId, MediaTypeEpisode, details.m_ratings); + + if (getDetails & VideoDbDetailsUniqueID) + GetUniqueIDs(details.m_iDbId, MediaTypeEpisode, details); + + if (getDetails & VideoDbDetailsBookmark) + GetBookMarkForEpisode(details, details.m_EpBookmark); + + if (getDetails & VideoDbDetailsStream) + GetStreamDetails(details); + + details.m_parsedDetails = getDetails; + } + return details; +} + +CVideoInfoTag CVideoDatabase::GetDetailsForMusicVideo(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */) +{ + return GetDetailsForMusicVideo(pDS->get_sql_record(), getDetails); +} + +CVideoInfoTag CVideoDatabase::GetDetailsForMusicVideo(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */) +{ + CVideoInfoTag details; + CArtist artist; + + if (record == nullptr) + return details; + + int idMVideo = record->at(0).get_asInt(); + + GetDetailsFromDB(record, VIDEODB_ID_MUSICVIDEO_MIN, VIDEODB_ID_MUSICVIDEO_MAX, DbMusicVideoOffsets, details); + details.m_iDbId = idMVideo; + details.m_type = MediaTypeMusicVideo; + + details.m_iFileId = record->at(VIDEODB_DETAILS_FILEID).get_asInt(); + details.m_strPath = record->at(VIDEODB_DETAILS_MUSICVIDEO_PATH).get_asString(); + std::string strFileName = record->at(VIDEODB_DETAILS_MUSICVIDEO_FILE).get_asString(); + ConstructPath(details.m_strFileNameAndPath,details.m_strPath,strFileName); + details.SetPlayCount(record->at(VIDEODB_DETAILS_MUSICVIDEO_PLAYCOUNT).get_asInt()); + details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MUSICVIDEO_LASTPLAYED).get_asString()); + details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MUSICVIDEO_DATEADDED).get_asString()); + details.SetResumePoint(record->at(VIDEODB_DETAILS_MUSICVIDEO_RESUME_TIME).get_asInt(), + record->at(VIDEODB_DETAILS_MUSICVIDEO_TOTAL_TIME).get_asInt(), + record->at(VIDEODB_DETAILS_MUSICVIDEO_PLAYER_STATE).get_asString()); + details.m_iUserRating = record->at(VIDEODB_DETAILS_MUSICVIDEO_USER_RATING).get_asInt(); + details.SetUniqueID(record->at(VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_VALUE).get_asString(), + record->at(VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_TYPE).get_asString(), true); + std::string premieredString = record->at(VIDEODB_DETAILS_MUSICVIDEO_PREMIERED).get_asString(); + if (premieredString.size() == 4) + details.SetYear(record->at(VIDEODB_DETAILS_MUSICVIDEO_PREMIERED).get_asInt()); + else + details.SetPremieredFromDBDate(premieredString); + + if (getDetails) + { + if (getDetails & VideoDbDetailsTag) + GetTags(details.m_iDbId, MediaTypeMusicVideo, details.m_tags); + + if (getDetails & VideoDbDetailsUniqueID) + GetUniqueIDs(details.m_iDbId, MediaTypeMusicVideo, details); + + if (getDetails & VideoDbDetailsStream) + GetStreamDetails(details); + + if (getDetails & VideoDbDetailsAll) + { + GetCast(details.m_iDbId, "musicvideo", details.m_cast); + } + + details.m_parsedDetails = getDetails; + } + return details; +} + +void CVideoDatabase::GetCast(int media_id, const std::string &media_type, std::vector<SActorInfo> &cast) +{ + try + { + if (!m_pDB) + return; + if (!m_pDS2) + return; + + std::string sql = PrepareSQL("SELECT actor.name," + " actor_link.role," + " actor_link.cast_order," + " actor.art_urls," + " art.url " + "FROM actor_link" + " JOIN actor ON" + " actor_link.actor_id=actor.actor_id" + " LEFT JOIN art ON" + " art.media_id=actor.actor_id AND art.media_type='actor' AND art.type='thumb' " + "WHERE actor_link.media_id=%i AND actor_link.media_type='%s'" + "ORDER BY actor_link.cast_order", media_id, media_type.c_str()); + m_pDS2->query(sql); + while (!m_pDS2->eof()) + { + SActorInfo info; + info.strName = m_pDS2->fv(0).get_asString(); + info.strRole = m_pDS2->fv(1).get_asString(); + + // ignore identical actors (since cast might already be prefilled) + if (std::none_of(cast.begin(), cast.end(), [info](const SActorInfo& actor) { + return actor.strName == info.strName && actor.strRole == info.strRole; + })) + { + info.order = m_pDS2->fv(2).get_asInt(); + info.thumbUrl.ParseFromData(m_pDS2->fv(3).get_asString()); + info.thumb = m_pDS2->fv(4).get_asString(); + cast.emplace_back(std::move(info)); + } + + m_pDS2->next(); + } + m_pDS2->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type); + } +} + +void CVideoDatabase::GetTags(int media_id, const std::string &media_type, std::vector<std::string> &tags) +{ + try + { + if (!m_pDB) + return; + if (!m_pDS2) + return; + + std::string sql = PrepareSQL("SELECT tag.name FROM tag INNER JOIN tag_link ON tag_link.tag_id = tag.tag_id WHERE tag_link.media_id = %i AND tag_link.media_type = '%s' ORDER BY tag.tag_id", media_id, media_type.c_str()); + m_pDS2->query(sql); + while (!m_pDS2->eof()) + { + tags.emplace_back(m_pDS2->fv(0).get_asString()); + m_pDS2->next(); + } + m_pDS2->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type); + } +} + +void CVideoDatabase::GetRatings(int media_id, const std::string &media_type, RatingMap &ratings) +{ + try + { + if (!m_pDB) + return; + if (!m_pDS2) + return; + + std::string sql = PrepareSQL("SELECT rating.rating_type, rating.rating, rating.votes FROM rating WHERE rating.media_id = %i AND rating.media_type = '%s'", media_id, media_type.c_str()); + m_pDS2->query(sql); + while (!m_pDS2->eof()) + { + ratings[m_pDS2->fv(0).get_asString()] = CRating(m_pDS2->fv(1).get_asFloat(), m_pDS2->fv(2).get_asInt()); + m_pDS2->next(); + } + m_pDS2->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type); + } +} + +void CVideoDatabase::GetUniqueIDs(int media_id, const std::string &media_type, CVideoInfoTag& details) +{ + try + { + if (!m_pDB) + return; + if (!m_pDS2) + return; + + std::string sql = PrepareSQL("SELECT type, value FROM uniqueid WHERE media_id = %i AND media_type = '%s'", media_id, media_type.c_str()); + m_pDS2->query(sql); + while (!m_pDS2->eof()) + { + details.SetUniqueID(m_pDS2->fv(1).get_asString(), m_pDS2->fv(0).get_asString()); + m_pDS2->next(); + } + m_pDS2->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type); + } +} + +bool CVideoDatabase::GetVideoSettings(const CFileItem &item, CVideoSettings &settings) +{ + return GetVideoSettings(GetFileId(item), settings); +} + +/// \brief GetVideoSettings() obtains any saved video settings for the current file. +/// \retval Returns true if the settings exist, false otherwise. +bool CVideoDatabase::GetVideoSettings(const std::string &filePath, CVideoSettings &settings) +{ + return GetVideoSettings(GetFileId(filePath), settings); +} + +bool CVideoDatabase::GetVideoSettings(int idFile, CVideoSettings &settings) +{ + try + { + if (idFile < 0) return false; + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL=PrepareSQL("select * from settings where settings.idFile = '%i'", idFile); + m_pDS->query( strSQL ); + + if (m_pDS->num_rows() > 0) + { // get the video settings info + settings.m_AudioDelay = m_pDS->fv("AudioDelay").get_asFloat(); + settings.m_AudioStream = m_pDS->fv("AudioStream").get_asInt(); + settings.m_Brightness = m_pDS->fv("Brightness").get_asFloat(); + settings.m_Contrast = m_pDS->fv("Contrast").get_asFloat(); + settings.m_CustomPixelRatio = m_pDS->fv("PixelRatio").get_asFloat(); + settings.m_CustomNonLinStretch = m_pDS->fv("NonLinStretch").get_asBool(); + settings.m_NoiseReduction = m_pDS->fv("NoiseReduction").get_asFloat(); + settings.m_PostProcess = m_pDS->fv("PostProcess").get_asBool(); + settings.m_Sharpness = m_pDS->fv("Sharpness").get_asFloat(); + settings.m_CustomZoomAmount = m_pDS->fv("ZoomAmount").get_asFloat(); + settings.m_CustomVerticalShift = m_pDS->fv("VerticalShift").get_asFloat(); + settings.m_Gamma = m_pDS->fv("Gamma").get_asFloat(); + settings.m_SubtitleDelay = m_pDS->fv("SubtitleDelay").get_asFloat(); + settings.m_SubtitleOn = m_pDS->fv("SubtitlesOn").get_asBool(); + settings.m_SubtitleStream = m_pDS->fv("SubtitleStream").get_asInt(); + settings.m_ViewMode = m_pDS->fv("ViewMode").get_asInt(); + settings.m_ResumeTime = m_pDS->fv("ResumeTime").get_asInt(); + settings.m_InterlaceMethod = (EINTERLACEMETHOD)m_pDS->fv("Deinterlace").get_asInt(); + settings.m_VolumeAmplification = m_pDS->fv("VolumeAmplification").get_asFloat(); + settings.m_ScalingMethod = (ESCALINGMETHOD)m_pDS->fv("ScalingMethod").get_asInt(); + settings.m_StereoMode = m_pDS->fv("StereoMode").get_asInt(); + settings.m_StereoInvert = m_pDS->fv("StereoInvert").get_asBool(); + settings.m_VideoStream = m_pDS->fv("VideoStream").get_asInt(); + settings.m_ToneMapMethod = + static_cast<ETONEMAPMETHOD>(m_pDS->fv("TonemapMethod").get_asInt()); + settings.m_ToneMapParam = m_pDS->fv("TonemapParam").get_asFloat(); + settings.m_Orientation = m_pDS->fv("Orientation").get_asInt(); + settings.m_CenterMixLevel = m_pDS->fv("CenterMixLevel").get_asInt(); + m_pDS->close(); + return true; + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +void CVideoDatabase::SetVideoSettings(const CFileItem &item, const CVideoSettings &settings) +{ + int idFile = AddFile(item); + SetVideoSettings(idFile, settings); +} + +/// \brief Sets the settings for a particular video file +void CVideoDatabase::SetVideoSettings(int idFile, const CVideoSettings &setting) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + if (idFile < 0) + return; + std::string strSQL = PrepareSQL("select * from settings where idFile=%i", idFile); + m_pDS->query( strSQL ); + if (m_pDS->num_rows() > 0) + { + m_pDS->close(); + // update the item + strSQL = PrepareSQL( + "update settings set " + "Deinterlace=%i,ViewMode=%i,ZoomAmount=%f,PixelRatio=%f,VerticalShift=%f," + "AudioStream=%i,SubtitleStream=%i,SubtitleDelay=%f,SubtitlesOn=%i,Brightness=%f,Contrast=" + "%f,Gamma=%f," + "VolumeAmplification=%f,AudioDelay=%f,Sharpness=%f,NoiseReduction=%f,NonLinStretch=%i," + "PostProcess=%i,ScalingMethod=%i,", + setting.m_InterlaceMethod, setting.m_ViewMode, + static_cast<double>(setting.m_CustomZoomAmount), + static_cast<double>(setting.m_CustomPixelRatio), + static_cast<double>(setting.m_CustomVerticalShift), setting.m_AudioStream, + setting.m_SubtitleStream, static_cast<double>(setting.m_SubtitleDelay), + setting.m_SubtitleOn, static_cast<double>(setting.m_Brightness), + static_cast<double>(setting.m_Contrast), static_cast<double>(setting.m_Gamma), + static_cast<double>(setting.m_VolumeAmplification), + static_cast<double>(setting.m_AudioDelay), static_cast<double>(setting.m_Sharpness), + static_cast<double>(setting.m_NoiseReduction), setting.m_CustomNonLinStretch, + setting.m_PostProcess, setting.m_ScalingMethod); + std::string strSQL2; + + strSQL2 = PrepareSQL("ResumeTime=%i,StereoMode=%i,StereoInvert=%i,VideoStream=%i," + "TonemapMethod=%i,TonemapParam=%f where idFile=%i\n", + setting.m_ResumeTime, setting.m_StereoMode, setting.m_StereoInvert, + setting.m_VideoStream, setting.m_ToneMapMethod, + static_cast<double>(setting.m_ToneMapParam), idFile); + strSQL += strSQL2; + m_pDS->exec(strSQL); + return ; + } + else + { // add the items + m_pDS->close(); + strSQL= "INSERT INTO settings (idFile,Deinterlace,ViewMode,ZoomAmount,PixelRatio, VerticalShift, " + "AudioStream,SubtitleStream,SubtitleDelay,SubtitlesOn,Brightness," + "Contrast,Gamma,VolumeAmplification,AudioDelay," + "ResumeTime," + "Sharpness,NoiseReduction,NonLinStretch,PostProcess,ScalingMethod,StereoMode,StereoInvert,VideoStream,TonemapMethod,TonemapParam,Orientation,CenterMixLevel) " + "VALUES "; + strSQL += PrepareSQL( + "(%i,%i,%i,%f,%f,%f,%i,%i,%f,%i,%f,%f,%f,%f,%f,%i,%f,%f,%i,%i,%i,%i,%i,%i,%i,%f,%i,%i)", + idFile, setting.m_InterlaceMethod, setting.m_ViewMode, + static_cast<double>(setting.m_CustomZoomAmount), + static_cast<double>(setting.m_CustomPixelRatio), + static_cast<double>(setting.m_CustomVerticalShift), setting.m_AudioStream, + setting.m_SubtitleStream, static_cast<double>(setting.m_SubtitleDelay), + setting.m_SubtitleOn, static_cast<double>(setting.m_Brightness), + static_cast<double>(setting.m_Contrast), static_cast<double>(setting.m_Gamma), + static_cast<double>(setting.m_VolumeAmplification), + static_cast<double>(setting.m_AudioDelay), setting.m_ResumeTime, + static_cast<double>(setting.m_Sharpness), static_cast<double>(setting.m_NoiseReduction), + setting.m_CustomNonLinStretch, setting.m_PostProcess, setting.m_ScalingMethod, + setting.m_StereoMode, setting.m_StereoInvert, setting.m_VideoStream, + setting.m_ToneMapMethod, static_cast<double>(setting.m_ToneMapParam), + setting.m_Orientation, setting.m_CenterMixLevel); + m_pDS->exec(strSQL); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idFile); + } +} + +void CVideoDatabase::SetArtForItem(int mediaId, const MediaType &mediaType, const std::map<std::string, std::string> &art) +{ + for (const auto &i : art) + SetArtForItem(mediaId, mediaType, i.first, i.second); +} + +void CVideoDatabase::SetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType, const std::string &url) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + // don't set <foo>.<bar> art types - these are derivative types from parent items + if (artType.find('.') != std::string::npos) + return; + + std::string sql = PrepareSQL("SELECT art_id,url FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str()); + m_pDS->query(sql); + if (!m_pDS->eof()) + { // update + int artId = m_pDS->fv(0).get_asInt(); + std::string oldUrl = m_pDS->fv(1).get_asString(); + m_pDS->close(); + if (oldUrl != url) + { + sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId); + m_pDS->exec(sql); + } + } + else + { // insert + m_pDS->close(); + sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) VALUES (%d, '%s', '%s', '%s')", mediaId, mediaType.c_str(), artType.c_str(), url.c_str()); + m_pDS->exec(sql); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__, mediaId, mediaType, + artType, url); + } +} + +bool CVideoDatabase::GetArtForItem(int mediaId, const MediaType &mediaType, std::map<std::string, std::string> &art) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; // using dataset 2 as we're likely called in loops on dataset 1 + + std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'", mediaId, mediaType.c_str()); + m_pDS2->query(sql); + while (!m_pDS2->eof()) + { + art.insert(make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString())); + m_pDS2->next(); + } + m_pDS2->close(); + return !art.empty(); + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId); + } + return false; +} + +std::string CVideoDatabase::GetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType) +{ + std::string query = PrepareSQL("SELECT url FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str()); + return GetSingleValue(query, m_pDS2); +} + +bool CVideoDatabase::RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType) +{ + return ExecuteQuery(PrepareSQL("DELETE FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str())); +} + +bool CVideoDatabase::RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::set<std::string> &artTypes) +{ + bool result = true; + for (const auto &i : artTypes) + result &= RemoveArtForItem(mediaId, mediaType, i); + + return result; +} + +bool CVideoDatabase::HasArtForItem(int mediaId, const MediaType &mediaType) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; // using dataset 2 as we're likely called in loops on dataset 1 + + std::string sql = PrepareSQL("SELECT 1 FROM art WHERE media_id=%i AND media_type='%s' LIMIT 1", mediaId, mediaType.c_str()); + m_pDS2->query(sql); + bool result = !m_pDS2->eof(); + m_pDS2->close(); + return result; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId); + } + return false; +} + +bool CVideoDatabase::GetTvShowSeasons(int showId, std::map<int, int> &seasons) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; // using dataset 2 as we're likely called in loops on dataset 1 + + // get all seasons for this show + std::string sql = PrepareSQL("select idSeason,season from seasons where idShow=%i", showId); + m_pDS2->query(sql); + + seasons.clear(); + while (!m_pDS2->eof()) + { + seasons.insert(std::make_pair(m_pDS2->fv(1).get_asInt(), m_pDS2->fv(0).get_asInt())); + m_pDS2->next(); + } + m_pDS2->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, showId); + } + return false; +} + +bool CVideoDatabase::GetTvShowNamedSeasons(int showId, std::map<int, std::string> &seasons) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; // using dataset 2 as we're likely called in loops on dataset 1 + + // get all named seasons for this show + std::string sql = PrepareSQL("select season, name from seasons where season > 0 and name is not null and name <> '' and idShow = %i", showId); + m_pDS2->query(sql); + + seasons.clear(); + while (!m_pDS2->eof()) + { + seasons.insert(std::make_pair(m_pDS2->fv(0).get_asInt(), m_pDS2->fv(1).get_asString())); + m_pDS2->next(); + } + m_pDS2->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, showId); + } + return false; +} + +std::string CVideoDatabase::GetTvShowNamedSeasonById(int tvshowId, int seasonId) +{ + return GetSingleValue("seasons", "name", + PrepareSQL("season=%i AND idShow=%i", seasonId, tvshowId)); +} + +bool CVideoDatabase::GetTvShowSeasonArt(int showId, std::map<int, std::map<std::string, std::string> > &seasonArt) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; // using dataset 2 as we're likely called in loops on dataset 1 + + std::map<int, int> seasons; + GetTvShowSeasons(showId, seasons); + + for (const auto &i : seasons) + { + std::map<std::string, std::string> art; + GetArtForItem(i.second, MediaTypeSeason, art); + seasonArt.insert(std::make_pair(i.first,art)); + } + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, showId); + } + return false; +} + +bool CVideoDatabase::GetArtTypes(const MediaType &mediaType, std::vector<std::string> &artTypes) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string sql = PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str()); + int numRows = RunQuery(sql); + if (numRows <= 0) + return numRows == 0; + + while (!m_pDS->eof()) + { + artTypes.emplace_back(m_pDS->fv(0).get_asString()); + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaType); + } + return false; +} + +namespace +{ +std::vector<std::string> GetBasicItemAvailableArtTypes(int mediaId, + VideoDbContentType dbType, + CVideoDatabase& db) +{ + std::vector<std::string> result; + CVideoInfoTag tag = db.GetDetailsByTypeAndId(dbType, mediaId); + + //! @todo artwork: fanart stored separately, doesn't need to be + tag.m_fanart.Unpack(); + if (tag.m_fanart.GetNumFanarts() && std::find(result.cbegin(), result.cend(), "fanart") == result.cend()) + result.emplace_back("fanart"); + + // all other images + tag.m_strPictureURL.Parse(); + for (const auto& urlEntry : tag.m_strPictureURL.GetUrls()) + { + std::string artType = urlEntry.m_aspect; + if (artType.empty()) + artType = tag.m_type == MediaTypeEpisode ? "thumb" : "poster"; + if (urlEntry.m_type == CScraperUrl::UrlType::General && // exclude season artwork for TV shows + !StringUtils::StartsWith(artType, "set.") && // exclude movie set artwork for movies + std::find(result.cbegin(), result.cend(), artType) == result.cend()) + { + result.push_back(artType); + } + } + return result; +} + +std::vector<std::string> GetSeasonAvailableArtTypes(int mediaId, CVideoDatabase& db) +{ + CVideoInfoTag tag; + db.GetSeasonInfo(mediaId, tag); + + std::vector<std::string> result; + + CVideoInfoTag sourceShow; + db.GetTvShowInfo("", sourceShow, tag.m_iIdShow); + sourceShow.m_strPictureURL.Parse(); + for (const auto& urlEntry : sourceShow.m_strPictureURL.GetUrls()) + { + std::string artType = urlEntry.m_aspect; + if (artType.empty()) + artType = "poster"; + if (urlEntry.m_type == CScraperUrl::UrlType::Season && urlEntry.m_season == tag.m_iSeason && + std::find(result.cbegin(), result.cend(), artType) == result.cend()) + { + result.push_back(artType); + } + } + return result; +} + +std::vector<std::string> GetMovieSetAvailableArtTypes(int mediaId, CVideoDatabase& db) +{ + std::vector<std::string> result; + CFileItemList items; + std::string baseDir = StringUtils::Format("videodb://movies/sets/{}", mediaId); + if (db.GetMoviesNav(baseDir, items)) + { + for (const auto& item : items) + { + CVideoInfoTag* pTag = item->GetVideoInfoTag(); + pTag->m_strPictureURL.Parse(); + + for (const auto& urlEntry : pTag->m_strPictureURL.GetUrls()) + { + if (!StringUtils::StartsWith(urlEntry.m_aspect, "set.")) + continue; + + std::string artType = urlEntry.m_aspect.substr(4); + if (std::find(result.cbegin(), result.cend(), artType) == result.cend()) + result.push_back(artType); + } + } + } + return result; +} + +std::vector<CScraperUrl::SUrlEntry> GetBasicItemAvailableArt(int mediaId, + VideoDbContentType dbType, + const std::string& artType, + CVideoDatabase& db) +{ + std::vector<CScraperUrl::SUrlEntry> result; + CVideoInfoTag tag = db.GetDetailsByTypeAndId(dbType, mediaId); + + if (artType.empty() || artType == "fanart") + { + tag.m_fanart.Unpack(); + for (unsigned int i = 0; i < tag.m_fanart.GetNumFanarts(); i++) + { + CScraperUrl::SUrlEntry url(tag.m_fanart.GetImageURL(i)); + url.m_preview = tag.m_fanart.GetPreviewURL(i); + url.m_aspect = "fanart"; + result.push_back(url); + } + } + tag.m_strPictureURL.Parse(); + for (auto urlEntry : tag.m_strPictureURL.GetUrls()) + { + if (urlEntry.m_aspect.empty()) + urlEntry.m_aspect = tag.m_type == MediaTypeEpisode ? "thumb" : "poster"; + if ((urlEntry.m_aspect == artType || + (artType.empty() && !StringUtils::StartsWith(urlEntry.m_aspect, "set."))) && + urlEntry.m_type == CScraperUrl::UrlType::General) + { + result.push_back(urlEntry); + } + } + + return result; +} + +std::vector<CScraperUrl::SUrlEntry> GetSeasonAvailableArt( + int mediaId, const std::string& artType, CVideoDatabase& db) +{ + CVideoInfoTag tag; + db.GetSeasonInfo(mediaId, tag); + + std::vector<CScraperUrl::SUrlEntry> result; + + CVideoInfoTag sourceShow; + db.GetTvShowInfo("", sourceShow, tag.m_iIdShow); + sourceShow.m_strPictureURL.Parse(); + for (auto urlEntry : sourceShow.m_strPictureURL.GetUrls()) + { + if (urlEntry.m_aspect.empty()) + urlEntry.m_aspect = "poster"; + if ((artType.empty() || urlEntry.m_aspect == artType) && + urlEntry.m_type == CScraperUrl::UrlType::Season && + urlEntry.m_season == tag.m_iSeason) + { + result.push_back(urlEntry); + } + } + return result; +} + +std::vector<CScraperUrl::SUrlEntry> GetMovieSetAvailableArt( + int mediaId, const std::string& artType, CVideoDatabase& db) +{ + std::vector<CScraperUrl::SUrlEntry> result; + CFileItemList items; + std::string baseDir = StringUtils::Format("videodb://movies/sets/{}", mediaId); + std::unordered_set<std::string> addedURLs; + if (db.GetMoviesNav(baseDir, items)) + { + for (const auto& item : items) + { + CVideoInfoTag* pTag = item->GetVideoInfoTag(); + pTag->m_strPictureURL.Parse(); + + for (auto urlEntry : pTag->m_strPictureURL.GetUrls()) + { + bool isSetArt = !artType.empty() ? urlEntry.m_aspect == "set." + artType : + StringUtils::StartsWith(urlEntry.m_aspect, "set."); + if (isSetArt && addedURLs.insert(urlEntry.m_url).second) + { + urlEntry.m_aspect = urlEntry.m_aspect.substr(4); + result.push_back(urlEntry); + } + } + } + } + return result; +} + +VideoDbContentType CovertMediaTypeToContentType(const MediaType& mediaType) +{ + VideoDbContentType dbType{VideoDbContentType::UNKNOWN}; + if (mediaType == MediaTypeTvShow) + dbType = VideoDbContentType::TVSHOWS; + else if (mediaType == MediaTypeMovie) + dbType = VideoDbContentType::MOVIES; + else if (mediaType == MediaTypeEpisode) + dbType = VideoDbContentType::EPISODES; + else if (mediaType == MediaTypeMusicVideo) + dbType = VideoDbContentType::MUSICVIDEOS; + + return dbType; +} +} // namespace + +std::vector<CScraperUrl::SUrlEntry> CVideoDatabase::GetAvailableArtForItem( + int mediaId, const MediaType& mediaType, const std::string& artType) +{ + VideoDbContentType dbType = CovertMediaTypeToContentType(mediaType); + + if (dbType != VideoDbContentType::UNKNOWN) + return GetBasicItemAvailableArt(mediaId, dbType, artType, *this); + if (mediaType == MediaTypeSeason) + return GetSeasonAvailableArt(mediaId, artType, *this); + if (mediaType == MediaTypeVideoCollection) + return GetMovieSetAvailableArt(mediaId, artType, *this); + return {}; +} + +std::vector<std::string> CVideoDatabase::GetAvailableArtTypesForItem(int mediaId, + const MediaType& mediaType) +{ + VideoDbContentType dbType = CovertMediaTypeToContentType(mediaType); + + if (dbType != VideoDbContentType::UNKNOWN) + return GetBasicItemAvailableArtTypes(mediaId, dbType, *this); + if (mediaType == MediaTypeSeason) + return GetSeasonAvailableArtTypes(mediaId, *this); + if (mediaType == MediaTypeVideoCollection) + return GetMovieSetAvailableArtTypes(mediaId, *this); + return {}; +} + +/// \brief GetStackTimes() obtains any saved video times for the stacked file +/// \retval Returns true if the stack times exist, false otherwise. +bool CVideoDatabase::GetStackTimes(const std::string &filePath, std::vector<uint64_t> ×) +{ + try + { + // obtain the FileID (if it exists) + int idFile = GetFileId(filePath); + if (idFile < 0) return false; + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + // ok, now obtain the settings for this file + std::string strSQL=PrepareSQL("select times from stacktimes where idFile=%i\n", idFile); + m_pDS->query( strSQL ); + if (m_pDS->num_rows() > 0) + { // get the video settings info + uint64_t timeTotal = 0; + std::vector<std::string> timeString = StringUtils::Split(m_pDS->fv("times").get_asString(), ","); + times.clear(); + for (const auto &i : timeString) + { + uint64_t partTime = static_cast<uint64_t>(atof(i.c_str()) * 1000.0); + times.push_back(partTime); // db stores in secs, convert to msecs + timeTotal += partTime; + } + m_pDS->close(); + return (timeTotal > 0); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +/// \brief Sets the stack times for a particular video file +void CVideoDatabase::SetStackTimes(const std::string& filePath, const std::vector<uint64_t> ×) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + int idFile = AddFile(filePath); + if (idFile < 0) + return; + + // delete any existing items + m_pDS->exec( PrepareSQL("delete from stacktimes where idFile=%i", idFile) ); + + // add the items + std::string timeString = StringUtils::Format("{:.3f}", times[0] / 1000.0f); + for (unsigned int i = 1; i < times.size(); i++) + timeString += StringUtils::Format(",{:.3f}", times[i] / 1000.0f); + + m_pDS->exec( PrepareSQL("insert into stacktimes (idFile,times) values (%i,'%s')\n", idFile, timeString.c_str()) ); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } +} + +void CVideoDatabase::RemoveContentForPath(const std::string& strPath, CGUIDialogProgress *progress /* = NULL */) +{ + if(URIUtils::IsMultiPath(strPath)) + { + std::vector<std::string> paths; + CMultiPathDirectory::GetPaths(strPath, paths); + + for(unsigned i=0;i<paths.size();i++) + RemoveContentForPath(paths[i], progress); + } + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (progress) + { + progress->SetHeading(CVariant{700}); + progress->SetLine(0, CVariant{""}); + progress->SetLine(1, CVariant{313}); + progress->SetLine(2, CVariant{330}); + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + std::vector<std::pair<int, std::string> > paths; + GetSubPaths(strPath, paths); + int iCurr = 0; + for (const auto &i : paths) + { + bool bMvidsChecked=false; + if (progress) + { + progress->SetPercentage((int)((float)(iCurr++)/paths.size()*100.f)); + progress->Progress(); + } + + const auto tvshowId = GetTvShowId(i.second); + if (tvshowId > 0) + DeleteTvShow(tvshowId); + else + { + std::string strSQL = PrepareSQL("select files.strFilename from files join movie on movie.idFile=files.idFile where files.idPath=%i", i.first); + m_pDS2->query(strSQL); + if (m_pDS2->eof()) + { + strSQL = PrepareSQL("select files.strFilename from files join musicvideo on musicvideo.idFile=files.idFile where files.idPath=%i", i.first); + m_pDS2->query(strSQL); + bMvidsChecked = true; + } + while (!m_pDS2->eof()) + { + std::string strMoviePath; + std::string strFileName = m_pDS2->fv("files.strFilename").get_asString(); + ConstructPath(strMoviePath, i.second, strFileName); + const auto movieId = GetMovieId(strMoviePath); + if (movieId > 0) + DeleteMovie(movieId); + else + { + const auto musicvideoId = GetMusicVideoId(strMoviePath); + if (musicvideoId > 0) + DeleteMusicVideo(musicvideoId); + } + m_pDS2->next(); + if (m_pDS2->eof() && !bMvidsChecked) + { + strSQL =PrepareSQL("select files.strFilename from files join musicvideo on musicvideo.idFile=files.idFile where files.idPath=%i", i.first); + m_pDS2->query(strSQL); + bMvidsChecked = true; + } + } + m_pDS2->close(); + m_pDS2->exec(PrepareSQL("update path set strContent='', strScraper='', strHash='',strSettings='',useFolderNames=0,scanRecursive=0 where idPath=%i", i.first)); + } + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath); + } + if (progress) + progress->Close(); +} + +void CVideoDatabase::SetScraperForPath(const std::string& filePath, const ScraperPtr& scraper, const VIDEO::SScanSettings& settings) +{ + // if we have a multipath, set scraper for all contained paths + if(URIUtils::IsMultiPath(filePath)) + { + std::vector<std::string> paths; + CMultiPathDirectory::GetPaths(filePath, paths); + + for(unsigned i=0;i<paths.size();i++) + SetScraperForPath(paths[i],scraper,settings); + + return; + } + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + int idPath = AddPath(filePath); + if (idPath < 0) + return; + + // Update + std::string strSQL; + if (settings.exclude) + { //NB See note in ::GetScraperForPath about strContent=='none' + strSQL = PrepareSQL( + "UPDATE path SET strContent='', strScraper='', scanRecursive=0, useFolderNames=0, " + "strSettings='', noUpdate=0, exclude=1, allAudio=%i WHERE idPath=%i", + settings.m_allExtAudio, idPath); + } + else if(!scraper) + { // catch clearing content, but not excluding + strSQL = PrepareSQL( + "UPDATE path SET strContent='', strScraper='', scanRecursive=0, useFolderNames=0, " + "strSettings='', noUpdate=0, exclude=0, allAudio=%i WHERE idPath=%i", + settings.m_allExtAudio, idPath); + } + else + { + std::string content = TranslateContent(scraper->Content()); + strSQL = PrepareSQL( + "UPDATE path SET strContent='%s', strScraper='%s', scanRecursive=%i, useFolderNames=%i, " + "strSettings='%s', noUpdate=%i, exclude=0, allAudio=%i WHERE idPath=%i", + content.c_str(), scraper->ID().c_str(), settings.recurse, settings.parent_name, + scraper->GetPathSettings().c_str(), settings.noupdate, settings.m_allExtAudio, idPath); + } + m_pDS->exec(strSQL); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath); + } +} + +bool CVideoDatabase::ScraperInUse(const std::string &scraperID) const +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string sql = PrepareSQL("select count(1) from path where strScraper='%s'", scraperID.c_str()); + if (!m_pDS->query(sql) || m_pDS->num_rows() == 0) + return false; + bool found = m_pDS->fv(0).get_asInt() > 0; + m_pDS->close(); + return found; + } + catch (...) + { + CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, scraperID); + } + return false; +} + +class CArtItem +{ +public: + CArtItem() { art_id = 0; media_id = 0; }; + int art_id; + std::string art_type; + std::string art_url; + int media_id; + std::string media_type; +}; + +// used for database update to v83 +class CShowItem +{ +public: + bool operator==(const CShowItem &r) const + { + return (!ident.empty() && ident == r.ident) || (title == r.title && year == r.year); + }; + int id; + int path; + std::string title; + std::string year; + std::string ident; +}; + +// used for database update to v84 +class CShowLink +{ +public: + int show; + int pathId; + std::string path; +}; + +void CVideoDatabase::UpdateTables(int iVersion) +{ + // Important: DO NOT use CREATE TABLE [...] AS SELECT [...] - it does not work on MySQL with GTID consistency enforced + + if (iVersion < 76) + { + m_pDS->exec("ALTER TABLE settings ADD StereoMode integer"); + m_pDS->exec("ALTER TABLE settings ADD StereoInvert bool"); + } + if (iVersion < 77) + m_pDS->exec("ALTER TABLE streamdetails ADD strStereoMode text"); + + if (iVersion < 81) + { // add idParentPath to path table + m_pDS->exec("ALTER TABLE path ADD idParentPath integer"); + std::map<std::string, int> paths; + m_pDS->query("select idPath,strPath from path"); + while (!m_pDS->eof()) + { + paths.insert(make_pair(m_pDS->fv(1).get_asString(), m_pDS->fv(0).get_asInt())); + m_pDS->next(); + } + m_pDS->close(); + // run through these paths figuring out the parent path, and add to the table if found + for (const auto &i : paths) + { + std::string parent = URIUtils::GetParentPath(i.first); + auto j = paths.find(parent); + if (j != paths.end()) + m_pDS->exec(PrepareSQL("UPDATE path SET idParentPath=%i WHERE idPath=%i", j->second, i.second)); + } + } + if (iVersion < 82) + { + // drop parent path id and basePath from tvshow table + m_pDS->exec("UPDATE tvshow SET c16=NULL,c17=NULL"); + } + if (iVersion < 83) + { + // drop duplicates in tvshow table, and update tvshowlinkpath accordingly + std::string sql = PrepareSQL("SELECT tvshow.idShow,idPath,c%02d,c%02d,c%02d FROM tvshow JOIN tvshowlinkpath ON tvshow.idShow = tvshowlinkpath.idShow", VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_PREMIERED, VIDEODB_ID_TV_IDENT_ID); + m_pDS->query(sql); + std::vector<CShowItem> shows; + while (!m_pDS->eof()) + { + CShowItem show; + show.id = m_pDS->fv(0).get_asInt(); + show.path = m_pDS->fv(1).get_asInt(); + show.title = m_pDS->fv(2).get_asString(); + show.year = m_pDS->fv(3).get_asString(); + show.ident = m_pDS->fv(4).get_asString(); + shows.emplace_back(std::move(show)); + m_pDS->next(); + } + m_pDS->close(); + if (!shows.empty()) + { + for (auto i = shows.begin() + 1; i != shows.end(); ++i) + { + // has this show been found before? + auto j = find(shows.begin(), i, *i); + if (j != i) + { // this is a duplicate + // update the tvshowlinkpath table + m_pDS->exec(PrepareSQL("UPDATE tvshowlinkpath SET idShow = %d WHERE idShow = %d AND idPath = %d", j->id, i->id, i->path)); + // update episodes, seasons, movie links + m_pDS->exec(PrepareSQL("UPDATE episode SET idShow = %d WHERE idShow = %d", j->id, i->id)); + m_pDS->exec(PrepareSQL("UPDATE seasons SET idShow = %d WHERE idShow = %d", j->id, i->id)); + m_pDS->exec(PrepareSQL("UPDATE movielinktvshow SET idShow = %d WHERE idShow = %d", j->id, i->id)); + // delete tvshow + m_pDS->exec(PrepareSQL("DELETE FROM genrelinktvshow WHERE idShow=%i", i->id)); + m_pDS->exec(PrepareSQL("DELETE FROM actorlinktvshow WHERE idShow=%i", i->id)); + m_pDS->exec(PrepareSQL("DELETE FROM directorlinktvshow WHERE idShow=%i", i->id)); + m_pDS->exec(PrepareSQL("DELETE FROM studiolinktvshow WHERE idShow=%i", i->id)); + m_pDS->exec(PrepareSQL("DELETE FROM tvshow WHERE idShow = %d", i->id)); + } + } + // cleanup duplicate seasons + m_pDS->exec("DELETE FROM seasons WHERE idSeason NOT IN (SELECT idSeason FROM (SELECT min(idSeason) as idSeason FROM seasons GROUP BY idShow,season) AS sub)"); + } + } + if (iVersion < 84) + { // replace any multipaths in tvshowlinkpath table + m_pDS->query("SELECT idShow, tvshowlinkpath.idPath, strPath FROM tvshowlinkpath JOIN path ON tvshowlinkpath.idPath=path.idPath WHERE path.strPath LIKE 'multipath://%'"); + std::vector<CShowLink> shows; + while (!m_pDS->eof()) + { + CShowLink link; + link.show = m_pDS->fv(0).get_asInt(); + link.pathId = m_pDS->fv(1).get_asInt(); + link.path = m_pDS->fv(2).get_asString(); + shows.emplace_back(std::move(link)); + m_pDS->next(); + } + m_pDS->close(); + // update these + for (auto i = shows.begin(); i != shows.end(); ++i) + { + std::vector<std::string> paths; + CMultiPathDirectory::GetPaths(i->path, paths); + for (auto j = paths.begin(); j != paths.end(); ++j) + { + int idPath = AddPath(*j, URIUtils::GetParentPath(*j)); + /* we can't rely on REPLACE INTO here as analytics (indices) aren't online yet */ + if (GetSingleValue(PrepareSQL("SELECT 1 FROM tvshowlinkpath WHERE idShow=%i AND idPath=%i", i->show, idPath)).empty()) + m_pDS->exec(PrepareSQL("INSERT INTO tvshowlinkpath(idShow, idPath) VALUES(%i,%i)", i->show, idPath)); + } + m_pDS->exec(PrepareSQL("DELETE FROM tvshowlinkpath WHERE idShow=%i AND idPath=%i", i->show, i->pathId)); + } + } + if (iVersion < 85) + { + // drop multipaths from the path table - they're not needed for anything at all + m_pDS->exec("DELETE FROM path WHERE strPath LIKE 'multipath://%'"); + } + if (iVersion < 87) + { // due to the tvshow merging above, there could be orphaned season or show art + m_pDS->exec("DELETE from art WHERE media_type='tvshow' AND NOT EXISTS (SELECT 1 FROM tvshow WHERE tvshow.idShow = art.media_id)"); + m_pDS->exec("DELETE from art WHERE media_type='season' AND NOT EXISTS (SELECT 1 FROM seasons WHERE seasons.idSeason = art.media_id)"); + } + if (iVersion < 91) + { + // create actor link table + m_pDS->exec("CREATE TABLE actor_link(actor_id INT, media_id INT, media_type TEXT, role TEXT, cast_order INT)"); + m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type, role, cast_order) SELECT DISTINCT idActor, idMovie, 'movie', strRole, iOrder from actorlinkmovie"); + m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type, role, cast_order) SELECT DISTINCT idActor, idShow, 'tvshow', strRole, iOrder from actorlinktvshow"); + m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type, role, cast_order) SELECT DISTINCT idActor, idEpisode, 'episode', strRole, iOrder from actorlinkepisode"); + m_pDS->exec("DROP TABLE IF EXISTS actorlinkmovie"); + m_pDS->exec("DROP TABLE IF EXISTS actorlinktvshow"); + m_pDS->exec("DROP TABLE IF EXISTS actorlinkepisode"); + m_pDS->exec("CREATE TABLE actor(actor_id INTEGER PRIMARY KEY, name TEXT, art_urls TEXT)"); + m_pDS->exec("INSERT INTO actor(actor_id, name, art_urls) SELECT idActor,strActor,strThumb FROM actors"); + m_pDS->exec("DROP TABLE IF EXISTS actors"); + + // directors + m_pDS->exec("CREATE TABLE director_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)"); + m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idMovie, 'movie' FROM directorlinkmovie"); + m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idShow, 'tvshow' FROM directorlinktvshow"); + m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idEpisode, 'episode' FROM directorlinkepisode"); + m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idMVideo, 'musicvideo' FROM directorlinkmusicvideo"); + m_pDS->exec("DROP TABLE IF EXISTS directorlinkmovie"); + m_pDS->exec("DROP TABLE IF EXISTS directorlinktvshow"); + m_pDS->exec("DROP TABLE IF EXISTS directorlinkepisode"); + m_pDS->exec("DROP TABLE IF EXISTS directorlinkmusicvideo"); + + // writers + m_pDS->exec("CREATE TABLE writer_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)"); + m_pDS->exec("INSERT INTO writer_link(actor_id, media_id, media_type) SELECT DISTINCT idWriter, idMovie, 'movie' FROM writerlinkmovie"); + m_pDS->exec("INSERT INTO writer_link(actor_id, media_id, media_type) SELECT DISTINCT idWriter, idEpisode, 'episode' FROM writerlinkepisode"); + m_pDS->exec("DROP TABLE IF EXISTS writerlinkmovie"); + m_pDS->exec("DROP TABLE IF EXISTS writerlinkepisode"); + + // music artist + m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type) SELECT DISTINCT idArtist, idMVideo, 'musicvideo' FROM artistlinkmusicvideo"); + m_pDS->exec("DROP TABLE IF EXISTS artistlinkmusicvideo"); + + // studios + m_pDS->exec("CREATE TABLE studio_link(studio_id INTEGER, media_id INTEGER, media_type TEXT)"); + m_pDS->exec("INSERT INTO studio_link(studio_id, media_id, media_type) SELECT DISTINCT idStudio, idMovie, 'movie' FROM studiolinkmovie"); + m_pDS->exec("INSERT INTO studio_link(studio_id, media_id, media_type) SELECT DISTINCT idStudio, idShow, 'tvshow' FROM studiolinktvshow"); + m_pDS->exec("INSERT INTO studio_link(studio_id, media_id, media_type) SELECT DISTINCT idStudio, idMVideo, 'musicvideo' FROM studiolinkmusicvideo"); + m_pDS->exec("DROP TABLE IF EXISTS studiolinkmovie"); + m_pDS->exec("DROP TABLE IF EXISTS studiolinktvshow"); + m_pDS->exec("DROP TABLE IF EXISTS studiolinkmusicvideo"); + m_pDS->exec("CREATE TABLE studionew(studio_id INTEGER PRIMARY KEY, name TEXT)"); + m_pDS->exec("INSERT INTO studionew(studio_id, name) SELECT idStudio,strStudio FROM studio"); + m_pDS->exec("DROP TABLE IF EXISTS studio"); + m_pDS->exec("ALTER TABLE studionew RENAME TO studio"); + + // genres + m_pDS->exec("CREATE TABLE genre_link(genre_id INTEGER, media_id INTEGER, media_type TEXT)"); + m_pDS->exec("INSERT INTO genre_link(genre_id, media_id, media_type) SELECT DISTINCT idGenre, idMovie, 'movie' FROM genrelinkmovie"); + m_pDS->exec("INSERT INTO genre_link(genre_id, media_id, media_type) SELECT DISTINCT idGenre, idShow, 'tvshow' FROM genrelinktvshow"); + m_pDS->exec("INSERT INTO genre_link(genre_id, media_id, media_type) SELECT DISTINCT idGenre, idMVideo, 'musicvideo' FROM genrelinkmusicvideo"); + m_pDS->exec("DROP TABLE IF EXISTS genrelinkmovie"); + m_pDS->exec("DROP TABLE IF EXISTS genrelinktvshow"); + m_pDS->exec("DROP TABLE IF EXISTS genrelinkmusicvideo"); + m_pDS->exec("CREATE TABLE genrenew(genre_id INTEGER PRIMARY KEY, name TEXT)"); + m_pDS->exec("INSERT INTO genrenew(genre_id, name) SELECT idGenre,strGenre FROM genre"); + m_pDS->exec("DROP TABLE IF EXISTS genre"); + m_pDS->exec("ALTER TABLE genrenew RENAME TO genre"); + + // country + m_pDS->exec("CREATE TABLE country_link(country_id INTEGER, media_id INTEGER, media_type TEXT)"); + m_pDS->exec("INSERT INTO country_link(country_id, media_id, media_type) SELECT DISTINCT idCountry, idMovie, 'movie' FROM countrylinkmovie"); + m_pDS->exec("DROP TABLE IF EXISTS countrylinkmovie"); + m_pDS->exec("CREATE TABLE countrynew(country_id INTEGER PRIMARY KEY, name TEXT)"); + m_pDS->exec("INSERT INTO countrynew(country_id, name) SELECT idCountry,strCountry FROM country"); + m_pDS->exec("DROP TABLE IF EXISTS country"); + m_pDS->exec("ALTER TABLE countrynew RENAME TO country"); + + // tags + m_pDS->exec("CREATE TABLE tag_link(tag_id INTEGER, media_id INTEGER, media_type TEXT)"); + m_pDS->exec("INSERT INTO tag_link(tag_id, media_id, media_type) SELECT DISTINCT idTag, idMedia, media_type FROM taglinks"); + m_pDS->exec("DROP TABLE IF EXISTS taglinks"); + m_pDS->exec("CREATE TABLE tagnew(tag_id INTEGER PRIMARY KEY, name TEXT)"); + m_pDS->exec("INSERT INTO tagnew(tag_id, name) SELECT idTag,strTag FROM tag"); + m_pDS->exec("DROP TABLE IF EXISTS tag"); + m_pDS->exec("ALTER TABLE tagnew RENAME TO tag"); + } + + if (iVersion < 93) + { + // cleanup main tables + std::string valuesSql; + for(int i = 0; i < VIDEODB_MAX_COLUMNS; i++) + { + valuesSql += StringUtils::Format("c{:02} = TRIM(c{:02})", i, i); + if (i < VIDEODB_MAX_COLUMNS - 1) + valuesSql += ","; + } + m_pDS->exec("UPDATE episode SET " + valuesSql); + m_pDS->exec("UPDATE movie SET " + valuesSql); + m_pDS->exec("UPDATE musicvideo SET " + valuesSql); + m_pDS->exec("UPDATE tvshow SET " + valuesSql); + + // cleanup additional tables + std::map<std::string, std::vector<std::string>> additionalTablesMap = { + {"actor", {"actor_link", "director_link", "writer_link"}}, + {"studio", {"studio_link"}}, + {"genre", {"genre_link"}}, + {"country", {"country_link"}}, + {"tag", {"tag_link"}} + }; + for (const auto& additionalTableEntry : additionalTablesMap) + { + std::string table = additionalTableEntry.first; + std::string tablePk = additionalTableEntry.first + "_id"; + std::map<int, std::string> duplicatesMinMap; + std::map<int, std::string> duplicatesMap; + + // cleanup name + m_pDS->exec(PrepareSQL("UPDATE %s SET name = TRIM(name)", + table.c_str())); + + // shrink name to length 255 + m_pDS->exec(PrepareSQL("UPDATE %s SET name = SUBSTR(name, 1, 255) WHERE LENGTH(name) > 255", + table.c_str())); + + // fetch main entries + m_pDS->query(PrepareSQL("SELECT MIN(%s), name FROM %s GROUP BY name HAVING COUNT(1) > 1", + tablePk.c_str(), table.c_str())); + + while (!m_pDS->eof()) + { + duplicatesMinMap.insert(std::make_pair(m_pDS->fv(0).get_asInt(), m_pDS->fv(1).get_asString())); + m_pDS->next(); + } + m_pDS->close(); + + // fetch duplicate entries + for (const auto& entry : duplicatesMinMap) + { + m_pDS->query(PrepareSQL("SELECT %s FROM %s WHERE name = '%s' AND %s <> %i", + tablePk.c_str(), table.c_str(), + entry.second.c_str(), tablePk.c_str(), entry.first)); + + std::stringstream ids; + while (!m_pDS->eof()) + { + int id = m_pDS->fv(0).get_asInt(); + m_pDS->next(); + + ids << id; + if (!m_pDS->eof()) + ids << ","; + } + m_pDS->close(); + + duplicatesMap.insert(std::make_pair(entry.first, ids.str())); + } + + // cleanup duplicates in link tables + for (const auto& subTable : additionalTableEntry.second) + { + // create indexes to speed up things + m_pDS->exec(PrepareSQL("CREATE INDEX ix_%s ON %s (%s)", + subTable.c_str(), subTable.c_str(), tablePk.c_str())); + + // migrate every duplicate entry to the main entry + for (const auto& entry : duplicatesMap) + { + m_pDS->exec(PrepareSQL("UPDATE %s SET %s = %i WHERE %s IN (%s) ", + subTable.c_str(), tablePk.c_str(), entry.first, + tablePk.c_str(), entry.second.c_str())); + } + + // clear all duplicates in the link tables + if (subTable == "actor_link") + { + // as a distinct won't work because of role and cast_order and a group by kills a + // low powered mysql, we de-dupe it with REPLACE INTO while using the real unique index + m_pDS->exec("CREATE TABLE temp_actor_link(actor_id INT, media_id INT, media_type TEXT, role TEXT, cast_order INT)"); + m_pDS->exec("CREATE UNIQUE INDEX ix_temp_actor_link ON temp_actor_link (actor_id, media_type(20), media_id)"); + m_pDS->exec("REPLACE INTO temp_actor_link SELECT * FROM actor_link"); + m_pDS->exec("DROP INDEX ix_temp_actor_link ON temp_actor_link"); + } + else + { + m_pDS->exec(PrepareSQL("CREATE TABLE temp_%s AS SELECT DISTINCT * FROM %s", + subTable.c_str(), subTable.c_str())); + } + + m_pDS->exec(PrepareSQL("DROP TABLE IF EXISTS %s", + subTable.c_str())); + + m_pDS->exec(PrepareSQL("ALTER TABLE temp_%s RENAME TO %s", + subTable.c_str(), subTable.c_str())); + } + + // delete duplicates in main table + for (const auto& entry : duplicatesMap) + { + m_pDS->exec(PrepareSQL("DELETE FROM %s WHERE %s IN (%s)", + table.c_str(), tablePk.c_str(), entry.second.c_str())); + } + } + } + + if (iVersion < 96) + { + m_pDS->exec("ALTER TABLE movie ADD userrating integer"); + m_pDS->exec("ALTER TABLE episode ADD userrating integer"); + m_pDS->exec("ALTER TABLE tvshow ADD userrating integer"); + m_pDS->exec("ALTER TABLE musicvideo ADD userrating integer"); + } + + if (iVersion < 97) + m_pDS->exec("ALTER TABLE sets ADD strOverview TEXT"); + + if (iVersion < 98) + m_pDS->exec("ALTER TABLE seasons ADD name text"); + + if (iVersion < 99) + { + // Add idSeason to episode table, so we don't have to join via idShow and season in the future + m_pDS->exec("ALTER TABLE episode ADD idSeason integer"); + + m_pDS->query("SELECT idSeason, idShow, season FROM seasons"); + while (!m_pDS->eof()) + { + m_pDS2->exec(PrepareSQL("UPDATE episode " + "SET idSeason = %d " + "WHERE " + "episode.idShow = %d AND " + "episode.c%02d = %d", + m_pDS->fv(0).get_asInt(), m_pDS->fv(1).get_asInt(), + VIDEODB_ID_EPISODE_SEASON, m_pDS->fv(2).get_asInt())); + + m_pDS->next(); + } + } + if (iVersion < 101) + m_pDS->exec("ALTER TABLE seasons ADD userrating INTEGER"); + + if (iVersion < 102) + { + m_pDS->exec("CREATE TABLE rating (rating_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, rating_type TEXT, rating FLOAT, votes INTEGER)"); + + std::string sql = PrepareSQL("SELECT DISTINCT idMovie, c%02d, c%02d FROM movie", VIDEODB_ID_RATING_ID, VIDEODB_ID_VOTES); + m_pDS->query(sql); + while (!m_pDS->eof()) + { + m_pDS2->exec(PrepareSQL("INSERT INTO rating(media_id, media_type, rating_type, rating, " + "votes) VALUES (%i, 'movie', 'default', %f, %i)", + m_pDS->fv(0).get_asInt(), + strtod(m_pDS->fv(1).get_asString().c_str(), NULL), + StringUtils::ReturnDigits(m_pDS->fv(2).get_asString()))); + int idRating = (int)m_pDS2->lastinsertid(); + m_pDS2->exec(PrepareSQL("UPDATE movie SET c%02d=%i WHERE idMovie=%i", VIDEODB_ID_RATING_ID, idRating, m_pDS->fv(0).get_asInt())); + m_pDS->next(); + } + m_pDS->close(); + + sql = PrepareSQL("SELECT DISTINCT idShow, c%02d, c%02d FROM tvshow", VIDEODB_ID_TV_RATING_ID, VIDEODB_ID_TV_VOTES); + m_pDS->query(sql); + while (!m_pDS->eof()) + { + m_pDS2->exec(PrepareSQL("INSERT INTO rating(media_id, media_type, rating_type, rating, " + "votes) VALUES (%i, 'tvshow', 'default', %f, %i)", + m_pDS->fv(0).get_asInt(), + strtod(m_pDS->fv(1).get_asString().c_str(), NULL), + StringUtils::ReturnDigits(m_pDS->fv(2).get_asString()))); + int idRating = (int)m_pDS2->lastinsertid(); + m_pDS2->exec(PrepareSQL("UPDATE tvshow SET c%02d=%i WHERE idShow=%i", VIDEODB_ID_TV_RATING_ID, idRating, m_pDS->fv(0).get_asInt())); + m_pDS->next(); + } + m_pDS->close(); + + sql = PrepareSQL("SELECT DISTINCT idEpisode, c%02d, c%02d FROM episode", VIDEODB_ID_EPISODE_RATING_ID, VIDEODB_ID_EPISODE_VOTES); + m_pDS->query(sql); + while (!m_pDS->eof()) + { + m_pDS2->exec(PrepareSQL("INSERT INTO rating(media_id, media_type, rating_type, rating, " + "votes) VALUES (%i, 'episode', 'default', %f, %i)", + m_pDS->fv(0).get_asInt(), + strtod(m_pDS->fv(1).get_asString().c_str(), NULL), + StringUtils::ReturnDigits(m_pDS->fv(2).get_asString()))); + int idRating = (int)m_pDS2->lastinsertid(); + m_pDS2->exec(PrepareSQL("UPDATE episode SET c%02d=%i WHERE idEpisode=%i", VIDEODB_ID_EPISODE_RATING_ID, idRating, m_pDS->fv(0).get_asInt())); + m_pDS->next(); + } + m_pDS->close(); + } + + if (iVersion < 103) + { + m_pDS->exec("ALTER TABLE settings ADD VideoStream integer"); + m_pDS->exec("ALTER TABLE streamdetails ADD strVideoLanguage text"); + } + + if (iVersion < 104) + { + m_pDS->exec("ALTER TABLE tvshow ADD duration INTEGER"); + + std::string sql = PrepareSQL( "SELECT episode.idShow, MAX(episode.c%02d) " + "FROM episode " + + "LEFT JOIN streamdetails " + "ON streamdetails.idFile = episode.idFile " + "AND streamdetails.iStreamType = 0 " // only grab video streams + + "WHERE episode.c%02d <> streamdetails.iVideoDuration " + "OR streamdetails.iVideoDuration IS NULL " + "GROUP BY episode.idShow", VIDEODB_ID_EPISODE_RUNTIME, VIDEODB_ID_EPISODE_RUNTIME); + + m_pDS->query(sql); + while (!m_pDS->eof()) + { + m_pDS2->exec(PrepareSQL("UPDATE tvshow SET duration=%i WHERE idShow=%i", m_pDS->fv(1).get_asInt(), m_pDS->fv(0).get_asInt())); + m_pDS->next(); + } + m_pDS->close(); + } + + if (iVersion < 105) + { + m_pDS->exec("ALTER TABLE movie ADD premiered TEXT"); + m_pDS->exec(PrepareSQL("UPDATE movie SET premiered=c%02d", VIDEODB_ID_YEAR)); + m_pDS->exec("ALTER TABLE musicvideo ADD premiered TEXT"); + m_pDS->exec(PrepareSQL("UPDATE musicvideo SET premiered=c%02d", VIDEODB_ID_MUSICVIDEO_YEAR)); + } + + if (iVersion < 107) + { + // need this due to the nested GetScraperPath query + std::unique_ptr<Dataset> pDS; + pDS.reset(m_pDB->CreateDataset()); + if (nullptr == pDS) + return; + + pDS->exec("CREATE TABLE uniqueid (uniqueid_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, value TEXT, type TEXT)"); + + for (int i = 0; i < 3; ++i) + { + std::string mediatype, columnID; + int columnUniqueID; + switch (i) + { + case (0): + mediatype = "movie"; + columnID = "idMovie"; + columnUniqueID = VIDEODB_ID_IDENT_ID; + break; + case (1): + mediatype = "tvshow"; + columnID = "idShow"; + columnUniqueID = VIDEODB_ID_TV_IDENT_ID; + break; + case (2): + mediatype = "episode"; + columnID = "idEpisode"; + columnUniqueID = VIDEODB_ID_EPISODE_IDENT_ID; + break; + default: + continue; + } + pDS->query(PrepareSQL("SELECT %s, c%02d FROM %s", columnID.c_str(), columnUniqueID, mediatype.c_str())); + while (!pDS->eof()) + { + std::string uniqueid = pDS->fv(1).get_asString(); + if (!uniqueid.empty()) + { + int mediaid = pDS->fv(0).get_asInt(); + if (StringUtils::StartsWith(uniqueid, "tt")) + m_pDS2->exec(PrepareSQL("INSERT INTO uniqueid(media_id, media_type, type, value) VALUES (%i, '%s', 'imdb', '%s')", mediaid, mediatype.c_str(), uniqueid.c_str())); + else + m_pDS2->exec(PrepareSQL("INSERT INTO uniqueid(media_id, media_type, type, value) VALUES (%i, '%s', 'unknown', '%s')", mediaid, mediatype.c_str(), uniqueid.c_str())); + m_pDS2->exec(PrepareSQL("UPDATE %s SET c%02d='%i' WHERE %s=%i", mediatype.c_str(), columnUniqueID, (int)m_pDS2->lastinsertid(), columnID.c_str(), mediaid)); + } + pDS->next(); + } + pDS->close(); + } + } + + if (iVersion < 109) + { + m_pDS->exec("ALTER TABLE settings RENAME TO settingsold"); + m_pDS->exec("CREATE TABLE settings ( idFile integer, Deinterlace bool," + "ViewMode integer,ZoomAmount float, PixelRatio float, VerticalShift float, AudioStream integer, SubtitleStream integer," + "SubtitleDelay float, SubtitlesOn bool, Brightness float, Contrast float, Gamma float," + "VolumeAmplification float, AudioDelay float, ResumeTime integer," + "Sharpness float, NoiseReduction float, NonLinStretch bool, PostProcess bool," + "ScalingMethod integer, DeinterlaceMode integer, StereoMode integer, StereoInvert bool, VideoStream integer)"); + m_pDS->exec("INSERT INTO settings SELECT idFile, Deinterlace, ViewMode, ZoomAmount, PixelRatio, VerticalShift, AudioStream, SubtitleStream, SubtitleDelay, SubtitlesOn, Brightness, Contrast, Gamma, VolumeAmplification, AudioDelay, ResumeTime, Sharpness, NoiseReduction, NonLinStretch, PostProcess, ScalingMethod, DeinterlaceMode, StereoMode, StereoInvert, VideoStream FROM settingsold"); + m_pDS->exec("DROP TABLE settingsold"); + } + + if (iVersion < 110) + { + m_pDS->exec("ALTER TABLE settings ADD TonemapMethod integer"); + m_pDS->exec("ALTER TABLE settings ADD TonemapParam float"); + } + + if (iVersion < 111) + m_pDS->exec("ALTER TABLE settings ADD Orientation integer"); + + if (iVersion < 112) + m_pDS->exec("ALTER TABLE settings ADD CenterMixLevel integer"); + + if (iVersion < 113) + { + // fb9c25f5 and e5f6d204 changed the behavior of path splitting for plugin URIs (previously it would only use the root) + // Re-split paths for plugin files in order to maintain watched state etc. + m_pDS->query("SELECT files.idFile, files.strFilename, path.strPath FROM files LEFT JOIN path ON files.idPath = path.idPath WHERE files.strFilename LIKE 'plugin://%'"); + while (!m_pDS->eof()) + { + std::string path, fn; + SplitPath(m_pDS->fv(1).get_asString(), path, fn); + if (path != m_pDS->fv(2).get_asString()) + { + int pathid = -1; + m_pDS2->query(PrepareSQL("SELECT idPath FROM path WHERE strPath='%s'", path.c_str())); + if (!m_pDS2->eof()) + pathid = m_pDS2->fv(0).get_asInt(); + m_pDS2->close(); + if (pathid < 0) + { + std::string parent = URIUtils::GetParentPath(path); + int parentid = -1; + m_pDS2->query(PrepareSQL("SELECT idPath FROM path WHERE strPath='%s'", parent.c_str())); + if (!m_pDS2->eof()) + parentid = m_pDS2->fv(0).get_asInt(); + m_pDS2->close(); + if (parentid < 0) + { + m_pDS2->exec(PrepareSQL("INSERT INTO path (strPath) VALUES ('%s')", parent.c_str())); + parentid = (int)m_pDS2->lastinsertid(); + } + m_pDS2->exec(PrepareSQL("INSERT INTO path (strPath, idParentPath) VALUES ('%s', %i)", path.c_str(), parentid)); + pathid = (int)m_pDS2->lastinsertid(); + } + m_pDS2->query(PrepareSQL("SELECT idFile FROM files WHERE strFileName='%s' AND idPath=%i", fn.c_str(), pathid)); + bool exists = !m_pDS2->eof(); + m_pDS2->close(); + if (exists) + m_pDS2->exec(PrepareSQL("DELETE FROM files WHERE idFile=%i", m_pDS->fv(0).get_asInt())); + else + m_pDS2->exec(PrepareSQL("UPDATE files SET idPath=%i WHERE idFile=%i", pathid, m_pDS->fv(0).get_asInt())); + } + m_pDS->next(); + } + m_pDS->close(); + } + + if (iVersion < 119) + m_pDS->exec("ALTER TABLE path ADD allAudio bool"); + + if (iVersion < 120) + m_pDS->exec("ALTER TABLE streamdetails ADD strHdrType text"); + + if (iVersion < 121) + { + // https://github.com/xbmc/xbmc/issues/21253 - Kodi picks up wrong "year" for PVR recording. + + m_pDS->query("SELECT idFile, strFilename FROM files WHERE strFilename LIKE '% (1969)%.pvr' OR " + "strFilename LIKE '% (1601)%.pvr'"); + while (!m_pDS->eof()) + { + std::string fixedFileName = m_pDS->fv(1).get_asString(); + size_t pos = fixedFileName.find(" (1969)"); + if (pos == std::string::npos) + pos = fixedFileName.find(" (1601)"); + + if (pos != std::string::npos) + { + fixedFileName.erase(pos, 7); + + m_pDS2->exec(PrepareSQL("UPDATE files SET strFilename='%s' WHERE idFile=%i", + fixedFileName.c_str(), m_pDS->fv(0).get_asInt())); + } + m_pDS->next(); + } + m_pDS->close(); + } +} + +int CVideoDatabase::GetSchemaVersion() const +{ + return 121; +} + +bool CVideoDatabase::LookupByFolders(const std::string &path, bool shows) +{ + SScanSettings settings; + bool foundDirectly = false; + ScraperPtr scraper = GetScraperForPath(path, settings, foundDirectly); + if (scraper && scraper->Content() == CONTENT_TVSHOWS && !shows) + return false; // episodes + return settings.parent_name_root; // shows, movies, musicvids +} + +bool CVideoDatabase::GetPlayCounts(const std::string &strPath, CFileItemList &items) +{ + if(URIUtils::IsMultiPath(strPath)) + { + std::vector<std::string> paths; + CMultiPathDirectory::GetPaths(strPath, paths); + + bool ret = false; + for(unsigned i=0;i<paths.size();i++) + ret |= GetPlayCounts(paths[i], items); + + return ret; + } + int pathID = -1; + if (!URIUtils::IsPlugin(strPath)) + { + pathID = GetPathId(strPath); + if (pathID < 0) + return false; // path (and thus files) aren't in the database + } + + try + { + // error! + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string sql = + "SELECT" + " files.strFilename, files.playCount," + " bookmark.timeInSeconds, bookmark.totalTimeInSeconds " + "FROM files" + " LEFT JOIN bookmark ON" + " files.idFile = bookmark.idFile AND bookmark.type = %i "; + + if (URIUtils::IsPlugin(strPath)) + { + for (auto& item : items) + { + if (!item || item->m_bIsFolder || !item->GetProperty("IsPlayable").asBoolean()) + continue; + + std::string path, filename; + SplitPath(item->GetPath(), path, filename); + m_pDS->query(PrepareSQL(sql + + "INNER JOIN path ON files.idPath = path.idPath " + "WHERE files.strFilename='%s' AND path.strPath='%s'", + (int)CBookmark::RESUME, filename.c_str(), path.c_str())); + + if (!m_pDS->eof()) + { + if (!item->GetVideoInfoTag()->IsPlayCountSet()) + item->GetVideoInfoTag()->SetPlayCount(m_pDS->fv(1).get_asInt()); + if (!item->GetVideoInfoTag()->GetResumePoint().IsSet()) + item->GetVideoInfoTag()->SetResumePoint(m_pDS->fv(2).get_asInt(), m_pDS->fv(3).get_asInt(), ""); + } + m_pDS->close(); + } + } + else + { + //! @todo also test a single query for the above and below + sql = PrepareSQL(sql + "WHERE files.idPath=%i", (int)CBookmark::RESUME, pathID); + + if (RunQuery(sql) <= 0) + return false; + + items.SetFastLookup(true); // note: it's possibly quicker the other way around (map on db returned items)? + while (!m_pDS->eof()) + { + std::string path; + ConstructPath(path, strPath, m_pDS->fv(0).get_asString()); + CFileItemPtr item = items.Get(path); + if (item) + { + if (!items.IsPlugin() || !item->GetVideoInfoTag()->IsPlayCountSet()) + item->GetVideoInfoTag()->SetPlayCount(m_pDS->fv(1).get_asInt()); + + if (!item->GetVideoInfoTag()->GetResumePoint().IsSet()) + { + item->GetVideoInfoTag()->SetResumePoint(m_pDS->fv(2).get_asInt(), m_pDS->fv(3).get_asInt(), ""); + } + } + m_pDS->next(); + } + } + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +int CVideoDatabase::GetPlayCount(int iFileId) +{ + if (iFileId < 0) + return 0; // not in db, so not watched + + try + { + // error! + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string strSQL = PrepareSQL("select playCount from files WHERE idFile=%i", iFileId); + int count = 0; + if (m_pDS->query(strSQL)) + { + // there should only ever be one row returned + if (m_pDS->num_rows() == 1) + count = m_pDS->fv(0).get_asInt(); + m_pDS->close(); + } + return count; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +int CVideoDatabase::GetPlayCount(const std::string& strFilenameAndPath) +{ + return GetPlayCount(GetFileId(strFilenameAndPath)); +} + +int CVideoDatabase::GetPlayCount(const CFileItem &item) +{ + return GetPlayCount(GetFileId(item)); +} + +CDateTime CVideoDatabase::GetLastPlayed(int iFileId) +{ + if (iFileId < 0) + return {}; // not in db, so not watched + + try + { + // error! + if (nullptr == m_pDB) + return {}; + if (nullptr == m_pDS) + return {}; + + std::string strSQL = PrepareSQL("select lastPlayed from files WHERE idFile=%i", iFileId); + CDateTime lastPlayed; + if (m_pDS->query(strSQL)) + { + // there should only ever be one row returned + if (m_pDS->num_rows() == 1) + lastPlayed.SetFromDBDateTime(m_pDS->fv(0).get_asString()); + m_pDS->close(); + } + return lastPlayed; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return {}; +} + +CDateTime CVideoDatabase::GetLastPlayed(const std::string& strFilenameAndPath) +{ + return GetLastPlayed(GetFileId(strFilenameAndPath)); +} + +void CVideoDatabase::UpdateFanart(const CFileItem& item, VideoDbContentType type) +{ + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + if (!item.HasVideoInfoTag() || item.GetVideoInfoTag()->m_iDbId < 0) return; + + std::string exec; + if (type == VideoDbContentType::TVSHOWS) + exec = PrepareSQL("UPDATE tvshow set c%02d='%s' WHERE idShow=%i", VIDEODB_ID_TV_FANART, item.GetVideoInfoTag()->m_fanart.m_xml.c_str(), item.GetVideoInfoTag()->m_iDbId); + else if (type == VideoDbContentType::MOVIES) + exec = PrepareSQL("UPDATE movie set c%02d='%s' WHERE idMovie=%i", VIDEODB_ID_FANART, item.GetVideoInfoTag()->m_fanart.m_xml.c_str(), item.GetVideoInfoTag()->m_iDbId); + + try + { + m_pDS->exec(exec); + + if (type == VideoDbContentType::TVSHOWS) + AnnounceUpdate(MediaTypeTvShow, item.GetVideoInfoTag()->m_iDbId); + else if (type == VideoDbContentType::MOVIES) + AnnounceUpdate(MediaTypeMovie, item.GetVideoInfoTag()->m_iDbId); + } + catch (...) + { + CLog::Log(LOGERROR, "{} - error updating fanart for {}", __FUNCTION__, item.GetPath()); + } +} + +CDateTime CVideoDatabase::SetPlayCount(const CFileItem& item, int count, const CDateTime& date) +{ + int id; + if (item.HasProperty("original_listitem_url") && + URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString())) + { + CFileItem item2(item); + item2.SetPath(item.GetProperty("original_listitem_url").asString()); + id = AddFile(item2); + } + else + id = AddFile(item); + if (id < 0) + return {}; + + // and mark as watched + try + { + const CDateTime lastPlayed(date.IsValid() ? date : CDateTime::GetCurrentDateTime()); + + if (nullptr == m_pDB) + return {}; + if (nullptr == m_pDS) + return {}; + + std::string strSQL; + if (count) + { + strSQL = PrepareSQL("update files set playCount=%i,lastPlayed='%s' where idFile=%i", count, + lastPlayed.GetAsDBDateTime().c_str(), id); + } + else + { + if (!date.IsValid()) + strSQL = PrepareSQL("update files set playCount=NULL,lastPlayed=NULL where idFile=%i", id); + else + strSQL = PrepareSQL("update files set playCount=NULL,lastPlayed='%s' where idFile=%i", + lastPlayed.GetAsDBDateTime().c_str(), id); + } + + m_pDS->exec(strSQL); + + // We only need to announce changes to video items in the library + if (item.HasVideoInfoTag() && item.GetVideoInfoTag()->m_iDbId > 0) + { + CVariant data; + if (CVideoLibraryQueue::GetInstance().IsScanningLibrary()) + data["transaction"] = true; + // Only provide the "playcount" value if it has actually changed + if (item.GetVideoInfoTag()->GetPlayCount() != count) + data["playcount"] = count; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate", + CFileItemPtr(new CFileItem(item)), data); + } + + return lastPlayed; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + + return {}; +} + +CDateTime CVideoDatabase::IncrementPlayCount(const CFileItem& item) +{ + return SetPlayCount(item, GetPlayCount(item) + 1); +} + +CDateTime CVideoDatabase::UpdateLastPlayed(const CFileItem& item) +{ + return SetPlayCount(item, GetPlayCount(item), CDateTime::GetCurrentDateTime()); +} + +void CVideoDatabase::UpdateMovieTitle(int idMovie, + const std::string& strNewMovieTitle, + VideoDbContentType iType) +{ + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + std::string content; + if (iType == VideoDbContentType::MOVIES) + { + CLog::Log(LOGINFO, "Changing Movie:id:{} New Title:{}", idMovie, strNewMovieTitle); + content = MediaTypeMovie; + } + else if (iType == VideoDbContentType::EPISODES) + { + CLog::Log(LOGINFO, "Changing Episode:id:{} New Title:{}", idMovie, strNewMovieTitle); + content = MediaTypeEpisode; + } + else if (iType == VideoDbContentType::TVSHOWS) + { + CLog::Log(LOGINFO, "Changing TvShow:id:{} New Title:{}", idMovie, strNewMovieTitle); + content = MediaTypeTvShow; + } + else if (iType == VideoDbContentType::MUSICVIDEOS) + { + CLog::Log(LOGINFO, "Changing MusicVideo:id:{} New Title:{}", idMovie, strNewMovieTitle); + content = MediaTypeMusicVideo; + } + else if (iType == VideoDbContentType::MOVIE_SETS) + { + CLog::Log(LOGINFO, "Changing Movie set:id:{} New Title:{}", idMovie, strNewMovieTitle); + std::string strSQL = PrepareSQL("UPDATE sets SET strSet='%s' WHERE idSet=%i", strNewMovieTitle.c_str(), idMovie ); + m_pDS->exec(strSQL); + } + + if (!content.empty()) + { + SetSingleValue(iType, idMovie, FieldTitle, strNewMovieTitle); + AnnounceUpdate(content, idMovie); + } + } + catch (...) + { + CLog::Log( + LOGERROR, + "{} (int idMovie, const std::string& strNewMovieTitle) failed on MovieID:{} and Title:{}", + __FUNCTION__, idMovie, strNewMovieTitle); + } +} + +bool CVideoDatabase::UpdateVideoSortTitle(int idDb, + const std::string& strNewSortTitle, + VideoDbContentType iType /* = MOVIES */) +{ + try + { + if (nullptr == m_pDB || nullptr == m_pDS) + return false; + if (iType != VideoDbContentType::MOVIES && iType != VideoDbContentType::TVSHOWS) + return false; + + std::string content = MediaTypeMovie; + if (iType == VideoDbContentType::TVSHOWS) + content = MediaTypeTvShow; + + if (SetSingleValue(iType, idDb, FieldSortTitle, strNewSortTitle)) + { + AnnounceUpdate(content, idDb); + return true; + } + } + catch (...) + { + CLog::Log(LOGERROR, + "{} (int idDb, const std::string& strNewSortTitle, VIDEODB_CONTENT_TYPE iType) " + "failed on ID: {} and Sort Title: {}", + __FUNCTION__, idDb, strNewSortTitle); + } + + return false; +} + +/// \brief EraseVideoSettings() Erases the videoSettings table and reconstructs it +void CVideoDatabase::EraseVideoSettings(const CFileItem &item) +{ + int idFile = GetFileId(item); + if (idFile < 0) + return; + + try + { + std::string sql = PrepareSQL("DELETE FROM settings WHERE idFile=%i", idFile); + + CLog::Log(LOGINFO, "Deleting settings information for files {}", + CURL::GetRedacted(item.GetPath())); + m_pDS->exec(sql); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } +} + +void CVideoDatabase::EraseAllVideoSettings() +{ + try + { + std::string sql = "DELETE FROM settings"; + + CLog::Log(LOGINFO, "Deleting all video settings"); + m_pDS->exec(sql); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } +} + +void CVideoDatabase::EraseAllVideoSettings(const std::string& path) +{ + std::string itemsToDelete; + + try + { + std::string sql = PrepareSQL("SELECT files.idFile FROM files WHERE idFile IN (SELECT idFile FROM files INNER JOIN path ON path.idPath = files.idPath AND path.strPath LIKE \"%s%%\")", path.c_str()); + m_pDS->query(sql); + while (!m_pDS->eof()) + { + std::string file = m_pDS->fv("files.idFile").get_asString() + ","; + itemsToDelete += file; + m_pDS->next(); + } + m_pDS->close(); + + if (!itemsToDelete.empty()) + { + itemsToDelete = "(" + StringUtils::TrimRight(itemsToDelete, ",") + ")"; + + sql = "DELETE FROM settings WHERE idFile IN " + itemsToDelete; + m_pDS->exec(sql); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } +} + +bool CVideoDatabase::GetGenresNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetNavCommon(strBaseDir, items, "genre", idContent, filter, countOnly); +} + +bool CVideoDatabase::GetCountriesNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetNavCommon(strBaseDir, items, "country", idContent, filter, countOnly); +} + +bool CVideoDatabase::GetStudiosNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetNavCommon(strBaseDir, items, "studio", idContent, filter, countOnly); +} + +bool CVideoDatabase::GetNavCommon(const std::string& strBaseDir, + CFileItemList& items, + const char* type, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + Filter extFilter = filter; + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + { + std::string view, view_id, media_type, extraField, extraJoin; + if (idContent == VideoDbContentType::MOVIES) + { + view = MediaTypeMovie; + view_id = "idMovie"; + media_type = MediaTypeMovie; + extraField = "files.playCount"; + } + else if (idContent == VideoDbContentType::TVSHOWS) //this will not get tvshows with 0 episodes + { + view = MediaTypeEpisode; + view_id = "idShow"; + media_type = MediaTypeTvShow; + // in order to make use of FieldPlaycount in smart playlists we need an extra join + if (StringUtils::EqualsNoCase(type, "tag")) + extraJoin = PrepareSQL("JOIN tvshow_view ON tvshow_view.idShow = tag_link.media_id AND tag_link.media_type='tvshow'"); + } + else if (idContent == VideoDbContentType::MUSICVIDEOS) + { + view = MediaTypeMusicVideo; + view_id = "idMVideo"; + media_type = MediaTypeMusicVideo; + extraField = "files.playCount"; + } + else + return false; + + strSQL = "SELECT {} " + PrepareSQL("FROM %s ", type); + extFilter.fields = PrepareSQL("%s.%s_id, %s.name, path.strPath", type, type, type); + extFilter.AppendField(extraField); + extFilter.AppendJoin(PrepareSQL("JOIN %s_link ON %s.%s_id = %s_link.%s_id", type, type, type, type, type)); + extFilter.AppendJoin(PrepareSQL("JOIN %s_view ON %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'", view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str())); + extFilter.AppendJoin(PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str())); + extFilter.AppendJoin("JOIN path ON path.idPath = files.idPath"); + extFilter.AppendJoin(extraJoin); + } + else + { + std::string view, view_id, media_type, extraField, extraJoin; + if (idContent == VideoDbContentType::MOVIES) + { + view = MediaTypeMovie; + view_id = "idMovie"; + media_type = MediaTypeMovie; + extraField = "count(1), count(files.playCount)"; + extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str()); + } + else if (idContent == VideoDbContentType::TVSHOWS) + { + view = MediaTypeTvShow; + view_id = "idShow"; + media_type = MediaTypeTvShow; + } + else if (idContent == VideoDbContentType::MUSICVIDEOS) + { + view = MediaTypeMusicVideo; + view_id = "idMVideo"; + media_type = MediaTypeMusicVideo; + extraField = "count(1), count(files.playCount)"; + extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str()); + } + else + return false; + + strSQL = "SELECT {} " + PrepareSQL("FROM %s ", type); + extFilter.fields = PrepareSQL("%s.%s_id, %s.name", type, type, type); + extFilter.AppendField(extraField); + extFilter.AppendJoin(PrepareSQL("JOIN %s_link ON %s.%s_id = %s_link.%s_id", type, type, type, type, type)); + extFilter.AppendJoin(PrepareSQL("JOIN %s_view ON %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'", + view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str())); + extFilter.AppendJoin(extraJoin); + extFilter.AppendGroup(PrepareSQL("%s.%s_id", type, type)); + } + + if (countOnly) + { + extFilter.fields = PrepareSQL("COUNT(DISTINCT %s.%s_id)", type, type); + extFilter.group.clear(); + extFilter.order.clear(); + } + strSQL = StringUtils::Format(strSQL, !extFilter.fields.empty() ? extFilter.fields : "*"); + + CVideoDbUrl videoUrl; + if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, videoUrl)) + return false; + + int iRowsFound = RunQuery(strSQL); + if (iRowsFound <= 0) + return iRowsFound == 0; + + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + { + std::map<int, std::pair<std::string,int> > mapItems; + while (!m_pDS->eof()) + { + int id = m_pDS->fv(0).get_asInt(); + std::string str = m_pDS->fv(1).get_asString(); + + // was this already found? + auto it = mapItems.find(id); + if (it == mapItems.end()) + { + // check path + if (g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv(2).get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + if (idContent == VideoDbContentType::MOVIES || + idContent == VideoDbContentType::MUSICVIDEOS) + mapItems.insert(std::pair<int, std::pair<std::string,int> >(id, std::pair<std::string, int>(str,m_pDS->fv(3).get_asInt()))); //fv(3) is file.playCount + else if (idContent == VideoDbContentType::TVSHOWS) + mapItems.insert(std::pair<int, std::pair<std::string,int> >(id, std::pair<std::string,int>(str,0))); + } + } + m_pDS->next(); + } + m_pDS->close(); + + for (const auto &i : mapItems) + { + CFileItemPtr pItem(new CFileItem(i.second.first)); + pItem->GetVideoInfoTag()->m_iDbId = i.first; + pItem->GetVideoInfoTag()->m_type = type; + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", i.first); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS) + pItem->GetVideoInfoTag()->SetPlayCount(i.second.second); + if (!items.Contains(pItem->GetPath())) + { + pItem->SetLabelPreformatted(true); + items.Add(pItem); + } + } + } + else + { + while (!m_pDS->eof()) + { + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + pItem->GetVideoInfoTag()->m_iDbId = m_pDS->fv(0).get_asInt(); + pItem->GetVideoInfoTag()->m_type = type; + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + pItem->SetLabelPreformatted(true); + if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS) + { // fv(3) is the number of videos watched, fv(2) is the total number. We set the playcount + // only if the number of videos watched is equal to the total number (i.e. every video watched) + pItem->GetVideoInfoTag()->SetPlayCount((m_pDS->fv(3).get_asInt() == m_pDS->fv(2).get_asInt()) ? 1 : 0); + } + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetTagsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetNavCommon(strBaseDir, items, "tag", idContent, filter, countOnly); +} + +bool CVideoDatabase::GetSetsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool ignoreSingleMovieSets /* = false */) +{ + if (idContent != VideoDbContentType::MOVIES) + return false; + + return GetSetsByWhere(strBaseDir, filter, items, ignoreSingleMovieSets); +} + +bool CVideoDatabase::GetSetsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool ignoreSingleMovieSets /* = false */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + Filter setFilter = filter; + setFilter.join += " JOIN sets ON movie_view.idSet = sets.idSet"; + if (!setFilter.order.empty()) + setFilter.order += ","; + setFilter.order += "sets.idSet"; + + if (!GetMoviesByWhere(strBaseDir, setFilter, items)) + return false; + + CFileItemList sets; + GroupAttribute groupingAttributes; + const CUrlOptions::UrlOptions& options = videoUrl.GetOptions(); + auto option = options.find("ignoreSingleMovieSets"); + + if (option != options.end()) + { + groupingAttributes = + option->second.asBoolean() ? GroupAttributeIgnoreSingleItems : GroupAttributeNone; + } + else + { + groupingAttributes = + ignoreSingleMovieSets ? GroupAttributeIgnoreSingleItems : GroupAttributeNone; + } + + if (!GroupUtils::Group(GroupBySet, strBaseDir, items, sets, groupingAttributes)) + return false; + + items.ClearItems(); + items.Append(sets); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetMusicVideoAlbumsNav(const std::string& strBaseDir, CFileItemList& items, int idArtist /* = -1 */, const Filter &filter /* = Filter() */, bool countOnly /* = false */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + std::string strSQL = "select {} from musicvideo_view "; + Filter extFilter = filter; + extFilter.fields = PrepareSQL("musicvideo_view.c%02d, musicvideo_view.idMVideo, actor.name, " + "musicvideo_view.c%02d, musicvideo_view.c%02d, musicvideo_view.c%02d ", + VIDEODB_ID_MUSICVIDEO_ALBUM, VIDEODB_ID_MUSICVIDEO_TITLE, + VIDEODB_ID_MUSICVIDEO_PLOT, VIDEODB_ID_MUSICVIDEO_ARTIST); + extFilter.AppendJoin( + PrepareSQL("JOIN actor_link ON actor_link.media_id=musicvideo_view.idMVideo ")); + extFilter.AppendJoin(PrepareSQL("JOIN actor ON actor.actor_id = actor_link.actor_id")); + extFilter.fields += ", path.strPath"; + extFilter.AppendJoin("join files on files.idFile = musicvideo_view.idFile join path on path.idPath = files.idPath"); + + if (StringUtils::EndsWith(strBaseDir,"albums/")) + extFilter.AppendWhere(PrepareSQL("musicvideo_view.c%02d != ''", VIDEODB_ID_MUSICVIDEO_ALBUM)); + + if (idArtist > -1) + videoUrl.AddOption("artistid", idArtist); + + extFilter.AppendGroup(PrepareSQL(" CASE WHEN musicvideo_view.c09 !='' THEN musicvideo_view.c09 " + "ELSE musicvideo_view.c00 END")); + + if (countOnly) + { + extFilter.fields = "COUNT(1)"; + extFilter.group.clear(); + extFilter.order.clear(); + } + strSQL = StringUtils::Format(strSQL, !extFilter.fields.empty() ? extFilter.fields : "*"); + + if (!BuildSQL(videoUrl.ToString(), strSQL, extFilter, strSQL, videoUrl)) + return false; + + int iRowsFound = RunQuery(strSQL); + /* fields returned by query are :- + (0) - Album title (if any) + (1) - idMVideo + (2) - Artist name + (3) - Music video title + (4) - Music video plot + (5) - Music Video artist + (6) - Path to video + */ + if (iRowsFound <= 0) + return iRowsFound == 0; + + std::string strArtist; + if (idArtist> -1) + strArtist = m_pDS->fv("actor.name").get_asString(); + + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + std::list <int> idMVideoList; + std::list <std::pair<std::string, std::string>> idData; + + while (!m_pDS->eof()) + { + bool isAlbum = true; + std::string strAlbum = m_pDS->fv(0).get_asString(); //Album title + int idMVideo = m_pDS->fv(1).get_asInt(); + if (strAlbum.empty()) + { + strAlbum = m_pDS->fv(3).get_asString(); // video title if not an album + isAlbum = false; + } + + CFileItemPtr pItem(new CFileItem(strAlbum)); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", idMVideo); + if (!isAlbum) + { + itemUrl.AddOption("albumid", idMVideo); + path += std::to_string(idMVideo); + + strSQL = PrepareSQL( + "SELECT type, url FROM art WHERE media_id = %i AND media_type = 'musicvideo'", + idMVideo); + m_pDS2->query(strSQL); + while (!m_pDS2->eof()) + { + pItem->SetArt(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()); + m_pDS2->next(); + } + m_pDS2->close(); + } + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + pItem->m_bIsFolder = isAlbum; + pItem->SetLabelPreformatted(true); + + if (!items.Contains(pItem->GetPath())) + if (g_passwordManager.IsDatabasePathUnlocked( + m_pDS->fv("path.strPath").get_asString(), + *CMediaSourceSettings::GetInstance().GetSources("video"))) + { + pItem->GetVideoInfoTag()->m_artist.emplace_back(strArtist); + pItem->GetVideoInfoTag()->m_iDbId = idMVideo; + items.Add(pItem); + idMVideoList.push_back(idMVideo); + idData.push_back(make_pair(m_pDS->fv(0).get_asString(), m_pDS->fv(5).get_asString())); + } + m_pDS->next(); + } + m_pDS->close(); + + for (int i = 0; i < items.Size(); i++) + { + CVideoInfoTag details; + + if (items[i]->m_bIsFolder) + { + details.SetPath(items[i]->GetPath()); + details.m_strAlbum = idData.front().first; + details.m_type = MediaTypeAlbum; + details.m_artist.emplace_back(idData.front().second); + details.m_iDbId = idMVideoList.front(); + items[i]->SetProperty("musicvideomediatype", MediaTypeAlbum); + items[i]->SetLabel(idData.front().first); + items[i]->SetFromVideoInfoTag(details); + + idMVideoList.pop_front(); + idData.pop_front(); + continue; + } + else + { + GetMusicVideoInfo("", details, idMVideoList.front()); + items[i]->SetFromVideoInfoTag(details); + idMVideoList.pop_front(); + idData.pop_front(); + } + } + + if (!strArtist.empty()) + items.SetProperty("customtitle",strArtist); // change displayed path from eg /23 to /Artist + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetWritersNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetPeopleNav(strBaseDir, items, "writer", idContent, filter, countOnly); +} + +bool CVideoDatabase::GetDirectorsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + return GetPeopleNav(strBaseDir, items, "director", idContent, filter, countOnly); +} + +bool CVideoDatabase::GetActorsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + if (GetPeopleNav(strBaseDir, items, "actor", idContent, filter, countOnly)) + { // set thumbs - ideally this should be in the normal thumb setting routines + for (int i = 0; i < items.Size() && !countOnly; i++) + { + CFileItemPtr pItem = items[i]; + if (idContent == VideoDbContentType::MUSICVIDEOS) + pItem->SetArt("icon", "DefaultArtist.png"); + else + pItem->SetArt("icon", "DefaultActor.png"); + } + return true; + } + return false; +} + +bool CVideoDatabase::GetPeopleNav(const std::string& strBaseDir, + CFileItemList& items, + const char* type, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */, + bool countOnly /* = false */) +{ + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + try + { + //! @todo This routine (and probably others at this same level) use playcount as a reference to filter on at a later + //! point. This means that we *MUST* filter these levels as you'll get double ups. Ideally we'd allow playcount + //! to filter through as we normally do for tvshows to save this happening. + //! Also, we apply this same filtering logic to the locked or unlocked paths to prevent these from showing. + //! Whether or not this should happen is a tricky one - it complicates all the high level categories (everything + //! above titles). + + // General routine that the other actor/director/writer routines call + + // get primary genres for movies + std::string strSQL; + bool bMainArtistOnly = false; + Filter extFilter = filter; + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + { + std::string view, view_id, media_type, extraField, extraJoin, group; + if (idContent == VideoDbContentType::MOVIES) + { + view = MediaTypeMovie; + view_id = "idMovie"; + media_type = MediaTypeMovie; + extraField = "files.playCount"; + } + else if (idContent == VideoDbContentType::TVSHOWS) + { + view = MediaTypeEpisode; + view_id = "idShow"; + media_type = MediaTypeTvShow; + extraField = "count(DISTINCT idShow)"; + group = "actor.actor_id"; + } + else if (idContent == VideoDbContentType::EPISODES) + { + view = MediaTypeEpisode; + view_id = "idEpisode"; + media_type = MediaTypeEpisode; + extraField = "files.playCount"; + } + else if (idContent == VideoDbContentType::MUSICVIDEOS) + { + bMainArtistOnly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_VIDEOLIBRARY_SHOWPERFORMERS); + if (StringUtils::EndsWith(strBaseDir, "directors/")) + // only set this to true if getting artists and show all performers is false + bMainArtistOnly = false; + view = MediaTypeMusicVideo; + view_id = "idMVideo"; + media_type = MediaTypeMusicVideo; + extraField = "count(1), count(files.playCount)"; + if (bMainArtistOnly) + extraJoin = + PrepareSQL(" WHERE actor.name IN (SELECT musicvideo_view.c10 from musicvideo_view)"); + group = "actor.actor_id"; + } + else + return false; + + strSQL = "SELECT {} FROM actor "; + extFilter.fields = "actor.actor_id, actor.name, actor.art_urls, path.strPath"; + extFilter.AppendField(extraField); + extFilter.AppendJoin(PrepareSQL("JOIN %s_link ON actor.actor_id = %s_link.actor_id", type, type)); + extFilter.AppendJoin(PrepareSQL("JOIN %s_view ON %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'", view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str())); + extFilter.AppendJoin(PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str())); + extFilter.AppendJoin("JOIN path ON path.idPath = files.idPath"); + extFilter.AppendJoin(extraJoin); + extFilter.AppendGroup(group); + } + else + { + std::string view, view_id, media_type, extraField, extraJoin; + if (idContent == VideoDbContentType::MOVIES) + { + view = MediaTypeMovie; + view_id = "idMovie"; + media_type = MediaTypeMovie; + extraField = "count(1), count(files.playCount)"; + extraJoin = PrepareSQL(" JOIN files ON files.idFile=%s_view.idFile", view.c_str()); + } + else if (idContent == VideoDbContentType::TVSHOWS) + { + view = MediaTypeTvShow; + view_id = "idShow"; + media_type = MediaTypeTvShow; + extraField = "count(idShow)"; + } + else if (idContent == VideoDbContentType::EPISODES) + { + view = MediaTypeEpisode; + view_id = "idEpisode"; + media_type = MediaTypeEpisode; + extraField = "count(1), count(files.playCount)"; + extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str()); + } + else if (idContent == VideoDbContentType::MUSICVIDEOS) + { + bMainArtistOnly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_VIDEOLIBRARY_SHOWPERFORMERS); + if (StringUtils::EndsWith(strBaseDir, "directors/")) + // only set this to true if getting artists and show all performers is false + bMainArtistOnly = false; + view = MediaTypeMusicVideo; + view_id = "idMVideo"; + media_type = MediaTypeMusicVideo; + extraField = "count(1), count(files.playCount)"; + extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str()); + if (bMainArtistOnly) + extraJoin = + extraJoin + + PrepareSQL(" WHERE actor.name IN (SELECT musicvideo_view.c10 from musicvideo_view)"); + } + else + return false; + + strSQL = "SELECT {} FROM actor "; + extFilter.fields = "actor.actor_id, actor.name, actor.art_urls"; + extFilter.AppendField(extraField); + extFilter.AppendJoin(PrepareSQL("JOIN %s_link on actor.actor_id = %s_link.actor_id", type, type)); + extFilter.AppendJoin(PrepareSQL("JOIN %s_view on %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'", view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str())); + extFilter.AppendJoin(extraJoin); + extFilter.AppendGroup("actor.actor_id"); + } + + if (countOnly) + { + extFilter.fields = "COUNT(1)"; + extFilter.group.clear(); + extFilter.order.clear(); + } + strSQL = StringUtils::Format(strSQL, !extFilter.fields.empty() ? extFilter.fields : "*"); + + CVideoDbUrl videoUrl; + if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, videoUrl)) + return false; + + // run query + auto start = std::chrono::steady_clock::now(); + + if (!m_pDS->query(strSQL)) return false; + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{} - query took {} ms", __FUNCTION__, duration.count()); + + start = std::chrono::steady_clock::now(); + + int iRowsFound = m_pDS->num_rows(); + if (iRowsFound == 0) + { + m_pDS->close(); + return true; + } + + if (countOnly) + { + CFileItemPtr pItem(new CFileItem()); + pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound); + items.Add(pItem); + + m_pDS->close(); + return true; + } + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + { + std::map<int, CActor> mapActors; + + while (!m_pDS->eof()) + { + int idActor = m_pDS->fv(0).get_asInt(); + CActor actor; + actor.name = m_pDS->fv(1).get_asString(); + actor.thumb = m_pDS->fv(2).get_asString(); + if (idContent != VideoDbContentType::TVSHOWS && + idContent != VideoDbContentType::MUSICVIDEOS) + { + actor.playcount = m_pDS->fv(3).get_asInt(); + actor.appearances = 1; + } + else actor.appearances = m_pDS->fv(4).get_asInt(); + auto it = mapActors.find(idActor); + // is this actor already known? + if (it == mapActors.end()) + { + // check path + if (g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + mapActors.insert(std::pair<int, CActor>(idActor, actor)); + } + else if (idContent != VideoDbContentType::TVSHOWS && + idContent != VideoDbContentType::MUSICVIDEOS) + it->second.appearances++; + m_pDS->next(); + } + m_pDS->close(); + + for (const auto &i : mapActors) + { + CFileItemPtr pItem(new CFileItem(i.second.name)); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", i.first); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder=true; + pItem->GetVideoInfoTag()->SetPlayCount(i.second.playcount); + pItem->GetVideoInfoTag()->m_strPictureURL.ParseFromData(i.second.thumb); + pItem->GetVideoInfoTag()->m_iDbId = i.first; + pItem->GetVideoInfoTag()->m_type = type; + pItem->GetVideoInfoTag()->m_relevance = i.second.appearances; + if (idContent == VideoDbContentType::MUSICVIDEOS) + { + // Get artist bio from music db later if available + pItem->GetVideoInfoTag()->m_artist.emplace_back(i.second.name); + pItem->SetProperty("musicvideomediatype", MediaTypeArtist); + } + items.Add(pItem); + } + } + else + { + while (!m_pDS->eof()) + { + try + { + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder=true; + pItem->GetVideoInfoTag()->m_strPictureURL.ParseFromData(m_pDS->fv(2).get_asString()); + pItem->GetVideoInfoTag()->m_iDbId = m_pDS->fv(0).get_asInt(); + pItem->GetVideoInfoTag()->m_type = type; + if (idContent != VideoDbContentType::TVSHOWS) + { + // fv(4) is the number of videos watched, fv(3) is the total number. We set the playcount + // only if the number of videos watched is equal to the total number (i.e. every video watched) + pItem->GetVideoInfoTag()->SetPlayCount((m_pDS->fv(4).get_asInt() == m_pDS->fv(3).get_asInt()) ? 1 : 0); + } + pItem->GetVideoInfoTag()->m_relevance = m_pDS->fv(3).get_asInt(); + if (idContent == VideoDbContentType::MUSICVIDEOS) + { + pItem->GetVideoInfoTag()->m_artist.emplace_back(pItem->GetLabel()); + pItem->SetProperty("musicvideomediatype", MediaTypeArtist); + } + items.Add(pItem); + m_pDS->next(); + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{}: out of memory - retrieved {} items", __FUNCTION__, items.Size()); + return items.Size() > 0; + } + } + m_pDS->close(); + } + + end = std::chrono::steady_clock::now(); + duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{} item retrieval took {} ms", __FUNCTION__, + duration.count()); + + return true; + } + catch (...) + { + m_pDS->close(); + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetYearsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent /* = UNKNOWN */, + const Filter& filter /* = Filter() */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string strSQL; + Filter extFilter = filter; + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + { + if (idContent == VideoDbContentType::MOVIES) + { + strSQL = "select movie_view.premiered, path.strPath, files.playCount from movie_view "; + extFilter.AppendJoin("join files on files.idFile = movie_view.idFile join path on files.idPath = path.idPath"); + } + else if (idContent == VideoDbContentType::TVSHOWS) + { + strSQL = PrepareSQL("select tvshow_view.c%02d, path.strPath from tvshow_view ", VIDEODB_ID_TV_PREMIERED); + extFilter.AppendJoin("join episode_view on episode_view.idShow = tvshow_view.idShow join files on files.idFile = episode_view.idFile join path on files.idPath = path.idPath"); + } + else if (idContent == VideoDbContentType::MUSICVIDEOS) + { + strSQL = "select musicvideo_view.premiered, path.strPath, files.playCount from musicvideo_view "; + extFilter.AppendJoin("join files on files.idFile = musicvideo_view.idFile join path on files.idPath = path.idPath"); + } + else + return false; + } + else + { + std::string group; + if (idContent == VideoDbContentType::MOVIES) + { + strSQL = "select movie_view.premiered, count(1), count(files.playCount) from movie_view "; + extFilter.AppendJoin("join files on files.idFile = movie_view.idFile"); + extFilter.AppendGroup("movie_view.premiered"); + } + else if (idContent == VideoDbContentType::TVSHOWS) + { + strSQL = PrepareSQL("select distinct tvshow_view.c%02d from tvshow_view", VIDEODB_ID_TV_PREMIERED); + extFilter.AppendGroup(PrepareSQL("tvshow_view.c%02d", VIDEODB_ID_TV_PREMIERED)); + } + else if (idContent == VideoDbContentType::MUSICVIDEOS) + { + strSQL = "select musicvideo_view.premiered, count(1), count(files.playCount) from musicvideo_view "; + extFilter.AppendJoin("join files on files.idFile = musicvideo_view.idFile"); + extFilter.AppendGroup("musicvideo_view.premiered"); + } + else + return false; + } + + CVideoDbUrl videoUrl; + if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, videoUrl)) + return false; + + int iRowsFound = RunQuery(strSQL); + if (iRowsFound <= 0) + return iRowsFound == 0; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + { + std::map<int, std::pair<std::string,int> > mapYears; + while (!m_pDS->eof()) + { + int lYear = 0; + std::string dateString = m_pDS->fv(0).get_asString(); + if (dateString.size() == 4) + lYear = m_pDS->fv(0).get_asInt(); + else + { + CDateTime time; + time.SetFromDateString(dateString); + if (time.IsValid()) + lYear = time.GetYear(); + } + auto it = mapYears.find(lYear); + if (it == mapYears.end()) + { + // check path + if (g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + std::string year = std::to_string(lYear); + if (idContent == VideoDbContentType::MOVIES || + idContent == VideoDbContentType::MUSICVIDEOS) + mapYears.insert(std::pair<int, std::pair<std::string,int> >(lYear, std::pair<std::string,int>(year,m_pDS->fv(2).get_asInt()))); + else + mapYears.insert(std::pair<int, std::pair<std::string,int> >(lYear, std::pair<std::string,int>(year,0))); + } + } + m_pDS->next(); + } + m_pDS->close(); + + for (const auto &i : mapYears) + { + if (i.first == 0) + continue; + CFileItemPtr pItem(new CFileItem(i.second.first)); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", i.first); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder=true; + if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS) + pItem->GetVideoInfoTag()->SetPlayCount(i.second.second); + items.Add(pItem); + } + } + else + { + while (!m_pDS->eof()) + { + int lYear = 0; + std::string strLabel = m_pDS->fv(0).get_asString(); + if (strLabel.size() == 4) + lYear = m_pDS->fv(0).get_asInt(); + else + { + CDateTime time; + time.SetFromDateString(strLabel); + if (time.IsValid()) + { + lYear = time.GetYear(); + strLabel = std::to_string(lYear); + } + } + if (lYear == 0) + { + m_pDS->next(); + continue; + } + CFileItemPtr pItem(new CFileItem(strLabel)); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", lYear); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder=true; + if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS) + { + // fv(2) is the number of videos watched, fv(1) is the total number. We set the playcount + // only if the number of videos watched is equal to the total number (i.e. every video watched) + pItem->GetVideoInfoTag()->SetPlayCount((m_pDS->fv(2).get_asInt() == m_pDS->fv(1).get_asInt()) ? 1 : 0); + } + + // take care of dupes .. + if (!items.Contains(pItem->GetPath())) + items.Add(pItem); + + m_pDS->next(); + } + m_pDS->close(); + } + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetSeasonsNav(const std::string& strBaseDir, CFileItemList& items, int idActor, int idDirector, int idGenre, int idYear, int idShow, bool getLinkedMovies /* = true */) +{ + // parse the base path to get additional filters + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + if (idShow != -1) + videoUrl.AddOption("tvshowid", idShow); + if (idActor != -1) + videoUrl.AddOption("actorid", idActor); + else if (idDirector != -1) + videoUrl.AddOption("directorid", idDirector); + else if (idGenre != -1) + videoUrl.AddOption("genreid", idGenre); + else if (idYear != -1) + videoUrl.AddOption("year", idYear); + + if (!GetSeasonsByWhere(videoUrl.ToString(), Filter(), items, false)) + return false; + + // now add any linked movies + if (getLinkedMovies && idShow != -1) + { + Filter movieFilter; + movieFilter.join = PrepareSQL("join movielinktvshow on movielinktvshow.idMovie=movie_view.idMovie"); + movieFilter.where = PrepareSQL("movielinktvshow.idShow = %i", idShow); + CFileItemList movieItems; + GetMoviesByWhere("videodb://movies/titles/", movieFilter, movieItems); + + if (movieItems.Size() > 0) + items.Append(movieItems); + } + + return true; +} + +bool CVideoDatabase::GetSeasonsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath /* = true */, const SortDescription &sortDescription /* = SortDescription() */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + int total = -1; + + std::string strSQL = "SELECT %s FROM season_view "; + CVideoDbUrl videoUrl; + std::string strSQLExtra; + Filter extFilter = filter; + SortDescription sorting = sortDescription; + if (!BuildSQL(strBaseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting)) + return false; + + // Apply the limiting directly here if there's no special sorting but limiting + if (extFilter.limit.empty() && sorting.sortBy == SortByNone && + (sorting.limitStart > 0 || sorting.limitEnd > 0 || + (sorting.limitStart == 0 && sorting.limitEnd == 0))) + { + total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10); + strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart); + } + + strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra; + + int iRowsFound = RunQuery(strSQL); + + // store the total value of items as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + if (iRowsFound <= 0) + return iRowsFound == 0; + + std::set<std::pair<int, int>> mapSeasons; + while (!m_pDS->eof()) + { + int id = m_pDS->fv(VIDEODB_ID_SEASON_ID).get_asInt(); + int showId = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_ID).get_asInt(); + int iSeason = m_pDS->fv(VIDEODB_ID_SEASON_NUMBER).get_asInt(); + std::string name = m_pDS->fv(VIDEODB_ID_SEASON_NAME).get_asString(); + std::string path = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_PATH).get_asString(); + + if (mapSeasons.find(std::make_pair(showId, iSeason)) == mapSeasons.end() && + (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || g_passwordManager.bMasterUser || + g_passwordManager.IsDatabasePathUnlocked(path, *CMediaSourceSettings::GetInstance().GetSources("video")))) + { + mapSeasons.insert(std::make_pair(showId, iSeason)); + + std::string strLabel = name; + if (strLabel.empty()) + { + if (iSeason == 0) + strLabel = g_localizeStrings.Get(20381); + else + strLabel = StringUtils::Format(g_localizeStrings.Get(20358), iSeason); + } + CFileItemPtr pItem(new CFileItem(strLabel)); + + CVideoDbUrl itemUrl = videoUrl; + std::string strDir; + if (appendFullShowPath) + strDir += StringUtils::Format("{}/", showId); + strDir += StringUtils::Format("{}/", iSeason); + itemUrl.AppendPath(strDir); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + pItem->GetVideoInfoTag()->m_strTitle = strLabel; + if (!name.empty()) + pItem->GetVideoInfoTag()->m_strSortTitle = name; + pItem->GetVideoInfoTag()->m_iSeason = iSeason; + pItem->GetVideoInfoTag()->m_iDbId = id; + pItem->GetVideoInfoTag()->m_iIdSeason = id; + pItem->GetVideoInfoTag()->m_type = MediaTypeSeason; + pItem->GetVideoInfoTag()->m_strPath = path; + pItem->GetVideoInfoTag()->m_strShowTitle = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_TITLE).get_asString(); + pItem->GetVideoInfoTag()->m_strPlot = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_PLOT).get_asString(); + pItem->GetVideoInfoTag()->SetPremieredFromDBDate(m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_PREMIERED).get_asString()); + pItem->GetVideoInfoTag()->m_firstAired.SetFromDBDate(m_pDS->fv(VIDEODB_ID_SEASON_PREMIERED).get_asString()); + pItem->GetVideoInfoTag()->m_iUserRating = m_pDS->fv(VIDEODB_ID_SEASON_USER_RATING).get_asInt(); + // season premiered date based on first episode airdate associated to the season + // tvshow premiered date is used as a fallback + if (pItem->GetVideoInfoTag()->m_firstAired.IsValid()) + pItem->GetVideoInfoTag()->SetPremiered(pItem->GetVideoInfoTag()->m_firstAired); + else if (pItem->GetVideoInfoTag()->HasPremiered()) + pItem->GetVideoInfoTag()->SetPremiered(pItem->GetVideoInfoTag()->GetPremiered()); + else if (pItem->GetVideoInfoTag()->HasYear()) + pItem->GetVideoInfoTag()->SetYear(pItem->GetVideoInfoTag()->GetYear()); + pItem->GetVideoInfoTag()->m_genre = StringUtils::Split(m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_GENRE).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + pItem->GetVideoInfoTag()->m_studio = StringUtils::Split(m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_STUDIO).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + pItem->GetVideoInfoTag()->m_strMPAARating = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_MPAA).get_asString(); + pItem->GetVideoInfoTag()->m_iIdShow = showId; + + const int totalEpisodes = m_pDS->fv(VIDEODB_ID_SEASON_EPISODES_TOTAL).get_asInt(); + const int watchedEpisodes = m_pDS->fv(VIDEODB_ID_SEASON_EPISODES_WATCHED).get_asInt(); + pItem->GetVideoInfoTag()->m_iEpisode = totalEpisodes; + pItem->SetProperty("totalepisodes", totalEpisodes); + pItem->SetProperty("numepisodes", totalEpisodes); // will be changed later to reflect watchmode setting + pItem->SetProperty("watchedepisodes", watchedEpisodes); + pItem->SetProperty("unwatchedepisodes", totalEpisodes - watchedEpisodes); + pItem->SetProperty("watchedepisodepercent", + totalEpisodes > 0 ? (watchedEpisodes * 100 / totalEpisodes) : 0); + if (iSeason == 0) + pItem->SetProperty("isspecial", true); + pItem->GetVideoInfoTag()->SetPlayCount((totalEpisodes == watchedEpisodes) ? 1 : 0); + pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, (pItem->GetVideoInfoTag()->GetPlayCount() > 0) && (pItem->GetVideoInfoTag()->m_iEpisode > 0)); + + items.Add(pItem); + } + + m_pDS->next(); + } + m_pDS->close(); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetSortedVideos(const MediaType &mediaType, const std::string& strBaseDir, const SortDescription &sortDescription, CFileItemList& items, const Filter &filter /* = Filter() */) +{ + if (nullptr == m_pDB || nullptr == m_pDS) + return false; + + if (mediaType != MediaTypeMovie && mediaType != MediaTypeTvShow && mediaType != MediaTypeEpisode && mediaType != MediaTypeMusicVideo) + return false; + + SortDescription sorting = sortDescription; + if (sortDescription.sortBy == SortByFile || sortDescription.sortBy == SortByTitle || + sortDescription.sortBy == SortBySortTitle || sortDescription.sortBy == SortByOriginalTitle || + sortDescription.sortBy == SortByLabel || sortDescription.sortBy == SortByDateAdded || + sortDescription.sortBy == SortByRating || sortDescription.sortBy == SortByUserRating || + sortDescription.sortBy == SortByYear || sortDescription.sortBy == SortByLastPlayed || + sortDescription.sortBy == SortByPlaycount) + sorting.sortAttributes = (SortAttribute)(sortDescription.sortAttributes | SortAttributeIgnoreFolders); + + bool success = false; + if (mediaType == MediaTypeMovie) + success = GetMoviesByWhere(strBaseDir, filter, items, sorting); + else if (mediaType == MediaTypeTvShow) + success = GetTvShowsByWhere(strBaseDir, filter, items, sorting); + else if (mediaType == MediaTypeEpisode) + success = GetEpisodesByWhere(strBaseDir, filter, items, true, sorting); + else if (mediaType == MediaTypeMusicVideo) + success = GetMusicVideosByWhere(strBaseDir, filter, items, true, sorting); + else + return false; + + items.SetContent(CMediaTypes::ToPlural(mediaType)); + return success; +} + +bool CVideoDatabase::GetItems(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */) +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + return GetItems(strBaseDir, videoUrl.GetType(), videoUrl.GetItemType(), items, filter, sortDescription); +} + +bool CVideoDatabase::GetItems(const std::string &strBaseDir, const std::string &mediaType, const std::string &itemType, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */) +{ + VideoDbContentType contentType; + if (StringUtils::EqualsNoCase(mediaType, "movies")) + contentType = VideoDbContentType::MOVIES; + else if (StringUtils::EqualsNoCase(mediaType, "tvshows")) + { + if (StringUtils::EqualsNoCase(itemType, "episodes")) + contentType = VideoDbContentType::EPISODES; + else + contentType = VideoDbContentType::TVSHOWS; + } + else if (StringUtils::EqualsNoCase(mediaType, "musicvideos")) + contentType = VideoDbContentType::MUSICVIDEOS; + else + return false; + + return GetItems(strBaseDir, contentType, itemType, items, filter, sortDescription); +} + +bool CVideoDatabase::GetItems(const std::string& strBaseDir, + VideoDbContentType mediaType, + const std::string& itemType, + CFileItemList& items, + const Filter& filter /* = Filter() */, + const SortDescription& sortDescription /* = SortDescription() */) +{ + if (StringUtils::EqualsNoCase(itemType, "movies") && + (mediaType == VideoDbContentType::MOVIES || mediaType == VideoDbContentType::MOVIE_SETS)) + return GetMoviesByWhere(strBaseDir, filter, items, sortDescription); + else if (StringUtils::EqualsNoCase(itemType, "tvshows") && + mediaType == VideoDbContentType::TVSHOWS) + { + Filter extFilter = filter; + if (!CServiceBroker::GetSettingsComponent()->GetSettings()-> + GetBool(CSettings::SETTING_VIDEOLIBRARY_SHOWEMPTYTVSHOWS)) + extFilter.AppendWhere("totalCount IS NOT NULL AND totalCount > 0"); + return GetTvShowsByWhere(strBaseDir, extFilter, items, sortDescription); + } + else if (StringUtils::EqualsNoCase(itemType, "musicvideos") && + mediaType == VideoDbContentType::MUSICVIDEOS) + return GetMusicVideosByWhere(strBaseDir, filter, items, true, sortDescription); + else if (StringUtils::EqualsNoCase(itemType, "episodes") && + mediaType == VideoDbContentType::EPISODES) + return GetEpisodesByWhere(strBaseDir, filter, items, true, sortDescription); + else if (StringUtils::EqualsNoCase(itemType, "seasons") && + mediaType == VideoDbContentType::TVSHOWS) + return GetSeasonsNav(strBaseDir, items); + else if (StringUtils::EqualsNoCase(itemType, "genres")) + return GetGenresNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "years")) + return GetYearsNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "actors")) + return GetActorsNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "directors")) + return GetDirectorsNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "writers")) + return GetWritersNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "studios")) + return GetStudiosNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "sets")) + return GetSetsNav(strBaseDir, items, mediaType, filter, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPSINGLEITEMSETS)); + else if (StringUtils::EqualsNoCase(itemType, "countries")) + return GetCountriesNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "tags")) + return GetTagsNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "artists") && + mediaType == VideoDbContentType::MUSICVIDEOS) + return GetActorsNav(strBaseDir, items, mediaType, filter); + else if (StringUtils::EqualsNoCase(itemType, "albums") && + mediaType == VideoDbContentType::MUSICVIDEOS) + return GetMusicVideoAlbumsNav(strBaseDir, items, -1, filter); + + return false; +} + +std::string CVideoDatabase::GetItemById(const std::string &itemType, int id) +{ + if (StringUtils::EqualsNoCase(itemType, "genres")) + return GetGenreById(id); + else if (StringUtils::EqualsNoCase(itemType, "years")) + return std::to_string(id); + else if (StringUtils::EqualsNoCase(itemType, "actors") || + StringUtils::EqualsNoCase(itemType, "directors") || + StringUtils::EqualsNoCase(itemType, "artists")) + return GetPersonById(id); + else if (StringUtils::EqualsNoCase(itemType, "studios")) + return GetStudioById(id); + else if (StringUtils::EqualsNoCase(itemType, "sets")) + return GetSetById(id); + else if (StringUtils::EqualsNoCase(itemType, "countries")) + return GetCountryById(id); + else if (StringUtils::EqualsNoCase(itemType, "tags")) + return GetTagById(id); + else if (StringUtils::EqualsNoCase(itemType, "albums")) + return GetMusicVideoAlbumById(id); + + return ""; +} + +bool CVideoDatabase::GetMoviesNav(const std::string& strBaseDir, CFileItemList& items, + int idGenre /* = -1 */, int idYear /* = -1 */, int idActor /* = -1 */, int idDirector /* = -1 */, + int idStudio /* = -1 */, int idCountry /* = -1 */, int idSet /* = -1 */, int idTag /* = -1 */, + const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + if (idGenre > 0) + videoUrl.AddOption("genreid", idGenre); + else if (idCountry > 0) + videoUrl.AddOption("countryid", idCountry); + else if (idStudio > 0) + videoUrl.AddOption("studioid", idStudio); + else if (idDirector > 0) + videoUrl.AddOption("directorid", idDirector); + else if (idYear > 0) + videoUrl.AddOption("year", idYear); + else if (idActor > 0) + videoUrl.AddOption("actorid", idActor); + else if (idSet > 0) + videoUrl.AddOption("setid", idSet); + else if (idTag > 0) + videoUrl.AddOption("tagid", idTag); + + Filter filter; + return GetMoviesByWhere(videoUrl.ToString(), filter, items, sortDescription, getDetails); +} + +bool CVideoDatabase::GetMoviesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + // parse the base path to get additional filters + CVideoDbUrl videoUrl; + Filter extFilter = filter; + SortDescription sorting = sortDescription; + if (!videoUrl.FromString(strBaseDir) || !GetFilter(videoUrl, extFilter, sorting)) + return false; + + int total = -1; + + std::string strSQL = "select %s from movie_view "; + std::string strSQLExtra; + if (!CDatabase::BuildSQL(strSQLExtra, extFilter, strSQLExtra)) + return false; + + // Apply the limiting directly here if there's no special sorting but limiting + if (extFilter.limit.empty() && sorting.sortBy == SortByNone && + (sorting.limitStart > 0 || sorting.limitEnd > 0 || + (sorting.limitStart == 0 && sorting.limitEnd == 0))) + { + total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10); + strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart); + } + + strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra; + + int iRowsFound = RunQuery(strSQL); + + // store the total value of items as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + if (iRowsFound <= 0) + return iRowsFound == 0; + + DatabaseResults results; + results.reserve(iRowsFound); + + if (!SortUtils::SortFromDataset(sortDescription, MediaTypeMovie, m_pDS, results)) + return false; + + // get data from returned rows + items.Reserve(results.size()); + const query_data &data = m_pDS->get_result_set().records; + for (const auto &i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + CVideoInfoTag movie = GetDetailsForMovie(record, getDetails); + if (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || + g_passwordManager.bMasterUser || + g_passwordManager.IsDatabasePathUnlocked(movie.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video"))) + { + CFileItemPtr pItem(new CFileItem(movie)); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = std::to_string(movie.m_iDbId); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + pItem->SetDynPath(movie.m_strFileNameAndPath); + + pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED,movie.GetPlayCount() > 0); + items.Add(pItem); + } + } + + // cleanup + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetTvShowsNav(const std::string& strBaseDir, CFileItemList& items, + int idGenre /* = -1 */, int idYear /* = -1 */, int idActor /* = -1 */, int idDirector /* = -1 */, int idStudio /* = -1 */, int idTag /* = -1 */, + const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + if (idGenre != -1) + videoUrl.AddOption("genreid", idGenre); + else if (idStudio != -1) + videoUrl.AddOption("studioid", idStudio); + else if (idDirector != -1) + videoUrl.AddOption("directorid", idDirector); + else if (idYear != -1) + videoUrl.AddOption("year", idYear); + else if (idActor != -1) + videoUrl.AddOption("actorid", idActor); + else if (idTag != -1) + videoUrl.AddOption("tagid", idTag); + + Filter filter; + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_SHOWEMPTYTVSHOWS)) + filter.AppendWhere("totalCount IS NOT NULL AND totalCount > 0"); + return GetTvShowsByWhere(videoUrl.ToString(), filter, items, sortDescription, getDetails); +} + +bool CVideoDatabase::GetTvShowsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + int total = -1; + + std::string strSQL = "SELECT %s FROM tvshow_view "; + CVideoDbUrl videoUrl; + std::string strSQLExtra; + Filter extFilter = filter; + SortDescription sorting = sortDescription; + if (!BuildSQL(strBaseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting)) + return false; + + // Apply the limiting directly here if there's no special sorting but limiting + if (extFilter.limit.empty() && sorting.sortBy == SortByNone && + (sorting.limitStart > 0 || sorting.limitEnd > 0 || + (sorting.limitStart == 0 && sorting.limitEnd == 0))) + { + total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10); + strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart); + } + + strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra; + + int iRowsFound = RunQuery(strSQL); + + // store the total value of items as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + if (iRowsFound <= 0) + return iRowsFound == 0; + + DatabaseResults results; + results.reserve(iRowsFound); + if (!SortUtils::SortFromDataset(sorting, MediaTypeTvShow, m_pDS, results)) + return false; + + // get data from returned rows + items.Reserve(results.size()); + const query_data &data = m_pDS->get_result_set().records; + for (const auto &i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + CFileItemPtr pItem(new CFileItem()); + CVideoInfoTag movie = GetDetailsForTvShow(record, getDetails, pItem.get()); + if (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || + g_passwordManager.bMasterUser || + g_passwordManager.IsDatabasePathUnlocked(movie.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video"))) + { + pItem->SetFromVideoInfoTag(movie); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = StringUtils::Format("{}/", record->at(0).get_asInt()); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + + pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, (pItem->GetVideoInfoTag()->GetPlayCount() > 0) && (pItem->GetVideoInfoTag()->m_iEpisode > 0)); + items.Add(pItem); + } + } + + // cleanup + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetEpisodesNav(const std::string& strBaseDir, CFileItemList& items, int idGenre, int idYear, int idActor, int idDirector, int idShow, int idSeason, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + if (idShow != -1) + { + videoUrl.AddOption("tvshowid", idShow); + if (idSeason >= 0) + videoUrl.AddOption("season", idSeason); + + if (idGenre != -1) + videoUrl.AddOption("genreid", idGenre); + else if (idYear !=-1) + videoUrl.AddOption("year", idYear); + else if (idActor != -1) + videoUrl.AddOption("actorid", idActor); + } + else if (idYear != -1) + videoUrl.AddOption("year", idYear); + + if (idDirector != -1) + videoUrl.AddOption("directorid", idDirector); + + Filter filter; + bool ret = GetEpisodesByWhere(videoUrl.ToString(), filter, items, false, sortDescription, getDetails); + + if (idSeason == -1 && idShow != -1) + { // add any linked movies + Filter movieFilter; + movieFilter.join = PrepareSQL("join movielinktvshow on movielinktvshow.idMovie=movie_view.idMovie"); + movieFilter.where = PrepareSQL("movielinktvshow.idShow = %i", idShow); + CFileItemList movieItems; + GetMoviesByWhere("videodb://movies/titles/", movieFilter, movieItems); + + if (movieItems.Size() > 0) + items.Append(movieItems); + } + + return ret; +} + +bool CVideoDatabase::GetEpisodesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath /* = true */, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + int total = -1; + + std::string strSQL = "select %s from episode_view "; + CVideoDbUrl videoUrl; + std::string strSQLExtra; + Filter extFilter = filter; + SortDescription sorting = sortDescription; + if (!BuildSQL(strBaseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting)) + return false; + + // Apply the limiting directly here if there's no special sorting but limiting + if (extFilter.limit.empty() && sorting.sortBy == SortByNone && + (sorting.limitStart > 0 || sorting.limitEnd > 0 || + (sorting.limitStart == 0 && sorting.limitEnd == 0))) + { + total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10); + strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart); + } + + strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra; + + int iRowsFound = RunQuery(strSQL); + + // store the total value of items as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + if (iRowsFound <= 0) + return iRowsFound == 0; + + DatabaseResults results; + results.reserve(iRowsFound); + if (!SortUtils::SortFromDataset(sorting, MediaTypeEpisode, m_pDS, results)) + return false; + + // get data from returned rows + items.Reserve(results.size()); + CLabelFormatter formatter("%H. %T", ""); + + const query_data &data = m_pDS->get_result_set().records; + for (const auto &i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + CVideoInfoTag episode = GetDetailsForEpisode(record, getDetails); + if (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || + g_passwordManager.bMasterUser || + g_passwordManager.IsDatabasePathUnlocked(episode.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video"))) + { + CFileItemPtr pItem(new CFileItem(episode)); + formatter.FormatLabel(pItem.get()); + + int idEpisode = record->at(0).get_asInt(); + + CVideoDbUrl itemUrl = videoUrl; + std::string path; + if (appendFullShowPath && videoUrl.GetItemType() != "episodes") + path = StringUtils::Format("{}/{}/{}", + record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_ID).get_asInt(), + episode.m_iSeason, idEpisode); + else + path = std::to_string(idEpisode); + itemUrl.AppendPath(path); + pItem->SetPath(itemUrl.ToString()); + pItem->SetDynPath(episode.m_strFileNameAndPath); + + pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, episode.GetPlayCount() > 0); + pItem->m_dateTime = episode.m_firstAired; + items.Add(pItem); + } + } + + // cleanup + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +bool CVideoDatabase::GetMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, int idGenre, int idYear, int idArtist, int idDirector, int idStudio, int idAlbum, int idTag /* = -1 */, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(strBaseDir)) + return false; + + if (idGenre != -1) + videoUrl.AddOption("genreid", idGenre); + else if (idStudio != -1) + videoUrl.AddOption("studioid", idStudio); + else if (idDirector != -1) + videoUrl.AddOption("directorid", idDirector); + else if (idYear !=-1) + videoUrl.AddOption("year", idYear); + else if (idArtist != -1) + videoUrl.AddOption("artistid", idArtist); + else if (idTag != -1) + videoUrl.AddOption("tagid", idTag); + if (idAlbum != -1) + videoUrl.AddOption("albumid", idAlbum); + + Filter filter; + return GetMusicVideosByWhere(videoUrl.ToString(), filter, items, true, sortDescription, getDetails); +} + +bool CVideoDatabase::GetRecentlyAddedMoviesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */) +{ + Filter filter; + filter.order = "dateAdded desc, idMovie desc"; + filter.limit = PrepareSQL("%u", limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryRecentlyAddedItems); + return GetMoviesByWhere(strBaseDir, filter, items, SortDescription(), getDetails); +} + +bool CVideoDatabase::GetRecentlyAddedEpisodesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */) +{ + Filter filter; + filter.order = "dateAdded desc, idEpisode desc"; + filter.limit = PrepareSQL("%u", limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryRecentlyAddedItems); + return GetEpisodesByWhere(strBaseDir, filter, items, false, SortDescription(), getDetails); +} + +bool CVideoDatabase::GetRecentlyAddedMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */) +{ + Filter filter; + filter.order = "dateAdded desc, idMVideo desc"; + filter.limit = PrepareSQL("%u", limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryRecentlyAddedItems); + return GetMusicVideosByWhere(strBaseDir, filter, items, true, SortDescription(), getDetails); +} + +bool CVideoDatabase::GetInProgressTvShowsNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */) +{ + Filter filter; + filter.order = PrepareSQL("c%02d", VIDEODB_ID_TV_TITLE); + filter.where = "watchedCount != 0 AND totalCount != watchedCount"; + return GetTvShowsByWhere(strBaseDir, filter, items, SortDescription(), getDetails); +} + +std::string CVideoDatabase::GetGenreById(int id) +{ + return GetSingleValue("genre", "name", PrepareSQL("genre_id=%i", id)); +} + +std::string CVideoDatabase::GetCountryById(int id) +{ + return GetSingleValue("country", "name", PrepareSQL("country_id=%i", id)); +} + +std::string CVideoDatabase::GetSetById(int id) +{ + return GetSingleValue("sets", "strSet", PrepareSQL("idSet=%i", id)); +} + +std::string CVideoDatabase::GetTagById(int id) +{ + return GetSingleValue("tag", "name", PrepareSQL("tag_id = %i", id)); +} + +std::string CVideoDatabase::GetPersonById(int id) +{ + return GetSingleValue("actor", "name", PrepareSQL("actor_id=%i", id)); +} + +std::string CVideoDatabase::GetStudioById(int id) +{ + return GetSingleValue("studio", "name", PrepareSQL("studio_id=%i", id)); +} + +std::string CVideoDatabase::GetTvShowTitleById(int id) +{ + return GetSingleValue("tvshow", PrepareSQL("c%02d", VIDEODB_ID_TV_TITLE), PrepareSQL("idShow=%i", id)); +} + +std::string CVideoDatabase::GetMusicVideoAlbumById(int id) +{ + return GetSingleValue("musicvideo", PrepareSQL("c%02d", VIDEODB_ID_MUSICVIDEO_ALBUM), PrepareSQL("idMVideo=%i", id)); +} + +bool CVideoDatabase::HasSets() const +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + m_pDS->query("SELECT movie_view.idSet,COUNT(1) AS c FROM movie_view " + "JOIN sets ON sets.idSet = movie_view.idSet " + "GROUP BY movie_view.idSet HAVING c>1"); + + bool bResult = (m_pDS->num_rows() > 0); + m_pDS->close(); + return bResult; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +int CVideoDatabase::GetTvShowForEpisode(int idEpisode) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS2) + return false; + + // make sure we use m_pDS2, as this is called in loops using m_pDS + std::string strSQL=PrepareSQL("select idShow from episode where idEpisode=%i", idEpisode); + m_pDS2->query( strSQL ); + + int result=-1; + if (!m_pDS2->eof()) + result=m_pDS2->fv(0).get_asInt(); + m_pDS2->close(); + + return result; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idEpisode); + } + return false; +} + +int CVideoDatabase::GetSeasonForEpisode(int idEpisode) +{ + char column[5]; + sprintf(column, "c%0d", VIDEODB_ID_EPISODE_SEASON); + std::string id = GetSingleValue("episode", column, PrepareSQL("idEpisode=%i", idEpisode)); + if (id.empty()) + return -1; + return atoi(id.c_str()); +} + +bool CVideoDatabase::HasContent() +{ + return (HasContent(VideoDbContentType::MOVIES) || HasContent(VideoDbContentType::TVSHOWS) || + HasContent(VideoDbContentType::MUSICVIDEOS)); +} + +bool CVideoDatabase::HasContent(VideoDbContentType type) +{ + bool result = false; + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + std::string sql; + if (type == VideoDbContentType::MOVIES) + sql = "select count(1) from movie"; + else if (type == VideoDbContentType::TVSHOWS) + sql = "select count(1) from tvshow"; + else if (type == VideoDbContentType::MUSICVIDEOS) + sql = "select count(1) from musicvideo"; + m_pDS->query( sql ); + + if (!m_pDS->eof()) + result = (m_pDS->fv(0).get_asInt() > 0); + + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return result; +} + +ScraperPtr CVideoDatabase::GetScraperForPath( const std::string& strPath ) +{ + SScanSettings settings; + return GetScraperForPath(strPath, settings); +} + +ScraperPtr CVideoDatabase::GetScraperForPath(const std::string& strPath, SScanSettings& settings) +{ + bool dummy; + return GetScraperForPath(strPath, settings, dummy); +} + +ScraperPtr CVideoDatabase::GetScraperForPath(const std::string& strPath, SScanSettings& settings, bool& foundDirectly) +{ + foundDirectly = false; + try + { + if (strPath.empty() || !m_pDB || !m_pDS) + return ScraperPtr(); + + ScraperPtr scraper; + std::string strPath2; + + if (URIUtils::IsMultiPath(strPath)) + strPath2 = CMultiPathDirectory::GetFirstPath(strPath); + else + strPath2 = strPath; + + std::string strPath1 = URIUtils::GetDirectory(strPath2); + int idPath = GetPathId(strPath1); + + if (idPath > -1) + { + std::string strSQL = PrepareSQL( + "SELECT path.strContent, path.strScraper, path.scanRecursive, path.useFolderNames, " + "path.strSettings, path.noUpdate, path.exclude, path.allAudio FROM path WHERE idPath=%i", + idPath); + m_pDS->query( strSQL ); + } + + int iFound = 1; + CONTENT_TYPE content = CONTENT_NONE; + if (!m_pDS->eof()) + { // path is stored in db + + settings.m_allExtAudio = m_pDS->fv("path.allAudio").get_asBool(); + + if (m_pDS->fv("path.exclude").get_asBool()) + { + settings.exclude = true; + m_pDS->close(); + return ScraperPtr(); + } + settings.exclude = false; + + // try and ascertain scraper for this path + std::string strcontent = m_pDS->fv("path.strContent").get_asString(); + StringUtils::ToLower(strcontent); + content = TranslateContent(strcontent); + + //FIXME paths stored should not have empty strContent + //assert(content != CONTENT_NONE); + std::string scraperID = m_pDS->fv("path.strScraper").get_asString(); + + AddonPtr addon; + if (!scraperID.empty() && + CServiceBroker::GetAddonMgr().GetAddon(scraperID, addon, ADDON::OnlyEnabled::CHOICE_YES)) + { + scraper = std::dynamic_pointer_cast<CScraper>(addon); + if (!scraper) + return ScraperPtr(); + + // store this path's content & settings + scraper->SetPathSettings(content, m_pDS->fv("path.strSettings").get_asString()); + settings.parent_name = m_pDS->fv("path.useFolderNames").get_asBool(); + settings.recurse = m_pDS->fv("path.scanRecursive").get_asInt(); + settings.noupdate = m_pDS->fv("path.noUpdate").get_asBool(); + } + } + + if (content == CONTENT_NONE) + { // this path is not saved in db + // we must drill up until a scraper is configured + std::string strParent; + while (URIUtils::GetParentPath(strPath1, strParent)) + { + iFound++; + + std::string strSQL = + PrepareSQL("SELECT path.strContent, path.strScraper, path.scanRecursive, " + "path.useFolderNames, path.strSettings, path.noUpdate, path.exclude, " + "path.allAudio FROM path WHERE strPath='%s'", + strParent.c_str()); + m_pDS->query(strSQL); + + CONTENT_TYPE content = CONTENT_NONE; + if (!m_pDS->eof()) + { + settings.m_allExtAudio = m_pDS->fv("path.allAudio").get_asBool(); + std::string strcontent = m_pDS->fv("path.strContent").get_asString(); + StringUtils::ToLower(strcontent); + if (m_pDS->fv("path.exclude").get_asBool()) + { + settings.exclude = true; + scraper.reset(); + m_pDS->close(); + break; + } + + content = TranslateContent(strcontent); + + AddonPtr addon; + if (content != CONTENT_NONE && + CServiceBroker::GetAddonMgr().GetAddon(m_pDS->fv("path.strScraper").get_asString(), + addon, ADDON::OnlyEnabled::CHOICE_YES)) + { + scraper = std::dynamic_pointer_cast<CScraper>(addon); + scraper->SetPathSettings(content, m_pDS->fv("path.strSettings").get_asString()); + settings.parent_name = m_pDS->fv("path.useFolderNames").get_asBool(); + settings.recurse = m_pDS->fv("path.scanRecursive").get_asInt(); + settings.noupdate = m_pDS->fv("path.noUpdate").get_asBool(); + settings.exclude = false; + break; + } + } + strPath1 = strParent; + } + } + m_pDS->close(); + + if (!scraper || scraper->Content() == CONTENT_NONE) + return ScraperPtr(); + + if (scraper->Content() == CONTENT_TVSHOWS) + { + settings.recurse = 0; + if(settings.parent_name) // single show + { + settings.parent_name_root = settings.parent_name = (iFound == 1); + } + else // show root + { + settings.parent_name_root = settings.parent_name = (iFound == 2); + } + } + else if (scraper->Content() == CONTENT_MOVIES) + { + settings.recurse = settings.recurse - (iFound-1); + settings.parent_name_root = settings.parent_name && (!settings.recurse || iFound > 1); + } + else if (scraper->Content() == CONTENT_MUSICVIDEOS) + { + settings.recurse = settings.recurse - (iFound-1); + settings.parent_name_root = settings.parent_name && (!settings.recurse || iFound > 1); + } + else + { + iFound = 0; + return ScraperPtr(); + } + foundDirectly = (iFound == 1); + return scraper; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return ScraperPtr(); +} + +bool CVideoDatabase::GetUseAllExternalAudioForVideo(const std::string& videoPath) +{ + // Find longest configured source path for given video path + std::string strSQL = PrepareSQL("SELECT allAudio FROM path WHERE allAudio IS NOT NULL AND " + "instr('%s', strPath) = 1 ORDER BY length(strPath) DESC LIMIT 1", + videoPath.c_str()); + m_pDS->query(strSQL); + + if (!m_pDS->eof()) + return m_pDS->fv("allAudio").get_asBool(); + + return false; +} + +std::string CVideoDatabase::GetContentForPath(const std::string& strPath) +{ + SScanSettings settings; + bool foundDirectly = false; + ScraperPtr scraper = GetScraperForPath(strPath, settings, foundDirectly); + if (scraper) + { + if (scraper->Content() == CONTENT_TVSHOWS) + { + // check for episodes or seasons. Assumptions are: + // 1. if episodes are in the path then we're in episodes. + // 2. if no episodes are found, and content was set directly on this path, then we're in shows. + // 3. if no episodes are found, and content was not set directly on this path, we're in seasons (assumes tvshows/seasons/episodes) + std::string sql = "SELECT COUNT(*) FROM episode_view "; + + if (foundDirectly) + sql += PrepareSQL("WHERE strPath = '%s'", strPath.c_str()); + else + sql += PrepareSQL("WHERE strPath LIKE '%s%%'", strPath.c_str()); + + m_pDS->query( sql ); + if (m_pDS->num_rows() && m_pDS->fv(0).get_asInt() > 0) + return "episodes"; + return foundDirectly ? "tvshows" : "seasons"; + } + return TranslateContent(scraper->Content()); + } + return ""; +} + +void CVideoDatabase::GetMovieGenresByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT genre.genre_id, genre.name, path.strPath FROM genre INNER JOIN genre_link ON genre_link.genre_id = genre.genre_id INNER JOIN movie ON (genre_link.media_type='movie' = genre_link.media_id=movie.idMovie) INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE genre.name LIKE '%%%s%%'",strSearch.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT genre.genre_id, genre.name FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id WHERE genre_link.media_type='movie' AND name LIKE '%%%s%%'", strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(), + *CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + pItem->SetPath("videodb://movies/genres/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMovieCountriesByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT country.country_id, country.name, path.strPath FROM country INNER JOIN country_link ON country_link.country_id=country.country_id INNER JOIN movie ON country_link.media_id=movie.idMovie INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE country_link.media_type='movie' AND country.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT country.country_id, country.name FROM country INNER JOIN country_link ON country_link.country_id=country.country_id WHERE country_link.media_type='movie' AND name like '%%%s%%'", strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(), + *CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + pItem->SetPath("videodb://movies/genres/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetTvShowGenresByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT genre.genre_id, genre.name, path.strPath FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id INNER JOIN tvshow ON genre_link.media_id=tvshow.idShow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idShow=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE genre_link.media_type='tvshow' AND genre.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT genre.genre_id, genre.name FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id WHERE genre_link.media_type='tvshow' AND name LIKE '%%%s%%'", strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + pItem->SetPath("videodb://tvshows/genres/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMovieActorsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT actor.actor_id, actor.name, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN movie ON actor_link.media_id=movie.idMovie INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='movie' AND actor.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, actor.name FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN movie ON actor_link.media_id=movie.idMovie WHERE actor_link.media_type='movie' AND actor.name LIKE '%%%s%%'", strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + pItem->SetPath("videodb://movies/actors/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetTvShowsActorsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT actor.actor_id, actor.name, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN tvshow ON actor_link.media_id=tvshow.idShow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idPath=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE actor_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, actor.name FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN tvshow ON actor_link.media_id=tvshow.idShow WHERE actor_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'",strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + pItem->SetPath("videodb://tvshows/actors/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMusicVideoArtistsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + std::string strLike; + if (!strSearch.empty()) + strLike = "and actor.name like '%%%s%%'"; + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT actor.actor_id, actor.name, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN musicvideo ON actor_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='musicvideo' "+strLike, strSearch.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, actor.name from actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id WHERE actor_link.media_type='musicvideo' "+strLike,strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + pItem->SetPath("videodb://musicvideos/artists/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMusicVideoGenresByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT genre.genre_id, genre.name, path.strPath FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id INNER JOIN musicvideo ON genre_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE genre_link.media_type='musicvideo' AND genre.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT genre.genre_id, genre.name FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id WHERE genre_link.media_type='musicvideo' AND genre.name LIKE '%%%s%%'", strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + pItem->SetPath("videodb://musicvideos/genres/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMusicVideoAlbumsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + strSQL = StringUtils::Format("SELECT DISTINCT" + " musicvideo.c{:02}," + " musicvideo.idMVideo," + " path.strPath" + " FROM" + " musicvideo" + " JOIN files ON" + " files.idFile=musicvideo.idFile" + " JOIN path ON" + " path.idPath=files.idPath", + VIDEODB_ID_MUSICVIDEO_ALBUM); + if (!strSearch.empty()) + strSQL += PrepareSQL(" WHERE musicvideo.c%02d like '%%%s%%'",VIDEODB_ID_MUSICVIDEO_ALBUM, strSearch.c_str()); + + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_pDS->fv(0).get_asString().empty()) + { + m_pDS->next(); + continue; + } + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv(2).get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString())); + std::string strDir = std::to_string(m_pDS->fv(1).get_asInt()); + pItem->SetPath("videodb://musicvideos/titles/"+ strDir); + pItem->m_bIsFolder=false; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMusicVideosByAlbum(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT musicvideo.idMVideo, musicvideo.c%02d,musicvideo.c%02d, path.strPath FROM musicvideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE musicvideo.c%02d LIKE '%%%s%%'", VIDEODB_ID_MUSICVIDEO_ALBUM, VIDEODB_ID_MUSICVIDEO_TITLE, VIDEODB_ID_MUSICVIDEO_ALBUM, strSearch.c_str()); + else + strSQL = PrepareSQL("select musicvideo.idMVideo,musicvideo.c%02d,musicvideo.c%02d from musicvideo where musicvideo.c%02d like '%%%s%%'",VIDEODB_ID_MUSICVIDEO_ALBUM,VIDEODB_ID_MUSICVIDEO_TITLE,VIDEODB_ID_MUSICVIDEO_ALBUM,strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()+" - "+m_pDS->fv(2).get_asString())); + std::string strDir = + StringUtils::Format("3/2/{}", m_pDS->fv("musicvideo.idMVideo").get_asInt()); + + pItem->SetPath("videodb://"+ strDir); + pItem->m_bIsFolder=false; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +bool CVideoDatabase::GetMusicVideosByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, bool checkLocks /*= true*/, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */) +{ + try + { + + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + int total = -1; + + std::string strSQL = "select %s from musicvideo_view "; + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(baseDir)) + return false; + + std::string strSQLExtra; + const CUrlOptions::UrlOptions& options = videoUrl.GetOptions(); + std::string strArtist; + int idArtist = -1; + // If we have an artistid then get the artist name and use that to fix up the path displayed in + // the GUI (musicvideos/artist-name instead of musicvideos/artistid) + auto option = options.find("artistid"); + if (option != options.end()) + { + idArtist = option->second.asInteger(); + strArtist = GetSingleValue( + PrepareSQL("SELECT name FROM actor where actor_id = '%i'", idArtist), m_pDS) + .c_str(); + } + Filter extFilter = filter; + SortDescription sorting = sortDescription; + if (!BuildSQL(baseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting)) + return false; + + // Apply the limiting directly here if there's no special sorting but limiting + if (extFilter.limit.empty() && sorting.sortBy == SortByNone && + (sorting.limitStart > 0 || sorting.limitEnd > 0 || + (sorting.limitStart == 0 && sorting.limitEnd == 0))) + { + total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10); + strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart); + } + + strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra; + + int iRowsFound = RunQuery(strSQL); + + // store the total value of items as a property + if (total < iRowsFound) + total = iRowsFound; + items.SetProperty("total", total); + + if (iRowsFound <= 0) + return iRowsFound == 0; + + DatabaseResults results; + results.reserve(iRowsFound); + if (!SortUtils::SortFromDataset(sorting, MediaTypeMusicVideo, m_pDS, results)) + return false; + + // get data from returned rows + items.Reserve(results.size()); + // get songs from returned subtable + const query_data &data = m_pDS->get_result_set().records; + for (const auto &i : results) + { + unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger(); + const dbiplus::sql_record* const record = data.at(targetRow); + + CVideoInfoTag musicvideo = GetDetailsForMusicVideo(record, getDetails); + if (!checkLocks || m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || g_passwordManager.bMasterUser || + g_passwordManager.IsDatabasePathUnlocked(musicvideo.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video"))) + { + CFileItemPtr item(new CFileItem(musicvideo)); + + CVideoDbUrl itemUrl = videoUrl; + std::string path = std::to_string(record->at(0).get_asInt()); + itemUrl.AppendPath(path); + item->SetPath(itemUrl.ToString()); + + item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, musicvideo.GetPlayCount() > 0); + items.Add(item); + } + } + + // cleanup + m_pDS->close(); + if (!strArtist.empty()) + items.SetProperty("customtitle", strArtist); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return false; +} + +unsigned int CVideoDatabase::GetRandomMusicVideoIDs(const std::string& strWhere, std::vector<std::pair<int,int> > &songIDs) +{ + try + { + if (nullptr == m_pDB) + return 0; + if (nullptr == m_pDS) + return 0; + + std::string strSQL = "select distinct idMVideo from musicvideo_view"; + if (!strWhere.empty()) + strSQL += " where " + strWhere; + strSQL += PrepareSQL(" ORDER BY RANDOM()"); + + if (!m_pDS->query(strSQL)) return 0; + songIDs.clear(); + if (m_pDS->num_rows() == 0) + { + m_pDS->close(); + return 0; + } + songIDs.reserve(m_pDS->num_rows()); + while (!m_pDS->eof()) + { + songIDs.push_back(std::make_pair<int,int>(2,m_pDS->fv(0).get_asInt())); + m_pDS->next(); + } // cleanup + m_pDS->close(); + return songIDs.size(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strWhere); + } + return 0; +} + +int CVideoDatabase::GetMatchingMusicVideo(const std::string& strArtist, const std::string& strAlbum, const std::string& strTitle) +{ + try + { + if (nullptr == m_pDB) + return -1; + if (nullptr == m_pDS) + return -1; + + std::string strSQL; + if (strAlbum.empty() && strTitle.empty()) + { // we want to return matching artists only + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN musicvideo ON actor_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='musicvideo' AND actor.name like '%s'", strArtist.c_str()); + else + strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id WHERE actor_link.media_type='musicvideo' AND actor.name LIKE '%s'", strArtist.c_str()); + } + else + { // we want to return the matching musicvideo + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT musicvideo.idMVideo FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN musicvideo ON actor_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='musicvideo' AND musicvideo.c%02d LIKE '%s' AND musicvideo.c%02d LIKE '%s' AND actor.name LIKE '%s'", VIDEODB_ID_MUSICVIDEO_ALBUM, strAlbum.c_str(), VIDEODB_ID_MUSICVIDEO_TITLE, strTitle.c_str(), strArtist.c_str()); + else + strSQL = PrepareSQL("select musicvideo.idMVideo from musicvideo join actor_link on actor_link.media_id=musicvideo.idMVideo AND actor_link.media_type='musicvideo' join actor on actor.actor_id=actor_link.actor_id where musicvideo.c%02d like '%s' and musicvideo.c%02d like '%s' and actor.name like '%s'",VIDEODB_ID_MUSICVIDEO_ALBUM,strAlbum.c_str(),VIDEODB_ID_MUSICVIDEO_TITLE,strTitle.c_str(),strArtist.c_str()); + } + m_pDS->query( strSQL ); + + if (m_pDS->eof()) + return -1; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->close(); + return -1; + } + + int lResult = m_pDS->fv(0).get_asInt(); + m_pDS->close(); + return lResult; + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + return -1; +} + +void CVideoDatabase::GetMoviesByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT movie.idMovie, movie.c%02d, path.strPath, movie.idSet FROM movie " + "INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON " + "path.idPath=files.idPath " + "WHERE movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%'", + VIDEODB_ID_TITLE, VIDEODB_ID_TITLE, strSearch.c_str(), + VIDEODB_ID_ORIGINALTITLE, strSearch.c_str()); + else + strSQL = PrepareSQL("SELECT movie.idMovie,movie.c%02d, movie.idSet FROM movie WHERE " + "movie.c%02d like '%%%s%%' OR movie.c%02d LIKE '%%%s%%'", + VIDEODB_ID_TITLE, VIDEODB_ID_TITLE, strSearch.c_str(), + VIDEODB_ID_ORIGINALTITLE, strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + int movieId = m_pDS->fv("movie.idMovie").get_asInt(); + int setId = m_pDS->fv("movie.idSet").get_asInt(); + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string path; + if (setId <= 0 || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPMOVIESETS)) + path = StringUtils::Format("videodb://movies/titles/{}", movieId); + else + path = StringUtils::Format("videodb://movies/sets/{}/{}", setId, movieId); + pItem->SetPath(path); + pItem->m_bIsFolder=false; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetTvShowsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT tvshow.idShow, tvshow.c%02d, path.strPath FROM tvshow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idShow=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE tvshow.c%02d LIKE '%%%s%%'", VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_TITLE, strSearch.c_str()); + else + strSQL = PrepareSQL("select tvshow.idShow,tvshow.c%02d from tvshow where tvshow.c%02d like '%%%s%%'",VIDEODB_ID_TV_TITLE,VIDEODB_ID_TV_TITLE,strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = + StringUtils::Format("tvshows/titles/{}/", m_pDS->fv("tvshow.idShow").get_asInt()); + + pItem->SetPath("videodb://"+ strDir); + pItem->m_bIsFolder=true; + pItem->GetVideoInfoTag()->m_iDbId = m_pDS->fv("tvshow.idShow").get_asInt(); + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetEpisodesByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d, path.strPath FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow INNER JOIN files ON files.idFile=episode.idFile INNER JOIN path ON path.idPath=files.idPath WHERE episode.c%02d LIKE '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_TITLE, strSearch.c_str()); + else + strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow WHERE episode.c%02d like '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_TITLE, strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()+" ("+m_pDS->fv(4).get_asString()+")")); + std::string path = StringUtils::Format("videodb://tvshows/titles/{}/{}/{}", + m_pDS->fv("episode.idShow").get_asInt(), + m_pDS->fv(2).get_asInt(), m_pDS->fv(0).get_asInt()); + pItem->SetPath(path); + pItem->m_bIsFolder=false; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMusicVideosByName(const std::string& strSearch, CFileItemList& items) +{ +// Alternative searching - not quite as fast though due to +// retrieving all information +// Filter filter(PrepareSQL("c%02d like '%s%%' or c%02d like '%% %s%%'", VIDEODB_ID_MUSICVIDEO_TITLE, strSearch.c_str(), VIDEODB_ID_MUSICVIDEO_TITLE, strSearch.c_str())); +// GetMusicVideosByWhere("videodb://musicvideos/titles/", filter, items); + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT musicvideo.idMVideo, musicvideo.c%02d, path.strPath FROM musicvideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE musicvideo.c%02d LIKE '%%%s%%'", VIDEODB_ID_MUSICVIDEO_TITLE, VIDEODB_ID_MUSICVIDEO_TITLE, strSearch.c_str()); + else + strSQL = PrepareSQL("select musicvideo.idMVideo,musicvideo.c%02d from musicvideo where musicvideo.c%02d like '%%%s%%'",VIDEODB_ID_MUSICVIDEO_TITLE,VIDEODB_ID_MUSICVIDEO_TITLE,strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string strDir = + StringUtils::Format("3/2/{}", m_pDS->fv("musicvideo.idMVideo").get_asInt()); + + pItem->SetPath("videodb://"+ strDir); + pItem->m_bIsFolder=false; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetEpisodesByPlot(const std::string& strSearch, CFileItemList& items) +{ +// Alternative searching - not quite as fast though due to +// retrieving all information +// Filter filter; +// filter.where = PrepareSQL("c%02d like '%s%%' or c%02d like '%% %s%%'", VIDEODB_ID_EPISODE_PLOT, strSearch.c_str(), VIDEODB_ID_EPISODE_PLOT, strSearch.c_str()); +// filter.where += PrepareSQL("or c%02d like '%s%%' or c%02d like '%% %s%%'", VIDEODB_ID_EPISODE_TITLE, strSearch.c_str(), VIDEODB_ID_EPISODE_TITLE, strSearch.c_str()); +// GetEpisodesByWhere("videodb://tvshows/titles/", filter, items); +// return; + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d, path.strPath FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow INNER JOIN files ON files.idFile=episode.idFile INNER JOIN path ON path.idPath=files.idPath WHERE episode.c%02d LIKE '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_PLOT, strSearch.c_str()); + else + strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow WHERE episode.c%02d LIKE '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_PLOT, strSearch.c_str()); + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()+" ("+m_pDS->fv(4).get_asString()+")")); + std::string path = StringUtils::Format("videodb://tvshows/titles/{}/{}/{}", + m_pDS->fv("episode.idShow").get_asInt(), + m_pDS->fv(2).get_asInt(), m_pDS->fv(0).get_asInt()); + pItem->SetPath(path); + pItem->m_bIsFolder=false; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMoviesByPlot(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("select movie.idMovie, movie.c%02d, path.strPath FROM movie INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%'", VIDEODB_ID_TITLE,VIDEODB_ID_PLOT, strSearch.c_str(), VIDEODB_ID_PLOTOUTLINE, strSearch.c_str(), VIDEODB_ID_TAGLINE,strSearch.c_str()); + else + strSQL = PrepareSQL("SELECT movie.idMovie, movie.c%02d FROM movie WHERE (movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%')", VIDEODB_ID_TITLE, VIDEODB_ID_PLOT, strSearch.c_str(), VIDEODB_ID_PLOTOUTLINE, strSearch.c_str(), VIDEODB_ID_TAGLINE, strSearch.c_str()); + + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv(2).get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + std::string path = + StringUtils::Format("videodb://movies/titles/{}", m_pDS->fv(0).get_asInt()); + pItem->SetPath(path); + pItem->m_bIsFolder=false; + + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMovieDirectorsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name, path.strPath FROM movie INNER JOIN director_link ON (director_link.media_id=movie.idMovie AND director_link.media_type='movie') INNER JOIN actor ON actor.actor_id=director_link.actor_id INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN movie ON director_link.media_id=movie.idMovie WHERE director_link.media_type='movie' AND actor.name like '%%%s%%'", strSearch.c_str()); + + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + + pItem->SetPath("videodb://movies/directors/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetTvShowsDirectorsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name, path.strPath FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN tvshow ON director_link.media_id=tvshow.idShow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idShow=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE director_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN tvshow ON director_link.media_id=tvshow.idShow WHERE director_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'", strSearch.c_str()); + + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + + pItem->SetPath("videodb://tvshows/directors/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::GetMusicVideoDirectorsByName(const std::string& strSearch, CFileItemList& items) +{ + std::string strSQL; + + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name, path.strPath FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN musicvideo ON director_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE director_link.media_type='musicvideo' AND actor.name LIKE '%%%s%%'", strSearch.c_str()); + else + strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN musicvideo ON director_link.media_id=musicvideo.idMVideo WHERE director_link.media_type='musicvideo' AND actor.name LIKE '%%%s%%'", strSearch.c_str()); + + m_pDS->query( strSQL ); + + while (!m_pDS->eof()) + { + if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser) + if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video"))) + { + m_pDS->next(); + continue; + } + + std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt()); + CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString())); + + pItem->SetPath("videodb://musicvideos/albums/"+ strDir); + pItem->m_bIsFolder=true; + items.Add(pItem); + m_pDS->next(); + } + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } +} + +void CVideoDatabase::CleanDatabase(CGUIDialogProgressBarHandle* handle, + const std::set<int>& paths, + bool showProgress) +{ + CGUIDialogProgress* progress = NULL; + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + if (nullptr == m_pDS2) + return; + + auto start = std::chrono::steady_clock::now(); + CLog::Log(LOGINFO, "{}: Starting videodatabase cleanup ..", __FUNCTION__); + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, + "OnCleanStarted"); + + if (handle) + { + handle->SetTitle(g_localizeStrings.Get(700)); + handle->SetText(""); + } + else if (showProgress) + { + progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>( + WINDOW_DIALOG_PROGRESS); + if (progress) + { + progress->SetHeading(CVariant{700}); + progress->SetLine(0, CVariant{""}); + progress->SetLine(1, CVariant{313}); + progress->SetLine(2, CVariant{330}); + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + } + + BeginTransaction(); + + // find all the files + std::string sql = "SELECT files.idFile, files.strFileName, path.strPath FROM files " + "INNER JOIN path ON path.idPath=files.idPath"; + if (!paths.empty()) + { + std::string strPaths; + for (const auto& i : paths) + strPaths += StringUtils::Format(",{}", i); + sql += PrepareSQL(" AND path.idPath IN (%s)", strPaths.substr(1).c_str()); + } + + // For directory caching to work properly, we need to sort the files by path + sql += " ORDER BY path.strPath"; + + m_pDS2->query(sql); + if (m_pDS2->num_rows() > 0) + { + std::string filesToTestForDelete; + VECSOURCES videoSources(*CMediaSourceSettings::GetInstance().GetSources("video")); + CServiceBroker::GetMediaManager().GetRemovableDrives(videoSources); + + int total = m_pDS2->num_rows(); + int current = 0; + std::string lastDir; + bool gotDir = true; + + while (!m_pDS2->eof()) + { + std::string path = m_pDS2->fv("path.strPath").get_asString(); + std::string fileName = m_pDS2->fv("files.strFileName").get_asString(); + std::string fullPath; + ConstructPath(fullPath, path, fileName); + + // get the first stacked file + if (URIUtils::IsStack(fullPath)) + fullPath = CStackDirectory::GetFirstStackedFile(fullPath); + + // get the actual archive path + if (URIUtils::IsInArchive(fullPath)) + fullPath = CURL(fullPath).GetHostName(); + + bool del = true; + if (URIUtils::IsPlugin(fullPath)) + { + SScanSettings settings; + bool foundDirectly = false; + ScraperPtr scraper = GetScraperForPath(fullPath, settings, foundDirectly); + if (scraper && + CPluginDirectory::CheckExists(TranslateContent(scraper->Content()), fullPath)) + del = false; + } + else + { + // Only consider keeping this file if not optical and belonging to a (matching) source + bool bIsSource; + if (!URIUtils::IsOnDVD(fullPath) && + CUtil::GetMatchingSource(fullPath, videoSources, bIsSource) >= 0) + { + const std::string pathDir = URIUtils::GetDirectory(fullPath); + + // Cache file's directory in case it's different from the previous file + if (lastDir != pathDir) + { + lastDir = pathDir; + CFileItemList items; // Dummy list + gotDir = CDirectory::GetDirectory(pathDir, items, "", + DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO); + } + + // Keep existing files + if (gotDir && CFile::Exists(fullPath, true)) + del = false; + } + } + if (del) + filesToTestForDelete += m_pDS2->fv("files.idFile").get_asString() + ","; + + if (handle == NULL && progress != NULL) + { + int percentage = current * 100 / total; + if (percentage > progress->GetPercentage()) + { + progress->SetPercentage(percentage); + progress->Progress(); + } + if (progress->IsCanceled()) + { + progress->Close(); + m_pDS2->close(); + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, + "OnCleanFinished"); + return; + } + } + else if (handle != NULL) + handle->SetPercentage(current * 100 / (float)total); + + m_pDS2->next(); + current++; + } + m_pDS2->close(); + + std::string filesToDelete; + + // Add any files that don't have a valid idPath entry to the filesToDelete list. + m_pDS->query("SELECT files.idFile FROM files WHERE NOT EXISTS (SELECT 1 FROM path " + "WHERE path.idPath = files.idPath)"); + while (!m_pDS->eof()) + { + std::string file = m_pDS->fv("files.idFile").get_asString() + ","; + filesToTestForDelete += file; + filesToDelete += file; + + m_pDS->next(); + } + m_pDS->close(); + + std::map<int, bool> pathsDeleteDecisions; + std::vector<int> movieIDs; + std::vector<int> tvshowIDs; + std::vector<int> episodeIDs; + std::vector<int> musicVideoIDs; + + if (!filesToTestForDelete.empty()) + { + StringUtils::TrimRight(filesToTestForDelete, ","); + + movieIDs = CleanMediaType(MediaTypeMovie, filesToTestForDelete, pathsDeleteDecisions, + filesToDelete, !showProgress); + episodeIDs = CleanMediaType(MediaTypeEpisode, filesToTestForDelete, pathsDeleteDecisions, + filesToDelete, !showProgress); + musicVideoIDs = CleanMediaType(MediaTypeMusicVideo, filesToTestForDelete, + pathsDeleteDecisions, filesToDelete, !showProgress); + } + + if (progress != NULL) + { + progress->SetPercentage(100); + progress->Progress(); + } + + if (!filesToDelete.empty()) + { + filesToDelete = "(" + StringUtils::TrimRight(filesToDelete, ",") + ")"; + + // Clean hashes of all paths that files are deleted from + // Otherwise there is a mismatch between the path contents and the hash in the + // database, leading to potentially missed items on re-scan (if deleted files are + // later re-added to a source) + CLog::LogFC(LOGDEBUG, LOGDATABASE, "Cleaning path hashes"); + m_pDS->query("SELECT DISTINCT strPath FROM path JOIN files ON files.idPath=path.idPath " + "WHERE files.idFile IN " + + filesToDelete); + int pathHashCount = m_pDS->num_rows(); + while (!m_pDS->eof()) + { + InvalidatePathHash(m_pDS->fv("strPath").get_asString()); + m_pDS->next(); + } + CLog::LogFC(LOGDEBUG, LOGDATABASE, "Cleaned {} path hashes", pathHashCount); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning files table", __FUNCTION__); + sql = "DELETE FROM files WHERE idFile IN " + filesToDelete; + m_pDS->exec(sql); + } + + if (!movieIDs.empty()) + { + std::string moviesToDelete; + for (const auto& i : movieIDs) + moviesToDelete += StringUtils::Format("{},", i); + moviesToDelete = "(" + StringUtils::TrimRight(moviesToDelete, ",") + ")"; + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning movie table", __FUNCTION__); + sql = "DELETE FROM movie WHERE idMovie IN " + moviesToDelete; + m_pDS->exec(sql); + } + + if (!episodeIDs.empty()) + { + std::string episodesToDelete; + for (const auto& i : episodeIDs) + episodesToDelete += StringUtils::Format("{},", i); + episodesToDelete = "(" + StringUtils::TrimRight(episodesToDelete, ",") + ")"; + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning episode table", __FUNCTION__); + sql = "DELETE FROM episode WHERE idEpisode IN " + episodesToDelete; + m_pDS->exec(sql); + } + + CLog::Log(LOGDEBUG, LOGDATABASE, + "{}: Cleaning paths that don't exist and have content set...", __FUNCTION__); + sql = "SELECT path.idPath, path.strPath, path.idParentPath FROM path " + "WHERE NOT ((strContent IS NULL OR strContent = '') " + "AND (strSettings IS NULL OR strSettings = '') " + "AND (strHash IS NULL OR strHash = '') " + "AND (exclude IS NULL OR exclude != 1))"; + m_pDS2->query(sql); + std::string strIds; + while (!m_pDS2->eof()) + { + auto pathsDeleteDecision = pathsDeleteDecisions.find(m_pDS2->fv(0).get_asInt()); + // Check if we have a decision for the parent path + auto pathsDeleteDecisionByParent = pathsDeleteDecisions.find(m_pDS2->fv(2).get_asInt()); + std::string path = m_pDS2->fv(1).get_asString(); + + bool exists = false; + if (URIUtils::IsPlugin(path)) + { + SScanSettings settings; + bool foundDirectly = false; + ScraperPtr scraper = GetScraperForPath(path, settings, foundDirectly); + if (scraper && CPluginDirectory::CheckExists(TranslateContent(scraper->Content()), path)) + exists = true; + } + else + exists = CDirectory::Exists(path, false); + + if (((pathsDeleteDecision != pathsDeleteDecisions.end() && pathsDeleteDecision->second) || + (pathsDeleteDecision == pathsDeleteDecisions.end() && !exists)) && + ((pathsDeleteDecisionByParent != pathsDeleteDecisions.end() && + pathsDeleteDecisionByParent->second) || + (pathsDeleteDecisionByParent == pathsDeleteDecisions.end()))) + strIds += m_pDS2->fv(0).get_asString() + ","; + + m_pDS2->next(); + } + m_pDS2->close(); + + if (!strIds.empty()) + { + sql = PrepareSQL("DELETE FROM path WHERE idPath IN (%s)", + StringUtils::TrimRight(strIds, ",").c_str()); + m_pDS->exec(sql); + sql = "DELETE FROM tvshowlinkpath " + "WHERE NOT EXISTS (SELECT 1 FROM path WHERE path.idPath = tvshowlinkpath.idPath)"; + m_pDS->exec(sql); + } + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning tvshow table", __FUNCTION__); + + std::string tvshowsToDelete; + sql = "SELECT idShow FROM tvshow " + "WHERE NOT EXISTS (SELECT 1 FROM tvshowlinkpath WHERE tvshowlinkpath.idShow = " + "tvshow.idShow)"; + m_pDS->query(sql); + while (!m_pDS->eof()) + { + tvshowIDs.push_back(m_pDS->fv(0).get_asInt()); + tvshowsToDelete += m_pDS->fv(0).get_asString() + ","; + m_pDS->next(); + } + m_pDS->close(); + if (!tvshowsToDelete.empty()) + { + sql = "DELETE FROM tvshow WHERE idShow IN (" + + StringUtils::TrimRight(tvshowsToDelete, ",") + ")"; + m_pDS->exec(sql); + } + + if (!musicVideoIDs.empty()) + { + std::string musicVideosToDelete; + for (const auto& i : musicVideoIDs) + musicVideosToDelete += StringUtils::Format("{},", i); + musicVideosToDelete = "(" + StringUtils::TrimRight(musicVideosToDelete, ",") + ")"; + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning musicvideo table", __FUNCTION__); + sql = "DELETE FROM musicvideo WHERE idMVideo IN " + musicVideosToDelete; + m_pDS->exec(sql); + } + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning path table", __FUNCTION__); + sql = StringUtils::Format( + "DELETE FROM path " + "WHERE (strContent IS NULL OR strContent = '') " + "AND (strSettings IS NULL OR strSettings = '') " + "AND (strHash IS NULL OR strHash = '') " + "AND (exclude IS NULL OR exclude != 1) " + "AND (idParentPath IS NULL OR NOT EXISTS (SELECT 1 FROM (SELECT idPath FROM path) as " + "parentPath WHERE parentPath.idPath = path.idParentPath)) " // MySQL only fix (#5007) + "AND NOT EXISTS (SELECT 1 FROM files WHERE files.idPath = path.idPath) " + "AND NOT EXISTS (SELECT 1 FROM tvshowlinkpath WHERE tvshowlinkpath.idPath = path.idPath) " + "AND NOT EXISTS (SELECT 1 FROM movie WHERE movie.c{:02} = path.idPath) " + "AND NOT EXISTS (SELECT 1 FROM episode WHERE episode.c{:02} = path.idPath) " + "AND NOT EXISTS (SELECT 1 FROM musicvideo WHERE musicvideo.c{:02} = path.idPath)", + VIDEODB_ID_PARENTPATHID, VIDEODB_ID_EPISODE_PARENTPATHID, + VIDEODB_ID_MUSICVIDEO_PARENTPATHID); + m_pDS->exec(sql); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning genre table", __FUNCTION__); + sql = + "DELETE FROM genre " + "WHERE NOT EXISTS (SELECT 1 FROM genre_link WHERE genre_link.genre_id = genre.genre_id)"; + m_pDS->exec(sql); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning country table", __FUNCTION__); + sql = "DELETE FROM country WHERE NOT EXISTS (SELECT 1 FROM country_link WHERE " + "country_link.country_id = country.country_id)"; + m_pDS->exec(sql); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning actor table of actors, directors and writers", + __FUNCTION__); + sql = + "DELETE FROM actor " + "WHERE NOT EXISTS (SELECT 1 FROM actor_link WHERE actor_link.actor_id = actor.actor_id) " + "AND NOT EXISTS (SELECT 1 FROM director_link WHERE director_link.actor_id = " + "actor.actor_id) " + "AND NOT EXISTS (SELECT 1 FROM writer_link WHERE writer_link.actor_id = actor.actor_id)"; + m_pDS->exec(sql); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning studio table", __FUNCTION__); + sql = "DELETE FROM studio " + "WHERE NOT EXISTS (SELECT 1 FROM studio_link WHERE studio_link.studio_id = " + "studio.studio_id)"; + m_pDS->exec(sql); + + CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning set table", __FUNCTION__); + sql = "DELETE FROM sets " + "WHERE NOT EXISTS (SELECT 1 FROM movie WHERE movie.idSet = sets.idSet)"; + m_pDS->exec(sql); + + CommitTransaction(); + + if (handle) + handle->SetTitle(g_localizeStrings.Get(331)); + + Compress(false); + + CUtil::DeleteVideoDatabaseDirectoryCache(); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGINFO, "{}: Cleaning videodatabase done. Operation took {} ms", __FUNCTION__, + duration.count()); + + for (const auto& i : movieIDs) + AnnounceRemove(MediaTypeMovie, i, true); + + for (const auto& i : episodeIDs) + AnnounceRemove(MediaTypeEpisode, i, true); + + for (const auto& i : tvshowIDs) + AnnounceRemove(MediaTypeTvShow, i, true); + + for (const auto& i : musicVideoIDs) + AnnounceRemove(MediaTypeMusicVideo, i, true); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + RollbackTransaction(); + } + if (progress) + progress->Close(); + + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnCleanFinished"); +} + +std::vector<int> CVideoDatabase::CleanMediaType(const std::string &mediaType, const std::string &cleanableFileIDs, + std::map<int, bool> &pathsDeleteDecisions, std::string &deletedFileIDs, bool silent) +{ + std::vector<int> cleanedIDs; + if (mediaType.empty() || cleanableFileIDs.empty()) + return cleanedIDs; + + const std::string& table = mediaType; + std::string idField; + std::string parentPathIdField; + bool isEpisode = false; + if (mediaType == MediaTypeMovie) + { + idField = "idMovie"; + parentPathIdField = StringUtils::Format("{}.c{:02}", table, VIDEODB_ID_PARENTPATHID); + } + else if (mediaType == MediaTypeEpisode) + { + idField = "idEpisode"; + parentPathIdField = "showPath.idParentPath"; + isEpisode = true; + } + else if (mediaType == MediaTypeMusicVideo) + { + idField = "idMVideo"; + parentPathIdField = StringUtils::Format("{}.c{:02}", table, VIDEODB_ID_MUSICVIDEO_PARENTPATHID); + } + else + return cleanedIDs; + + // now grab them media items + std::string sql = PrepareSQL("SELECT %s.%s, %s.idFile, path.idPath, parentPath.strPath FROM %s " + "JOIN files ON files.idFile = %s.idFile " + "JOIN path ON path.idPath = files.idPath ", + table.c_str(), idField.c_str(), table.c_str(), table.c_str(), + table.c_str()); + + if (isEpisode) + sql += "JOIN tvshowlinkpath ON tvshowlinkpath.idShow = episode.idShow JOIN path AS showPath ON showPath.idPath=tvshowlinkpath.idPath "; + + sql += PrepareSQL("LEFT JOIN path as parentPath ON parentPath.idPath = %s " + "WHERE %s.idFile IN (%s)", + parentPathIdField.c_str(), + table.c_str(), cleanableFileIDs.c_str()); + + VECSOURCES videoSources(*CMediaSourceSettings::GetInstance().GetSources("video")); + CServiceBroker::GetMediaManager().GetRemovableDrives(videoSources); + + // map of parent path ID to boolean pair (if not exists and user choice) + std::map<int, std::pair<bool, bool> > sourcePathsDeleteDecisions; + m_pDS2->query(sql); + while (!m_pDS2->eof()) + { + bool del = true; + if (m_pDS2->fv(3).get_isNull() == false) + { + std::string parentPath = m_pDS2->fv(3).get_asString(); + + // try to find the source path the parent path belongs to + SScanSettings scanSettings; + std::string sourcePath; + GetSourcePath(parentPath, sourcePath, scanSettings); + + bool bIsSourceName; + bool sourceNotFound = (CUtil::GetMatchingSource(parentPath, videoSources, bIsSourceName) < 0); + + if (sourceNotFound && sourcePath.empty()) + sourcePath = parentPath; + + int sourcePathID = GetPathId(sourcePath); + auto sourcePathsDeleteDecision = sourcePathsDeleteDecisions.find(sourcePathID); + if (sourcePathsDeleteDecision == sourcePathsDeleteDecisions.end()) + { + bool sourcePathNotExists = (sourceNotFound || !CDirectory::Exists(sourcePath, false)); + // if the parent path exists, the file will be deleted without asking + // if the parent path doesn't exist or does not belong to a valid media source, + // ask the user whether to remove all items it contained + if (sourcePathNotExists) + { + // in silent mode assume that the files are just temporarily missing + if (silent) + del = false; + else + { + CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO); + if (pDialog != NULL) + { + CURL sourceUrl(sourcePath); + pDialog->SetHeading(CVariant{15012}); + pDialog->SetText(CVariant{StringUtils::Format(g_localizeStrings.Get(15013), + sourceUrl.GetWithoutUserDetails())}); + pDialog->SetChoice(0, CVariant{15015}); + pDialog->SetChoice(1, CVariant{15014}); + pDialog->Open(); + + del = !pDialog->IsConfirmed(); + } + } + } + + sourcePathsDeleteDecisions.insert(std::make_pair(sourcePathID, std::make_pair(sourcePathNotExists, del))); + pathsDeleteDecisions.insert(std::make_pair(sourcePathID, sourcePathNotExists && del)); + } + // the only reason not to delete the file is if the parent path doesn't + // exist and the user decided to delete all the items it contained + else if (sourcePathsDeleteDecision->second.first && + !sourcePathsDeleteDecision->second.second) + del = false; + + if (scanSettings.parent_name) + pathsDeleteDecisions.insert(std::make_pair(m_pDS2->fv(2).get_asInt(), del)); + } + + if (del) + { + deletedFileIDs += m_pDS2->fv(1).get_asString() + ","; + cleanedIDs.push_back(m_pDS2->fv(0).get_asInt()); + } + + m_pDS2->next(); + } + m_pDS2->close(); + + return cleanedIDs; +} + +void CVideoDatabase::DumpToDummyFiles(const std::string &path) +{ + // get all tvshows + CFileItemList items; + GetTvShowsByWhere("videodb://tvshows/titles/", CDatabase::Filter(), items); + std::string showPath = URIUtils::AddFileToFolder(path, "shows"); + CDirectory::Create(showPath); + for (int i = 0; i < items.Size(); i++) + { + // create a folder in this directory + std::string showName = CUtil::MakeLegalFileName(items[i]->GetVideoInfoTag()->m_strShowTitle); + std::string TVFolder = URIUtils::AddFileToFolder(showPath, showName); + if (CDirectory::Create(TVFolder)) + { // right - grab the episodes and dump them as well + CFileItemList episodes; + Filter filter(PrepareSQL("idShow=%i", items[i]->GetVideoInfoTag()->m_iDbId)); + GetEpisodesByWhere("videodb://tvshows/titles/", filter, episodes); + for (int i = 0; i < episodes.Size(); i++) + { + CVideoInfoTag *tag = episodes[i]->GetVideoInfoTag(); + std::string episode = + StringUtils::Format("{}.s{:02}e{:02}.avi", showName, tag->m_iSeason, tag->m_iEpisode); + // and make a file + std::string episodePath = URIUtils::AddFileToFolder(TVFolder, episode); + CFile file; + if (file.OpenForWrite(episodePath)) + file.Close(); + } + } + } + // get all movies + items.Clear(); + GetMoviesByWhere("videodb://movies/titles/", CDatabase::Filter(), items); + std::string moviePath = URIUtils::AddFileToFolder(path, "movies"); + CDirectory::Create(moviePath); + for (int i = 0; i < items.Size(); i++) + { + CVideoInfoTag *tag = items[i]->GetVideoInfoTag(); + std::string movie = StringUtils::Format("{}.avi", tag->m_strTitle); + CFile file; + if (file.OpenForWrite(URIUtils::AddFileToFolder(moviePath, movie))) + file.Close(); + } +} + +void CVideoDatabase::ExportToXML(const std::string &path, bool singleFile /* = true */, bool images /* = false */, bool actorThumbs /* false */, bool overwrite /*=false*/) +{ + int iFailCount = 0; + CGUIDialogProgress *progress=NULL; + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + if (nullptr == m_pDS2) + return; + + // create a 3rd dataset as well as GetEpisodeDetails() etc. uses m_pDS2, and we need to do 3 nested queries on tv shows + std::unique_ptr<Dataset> pDS; + pDS.reset(m_pDB->CreateDataset()); + if (nullptr == pDS) + return; + + std::unique_ptr<Dataset> pDS2; + pDS2.reset(m_pDB->CreateDataset()); + if (nullptr == pDS2) + return; + + // if we're exporting to a single folder, we export thumbs as well + std::string exportRoot = URIUtils::AddFileToFolder(path, "kodi_videodb_" + CDateTime::GetCurrentDateTime().GetAsDBDate()); + std::string xmlFile = URIUtils::AddFileToFolder(exportRoot, "videodb.xml"); + std::string actorsDir = URIUtils::AddFileToFolder(exportRoot, "actors"); + std::string moviesDir = URIUtils::AddFileToFolder(exportRoot, "movies"); + std::string movieSetsDir = URIUtils::AddFileToFolder(exportRoot, "moviesets"); + std::string musicvideosDir = URIUtils::AddFileToFolder(exportRoot, "musicvideos"); + std::string tvshowsDir = URIUtils::AddFileToFolder(exportRoot, "tvshows"); + if (singleFile) + { + images = true; + overwrite = false; + actorThumbs = true; + CDirectory::Remove(exportRoot); + CDirectory::Create(exportRoot); + CDirectory::Create(actorsDir); + CDirectory::Create(moviesDir); + CDirectory::Create(movieSetsDir); + CDirectory::Create(musicvideosDir); + CDirectory::Create(tvshowsDir); + } + + progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + // find all movies + std::string sql = "select * from movie_view"; + + m_pDS->query(sql); + + if (progress) + { + progress->SetHeading(CVariant{647}); + progress->SetLine(0, CVariant{650}); + progress->SetLine(1, CVariant{""}); + progress->SetLine(2, CVariant{""}); + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + + int total = m_pDS->num_rows(); + int current = 0; + + // create our xml document + CXBMCTinyXML xmlDoc; + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + xmlDoc.InsertEndChild(decl); + TiXmlNode *pMain = NULL; + if (!singleFile) + pMain = &xmlDoc; + else + { + TiXmlElement xmlMainElement("videodb"); + pMain = xmlDoc.InsertEndChild(xmlMainElement); + XMLUtils::SetInt(pMain,"version", GetExportVersion()); + } + + while (!m_pDS->eof()) + { + CVideoInfoTag movie = GetDetailsForMovie(m_pDS, VideoDbDetailsAll); + // strip paths to make them relative + if (StringUtils::StartsWith(movie.m_strTrailer, movie.m_strPath)) + movie.m_strTrailer = movie.m_strTrailer.substr(movie.m_strPath.size()); + std::map<std::string, std::string> artwork; + if (GetArtForItem(movie.m_iDbId, movie.m_type, artwork) && singleFile) + { + TiXmlElement additionalNode("art"); + for (const auto &i : artwork) + XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second); + movie.Save(pMain, "movie", true, &additionalNode); + } + else + movie.Save(pMain, "movie", singleFile); + + // reset old skip state + bool bSkip = false; + + if (progress) + { + progress->SetLine(1, CVariant{movie.m_strTitle}); + progress->SetPercentage(current * 100 / total); + progress->Progress(); + if (progress->IsCanceled()) + { + progress->Close(); + m_pDS->close(); + return; + } + } + + CFileItem item(movie.m_strFileNameAndPath,false); + if (!singleFile && CUtil::SupportsWriteFileOperations(movie.m_strFileNameAndPath)) + { + if (!item.Exists(false)) + { + CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__, + movie.m_strFileNameAndPath); + bSkip = true; + } + else + { + std::string nfoFile(URIUtils::ReplaceExtension(item.GetTBNFile(), ".nfo")); + + if (item.IsOpticalMediaFile()) + { + nfoFile = URIUtils::AddFileToFolder( + URIUtils::GetParentPath(nfoFile), + URIUtils::GetFileName(nfoFile)); + } + + if (overwrite || !CFile::Exists(nfoFile, false)) + { + if(!xmlDoc.SaveFile(nfoFile)) + { + CLog::Log(LOGERROR, "{}: Movie nfo export failed! ('{}')", __FUNCTION__, nfoFile); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, + g_localizeStrings.Get(20302), + CURL::GetRedacted(nfoFile)); + iFailCount++; + } + } + } + } + if (!singleFile) + { + xmlDoc.Clear(); + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + xmlDoc.InsertEndChild(decl); + } + + if (images && !bSkip) + { + if (singleFile) + { + std::string strFileName(movie.m_strTitle); + if (movie.HasYear()) + strFileName += StringUtils::Format("_{}", movie.GetYear()); + item.SetPath(GetSafeFile(moviesDir, strFileName) + ".avi"); + } + for (const auto &i : artwork) + { + std::string savedThumb = item.GetLocalArt(i.first, false); + CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite); + } + if (actorThumbs) + ExportActorThumbs(actorsDir, movie, !singleFile, overwrite); + } + m_pDS->next(); + current++; + } + m_pDS->close(); + + if (!singleFile) + movieSetsDir = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER); + if (images && !movieSetsDir.empty()) + { + // find all movie sets + sql = "select idSet, strSet from sets"; + + m_pDS->query(sql); + + total = m_pDS->num_rows(); + current = 0; + + while (!m_pDS->eof()) + { + std::string title = m_pDS->fv("strSet").get_asString(); + + if (progress) + { + progress->SetLine(1, CVariant{title}); + progress->SetPercentage(current * 100 / total); + progress->Progress(); + if (progress->IsCanceled()) + { + progress->Close(); + m_pDS->close(); + return; + } + } + + std::string itemPath = URIUtils::AddFileToFolder(movieSetsDir, + CUtil::MakeLegalFileName(title, LEGAL_WIN32_COMPAT)); + if (CDirectory::Exists(itemPath) || CDirectory::Create(itemPath)) + { + std::map<std::string, std::string> artwork; + GetArtForItem(m_pDS->fv("idSet").get_asInt(), MediaTypeVideoCollection, artwork); + for (const auto& art : artwork) + { + std::string savedThumb = URIUtils::AddFileToFolder(itemPath, art.first); + CServiceBroker::GetTextureCache()->Export(art.second, savedThumb, overwrite); + } + } + else + CLog::Log( + LOGDEBUG, + "CVideoDatabase::{} - Not exporting movie set '{}' as could not create folder '{}'", + __FUNCTION__, title, itemPath); + m_pDS->next(); + current++; + } + m_pDS->close(); + } + + // find all musicvideos + sql = "select * from musicvideo_view"; + + m_pDS->query(sql); + + total = m_pDS->num_rows(); + current = 0; + + while (!m_pDS->eof()) + { + CVideoInfoTag movie = GetDetailsForMusicVideo(m_pDS, VideoDbDetailsAll); + std::map<std::string, std::string> artwork; + if (GetArtForItem(movie.m_iDbId, movie.m_type, artwork) && singleFile) + { + TiXmlElement additionalNode("art"); + for (const auto &i : artwork) + XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second); + movie.Save(pMain, "musicvideo", true, &additionalNode); + } + else + movie.Save(pMain, "musicvideo", singleFile); + + // reset old skip state + bool bSkip = false; + + if (progress) + { + progress->SetLine(1, CVariant{movie.m_strTitle}); + progress->SetPercentage(current * 100 / total); + progress->Progress(); + if (progress->IsCanceled()) + { + progress->Close(); + m_pDS->close(); + return; + } + } + + CFileItem item(movie.m_strFileNameAndPath,false); + if (!singleFile && CUtil::SupportsWriteFileOperations(movie.m_strFileNameAndPath)) + { + if (!item.Exists(false)) + { + CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__, + movie.m_strFileNameAndPath); + bSkip = true; + } + else + { + std::string nfoFile(URIUtils::ReplaceExtension(item.GetTBNFile(), ".nfo")); + + if (overwrite || !CFile::Exists(nfoFile, false)) + { + if(!xmlDoc.SaveFile(nfoFile)) + { + CLog::Log(LOGERROR, "{}: Musicvideo nfo export failed! ('{}')", __FUNCTION__, + nfoFile); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, + g_localizeStrings.Get(20302), + CURL::GetRedacted(nfoFile)); + iFailCount++; + } + } + } + } + if (!singleFile) + { + xmlDoc.Clear(); + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + xmlDoc.InsertEndChild(decl); + } + if (images && !bSkip) + { + if (singleFile) + { + std::string strFileName(StringUtils::Join(movie.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator) + "." + movie.m_strTitle); + if (movie.HasYear()) + strFileName += StringUtils::Format("_{}", movie.GetYear()); + item.SetPath(GetSafeFile(musicvideosDir, strFileName) + ".avi"); + } + for (const auto &i : artwork) + { + std::string savedThumb = item.GetLocalArt(i.first, false); + CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite); + } + } + m_pDS->next(); + current++; + } + m_pDS->close(); + + // repeat for all tvshows + sql = "SELECT * FROM tvshow_view"; + m_pDS->query(sql); + + total = m_pDS->num_rows(); + current = 0; + + while (!m_pDS->eof()) + { + CVideoInfoTag tvshow = GetDetailsForTvShow(m_pDS, VideoDbDetailsAll); + GetTvShowNamedSeasons(tvshow.m_iDbId, tvshow.m_namedSeasons); + + std::map<int, std::map<std::string, std::string> > seasonArt; + GetTvShowSeasonArt(tvshow.m_iDbId, seasonArt); + + std::map<std::string, std::string> artwork; + if (GetArtForItem(tvshow.m_iDbId, tvshow.m_type, artwork) && singleFile) + { + TiXmlElement additionalNode("art"); + for (const auto &i : artwork) + XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second); + for (const auto &i : seasonArt) + { + TiXmlElement seasonNode("season"); + seasonNode.SetAttribute("num", i.first); + for (const auto &j : i.second) + XMLUtils::SetString(&seasonNode, j.first.c_str(), j.second); + additionalNode.InsertEndChild(seasonNode); + } + tvshow.Save(pMain, "tvshow", true, &additionalNode); + } + else + tvshow.Save(pMain, "tvshow", singleFile); + + // reset old skip state + bool bSkip = false; + + if (progress) + { + progress->SetLine(1, CVariant{tvshow.m_strTitle}); + progress->SetPercentage(current * 100 / total); + progress->Progress(); + if (progress->IsCanceled()) + { + progress->Close(); + m_pDS->close(); + return; + } + } + + CFileItem item(tvshow.m_strPath, true); + if (!singleFile && CUtil::SupportsWriteFileOperations(tvshow.m_strPath)) + { + if (!item.Exists(false)) + { + CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__, + tvshow.m_strPath); + bSkip = true; + } + else + { + std::string nfoFile = URIUtils::AddFileToFolder(tvshow.m_strPath, "tvshow.nfo"); + + if (overwrite || !CFile::Exists(nfoFile, false)) + { + if(!xmlDoc.SaveFile(nfoFile)) + { + CLog::Log(LOGERROR, "{}: TVShow nfo export failed! ('{}')", __FUNCTION__, nfoFile); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, + g_localizeStrings.Get(20302), + CURL::GetRedacted(nfoFile)); + iFailCount++; + } + } + } + } + if (!singleFile) + { + xmlDoc.Clear(); + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + xmlDoc.InsertEndChild(decl); + } + if (images && !bSkip) + { + if (singleFile) + item.SetPath(GetSafeFile(tvshowsDir, tvshow.m_strTitle)); + + for (const auto &i : artwork) + { + std::string savedThumb = item.GetLocalArt(i.first, true); + CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite); + } + + if (actorThumbs) + ExportActorThumbs(actorsDir, tvshow, !singleFile, overwrite); + + // export season thumbs + for (const auto &i : seasonArt) + { + std::string seasonThumb; + if (i.first == -1) + seasonThumb = "season-all"; + else if (i.first == 0) + seasonThumb = "season-specials"; + else + seasonThumb = StringUtils::Format("season{:02}", i.first); + for (const auto &j : i.second) + { + std::string savedThumb(item.GetLocalArt(seasonThumb + "-" + j.first, true)); + if (!i.second.empty()) + CServiceBroker::GetTextureCache()->Export(j.second, savedThumb, overwrite); + } + } + } + + // now save the episodes from this show + sql = PrepareSQL("select * from episode_view where idShow=%i order by strFileName, idEpisode",tvshow.m_iDbId); + pDS->query(sql); + std::string showDir(item.GetPath()); + + while (!pDS->eof()) + { + CVideoInfoTag episode = GetDetailsForEpisode(pDS, VideoDbDetailsAll); + std::map<std::string, std::string> artwork; + if (GetArtForItem(episode.m_iDbId, MediaTypeEpisode, artwork) && singleFile) + { + TiXmlElement additionalNode("art"); + for (const auto &i : artwork) + XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second); + episode.Save(pMain->LastChild(), "episodedetails", true, &additionalNode); + } + else if (singleFile) + episode.Save(pMain->LastChild(), "episodedetails", singleFile); + else + episode.Save(pMain, "episodedetails", singleFile); + pDS->next(); + // multi-episode files need dumping to the same XML + while (!singleFile && !pDS->eof() && + episode.m_iFileId == pDS->fv("idFile").get_asInt()) + { + episode = GetDetailsForEpisode(pDS, VideoDbDetailsAll); + episode.Save(pMain, "episodedetails", singleFile); + pDS->next(); + } + + // reset old skip state + bool bSkip = false; + + CFileItem item(episode.m_strFileNameAndPath, false); + if (!singleFile && CUtil::SupportsWriteFileOperations(episode.m_strFileNameAndPath)) + { + if (!item.Exists(false)) + { + CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__, + episode.m_strFileNameAndPath); + bSkip = true; + } + else + { + std::string nfoFile(URIUtils::ReplaceExtension(item.GetTBNFile(), ".nfo")); + + if (overwrite || !CFile::Exists(nfoFile, false)) + { + if(!xmlDoc.SaveFile(nfoFile)) + { + CLog::Log(LOGERROR, "{}: Episode nfo export failed! ('{}')", __FUNCTION__, nfoFile); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, + g_localizeStrings.Get(20302), + CURL::GetRedacted(nfoFile)); + iFailCount++; + } + } + } + } + if (!singleFile) + { + xmlDoc.Clear(); + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + xmlDoc.InsertEndChild(decl); + } + + if (images && !bSkip) + { + if (singleFile) + { + std::string epName = + StringUtils::Format("s{:02}e{:02}.avi", episode.m_iSeason, episode.m_iEpisode); + item.SetPath(URIUtils::AddFileToFolder(showDir, epName)); + } + for (const auto &i : artwork) + { + std::string savedThumb = item.GetLocalArt(i.first, false); + CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite); + } + if (actorThumbs) + ExportActorThumbs(actorsDir, episode, !singleFile, overwrite); + } + } + pDS->close(); + m_pDS->next(); + current++; + } + m_pDS->close(); + + if (!singleFile && progress) + { + progress->SetPercentage(100); + progress->Progress(); + } + + if (singleFile) + { + // now dump path info + std::set<std::string> paths; + GetPaths(paths); + TiXmlElement xmlPathElement("paths"); + TiXmlNode *pPaths = pMain->InsertEndChild(xmlPathElement); + for (const auto &i : paths) + { + bool foundDirectly = false; + SScanSettings settings; + ScraperPtr info = GetScraperForPath(i, settings, foundDirectly); + if (info && foundDirectly) + { + TiXmlElement xmlPathElement2("path"); + TiXmlNode *pPath = pPaths->InsertEndChild(xmlPathElement2); + XMLUtils::SetString(pPath,"url", i); + XMLUtils::SetInt(pPath,"scanrecursive", settings.recurse); + XMLUtils::SetBoolean(pPath,"usefoldernames", settings.parent_name); + XMLUtils::SetString(pPath,"content", TranslateContent(info->Content())); + XMLUtils::SetString(pPath,"scraperpath", info->ID()); + } + } + xmlDoc.SaveFile(xmlFile); + } + CVariant data; + if (singleFile) + { + data["root"] = exportRoot; + data["file"] = xmlFile; + if (iFailCount > 0) + data["failcount"] = iFailCount; + } + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnExport", + data); + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + iFailCount++; + } + + if (progress) + progress->Close(); + + if (iFailCount > 0) + HELPERS::ShowOKDialogText( + CVariant{647}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011), iFailCount)}); +} + +void CVideoDatabase::ExportActorThumbs(const std::string &strDir, const CVideoInfoTag &tag, bool singleFiles, bool overwrite /*=false*/) +{ + std::string strPath(strDir); + if (singleFiles) + { + strPath = URIUtils::AddFileToFolder(tag.m_strPath, ".actors"); + if (!CDirectory::Exists(strPath)) + { + CDirectory::Create(strPath); + CFile::SetHidden(strPath, true); + } + } + + for (const auto &i : tag.m_cast) + { + CFileItem item; + item.SetLabel(i.strName); + if (!i.thumb.empty()) + { + std::string thumbFile(GetSafeFile(strPath, i.strName)); + CServiceBroker::GetTextureCache()->Export(i.thumb, thumbFile, overwrite); + } + } +} + +void CVideoDatabase::ImportFromXML(const std::string &path) +{ + CGUIDialogProgress *progress=NULL; + try + { + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; + + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(URIUtils::AddFileToFolder(path, "videodb.xml"))) + return; + + TiXmlElement *root = xmlDoc.RootElement(); + if (!root) return; + + progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (progress) + { + progress->SetHeading(CVariant{648}); + progress->SetLine(0, CVariant{649}); + progress->SetLine(1, CVariant{330}); + progress->SetLine(2, CVariant{""}); + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + + int iVersion = 0; + XMLUtils::GetInt(root, "version", iVersion); + + CLog::Log(LOGINFO, "{}: Starting import (export version = {})", __FUNCTION__, iVersion); + + TiXmlElement *movie = root->FirstChildElement(); + int current = 0; + int total = 0; + // first count the number of items... + while (movie) + { + if (StringUtils::CompareNoCase(movie->Value(), MediaTypeMovie, 5) == 0 || + StringUtils::CompareNoCase(movie->Value(), MediaTypeTvShow, 6) == 0 || + StringUtils::CompareNoCase(movie->Value(), MediaTypeMusicVideo, 10) == 0) + total++; + movie = movie->NextSiblingElement(); + } + + std::string actorsDir(URIUtils::AddFileToFolder(path, "actors")); + std::string moviesDir(URIUtils::AddFileToFolder(path, "movies")); + std::string movieSetsDir(URIUtils::AddFileToFolder(path, "moviesets")); + std::string musicvideosDir(URIUtils::AddFileToFolder(path, "musicvideos")); + std::string tvshowsDir(URIUtils::AddFileToFolder(path, "tvshows")); + CVideoInfoScanner scanner; + // add paths first (so we have scraper settings available) + TiXmlElement *path = root->FirstChildElement("paths"); + path = path->FirstChildElement(); + while (path) + { + std::string strPath; + if (XMLUtils::GetString(path,"url",strPath) && !strPath.empty()) + AddPath(strPath); + + std::string content; + if (XMLUtils::GetString(path,"content", content) && !content.empty()) + { // check the scraper exists, if so store the path + AddonPtr addon; + std::string id; + XMLUtils::GetString(path,"scraperpath",id); + if (CServiceBroker::GetAddonMgr().GetAddon(id, addon, ADDON::OnlyEnabled::CHOICE_YES)) + { + SScanSettings settings; + ScraperPtr scraper = std::dynamic_pointer_cast<CScraper>(addon); + // FIXME: scraper settings are not exported? + scraper->SetPathSettings(TranslateContent(content), ""); + XMLUtils::GetInt(path,"scanrecursive",settings.recurse); + XMLUtils::GetBoolean(path,"usefoldernames",settings.parent_name); + SetScraperForPath(strPath,scraper,settings); + } + } + path = path->NextSiblingElement(); + } + movie = root->FirstChildElement(); + while (movie) + { + CVideoInfoTag info; + if (StringUtils::CompareNoCase(movie->Value(), MediaTypeMovie, 5) == 0) + { + info.Load(movie); + CFileItem item(info); + bool useFolders = info.m_basePath.empty() ? LookupByFolders(item.GetPath()) : false; + std::string filename = info.m_strTitle; + if (info.HasYear()) + filename += StringUtils::Format("_{}", info.GetYear()); + CFileItem artItem(item); + artItem.SetPath(GetSafeFile(moviesDir, filename) + ".avi"); + scanner.GetArtwork(&artItem, CONTENT_MOVIES, useFolders, true, actorsDir); + item.SetArt(artItem.GetArt()); + if (!item.GetVideoInfoTag()->m_set.title.empty()) + { + std::string setPath = URIUtils::AddFileToFolder(movieSetsDir, + CUtil::MakeLegalFileName(item.GetVideoInfoTag()->m_set.title, LEGAL_WIN32_COMPAT)); + if (CDirectory::Exists(setPath)) + { + CGUIListItem::ArtMap setArt; + CFileItem artItem(setPath, true); + for (const auto& artType : CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection)) + { + std::string artPath = CVideoThumbLoader::GetLocalArt(artItem, artType, true); + if (!artPath.empty()) + { + setArt[artType] = artPath; + } + } + item.AppendArt(setArt, "set"); + } + } + scanner.AddVideo(&item, CONTENT_MOVIES, useFolders, true, NULL, true); + current++; + } + else if (StringUtils::CompareNoCase(movie->Value(), MediaTypeMusicVideo, 10) == 0) + { + info.Load(movie); + CFileItem item(info); + bool useFolders = info.m_basePath.empty() ? LookupByFolders(item.GetPath()) : false; + std::string filename = StringUtils::Join(info.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator) + "." + info.m_strTitle; + if (info.HasYear()) + filename += StringUtils::Format("_{}", info.GetYear()); + CFileItem artItem(item); + artItem.SetPath(GetSafeFile(musicvideosDir, filename) + ".avi"); + scanner.GetArtwork(&artItem, CONTENT_MUSICVIDEOS, useFolders, true, actorsDir); + item.SetArt(artItem.GetArt()); + scanner.AddVideo(&item, CONTENT_MUSICVIDEOS, useFolders, true, NULL, true); + current++; + } + else if (StringUtils::CompareNoCase(movie->Value(), MediaTypeTvShow, 6) == 0) + { + // load the TV show in. NOTE: This deletes all episodes under the TV Show, which may not be + // what we desire. It may make better sense to only delete (or even better, update) the show information + info.Load(movie); + URIUtils::AddSlashAtEnd(info.m_strPath); + DeleteTvShow(info.m_strPath); + CFileItem showItem(info); + bool useFolders = info.m_basePath.empty() ? LookupByFolders(showItem.GetPath(), true) : false; + CFileItem artItem(showItem); + std::string artPath(GetSafeFile(tvshowsDir, info.m_strTitle)); + artItem.SetPath(artPath); + scanner.GetArtwork(&artItem, CONTENT_TVSHOWS, useFolders, true, actorsDir); + showItem.SetArt(artItem.GetArt()); + int showID = scanner.AddVideo(&showItem, CONTENT_TVSHOWS, useFolders, true, NULL, true); + // season artwork + std::map<int, std::map<std::string, std::string> > seasonArt; + artItem.GetVideoInfoTag()->m_strPath = artPath; + scanner.GetSeasonThumbs(*artItem.GetVideoInfoTag(), seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), true); + for (const auto &i : seasonArt) + { + int seasonID = AddSeason(showID, i.first); + SetArtForItem(seasonID, MediaTypeSeason, i.second); + } + current++; + // now load the episodes + TiXmlElement *episode = movie->FirstChildElement("episodedetails"); + while (episode) + { + // no need to delete the episode info, due to the above deletion + CVideoInfoTag info; + info.Load(episode); + CFileItem item(info); + std::string filename = + StringUtils::Format("s{:02}e{:02}.avi", info.m_iSeason, info.m_iEpisode); + CFileItem artItem(item); + artItem.SetPath(GetSafeFile(artPath, filename)); + scanner.GetArtwork(&artItem, CONTENT_TVSHOWS, useFolders, true, actorsDir); + item.SetArt(artItem.GetArt()); + scanner.AddVideo(&item,CONTENT_TVSHOWS, false, false, showItem.GetVideoInfoTag(), true); + episode = episode->NextSiblingElement("episodedetails"); + } + } + movie = movie->NextSiblingElement(); + if (progress && total) + { + progress->SetPercentage(current * 100 / total); + progress->SetLine(2, CVariant{info.m_strTitle}); + progress->Progress(); + if (progress->IsCanceled()) + { + progress->Close(); + return; + } + } + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } + if (progress) + progress->Close(); +} + +bool CVideoDatabase::ImportArtFromXML(const TiXmlNode *node, std::map<std::string, std::string> &artwork) +{ + if (!node) return false; + const TiXmlNode *art = node->FirstChild(); + while (art && art->FirstChild()) + { + artwork.insert(make_pair(art->ValueStr(), art->FirstChild()->ValueStr())); + art = art->NextSibling(); + } + return !artwork.empty(); +} + +void CVideoDatabase::ConstructPath(std::string& strDest, const std::string& strPath, const std::string& strFileName) +{ + if (URIUtils::IsStack(strFileName) || + URIUtils::IsInArchive(strFileName) || URIUtils::IsPlugin(strPath)) + strDest = strFileName; + else + strDest = URIUtils::AddFileToFolder(strPath, strFileName); +} + +void CVideoDatabase::SplitPath(const std::string& strFileNameAndPath, std::string& strPath, std::string& strFileName) +{ + if (URIUtils::IsStack(strFileNameAndPath) || StringUtils::StartsWithNoCase(strFileNameAndPath, "rar://") || StringUtils::StartsWithNoCase(strFileNameAndPath, "zip://")) + { + URIUtils::GetParentPath(strFileNameAndPath,strPath); + strFileName = strFileNameAndPath; + } + else if (URIUtils::IsPlugin(strFileNameAndPath)) + { + CURL url(strFileNameAndPath); + strPath = url.GetOptions().empty() ? url.GetWithoutFilename() : url.GetWithoutOptions(); + strFileName = strFileNameAndPath; + } + else + { + URIUtils::Split(strFileNameAndPath, strPath, strFileName); + // Keep protocol options as part of the path + if (URIUtils::IsURL(strFileNameAndPath)) + { + CURL url(strFileNameAndPath); + if (!url.GetProtocolOptions().empty()) + strPath += "|" + url.GetProtocolOptions(); + } + } +} + +void CVideoDatabase::InvalidatePathHash(const std::string& strPath) +{ + SScanSettings settings; + bool foundDirectly; + ScraperPtr info = GetScraperForPath(strPath,settings,foundDirectly); + SetPathHash(strPath,""); + if (!info) + return; + if (info->Content() == CONTENT_TVSHOWS || (info->Content() == CONTENT_MOVIES && !foundDirectly)) // if we scan by folder name we need to invalidate parent as well + { + if (info->Content() == CONTENT_TVSHOWS || settings.parent_name_root) + { + std::string strParent; + if (URIUtils::GetParentPath(strPath, strParent) && (!URIUtils::IsPlugin(strPath) || !CURL(strParent).GetHostName().empty())) + SetPathHash(strParent, ""); + } + } +} + +bool CVideoDatabase::CommitTransaction() +{ + if (CDatabase::CommitTransaction()) + { // number of items in the db has likely changed, so recalculate + GUIINFO::CLibraryGUIInfo& guiInfo = CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider(); + guiInfo.SetLibraryBool(LIBRARY_HAS_MOVIES, HasContent(VideoDbContentType::MOVIES)); + guiInfo.SetLibraryBool(LIBRARY_HAS_TVSHOWS, HasContent(VideoDbContentType::TVSHOWS)); + guiInfo.SetLibraryBool(LIBRARY_HAS_MUSICVIDEOS, HasContent(VideoDbContentType::MUSICVIDEOS)); + return true; + } + return false; +} + +bool CVideoDatabase::SetSingleValue(VideoDbContentType type, + int dbId, + int dbField, + const std::string& strValue) +{ + std::string strSQL; + try + { + if (nullptr == m_pDB || nullptr == m_pDS) + return false; + + std::string strTable, strField; + if (type == VideoDbContentType::MOVIES) + { + strTable = "movie"; + strField = "idMovie"; + } + else if (type == VideoDbContentType::TVSHOWS) + { + strTable = "tvshow"; + strField = "idShow"; + } + else if (type == VideoDbContentType::EPISODES) + { + strTable = "episode"; + strField = "idEpisode"; + } + else if (type == VideoDbContentType::MUSICVIDEOS) + { + strTable = "musicvideo"; + strField = "idMVideo"; + } + + if (strTable.empty()) + return false; + + return SetSingleValue(strTable, StringUtils::Format("c{:02}", dbField), strValue, strField, + dbId); + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL); + } + return false; +} + +bool CVideoDatabase::SetSingleValue(VideoDbContentType type, + int dbId, + Field dbField, + const std::string& strValue) +{ + MediaType mediaType = DatabaseUtils::MediaTypeFromVideoContentType(type); + if (mediaType == MediaTypeNone) + return false; + + int dbFieldIndex = DatabaseUtils::GetField(dbField, mediaType); + if (dbFieldIndex < 0) + return false; + + return SetSingleValue(type, dbId, dbFieldIndex, strValue); +} + +bool CVideoDatabase::SetSingleValue(const std::string &table, const std::string &fieldName, const std::string &strValue, + const std::string &conditionName /* = "" */, int conditionValue /* = -1 */) +{ + if (table.empty() || fieldName.empty()) + return false; + + std::string sql; + try + { + if (nullptr == m_pDB || nullptr == m_pDS) + return false; + + sql = PrepareSQL("UPDATE %s SET %s='%s'", table.c_str(), fieldName.c_str(), strValue.c_str()); + if (!conditionName.empty()) + sql += PrepareSQL(" WHERE %s=%u", conditionName.c_str(), conditionValue); + if (m_pDS->exec(sql) == 0) + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, sql); + } + return false; +} + +std::string CVideoDatabase::GetSafeFile(const std::string &dir, const std::string &name) const +{ + std::string safeThumb(name); + StringUtils::Replace(safeThumb, ' ', '_'); + return URIUtils::AddFileToFolder(dir, CUtil::MakeLegalFileName(safeThumb)); +} + +void CVideoDatabase::AnnounceRemove(const std::string& content, int id, bool scanning /* = false */) +{ + CVariant data; + data["type"] = content; + data["id"] = id; + if (scanning) + data["transaction"] = true; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnRemove", data); +} + +void CVideoDatabase::AnnounceUpdate(const std::string& content, int id) +{ + CVariant data; + data["type"] = content; + data["id"] = id; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate", data); +} + +bool CVideoDatabase::GetItemsForPath(const std::string &content, const std::string &strPath, CFileItemList &items) +{ + const std::string& path(strPath); + + if(URIUtils::IsMultiPath(path)) + { + std::vector<std::string> paths; + CMultiPathDirectory::GetPaths(path, paths); + + for(unsigned i=0;i<paths.size();i++) + GetItemsForPath(content, paths[i], items); + + return items.Size() > 0; + } + + int pathID = GetPathId(path); + if (pathID < 0) + return false; + + if (content == "movies") + { + Filter filter(PrepareSQL("c%02d=%d", VIDEODB_ID_PARENTPATHID, pathID)); + GetMoviesByWhere("videodb://movies/titles/", filter, items); + } + else if (content == "episodes") + { + Filter filter(PrepareSQL("c%02d=%d", VIDEODB_ID_EPISODE_PARENTPATHID, pathID)); + GetEpisodesByWhere("videodb://tvshows/titles/", filter, items); + } + else if (content == "tvshows") + { + Filter filter(PrepareSQL("idParentPath=%d", pathID)); + GetTvShowsByWhere("videodb://tvshows/titles/", filter, items); + } + else if (content == "musicvideos") + { + Filter filter(PrepareSQL("c%02d=%d", VIDEODB_ID_MUSICVIDEO_PARENTPATHID, pathID)); + GetMusicVideosByWhere("videodb://musicvideos/titles/", filter, items); + } + for (int i = 0; i < items.Size(); i++) + items[i]->SetPath(items[i]->GetVideoInfoTag()->m_basePath); + return items.Size() > 0; +} + +void CVideoDatabase::AppendIdLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter) +{ + auto option = options.find((std::string)field + "id"); + if (option == options.end()) + return; + + filter.AppendJoin(PrepareSQL("JOIN %s_link ON %s_link.media_id=%s_view.%s AND %s_link.media_type='%s'", field, field, view, viewKey, field, mediaType.c_str())); + filter.AppendWhere(PrepareSQL("%s_link.%s_id = %i", field, table, (int)option->second.asInteger())); +} + +void CVideoDatabase::AppendLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter) +{ + auto option = options.find(field); + if (option == options.end()) + return; + + filter.AppendJoin(PrepareSQL("JOIN %s_link ON %s_link.media_id=%s_view.%s AND %s_link.media_type='%s'", field, field, view, viewKey, field, mediaType.c_str())); + filter.AppendJoin(PrepareSQL("JOIN %s ON %s.%s_id=%s_link.%s_id", table, table, field, table, field)); + filter.AppendWhere(PrepareSQL("%s.name like '%s'", table, option->second.asString().c_str())); +} + +bool CVideoDatabase::GetFilter(CDbUrl &videoUrl, Filter &filter, SortDescription &sorting) +{ + if (!videoUrl.IsValid()) + return false; + + std::string type = videoUrl.GetType(); + std::string itemType = ((const CVideoDbUrl &)videoUrl).GetItemType(); + const CUrlOptions::UrlOptions& options = videoUrl.GetOptions(); + + if (type == "movies") + { + AppendIdLinkFilter("genre", "genre", "movie", "movie", "idMovie", options, filter); + AppendLinkFilter("genre", "genre", "movie", "movie", "idMovie", options, filter); + + AppendIdLinkFilter("country", "country", "movie", "movie", "idMovie", options, filter); + AppendLinkFilter("country", "country", "movie", "movie", "idMovie", options, filter); + + AppendIdLinkFilter("studio", "studio", "movie", "movie", "idMovie", options, filter); + AppendLinkFilter("studio", "studio", "movie", "movie", "idMovie", options, filter); + + AppendIdLinkFilter("director", "actor", "movie", "movie", "idMovie", options, filter); + AppendLinkFilter("director", "actor", "movie", "movie", "idMovie", options, filter); + + auto option = options.find("year"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("movie_view.premiered like '%i%%'", (int)option->second.asInteger())); + + AppendIdLinkFilter("actor", "actor", "movie", "movie", "idMovie", options, filter); + AppendLinkFilter("actor", "actor", "movie", "movie", "idMovie", options, filter); + + option = options.find("setid"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("movie_view.idSet = %i", (int)option->second.asInteger())); + + option = options.find("set"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("movie_view.strSet LIKE '%s'", option->second.asString().c_str())); + + AppendIdLinkFilter("tag", "tag", "movie", "movie", "idMovie", options, filter); + AppendLinkFilter("tag", "tag", "movie", "movie", "idMovie", options, filter); + } + else if (type == "tvshows") + { + if (itemType == "tvshows") + { + AppendIdLinkFilter("genre", "genre", "tvshow", "tvshow", "idShow", options, filter); + AppendLinkFilter("genre", "genre", "tvshow", "tvshow", "idShow", options, filter); + + AppendIdLinkFilter("studio", "studio", "tvshow", "tvshow", "idShow", options, filter); + AppendLinkFilter("studio", "studio", "tvshow", "tvshow", "idShow", options, filter); + + AppendIdLinkFilter("director", "actor", "tvshow", "tvshow", "idShow", options, filter); + + auto option = options.find("year"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("tvshow_view.c%02d like '%%%i%%'", VIDEODB_ID_TV_PREMIERED, (int)option->second.asInteger())); + + AppendIdLinkFilter("actor", "actor", "tvshow", "tvshow", "idShow", options, filter); + AppendLinkFilter("actor", "actor", "tvshow", "tvshow", "idShow", options, filter); + + AppendIdLinkFilter("tag", "tag", "tvshow", "tvshow", "idShow", options, filter); + AppendLinkFilter("tag", "tag", "tvshow", "tvshow", "idShow", options, filter); + } + else if (itemType == "seasons") + { + auto option = options.find("tvshowid"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("season_view.idShow = %i", (int)option->second.asInteger())); + + AppendIdLinkFilter("genre", "genre", "tvshow", "season", "idShow", options, filter); + + AppendIdLinkFilter("director", "actor", "tvshow", "season", "idShow", options, filter); + + option = options.find("year"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("season_view.premiered like '%%%i%%'", (int)option->second.asInteger())); + + AppendIdLinkFilter("actor", "actor", "tvshow", "season", "idShow", options, filter); + } + else if (itemType == "episodes") + { + int idShow = -1; + auto option = options.find("tvshowid"); + if (option != options.end()) + idShow = (int)option->second.asInteger(); + + int season = -1; + option = options.find("season"); + if (option != options.end()) + season = (int)option->second.asInteger(); + + if (idShow > -1) + { + bool condition = false; + + AppendIdLinkFilter("genre", "genre", "tvshow", "episode", "idShow", options, filter); + AppendLinkFilter("genre", "genre", "tvshow", "episode", "idShow", options, filter); + + AppendIdLinkFilter("director", "actor", "tvshow", "episode", "idShow", options, filter); + AppendLinkFilter("director", "actor", "tvshow", "episode", "idShow", options, filter); + + option = options.find("year"); + if (option != options.end()) + { + condition = true; + filter.AppendWhere(PrepareSQL("episode_view.idShow = %i and episode_view.premiered like '%%%i%%'", idShow, (int)option->second.asInteger())); + } + + AppendIdLinkFilter("actor", "actor", "tvshow", "episode", "idShow", options, filter); + AppendLinkFilter("actor", "actor", "tvshow", "episode", "idShow", options, filter); + + if (!condition) + filter.AppendWhere(PrepareSQL("episode_view.idShow = %i", idShow)); + + if (season > -1) + { + if (season == 0) // season = 0 indicates a special - we grab all specials here (see below) + filter.AppendWhere(PrepareSQL("episode_view.c%02d = %i", VIDEODB_ID_EPISODE_SEASON, season)); + else + filter.AppendWhere(PrepareSQL("(episode_view.c%02d = %i or (episode_view.c%02d = 0 and (episode_view.c%02d = 0 or episode_view.c%02d = %i)))", + VIDEODB_ID_EPISODE_SEASON, season, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTSEASON, season)); + } + } + else + { + option = options.find("year"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("episode_view.premiered like '%%%i%%'", (int)option->second.asInteger())); + + AppendIdLinkFilter("director", "actor", "episode", "episode", "idEpisode", options, filter); + AppendLinkFilter("director", "actor", "episode", "episode", "idEpisode", options, filter); + } + } + } + else if (type == "musicvideos") + { + AppendIdLinkFilter("genre", "genre", "musicvideo", "musicvideo", "idMVideo", options, filter); + AppendLinkFilter("genre", "genre", "musicvideo", "musicvideo", "idMVideo", options, filter); + + AppendIdLinkFilter("studio", "studio", "musicvideo", "musicvideo", "idMVideo", options, filter); + AppendLinkFilter("studio", "studio", "musicvideo", "musicvideo", "idMVideo", options, filter); + + AppendIdLinkFilter("director", "actor", "musicvideo", "musicvideo", "idMVideo", options, filter); + AppendLinkFilter("director", "actor", "musicvideo", "musicvideo", "idMVideo", options, filter); + + auto option = options.find("year"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("musicvideo_view.premiered like '%i%%'", (int)option->second.asInteger())); + + option = options.find("artistid"); + if (option != options.end()) + { + if (itemType != "albums") + filter.AppendJoin(PrepareSQL("JOIN actor_link ON actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo'")); + filter.AppendWhere(PrepareSQL("actor_link.actor_id = %i", (int)option->second.asInteger())); + } + + option = options.find("artist"); + if (option != options.end()) + { + if (itemType != "albums") + { + filter.AppendJoin(PrepareSQL("JOIN actor_link ON actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo'")); + filter.AppendJoin(PrepareSQL("JOIN actor ON actor.actor_id=actor_link.actor_id")); + } + filter.AppendWhere(PrepareSQL("actor.name LIKE '%s'", option->second.asString().c_str())); + } + + option = options.find("albumid"); + if (option != options.end()) + filter.AppendWhere(PrepareSQL("musicvideo_view.c%02d = (select c%02d from musicvideo where idMVideo = %i)", VIDEODB_ID_MUSICVIDEO_ALBUM, VIDEODB_ID_MUSICVIDEO_ALBUM, (int)option->second.asInteger())); + + AppendIdLinkFilter("tag", "tag", "musicvideo", "musicvideo", "idMVideo", options, filter); + AppendLinkFilter("tag", "tag", "musicvideo", "musicvideo", "idMVideo", options, filter); + } + else + return false; + + auto option = options.find("xsp"); + if (option != options.end()) + { + CSmartPlaylist xsp; + if (!xsp.LoadFromJson(option->second.asString())) + return false; + + // check if the filter playlist matches the item type + if (xsp.GetType() == itemType || + (xsp.GetGroup() == itemType && !xsp.IsGroupMixed()) || + // handle episode listings with videodb://tvshows/titles/ which get the rest + // of the path (season and episodeid) appended later + (xsp.GetType() == "episodes" && itemType == "tvshows")) + { + std::set<std::string> playlists; + filter.AppendWhere(xsp.GetWhereClause(*this, playlists)); + + if (xsp.GetLimit() > 0) + sorting.limitEnd = xsp.GetLimit(); + if (xsp.GetOrder() != SortByNone) + sorting.sortBy = xsp.GetOrder(); + if (xsp.GetOrderDirection() != SortOrderNone) + sorting.sortOrder = xsp.GetOrderDirection(); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sorting.sortAttributes = SortAttributeIgnoreArticle; + } + } + + option = options.find("filter"); + if (option != options.end()) + { + CSmartPlaylist xspFilter; + if (!xspFilter.LoadFromJson(option->second.asString())) + return false; + + // check if the filter playlist matches the item type + if (xspFilter.GetType() == itemType) + { + std::set<std::string> playlists; + filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists)); + } + // remove the filter if it doesn't match the item type + else + videoUrl.RemoveOption("filter"); + } + + return true; +} + +bool CVideoDatabase::SetVideoUserRating(int dbId, int rating, const MediaType& mediaType) +{ + try + { + if (nullptr == m_pDB) + return false; + if (nullptr == m_pDS) + return false; + + if (mediaType == MediaTypeNone) + return false; + + std::string sql; + if (mediaType == MediaTypeMovie) + sql = PrepareSQL("UPDATE movie SET userrating=%i WHERE idMovie = %i", rating, dbId); + else if (mediaType == MediaTypeEpisode) + sql = PrepareSQL("UPDATE episode SET userrating=%i WHERE idEpisode = %i", rating, dbId); + else if (mediaType == MediaTypeMusicVideo) + sql = PrepareSQL("UPDATE musicvideo SET userrating=%i WHERE idMVideo = %i", rating, dbId); + else if (mediaType == MediaTypeTvShow) + sql = PrepareSQL("UPDATE tvshow SET userrating=%i WHERE idShow = %i", rating, dbId); + else if (mediaType == MediaTypeSeason) + sql = PrepareSQL("UPDATE seasons SET userrating=%i WHERE idSeason = %i", rating, dbId); + + m_pDS->exec(sql); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{} ({}, {}, {}) failed", __FUNCTION__, dbId, mediaType, rating); + } + return false; +} + +CDateTime CVideoDatabase::GetDateAdded(const std::string& filename, + CDateTime dateAdded /* = CDateTime() */) +{ + if (!dateAdded.IsValid()) + { + // suppress warnings if we have plugin source + if (!URIUtils::IsPlugin(filename)) + { + const auto dateAddedSetting = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryDateAdded; + + // 1 prefer using the files mtime (if it's valid) and + // only use the file's ctime if mtime isn't valid + if (dateAddedSetting == 1) + dateAdded = CFileUtils::GetModificationDate(filename, false); + // 2 use the newer datetime of the file's mtime and ctime + else if (dateAddedSetting == 2) + dateAdded = CFileUtils::GetModificationDate(filename, true); + } + + // 0 use the current datetime if non of the above match or one returns an invalid datetime + if (!dateAdded.IsValid()) + dateAdded = CDateTime::GetCurrentDateTime(); + } + + return dateAdded; +} + +void CVideoDatabase::EraseAllForPath(const std::string& path) +{ + try + { + std::string itemsToDelete; + std::string sql = + PrepareSQL("SELECT files.idFile FROM files WHERE idFile IN (SELECT idFile FROM files INNER " + "JOIN path ON path.idPath = files.idPath AND path.strPath LIKE \"%s%%\")", + path.c_str()); + + m_pDS->query(sql); + while (!m_pDS->eof()) + { + std::string file = m_pDS->fv("files.idFile").get_asString() + ","; + itemsToDelete += file; + m_pDS->next(); + } + m_pDS->close(); + + sql = PrepareSQL("DELETE FROM path WHERE strPath LIKE \"%s%%\"", path.c_str()); + m_pDS->exec(sql); + + if (!itemsToDelete.empty()) + { + itemsToDelete = "(" + StringUtils::TrimRight(itemsToDelete, ",") + ")"; + + sql = "DELETE FROM files WHERE idFile IN " + itemsToDelete; + m_pDS->exec(sql); + + sql = "DELETE FROM settings WHERE idFile IN " + itemsToDelete; + m_pDS->exec(sql); + + sql = "DELETE FROM bookmark WHERE idFile IN " + itemsToDelete; + m_pDS->exec(sql); + + sql = "DELETE FROM streamdetails WHERE idFile IN " + itemsToDelete; + m_pDS->exec(sql); + } + } + catch (...) + { + CLog::Log(LOGERROR, "{} failed", __FUNCTION__); + } +} diff --git a/xbmc/video/VideoDatabase.h b/xbmc/video/VideoDatabase.h new file mode 100644 index 0000000..af04052 --- /dev/null +++ b/xbmc/video/VideoDatabase.h @@ -0,0 +1,1154 @@ +/* + * Copyright (C) 2016-2018 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. + */ + +#pragma once + +#include "Bookmark.h" +#include "VideoInfoTag.h" +#include "addons/Scraper.h" +#include "dbwrappers/Database.h" +#include "utils/SortUtils.h" +#include "utils/UrlOptions.h" + +#include <memory> +#include <set> +#include <utility> +#include <vector> + +class CFileItem; +class CFileItemList; +class CVideoSettings; +class CGUIDialogProgress; +class CGUIDialogProgressBarHandle; + +namespace dbiplus +{ + class field_value; + typedef std::vector<field_value> sql_record; +} + +#ifndef my_offsetof +#ifndef TARGET_POSIX +#define my_offsetof(TYPE, MEMBER) offsetof(TYPE, MEMBER) +#else +/* + Custom version of standard offsetof() macro which can be used to get + offsets of members in class for non-POD types (according to the current + version of C++ standard offsetof() macro can't be used in such cases and + attempt to do so causes warnings to be emitted, OTOH in many cases it is + still OK to assume that all instances of the class has the same offsets + for the same members). + */ +#define my_offsetof(TYPE, MEMBER) \ + ((size_t)((char *)&(((TYPE *)0x10)->MEMBER) - (char*)0x10)) +#endif +#endif + +typedef std::vector<CVideoInfoTag> VECMOVIES; + +namespace VIDEO +{ + class IVideoInfoScannerObserver; + struct SScanSettings; +} + +enum VideoDbDetails +{ + VideoDbDetailsNone = 0x00, + VideoDbDetailsRating = 0x01, + VideoDbDetailsTag = 0x02, + VideoDbDetailsShowLink = 0x04, + VideoDbDetailsStream = 0x08, + VideoDbDetailsCast = 0x10, + VideoDbDetailsBookmark = 0x20, + VideoDbDetailsUniqueID = 0x40, + VideoDbDetailsAll = 0xFF +} ; + +// these defines are based on how many columns we have and which column certain data is going to be in +// when we do GetDetailsForMovie() +#define VIDEODB_MAX_COLUMNS 24 +#define VIDEODB_DETAILS_FILEID 1 + +#define VIDEODB_DETAILS_MOVIE_SET_ID VIDEODB_MAX_COLUMNS + 2 +#define VIDEODB_DETAILS_MOVIE_USER_RATING VIDEODB_MAX_COLUMNS + 3 +#define VIDEODB_DETAILS_MOVIE_PREMIERED VIDEODB_MAX_COLUMNS + 4 +#define VIDEODB_DETAILS_MOVIE_SET_NAME VIDEODB_MAX_COLUMNS + 5 +#define VIDEODB_DETAILS_MOVIE_SET_OVERVIEW VIDEODB_MAX_COLUMNS + 6 +#define VIDEODB_DETAILS_MOVIE_FILE VIDEODB_MAX_COLUMNS + 7 +#define VIDEODB_DETAILS_MOVIE_PATH VIDEODB_MAX_COLUMNS + 8 +#define VIDEODB_DETAILS_MOVIE_PLAYCOUNT VIDEODB_MAX_COLUMNS + 9 +#define VIDEODB_DETAILS_MOVIE_LASTPLAYED VIDEODB_MAX_COLUMNS + 10 +#define VIDEODB_DETAILS_MOVIE_DATEADDED VIDEODB_MAX_COLUMNS + 11 +#define VIDEODB_DETAILS_MOVIE_RESUME_TIME VIDEODB_MAX_COLUMNS + 12 +#define VIDEODB_DETAILS_MOVIE_TOTAL_TIME VIDEODB_MAX_COLUMNS + 13 +#define VIDEODB_DETAILS_MOVIE_PLAYER_STATE VIDEODB_MAX_COLUMNS + 14 +#define VIDEODB_DETAILS_MOVIE_RATING VIDEODB_MAX_COLUMNS + 15 +#define VIDEODB_DETAILS_MOVIE_VOTES VIDEODB_MAX_COLUMNS + 16 +#define VIDEODB_DETAILS_MOVIE_RATING_TYPE VIDEODB_MAX_COLUMNS + 17 +#define VIDEODB_DETAILS_MOVIE_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 18 +#define VIDEODB_DETAILS_MOVIE_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 19 + +#define VIDEODB_DETAILS_EPISODE_TVSHOW_ID VIDEODB_MAX_COLUMNS + 2 +#define VIDEODB_DETAILS_EPISODE_USER_RATING VIDEODB_MAX_COLUMNS + 3 +#define VIDEODB_DETAILS_EPISODE_SEASON_ID VIDEODB_MAX_COLUMNS + 4 +#define VIDEODB_DETAILS_EPISODE_FILE VIDEODB_MAX_COLUMNS + 5 +#define VIDEODB_DETAILS_EPISODE_PATH VIDEODB_MAX_COLUMNS + 6 +#define VIDEODB_DETAILS_EPISODE_PLAYCOUNT VIDEODB_MAX_COLUMNS + 7 +#define VIDEODB_DETAILS_EPISODE_LASTPLAYED VIDEODB_MAX_COLUMNS + 8 +#define VIDEODB_DETAILS_EPISODE_DATEADDED VIDEODB_MAX_COLUMNS + 9 +#define VIDEODB_DETAILS_EPISODE_TVSHOW_NAME VIDEODB_MAX_COLUMNS + 10 +#define VIDEODB_DETAILS_EPISODE_TVSHOW_GENRE VIDEODB_MAX_COLUMNS + 11 +#define VIDEODB_DETAILS_EPISODE_TVSHOW_STUDIO VIDEODB_MAX_COLUMNS + 12 +#define VIDEODB_DETAILS_EPISODE_TVSHOW_AIRED VIDEODB_MAX_COLUMNS + 13 +#define VIDEODB_DETAILS_EPISODE_TVSHOW_MPAA VIDEODB_MAX_COLUMNS + 14 +#define VIDEODB_DETAILS_EPISODE_RESUME_TIME VIDEODB_MAX_COLUMNS + 15 +#define VIDEODB_DETAILS_EPISODE_TOTAL_TIME VIDEODB_MAX_COLUMNS + 16 +#define VIDEODB_DETAILS_EPISODE_PLAYER_STATE VIDEODB_MAX_COLUMNS + 17 +#define VIDEODB_DETAILS_EPISODE_RATING VIDEODB_MAX_COLUMNS + 18 +#define VIDEODB_DETAILS_EPISODE_VOTES VIDEODB_MAX_COLUMNS + 19 +#define VIDEODB_DETAILS_EPISODE_RATING_TYPE VIDEODB_MAX_COLUMNS + 20 +#define VIDEODB_DETAILS_EPISODE_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 21 +#define VIDEODB_DETAILS_EPISODE_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 22 + +#define VIDEODB_DETAILS_TVSHOW_USER_RATING VIDEODB_MAX_COLUMNS + 1 +#define VIDEODB_DETAILS_TVSHOW_DURATION VIDEODB_MAX_COLUMNS + 2 +#define VIDEODB_DETAILS_TVSHOW_PARENTPATHID VIDEODB_MAX_COLUMNS + 3 +#define VIDEODB_DETAILS_TVSHOW_PATH VIDEODB_MAX_COLUMNS + 4 +#define VIDEODB_DETAILS_TVSHOW_DATEADDED VIDEODB_MAX_COLUMNS + 5 +#define VIDEODB_DETAILS_TVSHOW_LASTPLAYED VIDEODB_MAX_COLUMNS + 6 +#define VIDEODB_DETAILS_TVSHOW_NUM_EPISODES VIDEODB_MAX_COLUMNS + 7 +#define VIDEODB_DETAILS_TVSHOW_NUM_WATCHED VIDEODB_MAX_COLUMNS + 8 +#define VIDEODB_DETAILS_TVSHOW_NUM_SEASONS VIDEODB_MAX_COLUMNS + 9 +#define VIDEODB_DETAILS_TVSHOW_RATING VIDEODB_MAX_COLUMNS + 10 +#define VIDEODB_DETAILS_TVSHOW_VOTES VIDEODB_MAX_COLUMNS + 11 +#define VIDEODB_DETAILS_TVSHOW_RATING_TYPE VIDEODB_MAX_COLUMNS + 12 +#define VIDEODB_DETAILS_TVSHOW_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 13 +#define VIDEODB_DETAILS_TVSHOW_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 14 + +#define VIDEODB_DETAILS_MUSICVIDEO_USER_RATING VIDEODB_MAX_COLUMNS + 2 +#define VIDEODB_DETAILS_MUSICVIDEO_PREMIERED VIDEODB_MAX_COLUMNS + 3 +#define VIDEODB_DETAILS_MUSICVIDEO_FILE VIDEODB_MAX_COLUMNS + 4 +#define VIDEODB_DETAILS_MUSICVIDEO_PATH VIDEODB_MAX_COLUMNS + 5 +#define VIDEODB_DETAILS_MUSICVIDEO_PLAYCOUNT VIDEODB_MAX_COLUMNS + 6 +#define VIDEODB_DETAILS_MUSICVIDEO_LASTPLAYED VIDEODB_MAX_COLUMNS + 7 +#define VIDEODB_DETAILS_MUSICVIDEO_DATEADDED VIDEODB_MAX_COLUMNS + 8 +#define VIDEODB_DETAILS_MUSICVIDEO_RESUME_TIME VIDEODB_MAX_COLUMNS + 9 +#define VIDEODB_DETAILS_MUSICVIDEO_TOTAL_TIME VIDEODB_MAX_COLUMNS + 10 +#define VIDEODB_DETAILS_MUSICVIDEO_PLAYER_STATE VIDEODB_MAX_COLUMNS + 11 +#define VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 12 +#define VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 13 + +#define VIDEODB_TYPE_UNUSED 0 +#define VIDEODB_TYPE_STRING 1 +#define VIDEODB_TYPE_INT 2 +#define VIDEODB_TYPE_FLOAT 3 +#define VIDEODB_TYPE_BOOL 4 +#define VIDEODB_TYPE_COUNT 5 +#define VIDEODB_TYPE_STRINGARRAY 6 +#define VIDEODB_TYPE_DATE 7 +#define VIDEODB_TYPE_DATETIME 8 + +enum class VideoDbContentType +{ + UNKNOWN = -1, + MOVIES = 1, + TVSHOWS = 2, + MUSICVIDEOS = 3, + EPISODES = 4, + MOVIE_SETS = 5, + MUSICALBUMS = 6 +}; + +typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets) +{ + VIDEODB_ID_MIN = -1, + VIDEODB_ID_TITLE = 0, + VIDEODB_ID_PLOT = 1, + VIDEODB_ID_PLOTOUTLINE = 2, + VIDEODB_ID_TAGLINE = 3, + VIDEODB_ID_VOTES = 4, // unused + VIDEODB_ID_RATING_ID = 5, + VIDEODB_ID_CREDITS = 6, + VIDEODB_ID_YEAR = 7, // unused + VIDEODB_ID_THUMBURL = 8, + VIDEODB_ID_IDENT_ID = 9, + VIDEODB_ID_SORTTITLE = 10, + VIDEODB_ID_RUNTIME = 11, + VIDEODB_ID_MPAA = 12, + VIDEODB_ID_TOP250 = 13, + VIDEODB_ID_GENRE = 14, + VIDEODB_ID_DIRECTOR = 15, + VIDEODB_ID_ORIGINALTITLE = 16, + VIDEODB_ID_THUMBURL_SPOOF = 17, + VIDEODB_ID_STUDIOS = 18, + VIDEODB_ID_TRAILER = 19, + VIDEODB_ID_FANART = 20, + VIDEODB_ID_COUNTRY = 21, + VIDEODB_ID_BASEPATH = 22, + VIDEODB_ID_PARENTPATHID = 23, + VIDEODB_ID_MAX +} VIDEODB_IDS; + +const struct SDbTableOffsets +{ + int type; + size_t offset; +} DbMovieOffsets[] = +{ + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTitle) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlotOutline) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTagLine) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdRating) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_writingCredits) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strSortTitle) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_duration) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strMPAARating) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iTop250) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_genre) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_director) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strOriginalTitle) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_studio) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTrailer) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_fanart.m_xml) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_country) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_basePath) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_parentPathID) } +}; + +typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets) +{ + VIDEODB_ID_TV_MIN = -1, + VIDEODB_ID_TV_TITLE = 0, + VIDEODB_ID_TV_PLOT = 1, + VIDEODB_ID_TV_STATUS = 2, + VIDEODB_ID_TV_VOTES = 3, // unused + VIDEODB_ID_TV_RATING_ID = 4, + VIDEODB_ID_TV_PREMIERED = 5, + VIDEODB_ID_TV_THUMBURL = 6, + VIDEODB_ID_TV_THUMBURL_SPOOF = 7, + VIDEODB_ID_TV_GENRE = 8, + VIDEODB_ID_TV_ORIGINALTITLE = 9, + VIDEODB_ID_TV_EPISODEGUIDE = 10, + VIDEODB_ID_TV_FANART = 11, + VIDEODB_ID_TV_IDENT_ID = 12, + VIDEODB_ID_TV_MPAA = 13, + VIDEODB_ID_TV_STUDIOS = 14, + VIDEODB_ID_TV_SORTTITLE = 15, + VIDEODB_ID_TV_TRAILER = 16, + VIDEODB_ID_TV_MAX +} VIDEODB_TV_IDS; + +const struct SDbTableOffsets DbTvShowOffsets[] = +{ + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTitle) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strStatus) }, + { VIDEODB_TYPE_UNUSED, 0 }, //unused + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdRating) }, + { VIDEODB_TYPE_DATE, my_offsetof(CVideoInfoTag,m_premiered) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_genre) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strOriginalTitle)}, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strEpisodeGuide)}, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_fanart.m_xml)}, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID)}, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strMPAARating)}, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_studio)}, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strSortTitle)}, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTrailer)} +}; + +//! @todo is this comment valid for seasons? There is no offset structure or am I wrong? +typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets) +{ + VIDEODB_ID_SEASON_MIN = -1, + VIDEODB_ID_SEASON_ID = 0, + VIDEODB_ID_SEASON_TVSHOW_ID = 1, + VIDEODB_ID_SEASON_NUMBER = 2, + VIDEODB_ID_SEASON_NAME = 3, + VIDEODB_ID_SEASON_USER_RATING = 4, + VIDEODB_ID_SEASON_TVSHOW_PATH = 5, + VIDEODB_ID_SEASON_TVSHOW_TITLE = 6, + VIDEODB_ID_SEASON_TVSHOW_PLOT = 7, + VIDEODB_ID_SEASON_TVSHOW_PREMIERED = 8, + VIDEODB_ID_SEASON_TVSHOW_GENRE = 9, + VIDEODB_ID_SEASON_TVSHOW_STUDIO = 10, + VIDEODB_ID_SEASON_TVSHOW_MPAA = 11, + VIDEODB_ID_SEASON_EPISODES_TOTAL = 12, + VIDEODB_ID_SEASON_EPISODES_WATCHED = 13, + VIDEODB_ID_SEASON_PREMIERED = 14, + VIDEODB_ID_SEASON_MAX +} VIDEODB_SEASON_IDS; + +typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets) +{ + VIDEODB_ID_EPISODE_MIN = -1, + VIDEODB_ID_EPISODE_TITLE = 0, + VIDEODB_ID_EPISODE_PLOT = 1, + VIDEODB_ID_EPISODE_VOTES = 2, // unused + VIDEODB_ID_EPISODE_RATING_ID = 3, + VIDEODB_ID_EPISODE_CREDITS = 4, + VIDEODB_ID_EPISODE_AIRED = 5, + VIDEODB_ID_EPISODE_THUMBURL = 6, + VIDEODB_ID_EPISODE_THUMBURL_SPOOF = 7, + VIDEODB_ID_EPISODE_PLAYCOUNT = 8, // unused - feel free to repurpose + VIDEODB_ID_EPISODE_RUNTIME = 9, + VIDEODB_ID_EPISODE_DIRECTOR = 10, + VIDEODB_ID_EPISODE_PRODUCTIONCODE = 11, + VIDEODB_ID_EPISODE_SEASON = 12, + VIDEODB_ID_EPISODE_EPISODE = 13, + VIDEODB_ID_EPISODE_ORIGINALTITLE = 14, + VIDEODB_ID_EPISODE_SORTSEASON = 15, + VIDEODB_ID_EPISODE_SORTEPISODE = 16, + VIDEODB_ID_EPISODE_BOOKMARK = 17, + VIDEODB_ID_EPISODE_BASEPATH = 18, + VIDEODB_ID_EPISODE_PARENTPATHID = 19, + VIDEODB_ID_EPISODE_IDENT_ID = 20, + VIDEODB_ID_EPISODE_MAX +} VIDEODB_EPISODE_IDS; + +const struct SDbTableOffsets DbEpisodeOffsets[] = +{ + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTitle) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdRating) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_writingCredits) }, + { VIDEODB_TYPE_DATE, my_offsetof(CVideoInfoTag,m_firstAired) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_duration) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_director) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strProductionCode) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iSeason) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iEpisode) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strOriginalTitle)}, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iSpecialSortSeason) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iSpecialSortEpisode) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iBookmarkId) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_basePath) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_parentPathID) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID) } +}; + +typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets) +{ + VIDEODB_ID_MUSICVIDEO_MIN = -1, + VIDEODB_ID_MUSICVIDEO_TITLE = 0, + VIDEODB_ID_MUSICVIDEO_THUMBURL = 1, + VIDEODB_ID_MUSICVIDEO_THUMBURL_SPOOF = 2, + VIDEODB_ID_MUSICVIDEO_PLAYCOUNT = 3, // unused - feel free to repurpose + VIDEODB_ID_MUSICVIDEO_RUNTIME = 4, + VIDEODB_ID_MUSICVIDEO_DIRECTOR = 5, + VIDEODB_ID_MUSICVIDEO_STUDIOS = 6, + VIDEODB_ID_MUSICVIDEO_YEAR = 7, // unused + VIDEODB_ID_MUSICVIDEO_PLOT = 8, + VIDEODB_ID_MUSICVIDEO_ALBUM = 9, + VIDEODB_ID_MUSICVIDEO_ARTIST = 10, + VIDEODB_ID_MUSICVIDEO_GENRE = 11, + VIDEODB_ID_MUSICVIDEO_TRACK = 12, + VIDEODB_ID_MUSICVIDEO_BASEPATH = 13, + VIDEODB_ID_MUSICVIDEO_PARENTPATHID = 14, + VIDEODB_ID_MUSICVIDEO_IDENT_ID = 15, + VIDEODB_ID_MUSICVIDEO_MAX +} VIDEODB_MUSICVIDEO_IDS; + +const struct SDbTableOffsets DbMusicVideoOffsets[] = +{ + { VIDEODB_TYPE_STRING, my_offsetof(class CVideoInfoTag,m_strTitle) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_duration) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_director) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_studio) }, + { VIDEODB_TYPE_UNUSED, 0 }, // unused + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strAlbum) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_artist) }, + { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_genre) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iTrack) }, + { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_basePath) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_parentPathID) }, + { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID)} +}; + +#define COMPARE_PERCENTAGE 0.90f // 90% +#define COMPARE_PERCENTAGE_MIN 0.50f // 50% + +class CVideoDatabase : public CDatabase +{ +public: + + class CActor // used for actor retrieval for non-master users + { + public: + std::string name; + std::string thumb; + int playcount; + int appearances; + }; + + class CSeason // used for season retrieval for non-master users + { + public: + std::string path; + std::vector<std::string> genre; + int numEpisodes; + int numWatched; + int id; + }; + + class CSetInfo + { + public: + std::string name; + VECMOVIES movies; + DatabaseResults results; + }; + + CVideoDatabase(void); + ~CVideoDatabase(void) override; + + bool Open() override; + bool CommitTransaction() override; + + int AddNewEpisode(int idShow, CVideoInfoTag& details); + + // editing functions + /*! \brief Set the playcount of an item, update last played time + Sets the playcount and last played date to a given value + \param item CFileItem to set the playcount for + \param count The playcount to set. + \param date The date the file was last viewed (does not denote the video was watched to completion). + If empty we use current datetime (if count > 0) or never viewed (if count = 0). + \return on success, the new last played time set, invalid datetime otherwise. + \sa GetPlayCount, IncrementPlayCount, UpdateLastPlayed + */ + CDateTime SetPlayCount(const CFileItem& item, int count, const CDateTime& date = CDateTime()); + + /*! \brief Increment the playcount of an item + Increments the playcount and updates the last played date + \param item CFileItem to increment the playcount for + \return on success, the new last played time set, invalid datetime otherwise. + \sa GetPlayCount, SetPlayCount, GetPlayCounts + */ + CDateTime IncrementPlayCount(const CFileItem& item); + + /*! \brief Get the playcount of an item + \param item CFileItem to get the playcount for + \return the playcount of the item, or -1 on error + \sa SetPlayCount, IncrementPlayCount, GetPlayCounts + */ + int GetPlayCount(const CFileItem &item); + + /*! \brief Get the playcount of a filename and path + \param strFilenameAndPath filename and path to get the playcount for + \return the playcount of the item, or -1 on error + \sa SetPlayCount, IncrementPlayCount, GetPlayCounts + */ + int GetPlayCount(const std::string& strFilenameAndPath); + + /*! \brief Get the last played time of a filename and path + \param strFilenameAndPath filename and path to get the last played time for + \return the last played time of the item, or an invalid CDateTime on error + \sa UpdateLastPlayed + */ + CDateTime GetLastPlayed(const std::string& strFilenameAndPath); + + /*! \brief Update the last played time of an item + Updates the last played date + \param item CFileItem to update the last played time for + \return on success, the last played time set, invalid datetime otherwise. + \sa GetPlayCount, SetPlayCount, IncrementPlayCount, GetPlayCounts + */ + CDateTime UpdateLastPlayed(const CFileItem& item); + + /*! \brief Get the playcount and resume point of a list of items + Note that if the resume point is already set on an item, it won't be overridden. + \param path the path to fetch videos from + \param items CFileItemList to fetch the playcounts for + \sa GetPlayCount, SetPlayCount, IncrementPlayCount + */ + bool GetPlayCounts(const std::string &path, CFileItemList &items); + + void UpdateMovieTitle(int idMovie, + const std::string& strNewMovieTitle, + VideoDbContentType iType = VideoDbContentType::MOVIES); + bool UpdateVideoSortTitle(int idDb, + const std::string& strNewSortTitle, + VideoDbContentType iType = VideoDbContentType::MOVIES); + + bool HasMovieInfo(const std::string& strFilenameAndPath); + bool HasTvShowInfo(const std::string& strFilenameAndPath); + bool HasEpisodeInfo(const std::string& strFilenameAndPath); + bool HasMusicVideoInfo(const std::string& strFilenameAndPath); + + void GetFilePathById(int idMovie, std::string& filePath, VideoDbContentType iType); + std::string GetGenreById(int id); + std::string GetCountryById(int id); + std::string GetSetById(int id); + std::string GetTagById(int id); + std::string GetPersonById(int id); + std::string GetStudioById(int id); + std::string GetTvShowTitleById(int id); + std::string GetMusicVideoAlbumById(int id); + int GetTvShowForEpisode(int idEpisode); + int GetSeasonForEpisode(int idEpisode); + + bool LoadVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int getDetails = VideoDbDetailsAll); + bool GetMovieInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMovie = -1, int getDetails = VideoDbDetailsAll); + bool GetTvShowInfo(const std::string& strPath, CVideoInfoTag& details, int idTvShow = -1, CFileItem* item = NULL, int getDetails = VideoDbDetailsAll); + bool GetSeasonInfo(int idSeason, CVideoInfoTag& details, CFileItem* item); + bool GetSeasonInfo(int idSeason, CVideoInfoTag& details, bool allDetails = true); + bool GetEpisodeBasicInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode = -1); + bool GetEpisodeInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode = -1, int getDetails = VideoDbDetailsAll); + bool GetMusicVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMVideo = -1, int getDetails = VideoDbDetailsAll); + bool GetSetInfo(int idSet, CVideoInfoTag& details, CFileItem* item = nullptr); + bool GetFileInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idFile = -1); + + int GetPathId(const std::string& strPath); + int GetTvShowId(const std::string& strPath); + int GetEpisodeId(const std::string& strFilenameAndPath, int idEpisode=-1, int idSeason=-1); // idEpisode, idSeason are used for multipart episodes as hints + int GetSeasonId(int idShow, int season); + + void GetEpisodesByFile(const std::string& strFilenameAndPath, std::vector<CVideoInfoTag>& episodes); + + int SetDetailsForItem(CVideoInfoTag& details, const std::map<std::string, std::string> &artwork); + int SetDetailsForItem(int id, const MediaType& mediaType, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork); + + int SetDetailsForMovie(CVideoInfoTag& details, + const std::map<std::string, std::string>& artwork, + int idMovie = -1); + int SetDetailsForMovieSet(const CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, int idSet = -1); + + /*! \brief add a tvshow to the library, setting metadata detail + First checks for whether this TV Show is already in the database (based on idTvShow, or via GetMatchingTvShow) + and if present adds the paths to the show. If not present, we add a new show and set the show metadata. + \param paths a vector<string,string> list of the path(s) and parent path(s) for the show. + \param details a CVideoInfoTag filled with the metadata for the show. + \param artwork the artwork map for the show. + \param seasonArt the artwork map for seasons. + \param idTvShow the database id of the tvshow if known (defaults to -1) + \return the id of the tvshow. + */ + int SetDetailsForTvShow(const std::vector< std::pair<std::string, std::string> > &paths, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, const std::map<int, std::map<std::string, std::string> > &seasonArt, int idTvShow = -1); + bool UpdateDetailsForTvShow(int idTvShow, CVideoInfoTag &details, const std::map<std::string, std::string> &artwork, const std::map<int, std::map<std::string, std::string> > &seasonArt); + int SetDetailsForSeason(const CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, int idShow, int idSeason = -1); + int SetDetailsForEpisode(CVideoInfoTag& details, + const std::map<std::string, std::string>& artwork, + int idShow, + int idEpisode = -1); + int SetDetailsForMusicVideo(CVideoInfoTag& details, + const std::map<std::string, std::string>& artwork, + int idMVideo = -1); + void SetStreamDetailsForFile(const CStreamDetails& details, const std::string &strFileNameAndPath); + void SetStreamDetailsForFileId(const CStreamDetails& details, int idFile); + + bool SetSingleValue(VideoDbContentType type, int dbId, int dbField, const std::string& strValue); + bool SetSingleValue(VideoDbContentType type, + int dbId, + Field dbField, + const std::string& strValue); + bool SetSingleValue(const std::string &table, const std::string &fieldName, const std::string &strValue, + const std::string &conditionName = "", int conditionValue = -1); + + int UpdateDetailsForMovie(int idMovie, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, const std::set<std::string> &updatedDetails); + + void DeleteMovie(int idMovie, bool bKeepId = false); + void DeleteTvShow(int idTvShow, bool bKeepId = false); + void DeleteTvShow(const std::string& strPath); + void DeleteSeason(int idSeason, bool bKeepId = false); + void DeleteEpisode(int idEpisode, bool bKeepId = false); + void DeleteMusicVideo(int idMusicVideo, bool bKeepId = false); + void DeleteDetailsForTvShow(int idTvShow); + void DeleteStreamDetails(int idFile); + void RemoveContentForPath(const std::string& strPath,CGUIDialogProgress *progress = NULL); + void UpdateFanart(const CFileItem& item, VideoDbContentType type); + void DeleteSet(int idSet); + void DeleteTag(int idTag, VideoDbContentType mediaType); + + /*! \brief Get video settings for the specified file id + \param idFile file id to get the settings for + \return true if video settings found, false otherwise + \sa SetVideoSettings + */ + bool GetVideoSettings(int idFile, CVideoSettings &settings); + + /*! \brief Get video settings for the specified file item + \param item item to get the settings for + \return true if video settings found, false otherwise + \sa SetVideoSettings + */ + bool GetVideoSettings(const CFileItem &item, CVideoSettings &settings); + + /*! \brief Get video settings for the specified file path + \param filePath filepath to get the settings for + \return true if video settings found, false otherwise + \sa SetVideoSettings + */ + bool GetVideoSettings(const std::string &filePath, CVideoSettings &settings); + + /*! \brief Set video settings for the specified file path + \param fileItem to set the settings for + \sa GetVideoSettings + */ + void SetVideoSettings(const CFileItem &item, const CVideoSettings &settings); + + /*! \brief Set video settings for the specified file path + \param fileId to set the settings for + \sa GetVideoSettings + */ + void SetVideoSettings(int idFile, const CVideoSettings &settings); + + /** + * Erases video settings for file item + * @param fileitem + */ + void EraseVideoSettings(const CFileItem &item); + + /** + * Erases all video settings + */ + void EraseAllVideoSettings(); + + /** + * Erases video settings for files starting with path + * @param path pattern + */ + void EraseAllVideoSettings(const std::string& path); + + /** + * Erases all entries for files starting with path, including the files and path entries + * @param path pattern + */ + void EraseAllForPath(const std::string& path); + + bool GetStackTimes(const std::string &filePath, std::vector<uint64_t> ×); + void SetStackTimes(const std::string &filePath, const std::vector<uint64_t> ×); + + void GetBookMarksForFile(const std::string& strFilenameAndPath, VECBOOKMARKS& bookmarks, CBookmark::EType type = CBookmark::STANDARD, bool bAppend=false, long partNumber=0); + void AddBookMarkToFile(const std::string& strFilenameAndPath, const CBookmark &bookmark, CBookmark::EType type = CBookmark::STANDARD); + bool GetResumeBookMark(const std::string& strFilenameAndPath, CBookmark &bookmark); + void DeleteResumeBookMark(const CFileItem& item); + void ClearBookMarkOfFile(const std::string& strFilenameAndPath, CBookmark& bookmark, CBookmark::EType type = CBookmark::STANDARD); + void ClearBookMarksOfFile(const std::string& strFilenameAndPath, CBookmark::EType type = CBookmark::STANDARD); + void ClearBookMarksOfFile(int idFile, CBookmark::EType type = CBookmark::STANDARD); + bool GetBookMarkForEpisode(const CVideoInfoTag& tag, CBookmark& bookmark); + void AddBookMarkForEpisode(const CVideoInfoTag& tag, const CBookmark& bookmark); + void DeleteBookMarkForEpisode(const CVideoInfoTag& tag); + bool GetResumePoint(CVideoInfoTag& tag); + bool GetStreamDetails(CFileItem& item); + bool GetStreamDetails(CVideoInfoTag& tag) const; + bool GetDetailsByTypeAndId(CFileItem& item, VideoDbContentType type, int id); + CVideoInfoTag GetDetailsByTypeAndId(VideoDbContentType type, int id); + + // scraper settings + void SetScraperForPath(const std::string& filePath, const ADDON::ScraperPtr& info, const VIDEO::SScanSettings& settings); + ADDON::ScraperPtr GetScraperForPath(const std::string& strPath); + ADDON::ScraperPtr GetScraperForPath(const std::string& strPath, VIDEO::SScanSettings& settings); + + /*! \brief Retrieve the scraper and settings we should use for the specified path + If the scraper is not set on this particular path, we'll recursively check parent folders. + \param strPath path to start searching in. + \param settings [out] scan settings for this folder. + \param foundDirectly [out] true if a scraper was found directly for strPath, false if it was in a parent path. + \return A ScraperPtr containing the scraper information. Returns NULL if a trivial (Content == CONTENT_NONE) + scraper or no scraper is found. + */ + ADDON::ScraperPtr GetScraperForPath(const std::string& strPath, VIDEO::SScanSettings& settings, bool& foundDirectly); + + /*! \brief Retrieve the content type of videos in the given path + If content is set on the folder, we return the given content type, except in the case of tvshows, + where we first check for whether we have episodes directly in the path (thus return episodes) or whether + we've found a scraper directly (shows). Any folders inbetween are treated as seasons (regardless of whether + they actually are seasons). Note that any subfolders in movies will be treated as movies. + \param strPath path to start searching in. + \return A content type string for the current path. + */ + std::string GetContentForPath(const std::string& strPath); + + /*! \brief Get videos of the given content type from the given path + \param content the content type to fetch. + \param path the path to fetch videos from. + \param items the returned items + \return true if items are found, false otherwise. + */ + bool GetItemsForPath(const std::string &content, const std::string &path, CFileItemList &items); + + /*! \brief Check whether a given scraper is in use. + \param scraperID the scraper to check for. + \return true if the scraper is in use, false otherwise. + */ + bool ScraperInUse(const std::string &scraperID) const; + + // scanning hashes and paths scanned + bool SetPathHash(const std::string &path, const std::string &hash); + bool GetPathHash(const std::string &path, std::string &hash); + bool GetPaths(std::set<std::string> &paths); + bool GetPathsForTvShow(int idShow, std::set<int>& paths); + + /*! \brief return the paths linked to a tvshow. + \param idShow the id of the tvshow. + \param paths [out] the list of paths associated with the show. + \return true on success, false on failure. + */ + bool GetPathsLinkedToTvShow(int idShow, std::vector<std::string> &paths); + + /*! \brief retrieve subpaths of a given path. Assumes a hierarchical folder structure + \param basepath the root path to retrieve subpaths for + \param subpaths the returned subpaths + \return true if we successfully retrieve subpaths (may be zero), false on error + */ + bool GetSubPaths(const std::string& basepath, std::vector< std::pair<int, std::string> >& subpaths); + + bool GetSourcePath(const std::string &path, std::string &sourcePath); + bool GetSourcePath(const std::string &path, std::string &sourcePath, VIDEO::SScanSettings& settings); + + // for music + musicvideo linkups - if no album and title given it will return the artist id, else the id of the matching video + int GetMatchingMusicVideo(const std::string& strArtist, const std::string& strAlbum = "", const std::string& strTitle = ""); + + // searching functions + void GetMoviesByActor(const std::string& strActor, CFileItemList& items); + void GetTvShowsByActor(const std::string& strActor, CFileItemList& items); + void GetEpisodesByActor(const std::string& strActor, CFileItemList& items); + + void GetMusicVideosByArtist(const std::string& strArtist, CFileItemList& items); + void GetMusicVideosByAlbum(const std::string& strAlbum, CFileItemList& items); + + void GetMovieGenresByName(const std::string& strSearch, CFileItemList& items); + void GetTvShowGenresByName(const std::string& strSearch, CFileItemList& items); + void GetMusicVideoGenresByName(const std::string& strSearch, CFileItemList& items); + + void GetMovieCountriesByName(const std::string& strSearch, CFileItemList& items); + + void GetMusicVideoAlbumsByName(const std::string& strSearch, CFileItemList& items); + + void GetMovieActorsByName(const std::string& strSearch, CFileItemList& items); + void GetTvShowsActorsByName(const std::string& strSearch, CFileItemList& items); + void GetMusicVideoArtistsByName(const std::string& strSearch, CFileItemList& items); + + void GetMovieDirectorsByName(const std::string& strSearch, CFileItemList& items); + void GetTvShowsDirectorsByName(const std::string& strSearch, CFileItemList& items); + void GetMusicVideoDirectorsByName(const std::string& strSearch, CFileItemList& items); + + void GetMoviesByName(const std::string& strSearch, CFileItemList& items); + void GetTvShowsByName(const std::string& strSearch, CFileItemList& items); + void GetEpisodesByName(const std::string& strSearch, CFileItemList& items); + void GetMusicVideosByName(const std::string& strSearch, CFileItemList& items); + + void GetEpisodesByPlot(const std::string& strSearch, CFileItemList& items); + void GetMoviesByPlot(const std::string& strSearch, CFileItemList& items); + + bool LinkMovieToTvshow(int idMovie, int idShow, bool bRemove); + bool IsLinkedToTvshow(int idMovie); + bool GetLinksToTvShow(int idMovie, std::vector<int>& ids); + + // general browsing + bool GetGenresNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetCountriesNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetStudiosNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetYearsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter()); + bool GetActorsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetDirectorsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetWritersNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetSetsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool ignoreSingleMovieSets = false); + bool GetTagsNav(const std::string& strBaseDir, + CFileItemList& items, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetMusicVideoAlbumsNav(const std::string& strBaseDir, CFileItemList& items, int idArtist, const Filter &filter = Filter(), bool countOnly = false); + + bool GetMoviesNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idActor=-1, int idDirector=-1, int idStudio=-1, int idCountry=-1, int idSet=-1, int idTag=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + bool GetTvShowsNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idActor=-1, int idDirector=-1, int idStudio=-1, int idTag=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + bool GetSeasonsNav(const std::string& strBaseDir, CFileItemList& items, int idActor=-1, int idDirector=-1, int idGenre=-1, int idYear=-1, int idShow=-1, bool getLinkedMovies = true); + bool GetEpisodesNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idActor=-1, int idDirector=-1, int idShow=-1, int idSeason=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + bool GetMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idArtist=-1, int idDirector=-1, int idStudio=-1, int idAlbum=-1, int idTag=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + + bool GetRecentlyAddedMoviesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone); + bool GetRecentlyAddedEpisodesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone); + bool GetRecentlyAddedMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone); + bool GetInProgressTvShowsNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone); + + bool HasContent(); + bool HasContent(VideoDbContentType type); + bool HasSets() const; + + void CleanDatabase(CGUIDialogProgressBarHandle* handle = NULL, const std::set<int>& paths = std::set<int>(), bool showProgress = true); + + /*! \brief Add a file to the database, if necessary + If the file is already in the database, we simply return its id. + \param url - full path of the file to add. + \param parentPath the parent path of the path to add. If empty, URIUtils::GetParentPath() will determine the path. + \param dateAdded datetime when the file was added to the filesystem/database + \param playcount the playcount of the file to add. + \param lastPlayed the date and time when the file to add was last played. + \return id of the file, -1 if it could not be added. + */ + int AddFile(const std::string& url, + const std::string& parentPath = "", + const CDateTime& dateAdded = CDateTime(), + int playcount = 0, + const CDateTime& lastPlayed = CDateTime()); + + /*! \brief Add a file to the database, if necessary + Works for both videodb:// items and normal fileitems + \param item CFileItem to add. + \return id of the file, -1 if it could not be added. + */ + int AddFile(const CFileItem& item); + + /*! \brief Add a file to the database, if necessary + Works for both videodb:// items and normal fileitems + \param url full path of the file to add. + \param details details of the item to add. + \return id of the file, -1 if it could not be added. + */ + int AddFile(const CVideoInfoTag& details, const std::string& parentPath = ""); + + /*! \brief Add a path to the database, if necessary + If the path is already in the database, we simply return its id. + \param strPath the path to add + \param parentPath the parent path of the path to add. If empty, URIUtils::GetParentPath() will determine the path. + \param dateAdded datetime when the path was added to the filesystem/database + \return id of the file, -1 if it could not be added. + */ + int AddPath(const std::string& strPath, const std::string &parentPath = "", const CDateTime& dateAdded = CDateTime()); + + /*! \brief Updates the dateAdded field in the files table for the file + with the given idFile and the given path based on the files modification date + \param details details of the video file + */ + void UpdateFileDateAdded(CVideoInfoTag& details); + + void ExportToXML(const std::string &path, bool singleFile = true, bool images=false, bool actorThumbs=false, bool overwrite=false); + void ExportActorThumbs(const std::string &path, const CVideoInfoTag& tag, bool singleFiles, bool overwrite=false); + void ImportFromXML(const std::string &path); + void DumpToDummyFiles(const std::string &path); + bool ImportArtFromXML(const TiXmlNode *node, std::map<std::string, std::string> &artwork); + + // smart playlists and main retrieval work in these functions + bool GetMoviesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + bool GetSetsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool ignoreSingleMovieSets = false); + bool GetTvShowsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + bool GetSeasonsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath = true, const SortDescription &sortDescription = SortDescription()); + bool GetEpisodesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath = true, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + bool GetMusicVideosByWhere(const std::string &baseDir, const Filter &filter, CFileItemList& items, bool checkLocks = true, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone); + + // retrieve sorted and limited items + bool GetSortedVideos(const MediaType &mediaType, const std::string& strBaseDir, const SortDescription &sortDescription, CFileItemList& items, const Filter &filter = Filter()); + + // retrieve a list of items + bool GetItems(const std::string &strBaseDir, CFileItemList &items, const Filter &filter = Filter(), const SortDescription &sortDescription = SortDescription()); + bool GetItems(const std::string &strBaseDir, const std::string &mediaType, const std::string &itemType, CFileItemList &items, const Filter &filter = Filter(), const SortDescription &sortDescription = SortDescription()); + bool GetItems(const std::string& strBaseDir, + VideoDbContentType mediaType, + const std::string& itemType, + CFileItemList& items, + const Filter& filter = Filter(), + const SortDescription& sortDescription = SortDescription()); + std::string GetItemById(const std::string &itemType, int id); + + // partymode + /*! \brief Gets music video IDs in random order that match the where clause + \param strWhere the SQL where clause to apply in the query + \param songIDs a vector of <2, id> pairs suited to party mode use + \return count of music video IDs found. + */ + unsigned int GetRandomMusicVideoIDs(const std::string& strWhere, std::vector<std::pair<int, int> > &songIDs); + + static void VideoContentTypeToString(VideoDbContentType type, std::string& out) + { + switch (type) + { + case VideoDbContentType::MOVIES: + out = MediaTypeMovie; + break; + case VideoDbContentType::TVSHOWS: + out = MediaTypeTvShow; + break; + case VideoDbContentType::EPISODES: + out = MediaTypeEpisode; + break; + case VideoDbContentType::MUSICVIDEOS: + out = MediaTypeMusicVideo; + break; + default: + break; + } + } + + void SetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType, const std::string &url); + void SetArtForItem(int mediaId, const MediaType &mediaType, const std::map<std::string, std::string> &art); + bool GetArtForItem(int mediaId, const MediaType &mediaType, std::map<std::string, std::string> &art); + std::string GetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType); + bool HasArtForItem(int mediaId, const MediaType &mediaType); + bool RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType); + bool RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::set<std::string> &artTypes); + bool GetTvShowSeasons(int showId, std::map<int, int> &seasons); + bool GetTvShowNamedSeasons(int showId, std::map<int, std::string> &seasons); + + /*! + * \brief Get the custom named season. + * \param tvshowId The tv show id relative to the season. + * \param seasonId The season id for which to search the named title. + * \return The named title if found, otherwise empty. + */ + std::string GetTvShowNamedSeasonById(int tvshowId, int seasonId); + + bool GetTvShowSeasonArt(int mediaId, std::map<int, std::map<std::string, std::string> > &seasonArt); + bool GetArtTypes(const MediaType &mediaType, std::vector<std::string> &artTypes); + + /*! \brief Fetch the distinct types of available-but-unassigned art held in the + database for a specific media item. + \param mediaId the id in the media table. + \param mediaType the type of media, which corresponds to the table the item resides in. + \return the types of art e.g. "thumb", "fanart", etc. + */ + std::vector<std::string> GetAvailableArtTypesForItem(int mediaId, const MediaType& mediaType); + + /*! \brief Fetch the list of available-but-unassigned art URLs held in the + database for a specific media item and art type. + \param mediaId the id in the media table. + \param mediaType corresponds to the table the item resides in. + \param artType e.g. "thumb", "fanart", etc. + \return list of URLs + */ + std::vector<CScraperUrl::SUrlEntry> GetAvailableArtForItem( + int mediaId, const MediaType& mediaType, const std::string& artType); + + int AddTag(const std::string &tag); + void AddTagToItem(int idItem, int idTag, const std::string &type); + void RemoveTagFromItem(int idItem, int idTag, const std::string &type); + void RemoveTagsFromItem(int idItem, const std::string &type); + + bool GetFilter(CDbUrl &videoUrl, Filter &filter, SortDescription &sorting) override; + + /*! \brief Will check if the season exists and if that is not the case add it to the database. + \param showID The id of the show in question. + \param season The season number we want to add. + \return The dbId of the season. + */ + int AddSeason(int showID, int season, const std::string& name = ""); + int AddSet(const std::string& strSet, const std::string& strOverview = ""); + void ClearMovieSet(int idMovie); + void SetMovieSet(int idMovie, int idSet); + bool SetVideoUserRating(int dbId, int rating, const MediaType& mediaType); + bool GetUseAllExternalAudioForVideo(const std::string& videoPath); + +protected: + int AddNewMovie(CVideoInfoTag& details); + int AddNewMusicVideo(CVideoInfoTag& details); + + int GetMovieId(const std::string& strFilenameAndPath); + int GetMusicVideoId(const std::string& strFilenameAndPath); + + /*! \brief Get the id of this fileitem + Works for both videodb:// items and normal fileitems + \param item CFileItem to grab the fileid of + \return id of the file, -1 if it is not in the db. + */ + int GetFileId(const CFileItem &item); + int GetFileId(const CVideoInfoTag& details); + + /*! \brief Get the id of the file of this item and store it in the item + \param details CVideoInfoTag for which to get and store the id of the file + \return id of the file, -1 if it is not in the db. + */ + int GetAndFillFileId(CVideoInfoTag& details); + + /*! \brief Get the id of a file from path + \param url full path to the file + \return id of the file, -1 if it is not in the db. + */ + int GetFileId(const std::string& url); + + int AddToTable(const std::string& table, const std::string& firstField, const std::string& secondField, const std::string& value); + int UpdateRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating); + int AddRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating); + int UpdateUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details); + int AddUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details); + int AddActor(const std::string& strActor, const std::string& thumbURL, const std::string &thumb = ""); + + int AddTvShow(); + + /*! \brief Adds a path to the tvshow link table. + \param idShow the id of the show. + \param path the path to add. + \param parentPath the parent path of the path to add. + \param dateAdded date/time when the path was added + \return true if successfully added, false otherwise. + */ + bool AddPathToTvShow(int idShow, const std::string &path, const std::string &parentPath, const CDateTime& dateAdded = CDateTime()); + + /*! \brief Check whether a show is already in the library. + Matches on unique identifier or matching title and premiered date. + \param show the details of the show to check for. + \return the show id if found, else -1. + */ + int GetMatchingTvShow(const CVideoInfoTag &show); + + // link functions - these two do all the work + void AddLinkToActor(int mediaId, const char *mediaType, int actorId, const std::string &role, int order); + void AddToLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey = NULL); + void RemoveFromLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey = NULL); + + void AddLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values); + void UpdateLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values); + void AddActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values); + void UpdateActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values); + + void AddCast(int mediaId, const char *mediaType, const std::vector<SActorInfo> &cast); + + CVideoInfoTag GetDetailsForMovie(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone); + CVideoInfoTag GetDetailsForMovie(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone); + CVideoInfoTag GetDetailsForTvShow(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone, CFileItem* item = NULL); + CVideoInfoTag GetDetailsForTvShow(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone, CFileItem* item = NULL); + CVideoInfoTag GetBasicDetailsForEpisode(std::unique_ptr<dbiplus::Dataset> &pDS); + CVideoInfoTag GetBasicDetailsForEpisode(const dbiplus::sql_record* const record); + CVideoInfoTag GetDetailsForEpisode(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone); + CVideoInfoTag GetDetailsForEpisode(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone); + CVideoInfoTag GetDetailsForMusicVideo(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone); + CVideoInfoTag GetDetailsForMusicVideo(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone); + bool GetPeopleNav(const std::string& strBaseDir, + CFileItemList& items, + const char* type, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + bool GetNavCommon(const std::string& strBaseDir, + CFileItemList& items, + const char* type, + VideoDbContentType idContent = VideoDbContentType::UNKNOWN, + const Filter& filter = Filter(), + bool countOnly = false); + void GetCast(int media_id, const std::string &media_type, std::vector<SActorInfo> &cast); + void GetTags(int media_id, const std::string &media_type, std::vector<std::string> &tags); + void GetRatings(int media_id, const std::string &media_type, RatingMap &ratings); + void GetUniqueIDs(int media_id, const std::string &media_type, CVideoInfoTag& details); + + void GetDetailsFromDB(std::unique_ptr<dbiplus::Dataset> &pDS, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset = 2); + void GetDetailsFromDB(const dbiplus::sql_record* const record, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset = 2); + std::string GetValueString(const CVideoInfoTag &details, int min, int max, const SDbTableOffsets *offsets) const; + +private: + void CreateTables() override; + void CreateAnalytics() override; + void UpdateTables(int version) override; + void CreateLinkIndex(const char *table); + void CreateForeignLinkIndex(const char *table, const char *foreignkey); + + /*! \brief (Re)Create the generic database views for movies, tvshows, + episodes and music videos + */ + virtual void CreateViews(); + + /*! \brief Helper to get a database id given a query. + Returns an integer, -1 if not found, and greater than 0 if found. + \param query the SQL that will retrieve a database id. + \return -1 if not found, else a valid database id (i.e. > 0) + */ + int GetDbId(const std::string &query); + + /*! \brief Run a query on the main dataset and return the number of rows + If no rows are found we close the dataset and return 0. + \param sql the sql query to run + \return the number of rows, -1 for an error. + */ + int RunQuery(const std::string &sql); + + void AppendIdLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter); + void AppendLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter); + + /*! \brief Determine whether the path is using lookup using folders + \param path the path to check + \param shows whether this path is from a tvshow (defaults to false) + */ + bool LookupByFolders(const std::string &path, bool shows = false); + + /*! \brief Get the playcount for a file id + \param iFileId file id to get the playcount for + \return the playcount of the item, or -1 on error + \sa SetPlayCount, IncrementPlayCount, GetPlayCounts + */ + int GetPlayCount(int iFileId); + + /*! \brief Get the last played time of a filename and path + \param iFileId file id to get the playcount for + \return the last played time of the item, or an invalid CDateTime on error + \sa UpdateLastPlayed + */ + CDateTime GetLastPlayed(int iFileId); + + bool GetSeasonInfo(int idSeason, CVideoInfoTag& details, bool allDetails, CFileItem* item); + + int GetMinSchemaVersion() const override { return 75; } + int GetSchemaVersion() const override; + virtual int GetExportVersion() const { return 1; } + const char* GetBaseDBName() const override { return "MyVideos"; } + + void ConstructPath(std::string& strDest, const std::string& strPath, const std::string& strFileName); + void SplitPath(const std::string& strFileNameAndPath, std::string& strPath, std::string& strFileName); + void InvalidatePathHash(const std::string& strPath); + + /*! \brief Get a safe filename from a given string + \param dir directory to use for the file + \param name movie, show name, or actor to get a safe filename for + \return safe filename based on this title + */ + std::string GetSafeFile(const std::string &dir, const std::string &name) const; + + std::vector<int> CleanMediaType(const std::string &mediaType, const std::string &cleanableFileIDs, + std::map<int, bool> &pathsDeleteDecisions, std::string &deletedFileIDs, bool silent); + + static void AnnounceRemove(const std::string& content, int id, bool scanning = false); + static void AnnounceUpdate(const std::string& content, int id); + + static CDateTime GetDateAdded(const std::string& filename, CDateTime dateAdded = CDateTime()); +}; diff --git a/xbmc/video/VideoDbUrl.cpp b/xbmc/video/VideoDbUrl.cpp new file mode 100644 index 0000000..0bb899c --- /dev/null +++ b/xbmc/video/VideoDbUrl.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2016-2018 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 "VideoDbUrl.h" + +#include "filesystem/VideoDatabaseDirectory.h" +#include "playlists/SmartPlayList.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +using namespace XFILE; + +CVideoDbUrl::CVideoDbUrl() + : CDbUrl() +{ } + +CVideoDbUrl::~CVideoDbUrl() = default; + +bool CVideoDbUrl::parse() +{ + // the URL must start with videodb:// + if (!m_url.IsProtocol("videodb") || m_url.GetFileName().empty()) + return false; + + std::string path = m_url.Get(); + VIDEODATABASEDIRECTORY::NODE_TYPE dirType = CVideoDatabaseDirectory::GetDirectoryType(path); + VIDEODATABASEDIRECTORY::NODE_TYPE childType = CVideoDatabaseDirectory::GetDirectoryChildType(path); + + switch (dirType) + { + case VIDEODATABASEDIRECTORY::NODE_TYPE_MOVIES_OVERVIEW: + case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MOVIES: + case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MOVIES: + case VIDEODATABASEDIRECTORY::NODE_TYPE_SETS: + m_type = "movies"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_TVSHOWS_OVERVIEW: + case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS: + case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS: + case VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES: + case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_EPISODES: + case VIDEODATABASEDIRECTORY::NODE_TYPE_INPROGRESS_TVSHOWS: + m_type = "tvshows"; + break; + + + case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_OVERVIEW: + case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: + case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS: + case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_ALBUM: + m_type = "musicvideos"; + + default: + break; + } + + switch (childType) + { + case VIDEODATABASEDIRECTORY::NODE_TYPE_MOVIES_OVERVIEW: + case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MOVIES: + case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MOVIES: + m_type = "movies"; + m_itemType = "movies"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_TVSHOWS_OVERVIEW: + case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS: + case VIDEODATABASEDIRECTORY::NODE_TYPE_INPROGRESS_TVSHOWS: + m_type = "tvshows"; + m_itemType = "tvshows"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS: + m_type = "tvshows"; + m_itemType = "seasons"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES: + case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_EPISODES: + m_type = "tvshows"; + m_itemType = "episodes"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_OVERVIEW: + case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: + case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS: + m_type = "musicvideos"; + m_itemType = "musicvideos"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE: + m_itemType = "genres"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR: + m_itemType = "actors"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_YEAR: + m_itemType = "years"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_DIRECTOR: + m_itemType = "directors"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_STUDIO: + m_itemType = "studios"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_COUNTRY: + m_itemType = "countries"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_SETS: + m_itemType = "sets"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_ALBUM: + m_type = "musicvideos"; + m_itemType = "albums"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_TAGS: + m_itemType = "tags"; + break; + + case VIDEODATABASEDIRECTORY::NODE_TYPE_ROOT: + case VIDEODATABASEDIRECTORY::NODE_TYPE_OVERVIEW: + default: + return false; + } + + if (m_type.empty() || m_itemType.empty()) + return false; + + // parse query params + VIDEODATABASEDIRECTORY::CQueryParams queryParams; + if (!CVideoDatabaseDirectory::GetQueryParams(path, queryParams)) + return false; + + // retrieve and parse all options + AddOptions(m_url.GetOptions()); + + // add options based on the QueryParams + if (queryParams.GetActorId() != -1) + { + std::string optionName = "actorid"; + if (m_type == "musicvideos") + optionName = "artistid"; + + AddOption(optionName, (int)queryParams.GetActorId()); + } + if (queryParams.GetAlbumId() != -1) + AddOption("albumid", (int)queryParams.GetAlbumId()); + if (queryParams.GetCountryId() != -1) + AddOption("countryid", (int)queryParams.GetCountryId()); + if (queryParams.GetDirectorId() != -1) + AddOption("directorid", (int)queryParams.GetDirectorId()); + if (queryParams.GetEpisodeId() != -1) + AddOption("episodeid", (int)queryParams.GetEpisodeId()); + if (queryParams.GetGenreId() != -1) + AddOption("genreid", (int)queryParams.GetGenreId()); + if (queryParams.GetMovieId() != -1) + AddOption("movieid", (int)queryParams.GetMovieId()); + if (queryParams.GetMVideoId() != -1) + AddOption("musicvideoid", (int)queryParams.GetMVideoId()); + if (queryParams.GetSeason() != -1 && queryParams.GetSeason() >= -2) + AddOption("season", (int)queryParams.GetSeason()); + if (queryParams.GetSetId() != -1) + AddOption("setid", (int)queryParams.GetSetId()); + if (queryParams.GetStudioId() != -1) + AddOption("studioid", (int)queryParams.GetStudioId()); + if (queryParams.GetTvShowId() != -1) + AddOption("tvshowid", (int)queryParams.GetTvShowId()); + if (queryParams.GetYear() != -1) + AddOption("year", (int)queryParams.GetYear()); + + return true; +} + +bool CVideoDbUrl::validateOption(const std::string &key, const CVariant &value) +{ + if (!CDbUrl::validateOption(key, value)) + return false; + + // if the value is empty it will remove the option which is ok + // otherwise we only care about the "filter" option here + if (value.empty() || !StringUtils::EqualsNoCase(key, "filter")) + return true; + + if (!value.isString()) + return false; + + CSmartPlaylist xspFilter; + if (!xspFilter.LoadFromJson(value.asString())) + return false; + + // check if the filter playlist matches the item type + return (xspFilter.GetType() == m_itemType || + (xspFilter.GetType() == "movies" && m_itemType == "sets")); +} diff --git a/xbmc/video/VideoDbUrl.h b/xbmc/video/VideoDbUrl.h new file mode 100644 index 0000000..41d6649 --- /dev/null +++ b/xbmc/video/VideoDbUrl.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012-2018 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. + */ + +#pragma once + +#include "DbUrl.h" + +class CVariant; + +class CVideoDbUrl : public CDbUrl +{ +public: + CVideoDbUrl(); + ~CVideoDbUrl() override; + + const std::string& GetItemType() const { return m_itemType; } + +protected: + bool parse() override; + bool validateOption(const std::string &key, const CVariant &value) override; + +private: + std::string m_itemType; +}; diff --git a/xbmc/video/VideoInfoDownloader.cpp b/xbmc/video/VideoInfoDownloader.cpp new file mode 100644 index 0000000..b47f25c --- /dev/null +++ b/xbmc/video/VideoInfoDownloader.cpp @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2005-2018 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 "VideoInfoDownloader.h" + +#include "dialogs/GUIDialogProgress.h" +#include "filesystem/CurlFile.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "utils/Variant.h" +#include "utils/log.h" + +using namespace VIDEO; +using namespace KODI::MESSAGING; +using namespace std::chrono_literals; + +#ifndef __GNUC__ +#pragma warning (disable:4018) +#endif + +CVideoInfoDownloader::CVideoInfoDownloader(const ADDON::ScraperPtr &scraper) : + CThread("VideoInfoDownloader"), m_state(DO_NOTHING), m_found(0), m_info(scraper) +{ + m_http = new XFILE::CCurlFile; +} + +CVideoInfoDownloader::~CVideoInfoDownloader() +{ + delete m_http; +} + +// return value: 0 = we failed, -1 = we failed and reported an error, 1 = success +int CVideoInfoDownloader::InternalFindMovie(const std::string &movieTitle, int movieYear, + MOVIELIST& movielist, + bool cleanChars /* = true */) +{ + try + { + movielist = m_info->FindMovie(*m_http, movieTitle, movieYear, cleanChars); + } + catch (const ADDON::CScraperError &sce) + { + ShowErrorDialog(sce); + return sce.FAborted() ? 0 : -1; + } + return 1; // success +} + +void CVideoInfoDownloader::ShowErrorDialog(const ADDON::CScraperError &sce) +{ + if (!sce.Title().empty()) + HELPERS::ShowOKDialogText(CVariant{ sce.Title() }, CVariant{ sce.Message() }); +} + +// threaded functions +void CVideoInfoDownloader::Process() +{ + // note here that we're calling our external functions but we're calling them with + // no progress bar set, so they're effectively calling our internal functions directly. + m_found = 0; + if (m_state == FIND_MOVIE) + { + if (!(m_found=FindMovie(m_movieTitle, m_movieYear, m_movieList))) + CLog::Log(LOGERROR, "{}: Error looking up item {} ({})", __FUNCTION__, m_movieTitle, + m_movieYear); + m_state = DO_NOTHING; + return; + } + + if (!m_url.HasUrls()) + { + // empty url when it's not supposed to be.. + // this might happen if the previously scraped item was removed from the site (see ticket #10537) + CLog::Log(LOGERROR, "{}: Error getting details for {} ({}) due to an empty url", __FUNCTION__, + m_movieTitle, m_movieYear); + } + else if (m_state == GET_DETAILS) + { + if (!GetDetails(m_url, m_movieDetails)) + CLog::Log(LOGERROR, "{}: Error getting details from {}", __FUNCTION__, + m_url.GetFirstThumbUrl()); + } + else if (m_state == GET_EPISODE_DETAILS) + { + if (!GetEpisodeDetails(m_url, m_movieDetails)) + CLog::Log(LOGERROR, "{}: Error getting episode details from {}", __FUNCTION__, + m_url.GetFirstThumbUrl()); + } + else if (m_state == GET_EPISODE_LIST) + { + if (!GetEpisodeList(m_url, m_episode)) + CLog::Log(LOGERROR, "{}: Error getting episode list from {}", __FUNCTION__, + m_url.GetFirstThumbUrl()); + } + m_found = 1; + m_state = DO_NOTHING; +} + +int CVideoInfoDownloader::FindMovie(const std::string &movieTitle, int movieYear, + MOVIELIST& movieList, + CGUIDialogProgress *pProgress /* = NULL */) +{ + //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::FindMovie({})", strMovie); + + if (pProgress) + { // threaded version + m_state = FIND_MOVIE; + m_movieTitle = movieTitle; + m_movieYear = movieYear; + m_found = 0; + if (IsRunning()) + StopThread(); + Create(); + while (m_state != DO_NOTHING) + { + pProgress->Progress(); + if (pProgress->IsCanceled()) + { + CloseThread(); + return 0; + } + CThread::Sleep(1ms); + } + // transfer to our movielist + m_movieList.swap(movieList); + int found=m_found; + CloseThread(); + return found; + } + + // unthreaded + int success = InternalFindMovie(movieTitle, movieYear, movieList); + // NOTE: this might be improved by rescraping if the match quality isn't high? + if (success == 1 && movieList.empty()) + { // no results. try without cleaning chars like '.' and '_' + success = InternalFindMovie(movieTitle, movieYear, movieList, false); + } + return success; +} + +bool CVideoInfoDownloader::GetArtwork(CVideoInfoTag &details) +{ + return m_info->GetArtwork(*m_http, details); +} + +bool CVideoInfoDownloader::GetDetails(const CScraperUrl &url, + CVideoInfoTag &movieDetails, + CGUIDialogProgress *pProgress /* = NULL */) +{ + //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::GetDetails({})", url.m_strURL); + m_url = url; + m_movieDetails = movieDetails; + + // fill in the defaults + movieDetails.Reset(); + if (pProgress) + { // threaded version + m_state = GET_DETAILS; + m_found = 0; + if (IsRunning()) + StopThread(); + Create(); + while (!m_found) + { + pProgress->Progress(); + if (pProgress->IsCanceled()) + { + CloseThread(); + return false; + } + CThread::Sleep(1ms); + } + movieDetails = m_movieDetails; + CloseThread(); + return true; + } + else // unthreaded + return m_info->GetVideoDetails(*m_http, url, true/*fMovie*/, movieDetails); +} + +bool CVideoInfoDownloader::GetEpisodeDetails(const CScraperUrl &url, + CVideoInfoTag &movieDetails, + CGUIDialogProgress *pProgress /* = NULL */) +{ + //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::GetDetails({})", url.m_strURL); + m_url = url; + m_movieDetails = movieDetails; + + // fill in the defaults + movieDetails.Reset(); + if (pProgress) + { // threaded version + m_state = GET_EPISODE_DETAILS; + m_found = 0; + if (IsRunning()) + StopThread(); + Create(); + while (!m_found) + { + pProgress->Progress(); + if (pProgress->IsCanceled()) + { + CloseThread(); + return false; + } + CThread::Sleep(1ms); + } + movieDetails = m_movieDetails; + CloseThread(); + return true; + } + else // unthreaded + return m_info->GetVideoDetails(*m_http, url, false/*fMovie*/, movieDetails); +} + +bool CVideoInfoDownloader::GetEpisodeList(const CScraperUrl& url, + EPISODELIST& movieDetails, + CGUIDialogProgress *pProgress /* = NULL */) +{ + //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::GetDetails({})", url.m_strURL); + m_url = url; + m_episode = movieDetails; + + // fill in the defaults + movieDetails.clear(); + if (pProgress) + { // threaded version + m_state = GET_EPISODE_LIST; + m_found = 0; + if (IsRunning()) + StopThread(); + Create(); + while (!m_found) + { + pProgress->Progress(); + if (pProgress->IsCanceled()) + { + CloseThread(); + return false; + } + CThread::Sleep(1ms); + } + movieDetails = m_episode; + CloseThread(); + return true; + } + else // unthreaded + return !(movieDetails = m_info->GetEpisodeList(*m_http, url)).empty(); +} + +void CVideoInfoDownloader::CloseThread() +{ + m_http->Cancel(); + StopThread(); + m_http->Reset(); + m_state = DO_NOTHING; + m_found = 0; +} + diff --git a/xbmc/video/VideoInfoDownloader.h b/xbmc/video/VideoInfoDownloader.h new file mode 100644 index 0000000..5a0f893 --- /dev/null +++ b/xbmc/video/VideoInfoDownloader.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "Episode.h" +#include "VideoInfoTag.h" +#include "addons/Scraper.h" +#include "threads/Thread.h" + +#include <string> +#include <vector> + +// forward declarations +class CXBMCTinyXML; +class CGUIDialogProgress; + +namespace ADDON +{ +class CScraperError; +} +namespace XFILE +{ +class CurlFile; +} + +typedef std::vector<CScraperUrl> MOVIELIST; + +class CVideoInfoDownloader : public CThread +{ +public: + explicit CVideoInfoDownloader(const ADDON::ScraperPtr &scraper); + ~CVideoInfoDownloader() override; + + // threaded lookup functions + + /*! \brief Do a search for matching media items (possibly asynchronously) with our scraper + \param movieTitle title of the media item to look for + \param movieYear year of the media item to look for (-1 if not known) + \param movielist [out] list of results to fill. May be empty on success. + \param pProgress progress bar to update as we go. If NULL we run on thread, if non-NULL we run off thread. + \return 1 on success, -1 on a scraper-specific error, 0 on some other error + */ + int FindMovie(const std::string& movieTitle, int movieYear, MOVIELIST& movielist, CGUIDialogProgress *pProgress = NULL); + + /*! \brief Fetch art URLs for an item with our scraper + \param details the video info tag structure to fill with art. + \return true on success, false on failure. + */ + bool GetArtwork(CVideoInfoTag &details); + + bool GetDetails(const CScraperUrl& url, CVideoInfoTag &movieDetails, CGUIDialogProgress *pProgress = NULL); + bool GetEpisodeDetails(const CScraperUrl& url, CVideoInfoTag &movieDetails, CGUIDialogProgress *pProgress = NULL); + bool GetEpisodeList(const CScraperUrl& url, VIDEO::EPISODELIST& details, CGUIDialogProgress *pProgress = NULL); + + static void ShowErrorDialog(const ADDON::CScraperError &sce); + +protected: + enum LOOKUP_STATE { DO_NOTHING = 0, + FIND_MOVIE = 1, + GET_DETAILS = 2, + GET_EPISODE_LIST = 3, + GET_EPISODE_DETAILS = 4 }; + + XFILE::CCurlFile* m_http; + std::string m_movieTitle; + int m_movieYear; + MOVIELIST m_movieList; + CVideoInfoTag m_movieDetails; + CScraperUrl m_url; + VIDEO::EPISODELIST m_episode; + LOOKUP_STATE m_state; + int m_found; + ADDON::ScraperPtr m_info; + + // threaded stuff + void Process() override; + void CloseThread(); + + int InternalFindMovie(const std::string& movieTitle, int movieYear, MOVIELIST& movielist, bool cleanChars = true); +}; + diff --git a/xbmc/video/VideoInfoScanner.cpp b/xbmc/video/VideoInfoScanner.cpp new file mode 100644 index 0000000..92fa5c1 --- /dev/null +++ b/xbmc/video/VideoInfoScanner.cpp @@ -0,0 +1,2247 @@ +/* + * Copyright (C) 2005-2018 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 "VideoInfoScanner.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "GUIUserMessages.h" +#include "NfoFile.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "URL.h" +#include "Util.h" +#include "VideoInfoDownloader.h" +#include "cores/VideoPlayer/DVDFileInfo.h" +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "dialogs/GUIDialogProgress.h" +#include "events/EventLog.h" +#include "events/MediaLibraryEvent.h" +#include "filesystem/Directory.h" +#include "filesystem/DirectoryCache.h" +#include "filesystem/File.h" +#include "filesystem/MultiPathDirectory.h" +#include "filesystem/PluginDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "interfaces/AnnouncementManager.h" +#include "messaging/helpers/DialogHelper.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "tags/VideoInfoTagLoaderFactory.h" +#include "utils/Digest.h" +#include "utils/FileExtensionProvider.h" +#include "utils/RegExp.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoThumbLoader.h" + +#include <algorithm> +#include <utility> + +using namespace XFILE; +using namespace ADDON; +using namespace KODI::MESSAGING; + +using KODI::MESSAGING::HELPERS::DialogResponse; +using KODI::UTILITY::CDigest; + +namespace VIDEO +{ + + CVideoInfoScanner::CVideoInfoScanner() + { + m_bStop = false; + m_scanAll = false; + } + + CVideoInfoScanner::~CVideoInfoScanner() + = default; + + void CVideoInfoScanner::Process() + { + m_bStop = false; + + try + { + if (m_showDialog && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE)) + { + CGUIDialogExtendedProgressBar* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS); + if (dialog) + m_handle = dialog->GetHandle(g_localizeStrings.Get(314)); + } + + // check if we only need to perform a cleaning + if (m_bClean && m_pathsToScan.empty()) + { + std::set<int> paths; + m_database.CleanDatabase(m_handle, paths, false); + + if (m_handle) + m_handle->MarkFinished(); + m_handle = NULL; + + m_bRunning = false; + + return; + } + + auto start = std::chrono::steady_clock::now(); + + m_database.Open(); + + m_bCanInterrupt = true; + + CLog::Log(LOGINFO, "VideoInfoScanner: Starting scan .."); + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, + "OnScanStarted"); + + // Database operations should not be canceled + // using Interrupt() while scanning as it could + // result in unexpected behaviour. + m_bCanInterrupt = false; + + bool bCancelled = false; + while (!bCancelled && !m_pathsToScan.empty()) + { + /* + * A copy of the directory path is used because the path supplied is + * immediately removed from the m_pathsToScan set in DoScan(). If the + * reference points to the entry in the set a null reference error + * occurs. + */ + std::string directory = *m_pathsToScan.begin(); + if (m_bStop) + { + bCancelled = true; + } + else if (!CDirectory::Exists(directory)) + { + /* + * Note that this will skip clean (if m_bClean is enabled) if the directory really + * doesn't exist rather than a NAS being switched off. A manual clean from settings + * will still pick up and remove it though. + */ + CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan{}.", __FUNCTION__, + CURL::GetRedacted(directory), m_bClean ? " and clean" : ""); + m_pathsToScan.erase(m_pathsToScan.begin()); + } + else if (!DoScan(directory)) + bCancelled = true; + } + + if (!bCancelled) + { + if (m_bClean) + m_database.CleanDatabase(m_handle, m_pathsToClean, false); + else + { + if (m_handle) + m_handle->SetTitle(g_localizeStrings.Get(331)); + m_database.Compress(false); + } + } + + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools(); + m_database.Close(); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGINFO, "VideoInfoScanner: Finished scan. Scanning for video info took {} ms", + duration.count()); + } + catch (...) + { + CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning."); + } + + m_bRunning = false; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, + "OnScanFinished"); + + if (m_handle) + m_handle->MarkFinished(); + m_handle = NULL; + } + + void CVideoInfoScanner::Start(const std::string& strDirectory, bool scanAll) + { + m_strStartDir = strDirectory; + m_scanAll = scanAll; + m_pathsToScan.clear(); + m_pathsToClean.clear(); + + m_database.Open(); + if (strDirectory.empty()) + { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as + // we go. + m_database.GetPaths(m_pathsToScan); + } + else + { // scan all the paths of this subtree that is in the database + std::vector<std::string> rootDirs; + if (URIUtils::IsMultiPath(strDirectory)) + CMultiPathDirectory::GetPaths(strDirectory, rootDirs); + else + rootDirs.push_back(strDirectory); + + for (std::vector<std::string>::const_iterator it = rootDirs.begin(); it < rootDirs.end(); ++it) + { + m_pathsToScan.insert(*it); + std::vector<std::pair<int, std::string>> subpaths; + m_database.GetSubPaths(*it, subpaths); + for (std::vector<std::pair<int, std::string>>::iterator it = subpaths.begin(); it < subpaths.end(); ++it) + m_pathsToScan.insert(it->second); + } + } + m_database.Close(); + m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryCleanOnUpdate; + + m_bRunning = true; + Process(); + } + + void CVideoInfoScanner::Stop() + { + if (m_bCanInterrupt) + m_database.Interrupt(); + + m_bStop = true; + } + + static void OnDirectoryScanned(const std::string& strDirectory) + { + CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0); + msg.SetStringParam(strDirectory); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); + } + + bool CVideoInfoScanner::DoScan(const std::string& strDirectory) + { + if (m_handle) + { + m_handle->SetText(g_localizeStrings.Get(20415)); + } + + /* + * Remove this path from the list we're processing. This must be done prior to + * the check for file or folder exclusion to prevent an infinite while loop + * in Process(). + */ + std::set<std::string>::iterator it = m_pathsToScan.find(strDirectory); + if (it != m_pathsToScan.end()) + m_pathsToScan.erase(it); + + // load subfolder + CFileItemList items; + bool foundDirectly = false; + bool bSkip = false; + + SScanSettings settings; + ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly); + CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE; + + // exclude folders that match our exclude regexps + const std::vector<std::string> ®exps = content == CONTENT_TVSHOWS ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps + : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps; + + if (CUtil::ExcludeFileOrFolder(strDirectory, regexps)) + return true; + + if (HasNoMedia(strDirectory)) + return true; + + bool ignoreFolder = !m_scanAll && settings.noupdate; + if (content == CONTENT_NONE || ignoreFolder) + return true; + + if (URIUtils::IsPlugin(strDirectory) && !CPluginDirectory::IsMediaLibraryScanningAllowed(TranslateContent(content), strDirectory)) + { + CLog::Log( + LOGINFO, + "VideoInfoScanner: Plugin '{}' does not support media library scanning for '{}' content", + CURL::GetRedacted(strDirectory), TranslateContent(content)); + return true; + } + + std::string hash, dbHash; + if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS) + { + if (m_handle) + { + int str = content == CONTENT_MOVIES ? 20317:20318; + m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name())); + } + + std::string fastHash; + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash && !URIUtils::IsPlugin(strDirectory)) + fastHash = GetFastHash(strDirectory, regexps); + + if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && StringUtils::EqualsNoCase(fastHash, dbHash)) + { // fast hashes match - no need to process anything + hash = fastHash; + } + else + { // need to fetch the folder + CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), + DIR_FLAG_DEFAULTS); + // do not consider inner folders with .nomedia + items.erase(std::remove_if(items.begin(), items.end(), + [this](const CFileItemPtr& item) { + return item->m_bIsFolder && HasNoMedia(item->GetPath()); + }), + items.end()); + items.Stack(); + + // check whether to re-use previously computed fast hash + if (!CanFastHash(items, regexps) || fastHash.empty()) + GetPathHash(items, hash); + else + hash = fastHash; + } + + if (StringUtils::EqualsNoCase(hash, dbHash)) + { // hash matches - skipping + CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change{}", + CURL::GetRedacted(strDirectory), !fastHash.empty() ? " (fasthash)" : ""); + bSkip = true; + } + else if (hash.empty()) + { // directory empty or non-existent - add to clean list and skip + CLog::Log(LOGDEBUG, + "VideoInfoScanner: Skipping dir '{}' as it's empty or doesn't exist - adding to " + "clean list", + CURL::GetRedacted(strDirectory)); + if (m_bClean) + m_pathsToClean.insert(m_database.GetPathId(strDirectory)); + bSkip = true; + } + else if (dbHash.empty()) + { // new folder - scan + CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database", + CURL::GetRedacted(strDirectory)); + } + else + { // hash changed - rescan + CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})", + CURL::GetRedacted(strDirectory), dbHash, hash); + } + } + else if (content == CONTENT_TVSHOWS) + { + if (m_handle) + m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name())); + + if (foundDirectly && !settings.parent_name_root) + { + CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), + DIR_FLAG_DEFAULTS); + items.SetPath(strDirectory); + GetPathHash(items, hash); + bSkip = true; + if (!m_database.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash)) + bSkip = false; + else + items.Clear(); + } + else + { + CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory))); + item->SetPath(strDirectory); + item->m_bIsFolder = true; + items.Add(item); + items.SetPath(URIUtils::GetParentPath(item->GetPath())); + } + } + + if (!bSkip) + { + if (RetrieveVideoInfo(items, settings.parent_name_root, content)) + { + if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS)) + { + m_database.SetPathHash(strDirectory, hash); + if (m_bClean) + m_pathsToClean.insert(m_database.GetPathId(strDirectory)); + CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir {}", + CURL::GetRedacted(strDirectory)); + } + } + else + { + if (m_bClean) + m_pathsToClean.insert(m_database.GetPathId(strDirectory)); + CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir {}", + CURL::GetRedacted(strDirectory)); + } + } + else if (!StringUtils::EqualsNoCase(hash, dbHash) && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS)) + { // update the hash either way - we may have changed the hash to a fast version + m_database.SetPathHash(strDirectory, hash); + } + + if (m_handle) + OnDirectoryScanned(strDirectory); + + for (int i = 0; i < items.Size(); ++i) + { + CFileItemPtr pItem = items[i]; + + if (m_bStop) + break; + + // if we have a directory item (non-playlist) we then recurse into that folder + // do not recurse for tv shows - we have already looked recursively for episodes + if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList() && settings.recurse > 0 && content != CONTENT_TVSHOWS) + { + if (!DoScan(pItem->GetPath())) + { + m_bStop = true; + } + } + } + return !m_bStop; + } + + bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress) + { + if (pDlgProgress) + { + if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes)) + { + pDlgProgress->ShowProgressBar(true); + pDlgProgress->SetPercentage(0); + } + else + pDlgProgress->ShowProgressBar(false); + + pDlgProgress->Progress(); + } + + m_database.Open(); + + bool FoundSomeInfo = false; + std::vector<int> seenPaths; + for (int i = 0; i < items.Size(); ++i) + { + CFileItemPtr pItem = items[i]; + + // we do this since we may have a override per dir + ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath()); + if (!info2) // skip + continue; + + // Discard all .nomedia folders + if (pItem->m_bIsFolder && HasNoMedia(pItem->GetPath())) + continue; + + // Discard all exclude files defined by regExExclude + if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps + : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps)) + continue; + + if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS) + { + if (m_handle) + m_handle->SetPercentage(i*100.f/items.Size()); + } + + // clear our scraper cache + info2->ClearCache(); + + INFO_RET ret = INFO_CANCELLED; + if (info2->Content() == CONTENT_TVSHOWS) + ret = RetrieveInfoForTvShow(pItem.get(), bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress); + else if (info2->Content() == CONTENT_MOVIES) + ret = RetrieveInfoForMovie(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress); + else if (info2->Content() == CONTENT_MUSICVIDEOS) + ret = RetrieveInfoForMusicVideo(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress); + else + { + CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type {} ({})", info2->Content(), + CURL::GetRedacted(pItem->GetPath())); + FoundSomeInfo = false; + break; + } + if (ret == INFO_CANCELLED || ret == INFO_ERROR) + { + CLog::Log(LOGWARNING, + "VideoInfoScanner: Error {} occurred while retrieving" + "information for {}.", + ret, CURL::GetRedacted(pItem->GetPath())); + FoundSomeInfo = false; + break; + } + if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY) + FoundSomeInfo = true; + else if (ret == INFO_NOT_FOUND) + { + CLog::Log(LOGWARNING, + "No information found for item '{}', it won't be added to the library.", + CURL::GetRedacted(pItem->GetPath())); + + MediaType mediaType = MediaTypeMovie; + if (info2->Content() == CONTENT_TVSHOWS) + mediaType = MediaTypeTvShow; + else if (info2->Content() == CONTENT_MUSICVIDEOS) + mediaType = MediaTypeMusicVideo; + + auto eventLog = CServiceBroker::GetEventLog(); + if (eventLog) + eventLog->Add(EventPtr(new CMediaLibraryEvent( + mediaType, pItem->GetPath(), 24145, + StringUtils::Format(g_localizeStrings.Get(24147), mediaType, + URIUtils::GetFileName(pItem->GetPath())), + pItem->GetArt("thumb"), CURL::GetRedacted(pItem->GetPath()), EventLevel::Warning))); + } + + pURL = NULL; + + // Keep track of directories we've seen + if (m_bClean && pItem->m_bIsFolder) + seenPaths.push_back(m_database.GetPathId(pItem->GetPath())); + } + + if (content == CONTENT_TVSHOWS && ! seenPaths.empty()) + { + std::vector<std::pair<int, std::string>> libPaths; + m_database.GetSubPaths(items.GetPath(), libPaths); + for (std::vector<std::pair<int, std::string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i) + { + if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end()) + m_pathsToClean.insert(i->first); + } + } + if(pDlgProgress) + pDlgProgress->ShowProgressBar(false); + + m_database.Close(); + return FoundSomeInfo; + } + + CInfoScanner::INFO_RET + CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem, + bool bDirNames, + ScraperPtr &info2, + bool useLocal, + CScraperUrl* pURL, + bool fetchEpisodes, + CGUIDialogProgress* pDlgProgress) + { + long idTvShow = -1; + std::string strPath = pItem->GetPath(); + if (pItem->m_bIsFolder) + idTvShow = m_database.GetTvShowId(strPath); + else if (pItem->IsPlugin() && pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_iIdShow >= 0) + { + // for plugin source we cannot get idTvShow from episode path with URIUtils::GetDirectory() in all cases + // so use m_iIdShow from video info tag if possible + idTvShow = pItem->GetVideoInfoTag()->m_iIdShow; + CVideoInfoTag showInfo; + if (m_database.GetTvShowInfo(std::string(), showInfo, idTvShow, nullptr, 0)) + strPath = showInfo.GetPath(); + } + else + { + strPath = URIUtils::GetDirectory(strPath); + idTvShow = m_database.GetTvShowId(strPath); + } + if (idTvShow > -1 && (fetchEpisodes || !pItem->m_bIsFolder)) + { + INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress); + if (ret == INFO_ADDED) + m_database.SetPathHash(strPath, pItem->GetProperty("hash").asString()); + return ret; + } + + if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361, pItem->GetLabel())) + return INFO_CANCELLED; + + if (m_handle) + m_handle->SetText(pItem->GetMovieName(bDirNames)); + + CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO; + CScraperUrl scrUrl; + // handle .nfo files + std::unique_ptr<IVideoInfoTagLoader> loader; + if (useLocal) + { + loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames)); + if (loader) + { + pItem->GetVideoInfoTag()->Reset(); + result = loader->Load(*pItem->GetVideoInfoTag(), false); + } + } + + if (result == CInfoScanner::FULL_NFO) + { + + long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal); + if (lResult < 0) + return INFO_ERROR; + if (fetchEpisodes) + { + INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress); + if (ret == INFO_ADDED) + m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString()); + return ret; + } + return INFO_ADDED; + } + if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO) + { + scrUrl = loader->ScraperUrl(); + pURL = &scrUrl; + } + + CScraperUrl url; + int retVal = 0; + std::string movieTitle = pItem->GetMovieName(bDirNames); + int movieYear = -1; // hint that movie title was not found + if (result == CInfoScanner::TITLE_NFO) + { + CVideoInfoTag* tag = pItem->GetVideoInfoTag(); + movieTitle = tag->GetTitle(); + movieYear = tag->GetYear(); // movieYear is expected to be >= 0 + } + if (pURL && pURL->HasUrls()) + url = *pURL; + else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0) + return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND; + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')", + url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content())); + + long lResult = -1; + if (GetDetails(pItem, url, info2, + (result == CInfoScanner::COMBINED_NFO || + result == CInfoScanner::OVERRIDE_NFO) ? loader.get() : nullptr, + pDlgProgress)) + { + if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0) + return INFO_ERROR; + } + if (fetchEpisodes) + { + INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress); + if (ret == INFO_ADDED) + m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString()); + } + return INFO_ADDED; + } + + CInfoScanner::INFO_RET + CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem, + bool bDirNames, + ScraperPtr &info2, + bool useLocal, + CScraperUrl* pURL, + CGUIDialogProgress* pDlgProgress) + { + if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() || + (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm"))) + return INFO_NOT_NEEDED; + + if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel())) + return INFO_CANCELLED; + + if (m_database.HasMovieInfo(pItem->GetDynPath())) + return INFO_HAVE_ALREADY; + + if (m_handle) + m_handle->SetText(pItem->GetMovieName(bDirNames)); + + CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO; + CScraperUrl scrUrl; + // handle .nfo files + std::unique_ptr<IVideoInfoTagLoader> loader; + if (useLocal) + { + loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames)); + if (loader) + { + pItem->GetVideoInfoTag()->Reset(); + result = loader->Load(*pItem->GetVideoInfoTag(), false); + } + } + if (result == CInfoScanner::FULL_NFO) + { + if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0) + return INFO_ERROR; + return INFO_ADDED; + } + if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO) + { + scrUrl = loader->ScraperUrl(); + pURL = &scrUrl; + } + + CScraperUrl url; + int retVal = 0; + std::string movieTitle = pItem->GetMovieName(bDirNames); + int movieYear = -1; // hint that movie title was not found + if (result == CInfoScanner::TITLE_NFO) + { + CVideoInfoTag* tag = pItem->GetVideoInfoTag(); + movieTitle = tag->GetTitle(); + movieYear = tag->GetYear(); // movieYear is expected to be >= 0 + } + if (pURL && pURL->HasUrls()) + url = *pURL; + else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0) + return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND; + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')", + url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content())); + + if (GetDetails(pItem, url, info2, + (result == CInfoScanner::COMBINED_NFO || + result == CInfoScanner::OVERRIDE_NFO) ? loader.get() : nullptr, + pDlgProgress)) + { + if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0) + return INFO_ERROR; + return INFO_ADDED; + } + //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled + return INFO_NOT_FOUND; + } + + CInfoScanner::INFO_RET + CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem, + bool bDirNames, + ScraperPtr &info2, + bool useLocal, + CScraperUrl* pURL, + CGUIDialogProgress* pDlgProgress) + { + if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() || + (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm"))) + return INFO_NOT_NEEDED; + + if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel())) + return INFO_CANCELLED; + + if (m_database.HasMusicVideoInfo(pItem->GetPath())) + return INFO_HAVE_ALREADY; + + if (m_handle) + m_handle->SetText(pItem->GetMovieName(bDirNames)); + + CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO; + CScraperUrl scrUrl; + // handle .nfo files + std::unique_ptr<IVideoInfoTagLoader> loader; + if (useLocal) + { + loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames)); + if (loader) + { + pItem->GetVideoInfoTag()->Reset(); + result = loader->Load(*pItem->GetVideoInfoTag(), false); + } + } + if (result == CInfoScanner::FULL_NFO) + { + if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0) + return INFO_ERROR; + return INFO_ADDED; + } + if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO) + { + scrUrl = loader->ScraperUrl(); + pURL = &scrUrl; + } + + CScraperUrl url; + int retVal = 0; + std::string movieTitle = pItem->GetMovieName(bDirNames); + int movieYear = -1; // hint that movie title was not found + if (result == CInfoScanner::TITLE_NFO) + { + CVideoInfoTag* tag = pItem->GetVideoInfoTag(); + movieTitle = tag->GetTitle(); + movieYear = tag->GetYear(); // movieYear is expected to be >= 0 + } + if (pURL && pURL->HasUrls()) + url = *pURL; + else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0) + return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND; + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')", + url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content())); + + if (GetDetails(pItem, url, info2, + (result == CInfoScanner::COMBINED_NFO || + result == CInfoScanner::OVERRIDE_NFO) ? loader.get() : nullptr, + pDlgProgress)) + { + if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0) + return INFO_ERROR; + return INFO_ADDED; + } + //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled + return INFO_NOT_FOUND; + } + + CInfoScanner::INFO_RET + CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item, + long showID, + const ADDON::ScraperPtr &scraper, + bool useLocal, + CGUIDialogProgress *progress) + { + // enumerate episodes + EPISODELIST files; + if (!EnumerateSeriesFolder(item, files)) + return INFO_HAVE_ALREADY; + if (files.empty()) // no update or no files + return INFO_NOT_NEEDED; + + if (m_bStop || (progress && progress->IsCanceled())) + return INFO_CANCELLED; + + CVideoInfoTag showInfo; + m_database.GetTvShowInfo("", showInfo, showID); + INFO_RET ret = OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress); + + if (ret == INFO_ADDED) + { + std::map<int, std::map<std::string, std::string>> seasonArt; + m_database.GetTvShowSeasonArt(showID, seasonArt); + + bool updateSeasonArt = false; + for (std::map<int, std::map<std::string, std::string>>::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i) + { + if (i->second.empty()) + { + updateSeasonArt = true; + break; + } + } + + if (updateSeasonArt) + { + if (!item->IsPlugin() || scraper->ID() != "metadata.local") + { + CVideoInfoDownloader loader(scraper); + loader.GetArtwork(showInfo); + } + GetSeasonThumbs(showInfo, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !item->IsPlugin()); + for (std::map<int, std::map<std::string, std::string> >::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i) + { + int seasonID = m_database.AddSeason(showID, i->first); + m_database.SetArtForItem(seasonID, MediaTypeSeason, i->second); + } + } + } + return ret; + } + + bool CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList) + { + CFileItemList items; + const std::vector<std::string> ®exps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps; + + bool bSkip = false; + + if (item->m_bIsFolder) + { + /* + * Note: DoScan() will not remove this path as it's not recursing for tvshows. + * Remove this path from the list we're processing in order to avoid hitting + * it twice in the main loop. + */ + std::set<std::string>::iterator it = m_pathsToScan.find(item->GetPath()); + if (it != m_pathsToScan.end()) + m_pathsToScan.erase(it); + + std::string hash, dbHash; + bool allowEmptyHash = false; + if (item->IsPlugin()) + { + // if plugin has already calculated a hash for directory contents - use it + // in this case we don't need to get directory listing from plugin for hash checking + if (item->HasProperty("hash")) + { + hash = item->GetProperty("hash").asString(); + allowEmptyHash = true; + } + } + else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash) + hash = GetRecursiveFastHash(item->GetPath(), regexps); + + if (m_database.GetPathHash(item->GetPath(), dbHash) && (allowEmptyHash || !hash.empty()) && StringUtils::EqualsNoCase(dbHash, hash)) + { + // fast hashes match - no need to process anything + bSkip = true; + } + + // fast hash cannot be computed or we need to rescan. fetch the listing. + if (!bSkip) + { + int flags = DIR_FLAG_DEFAULTS; + if (!hash.empty()) + flags |= DIR_FLAG_NO_FILE_INFO; + + CUtil::GetRecursiveListing(item->GetPath(), items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), flags); + + // fast hash failed - compute slow one + if (hash.empty()) + { + GetPathHash(items, hash); + if (StringUtils::EqualsNoCase(dbHash, hash)) + { + // slow hashes match - no need to process anything + bSkip = true; + } + } + } + + if (bSkip) + { + CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change", + CURL::GetRedacted(item->GetPath())); + // update our dialog with our progress + if (m_handle) + OnDirectoryScanned(item->GetPath()); + return false; + } + + if (dbHash.empty()) + CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database", + CURL::GetRedacted(item->GetPath())); + else + CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})", + CURL::GetRedacted(item->GetPath()), dbHash, hash); + + if (m_bClean) + { + m_pathsToClean.insert(m_database.GetPathId(item->GetPath())); + m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean); + } + item->SetProperty("hash", hash); + } + else + { + CFileItemPtr newItem(new CFileItem(*item)); + items.Add(newItem); + } + + /* + stack down any dvd folders + need to sort using the full path since this is a collapsed recursive listing of all subdirs + video_ts.ifo files should sort at the top of a dvd folder in ascending order + + /foo/bar/video_ts.ifo + /foo/bar/vts_x_y.ifo + /foo/bar/vts_x_y.vob + */ + + // since we're doing this now anyway, should other items be stacked? + items.Sort(SortByPath, SortOrderAscending); + int x = 0; + while (x < items.Size()) + { + if (items[x]->m_bIsFolder) + { + x++; + continue; + } + + std::string strPathX, strFileX; + URIUtils::Split(items[x]->GetPath(), strPathX, strFileX); + //CLog::Log(LOGDEBUG,"{}:{}:{}", x, strPathX, strFileX); + + const int y = x + 1; + if (StringUtils::EqualsNoCase(strFileX, "VIDEO_TS.IFO")) + { + while (y < items.Size()) + { + std::string strPathY, strFileY; + URIUtils::Split(items[y]->GetPath(), strPathY, strFileY); + //CLog::Log(LOGDEBUG," {}:{}:{}", y, strPathY, strFileY); + + if (StringUtils::EqualsNoCase(strPathY, strPathX)) + /* + remove everything sorted below the video_ts.ifo file in the same path. + understandably this wont stack correctly if there are other files in the the dvd folder. + this should be unlikely and thus is being ignored for now but we can monitor the + where the path changes and potentially remove the items above the video_ts.ifo file. + */ + items.Remove(y); + else + break; + } + } + x++; + } + + // enumerate + for (int i=0;i<items.Size();++i) + { + if (items[i]->m_bIsFolder) + continue; + std::string strPath = URIUtils::GetDirectory(items[i]->GetPath()); + URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows + + if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strPath), "sample")) + continue; + + // Discard all exclude files defined by regExExcludes + if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps)) + continue; + + /* + * Check if the media source has already set the season and episode or original air date in + * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid + * any false positive matches. + */ + if (ProcessItemByVideoInfoTag(items[i].get(), episodeList)) + continue; + + if (!EnumerateEpisodeItem(items[i].get(), episodeList)) + CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file {}", CURL::GetRedacted(items[i]->GetPath())); + } + return true; + } + + bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList) + { + if (!item->HasVideoInfoTag()) + return false; + + const CVideoInfoTag* tag = item->GetVideoInfoTag(); + bool isValid = false; + /* + * First check the season and episode number. This takes precedence over the original air + * date and episode title. Must be a valid season and episode number combination. + */ + if (tag->m_iSeason > -1 && tag->m_iEpisode > 0) + isValid = true; + + // episode 0 with non-zero season is valid! (e.g. prequel episode) + if (item->IsPlugin() && tag->m_iSeason > 0 && tag->m_iEpisode >= 0) + isValid = true; + + if (isValid) + { + EPISODE episode; + episode.strPath = item->GetPath(); + episode.iSeason = tag->m_iSeason; + episode.iEpisode = tag->m_iEpisode; + episode.isFolder = false; + // save full item for plugin source + if (item->IsPlugin()) + episode.item = std::make_shared<CFileItem>(*item); + episodeList.push_back(episode); + CLog::Log(LOGDEBUG, "{} - found match for: {}. Season {}, Episode {}", __FUNCTION__, + CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode); + return true; + } + + /* + * Next preference is the first aired date. If it exists use that for matching the TV Show + * information. Also set the title in case there are multiple matches for the first aired date. + */ + if (tag->m_firstAired.IsValid()) + { + EPISODE episode; + episode.strPath = item->GetPath(); + episode.strTitle = tag->m_strTitle; + episode.isFolder = false; + /* + * Set season and episode to -1 to indicate to use the aired date. + */ + episode.iSeason = -1; + episode.iEpisode = -1; + /* + * The first aired date string must be parseable. + */ + episode.cDate = item->GetVideoInfoTag()->m_firstAired; + episodeList.push_back(episode); + CLog::Log(LOGDEBUG, "{} - found match for: '{}', firstAired: '{}' = '{}', title: '{}'", + __FUNCTION__, CURL::GetRedacted(episode.strPath), + tag->m_firstAired.GetAsDBDateTime(), episode.cDate.GetAsLocalizedDate(), + episode.strTitle); + return true; + } + + /* + * Next preference is the episode title. If it exists use that for matching the TV Show + * information. + */ + if (!tag->m_strTitle.empty()) + { + EPISODE episode; + episode.strPath = item->GetPath(); + episode.strTitle = tag->m_strTitle; + episode.isFolder = false; + /* + * Set season and episode to -1 to indicate to use the title. + */ + episode.iSeason = -1; + episode.iEpisode = -1; + episodeList.push_back(episode); + CLog::Log(LOGDEBUG, "{} - found match for: '{}', title: '{}'", __FUNCTION__, + CURL::GetRedacted(episode.strPath), episode.strTitle); + return true; + } + + /* + * There is no further episode information available if both the season and episode number have + * been set to 0. Return the match as true so no further matching is attempted, but don't add it + * to the episode list. + */ + if (tag->m_iSeason == 0 && tag->m_iEpisode == 0) + { + CLog::Log(LOGDEBUG, + "{} - found exclusion match for: {}. Both Season and Episode are 0. Item will be " + "ignored for scanning.", + __FUNCTION__, CURL::GetRedacted(item->GetPath())); + return true; + } + + return false; + } + + bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList) + { + SETTINGS_TVSHOWLIST expression = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowEnumRegExps; + + std::string strLabel; + + // remove path to main file if it's a bd or dvd folder to regex the right (folder) name + if (item->IsOpticalMediaFile()) + { + strLabel = item->GetLocalMetadataPath(); + URIUtils::RemoveSlashAtEnd(strLabel); + } + else + strLabel = item->GetPath(); + + // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi + strLabel = CURL::Decode(CURL::GetRedacted(strLabel)); + + for (unsigned int i=0;i<expression.size();++i) + { + CRegExp reg(true, CRegExp::autoUtf8); + if (!reg.RegComp(expression[i].regexp)) + continue; + + int regexppos, regexp2pos; + //CLog::Log(LOGDEBUG,"running expression {} on {}",expression[i].regexp,strLabel); + if ((regexppos = reg.RegFind(strLabel.c_str())) < 0) + continue; + + EPISODE episode; + episode.strPath = item->GetPath(); + episode.iSeason = -1; + episode.iEpisode = -1; + episode.cDate.SetValid(false); + episode.isFolder = false; + + bool byDate = expression[i].byDate ? true : false; + bool byTitle = expression[i].byTitle; + int defaultSeason = expression[i].defaultSeason; + + if (byDate) + { + if (!GetAirDateFromRegExp(reg, episode)) + continue; + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match {} ({}) [{}]", + CURL::GetRedacted(episode.strPath), episode.cDate.GetAsLocalizedDate(), + expression[i].regexp); + } + else if (byTitle) + { + if (!GetEpisodeTitleFromRegExp(reg, episode)) + continue; + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Found title based match {} ({}) [{}]", + CURL::GetRedacted(episode.strPath), episode.strTitle, expression[i].regexp); + } + else + { + if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason)) + continue; + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match {} (s{}e{}) [{}]", + CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode, + expression[i].regexp); + } + + // Grab the remainder from first regexp run + // as second run might modify or empty it. + std::string remainder(reg.GetMatch(3)); + + /* + * Check if the files base path is a dedicated folder that contains + * only this single episode. If season and episode match with the + * actual media file, we set episode.isFolder to true. + */ + std::string strBasePath = item->GetBaseMoviePath(true); + URIUtils::RemoveSlashAtEnd(strBasePath); + strBasePath = URIUtils::GetFileName(strBasePath); + + if (reg.RegFind(strBasePath.c_str()) > -1) + { + EPISODE parent; + if (byDate) + { + GetAirDateFromRegExp(reg, parent); + if (episode.cDate == parent.cDate) + episode.isFolder = true; + } + else + { + GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason); + if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode) + episode.isFolder = true; + } + } + + // add what we found by now + episodeList.push_back(episode); + + CRegExp reg2(true, CRegExp::autoUtf8); + // check the remainder of the string for any further episodes. + if (!byDate && reg2.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowMultiPartEnumRegExp)) + { + int offset = 0; + + // we want "long circuit" OR below so that both offsets are evaluated + while (static_cast<int>((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) | + static_cast<int>((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1)) + { + if (((regexppos <= regexp2pos) && regexppos != -1) || + (regexppos >= 0 && regexp2pos == -1)) + { + GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason); + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season {}, multipart episode {} [{}]", + episode.iSeason, episode.iEpisode, + CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_tvshowMultiPartEnumRegExp); + + episodeList.push_back(episode); + remainder = reg.GetMatch(3); + offset = 0; + } + else if (((regexp2pos < regexppos) && regexp2pos != -1) || + (regexp2pos >= 0 && regexppos == -1)) + { + episode.iEpisode = atoi(reg2.GetMatch(1).c_str()); + CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode {} [{}]", + episode.iEpisode, + CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_tvshowMultiPartEnumRegExp); + episodeList.push_back(episode); + offset += regexp2pos + reg2.GetFindLen(); + } + } + } + return true; + } + return false; + } + + bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp ®, EPISODE &episodeInfo, int defaultSeason) + { + std::string season(reg.GetMatch(1)); + std::string episode(reg.GetMatch(2)); + + if (!season.empty() || !episode.empty()) + { + char* endptr = NULL; + if (season.empty() && !episode.empty()) + { // no season specified -> assume defaultSeason + episodeInfo.iSeason = defaultSeason; + if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1) + episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10); + } + else if (!season.empty() && episode.empty()) + { // no episode specification -> assume defaultSeason + episodeInfo.iSeason = defaultSeason; + if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1) + episodeInfo.iEpisode = atoi(season.c_str()); + } + else + { // season and episode specified + episodeInfo.iSeason = atoi(season.c_str()); + episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10); + } + if (endptr) + { + if (isalpha(*endptr)) + episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1; + else if (*endptr == '.') + episodeInfo.iSubepisode = atoi(endptr+1); + } + return true; + } + return false; + } + + bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp ®, EPISODE &episodeInfo) + { + std::string param1(reg.GetMatch(1)); + std::string param2(reg.GetMatch(2)); + std::string param3(reg.GetMatch(3)); + + if (!param1.empty() && !param2.empty() && !param3.empty()) + { + // regular expression by date + int len1 = param1.size(); + int len2 = param2.size(); + int len3 = param3.size(); + + if (len1==4 && len2==2 && len3==2) + { + // yyyy mm dd format + episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str())); + } + else if (len1==2 && len2==2 && len3==4) + { + // mm dd yyyy format + episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str())); + } + } + return episodeInfo.cDate.IsValid(); + } + + bool CVideoInfoScanner::GetEpisodeTitleFromRegExp(CRegExp& reg, EPISODE& episodeInfo) + { + std::string param1(reg.GetMatch(1)); + + if (!param1.empty()) + { + episodeInfo.strTitle = param1; + return true; + } + return false; + } + + long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */) + { + // ensure our database is open (this can get called via other classes) + if (!m_database.Open()) + return -1; + + if (!libraryImport) + GetArtwork(pItem, content, videoFolder, useLocal && !pItem->IsPlugin(), showInfo ? showInfo->m_strPath : ""); + + // ensure the art map isn't completely empty by specifying an empty thumb + std::map<std::string, std::string> art = pItem->GetArt(); + if (art.empty()) + art["thumb"] = ""; + + CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag(); + if (movieDetails.m_basePath.empty()) + movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder); + movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath)); + + movieDetails.m_strFileNameAndPath = pItem->GetPath(); + + if (pItem->m_bIsFolder) + movieDetails.m_strPath = pItem->GetPath(); + + std::string strTitle(movieDetails.m_strTitle); + + if (showInfo && content == CONTENT_TVSHOWS) + { + strTitle = StringUtils::Format("{} - {}x{} - {}", showInfo->m_strTitle, + movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle); + } + + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS) && + CDVDFileInfo::GetFileStreamDetails(pItem)) + CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}", + CURL::GetRedacted(pItem->GetPath())); + + CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to {}:{}", TranslateContent(content), CURL::GetRedacted(pItem->GetPath())); + long lResult = -1; + + if (content == CONTENT_MOVIES) + { + // find local trailer first + std::string strTrailer = pItem->FindTrailer(); + if (!strTrailer.empty()) + movieDetails.m_strTrailer = strTrailer; + + lResult = m_database.SetDetailsForMovie(movieDetails, art); + movieDetails.m_iDbId = lResult; + movieDetails.m_type = MediaTypeMovie; + + // setup links to shows if the linked shows are in the db + for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i) + { + CFileItemList items; + m_database.GetTvShowsByName(movieDetails.m_showLink[i], items); + if (items.Size()) + m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false); + else + CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie {} to show {}", + movieDetails.m_strTitle, movieDetails.m_showLink[i]); + } + } + else if (content == CONTENT_TVSHOWS) + { + if (pItem->m_bIsFolder) + { + /* + multipaths are not stored in the database, so in the case we have one, + we split the paths, and compute the parent paths in each case. + */ + std::vector<std::string> multipath; + if (!URIUtils::IsMultiPath(pItem->GetPath()) || !CMultiPathDirectory::GetPaths(pItem->GetPath(), multipath)) + multipath.push_back(pItem->GetPath()); + std::vector<std::pair<std::string, std::string> > paths; + for (std::vector<std::string>::const_iterator i = multipath.begin(); i != multipath.end(); ++i) + paths.emplace_back(*i, URIUtils::GetParentPath(*i)); + + std::map<int, std::map<std::string, std::string> > seasonArt; + + if (!libraryImport) + GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !pItem->IsPlugin()); + + lResult = m_database.SetDetailsForTvShow(paths, movieDetails, art, seasonArt); + movieDetails.m_iDbId = lResult; + movieDetails.m_type = MediaTypeTvShow; + } + else + { + // we add episode then set details, as otherwise set details will delete the + // episode then add, which breaks multi-episode files. + int idShow = showInfo ? showInfo->m_iDbId : -1; + int idEpisode = m_database.AddNewEpisode(idShow, movieDetails); + lResult = m_database.SetDetailsForEpisode(movieDetails, art, idShow, idEpisode); + movieDetails.m_iDbId = lResult; + movieDetails.m_type = MediaTypeEpisode; + movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : ""; + if (movieDetails.m_EpBookmark.timeInSeconds > 0) + { + movieDetails.m_strFileNameAndPath = pItem->GetPath(); + movieDetails.m_EpBookmark.seasonNumber = movieDetails.m_iSeason; + movieDetails.m_EpBookmark.episodeNumber = movieDetails.m_iEpisode; + m_database.AddBookMarkForEpisode(movieDetails, movieDetails.m_EpBookmark); + } + } + } + else if (content == CONTENT_MUSICVIDEOS) + { + lResult = m_database.SetDetailsForMusicVideo(movieDetails, art); + movieDetails.m_iDbId = lResult; + movieDetails.m_type = MediaTypeMusicVideo; + } + + if (!pItem->m_bIsFolder) + { + const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings(); + if ((libraryImport || advancedSettings->m_bVideoLibraryImportWatchedState) && + (movieDetails.IsPlayCountSet() || movieDetails.m_lastPlayed.IsValid())) + m_database.SetPlayCount(*pItem, movieDetails.GetPlayCount(), movieDetails.m_lastPlayed); + + if ((libraryImport || advancedSettings->m_bVideoLibraryImportResumePoint) && + movieDetails.GetResumePoint().IsSet()) + m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.GetResumePoint(), CBookmark::RESUME); + } + + m_database.Close(); + + CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem)); + CVariant data; + data["added"] = true; + if (m_bRunning) + data["transaction"] = true; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate", + itemCopy, data); + return lResult; + } + + std::string ContentToMediaType(CONTENT_TYPE content, bool folder) + { + switch (content) + { + case CONTENT_MOVIES: + return MediaTypeMovie; + case CONTENT_MUSICVIDEOS: + return MediaTypeMusicVideo; + case CONTENT_TVSHOWS: + return folder ? MediaTypeTvShow : MediaTypeEpisode; + default: + return ""; + } + } + + std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height) + { + std::string type = "thumb"; + if (width*5 < height*4) + type = "poster"; + else if (width*1 > height*4) + type = "banner"; + return type; + } + + std::string CVideoInfoScanner::GetMovieSetInfoFolder(const std::string& setTitle) + { + if (setTitle.empty()) + return ""; + std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER); + if (path.empty()) + return ""; + path = URIUtils::AddFileToFolder(path, CUtil::MakeLegalFileName(setTitle, LEGAL_WIN32_COMPAT)); + URIUtils::AddSlashAtEnd(path); + CLog::Log(LOGDEBUG, + "VideoInfoScanner: Looking for local artwork for movie set '{}' in folder '{}'", + setTitle, + CURL::GetRedacted(path)); + return CDirectory::Exists(path) ? path : ""; + } + + void CVideoInfoScanner::AddLocalItemArtwork(CGUIListItem::ArtMap& itemArt, + const std::vector<std::string>& wantedArtTypes, const std::string& itemPath, + bool addAll, bool exactName) + { + std::string path = URIUtils::GetDirectory(itemPath); + if (path.empty()) + return; + + CFileItemList availableArtFiles; + CDirectory::GetDirectory(path, availableArtFiles, + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), + DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO); + + std::string baseFilename = URIUtils::GetFileName(itemPath); + if (!baseFilename.empty()) + { + URIUtils::RemoveExtension(baseFilename); + baseFilename.append("-"); + } + + for (const auto& artFile : availableArtFiles) + { + std::string candidate = URIUtils::GetFileName(artFile->GetPath()); + + bool matchesFilename = + !baseFilename.empty() && StringUtils::StartsWith(candidate, baseFilename); + if (!baseFilename.empty() && !matchesFilename) + continue; + + if (matchesFilename) + candidate.erase(0, baseFilename.length()); + URIUtils::RemoveExtension(candidate); + StringUtils::ToLower(candidate); + + // move 'folder' to thumb / poster / banner based on aspect ratio + // if such artwork doesn't already exist + if (!matchesFilename && StringUtils::EqualsNoCase(candidate, "folder") && + !CVideoThumbLoader::IsArtTypeInWhitelist("folder", wantedArtTypes, exactName)) + { + // cache the image to determine sizing + CTextureDetails details; + if (CServiceBroker::GetTextureCache()->CacheImage(artFile->GetPath(), details)) + { + candidate = GetArtTypeFromSize(details.width, details.height); + if (itemArt.find(candidate) != itemArt.end()) + continue; + } + } + + if ((addAll && CVideoThumbLoader::IsValidArtType(candidate)) || + CVideoThumbLoader::IsArtTypeInWhitelist(candidate, wantedArtTypes, exactName)) + { + itemArt[candidate] = artFile->GetPath(); + } + } + } + + void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath) + { + int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL); + if (artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE) + return; + + CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag(); + movieDetails.m_fanart.Unpack(); + movieDetails.m_strPictureURL.Parse(); + + CGUIListItem::ArtMap art = pItem->GetArt(); + + // get and cache thumb images + std::string mediaType = ContentToMediaType(content, pItem->m_bIsFolder); + std::vector<std::string> artTypes = CVideoThumbLoader::GetArtTypes(mediaType); + bool moviePartOfSet = content == CONTENT_MOVIES && !movieDetails.m_set.title.empty(); + std::vector<std::string> movieSetArtTypes; + if (moviePartOfSet) + { + movieSetArtTypes = CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection); + for (const std::string& artType : movieSetArtTypes) + artTypes.push_back("set." + artType); + } + bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL; + bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC; + // find local art + if (useLocal) + { + if (!pItem->SkipLocalArt()) + { + if (bApplyToDir && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS)) + { + std::string filename = pItem->GetLocalArtBaseFilename(); + std::string directory = URIUtils::GetDirectory(filename); + if (filename != directory) + AddLocalItemArtwork(art, artTypes, directory, addAll, exactName); + } + AddLocalItemArtwork(art, artTypes, pItem->GetLocalArtBaseFilename(), addAll, exactName); + } + + if (moviePartOfSet) + { + std::string movieSetInfoPath = GetMovieSetInfoFolder(movieDetails.m_set.title); + if (!movieSetInfoPath.empty()) + { + CGUIListItem::ArtMap movieSetArt; + AddLocalItemArtwork(movieSetArt, movieSetArtTypes, movieSetInfoPath, addAll, exactName); + for (const auto& artItem : movieSetArt) + { + art["set." + artItem.first] = artItem.second; + } + } + } + } + + // find embedded art + if (pItem->HasVideoInfoTag() && !pItem->GetVideoInfoTag()->m_coverArt.empty()) + { + for (auto& it : pItem->GetVideoInfoTag()->m_coverArt) + { + if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(it.m_type, artTypes, exactName)) && + art.find(it.m_type) == art.end()) + { + std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), + "video_" + it.m_type); + art.insert(std::make_pair(it.m_type, thumb)); + } + } + } + + // add online fanart (treated separately due to it being stored in m_fanart) + if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist("fanart", artTypes, exactName)) && + art.find("fanart") == art.end()) + { + std::string fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL(); + if (!fanart.empty()) + art.insert(std::make_pair("fanart", fanart)); + } + + // add online art + for (const auto& url : pItem->GetVideoInfoTag()->m_strPictureURL.GetUrls()) + { + if (url.m_type != CScraperUrl::UrlType::General) + continue; + std::string aspect = url.m_aspect; + if (aspect.empty()) + // Backward compatibility with Kodi 11 Eden NFO files + aspect = mediaType == MediaTypeEpisode ? "thumb" : "poster"; + + if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) && + art.find(aspect) == art.end()) + { + std::string image = GetImage(url, pItem->GetPath()); + if (!image.empty()) + art.insert(std::make_pair(aspect, image)); + } + } + + for (const auto& artType : artTypes) + { + if (art.find(artType) != art.end()) + CServiceBroker::GetTextureCache()->BackgroundCacheImage(art[artType]); + } + + pItem->SetArt(art); + + // parent folder to apply the thumb to and to search for local actor thumbs + std::string parentDir = URIUtils::GetBasePath(pItem->GetPath()); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS)) + FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath); + if (bApplyToDir) + ApplyThumbToFolder(parentDir, art["thumb"]); + } + + std::string CVideoInfoScanner::GetImage(const CScraperUrl::SUrlEntry &image, const std::string& itemPath) + { + std::string thumb = CScraperUrl::GetThumbUrl(image); + if (!thumb.empty() && thumb.find('/') == std::string::npos && + thumb.find('\\') == std::string::npos) + { + std::string strPath = URIUtils::GetDirectory(itemPath); + thumb = URIUtils::AddFileToFolder(strPath, thumb); + } + return thumb; + } + + CInfoScanner::INFO_RET + CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files, + const ADDON::ScraperPtr &scraper, + bool useLocal, + const CVideoInfoTag& showInfo, + CGUIDialogProgress* pDlgProgress /* = NULL */) + { + if (pDlgProgress) + { + pDlgProgress->SetLine(1, CVariant{showInfo.m_strTitle}); + pDlgProgress->SetLine(2, CVariant{20361}); + pDlgProgress->SetPercentage(0); + pDlgProgress->ShowProgressBar(true); + pDlgProgress->Progress(); + } + + EPISODELIST episodes; + bool hasEpisodeGuide = false; + + int iMax = files.size(); + int iCurr = 1; + for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file) + { + if (pDlgProgress) + { + pDlgProgress->SetLine(2, CVariant{20361}); + pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100)); + pDlgProgress->Progress(); + } + if (m_handle) + m_handle->SetPercentage(100.f*iCurr++/iMax); + + if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop) + return INFO_CANCELLED; + + if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1) + { + if (m_handle) + m_handle->SetText(g_localizeStrings.Get(20415)); + continue; + } + + CFileItem item; + if (file->item) + item = *file->item; + else + { + item.SetPath(file->strPath); + item.GetVideoInfoTag()->m_iEpisode = file->iEpisode; + } + + // handle .nfo files + CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO; + CScraperUrl scrUrl; + const ScraperPtr& info(scraper); + std::unique_ptr<IVideoInfoTagLoader> loader; + if (useLocal) + { + loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(item, info, false)); + if (loader) + { + // no reset here on purpose + result = loader->Load(*item.GetVideoInfoTag(), false); + } + } + if (result == CInfoScanner::FULL_NFO) + { + // override with episode and season number from file if available + if (file->iEpisode > -1) + { + item.GetVideoInfoTag()->m_iEpisode = file->iEpisode; + item.GetVideoInfoTag()->m_iSeason = file->iSeason; + } + if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0) + return INFO_ERROR; + continue; + } + + if (!hasEpisodeGuide) + { + // fetch episode guide + if (!showInfo.m_strEpisodeGuide.empty()) + { + CScraperUrl url; + url.ParseAndAppendUrlsFromEpisodeGuide(showInfo.m_strEpisodeGuide); + + if (pDlgProgress) + { + pDlgProgress->SetLine(2, CVariant{20354}); + pDlgProgress->Progress(); + } + + CVideoInfoDownloader imdb(scraper); + if (!imdb.GetEpisodeList(url, episodes)) + return INFO_NOT_FOUND; + + hasEpisodeGuide = true; + } + } + + if (episodes.empty()) + { + CLog::Log(LOGERROR, + "VideoInfoScanner: Asked to lookup episode {}" + " online, but we have no episode guide. Check your tvshow.nfo and make" + " sure the <episodeguide> tag is in place.", + CURL::GetRedacted(file->strPath)); + continue; + } + + EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode); + EPISODE backupkey(file->iSeason, file->iEpisode, 0); + bool bFound = false; + EPISODELIST::iterator guide = episodes.begin(); + EPISODELIST matches; + + for (; guide != episodes.end(); ++guide ) + { + if ((file->iEpisode!=-1) && (file->iSeason!=-1)) + { + if (key==*guide) + { + bFound = true; + break; + } + else if ((file->iSubepisode!=0) && (backupkey==*guide)) + { + matches.push_back(*guide); + continue; + } + } + if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate) + { + matches.push_back(*guide); + continue; + } + if (!guide->cScraperUrl.GetTitle().empty() && + StringUtils::EqualsNoCase(guide->cScraperUrl.GetTitle(), file->strTitle)) + { + bFound = true; + break; + } + if (!guide->strTitle.empty() && StringUtils::EqualsNoCase(guide->strTitle, file->strTitle)) + { + bFound = true; + break; + } + } + + if (!bFound) + { + /* + * If there is only one match or there are matches but no title to compare with to help + * identify the best match, then pick the first match as the best possible candidate. + * + * Otherwise, use the title to further refine the best match. + */ + if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1)) + { + guide = matches.begin(); + bFound = true; + } + else if (!file->strTitle.empty()) + { + CLog::Log(LOGDEBUG, "VideoInfoScanner: analyzing parsed title '{}'", file->strTitle); + double minscore = 0; // Default minimum score is 0 to find whatever is the best match. + + EPISODELIST *candidates; + if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes. + { + minscore = 0.8; // 80% should ensure a good match. + candidates = &episodes; + } + else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best. + candidates = &matches; + + std::vector<std::string> titles; + for (guide = candidates->begin(); guide != candidates->end(); ++guide) + { + auto title = guide->cScraperUrl.GetTitle(); + if (title.empty()) + { + title = guide->strTitle; + } + StringUtils::ToLower(title); + guide->cScraperUrl.SetTitle(title); + titles.push_back(title); + } + + double matchscore; + std::string loweredTitle(file->strTitle); + StringUtils::ToLower(loweredTitle); + int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore); + if (index >= 0 && matchscore >= minscore) + { + guide = candidates->begin() + index; + bFound = true; + CLog::Log(LOGDEBUG, + "{} fuzzy title match for show: '{}', title: '{}', match: '{}', score: {:f} " + ">= {:f}", + __FUNCTION__, showInfo.m_strTitle, file->strTitle, titles[index], matchscore, + minscore); + } + } + } + + if (bFound) + { + CVideoInfoDownloader imdb(scraper); + CFileItem item; + item.SetPath(file->strPath); + if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress)) + return INFO_NOT_FOUND; //! @todo should we just skip to the next episode? + + // Only set season/epnum from filename when it is not already set by a scraper + if (item.GetVideoInfoTag()->m_iSeason == -1) + item.GetVideoInfoTag()->m_iSeason = guide->iSeason; + if (item.GetVideoInfoTag()->m_iEpisode == -1) + item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode; + + if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0) + return INFO_ERROR; + } + else + { + CLog::Log( + LOGDEBUG, + "{} - no match for show: '{}', season: {}, episode: {}.{}, airdate: '{}', title: '{}'", + __FUNCTION__, showInfo.m_strTitle, file->iSeason, file->iEpisode, file->iSubepisode, + file->cDate.GetAsLocalizedDate(), file->strTitle); + } + } + return INFO_ADDED; + } + + bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, + const ScraperPtr& scraper, + IVideoInfoTagLoader* loader, + CGUIDialogProgress* pDialog /* = NULL */) + { + CVideoInfoTag movieDetails; + + if (m_handle && !url.GetTitle().empty()) + m_handle->SetText(url.GetTitle()); + + CVideoInfoDownloader imdb(scraper); + bool ret = imdb.GetDetails(url, movieDetails, pDialog); + + if (ret) + { + if (loader) + loader->Load(movieDetails, true); + + if (m_handle && url.GetTitle().empty()) + m_handle->SetText(movieDetails.m_strTitle); + + if (pDialog) + { + pDialog->SetLine(1, CVariant{movieDetails.m_strTitle}); + pDialog->Progress(); + } + + *pItem->GetVideoInfoTag() = movieDetails; + return true; + } + return false; // no info found, or cancelled + } + + void CVideoInfoScanner::ApplyThumbToFolder(const std::string &folder, const std::string &imdbThumb) + { + // copy icon to folder also; + if (!imdbThumb.empty()) + { + CFileItem folderItem(folder, true); + CThumbLoader loader; + loader.SetCachedImage(folderItem, "thumb", imdbThumb); + } + } + + int CVideoInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash) + { + // Create a hash based on the filenames, filesize and filedate. Also count the number of files + if (0 == items.Size()) return 0; + CDigest digest{CDigest::Type::MD5}; + int count = 0; + for (int i = 0; i < items.Size(); ++i) + { + const CFileItemPtr pItem = items[i]; + digest.Update(pItem->GetPath()); + if (pItem->IsPlugin()) + { + // allow plugin to calculate hash itself using strings rather than binary data for size and date + // according to ListItem.setInfo() documentation date format should be "d.m.Y" + if (pItem->m_dwSize) + digest.Update(std::to_string(pItem->m_dwSize)); + if (pItem->m_dateTime.IsValid()) + digest.Update(StringUtils::Format("{:02}.{:02}.{:04}", pItem->m_dateTime.GetDay(), + pItem->m_dateTime.GetMonth(), + pItem->m_dateTime.GetYear())); + } + else + { + digest.Update(&pItem->m_dwSize, sizeof(pItem->m_dwSize)); + KODI::TIME::FileTime time = pItem->m_dateTime; + digest.Update(&time, sizeof(KODI::TIME::FileTime)); + } + if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO()) + count++; + } + hash = digest.Finalize(); + return count; + } + + bool CVideoInfoScanner::CanFastHash(const CFileItemList &items, const std::vector<std::string> &excludes) const + { + if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash || items.IsPlugin()) + return false; + + for (int i = 0; i < items.Size(); ++i) + { + if (items[i]->m_bIsFolder && !CUtil::ExcludeFileOrFolder(items[i]->GetPath(), excludes)) + return false; + } + return true; + } + + std::string CVideoInfoScanner::GetFastHash(const std::string &directory, + const std::vector<std::string> &excludes) const + { + CDigest digest{CDigest::Type::MD5}; + + if (excludes.size()) + digest.Update(StringUtils::Join(excludes, "|")); + + struct __stat64 buffer; + if (XFILE::CFile::Stat(directory, &buffer) == 0) + { + int64_t time = buffer.st_mtime; + if (!time) + time = buffer.st_ctime; + if (time) + { + digest.Update((unsigned char *)&time, sizeof(time)); + return digest.Finalize(); + } + } + return ""; + } + + std::string CVideoInfoScanner::GetRecursiveFastHash(const std::string &directory, + const std::vector<std::string> &excludes) const + { + CFileItemList items; + items.Add(CFileItemPtr(new CFileItem(directory, true))); + CUtil::GetRecursiveDirsListing(directory, items, DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO); + + CDigest digest{CDigest::Type::MD5}; + + if (excludes.size()) + digest.Update(StringUtils::Join(excludes, "|")); + + int64_t time = 0; + for (int i=0; i < items.Size(); ++i) + { + int64_t stat_time = 0; + struct __stat64 buffer; + if (XFILE::CFile::Stat(items[i]->GetPath(), &buffer) == 0) + { + //! @todo some filesystems may return the mtime/ctime inline, in which case this is + //! unnecessarily expensive. Consider supporting Stat() in our directory cache? + stat_time = buffer.st_mtime ? buffer.st_mtime : buffer.st_ctime; + time += stat_time; + } + + if (!stat_time) + return ""; + } + + if (time) + { + digest.Update((unsigned char *)&time, sizeof(time)); + return digest.Finalize(); + } + return ""; + } + + void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, + std::map<int, std::map<std::string, std::string>> &seasonArt, const std::vector<std::string> &artTypes, bool useLocal) + { + int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()-> + GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL); + bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL; + bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC; + if (useLocal) + { + // find the maximum number of seasons we have local thumbs for + int maxSeasons = 0; + CFileItemList items; + std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(); + if (!show.m_strPath.empty()) + { + CDirectory::GetDirectory(show.m_strPath, items, extensions, + DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | + DIR_FLAG_NO_FILE_INFO); + } + extensions.erase(std::remove(extensions.begin(), extensions.end(), '.'), extensions.end()); + CRegExp reg; + if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z0-9]+)?\\.(" + extensions + ")")) + { + for (const auto& item : items) + { + std::string name = URIUtils::GetFileName(item->GetPath()); + if (reg.RegFind(name) > -1) + { + int season = atoi(reg.GetMatch(1).c_str()); + if (season > maxSeasons) + maxSeasons = season; + } + } + } + for (int season = -1; season <= maxSeasons; season++) + { + // skip if we already have some art + std::map<int, std::map<std::string, std::string>>::const_iterator it = seasonArt.find(season); + if (it != seasonArt.end() && !it->second.empty()) + continue; + + std::map<std::string, std::string> art; + std::string basePath; + if (season == -1) + basePath = "season-all"; + else if (season == 0) + basePath = "season-specials"; + else + basePath = StringUtils::Format("season{:02}", season); + + AddLocalItemArtwork(art, artTypes, + URIUtils::AddFileToFolder(show.m_strPath, basePath), + addAll, exactName); + + seasonArt[season] = art; + } + } + // add online art + for (const auto& url : show.m_strPictureURL.GetUrls()) + { + if (url.m_type != CScraperUrl::UrlType::Season) + continue; + std::string aspect = url.m_aspect; + if (aspect.empty()) + aspect = "thumb"; + std::map<std::string, std::string>& art = seasonArt[url.m_season]; + if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) && + art.find(aspect) == art.end()) + { + std::string image = CScraperUrl::GetThumbUrl(url); + if (!image.empty()) + art.insert(std::make_pair(aspect, image)); + } + } + } + + void CVideoInfoScanner::FetchActorThumbs(std::vector<SActorInfo>& actors, const std::string& strPath) + { + CFileItemList items; + // don't try to fetch anything local with plugin source + if (!URIUtils::IsPlugin(strPath)) + { + std::string actorsDir = URIUtils::AddFileToFolder(strPath, ".actors"); + if (CDirectory::Exists(actorsDir)) + CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | + DIR_FLAG_NO_FILE_INFO); + } + for (std::vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i) + { + if (i->thumb.empty()) + { + std::string thumbFile = i->strName; + StringUtils::Replace(thumbFile, ' ', '_'); + for (int j = 0; j < items.Size(); j++) + { + std::string compare = URIUtils::GetFileName(items[j]->GetPath()); + URIUtils::RemoveExtension(compare); + if (!items[j]->m_bIsFolder && compare == thumbFile) + { + i->thumb = items[j]->GetPath(); + break; + } + } + if (i->thumb.empty() && !i->thumbUrl.GetFirstUrlByType().m_url.empty()) + i->thumb = CScraperUrl::GetThumbUrl(i->thumbUrl.GetFirstUrlByType()); + if (!i->thumb.empty()) + CServiceBroker::GetTextureCache()->BackgroundCacheImage(i->thumb); + } + } + } + + bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog) + { + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoScannerIgnoreErrors) + return true; + + if (pDialog) + { + HELPERS::ShowOKDialogText(CVariant{20448}, CVariant{20449}); + return false; + } + return HELPERS::ShowYesNoDialogText(CVariant{20448}, CVariant{20450}) == + DialogResponse::CHOICE_YES; + } + + bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const std::string &line1) + { + if (progress) + { + progress->SetHeading(CVariant{heading}); + progress->SetLine(0, CVariant{line1}); + progress->SetLine(2, CVariant{""}); + progress->Progress(); + return progress->IsCanceled(); + } + return m_bStop; + } + + int CVideoInfoScanner::FindVideo(const std::string &title, int year, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress) + { + MOVIELIST movielist; + CVideoInfoDownloader imdb(scraper); + int returncode = imdb.FindMovie(title, year, movielist, progress); + if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress)))) + { // scraper reported an error, or we had an error and user wants to cancel the scan + m_bStop = true; + return -1; // cancelled + } + if (returncode > 0 && movielist.size()) + { + url = movielist[0]; + return 1; // found a movie + } + return 0; // didn't find anything + } + +} diff --git a/xbmc/video/VideoInfoScanner.h b/xbmc/video/VideoInfoScanner.h new file mode 100644 index 0000000..3bd2314 --- /dev/null +++ b/xbmc/video/VideoInfoScanner.h @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "InfoScanner.h" +#include "VideoDatabase.h" +#include "addons/Scraper.h" +#include "guilib/GUIListItem.h" + +#include <set> +#include <string> +#include <vector> + +class CRegExp; +class CFileItem; +class CFileItemList; + +namespace VIDEO +{ + class IVideoInfoTagLoader; + + typedef struct SScanSettings + { + SScanSettings() + { + parent_name = false; + parent_name_root = false; + noupdate = false; + exclude = false; + m_allExtAudio = false; + recurse = 1; + } + bool parent_name; /* use the parent dirname as name of lookup */ + bool parent_name_root; /* use the name of directory where scan started as name for files in that dir */ + int recurse; /* recurse into sub folders (indicate levels) */ + bool noupdate; /* exclude from update library function */ + bool exclude; /* exclude this path from scraping */ + bool m_allExtAudio; /* treat all audio files in video directory as external tracks */ + } SScanSettings; + + class CVideoInfoScanner : public CInfoScanner + { + public: + CVideoInfoScanner(); + ~CVideoInfoScanner() override; + + /*! \brief Scan a folder using the background scanner + \param strDirectory path to scan + \param scanAll whether to scan everything not already scanned (regardless of whether the user normally doesn't want a folder scanned.) Defaults to false. + */ + void Start(const std::string& strDirectory, bool scanAll = false); + void Stop(); + + /*! \brief Add an item to the database. + \param pItem item to add to the database. + \param content content type of the item. + \param videoFolder whether the video is represented by a folder (single movie per folder). Defaults to false. + \param useLocal whether to use local information for artwork etc. + \param showInfo pointer to CVideoInfoTag details for the show if this is an episode. Defaults to NULL. + \param libraryImport Whether this call belongs to a full library import or not. Defaults to false. + \return database id of the added item, or -1 on failure. + */ + long AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder = false, bool useLocal = true, const CVideoInfoTag *showInfo = NULL, bool libraryImport = false); + + /*! \brief Retrieve information for a list of items and add them to the database. + \param items list of items to retrieve info for. + \param bDirNames whether we should use folder or file names for lookups. + \param content type of content to retrieve. + \param useLocal should local data (.nfo and art) be used. Defaults to true. + \param pURL an optional URL to use to retrieve online info. Defaults to NULL. + \param fetchEpisodes whether we are fetching episodes with shows. Defaults to true. + \param pDlgProgress progress dialog to update and check for cancellation during processing. Defaults to NULL. + \return true if we successfully found information for some items, false otherwise + */ + bool RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal = true, CScraperUrl *pURL = NULL, bool fetchEpisodes = true, CGUIDialogProgress* pDlgProgress = NULL); + + static void ApplyThumbToFolder(const std::string &folder, const std::string &imdbThumb); + static bool DownloadFailed(CGUIDialogProgress* pDlgProgress); + + /*! \brief Retrieve any artwork associated with an item + \param pItem item to find artwork for. + \param content content type of the item. + \param bApplyToDir whether we should apply any thumbs to a folder. Defaults to false. + \param useLocal whether we should use local thumbs. Defaults to true. + \param actorArtPath the path to search for actor thumbs. Defaults to empty. + */ + void GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir=false, bool useLocal=true, const std::string &actorArtPath = ""); + + /*! \brief Get season thumbs for a tvshow. + All seasons (regardless of whether the user has episodes) are added to the art map. + \param show tvshow info tag + \param art artwork map to which season thumbs are added. + \param useLocal whether to use local thumbs, defaults to true + */ + static void GetSeasonThumbs(const CVideoInfoTag &show, std::map<int, std::map<std::string, std::string> > &art, const std::vector<std::string> &artTypes, bool useLocal = true); + static std::string GetImage(const CScraperUrl::SUrlEntry &image, const std::string& itemPath); + + bool EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList); + + static std::string GetMovieSetInfoFolder(const std::string& setTitle); + + protected: + virtual void Process(); + bool DoScan(const std::string& strDirectory) override; + + INFO_RET RetrieveInfoForTvShow(CFileItem *pItem, bool bDirNames, ADDON::ScraperPtr &scraper, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress); + INFO_RET RetrieveInfoForMovie(CFileItem *pItem, bool bDirNames, ADDON::ScraperPtr &scraper, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress); + INFO_RET RetrieveInfoForMusicVideo(CFileItem *pItem, bool bDirNames, ADDON::ScraperPtr &scraper, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress); + INFO_RET RetrieveInfoForEpisodes(CFileItem *item, long showID, const ADDON::ScraperPtr &scraper, bool useLocal, CGUIDialogProgress *progress = NULL); + + /*! \brief Update the progress bar with the heading and line and check for cancellation + \param progress CGUIDialogProgress bar + \param heading string id of heading + \param line1 string to set for the first line + \return true if the user has cancelled the scanner, false otherwise + */ + bool ProgressCancelled(CGUIDialogProgress* progress, int heading, const std::string &line1); + + /*! \brief Find a url for the given video using the given scraper + \param title title of the video to lookup + \param year year of the video to lookup + \param scraper scraper to use for the lookup + \param url [out] returned url from the scraper + \param progress CGUIDialogProgress bar + \return >0 on success, <0 on failure (cancellation), and 0 on no info found + */ + int FindVideo(const std::string &title, int year, const ADDON::ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress); + + /*! \brief Find a url for the given video using the given scraper + \param item the video to lookup + \param scraper scraper to use for the lookup + \param url [out] returned url from the scraper + \param progress CGUIDialogProgress bar + \return >0 on success, <0 on failure (cancellation), and 0 on no info found + */ + int FindVideoUsingTag(CFileItem& item, const ADDON::ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress); + + /*! \brief Retrieve detailed information for an item from an online source, optionally supplemented with local data + @todo sort out some better return codes. + \param pItem item to retrieve online details for. + \param url URL to use to retrieve online details. + \param scraper Scraper that handles parsing the online data. + \param nfoFile if set, we override the online data with the locally supplied data. Defaults to NULL. + \param pDialog progress dialog to update and check for cancellation during processing. Defaults to NULL. + \return true if information is found, false if an error occurred, the lookup was cancelled, or no information was found. + */ + bool GetDetails(CFileItem *pItem, CScraperUrl &url, + const ADDON::ScraperPtr &scraper, + VIDEO::IVideoInfoTagLoader* nfoFile = nullptr, + CGUIDialogProgress* pDialog = nullptr); + + /*! \brief Extract episode and season numbers from a processed regexp + \param reg Regular expression object with at least 2 matches + \param episodeInfo Episode information to fill in. + \param defaultSeason Season to use if not found in reg. + \return true on success (2 matches), false on failure (fewer than 2 matches) + */ + bool GetEpisodeAndSeasonFromRegExp(CRegExp ®, EPISODE &episodeInfo, int defaultSeason); + + /*! \brief Extract episode air-date from a processed regexp + \param reg Regular expression object with at least 3 matches + \param episodeInfo Episode information to fill in. + \return true on success (3 matches), false on failure (fewer than 3 matches) + */ + bool GetAirDateFromRegExp(CRegExp ®, EPISODE &episodeInfo); + + /*! \brief Extract episode title from a processed regexp + \param reg Regular expression object with at least 1 match + \param episodeInfo Episode information to fill in. + \return true on success (1 match), false on failure (no matches) + */ + bool GetEpisodeTitleFromRegExp(CRegExp& reg, EPISODE& episodeInfo); + + /*! \brief Fetch thumbs for actors + Updates each actor with their thumb (local or online) + \param actors - vector of SActorInfo + \param strPath - path on filesystem to look for local thumbs + */ + void FetchActorThumbs(std::vector<SActorInfo>& actors, const std::string& strPath); + + static int GetPathHash(const CFileItemList &items, std::string &hash); + + /*! \brief Retrieve a "fast" hash of the given directory (if available) + Performs a stat() on the directory, and uses modified time to create a "fast" + hash of the folder. If no modified time is available, the create time is used, + and if neither are available, an empty hash is returned. + In case exclude from scan expressions are present, the string array will be appended + to the md5 hash to ensure we're doing a re-scan whenever the user modifies those. + \param directory folder to hash + \param excludes string array of exclude expressions + \return the md5 hash of the folder" + */ + std::string GetFastHash(const std::string &directory, const std::vector<std::string> &excludes) const; + + /*! \brief Retrieve a "fast" hash of the given directory recursively (if available) + Performs a stat() on the directory, and uses modified time to create a "fast" + hash of each folder. If no modified time is available, the create time is used, + and if neither are available, an empty hash is returned. + In case exclude from scan expressions are present, the string array will be appended + to the md5 hash to ensure we're doing a re-scan whenever the user modifies those. + \param directory folder to hash (recursively) + \param excludes string array of exclude expressions + \return the md5 hash of the folder + */ + std::string GetRecursiveFastHash(const std::string &directory, const std::vector<std::string> &excludes) const; + + /*! \brief Decide whether a folder listing could use the "fast" hash + Fast hashing can be done whenever the folder contains no scannable subfolders, as the + fast hash technique uses modified time to determine when folder content changes, which + is generally not propagated up the directory tree. + \param items the directory listing + \param excludes string array of exclude expressions + \return true if this directory listing can be fast hashed, false otherwise + */ + bool CanFastHash(const CFileItemList &items, const std::vector<std::string> &excludes) const; + + /*! \brief Process a series folder, filling in episode details and adding them to the database. + @todo Ideally we would return INFO_HAVE_ALREADY if we don't have to update any episodes + and we should return INFO_NOT_FOUND only if no information is found for any of + the episodes. INFO_ADDED then indicates we've added one or more episodes. + \param files the episode files to process. + \param scraper scraper to use for finding online info + \param showInfo information for the show. + \param pDlgProcess progress dialog to update during processing. Defaults to NULL. + \return INFO_ERROR on failure, INFO_CANCELLED on cancellation, + INFO_NOT_FOUND if an episode isn't found, or INFO_ADDED if all episodes are added. + */ + INFO_RET OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress = NULL); + + bool EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList); + bool ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList); + + bool m_bStop; + bool m_scanAll; + std::string m_strStartDir; + CVideoDatabase m_database; + std::set<std::string> m_pathsToCount; + std::set<int> m_pathsToClean; + + private: + static void AddLocalItemArtwork(CGUIListItem::ArtMap& itemArt, + const std::vector<std::string>& wantedArtTypes, const std::string& itemPath, + bool addAll, bool exactName); + + /*! \brief Retrieve the art type for an image from the given size. + \param width the width of the image. + \param height the height of the image. + \return "poster" if the aspect ratio is at most 4:5, "banner" if the aspect ratio + is at least 1:4, "thumb" otherwise. + */ + static std::string GetArtTypeFromSize(unsigned int width, unsigned int height); + }; +} + diff --git a/xbmc/video/VideoInfoTag.cpp b/xbmc/video/VideoInfoTag.cpp new file mode 100644 index 0000000..811dd06 --- /dev/null +++ b/xbmc/video/VideoInfoTag.cpp @@ -0,0 +1,1791 @@ +/* + * Copyright (C) 2005-2018 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 "VideoInfoTag.h" + +#include "ServiceBroker.h" +#include "TextureDatabase.h" +#include "guilib/LocalizeStrings.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/Archive.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <sstream> +#include <string> +#include <vector> + +void CVideoInfoTag::Reset() +{ + m_director.clear(); + m_writingCredits.clear(); + m_genre.clear(); + m_country.clear(); + m_strTagLine.clear(); + m_strPlotOutline.clear(); + m_strPlot.clear(); + m_strPictureURL.Clear(); + m_strTitle.clear(); + m_strShowTitle.clear(); + m_strOriginalTitle.clear(); + m_strSortTitle.clear(); + m_cast.clear(); + m_set.title.clear(); + m_set.id = -1; + m_set.overview.clear(); + m_tags.clear(); + m_strFile.clear(); + m_strPath.clear(); + m_strMPAARating.clear(); + m_strFileNameAndPath.clear(); + m_premiered.Reset(); + m_bHasPremiered = false; + m_strStatus.clear(); + m_strProductionCode.clear(); + m_firstAired.Reset(); + m_studio.clear(); + m_strAlbum.clear(); + m_artist.clear(); + m_strTrailer.clear(); + m_iTop250 = 0; + m_year = -1; + m_iSeason = -1; + m_iEpisode = -1; + m_iIdUniqueID = -1; + m_uniqueIDs.clear(); + m_strDefaultUniqueID = "unknown"; + m_iSpecialSortSeason = -1; + m_iSpecialSortEpisode = -1; + m_strDefaultRating = "default"; + m_iIdRating = -1; + m_ratings.clear(); + m_iUserRating = 0; + m_iDbId = -1; + m_iFileId = -1; + m_iBookmarkId = -1; + m_iTrack = -1; + m_fanart.m_xml.clear(); + m_duration = 0; + m_lastPlayed.Reset(); + m_showLink.clear(); + m_namedSeasons.clear(); + m_streamDetails.Reset(); + m_playCount = PLAYCOUNT_NOT_SET; + m_EpBookmark.Reset(); + m_EpBookmark.type = CBookmark::EPISODE; + m_basePath.clear(); + m_parentPathID = -1; + m_resumePoint.Reset(); + m_resumePoint.type = CBookmark::RESUME; + m_iIdShow = -1; + m_iIdSeason = -1; + m_dateAdded.Reset(); + m_type.clear(); + m_relevance = -1; + m_parsedDetails = 0; + m_coverArt.clear(); +} + +bool CVideoInfoTag::Save(TiXmlNode *node, const std::string &tag, bool savePathInfo, const TiXmlElement *additionalNode) +{ + if (!node) return false; + + // we start with a <tag> tag + TiXmlElement movieElement(tag.c_str()); + TiXmlNode *movie = node->InsertEndChild(movieElement); + + if (!movie) return false; + + XMLUtils::SetString(movie, "title", m_strTitle); + if (!m_strOriginalTitle.empty()) + XMLUtils::SetString(movie, "originaltitle", m_strOriginalTitle); + if (!m_strShowTitle.empty()) + XMLUtils::SetString(movie, "showtitle", m_strShowTitle); + if (!m_strSortTitle.empty()) + XMLUtils::SetString(movie, "sorttitle", m_strSortTitle); + if (!m_ratings.empty()) + { + TiXmlElement ratings("ratings"); + for (const auto& it : m_ratings) + { + TiXmlElement rating("rating"); + rating.SetAttribute("name", it.first.c_str()); + XMLUtils::SetFloat(&rating, "value", it.second.rating); + XMLUtils::SetInt(&rating, "votes", it.second.votes); + rating.SetAttribute("max", 10); + if (it.first == m_strDefaultRating) + rating.SetAttribute("default", "true"); + ratings.InsertEndChild(rating); + } + movie->InsertEndChild(ratings); + } + XMLUtils::SetInt(movie, "userrating", m_iUserRating); + + if (m_EpBookmark.timeInSeconds > 0) + { + TiXmlElement epbookmark("episodebookmark"); + XMLUtils::SetDouble(&epbookmark, "position", m_EpBookmark.timeInSeconds); + if (!m_EpBookmark.playerState.empty()) + { + TiXmlElement playerstate("playerstate"); + CXBMCTinyXML doc; + doc.Parse(m_EpBookmark.playerState); + playerstate.InsertEndChild(*doc.RootElement()); + epbookmark.InsertEndChild(playerstate); + } + movie->InsertEndChild(epbookmark); + } + + XMLUtils::SetInt(movie, "top250", m_iTop250); + if (tag == "episodedetails" || tag == "tvshow") + { + XMLUtils::SetInt(movie, "season", m_iSeason); + XMLUtils::SetInt(movie, "episode", m_iEpisode); + XMLUtils::SetInt(movie, "displayseason",m_iSpecialSortSeason); + XMLUtils::SetInt(movie, "displayepisode",m_iSpecialSortEpisode); + } + if (tag == "musicvideo") + { + XMLUtils::SetInt(movie, "track", m_iTrack); + XMLUtils::SetString(movie, "album", m_strAlbum); + } + XMLUtils::SetString(movie, "outline", m_strPlotOutline); + XMLUtils::SetString(movie, "plot", m_strPlot); + XMLUtils::SetString(movie, "tagline", m_strTagLine); + XMLUtils::SetInt(movie, "runtime", GetDuration() / 60); + if (m_strPictureURL.HasData()) + { + CXBMCTinyXML doc; + doc.Parse(m_strPictureURL.GetData()); + const TiXmlNode* thumb = doc.FirstChild("thumb"); + while (thumb) + { + movie->InsertEndChild(*thumb); + thumb = thumb->NextSibling("thumb"); + } + } + if (m_fanart.m_xml.size()) + { + CXBMCTinyXML doc; + doc.Parse(m_fanart.m_xml); + movie->InsertEndChild(*doc.RootElement()); + } + XMLUtils::SetString(movie, "mpaa", m_strMPAARating); + XMLUtils::SetInt(movie, "playcount", GetPlayCount()); + XMLUtils::SetDate(movie, "lastplayed", m_lastPlayed); + if (savePathInfo) + { + XMLUtils::SetString(movie, "file", m_strFile); + XMLUtils::SetString(movie, "path", m_strPath); + XMLUtils::SetString(movie, "filenameandpath", m_strFileNameAndPath); + XMLUtils::SetString(movie, "basepath", m_basePath); + } + if (!m_strEpisodeGuide.empty()) + { + CXBMCTinyXML doc; + doc.Parse(m_strEpisodeGuide); + if (doc.RootElement()) + movie->InsertEndChild(*doc.RootElement()); + else + XMLUtils::SetString(movie, "episodeguide", m_strEpisodeGuide); + } + + XMLUtils::SetString(movie, "id", GetUniqueID()); + for (const auto& uniqueid : m_uniqueIDs) + { + TiXmlElement uniqueID("uniqueid"); + uniqueID.SetAttribute("type", uniqueid.first); + if (uniqueid.first == m_strDefaultUniqueID) + uniqueID.SetAttribute("default", "true"); + TiXmlText value(uniqueid.second); + uniqueID.InsertEndChild(value); + + movie->InsertEndChild(uniqueID); + } + XMLUtils::SetStringArray(movie, "genre", m_genre); + XMLUtils::SetStringArray(movie, "country", m_country); + if (!m_set.title.empty()) + { + TiXmlElement set("set"); + XMLUtils::SetString(&set, "name", m_set.title); + if (!m_set.overview.empty()) + XMLUtils::SetString(&set, "overview", m_set.overview); + movie->InsertEndChild(set); + } + XMLUtils::SetStringArray(movie, "tag", m_tags); + XMLUtils::SetStringArray(movie, "credits", m_writingCredits); + XMLUtils::SetStringArray(movie, "director", m_director); + if (HasPremiered()) + XMLUtils::SetDate(movie, "premiered", m_premiered); + if (HasYear()) + XMLUtils::SetInt(movie, "year", GetYear()); + XMLUtils::SetString(movie, "status", m_strStatus); + XMLUtils::SetString(movie, "code", m_strProductionCode); + XMLUtils::SetDate(movie, "aired", m_firstAired); + XMLUtils::SetStringArray(movie, "studio", m_studio); + XMLUtils::SetString(movie, "trailer", m_strTrailer); + + if (m_streamDetails.HasItems()) + { + // it goes fileinfo/streamdetails/[video|audio|subtitle] + TiXmlElement fileinfo("fileinfo"); + TiXmlElement streamdetails("streamdetails"); + for (int iStream=1; iStream<=m_streamDetails.GetVideoStreamCount(); iStream++) + { + TiXmlElement stream("video"); + XMLUtils::SetString(&stream, "codec", m_streamDetails.GetVideoCodec(iStream)); + XMLUtils::SetFloat(&stream, "aspect", m_streamDetails.GetVideoAspect(iStream)); + XMLUtils::SetInt(&stream, "width", m_streamDetails.GetVideoWidth(iStream)); + XMLUtils::SetInt(&stream, "height", m_streamDetails.GetVideoHeight(iStream)); + XMLUtils::SetInt(&stream, "durationinseconds", m_streamDetails.GetVideoDuration(iStream)); + XMLUtils::SetString(&stream, "stereomode", m_streamDetails.GetStereoMode(iStream)); + XMLUtils::SetString(&stream, "hdrtype", m_streamDetails.GetVideoHdrType(iStream)); + streamdetails.InsertEndChild(stream); + } + for (int iStream=1; iStream<=m_streamDetails.GetAudioStreamCount(); iStream++) + { + TiXmlElement stream("audio"); + XMLUtils::SetString(&stream, "codec", m_streamDetails.GetAudioCodec(iStream)); + XMLUtils::SetString(&stream, "language", m_streamDetails.GetAudioLanguage(iStream)); + XMLUtils::SetInt(&stream, "channels", m_streamDetails.GetAudioChannels(iStream)); + streamdetails.InsertEndChild(stream); + } + for (int iStream=1; iStream<=m_streamDetails.GetSubtitleStreamCount(); iStream++) + { + TiXmlElement stream("subtitle"); + XMLUtils::SetString(&stream, "language", m_streamDetails.GetSubtitleLanguage(iStream)); + streamdetails.InsertEndChild(stream); + } + fileinfo.InsertEndChild(streamdetails); + movie->InsertEndChild(fileinfo); + } /* if has stream details */ + + // cast + for (iCast it = m_cast.begin(); it != m_cast.end(); ++it) + { + // add a <actor> tag + TiXmlElement cast("actor"); + TiXmlNode *node = movie->InsertEndChild(cast); + XMLUtils::SetString(node, "name", it->strName); + XMLUtils::SetString(node, "role", it->strRole); + XMLUtils::SetInt(node, "order", it->order); + XMLUtils::SetString(node, "thumb", it->thumbUrl.GetFirstUrlByType().m_url); + } + XMLUtils::SetStringArray(movie, "artist", m_artist); + XMLUtils::SetStringArray(movie, "showlink", m_showLink); + + for (const auto& namedSeason : m_namedSeasons) + { + TiXmlElement season("namedseason"); + season.SetAttribute("number", namedSeason.first); + TiXmlText value(namedSeason.second); + season.InsertEndChild(value); + movie->InsertEndChild(season); + } + + TiXmlElement resume("resume"); + XMLUtils::SetDouble(&resume, "position", m_resumePoint.timeInSeconds); + XMLUtils::SetDouble(&resume, "total", m_resumePoint.totalTimeInSeconds); + if (!m_resumePoint.playerState.empty()) + { + TiXmlElement playerstate("playerstate"); + CXBMCTinyXML doc; + doc.Parse(m_resumePoint.playerState); + playerstate.InsertEndChild(*doc.RootElement()); + resume.InsertEndChild(playerstate); + } + movie->InsertEndChild(resume); + + XMLUtils::SetDateTime(movie, "dateadded", m_dateAdded); + + if (additionalNode) + movie->InsertEndChild(*additionalNode); + + return true; +} + +bool CVideoInfoTag::Load(const TiXmlElement *element, bool append, bool prioritise) +{ + if (!element) + return false; + if (!append) + Reset(); + ParseNative(element, prioritise); + return true; +} + +void CVideoInfoTag::Merge(CVideoInfoTag& other) +{ + if (!other.m_director.empty()) + m_director = other.m_director; + if (!other.m_writingCredits.empty()) + m_writingCredits = other.m_writingCredits; + if (!other.m_genre.empty()) + m_genre = other.m_genre; + if (!other.m_country.empty()) + m_country = other.m_country; + if (!other.m_strTagLine.empty()) + m_strTagLine = other.m_strTagLine; + if (!other.m_strPlotOutline.empty()) + m_strPlotOutline = other.m_strPlotOutline; + if (!other.m_strPlot.empty()) + m_strPlot = other.m_strPlot; + if (other.m_strPictureURL.HasData()) + m_strPictureURL = other.m_strPictureURL; + if (!other.m_strTitle.empty()) + m_strTitle = other.m_strTitle; + if (!other.m_strShowTitle.empty()) + m_strShowTitle = other.m_strShowTitle; + if (!other.m_strOriginalTitle.empty()) + m_strOriginalTitle = other.m_strOriginalTitle; + if (!other.m_strSortTitle.empty()) + m_strSortTitle = other.m_strSortTitle; + if (other.m_cast.size()) + m_cast = other.m_cast; + + if (!other.m_set.title.empty()) + m_set.title = other.m_set.title; + if (other.m_set.id) + m_set.id = other.m_set.id; + if (!other.m_set.overview.empty()) + m_set.overview = other.m_set.overview; + if (!other.m_tags.empty()) + m_tags = other.m_tags; + + if (!other.m_strFile.empty()) + m_strFile = other.m_strFile; + if (!other.m_strPath.empty()) + m_strPath = other.m_strPath; + + if (!other.m_strMPAARating.empty()) + m_strMPAARating = other.m_strMPAARating; + if (!other.m_strFileNameAndPath.empty()) + m_strFileNameAndPath = other.m_strFileNameAndPath; + + if (other.m_premiered.IsValid()) + SetPremiered(other.GetPremiered()); + + if (!other.m_strStatus.empty()) + m_strStatus = other.m_strStatus; + if (!other.m_strProductionCode.empty()) + m_strProductionCode = other.m_strProductionCode; + + if (other.m_firstAired.IsValid()) + m_firstAired = other.m_firstAired; + if (!other.m_studio.empty()) + m_studio = other.m_studio; + if (!other.m_strAlbum.empty()) + m_strAlbum = other.m_strAlbum; + if (!other.m_artist.empty()) + m_artist = other.m_artist; + if (!other.m_strTrailer.empty()) + m_strTrailer = other.m_strTrailer; + if (other.m_iTop250) + m_iTop250 = other.m_iTop250; + if (other.m_iSeason != -1) + m_iSeason = other.m_iSeason; + if (other.m_iEpisode != -1) + m_iEpisode = other.m_iEpisode; + + if (other.m_iIdUniqueID != -1) + m_iIdUniqueID = other.m_iIdUniqueID; + if (other.m_uniqueIDs.size()) + { + m_uniqueIDs = other.m_uniqueIDs; + m_strDefaultUniqueID = other.m_strDefaultUniqueID; + }; + if (other.m_iSpecialSortSeason != -1) + m_iSpecialSortSeason = other.m_iSpecialSortSeason; + if (other.m_iSpecialSortEpisode != -1) + m_iSpecialSortEpisode = other.m_iSpecialSortEpisode; + + if (!other.m_ratings.empty()) + { + m_ratings = other.m_ratings; + m_strDefaultRating = other.m_strDefaultRating; + }; + if (other.m_iIdRating != -1) + m_iIdRating = other.m_iIdRating; + if (other.m_iUserRating) + m_iUserRating = other.m_iUserRating; + + if (other.m_iDbId != -1) + m_iDbId = other.m_iDbId; + if (other.m_iFileId != -1) + m_iFileId = other.m_iFileId; + if (other.m_iBookmarkId != -1) + m_iBookmarkId = other.m_iBookmarkId; + if (other.m_iTrack != -1) + m_iTrack = other.m_iTrack; + + if (other.m_fanart.GetNumFanarts()) + m_fanart = other.m_fanart; + + if (other.m_duration) + m_duration = other.m_duration; + if (other.m_lastPlayed.IsValid()) + m_lastPlayed = other.m_lastPlayed; + + if (!other.m_showLink.empty()) + m_showLink = other.m_showLink; + if (other.m_namedSeasons.size()) + m_namedSeasons = other.m_namedSeasons; + if (other.m_streamDetails.HasItems()) + m_streamDetails = other.m_streamDetails; + if (other.IsPlayCountSet()) + SetPlayCount(other.GetPlayCount()); + + if (other.m_EpBookmark.IsSet()) + m_EpBookmark = other.m_EpBookmark; + + if (!other.m_basePath.empty()) + m_basePath = other.m_basePath; + if (other.m_parentPathID != -1) + m_parentPathID = other.m_parentPathID; + if (other.GetResumePoint().IsSet()) + SetResumePoint(other.GetResumePoint()); + if (other.m_iIdShow != -1) + m_iIdShow = other.m_iIdShow; + if (other.m_iIdSeason != -1) + m_iIdSeason = other.m_iIdSeason; + + if (other.m_dateAdded.IsValid()) + m_dateAdded = other.m_dateAdded; + + if (!other.m_type.empty()) + m_type = other.m_type; + + if (other.m_relevance != -1) + m_relevance = other.m_relevance; + if (other.m_parsedDetails) + m_parsedDetails = other.m_parsedDetails; + if (other.m_coverArt.size()) + m_coverArt = other.m_coverArt; + if (other.m_year != -1) + m_year = other.m_year; +} + +void CVideoInfoTag::Archive(CArchive& ar) +{ + if (ar.IsStoring()) + { + ar << m_director; + ar << m_writingCredits; + ar << m_genre; + ar << m_country; + ar << m_strTagLine; + ar << m_strPlotOutline; + ar << m_strPlot; + ar << m_strPictureURL.GetData(); + ar << m_fanart.m_xml; + ar << m_strTitle; + ar << m_strSortTitle; + ar << m_studio; + ar << m_strTrailer; + ar << (int)m_cast.size(); + for (unsigned int i=0;i<m_cast.size();++i) + { + ar << m_cast[i].strName; + ar << m_cast[i].strRole; + ar << m_cast[i].order; + ar << m_cast[i].thumb; + ar << m_cast[i].thumbUrl.GetData(); + } + + ar << m_set.title; + ar << m_set.id; + ar << m_set.overview; + ar << m_tags; + ar << m_duration; + ar << m_strFile; + ar << m_strPath; + ar << m_strMPAARating; + ar << m_strFileNameAndPath; + ar << m_strOriginalTitle; + ar << m_strEpisodeGuide; + ar << m_premiered; + ar << m_bHasPremiered; + ar << m_strStatus; + ar << m_strProductionCode; + ar << m_firstAired; + ar << m_strShowTitle; + ar << m_strAlbum; + ar << m_artist; + ar << GetPlayCount(); + ar << m_lastPlayed; + ar << m_iTop250; + ar << m_iSeason; + ar << m_iEpisode; + ar << (int)m_uniqueIDs.size(); + for (const auto& i : m_uniqueIDs) + { + ar << i.first; + ar << (i.first == m_strDefaultUniqueID); + ar << i.second; + } + ar << (int)m_ratings.size(); + for (const auto& i : m_ratings) + { + ar << i.first; + ar << (i.first == m_strDefaultRating); + ar << i.second.rating; + ar << i.second.votes; + } + ar << m_iUserRating; + ar << m_iDbId; + ar << m_iFileId; + ar << m_iSpecialSortSeason; + ar << m_iSpecialSortEpisode; + ar << m_iBookmarkId; + ar << m_iTrack; + ar << dynamic_cast<IArchivable&>(m_streamDetails); + ar << m_showLink; + ar << static_cast<int>(m_namedSeasons.size()); + for (const auto& namedSeason : m_namedSeasons) + { + ar << namedSeason.first; + ar << namedSeason.second; + } + ar << m_EpBookmark.playerState; + ar << m_EpBookmark.timeInSeconds; + ar << m_basePath; + ar << m_parentPathID; + ar << m_resumePoint.timeInSeconds; + ar << m_resumePoint.totalTimeInSeconds; + ar << m_resumePoint.playerState; + ar << m_iIdShow; + ar << m_dateAdded.GetAsDBDateTime(); + ar << m_type; + ar << m_iIdSeason; + ar << m_coverArt.size(); + for (auto& it : m_coverArt) + ar << it; + } + else + { + ar >> m_director; + ar >> m_writingCredits; + ar >> m_genre; + ar >> m_country; + ar >> m_strTagLine; + ar >> m_strPlotOutline; + ar >> m_strPlot; + std::string data; + ar >> data; + m_strPictureURL.SetData(data); + ar >> m_fanart.m_xml; + ar >> m_strTitle; + ar >> m_strSortTitle; + ar >> m_studio; + ar >> m_strTrailer; + int iCastSize; + ar >> iCastSize; + m_cast.reserve(iCastSize); + for (int i=0;i<iCastSize;++i) + { + SActorInfo info; + ar >> info.strName; + ar >> info.strRole; + ar >> info.order; + ar >> info.thumb; + std::string strXml; + ar >> strXml; + info.thumbUrl.ParseFromData(strXml); + m_cast.push_back(info); + } + + ar >> m_set.title; + ar >> m_set.id; + ar >> m_set.overview; + ar >> m_tags; + ar >> m_duration; + ar >> m_strFile; + ar >> m_strPath; + ar >> m_strMPAARating; + ar >> m_strFileNameAndPath; + ar >> m_strOriginalTitle; + ar >> m_strEpisodeGuide; + ar >> m_premiered; + ar >> m_bHasPremiered; + ar >> m_strStatus; + ar >> m_strProductionCode; + ar >> m_firstAired; + ar >> m_strShowTitle; + ar >> m_strAlbum; + ar >> m_artist; + ar >> m_playCount; + ar >> m_lastPlayed; + ar >> m_iTop250; + ar >> m_iSeason; + ar >> m_iEpisode; + int iUniqueIDSize; + ar >> iUniqueIDSize; + for (int i = 0; i < iUniqueIDSize; ++i) + { + std::string value; + std::string name; + bool defaultUniqueID; + ar >> name; + ar >> defaultUniqueID; + ar >> value; + SetUniqueID(value, name); + if (defaultUniqueID) + m_strDefaultUniqueID = name; + } + int iRatingSize; + ar >> iRatingSize; + for (int i = 0; i < iRatingSize; ++i) + { + CRating rating; + std::string name; + bool defaultRating; + ar >> name; + ar >> defaultRating; + ar >> rating.rating; + ar >> rating.votes; + SetRating(rating, name); + if (defaultRating) + m_strDefaultRating = name; + } + ar >> m_iUserRating; + ar >> m_iDbId; + ar >> m_iFileId; + ar >> m_iSpecialSortSeason; + ar >> m_iSpecialSortEpisode; + ar >> m_iBookmarkId; + ar >> m_iTrack; + ar >> dynamic_cast<IArchivable&>(m_streamDetails); + ar >> m_showLink; + + int namedSeasonSize; + ar >> namedSeasonSize; + for (int i = 0; i < namedSeasonSize; ++i) + { + int seasonNumber; + ar >> seasonNumber; + std::string seasonName; + ar >> seasonName; + m_namedSeasons.insert(std::make_pair(seasonNumber, seasonName)); + } + ar >> m_EpBookmark.playerState; + ar >> m_EpBookmark.timeInSeconds; + ar >> m_basePath; + ar >> m_parentPathID; + ar >> m_resumePoint.timeInSeconds; + ar >> m_resumePoint.totalTimeInSeconds; + ar >> m_resumePoint.playerState; + ar >> m_iIdShow; + + std::string dateAdded; + ar >> dateAdded; + m_dateAdded.SetFromDBDateTime(dateAdded); + ar >> m_type; + ar >> m_iIdSeason; + size_t size; + ar >> size; + m_coverArt.resize(size); + for (size_t i = 0; i < size; ++i) + ar >> m_coverArt[i]; + } +} + +void CVideoInfoTag::Serialize(CVariant& value) const +{ + value["director"] = m_director; + value["writer"] = m_writingCredits; + value["genre"] = m_genre; + value["country"] = m_country; + value["tagline"] = m_strTagLine; + value["plotoutline"] = m_strPlotOutline; + value["plot"] = m_strPlot; + value["title"] = m_strTitle; + value["votes"] = std::to_string(GetRating().votes); + value["studio"] = m_studio; + value["trailer"] = m_strTrailer; + value["cast"] = CVariant(CVariant::VariantTypeArray); + for (unsigned int i = 0; i < m_cast.size(); ++i) + { + CVariant actor; + actor["name"] = m_cast[i].strName; + actor["role"] = m_cast[i].strRole; + actor["order"] = m_cast[i].order; + if (!m_cast[i].thumb.empty()) + actor["thumbnail"] = CTextureUtils::GetWrappedImageURL(m_cast[i].thumb); + value["cast"].push_back(actor); + } + value["set"] = m_set.title; + value["setid"] = m_set.id; + value["setoverview"] = m_set.overview; + value["tag"] = m_tags; + value["runtime"] = GetDuration(); + value["file"] = m_strFile; + value["path"] = m_strPath; + value["imdbnumber"] = GetUniqueID(); + value["mpaa"] = m_strMPAARating; + value["filenameandpath"] = m_strFileNameAndPath; + value["originaltitle"] = m_strOriginalTitle; + value["sorttitle"] = m_strSortTitle; + value["episodeguide"] = m_strEpisodeGuide; + value["premiered"] = m_premiered.IsValid() ? m_premiered.GetAsDBDate() : StringUtils::Empty; + value["status"] = m_strStatus; + value["productioncode"] = m_strProductionCode; + value["firstaired"] = m_firstAired.IsValid() ? m_firstAired.GetAsDBDate() : StringUtils::Empty; + value["showtitle"] = m_strShowTitle; + value["album"] = m_strAlbum; + value["artist"] = m_artist; + value["playcount"] = GetPlayCount(); + value["lastplayed"] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty; + value["top250"] = m_iTop250; + value["year"] = GetYear(); + value["season"] = m_iSeason; + value["episode"] = m_iEpisode; + for (const auto& i : m_uniqueIDs) + value["uniqueid"][i.first] = i.second; + + value["rating"] = GetRating().rating; + CVariant ratings = CVariant(CVariant::VariantTypeObject); + for (const auto& i : m_ratings) + { + CVariant rating; + rating["rating"] = i.second.rating; + rating["votes"] = i.second.votes; + rating["default"] = i.first == m_strDefaultRating; + + ratings[i.first] = rating; + } + value["ratings"] = ratings; + value["userrating"] = m_iUserRating; + value["dbid"] = m_iDbId; + value["fileid"] = m_iFileId; + value["track"] = m_iTrack; + value["showlink"] = m_showLink; + m_streamDetails.Serialize(value["streamdetails"]); + CVariant resume = CVariant(CVariant::VariantTypeObject); + resume["position"] = m_resumePoint.timeInSeconds; + resume["total"] = m_resumePoint.totalTimeInSeconds; + value["resume"] = resume; + value["tvshowid"] = m_iIdShow; + value["dateadded"] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty; + value["type"] = m_type; + value["seasonid"] = m_iIdSeason; + value["specialsortseason"] = m_iSpecialSortSeason; + value["specialsortepisode"] = m_iSpecialSortEpisode; +} + +void CVideoInfoTag::ToSortable(SortItem& sortable, Field field) const +{ + switch (field) + { + case FieldDirector: sortable[FieldDirector] = m_director; break; + case FieldWriter: sortable[FieldWriter] = m_writingCredits; break; + case FieldGenre: sortable[FieldGenre] = m_genre; break; + case FieldCountry: sortable[FieldCountry] = m_country; break; + case FieldTagline: sortable[FieldTagline] = m_strTagLine; break; + case FieldPlotOutline: sortable[FieldPlotOutline] = m_strPlotOutline; break; + case FieldPlot: sortable[FieldPlot] = m_strPlot; break; + case FieldTitle: + { + // make sure not to overwrite an existing title with an empty one + std::string title = m_strTitle; + if (!title.empty() || sortable.find(FieldTitle) == sortable.end()) + sortable[FieldTitle] = title; + break; + } + case FieldVotes: sortable[FieldVotes] = GetRating().votes; break; + case FieldStudio: sortable[FieldStudio] = m_studio; break; + case FieldTrailer: sortable[FieldTrailer] = m_strTrailer; break; + case FieldSet: sortable[FieldSet] = m_set.title; break; + case FieldTime: sortable[FieldTime] = GetDuration(); break; + case FieldFilename: sortable[FieldFilename] = m_strFile; break; + case FieldMPAA: sortable[FieldMPAA] = m_strMPAARating; break; + case FieldPath: + { + // make sure not to overwrite an existing path with an empty one + std::string path = GetPath(); + if (!path.empty() || sortable.find(FieldPath) == sortable.end()) + sortable[FieldPath] = path; + break; + } + case FieldSortTitle: + { + // seasons with a custom name/title need special handling as they should be sorted by season number + if (m_type == MediaTypeSeason && !m_strSortTitle.empty()) + sortable[FieldSortTitle] = StringUtils::Format(g_localizeStrings.Get(20358), m_iSeason); + else + sortable[FieldSortTitle] = m_strSortTitle; + break; + } + case FieldOriginalTitle: + { + // seasons with a custom name/title need special handling as they should be sorted by season number + if (m_type == MediaTypeSeason && !m_strOriginalTitle.empty()) + sortable[FieldOriginalTitle] = + StringUtils::Format(g_localizeStrings.Get(20358).c_str(), m_iSeason); + else + sortable[FieldOriginalTitle] = m_strOriginalTitle; + break; + } + case FieldTvShowStatus: sortable[FieldTvShowStatus] = m_strStatus; break; + case FieldProductionCode: sortable[FieldProductionCode] = m_strProductionCode; break; + case FieldAirDate: sortable[FieldAirDate] = m_firstAired.IsValid() ? m_firstAired.GetAsDBDate() : (m_premiered.IsValid() ? m_premiered.GetAsDBDate() : StringUtils::Empty); break; + case FieldTvShowTitle: sortable[FieldTvShowTitle] = m_strShowTitle; break; + case FieldAlbum: sortable[FieldAlbum] = m_strAlbum; break; + case FieldArtist: sortable[FieldArtist] = m_artist; break; + case FieldPlaycount: sortable[FieldPlaycount] = GetPlayCount(); break; + case FieldLastPlayed: sortable[FieldLastPlayed] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty; break; + case FieldTop250: sortable[FieldTop250] = m_iTop250; break; + case FieldYear: sortable[FieldYear] = GetYear(); break; + case FieldSeason: sortable[FieldSeason] = m_iSeason; break; + case FieldEpisodeNumber: sortable[FieldEpisodeNumber] = m_iEpisode; break; + case FieldNumberOfEpisodes: sortable[FieldNumberOfEpisodes] = m_iEpisode; break; + case FieldNumberOfWatchedEpisodes: sortable[FieldNumberOfWatchedEpisodes] = m_iEpisode; break; + case FieldEpisodeNumberSpecialSort: sortable[FieldEpisodeNumberSpecialSort] = m_iSpecialSortEpisode; break; + case FieldSeasonSpecialSort: sortable[FieldSeasonSpecialSort] = m_iSpecialSortSeason; break; + case FieldRating: sortable[FieldRating] = GetRating().rating; break; + case FieldUserRating: sortable[FieldUserRating] = m_iUserRating; break; + case FieldId: sortable[FieldId] = m_iDbId; break; + case FieldTrackNumber: sortable[FieldTrackNumber] = m_iTrack; break; + case FieldTag: sortable[FieldTag] = m_tags; break; + + case FieldVideoResolution: sortable[FieldVideoResolution] = m_streamDetails.GetVideoHeight(); break; + case FieldVideoAspectRatio: sortable[FieldVideoAspectRatio] = m_streamDetails.GetVideoAspect(); break; + case FieldVideoCodec: sortable[FieldVideoCodec] = m_streamDetails.GetVideoCodec(); break; + case FieldStereoMode: sortable[FieldStereoMode] = m_streamDetails.GetStereoMode(); break; + + case FieldAudioChannels: sortable[FieldAudioChannels] = m_streamDetails.GetAudioChannels(); break; + case FieldAudioCodec: sortable[FieldAudioCodec] = m_streamDetails.GetAudioCodec(); break; + case FieldAudioLanguage: sortable[FieldAudioLanguage] = m_streamDetails.GetAudioLanguage(); break; + + case FieldSubtitleLanguage: sortable[FieldSubtitleLanguage] = m_streamDetails.GetSubtitleLanguage(); break; + + case FieldInProgress: sortable[FieldInProgress] = m_resumePoint.IsPartWay(); break; + case FieldDateAdded: sortable[FieldDateAdded] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty; break; + case FieldMediaType: sortable[FieldMediaType] = m_type; break; + case FieldRelevance: sortable[FieldRelevance] = m_relevance; break; + default: break; + } +} + +const CRating CVideoInfoTag::GetRating(std::string type) const +{ + if (type.empty()) + type = m_strDefaultRating; + + const auto& rating = m_ratings.find(type); + if (rating == m_ratings.end()) + return CRating(); + + return rating->second; +} + +const std::string& CVideoInfoTag::GetDefaultRating() const +{ + return m_strDefaultRating; +} + +bool CVideoInfoTag::HasYear() const +{ + return m_year > 0 || m_firstAired.IsValid() || m_premiered.IsValid(); +} + +int CVideoInfoTag::GetYear() const +{ + if (m_year > 0) + return m_year; + if (m_firstAired.IsValid()) + return GetFirstAired().GetYear(); + if (m_premiered.IsValid()) + return GetPremiered().GetYear(); + return 0; +} + +bool CVideoInfoTag::HasPremiered() const +{ + return m_bHasPremiered; +} + +const CDateTime& CVideoInfoTag::GetPremiered() const +{ + return m_premiered; +} + +const CDateTime& CVideoInfoTag::GetFirstAired() const +{ + return m_firstAired; +} + +const std::string CVideoInfoTag::GetUniqueID(std::string type) const +{ + if (type.empty()) + type = m_strDefaultUniqueID; + + const auto& uniqueid = m_uniqueIDs.find(type); + if (uniqueid == m_uniqueIDs.end()) + return ""; + + return uniqueid->second; +} + +const std::map<std::string, std::string>& CVideoInfoTag::GetUniqueIDs() const +{ + return m_uniqueIDs; +} + +const std::string& CVideoInfoTag::GetDefaultUniqueID() const +{ + return m_strDefaultUniqueID; +} + +bool CVideoInfoTag::HasUniqueID() const +{ + return !m_uniqueIDs.empty(); +} + +const std::string CVideoInfoTag::GetCast(bool bIncludeRole /*= false*/) const +{ + std::string strLabel; + for (iCast it = m_cast.begin(); it != m_cast.end(); ++it) + { + std::string character; + if (it->strRole.empty() || !bIncludeRole) + character = StringUtils::Format("{}\n", it->strName); + else + character = + StringUtils::Format("{} {} {}\n", it->strName, g_localizeStrings.Get(20347), it->strRole); + strLabel += character; + } + return StringUtils::TrimRight(strLabel, "\n"); +} + +void CVideoInfoTag::ParseNative(const TiXmlElement* movie, bool prioritise) +{ + std::string value; + float fValue; + + if (XMLUtils::GetString(movie, "title", value)) + SetTitle(value); + + if (XMLUtils::GetString(movie, "originaltitle", value)) + SetOriginalTitle(value); + + if (XMLUtils::GetString(movie, "showtitle", value)) + SetShowTitle(value); + + if (XMLUtils::GetString(movie, "sorttitle", value)) + SetSortTitle(value); + + const TiXmlElement* node = movie->FirstChildElement("ratings"); + if (node) + { + for (const TiXmlElement* child = node->FirstChildElement("rating"); child != nullptr; child = child->NextSiblingElement("rating")) + { + CRating r; + std::string name; + if (child->QueryStringAttribute("name", &name) != TIXML_SUCCESS) + name = "default"; + XMLUtils::GetFloat(child, "value", r.rating); + if (XMLUtils::GetString(child, "votes", value)) + r.votes = StringUtils::ReturnDigits(value); + int max_value = 10; + if ((child->QueryIntAttribute("max", &max_value) == TIXML_SUCCESS) && max_value >= 1) + r.rating = r.rating / max_value * 10; // Normalise the Movie Rating to between 1 and 10 + SetRating(r, name); + bool isDefault = false; + // guard against assert in tinyxml + const char* rAtt = child->Attribute("default", static_cast<int*>(nullptr)); + if (rAtt && strlen(rAtt) != 0 && + (child->QueryBoolAttribute("default", &isDefault) == TIXML_SUCCESS) && isDefault) + m_strDefaultRating = name; + } + } + else if (XMLUtils::GetFloat(movie, "rating", fValue)) + { + CRating r(fValue, 0); + if (XMLUtils::GetString(movie, "votes", value)) + r.votes = StringUtils::ReturnDigits(value); + int max_value = 10; + const TiXmlElement* rElement = movie->FirstChildElement("rating"); + if (rElement && (rElement->QueryIntAttribute("max", &max_value) == TIXML_SUCCESS) && max_value >= 1) + r.rating = r.rating / max_value * 10; // Normalise the Movie Rating to between 1 and 10 + SetRating(r, "default"); + m_strDefaultRating = "default"; + } + XMLUtils::GetInt(movie, "userrating", m_iUserRating); + + const TiXmlElement *epbookmark = movie->FirstChildElement("episodebookmark"); + if (epbookmark) + { + XMLUtils::GetDouble(epbookmark, "position", m_EpBookmark.timeInSeconds); + const TiXmlElement *playerstate = epbookmark->FirstChildElement("playerstate"); + if (playerstate) + { + const TiXmlElement *value = playerstate->FirstChildElement(); + if (value) + m_EpBookmark.playerState << *value; + } + } + else + XMLUtils::GetDouble(movie, "epbookmark", m_EpBookmark.timeInSeconds); + + int max_value = 10; + const TiXmlElement* urElement = movie->FirstChildElement("userrating"); + if (urElement && (urElement->QueryIntAttribute("max", &max_value) == TIXML_SUCCESS) && max_value >= 1) + m_iUserRating = m_iUserRating / max_value * 10; // Normalise the user Movie Rating to between 1 and 10 + XMLUtils::GetInt(movie, "top250", m_iTop250); + XMLUtils::GetInt(movie, "season", m_iSeason); + XMLUtils::GetInt(movie, "episode", m_iEpisode); + XMLUtils::GetInt(movie, "track", m_iTrack); + + XMLUtils::GetInt(movie, "displayseason", m_iSpecialSortSeason); + XMLUtils::GetInt(movie, "displayepisode", m_iSpecialSortEpisode); + int after=0; + XMLUtils::GetInt(movie, "displayafterseason",after); + if (after > 0) + { + m_iSpecialSortSeason = after; + m_iSpecialSortEpisode = 0x1000; // should be more than any realistic episode number + } + + if (XMLUtils::GetString(movie, "outline", value)) + SetPlotOutline(value); + + if (XMLUtils::GetString(movie, "plot", value)) + SetPlot(value); + + if (XMLUtils::GetString(movie, "tagline", value)) + SetTagLine(value); + + + if (XMLUtils::GetString(movie, "runtime", value) && !value.empty()) + m_duration = GetDurationFromMinuteString(StringUtils::Trim(value)); + + if (XMLUtils::GetString(movie, "mpaa", value)) + SetMPAARating(value); + + XMLUtils::GetInt(movie, "playcount", m_playCount); + XMLUtils::GetDate(movie, "lastplayed", m_lastPlayed); + + if (XMLUtils::GetString(movie, "file", value)) + SetFile(value); + + if (XMLUtils::GetString(movie, "path", value)) + SetPath(value); + + const TiXmlElement* uniqueid = movie->FirstChildElement("uniqueid"); + if (uniqueid == nullptr) + { + if (XMLUtils::GetString(movie, "id", value)) + SetUniqueID(value); + } + else + { + for (; uniqueid != nullptr; uniqueid = uniqueid->NextSiblingElement("uniqueid")) + { + if (uniqueid->FirstChild()) + { + if (uniqueid->QueryStringAttribute("type", &value) == TIXML_SUCCESS) + SetUniqueID(uniqueid->FirstChild()->ValueStr(), value); + else + SetUniqueID(uniqueid->FirstChild()->ValueStr()); + bool isDefault; + if (m_strDefaultUniqueID == "unknown" && + (uniqueid->QueryBoolAttribute("default", &isDefault) == TIXML_SUCCESS) && isDefault) + { + m_strDefaultUniqueID = value; + } + } + } + } + + if (XMLUtils::GetString(movie, "filenameandpath", value)) + SetFileNameAndPath(value); + + if (XMLUtils::GetDate(movie, "premiered", m_premiered)) + { + m_bHasPremiered = true; + } + else + { + int year; + if (XMLUtils::GetInt(movie, "year", year)) + SetYear(year); + } + + if (XMLUtils::GetString(movie, "status", value)) + SetStatus(value); + + if (XMLUtils::GetString(movie, "code", value)) + SetProductionCode(value); + + XMLUtils::GetDate(movie, "aired", m_firstAired); + + if (XMLUtils::GetString(movie, "album", value)) + SetAlbum(value); + + if (XMLUtils::GetString(movie, "trailer", value)) + SetTrailer(value); + + if (XMLUtils::GetString(movie, "basepath", value)) + SetBasePath(value); + + // make sure the picture URLs have been parsed + m_strPictureURL.Parse(); + size_t iThumbCount = m_strPictureURL.GetUrls().size(); + std::string xmlAdd = m_strPictureURL.GetData(); + + const TiXmlElement* thumb = movie->FirstChildElement("thumb"); + while (thumb) + { + m_strPictureURL.ParseAndAppendUrl(thumb); + if (prioritise) + { + std::string temp; + temp << *thumb; + xmlAdd = temp+xmlAdd; + } + thumb = thumb->NextSiblingElement("thumb"); + } + + // prioritise thumbs from nfos + if (prioritise && iThumbCount && iThumbCount != m_strPictureURL.GetUrls().size()) + { + auto thumbUrls = m_strPictureURL.GetUrls(); + rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end()); + m_strPictureURL.SetUrls(thumbUrls); + m_strPictureURL.SetData(xmlAdd); + } + + const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator; + + std::vector<std::string> genres(m_genre); + if (XMLUtils::GetStringArray(movie, "genre", genres, prioritise, itemSeparator)) + SetGenre(genres); + + std::vector<std::string> country(m_country); + if (XMLUtils::GetStringArray(movie, "country", country, prioritise, itemSeparator)) + SetCountry(country); + + std::vector<std::string> credits(m_writingCredits); + if (XMLUtils::GetStringArray(movie, "credits", credits, prioritise, itemSeparator)) + SetWritingCredits(credits); + + std::vector<std::string> director(m_director); + if (XMLUtils::GetStringArray(movie, "director", director, prioritise, itemSeparator)) + SetDirector(director); + + std::vector<std::string> showLink(m_showLink); + if (XMLUtils::GetStringArray(movie, "showlink", showLink, prioritise, itemSeparator)) + SetShowLink(showLink); + + const TiXmlElement* namedSeason = movie->FirstChildElement("namedseason"); + while (namedSeason != nullptr) + { + if (namedSeason->FirstChild() != nullptr) + { + int seasonNumber; + std::string seasonName = namedSeason->FirstChild()->ValueStr(); + if (!seasonName.empty() && + namedSeason->Attribute("number", &seasonNumber) != nullptr) + m_namedSeasons.insert(std::make_pair(seasonNumber, seasonName)); + } + + namedSeason = namedSeason->NextSiblingElement("namedseason"); + } + + // cast + node = movie->FirstChildElement("actor"); + if (node && node->FirstChild() && prioritise) + m_cast.clear(); + while (node) + { + const TiXmlNode *actor = node->FirstChild("name"); + if (actor && actor->FirstChild()) + { + SActorInfo info; + info.strName = actor->FirstChild()->Value(); + + if (XMLUtils::GetString(node, "role", value)) + info.strRole = StringUtils::Trim(value); + + XMLUtils::GetInt(node, "order", info.order); + const TiXmlElement* thumb = node->FirstChildElement("thumb"); + while (thumb) + { + info.thumbUrl.ParseAndAppendUrl(thumb); + thumb = thumb->NextSiblingElement("thumb"); + } + const char* clear=node->Attribute("clear"); + if (clear && StringUtils::CompareNoCase(clear, "true")) + m_cast.clear(); + m_cast.push_back(info); + } + node = node->NextSiblingElement("actor"); + } + + // Pre-Jarvis NFO file: + // <set>A set</set> + if (XMLUtils::GetString(movie, "set", value)) + SetSet(value); + // Jarvis+: + // <set><name>A set</name><overview>A set with a number of movies...</overview></set> + node = movie->FirstChildElement("set"); + if (node) + { + // No name, no set + if (XMLUtils::GetString(node, "name", value)) + { + SetSet(value); + if (XMLUtils::GetString(node, "overview", value)) + SetSetOverview(value); + } + } + + std::vector<std::string> tags(m_tags); + if (XMLUtils::GetStringArray(movie, "tag", tags, prioritise, itemSeparator)) + SetTags(tags); + + std::vector<std::string> studio(m_studio); + if (XMLUtils::GetStringArray(movie, "studio", studio, prioritise, itemSeparator)) + SetStudio(studio); + + // artists + std::vector<std::string> artist(m_artist); + node = movie->FirstChildElement("artist"); + if (node && node->FirstChild() && prioritise) + artist.clear(); + while (node) + { + const TiXmlNode* pNode = node->FirstChild("name"); + const char* pValue=NULL; + if (pNode && pNode->FirstChild()) + pValue = pNode->FirstChild()->Value(); + else if (node->FirstChild()) + pValue = node->FirstChild()->Value(); + if (pValue) + { + const char* clear=node->Attribute("clear"); + if (clear && StringUtils::CompareNoCase(clear, "true") == 0) + artist.clear(); + std::vector<std::string> newArtists = StringUtils::Split(pValue, itemSeparator); + artist.insert(artist.end(), newArtists.begin(), newArtists.end()); + } + node = node->NextSiblingElement("artist"); + } + SetArtist(artist); + + node = movie->FirstChildElement("fileinfo"); + if (node) + { + // Try to pull from fileinfo/streamdetails/[video|audio|subtitle] + const TiXmlNode *nodeStreamDetails = node->FirstChild("streamdetails"); + if (nodeStreamDetails) + { + const TiXmlNode *nodeDetail = NULL; + while ((nodeDetail = nodeStreamDetails->IterateChildren("audio", nodeDetail))) + { + CStreamDetailAudio *p = new CStreamDetailAudio(); + if (XMLUtils::GetString(nodeDetail, "codec", value)) + p->m_strCodec = StringUtils::Trim(value); + + if (XMLUtils::GetString(nodeDetail, "language", value)) + p->m_strLanguage = StringUtils::Trim(value); + + XMLUtils::GetInt(nodeDetail, "channels", p->m_iChannels); + StringUtils::ToLower(p->m_strCodec); + StringUtils::ToLower(p->m_strLanguage); + m_streamDetails.AddStream(p); + } + nodeDetail = NULL; + while ((nodeDetail = nodeStreamDetails->IterateChildren("video", nodeDetail))) + { + CStreamDetailVideo *p = new CStreamDetailVideo(); + if (XMLUtils::GetString(nodeDetail, "codec", value)) + p->m_strCodec = StringUtils::Trim(value); + + XMLUtils::GetFloat(nodeDetail, "aspect", p->m_fAspect); + XMLUtils::GetInt(nodeDetail, "width", p->m_iWidth); + XMLUtils::GetInt(nodeDetail, "height", p->m_iHeight); + XMLUtils::GetInt(nodeDetail, "durationinseconds", p->m_iDuration); + if (XMLUtils::GetString(nodeDetail, "stereomode", value)) + p->m_strStereoMode = StringUtils::Trim(value); + if (XMLUtils::GetString(nodeDetail, "language", value)) + p->m_strLanguage = StringUtils::Trim(value); + if (XMLUtils::GetString(nodeDetail, "hdrtype", value)) + p->m_strHdrType = StringUtils::Trim(value); + + StringUtils::ToLower(p->m_strCodec); + StringUtils::ToLower(p->m_strStereoMode); + StringUtils::ToLower(p->m_strLanguage); + StringUtils::ToLower(p->m_strHdrType); + m_streamDetails.AddStream(p); + } + nodeDetail = NULL; + while ((nodeDetail = nodeStreamDetails->IterateChildren("subtitle", nodeDetail))) + { + CStreamDetailSubtitle *p = new CStreamDetailSubtitle(); + if (XMLUtils::GetString(nodeDetail, "language", value)) + p->m_strLanguage = StringUtils::Trim(value); + StringUtils::ToLower(p->m_strLanguage); + m_streamDetails.AddStream(p); + } + } + m_streamDetails.DetermineBestStreams(); + } /* if fileinfo */ + + if (m_strEpisodeGuide.empty()) + { + const TiXmlElement* epguide = movie->FirstChildElement("episodeguide"); + if (epguide) + { + // DEPRECIATE ME - support for old XML-encoded <episodeguide> blocks. + if (epguide->FirstChild() && + StringUtils::CompareNoCase("<episodeguide", epguide->FirstChild()->Value(), 13) == 0) + { + m_strEpisodeGuide = epguide->FirstChild()->Value(); + } + else + { + std::stringstream stream; + stream << *epguide; + m_strEpisodeGuide = stream.str(); + } + } + } + + // fanart + const TiXmlElement *fanart = movie->FirstChildElement("fanart"); + if (fanart) + { + // we prioritise mixed-mode nfo's with fanart set + if (prioritise) + { + std::string temp; + temp << *fanart; + m_fanart.m_xml = temp+m_fanart.m_xml; + } + else + m_fanart.m_xml << *fanart; + m_fanart.Unpack(); + } + + // resumePoint + const TiXmlNode *resume = movie->FirstChild("resume"); + if (resume) + { + XMLUtils::GetDouble(resume, "position", m_resumePoint.timeInSeconds); + XMLUtils::GetDouble(resume, "total", m_resumePoint.totalTimeInSeconds); + const TiXmlElement *playerstate = resume->FirstChildElement("playerstate"); + if (playerstate) + { + const TiXmlElement *value = playerstate->FirstChildElement(); + if (value) + m_resumePoint.playerState << *value; + } + } + + XMLUtils::GetDateTime(movie, "dateadded", m_dateAdded); +} + +bool CVideoInfoTag::HasStreamDetails() const +{ + return m_streamDetails.HasItems(); +} + +bool CVideoInfoTag::IsEmpty() const +{ + return (m_strTitle.empty() && + m_strFile.empty() && + m_strPath.empty()); +} + +void CVideoInfoTag::SetDuration(int duration) +{ + m_duration = duration; +} + +unsigned int CVideoInfoTag::GetDuration() const +{ + /* + Prefer the duration from the stream if it isn't too + small (60%) compared to the duration from the tag. + */ + unsigned int duration = m_streamDetails.GetVideoDuration(); + if (duration > m_duration * 0.6) + return duration; + + return m_duration; +} + +unsigned int CVideoInfoTag::GetStaticDuration() const +{ + return m_duration; +} + +unsigned int CVideoInfoTag::GetDurationFromMinuteString(const std::string &runtime) +{ + unsigned int duration = (unsigned int)str2uint64(runtime); + if (!duration) + { // failed for some reason, or zero + duration = strtoul(runtime.c_str(), NULL, 10); + CLog::Log(LOGWARNING, "{} <runtime> should be in minutes. Interpreting '{}' as {} minutes", + __FUNCTION__, runtime, duration); + } + return duration*60; +} + +void CVideoInfoTag::SetBasePath(std::string basePath) +{ + m_basePath = Trim(std::move(basePath)); +} + +void CVideoInfoTag::SetDirector(std::vector<std::string> director) +{ + m_director = Trim(std::move(director)); +} + +void CVideoInfoTag::SetWritingCredits(std::vector<std::string> writingCredits) +{ + m_writingCredits = Trim(std::move(writingCredits)); +} + +void CVideoInfoTag::SetGenre(std::vector<std::string> genre) +{ + m_genre = Trim(std::move(genre)); +} + +void CVideoInfoTag::SetCountry(std::vector<std::string> country) +{ + m_country = Trim(std::move(country)); +} + +void CVideoInfoTag::SetTagLine(std::string tagLine) +{ + m_strTagLine = Trim(std::move(tagLine)); +} + +void CVideoInfoTag::SetPlotOutline(std::string plotOutline) +{ + m_strPlotOutline = Trim(std::move(plotOutline)); +} + +void CVideoInfoTag::SetTrailer(std::string trailer) +{ + m_strTrailer = Trim(std::move(trailer)); +} + +void CVideoInfoTag::SetPlot(std::string plot) +{ + m_strPlot = Trim(std::move(plot)); +} + +void CVideoInfoTag::SetTitle(std::string title) +{ + m_strTitle = Trim(std::move(title)); +} + +std::string const &CVideoInfoTag::GetTitle() +{ + return m_strTitle; +} + +void CVideoInfoTag::SetSortTitle(std::string sortTitle) +{ + m_strSortTitle = Trim(std::move(sortTitle)); +} + +void CVideoInfoTag::SetPictureURL(CScraperUrl &pictureURL) +{ + m_strPictureURL = pictureURL; +} + +void CVideoInfoTag::SetRating(float rating, int votes, const std::string& type /* = "" */, bool def /* = false */) +{ + SetRating(CRating(rating, votes), type, def); +} + +void CVideoInfoTag::SetRating(CRating rating, const std::string& type /* = "" */, bool def /* = false */) +{ + if (rating.rating <= 0 || rating.rating > 10) + return; + + if (type.empty()) + m_ratings[m_strDefaultRating] = rating; + else + { + if (def || m_ratings.empty()) + m_strDefaultRating = type; + m_ratings[type] = rating; + } +} + +void CVideoInfoTag::SetRating(float rating, const std::string& type /* = "" */, bool def /* = false */) +{ + if (rating <= 0 || rating > 10) + return; + + if (type.empty()) + m_ratings[m_strDefaultRating].rating = rating; + else + { + if (def || m_ratings.empty()) + m_strDefaultRating = type; + m_ratings[type].rating = rating; + } +} + +void CVideoInfoTag::RemoveRating(const std::string& type) +{ + if (m_ratings.find(type) != m_ratings.end()) + { + m_ratings.erase(type); + if (m_strDefaultRating == type && !m_ratings.empty()) + m_strDefaultRating = m_ratings.begin()->first; + } +} + +void CVideoInfoTag::SetRatings(RatingMap ratings, const std::string& defaultRating /* = "" */) +{ + m_ratings = std::move(ratings); + + if (!defaultRating.empty() && m_ratings.find(defaultRating) != m_ratings.end()) + m_strDefaultRating = defaultRating; +} + +void CVideoInfoTag::SetVotes(int votes, const std::string& type /* = "" */) +{ + if (type.empty()) + m_ratings[m_strDefaultRating].votes = votes; + else + m_ratings[type].votes = votes; +} + +void CVideoInfoTag::SetPremiered(const CDateTime& premiered) +{ + m_premiered = premiered; + m_bHasPremiered = premiered.IsValid(); +} + +void CVideoInfoTag::SetPremieredFromDBDate(const std::string& premieredString) +{ + CDateTime premiered; + premiered.SetFromDBDate(premieredString); + SetPremiered(premiered); +} + +void CVideoInfoTag::SetYear(int year) +{ + if (year <= 0) + return; + + m_year = year; +} + +void CVideoInfoTag::SetArtist(std::vector<std::string> artist) +{ + m_artist = Trim(std::move(artist)); +} + +void CVideoInfoTag::SetUniqueIDs(std::map<std::string, std::string> uniqueIDs) +{ + for (const auto& uniqueid : uniqueIDs) + { + if (uniqueid.first.empty()) + uniqueIDs.erase(uniqueid.first); + } + if (uniqueIDs.find(m_strDefaultUniqueID) == uniqueIDs.end()) + { + const auto defaultUniqueId = GetUniqueID(); + if (!defaultUniqueId.empty()) + uniqueIDs[m_strDefaultUniqueID] = defaultUniqueId; + } + m_uniqueIDs = std::move(uniqueIDs); +} + +void CVideoInfoTag::SetSet(std::string set) +{ + m_set.title = Trim(std::move(set)); +} + +void CVideoInfoTag::SetSetOverview(std::string setOverview) +{ + m_set.overview = Trim(std::move(setOverview)); +} + +void CVideoInfoTag::SetTags(std::vector<std::string> tags) +{ + m_tags = Trim(std::move(tags)); +} + +void CVideoInfoTag::SetFile(std::string file) +{ + m_strFile = Trim(std::move(file)); +} + +void CVideoInfoTag::SetPath(std::string path) +{ + m_strPath = Trim(std::move(path)); +} + +void CVideoInfoTag::SetMPAARating(std::string mpaaRating) +{ + m_strMPAARating = Trim(std::move(mpaaRating)); +} + +void CVideoInfoTag::SetFileNameAndPath(std::string fileNameAndPath) +{ + m_strFileNameAndPath = Trim(std::move(fileNameAndPath)); +} + +void CVideoInfoTag::SetOriginalTitle(std::string originalTitle) +{ + m_strOriginalTitle = Trim(std::move(originalTitle)); +} + +void CVideoInfoTag::SetEpisodeGuide(std::string episodeGuide) +{ + if (StringUtils::StartsWith(episodeGuide, "<episodeguide")) + m_strEpisodeGuide = Trim(std::move(episodeGuide)); + else + m_strEpisodeGuide = + StringUtils::Format("<episodeguide>{}</episodeguide>", Trim(std::move(episodeGuide))); +} + +void CVideoInfoTag::SetStatus(std::string status) +{ + m_strStatus = Trim(std::move(status)); +} + +void CVideoInfoTag::SetProductionCode(std::string productionCode) +{ + m_strProductionCode = Trim(std::move(productionCode)); +} + +void CVideoInfoTag::SetShowTitle(std::string showTitle) +{ + m_strShowTitle = Trim(std::move(showTitle)); +} + +void CVideoInfoTag::SetStudio(std::vector<std::string> studio) +{ + m_studio = Trim(std::move(studio)); +} + +void CVideoInfoTag::SetAlbum(std::string album) +{ + m_strAlbum = Trim(std::move(album)); +} + +void CVideoInfoTag::SetShowLink(std::vector<std::string> showLink) +{ + m_showLink = Trim(std::move(showLink)); +} + +void CVideoInfoTag::SetUniqueID(const std::string& uniqueid, const std::string& type /* = "" */, bool isDefaultID /* = false */) +{ + if (uniqueid.empty()) + return; + + if (type.empty()) + m_uniqueIDs[m_strDefaultUniqueID] = uniqueid; + else + { + m_uniqueIDs[type] = uniqueid; + if (isDefaultID) + m_strDefaultUniqueID = type; + } +} + +void CVideoInfoTag::RemoveUniqueID(const std::string& type) +{ + if (m_uniqueIDs.find(type) != m_uniqueIDs.end()) + m_uniqueIDs.erase(type); +} + +void CVideoInfoTag::SetNamedSeasons(std::map<int, std::string> namedSeasons) +{ + m_namedSeasons = std::move(namedSeasons); +} + +void CVideoInfoTag::SetUserrating(int userrating) +{ + //This value needs to be between 0-10 - 0 will unset the userrating + userrating = std::max(userrating, 0); + userrating = std::min(userrating, 10); + + m_iUserRating = userrating; +} + +std::string CVideoInfoTag::Trim(std::string &&value) +{ + return StringUtils::Trim(value); +} + +std::vector<std::string> CVideoInfoTag::Trim(std::vector<std::string>&& items) +{ + std::for_each(items.begin(), items.end(), [](std::string &str){ + str = StringUtils::Trim(str); + }); + return std::move(items); +} + +int CVideoInfoTag::GetPlayCount() const +{ + return IsPlayCountSet() ? m_playCount : 0; +} + +bool CVideoInfoTag::SetPlayCount(int count) +{ + m_playCount = count; + return true; +} + +bool CVideoInfoTag::IncrementPlayCount() +{ + if (!IsPlayCountSet()) + m_playCount = 0; + + m_playCount++; + return true; +} + +void CVideoInfoTag::ResetPlayCount() +{ + m_playCount = PLAYCOUNT_NOT_SET; +} + +bool CVideoInfoTag::IsPlayCountSet() const +{ + return m_playCount != PLAYCOUNT_NOT_SET; +} + +CBookmark CVideoInfoTag::GetResumePoint() const +{ + return m_resumePoint; +} + +bool CVideoInfoTag::SetResumePoint(const CBookmark &resumePoint) +{ + m_resumePoint = resumePoint; + return true; +} + +bool CVideoInfoTag::SetResumePoint(double timeInSeconds, double totalTimeInSeconds, const std::string &playerState) +{ + CBookmark resumePoint; + resumePoint.timeInSeconds = timeInSeconds; + resumePoint.totalTimeInSeconds = totalTimeInSeconds; + resumePoint.playerState = playerState; + resumePoint.type = CBookmark::RESUME; + + m_resumePoint = resumePoint; + return true; +} diff --git a/xbmc/video/VideoInfoTag.h b/xbmc/video/VideoInfoTag.h new file mode 100644 index 0000000..ffbdb15 --- /dev/null +++ b/xbmc/video/VideoInfoTag.h @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "XBDateTime.h" +#include "utils/EmbeddedArt.h" +#include "utils/Fanart.h" +#include "utils/ISortable.h" +#include "utils/ScraperUrl.h" +#include "utils/StreamDetails.h" +#include "video/Bookmark.h" + +#include <string> +#include <vector> + +class CArchive; +class TiXmlNode; +class TiXmlElement; +class CVariant; + +struct SActorInfo +{ + bool operator<(const SActorInfo &right) const + { + return order < right.order; + } + std::string strName; + std::string strRole; + CScraperUrl thumbUrl; + std::string thumb; + int order = -1; +}; + +class CRating +{ +public: + CRating() = default; + explicit CRating(float r): rating(r) {} + CRating(float r, int v): rating(r), votes(v) {} + float rating = 0.0f; + int votes = 0; +}; +typedef std::map<std::string, CRating> RatingMap; + +class CVideoInfoTag : public IArchivable, public ISerializable, public ISortable +{ +public: + CVideoInfoTag() { Reset(); } + virtual ~CVideoInfoTag() = default; + void Reset(); + /* \brief Load information to a videoinfotag from an XML element + There are three types of tags supported: + 1. Single-value tags, such as <title>. These are set if available, else are left untouched. + 2. Additive tags, such as <set> or <genre>. These are appended to or replaced (if available) based on the value + of the prioritise parameter. In addition, a clear attribute is available in the XML to clear the current value prior + to appending. + 3. Image tags such as <thumb> and <fanart>. If the prioritise value is specified, any additional values are prepended + to the existing values. + + \param element the root XML element to parse. + \param append whether information should be added to the existing tag, or whether it should be reset first. + \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false. + + \sa ParseNative + */ + bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false); + bool Save(TiXmlNode *node, const std::string &tag, bool savePathInfo = true, const TiXmlElement *additionalNode = NULL); + void Merge(CVideoInfoTag& other); + void Archive(CArchive& ar) override; + void Serialize(CVariant& value) const override; + void ToSortable(SortItem& sortable, Field field) const override; + const CRating GetRating(std::string type = "") const; + const std::string& GetDefaultRating() const; + const std::string GetUniqueID(std::string type = "") const; + const std::map<std::string, std::string>& GetUniqueIDs() const; + const std::string& GetDefaultUniqueID() const; + bool HasUniqueID() const; + virtual bool HasYear() const; + virtual int GetYear() const; + bool HasPremiered() const; + const CDateTime& GetPremiered() const; + const CDateTime& GetFirstAired() const; + const std::string GetCast(bool bIncludeRole = false) const; + bool HasStreamDetails() const; + bool IsEmpty() const; + + const std::string& GetPath() const + { + if (m_strFileNameAndPath.empty()) + return m_strPath; + return m_strFileNameAndPath; + }; + + /*! \brief set the duration in seconds + \param duration the duration to set + */ + void SetDuration(int duration); + + /*! \brief retrieve the duration in seconds. + Prefers the duration from stream details if available. + */ + unsigned int GetDuration() const; + + /*! \brief retrieve the duration in seconds. + Ignores the duration from stream details even if available. + */ + unsigned int GetStaticDuration() const; + + /*! \brief get the duration in seconds from a minute string + \param runtime the runtime string from a scraper or similar + \return the time in seconds, if decipherable. + */ + static unsigned int GetDurationFromMinuteString(const std::string &runtime); + + void SetBasePath(std::string basePath); + void SetDirector(std::vector<std::string> director); + void SetWritingCredits(std::vector<std::string> writingCredits); + void SetGenre(std::vector<std::string> genre); + void SetCountry(std::vector<std::string> country); + void SetTagLine(std::string tagLine); + void SetPlotOutline(std::string plotOutline); + void SetTrailer(std::string trailer); + void SetPlot(std::string plot); + std::string const &GetTitle(); + void SetTitle(std::string title); + void SetSortTitle(std::string sortTitle); + void SetPictureURL(CScraperUrl &pictureURL); + void SetRating(float rating, int votes, const std::string& type = "", bool def = false); + void SetRating(CRating rating, const std::string& type = "", bool def = false); + void SetRating(float rating, const std::string& type = "", bool def = false); + void RemoveRating(const std::string& type); + void SetRatings(RatingMap ratings, const std::string& defaultRating = ""); + void SetVotes(int votes, const std::string& type = ""); + void SetUniqueIDs(std::map<std::string, std::string> uniqueIDs); + void SetPremiered(const CDateTime& premiered); + void SetPremieredFromDBDate(const std::string& premieredString); + virtual void SetYear(int year); + void SetArtist(std::vector<std::string> artist); + void SetSet(std::string set); + void SetSetOverview(std::string setOverview); + void SetTags(std::vector<std::string> tags); + void SetFile(std::string file); + void SetPath(std::string path); + void SetMPAARating(std::string mpaaRating); + void SetFileNameAndPath(std::string fileNameAndPath); + void SetOriginalTitle(std::string originalTitle); + void SetEpisodeGuide(std::string episodeGuide); + void SetStatus(std::string status); + void SetProductionCode(std::string productionCode); + void SetShowTitle(std::string showTitle); + void SetStudio(std::vector<std::string> studio); + void SetAlbum(std::string album); + void SetShowLink(std::vector<std::string> showLink); + void SetUniqueID(const std::string& uniqueid, const std::string& type = "", bool def = false); + void RemoveUniqueID(const std::string& type); + void SetNamedSeasons(std::map<int, std::string> namedSeasons); + void SetUserrating(int userrating); + + /*! + * @brief Get this videos's play count. + * @return the play count. + */ + virtual int GetPlayCount() const; + + /*! + * @brief Set this videos's play count. + * @param count play count. + * @return True if play count was set successfully, false otherwise. + */ + virtual bool SetPlayCount(int count); + + /*! + * @brief Increment this videos's play count. + * @return True if play count was increased successfully, false otherwise. + */ + virtual bool IncrementPlayCount(); + + /*! + * @brief Reset playcount + */ + virtual void ResetPlayCount(); + + /*! + * @brief Check if the playcount is set + * @return True if play count value is set + */ + virtual bool IsPlayCountSet() const; + + /*! + * @brief Get this videos's resume point. + * @return the resume point. + */ + virtual CBookmark GetResumePoint() const; + + /*! + * @brief Set this videos's resume point. + * @param resumePoint resume point. + * @return True if resume point was set successfully, false otherwise. + */ + virtual bool SetResumePoint(const CBookmark &resumePoint); + + /*! + * @brief Set this videos's resume point. + * @param timeInSeconds the time of the resume point + * @param totalTimeInSeconds the total time of the video + * @param playerState the player state + * @return True if resume point was set successfully, false otherwise. + */ + virtual bool SetResumePoint(double timeInSeconds, double totalTimeInSeconds, const std::string &playerState); + + std::string m_basePath; // the base path of the video, for folder-based lookups + int m_parentPathID; // the parent path id where the base path of the video lies + std::vector<std::string> m_director; + std::vector<std::string> m_writingCredits; + std::vector<std::string> m_genre; + std::vector<std::string> m_country; + std::string m_strTagLine; + std::string m_strPlotOutline; + std::string m_strTrailer; + std::string m_strPlot; + CScraperUrl m_strPictureURL; + std::string m_strTitle; + std::string m_strSortTitle; + std::vector<std::string> m_artist; + std::vector< SActorInfo > m_cast; + typedef std::vector< SActorInfo >::const_iterator iCast; + struct SetInfo //!< Struct holding information about a movie set + { + std::string title; //!< Title of the movie set + int id; //!< ID of movie set in database + std::string overview; //!< Overview/description of the movie set + }; + SetInfo m_set; //!< Assigned movie set + std::vector<std::string> m_tags; + std::string m_strFile; + std::string m_strPath; + std::string m_strMPAARating; + std::string m_strFileNameAndPath; + std::string m_strOriginalTitle; + std::string m_strEpisodeGuide; + CDateTime m_premiered; + bool m_bHasPremiered; + std::string m_strStatus; + std::string m_strProductionCode; + CDateTime m_firstAired; + std::string m_strShowTitle; + std::vector<std::string> m_studio; + std::string m_strAlbum; + CDateTime m_lastPlayed; + std::vector<std::string> m_showLink; + std::map<int, std::string> m_namedSeasons; + int m_iTop250; + int m_year; + int m_iSeason; + int m_iEpisode; + int m_iIdUniqueID; + int m_iDbId; + int m_iFileId; + int m_iSpecialSortSeason; + int m_iSpecialSortEpisode; + int m_iTrack; + RatingMap m_ratings; + int m_iIdRating; + int m_iUserRating; + CBookmark m_EpBookmark; + int m_iBookmarkId; + int m_iIdShow; + int m_iIdSeason; + CFanart m_fanart; + CStreamDetails m_streamDetails; + CDateTime m_dateAdded; + MediaType m_type; + int m_relevance; // Used for actors' number of appearances + int m_parsedDetails; + std::vector<EmbeddedArtInfo> m_coverArt; ///< art information + + // TODO: cannot be private, because of 'struct SDbTableOffsets' + unsigned int m_duration; ///< duration in seconds + +private: + /* \brief Parse our native XML format for video info. + See Load for a description of the available tag types. + + \param element the root XML element to parse. + \param prioritise whether additive tags should be replaced (or prepended) by the content of the tags, or appended to. + \sa Load + */ + void ParseNative(const TiXmlElement* element, bool prioritise); + + std::string m_strDefaultRating; + std::string m_strDefaultUniqueID; + std::map<std::string, std::string> m_uniqueIDs; + std::string Trim(std::string &&value); + std::vector<std::string> Trim(std::vector<std::string> &&items); + + int m_playCount; + CBookmark m_resumePoint; + static const int PLAYCOUNT_NOT_SET = -1; +}; + +typedef std::vector<CVideoInfoTag> VECMOVIES; diff --git a/xbmc/video/VideoLibraryQueue.cpp b/xbmc/video/VideoLibraryQueue.cpp new file mode 100644 index 0000000..65e65bb --- /dev/null +++ b/xbmc/video/VideoLibraryQueue.cpp @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2014-2018 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 "VideoLibraryQueue.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "video/jobs/VideoLibraryCleaningJob.h" +#include "video/jobs/VideoLibraryJob.h" +#include "video/jobs/VideoLibraryMarkWatchedJob.h" +#include "video/jobs/VideoLibraryRefreshingJob.h" +#include "video/jobs/VideoLibraryResetResumePointJob.h" +#include "video/jobs/VideoLibraryScanningJob.h" + +#include <mutex> +#include <utility> + +CVideoLibraryQueue::CVideoLibraryQueue() + : CJobQueue(false, 1, CJob::PRIORITY_LOW), + m_jobs() +{ } + +CVideoLibraryQueue::~CVideoLibraryQueue() +{ + std::unique_lock<CCriticalSection> lock(m_critical); + m_jobs.clear(); +} + +CVideoLibraryQueue& CVideoLibraryQueue::GetInstance() +{ + static CVideoLibraryQueue s_instance; + return s_instance; +} + +void CVideoLibraryQueue::ScanLibrary(const std::string& directory, bool scanAll /* = false */ , bool showProgress /* = true */) +{ + AddJob(new CVideoLibraryScanningJob(directory, scanAll, showProgress)); +} + +bool CVideoLibraryQueue::IsScanningLibrary() const +{ + // check if the library is being cleaned synchronously + if (m_cleaning) + return true; + + // check if the library is being scanned asynchronously + VideoLibraryJobMap::const_iterator scanningJobs = m_jobs.find("VideoLibraryScanningJob"); + if (scanningJobs != m_jobs.end() && !scanningJobs->second.empty()) + return true; + + // check if the library is being cleaned asynchronously + VideoLibraryJobMap::const_iterator cleaningJobs = m_jobs.find("VideoLibraryCleaningJob"); + if (cleaningJobs != m_jobs.end() && !cleaningJobs->second.empty()) + return true; + + return false; +} + +void CVideoLibraryQueue::StopLibraryScanning() +{ + std::unique_lock<CCriticalSection> lock(m_critical); + VideoLibraryJobMap::const_iterator scanningJobs = m_jobs.find("VideoLibraryScanningJob"); + if (scanningJobs == m_jobs.end()) + return; + + // get a copy of the scanning jobs because CancelJob() will modify m_scanningJobs + VideoLibraryJobs tmpScanningJobs(scanningJobs->second.begin(), scanningJobs->second.end()); + + // cancel all scanning jobs + for (VideoLibraryJobs::const_iterator job = tmpScanningJobs.begin(); job != tmpScanningJobs.end(); ++job) + CancelJob(*job); + Refresh(); +} + +bool CVideoLibraryQueue::CleanLibrary(const std::set<int>& paths /* = std::set<int>() */, + bool asynchronous /* = true */, + CGUIDialogProgressBarHandle* progressBar /* = NULL */) +{ + CVideoLibraryCleaningJob* cleaningJob = new CVideoLibraryCleaningJob(paths, progressBar); + + if (asynchronous) + AddJob(cleaningJob); + else + { + // we can't perform a modal library cleaning if other jobs are running + if (IsRunning()) + return false; + + m_modal = true; + m_cleaning = true; + cleaningJob->DoWork(); + + delete cleaningJob; + m_cleaning = false; + m_modal = false; + Refresh(); + } + + return true; +} + +bool CVideoLibraryQueue::CleanLibraryModal(const std::set<int>& paths /* = std::set<int>() */) +{ + // we can't perform a modal library cleaning if other jobs are running + if (IsRunning()) + return false; + + m_modal = true; + m_cleaning = true; + CVideoLibraryCleaningJob cleaningJob(paths, true); + cleaningJob.DoWork(); + m_cleaning = false; + m_modal = false; + Refresh(); + + return true; +} + +void CVideoLibraryQueue::RefreshItem(std::shared_ptr<CFileItem> item, + bool ignoreNfo /* = false */, + bool forceRefresh /* = true */, + bool refreshAll /* = false */, + const std::string& searchTitle /* = "" */) +{ + AddJob(new CVideoLibraryRefreshingJob(std::move(item), forceRefresh, refreshAll, ignoreNfo, + searchTitle)); +} + +bool CVideoLibraryQueue::RefreshItemModal(std::shared_ptr<CFileItem> item, + bool forceRefresh /* = true */, + bool refreshAll /* = false */) +{ + // we can't perform a modal item refresh if other jobs are running + if (IsRunning()) + return false; + + m_modal = true; + CVideoLibraryRefreshingJob refreshingJob(std::move(item), forceRefresh, refreshAll); + + bool result = refreshingJob.DoModal(); + m_modal = false; + + return result; +} + +void CVideoLibraryQueue::MarkAsWatched(const std::shared_ptr<CFileItem>& item, bool watched) +{ + if (item == NULL) + return; + + AddJob(new CVideoLibraryMarkWatchedJob(item, watched)); +} + +void CVideoLibraryQueue::ResetResumePoint(const std::shared_ptr<CFileItem>& item) +{ + if (item == nullptr) + return; + + AddJob(new CVideoLibraryResetResumePointJob(item)); +} + +void CVideoLibraryQueue::AddJob(CVideoLibraryJob *job) +{ + if (job == NULL) + return; + + std::unique_lock<CCriticalSection> lock(m_critical); + if (!CJobQueue::AddJob(job)) + return; + + // add the job to our list of queued/running jobs + std::string jobType = job->GetType(); + VideoLibraryJobMap::iterator jobsIt = m_jobs.find(jobType); + if (jobsIt == m_jobs.end()) + { + VideoLibraryJobs jobs; + jobs.insert(job); + m_jobs.insert(std::make_pair(jobType, jobs)); + } + else + jobsIt->second.insert(job); +} + +void CVideoLibraryQueue::CancelJob(CVideoLibraryJob *job) +{ + if (job == NULL) + return; + + std::unique_lock<CCriticalSection> lock(m_critical); + // remember the job type needed later because the job might be deleted + // in the call to CJobQueue::CancelJob() + std::string jobType; + if (job->GetType() != NULL) + jobType = job->GetType(); + + // check if the job supports cancellation and cancel it + if (job->CanBeCancelled()) + job->Cancel(); + + // remove the job from the job queue + CJobQueue::CancelJob(job); + + // remove the job from our list of queued/running jobs + VideoLibraryJobMap::iterator jobsIt = m_jobs.find(jobType); + if (jobsIt != m_jobs.end()) + jobsIt->second.erase(job); +} + +void CVideoLibraryQueue::CancelAllJobs() +{ + std::unique_lock<CCriticalSection> lock(m_critical); + CJobQueue::CancelJobs(); + + // remove all scanning jobs + m_jobs.clear(); +} + +bool CVideoLibraryQueue::IsRunning() const +{ + return CJobQueue::IsProcessing() || m_modal; +} + +void CVideoLibraryQueue::Refresh() +{ + CUtil::DeleteVideoDatabaseDirectoryCache(); + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +void CVideoLibraryQueue::OnJobComplete(unsigned int jobID, bool success, CJob *job) +{ + if (success) + { + if (QueueEmpty()) + Refresh(); + } + + { + std::unique_lock<CCriticalSection> lock(m_critical); + // remove the job from our list of queued/running jobs + VideoLibraryJobMap::iterator jobsIt = m_jobs.find(job->GetType()); + if (jobsIt != m_jobs.end()) + jobsIt->second.erase(static_cast<CVideoLibraryJob*>(job)); + } + + return CJobQueue::OnJobComplete(jobID, success, job); +} diff --git a/xbmc/video/VideoLibraryQueue.h b/xbmc/video/VideoLibraryQueue.h new file mode 100644 index 0000000..875dc5d --- /dev/null +++ b/xbmc/video/VideoLibraryQueue.h @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2014-2018 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. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "utils/JobManager.h" + +#include <map> +#include <memory> +#include <set> + +class CFileItem; +class CGUIDialogProgressBarHandle; +class CVideoLibraryJob; + +/*! + \brief Queue for video library jobs. + + The queue can only process a single job at any time and every job will be + executed at the lowest priority. + */ +class CVideoLibraryQueue : protected CJobQueue +{ +public: + ~CVideoLibraryQueue() override; + + /*! + \brief Gets the singleton instance of the video library queue. + */ + static CVideoLibraryQueue& GetInstance(); + + /*! + \brief Enqueue a library scan job. + + \param[in] directory Directory to scan + \param[in] scanAll Ignore exclude setting for items. Defaults to false + \param[in] showProgress Whether or not to show a progress dialog. Defaults to true + */ + void ScanLibrary(const std::string& directory, bool scanAll = false, bool showProgress = true); + + /*! + \brief Check if a library scan is in progress. + + \return True if a scan is in progress, false otherwise + */ + bool IsScanningLibrary() const; + + /*! + \brief Stop and dequeue all scanning jobs. + */ + void StopLibraryScanning(); + + /*! + \brief Enqueue a library cleaning job. + + \param[in] paths Set with database IDs of paths to be cleaned + \param[in] asynchronous Run the clean job asynchronously. Defaults to true + \param[in] progressBar Progress bar to update in GUI. Defaults to NULL (no progress bar to update) + \return True if the video library cleaning job has started, false otherwise + */ + bool CleanLibrary(const std::set<int>& paths = std::set<int>(), + bool asynchronous = true, + CGUIDialogProgressBarHandle* progressBar = NULL); + + /*! + \brief Executes a library cleaning with a modal dialog. + + \param[in] paths Set with database IDs of paths to be cleaned + \return True if the video library cleaning job has started, false otherwise + */ + bool CleanLibraryModal(const std::set<int>& paths = std::set<int>()); + + /*! + \brief Enqueues a job to refresh the details of the given item. + + \param[inout] item Video item to be refreshed + \param[in] ignoreNfo Whether or not to ignore local NFO files + \param[in] forceRefresh Whether to force a complete refresh (including NFO or internet lookup) + \param[in] refreshAll Whether to refresh all sub-items (in case of a tvshow) + \param[in] searchTitle Title to use for the search (instead of determining it from the item's filename/path) + */ + void RefreshItem(std::shared_ptr<CFileItem> item, + bool ignoreNfo = false, + bool forceRefresh = true, + bool refreshAll = false, + const std::string& searchTitle = ""); + + /*! + \brief Refreshes the details of the given item with a modal dialog. + + \param[inout] item Video item to be refreshed + \param[in] forceRefresh Whether to force a complete refresh (including NFO or internet lookup) + \param[in] refreshAll Whether to refresh all sub-items (in case of a tvshow) + \return True if the item has been successfully refreshed, false otherwise. + */ + bool RefreshItemModal(std::shared_ptr<CFileItem> item, + bool forceRefresh = true, + bool refreshAll = false); + + /*! + \brief Queue a watched status update job. + + \param[in] item Item to update watched status for + \param[in] watched New watched status + */ + void MarkAsWatched(const std::shared_ptr<CFileItem>& item, bool watched); + + /*! + \brief Queue a reset resume point job. + + \param[in] item Item to reset the resume point for + */ + void ResetResumePoint(const std::shared_ptr<CFileItem>& item); + + /*! + \brief Adds the given job to the queue. + + \param[in] job Video library job to be queued. + */ + void AddJob(CVideoLibraryJob *job); + + /*! + \brief Cancels the given job and removes it from the queue. + + \param[in] job Video library job to be canceled and removed from the queue. + */ + void CancelJob(CVideoLibraryJob *job); + + /*! + \brief Cancels all running and queued jobs. + */ + void CancelAllJobs(); + + /*! + \brief Whether any jobs are running or not. + */ + bool IsRunning() const; + +protected: + // implementation of IJobCallback + void OnJobComplete(unsigned int jobID, bool success, CJob *job) override; + + /*! + \brief Notifies all to refresh the current listings. + */ + void Refresh(); + +private: + CVideoLibraryQueue(); + CVideoLibraryQueue(const CVideoLibraryQueue&) = delete; + CVideoLibraryQueue const& operator=(CVideoLibraryQueue const&) = delete; + + typedef std::set<CVideoLibraryJob*> VideoLibraryJobs; + typedef std::map<std::string, VideoLibraryJobs> VideoLibraryJobMap; + VideoLibraryJobMap m_jobs; + CCriticalSection m_critical; + + bool m_modal = false; + bool m_cleaning = false; +}; diff --git a/xbmc/video/VideoThumbLoader.cpp b/xbmc/video/VideoThumbLoader.cpp new file mode 100644 index 0000000..31ebe3e --- /dev/null +++ b/xbmc/video/VideoThumbLoader.cpp @@ -0,0 +1,803 @@ +/* + * Copyright (C) 2005-2018 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 "VideoThumbLoader.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "URL.h" +#include "cores/VideoPlayer/DVDFileInfo.h" +#include "cores/VideoSettings.h" +#include "filesystem/Directory.h" +#include "filesystem/DirectoryCache.h" +#include "filesystem/StackDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/StereoscopicsManager.h" +#include "music/MusicDatabase.h" +#include "music/tags/MusicInfoTag.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingUtils.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/EmbeddedArt.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" +#include "video/VideoInfoTag.h" +#include "video/tags/VideoInfoTagLoaderFactory.h" + +#include <algorithm> +#include <cstdlib> +#include <utility> + +using namespace XFILE; +using namespace VIDEO; + +CThumbExtractor::CThumbExtractor(const CFileItem& item, + const std::string& listpath, + bool thumb, + const std::string& target, + int64_t pos, + bool fillStreamDetails) + : m_target(target), m_listpath(listpath), m_item(item) +{ + m_thumb = thumb; + m_pos = pos; + m_fillStreamDetails = fillStreamDetails; + + if (item.IsVideoDb() && item.HasVideoInfoTag()) + m_item.SetPath(item.GetVideoInfoTag()->m_strFileNameAndPath); + + if (m_item.IsStack()) + m_item.SetPath(CStackDirectory::GetFirstStackedFile(m_item.GetPath())); +} + +CThumbExtractor::~CThumbExtractor() = default; + +bool CThumbExtractor::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(),GetType()) == 0) + { + const CThumbExtractor* jobExtract = dynamic_cast<const CThumbExtractor*>(job); + if (jobExtract && jobExtract->m_listpath == m_listpath + && jobExtract->m_target == m_target) + return true; + } + return false; +} + +bool CThumbExtractor::DoWork() +{ + if (m_item.IsLiveTV() + // Due to a pvr addon api design flaw (no support for multiple concurrent streams + // per addon instance), pvr recording thumbnail extraction does not work (reliably). + || URIUtils::IsPVRRecording(m_item.GetDynPath()) + || URIUtils::IsUPnP(m_item.GetPath()) + || URIUtils::IsBluray(m_item.GetPath()) + || URIUtils::IsPlugin(m_item.GetDynPath()) // plugin path not fully resolved + || m_item.IsBDFile() + || m_item.IsDVD() + || m_item.IsDiscImage() + || m_item.IsDVDFile(false, true) + || m_item.IsInternetStream() + || m_item.IsDiscStub() + || m_item.IsPlayList()) + return false; + + // For HTTP/FTP we only allow extraction when on a LAN + if (URIUtils::IsRemote(m_item.GetPath()) && + !URIUtils::IsOnLAN(m_item.GetPath()) && + (URIUtils::IsFTP(m_item.GetPath()) || + URIUtils::IsHTTP(m_item.GetPath()))) + return false; + + bool result=false; + if (m_thumb) + { + CLog::Log(LOGDEBUG, "{} - trying to extract thumb from video file {}", __FUNCTION__, + CURL::GetRedacted(m_item.GetPath())); + // construct the thumb cache file + CTextureDetails details; + details.file = CTextureCache::GetCacheFile(m_target) + ".jpg"; + result = CDVDFileInfo::ExtractThumb(m_item, details, m_fillStreamDetails ? &m_item.GetVideoInfoTag()->m_streamDetails : nullptr, m_pos); + if (result) + { + CServiceBroker::GetTextureCache()->AddCachedTexture(m_target, details); + m_item.SetProperty("HasAutoThumb", true); + m_item.SetProperty("AutoThumbImage", m_target); + m_item.SetArt("thumb", m_target); + + CVideoInfoTag* info = m_item.GetVideoInfoTag(); + if (info->m_iDbId > 0 && !info->m_type.empty()) + { + CVideoDatabase db; + if (db.Open()) + { + db.SetArtForItem(info->m_iDbId, info->m_type, "thumb", m_item.GetArt("thumb")); + db.Close(); + } + } + } + } + else if (!m_item.IsPlugin() && + (!m_item.HasVideoInfoTag() || + !m_item.GetVideoInfoTag()->HasStreamDetails())) + { + // No tag or no details set, so extract them + CLog::Log(LOGDEBUG, "{} - trying to extract filestream details from video file {}", + __FUNCTION__, CURL::GetRedacted(m_item.GetPath())); + result = CDVDFileInfo::GetFileStreamDetails(&m_item); + } + + if (result) + { + CVideoInfoTag* info = m_item.GetVideoInfoTag(); + CVideoDatabase db; + if (db.Open()) + { + if (URIUtils::IsStack(m_listpath)) + { + // Don't know the total time of the stack, so set duration to zero to avoid confusion + info->m_streamDetails.SetVideoDuration(0, 0); + + // Restore original stack path + m_item.SetPath(m_listpath); + } + + db.BeginTransaction(); + + if (info->m_iFileId < 0) + db.SetStreamDetailsForFile(info->m_streamDetails, !info->m_strFileNameAndPath.empty() ? info->m_strFileNameAndPath : m_item.GetPath()); + else + db.SetStreamDetailsForFileId(info->m_streamDetails, info->m_iFileId); + + // overwrite the runtime value if the one from streamdetails is available + if (info->m_iDbId > 0 + && info->GetStaticDuration() != info->GetDuration()) + { + info->SetDuration(info->GetDuration()); + + // store the updated information in the database + db.SetDetailsForItem(info->m_iDbId, info->m_type, *info, m_item.GetArt()); + } + + db.CommitTransaction(); + db.Close(); + } + return true; + } + + return false; +} + +CVideoThumbLoader::CVideoThumbLoader() : + CThumbLoader(), CJobQueue(true, 1, CJob::PRIORITY_LOW_PAUSABLE) +{ + m_videoDatabase = new CVideoDatabase(); +} + +CVideoThumbLoader::~CVideoThumbLoader() +{ + StopThread(); + delete m_videoDatabase; +} + +void CVideoThumbLoader::OnLoaderStart() +{ + m_videoDatabase->Open(); + m_artCache.clear(); + CThumbLoader::OnLoaderStart(); +} + +void CVideoThumbLoader::OnLoaderFinish() +{ + m_videoDatabase->Close(); + m_artCache.clear(); + CThumbLoader::OnLoaderFinish(); +} + +static void SetupRarOptions(CFileItem& item, const std::string& path) +{ + std::string path2(path); + if (item.IsVideoDb() && item.HasVideoInfoTag()) + path2 = item.GetVideoInfoTag()->m_strFileNameAndPath; + CURL url(path2); + std::string opts = url.GetOptions(); + if (opts.find("flags") != std::string::npos) + return; + if (opts.size()) + opts += "&flags=8"; + else + opts = "?flags=8"; + url.SetOptions(opts); + if (item.IsVideoDb() && item.HasVideoInfoTag()) + item.GetVideoInfoTag()->m_strFileNameAndPath = url.Get(); + else + item.SetPath(url.Get()); + g_directoryCache.ClearDirectory(url.GetWithoutFilename()); +} + +namespace +{ +std::vector<std::string> GetSettingListAsString(const std::string& settingID) +{ + std::vector<CVariant> values = + CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(settingID); + std::vector<std::string> result; + std::transform(values.begin(), values.end(), std::back_inserter(result), + [](const CVariant& s) { return s.asString(); }); + return result; +} + +const std::map<std::string, std::vector<std::string>> artTypeDefaults = { + {MediaTypeEpisode, {"thumb"}}, + {MediaTypeTvShow, {"poster", "fanart", "banner"}}, + {MediaTypeSeason, {"poster", "fanart", "banner"}}, + {MediaTypeMovie, {"poster", "fanart"}}, + {MediaTypeVideoCollection, {"poster", "fanart"}}, + {MediaTypeMusicVideo, {"poster", "fanart"}}, + {MediaTypeNone, { "poster", "fanart", "banner", "thumb" }}, +}; + +const std::vector<std::string> artTypeDefaultsFallback = {}; + +const std::vector<std::string>& GetArtTypeDefault(const std::string& mediaType) +{ + auto defaults = artTypeDefaults.find(mediaType); + if (defaults != artTypeDefaults.end()) + return defaults->second; + return artTypeDefaultsFallback; +} + +const std::map<std::string, std::string> artTypeSettings = { + {MediaTypeEpisode, CSettings::SETTING_VIDEOLIBRARY_EPISODEART_WHITELIST}, + {MediaTypeTvShow, CSettings::SETTING_VIDEOLIBRARY_TVSHOWART_WHITELIST}, + {MediaTypeSeason, CSettings::SETTING_VIDEOLIBRARY_TVSHOWART_WHITELIST}, + {MediaTypeMovie, CSettings::SETTING_VIDEOLIBRARY_MOVIEART_WHITELIST}, + {MediaTypeVideoCollection, CSettings::SETTING_VIDEOLIBRARY_MOVIEART_WHITELIST}, + {MediaTypeMusicVideo, CSettings::SETTING_VIDEOLIBRARY_MUSICVIDEOART_WHITELIST}, +}; +} // namespace + +std::vector<std::string> CVideoThumbLoader::GetArtTypes(const std::string &type) +{ + int artworkLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL); + if (artworkLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE) + { + return {}; + } + + std::vector<std::string> result = GetArtTypeDefault(type); + if (artworkLevel != CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_CUSTOM) + { + return result; + } + + auto settings = artTypeSettings.find(type); + if (settings == artTypeSettings.end()) + return result; + + for (auto& artType : GetSettingListAsString(settings->second)) + { + if (find(result.begin(), result.end(), artType) == result.end()) + result.push_back(artType); + } + + return result; +} + +bool CVideoThumbLoader::IsValidArtType(const std::string& potentialArtType) +{ + return !potentialArtType.empty() && potentialArtType.length() <= 25 && + std::find_if_not( + potentialArtType.begin(), potentialArtType.end(), + StringUtils::isasciialphanum + ) == potentialArtType.end(); +} + +bool CVideoThumbLoader::IsArtTypeInWhitelist(const std::string& artType, const std::vector<std::string>& whitelist, bool exact) +{ + // whitelist contains art "families", 'fanart' also matches 'fanart1', 'fanart2', and so on + std::string compareArtType = artType; + if (!exact) + StringUtils::TrimRight(compareArtType, "0123456789"); + + return std::find(whitelist.begin(), whitelist.end(), compareArtType) != whitelist.end(); +} + +/** + * Look for a thumbnail for pItem. If one does not exist, look for an autogenerated + * thumbnail. If that does not exist, attempt to autogenerate one. Finally, check + * for the existence of fanart and set properties accordingly. + * @return: true if pItem has been modified + */ +bool CVideoThumbLoader::LoadItem(CFileItem* pItem) +{ + bool result = LoadItemCached(pItem); + result |= LoadItemLookup(pItem); + + return result; +} + +bool CVideoThumbLoader::LoadItemCached(CFileItem* pItem) +{ + if (pItem->m_bIsShareOrDrive + || pItem->IsParentFolder()) + return false; + + m_videoDatabase->Open(); + + if (!pItem->HasVideoInfoTag() || !pItem->GetVideoInfoTag()->HasStreamDetails()) // no stream details + { + if ((pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_iFileId >= 0) // file (or maybe folder) is in the database + || (!pItem->m_bIsFolder && pItem->IsVideo())) // Some other video file for which we haven't yet got any database details + { + if (m_videoDatabase->GetStreamDetails(*pItem)) + pItem->SetInvalid(); + } + } + + // video db items normally have info in the database + if (pItem->HasVideoInfoTag() && !pItem->GetProperty("libraryartfilled").asBoolean()) + { + FillLibraryArt(*pItem); + + if (!pItem->GetVideoInfoTag()->m_type.empty() && + pItem->GetVideoInfoTag()->m_type != MediaTypeMovie && + pItem->GetVideoInfoTag()->m_type != MediaTypeTvShow && + pItem->GetVideoInfoTag()->m_type != MediaTypeEpisode && + pItem->GetVideoInfoTag()->m_type != MediaTypeMusicVideo) + { + m_videoDatabase->Close(); + return true; // nothing else to be done + } + } + + // if we have no art, look for it all + std::map<std::string, std::string> artwork = pItem->GetArt(); + if (artwork.empty()) + { + std::vector<std::string> artTypes = GetArtTypes(pItem->HasVideoInfoTag() ? pItem->GetVideoInfoTag()->m_type : ""); + if (find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end()) + artTypes.emplace_back("thumb"); // always look for "thumb" art for files + for (std::vector<std::string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i) + { + std::string type = *i; + std::string art = GetCachedImage(*pItem, type); + if (!art.empty()) + artwork.insert(std::make_pair(type, art)); + } + pItem->AppendArt(artwork); + } + + m_videoDatabase->Close(); + + return true; +} + +bool CVideoThumbLoader::LoadItemLookup(CFileItem* pItem) +{ + if (pItem->m_bIsShareOrDrive || pItem->IsParentFolder() || pItem->GetPath() == "add") + return false; + + if (pItem->HasVideoInfoTag() && + !pItem->GetVideoInfoTag()->m_type.empty() && + pItem->GetVideoInfoTag()->m_type != MediaTypeMovie && + pItem->GetVideoInfoTag()->m_type != MediaTypeTvShow && + pItem->GetVideoInfoTag()->m_type != MediaTypeEpisode && + pItem->GetVideoInfoTag()->m_type != MediaTypeMusicVideo) + return false; // Nothing to do here + + DetectAndAddMissingItemData(*pItem); + + m_videoDatabase->Open(); + + std::map<std::string, std::string> artwork = pItem->GetArt(); + std::vector<std::string> artTypes = GetArtTypes(pItem->HasVideoInfoTag() ? pItem->GetVideoInfoTag()->m_type : ""); + if (find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end()) + artTypes.emplace_back("thumb"); // always look for "thumb" art for files + for (std::vector<std::string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i) + { + std::string type = *i; + if (!pItem->HasArt(type)) + { + std::string art = GetLocalArt(*pItem, type, type=="fanart"); + if (!art.empty()) // cache it + { + SetCachedImage(*pItem, type, art); + CServiceBroker::GetTextureCache()->BackgroundCacheImage(art); + artwork.insert(std::make_pair(type, art)); + } + else + { + // If nothing was found, try embedded art + if (pItem->HasVideoInfoTag() && !pItem->GetVideoInfoTag()->m_coverArt.empty()) + { + for (auto& it : pItem->GetVideoInfoTag()->m_coverArt) + { + if (it.m_type == type) + { + art = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), "video_" + type); + artwork.insert(std::make_pair(type, art)); + } + } + } + } + } + } + pItem->AppendArt(artwork); + + // We can only extract flags/thumbs for file-like items + if (!pItem->m_bIsFolder && pItem->IsVideo()) + { + // An auto-generated thumb may have been cached on a different device - check we have it here + std::string url = pItem->GetArt("thumb"); + if (StringUtils::StartsWith(url, "image://video@") && + !CServiceBroker::GetTextureCache()->HasCachedImage(url)) + pItem->SetArt("thumb", ""); + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (!pItem->HasArt("thumb")) + { + // create unique thumb for auto generated thumbs + std::string thumbURL = GetEmbeddedThumbURL(*pItem); + if (CServiceBroker::GetTextureCache()->HasCachedImage(thumbURL)) + { + CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumbURL); + pItem->SetProperty("HasAutoThumb", true); + pItem->SetProperty("AutoThumbImage", thumbURL); + pItem->SetArt("thumb", thumbURL); + + if (pItem->HasVideoInfoTag()) + { + // Item has cached autogen image but no art entry. Save it to db. + CVideoInfoTag* info = pItem->GetVideoInfoTag(); + if (info->m_iDbId > 0 && !info->m_type.empty()) + m_videoDatabase->SetArtForItem(info->m_iDbId, info->m_type, "thumb", thumbURL); + } + } + else if (settings->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB) && + settings->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS) && + settings->GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL) != + CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE) + { + CFileItem item(*pItem); + std::string path(item.GetPath()); + if (URIUtils::IsInRAR(item.GetPath())) + SetupRarOptions(item,path); + + CThumbExtractor* extract = new CThumbExtractor(item, path, true, thumbURL); + AddJob(extract); + + m_videoDatabase->Close(); + return true; + } + } + + // flag extraction + if (settings->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS) && + (!pItem->HasVideoInfoTag() || + !pItem->GetVideoInfoTag()->HasStreamDetails() ) ) + { + CFileItem item(*pItem); + std::string path(item.GetPath()); + if (URIUtils::IsInRAR(item.GetPath())) + SetupRarOptions(item,path); + CThumbExtractor* extract = new CThumbExtractor(item,path,false); + AddJob(extract); + } + } + + m_videoDatabase->Close(); + return true; +} + +bool CVideoThumbLoader::FillLibraryArt(CFileItem &item) +{ + CVideoInfoTag &tag = *item.GetVideoInfoTag(); + std::map<std::string, std::string> artwork; + // Video item can be an album - either a + // a) search result with full details including music library album id, or + // b) musicvideo album that needs matching to a music album, storing id as well as fetch art. + if (tag.m_type == MediaTypeAlbum) + { + int idAlbum = -1; + if (item.HasMusicInfoTag()) // Album is a search result + idAlbum = item.GetMusicInfoTag()->GetAlbumId(); + CMusicDatabase database; + database.Open(); + if (idAlbum < 0 && !tag.m_strAlbum.empty() && + item.GetProperty("musicvideomediatype") == MediaTypeAlbum) + { + // Musicvideo album - try to match album in music db on artist(s) and album name. + // Get review if available and save the matching music library album id. + std::string strArtist = StringUtils::Join( + tag.m_artist, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + std::string strReview; + if (database.GetMatchingMusicVideoAlbum( + tag.m_strAlbum, strArtist, idAlbum, strReview)) + { + item.SetProperty("album_musicid", idAlbum); + item.SetProperty("album_description", strReview); + } + } + // Get album art only (not related artist art) + if (database.GetArtForItem(idAlbum, MediaTypeAlbum, artwork)) + item.SetArt(artwork); + database.Close(); + } + else if (tag.m_type == "actor" && !tag.m_artist.empty() && + item.GetProperty("musicvideomediatype") == MediaTypeArtist) + { + // Try to match artist in music db on name, get bio if available and fetch artist art + // Save the matching music library artist id. + CMusicDatabase database; + database.Open(); + CArtist artist; + int idArtist = database.GetArtistByName(tag.m_artist[0]); + if (idArtist > 0) + { + database.GetArtist(idArtist, artist); + tag.m_strPlot = artist.strBiography; + item.SetProperty("artist_musicid", idArtist); + } + if (database.GetArtForItem(idArtist, MediaTypeArtist, artwork)) + item.SetArt(artwork); + database.Close(); + } + + if (tag.m_iDbId > -1 && !tag.m_type.empty()) + { + m_videoDatabase->Open(); + if (m_videoDatabase->GetArtForItem(tag.m_iDbId, tag.m_type, artwork)) + item.AppendArt(artwork); + else if (tag.m_type == "actor" && !tag.m_artist.empty() && + item.GetProperty("musicvideomediatype") != MediaTypeArtist) + { + // Fallback to music library for actors without art + //! @todo Is m_artist set other than musicvideo? Remove this fallback if not. + CMusicDatabase database; + database.Open(); + int idArtist = database.GetArtistByName(item.GetLabel()); + if (database.GetArtForItem(idArtist, MediaTypeArtist, artwork)) + item.SetArt(artwork); + database.Close(); + } + + if (tag.m_type == MediaTypeEpisode || tag.m_type == MediaTypeSeason) + { + // For episodes and seasons, we want to set fanart for that of the show + if (!item.HasArt("tvshow.fanart") && tag.m_iIdShow >= 0) + { + const ArtMap& artmap = GetArtFromCache(MediaTypeTvShow, tag.m_iIdShow); + if (!artmap.empty()) + { + item.AppendArt(artmap, MediaTypeTvShow); + item.SetArtFallback("fanart", "tvshow.fanart"); + item.SetArtFallback("tvshow.thumb", "tvshow.poster"); + } + } + + if (tag.m_type == MediaTypeEpisode && !item.HasArt("season.poster") && tag.m_iSeason > -1) + { + const ArtMap& artmap = GetArtFromCache(MediaTypeSeason, tag.m_iIdSeason); + if (!artmap.empty()) + item.AppendArt(artmap, MediaTypeSeason); + } + } + else if (tag.m_type == MediaTypeMovie && tag.m_set.id >= 0 && !item.HasArt("set.fanart")) + { + const ArtMap& artmap = GetArtFromCache(MediaTypeVideoCollection, tag.m_set.id); + if (!artmap.empty()) + item.AppendArt(artmap, MediaTypeVideoCollection); + } + m_videoDatabase->Close(); + } + item.SetProperty("libraryartfilled", true); + return !item.GetArt().empty(); +} + +bool CVideoThumbLoader::FillThumb(CFileItem &item) +{ + if (item.HasArt("thumb")) + return true; + std::string thumb = GetCachedImage(item, "thumb"); + if (thumb.empty()) + { + thumb = GetLocalArt(item, "thumb"); + if (!thumb.empty()) + SetCachedImage(item, "thumb", thumb); + } + if (!thumb.empty()) + item.SetArt("thumb", thumb); + else + { + // If nothing was found, try embedded art + if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->m_coverArt.empty()) + { + for (auto& it : item.GetVideoInfoTag()->m_coverArt) + { + if (it.m_type == "thumb") + { + thumb = CTextureUtils::GetWrappedImageURL(item.GetPath(), "video_" + it.m_type); + item.SetArt(it.m_type, thumb); + } + } + } + } + + return !thumb.empty(); +} + +std::string CVideoThumbLoader::GetLocalArt(const CFileItem &item, const std::string &type, bool checkFolder) +{ + if (item.SkipLocalArt()) + return ""; + + /* Cache directory for (sub) folders with Curl("streamed") filesystems. We need to do this + else entering (new) directories from the app thread becomes much slower. This + is caused by the fact that Curl Stat/Exist() is really slow and that the + thumbloader thread accesses the streamed filesystem at the same time as the + app thread and the latter has to wait for it. + */ + if (item.m_bIsFolder && + (item.IsStreamedFilesystem() || + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheBufferMode == + CACHE_BUFFER_MODE_ALL)) + { + CFileItemList items; // Dummy list + CDirectory::GetDirectory(item.GetPath(), items, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO); + } + + std::string art; + if (!type.empty()) + { + art = item.FindLocalArt(type + ".jpg", checkFolder); + if (art.empty()) + art = item.FindLocalArt(type + ".png", checkFolder); + } + if (art.empty() && (type.empty() || type == "thumb")) + { // backward compatibility + art = item.FindLocalArt("", false); + if (art.empty() && (checkFolder || (item.m_bIsFolder && !item.IsFileFolder()) || item.IsOpticalMediaFile())) + { // try movie.tbn + art = item.FindLocalArt("movie.tbn", true); + if (art.empty()) // try folder.jpg + art = item.FindLocalArt("folder.jpg", true); + } + } + + return art; +} + +std::string CVideoThumbLoader::GetEmbeddedThumbURL(const CFileItem &item) +{ + std::string path(item.GetPath()); + if (item.IsVideoDb() && item.HasVideoInfoTag()) + path = item.GetVideoInfoTag()->m_strFileNameAndPath; + if (URIUtils::IsStack(path)) + path = CStackDirectory::GetFirstStackedFile(path); + + return CTextureUtils::GetWrappedImageURL(path, "video"); +} + +bool CVideoThumbLoader::GetEmbeddedThumb(const std::string& path, + const std::string& type, EmbeddedArt& art) +{ + CFileItem item(path, false); + std::unique_ptr<IVideoInfoTagLoader> pLoader; + pLoader.reset(CVideoInfoTagLoaderFactory::CreateLoader(item,ADDON::ScraperPtr(),false)); + CVideoInfoTag tag; + std::vector<EmbeddedArt> artv; + if (pLoader) + pLoader->Load(tag, false, &artv); + + for (const EmbeddedArt& it : artv) + { + if (it.m_type == type) + { + art = it; + break; + } + } + + return !art.Empty(); +} + +void CVideoThumbLoader::OnJobComplete(unsigned int jobID, bool success, CJob* job) +{ + if (success) + { + CThumbExtractor* loader = static_cast<CThumbExtractor*>(job); + loader->m_item.SetPath(loader->m_listpath); + + if (m_pObserver) + m_pObserver->OnItemLoaded(&loader->m_item); + CFileItemPtr pItem(new CFileItem(loader->m_item)); + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, pItem); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); + } + CJobQueue::OnJobComplete(jobID, success, job); +} + +void CVideoThumbLoader::DetectAndAddMissingItemData(CFileItem &item) +{ + if (item.m_bIsFolder) return; + + if (item.HasVideoInfoTag()) + { + CStreamDetails& details = item.GetVideoInfoTag()->m_streamDetails; + + // add audio language properties + for (int i = 1; i <= details.GetAudioStreamCount(); i++) + { + std::string index = std::to_string(i); + item.SetProperty("AudioChannels." + index, details.GetAudioChannels(i)); + item.SetProperty("AudioCodec." + index, details.GetAudioCodec(i).c_str()); + item.SetProperty("AudioLanguage." + index, details.GetAudioLanguage(i).c_str()); + } + + // add subtitle language properties + for (int i = 1; i <= details.GetSubtitleStreamCount(); i++) + { + std::string index = std::to_string(i); + item.SetProperty("SubtitleLanguage." + index, details.GetSubtitleLanguage(i).c_str()); + } + } + + const CStereoscopicsManager &stereoscopicsManager = CServiceBroker::GetGUI()->GetStereoscopicsManager(); + + std::string stereoMode; + + // detect stereomode for videos + if (item.HasVideoInfoTag()) + stereoMode = item.GetVideoInfoTag()->m_streamDetails.GetStereoMode(); + + if (stereoMode.empty()) + { + std::string path = item.GetPath(); + if (item.IsVideoDb() && item.HasVideoInfoTag()) + path = item.GetVideoInfoTag()->GetPath(); + + // check for custom stereomode setting in video settings + CVideoSettings itemVideoSettings; + m_videoDatabase->Open(); + if (m_videoDatabase->GetVideoSettings(item, itemVideoSettings) && itemVideoSettings.m_StereoMode != RENDER_STEREO_MODE_OFF) + { + stereoMode = CStereoscopicsManager::ConvertGuiStereoModeToString(static_cast<RENDER_STEREO_MODE>(itemVideoSettings.m_StereoMode)); + } + m_videoDatabase->Close(); + + // still empty, try grabbing from filename + //! @todo in case of too many false positives due to using the full path, extract the filename only using string utils + if (stereoMode.empty()) + stereoMode = stereoscopicsManager.DetectStereoModeByString(path); + } + + if (!stereoMode.empty()) + item.SetProperty("stereomode", CStereoscopicsManager::NormalizeStereoMode(stereoMode)); +} + +const ArtMap& CVideoThumbLoader::GetArtFromCache(const std::string &mediaType, const int id) +{ + std::pair<MediaType, int> key = std::make_pair(mediaType, id); + auto it = m_artCache.find(key); + if (it == m_artCache.end()) + { + ArtMap newart; + m_videoDatabase->GetArtForItem(id, mediaType, newart); + it = m_artCache.insert(std::make_pair(key, std::move(newart))).first; + } + return it->second; +} diff --git a/xbmc/video/VideoThumbLoader.h b/xbmc/video/VideoThumbLoader.h new file mode 100644 index 0000000..3c2d9f0 --- /dev/null +++ b/xbmc/video/VideoThumbLoader.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2012-2018 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. + */ + +#pragma once + +#include "FileItem.h" +#include "ThumbLoader.h" +#include "utils/JobManager.h" + +#include <map> +#include <vector> + +class CStreamDetails; +class CVideoDatabase; +class EmbeddedArt; + +using ArtMap = std::map<std::string, std::string>; +using ArtCache = std::map<std::pair<MediaType, int>, ArtMap>; + +/*! + \ingroup thumbs,jobs + \brief Thumb extractor job class + + Used by the CVideoThumbLoader to perform asynchronous generation of thumbs + + \sa CVideoThumbLoader and CJob + */ +class CThumbExtractor : public CJob +{ +public: + CThumbExtractor(const CFileItem& item, const std::string& listpath, bool thumb, const std::string& strTarget="", int64_t pos = -1, bool fillStreamDetails = true); + ~CThumbExtractor() override; + + /*! + \brief Work function that extracts thumb. + */ + bool DoWork() override; + + const char* GetType() const override + { + return kJobTypeMediaFlags; + } + + bool operator==(const CJob* job) const override; + + std::string m_target; ///< thumbpath + std::string m_listpath; ///< path used in fileitem list + CFileItem m_item; + bool m_thumb; ///< extract thumb? + int64_t m_pos; ///< position to extract thumb from + bool m_fillStreamDetails; ///< fill in stream details? +}; + +class CVideoThumbLoader : public CThumbLoader, public CJobQueue +{ +public: + CVideoThumbLoader(); + ~CVideoThumbLoader() override; + + void OnLoaderStart() override; + void OnLoaderFinish() override; + + bool LoadItem(CFileItem* pItem) override; + bool LoadItemCached(CFileItem* pItem) override; + bool LoadItemLookup(CFileItem* pItem) override; + + /*! \brief Fill the thumb of a video item + First uses a cached thumb from a previous run, then checks for a local thumb + and caches it for the next run + \param item the CFileItem object to fill + \return true if we fill the thumb, false otherwise + */ + virtual bool FillThumb(CFileItem &item); + + /*! \brief Find a particular art type for a given item, optionally checking at the folder level + \param item the CFileItem to search. + \param type the type of art to look for. + \param checkFolder whether to also check the folder level for files. Defaults to false. + \return the art file (if found), else empty. + */ + static std::string GetLocalArt(const CFileItem &item, const std::string &type, bool checkFolder = false); + + /*! \brief return the available art types for a given media type + \param type the type of media. + \return a vector of art types. + \sa GetLocalArt + */ + static std::vector<std::string> GetArtTypes(const std::string &type); + + static bool IsValidArtType(const std::string& potentialArtType); + + static bool IsArtTypeInWhitelist(const std::string& artType, const std::vector<std::string>& whitelist, bool exact); + + /*! \brief helper function to retrieve a thumb URL for embedded video thumbs + \param item a video CFileItem. + \return a URL for the embedded thumb. + */ + static std::string GetEmbeddedThumbURL(const CFileItem &item); + + /*! \brief helper function to fill the art for a video library item + \param item a video CFileItem + \return true if we fill art, false otherwise + */ + bool FillLibraryArt(CFileItem &item) override; + + /*! + \brief Callback from CThumbExtractor on completion of a generated image + + Performs the callbacks and updates the GUI. + + \sa CImageLoader, IJobCallback + */ + void OnJobComplete(unsigned int jobID, bool success, CJob *job) override; + + static bool GetEmbeddedThumb(const std::string& path, + const std::string& type, + EmbeddedArt& art); + +protected: + CVideoDatabase *m_videoDatabase; + ArtCache m_artCache; + + /*! \brief Tries to detect missing data/info from a file and adds those + \param item The CFileItem to process + \return void + */ + void DetectAndAddMissingItemData(CFileItem &item); + + const ArtMap& GetArtFromCache(const std::string &mediaType, const int id); +}; 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 diff --git a/xbmc/video/VideoUtils.h b/xbmc/video/VideoUtils.h new file mode 100644 index 0000000..d82df47 --- /dev/null +++ b/xbmc/video/VideoUtils.h @@ -0,0 +1,69 @@ +/* + * 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. + */ + +#pragma once + +#include <memory> + +class CFileItem; +class CFileItemList; + +namespace VIDEO_UTILS +{ +/*! \brief Start playback of the given item. If the item is a folder, build a playlist with + all items contained in the folder and start playback of the playlist. If item is a single video + item, start playback directly, without adding it to the video playlist first. + \param item [in] the item to play + */ +void PlayItem(const std::shared_ptr<CFileItem>& item); + +enum class QueuePosition +{ + POSITION_BEGIN, // place at begin of queue, before other items + POSITION_END, // place at end of queue, after other items +}; + +/*! \brief Queue the given item in the currently active playlist. If no playlist is active, + put the item into the video playlist. + \param item [in] the item to queue + \param pos [in] whether to place the item and the begin or the end of the queue + */ +void QueueItem(const std::shared_ptr<CFileItem>& item, QueuePosition pos); + +/*! \brief For a given item, get the items to put in a playlist. If the item is a folder, all ++ subitems will be added recursively to the returned item list. If the item is a playlist, the ++ playlist will be loaded and contained items will be added to the returned item list. Shows a ++ busy dialog if action takes certain amount of time to give the user visual feedback. ++ \param item [in] the item to add to the playlist ++ \param queuedItems [out] the items that can be put in a play list ++ \return true on success, false otherwise ++ */ +bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems); + +/*! + \brief Check whether the given item can be played by the app playlist player as one or more videos. + \param item The item to check + \return True if playable, false otherwise. + */ +bool IsItemPlayable(const CFileItem& item); + +struct ResumeInformation +{ + bool isResumable{false}; // the playback of the item can be resumed + int64_t startOffset{0}; // a start offset + int partNumber{0}; // a part number +}; + +/*! + \brief Check whether playback of the given item can be resumed, get detailed information. + \param item The item to retrieve information for + \return The resume information. + */ +ResumeInformation GetItemResumeInformation(const CFileItem& item); + +} // namespace VIDEO_UTILS diff --git a/xbmc/video/ViewModeSettings.cpp b/xbmc/video/ViewModeSettings.cpp new file mode 100644 index 0000000..6c8a919 --- /dev/null +++ b/xbmc/video/ViewModeSettings.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016-2018 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 "ViewModeSettings.h" + +#include "cores/VideoSettings.h" +#include "guilib/LocalizeStrings.h" +#include "settings/lib/SettingDefinitions.h" + +struct ViewModeProperties +{ + int stringIndex; + int viewMode; + bool hideFromQuickCycle = false; + bool hideFromList = false; +}; + +#define HIDE_ITEM true + +/** The list of all the view modes along with their properties + */ +static const ViewModeProperties viewModes[] = +{ + { 630, ViewModeNormal }, + { 631, ViewModeZoom }, + { 39008, ViewModeZoom120Width }, + { 39009, ViewModeZoom110Width }, + { 632, ViewModeStretch4x3 }, + { 633, ViewModeWideZoom }, + { 634, ViewModeStretch16x9 }, + { 644, ViewModeStretch16x9Nonlin, HIDE_ITEM, HIDE_ITEM }, + { 635, ViewModeOriginal }, + { 636, ViewModeCustom, HIDE_ITEM } +}; + +#define NUMBER_OF_VIEW_MODES (sizeof(viewModes) / sizeof(viewModes[0])) + +/** Gets the index of a view mode + * + * @param viewMode The view mode + * @return The index of the view mode in the viewModes array + */ +static int GetViewModeIndex(int viewMode) +{ + size_t i; + + // Find the current view mode + for (i = 0; i < NUMBER_OF_VIEW_MODES; i++) + { + if (viewModes[i].viewMode == viewMode) + return i; + } + + return 0; // An invalid view mode will always return the first view mode +} + +/** Gets the next view mode for quick cycling through the modes + * + * @param viewMode The current view mode + * @return The next view mode + */ +int CViewModeSettings::GetNextQuickCycleViewMode(int viewMode) +{ + // Find the next quick cycle view mode + for (size_t i = GetViewModeIndex(viewMode) + 1; i < NUMBER_OF_VIEW_MODES; i++) + { + if (!viewModes[i].hideFromQuickCycle) + return viewModes[i].viewMode; + } + + return ViewModeNormal; +} + +/** Gets the string index for the view mode + * + * @param viewMode The current view mode + * @return The string index + */ +int CViewModeSettings::GetViewModeStringIndex(int viewMode) +{ + return viewModes[GetViewModeIndex(viewMode)].stringIndex; +} + +/** Fills the list with all visible view modes + */ +void CViewModeSettings::ViewModesFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + // Add all appropriate view modes to the list control + for (const auto &item : viewModes) + { + if (!item.hideFromList) + list.emplace_back(g_localizeStrings.Get(item.stringIndex), item.viewMode); + } +} diff --git a/xbmc/video/ViewModeSettings.h b/xbmc/video/ViewModeSettings.h new file mode 100644 index 0000000..b43ed85 --- /dev/null +++ b/xbmc/video/ViewModeSettings.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016-2018 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. + */ + +#pragma once + +#include "settings/lib/Setting.h" + +#include <string> +#include <utility> +#include <vector> + +struct IntegerSettingOption; + +class CViewModeSettings +{ +private: + CViewModeSettings(); + ~CViewModeSettings() = default; + +public: + /** Gets the next view mode for quick cycling through the modes + * + * @param viewMode The current view mode + * @return The next view mode + */ + static int GetNextQuickCycleViewMode(int viewMode); + + /** Gets the string index for the view mode + * + * @param viewMode The current view mode + * @return The string index + */ + static int GetViewModeStringIndex(int viewMode); + + /** Fills the list with all visible view modes + */ + static void ViewModesFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); +}; diff --git a/xbmc/video/dialogs/CMakeLists.txt b/xbmc/video/dialogs/CMakeLists.txt new file mode 100644 index 0000000..b2ec11f --- /dev/null +++ b/xbmc/video/dialogs/CMakeLists.txt @@ -0,0 +1,26 @@ +set(SOURCES GUIDialogAudioSettings.cpp + GUIDialogFullScreenInfo.cpp + GUIDialogSubtitles.cpp + GUIDialogSubtitleSettings.cpp + GUIDialogTeletext.cpp + GUIDialogVideoBookmarks.cpp + GUIDialogVideoInfo.cpp + GUIDialogVideoOSD.cpp + GUIDialogVideoSettings.cpp) + +set(HEADERS GUIDialogAudioSettings.h + GUIDialogFullScreenInfo.h + GUIDialogSubtitles.h + GUIDialogSubtitleSettings.h + GUIDialogTeletext.h + GUIDialogVideoBookmarks.h + GUIDialogVideoInfo.h + GUIDialogVideoOSD.h + GUIDialogVideoSettings.h) + +if(OPENGL_FOUND OR CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore) + list(APPEND SOURCES GUIDialogCMSSettings.cpp) + list(APPEND HEADERS GUIDialogCMSSettings.h) +endif() + +core_add_library(video_dialogs) diff --git a/xbmc/video/dialogs/GUIDialogAudioSettings.cpp b/xbmc/video/dialogs/GUIDialogAudioSettings.cpp new file mode 100644 index 0000000..83b888b --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogAudioSettings.cpp @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogAudioSettings.h" + +#include "GUIPassword.h" +#include "ServiceBroker.h" +#include "addons/Skin.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "application/ApplicationVolumeHandling.h" +#include "cores/AudioEngine/Utils/AEUtil.h" +#include "cores/IPlayer.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIMessage.h" +#include "guilib/LocalizeStrings.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "settings/lib/SettingDefinitions.h" +#include "settings/lib/SettingsManager.h" +#include "utils/LangCodeExpander.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <string> +#include <vector> + +#define SETTING_AUDIO_VOLUME "audio.volume" +#define SETTING_AUDIO_VOLUME_AMPLIFICATION "audio.volumeamplification" +#define SETTING_AUDIO_CENTERMIXLEVEL "audio.centermixlevel" +#define SETTING_AUDIO_DELAY "audio.delay" +#define SETTING_AUDIO_STREAM "audio.stream" +#define SETTING_AUDIO_PASSTHROUGH "audio.digitalanalog" +#define SETTING_AUDIO_MAKE_DEFAULT "audio.makedefault" + +CGUIDialogAudioSettings::CGUIDialogAudioSettings() + : CGUIDialogSettingsManualBase(WINDOW_DIALOG_AUDIO_OSD_SETTINGS, "DialogSettings.xml") +{ } + +CGUIDialogAudioSettings::~CGUIDialogAudioSettings() = default; + +void CGUIDialogAudioSettings::FrameMove() +{ + // update the volume setting if necessary + const auto& components = CServiceBroker::GetAppComponents(); + const auto appVolume = components.GetComponent<CApplicationVolumeHandling>(); + float newVolume = appVolume->GetVolumeRatio(); + if (newVolume != m_volume) + GetSettingsManager()->SetNumber(SETTING_AUDIO_VOLUME, static_cast<double>(newVolume)); + + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->HasPlayer()) + { + const CVideoSettings videoSettings = appPlayer->GetVideoSettings(); + + // these settings can change on the fly + //! @todo (needs special handling): m_settingsManager->SetInt(SETTING_AUDIO_STREAM, g_application.GetAppPlayer().GetAudioStream()); + GetSettingsManager()->SetNumber(SETTING_AUDIO_DELAY, + static_cast<double>(videoSettings.m_AudioDelay)); + GetSettingsManager()->SetBool(SETTING_AUDIO_PASSTHROUGH, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH)); + } + + CGUIDialogSettingsManualBase::FrameMove(); +} + +std::string CGUIDialogAudioSettings::FormatDelay(float value, float interval) +{ + if (fabs(value) < 0.5f * interval) + return StringUtils::Format(g_localizeStrings.Get(22003), 0.0); + if (value < 0) + return StringUtils::Format(g_localizeStrings.Get(22004), fabs(value)); + + return StringUtils::Format(g_localizeStrings.Get(22005), value); +} + +std::string CGUIDialogAudioSettings::FormatDecibel(float value) +{ + return StringUtils::Format(g_localizeStrings.Get(14054), value); +} + +std::string CGUIDialogAudioSettings::FormatPercentAsDecibel(float value) +{ + return StringUtils::Format(g_localizeStrings.Get(14054), CAEUtil::PercentToGain(value)); +} + +void CGUIDialogAudioSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + return; + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_AUDIO_VOLUME) + { + m_volume = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + const auto appVolume = components.GetComponent<CApplicationVolumeHandling>(); + appVolume->SetVolume(m_volume, false); // false - value is not in percent + } + else if (settingId == SETTING_AUDIO_VOLUME_AMPLIFICATION) + { + float value = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + appPlayer->SetDynamicRangeCompression((long)(value * 100)); + } + else if (settingId == SETTING_AUDIO_CENTERMIXLEVEL) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_CenterMixLevel = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_AUDIO_DELAY) + { + float value = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + appPlayer->SetAVDelay(value); + } + else if (settingId == SETTING_AUDIO_STREAM) + { + m_audioStream = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + // only change the audio stream if a different one has been asked for + if (appPlayer->GetAudioStream() != m_audioStream) + { + appPlayer->SetAudioStream(m_audioStream); // Set the audio stream to the one selected + } + } + else if (settingId == SETTING_AUDIO_PASSTHROUGH) + { + m_passthrough = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH, m_passthrough); + } +} + +void CGUIDialogAudioSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + return; + + CGUIDialogSettingsManualBase::OnSettingAction(setting); + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_AUDIO_MAKE_DEFAULT) + Save(); +} + +bool CGUIDialogAudioSettings::Save() +{ + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (!g_passwordManager.CheckSettingLevelLock(SettingLevel::Expert) && + profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE) + return true; + + // prompt user if they are sure + if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{12376}, CVariant{12377})) + return true; + + // reset the settings + CVideoDatabase db; + if (!db.Open()) + return true; + + db.EraseAllVideoSettings(); + db.Close(); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + CMediaSettings::GetInstance().GetDefaultVideoSettings() = appPlayer->GetVideoSettings(); + CMediaSettings::GetInstance().GetDefaultVideoSettings().m_AudioStream = -1; + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + return true; +} + +void CGUIDialogAudioSettings::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + + SetHeading(13396); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067); +} + +void CGUIDialogAudioSettings::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + const std::shared_ptr<CSettingCategory> category = AddCategory("audiosubtitlesettings", -1); + if (category == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings"); + return; + } + + // get all necessary setting groups + const std::shared_ptr<CSettingGroup> groupAudio = AddGroup(category); + if (groupAudio == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings"); + return; + } + const std::shared_ptr<CSettingGroup> groupSubtitles = AddGroup(category); + if (groupSubtitles == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings"); + return; + } + const std::shared_ptr<CSettingGroup> groupSaveAsDefault = AddGroup(category); + if (groupSaveAsDefault == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings"); + return; + } + + bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml"); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + const CVideoSettings videoSettings = appPlayer->GetVideoSettings(); + if (appPlayer->HasPlayer()) + { + appPlayer->GetAudioCapabilities(m_audioCaps); + } + + // register IsPlayingPassthrough condition + GetSettingsManager()->AddDynamicCondition("IsPlayingPassthrough", IsPlayingPassthrough); + + CSettingDependency dependencyAudioOutputPassthroughDisabled(SettingDependencyType::Enable, GetSettingsManager()); + dependencyAudioOutputPassthroughDisabled.Or() + ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_AUDIO_PASSTHROUGH, "false", SettingDependencyOperator::Equals, false, GetSettingsManager()))) + ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition("IsPlayingPassthrough", "", "", true, GetSettingsManager()))); + SettingDependencies depsAudioOutputPassthroughDisabled; + depsAudioOutputPassthroughDisabled.push_back(dependencyAudioOutputPassthroughDisabled); + + // audio settings + // audio volume setting + const auto appVolume = components.GetComponent<CApplicationVolumeHandling>(); + m_volume = appVolume->GetVolumeRatio(); + std::shared_ptr<CSettingNumber> settingAudioVolume = + AddSlider(groupAudio, SETTING_AUDIO_VOLUME, 13376, SettingLevel::Basic, m_volume, 14054, + CApplicationVolumeHandling::VOLUME_MINIMUM, + CApplicationVolumeHandling::VOLUME_MAXIMUM / 100.0f, + CApplicationVolumeHandling::VOLUME_MAXIMUM); + settingAudioVolume->SetDependencies(depsAudioOutputPassthroughDisabled); + std::static_pointer_cast<CSettingControlSlider>(settingAudioVolume->GetControl())->SetFormatter(SettingFormatterPercentAsDecibel); + + // audio volume amplification setting + if (SupportsAudioFeature(IPC_AUD_AMP)) + { + std::shared_ptr<CSettingNumber> settingAudioVolumeAmplification = AddSlider(groupAudio, SETTING_AUDIO_VOLUME_AMPLIFICATION, 660, SettingLevel::Basic, videoSettings.m_VolumeAmplification, 14054, VOLUME_DRC_MINIMUM * 0.01f, (VOLUME_DRC_MAXIMUM - VOLUME_DRC_MINIMUM) / 6000.0f, VOLUME_DRC_MAXIMUM * 0.01f); + settingAudioVolumeAmplification->SetDependencies(depsAudioOutputPassthroughDisabled); + } + + // downmix: center mix level + { + AddSlider(groupAudio, SETTING_AUDIO_CENTERMIXLEVEL, 39112, SettingLevel::Basic, + videoSettings.m_CenterMixLevel, 14050, -10, 1, 30, + -1, false, false, true, 39113); + } + + // audio delay setting + if (SupportsAudioFeature(IPC_AUD_OFFSET)) + { + std::shared_ptr<CSettingNumber> settingAudioDelay = AddSlider( + groupAudio, SETTING_AUDIO_DELAY, 297, SettingLevel::Basic, videoSettings.m_AudioDelay, 0, + -CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange, + AUDIO_DELAY_STEP, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange, 297, + usePopup); + std::static_pointer_cast<CSettingControlSlider>(settingAudioDelay->GetControl())->SetFormatter(SettingFormatterDelay); + } + + // audio stream setting + if (SupportsAudioFeature(IPC_AUD_SELECT_STREAM)) + AddAudioStreams(groupAudio, SETTING_AUDIO_STREAM); + + // audio digital/analog setting + if (SupportsAudioFeature(IPC_AUD_SELECT_OUTPUT)) + { + m_passthrough = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH); + AddToggle(groupAudio, SETTING_AUDIO_PASSTHROUGH, 348, SettingLevel::Basic, m_passthrough); + } + + // subtitle stream setting + AddButton(groupSaveAsDefault, SETTING_AUDIO_MAKE_DEFAULT, 12376, SettingLevel::Basic); +} + +bool CGUIDialogAudioSettings::SupportsAudioFeature(int feature) +{ + for (Features::iterator itr = m_audioCaps.begin(); itr != m_audioCaps.end(); ++itr) + { + if (*itr == feature || *itr == IPC_AUD_ALL) + return true; + } + + return false; +} + +void CGUIDialogAudioSettings::AddAudioStreams(const std::shared_ptr<CSettingGroup>& group, + const std::string& settingId) +{ + if (group == NULL || settingId.empty()) + return; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + m_audioStream = appPlayer->GetAudioStream(); + if (m_audioStream < 0) + m_audioStream = 0; + + AddList(group, settingId, 460, SettingLevel::Basic, m_audioStream, AudioStreamsOptionFiller, 460); +} + +bool CGUIDialogAudioSettings::IsPlayingPassthrough(const std::string& condition, + const std::string& value, + const SettingConstPtr& setting, + void* data) +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + return appPlayer->IsPassthrough(); +} + +void CGUIDialogAudioSettings::AudioStreamsOptionFiller(const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + int audioStreamCount = appPlayer->GetAudioStreamCount(); + + std::string strFormat = "{:s} - {:s} - {:d} " + g_localizeStrings.Get(10127); + std::string strUnknown = "[" + g_localizeStrings.Get(13205) + "]"; + + // cycle through each audio stream and add it to our list control + for (int i = 0; i < audioStreamCount; ++i) + { + std::string strItem; + std::string strLanguage; + + AudioStreamInfo info; + appPlayer->GetAudioStreamInfo(i, info); + + if (!g_LangCodeExpander.Lookup(info.language, strLanguage)) + strLanguage = strUnknown; + + if (info.name.length() == 0) + info.name = strUnknown; + + strItem = StringUtils::Format(strFormat, strLanguage, info.name, info.channels); + + strItem += FormatFlags(info.flags); + strItem += StringUtils::Format(" ({}/{})", i + 1, audioStreamCount); + list.emplace_back(strItem, i); + } + + if (list.empty()) + { + list.emplace_back(g_localizeStrings.Get(231), -1); + current = -1; + } +} + +std::string CGUIDialogAudioSettings::SettingFormatterDelay( + const std::shared_ptr<const CSettingControlSlider>& control, + const CVariant& value, + const CVariant& minimum, + const CVariant& step, + const CVariant& maximum) +{ + if (!value.isDouble()) + return ""; + + float fValue = value.asFloat(); + float fStep = step.asFloat(); + + if (fabs(fValue) < 0.5f * fStep) + return StringUtils::Format(g_localizeStrings.Get(22003), 0.0); + if (fValue < 0) + return StringUtils::Format(g_localizeStrings.Get(22004), fabs(fValue)); + + return StringUtils::Format(g_localizeStrings.Get(22005), fValue); +} + +std::string CGUIDialogAudioSettings::SettingFormatterPercentAsDecibel( + const std::shared_ptr<const CSettingControlSlider>& control, + const CVariant& value, + const CVariant& minimum, + const CVariant& step, + const CVariant& maximum) +{ + if (control == NULL || !value.isDouble()) + return ""; + + std::string formatString = control->GetFormatString(); + if (control->GetFormatLabel() > -1) + formatString = g_localizeStrings.Get(control->GetFormatLabel()); + + return StringUtils::Format(formatString, CAEUtil::PercentToGain(value.asFloat())); +} + +std::string CGUIDialogAudioSettings::FormatFlags(StreamFlags flags) +{ + std::vector<std::string> localizedFlags; + if (flags & StreamFlags::FLAG_DEFAULT) + localizedFlags.emplace_back(g_localizeStrings.Get(39105)); + if (flags & StreamFlags::FLAG_FORCED) + localizedFlags.emplace_back(g_localizeStrings.Get(39106)); + if (flags & StreamFlags::FLAG_HEARING_IMPAIRED) + localizedFlags.emplace_back(g_localizeStrings.Get(39107)); + if (flags & StreamFlags::FLAG_VISUAL_IMPAIRED) + localizedFlags.emplace_back(g_localizeStrings.Get(39108)); + if (flags & StreamFlags::FLAG_ORIGINAL) + localizedFlags.emplace_back(g_localizeStrings.Get(39111)); + + std::string formated = StringUtils::Join(localizedFlags, ", "); + + if (!formated.empty()) + formated = StringUtils::Format(" [{}]", formated); + + return formated; +} diff --git a/xbmc/video/dialogs/GUIDialogAudioSettings.h b/xbmc/video/dialogs/GUIDialogAudioSettings.h new file mode 100644 index 0000000..de69b77 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogAudioSettings.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "cores/VideoPlayer/Interface/StreamInfo.h" +#include "settings/dialogs/GUIDialogSettingsManualBase.h" + +#include <string> +#include <utility> +#include <vector> + +class CVariant; +struct IntegerSettingOption; + +class CGUIDialogAudioSettings : public CGUIDialogSettingsManualBase +{ +public: + CGUIDialogAudioSettings(); + ~CGUIDialogAudioSettings() override; + + // specialization of CGUIWindow + void FrameMove() override; + + static std::string FormatDelay(float value, float interval); + static std::string FormatDecibel(float value); + static std::string FormatPercentAsDecibel(float value); + +protected: + // implementations of ISettingCallback + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + + // specialization of CGUIDialogSettingsBase + bool AllowResettingSettings() const override { return false; } + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + + bool SupportsAudioFeature(int feature); + + void AddAudioStreams(const std::shared_ptr<CSettingGroup>& group, const std::string& settingId); + + static bool IsPlayingPassthrough(const std::string& condition, + const std::string& value, + const std::shared_ptr<const CSetting>& setting, + void* data); + + static void AudioStreamsOptionFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + + static std::string SettingFormatterDelay( + const std::shared_ptr<const CSettingControlSlider>& control, + const CVariant& value, + const CVariant& minimum, + const CVariant& step, + const CVariant& maximum); + static std::string SettingFormatterPercentAsDecibel( + const std::shared_ptr<const CSettingControlSlider>& control, + const CVariant& value, + const CVariant& minimum, + const CVariant& step, + const CVariant& maximum); + + float m_volume; + int m_audioStream; + bool m_passthrough = false; + + typedef std::vector<int> Features; + Features m_audioCaps; +private: + static std::string FormatFlags(StreamFlags flags); +}; diff --git a/xbmc/video/dialogs/GUIDialogCMSSettings.cpp b/xbmc/video/dialogs/GUIDialogCMSSettings.cpp new file mode 100644 index 0000000..ac1a7ad --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogCMSSettings.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogCMSSettings.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "ServiceBroker.h" +#include "addons/Skin.h" +#include "cores/VideoPlayer/VideoRenderers/ColorManager.h" +#include "cores/VideoPlayer/VideoRenderers/RenderManager.h" +#include "filesystem/Directory.h" +#include "guilib/GUIWindowManager.h" +#include "profiles/ProfileManager.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "settings/lib/SettingDefinitions.h" +#include "settings/lib/SettingsManager.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <vector> + +#define SETTING_VIDEO_CMSENABLE "videoscreen.cmsenabled" +#define SETTING_VIDEO_CMSMODE "videoscreen.cmsmode" +#define SETTING_VIDEO_CMS3DLUT "videoscreen.cms3dlut" +#define SETTING_VIDEO_CMSWHITEPOINT "videoscreen.cmswhitepoint" +#define SETTING_VIDEO_CMSPRIMARIES "videoscreen.cmsprimaries" +#define SETTING_VIDEO_CMSGAMMAMODE "videoscreen.cmsgammamode" +#define SETTING_VIDEO_CMSGAMMA "videoscreen.cmsgamma" +#define SETTING_VIDEO_CMSLUTSIZE "videoscreen.cmslutsize" + +CGUIDialogCMSSettings::CGUIDialogCMSSettings() + : CGUIDialogSettingsManualBase(WINDOW_DIALOG_CMS_OSD_SETTINGS, "DialogSettings.xml") +{ } + +CGUIDialogCMSSettings::~CGUIDialogCMSSettings() = default; + +void CGUIDialogCMSSettings::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + + SetHeading(36560); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067); +} + +void CGUIDialogCMSSettings::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + const std::shared_ptr<CSettingCategory> category = AddCategory("cms", -1); + if (category == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogCMSSettings: unable to setup settings"); + return; + } + + // get all necessary setting groups + const std::shared_ptr<CSettingGroup> groupColorManagement = AddGroup(category); + if (groupColorManagement == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogCMSSettings: unable to setup settings"); + return; + } + + bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml"); + + TranslatableIntegerSettingOptions entries; + + // create "depsCmsEnabled" for settings depending on CMS being enabled + CSettingDependency dependencyCmsEnabled(SettingDependencyType::Enable, GetSettingsManager()); + dependencyCmsEnabled.Or() + ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSENABLE, "true", SettingDependencyOperator::Equals, false, GetSettingsManager()))); + SettingDependencies depsCmsEnabled; + depsCmsEnabled.push_back(dependencyCmsEnabled); + + // create "depsCms3dlut" for 3dlut settings + CSettingDependency dependencyCms3dlut(SettingDependencyType::Visible, GetSettingsManager()); + dependencyCms3dlut.And() + ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSMODE, std::to_string(CMS_MODE_3DLUT), SettingDependencyOperator::Equals, false, GetSettingsManager()))); + SettingDependencies depsCms3dlut; + depsCms3dlut.push_back(dependencyCmsEnabled); + depsCms3dlut.push_back(dependencyCms3dlut); + + // create "depsCmsIcc" for display settings with icc profile + CSettingDependency dependencyCmsIcc(SettingDependencyType::Visible, GetSettingsManager()); + dependencyCmsIcc.And() + ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSMODE, std::to_string(CMS_MODE_PROFILE), SettingDependencyOperator::Equals, false, GetSettingsManager()))); + SettingDependencies depsCmsIcc; + depsCmsIcc.push_back(dependencyCmsEnabled); + depsCmsIcc.push_back(dependencyCmsIcc); + + // create "depsCmsGamma" for effective gamma adjustment (not available with bt.1886) + CSettingDependency dependencyCmsGamma(SettingDependencyType::Visible, GetSettingsManager()); + dependencyCmsGamma.And() + ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSGAMMAMODE, std::to_string(CMS_TRC_BT1886), SettingDependencyOperator::Equals, true, GetSettingsManager()))); + SettingDependencies depsCmsGamma; + depsCmsGamma.push_back(dependencyCmsEnabled); + depsCmsGamma.push_back(dependencyCmsIcc); + depsCmsGamma.push_back(dependencyCmsGamma); + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + + // color management settings + AddToggle(groupColorManagement, SETTING_VIDEO_CMSENABLE, 36560, SettingLevel::Basic, settings->GetBool(SETTING_VIDEO_CMSENABLE)); + + int currentMode = settings->GetInt(SETTING_VIDEO_CMSMODE); + entries.clear(); + // entries.push_back(TranslatableIntegerSettingOption(16039, CMS_MODE_OFF)); // FIXME: get from CMS class + entries.push_back(TranslatableIntegerSettingOption(36580, CMS_MODE_3DLUT)); +#ifdef HAVE_LCMS2 + entries.push_back(TranslatableIntegerSettingOption(36581, CMS_MODE_PROFILE)); +#endif + std::shared_ptr<CSettingInt> settingCmsMode = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSMODE, 36562, SettingLevel::Basic, currentMode, entries); + settingCmsMode->SetDependencies(depsCmsEnabled); + + std::string current3dLUT = settings->GetString(SETTING_VIDEO_CMS3DLUT); + std::shared_ptr<CSettingString> settingCms3dlut = AddList(groupColorManagement, SETTING_VIDEO_CMS3DLUT, 36564, SettingLevel::Basic, current3dLUT, Cms3dLutsFiller, 36564); + settingCms3dlut->SetDependencies(depsCms3dlut); + + // display settings + int currentWhitepoint = settings->GetInt(SETTING_VIDEO_CMSWHITEPOINT); + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(36586, CMS_WHITEPOINT_D65)); + entries.push_back(TranslatableIntegerSettingOption(36587, CMS_WHITEPOINT_D93)); + std::shared_ptr<CSettingInt> settingCmsWhitepoint = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSWHITEPOINT, 36568, SettingLevel::Basic, currentWhitepoint, entries); + settingCmsWhitepoint->SetDependencies(depsCmsIcc); + + int currentPrimaries = settings->GetInt(SETTING_VIDEO_CMSPRIMARIES); + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(36588, CMS_PRIMARIES_AUTO)); + entries.push_back(TranslatableIntegerSettingOption(36589, CMS_PRIMARIES_BT709)); + entries.push_back(TranslatableIntegerSettingOption(36579, CMS_PRIMARIES_BT2020)); + entries.push_back(TranslatableIntegerSettingOption(36590, CMS_PRIMARIES_170M)); + entries.push_back(TranslatableIntegerSettingOption(36591, CMS_PRIMARIES_BT470M)); + entries.push_back(TranslatableIntegerSettingOption(36592, CMS_PRIMARIES_BT470BG)); + entries.push_back(TranslatableIntegerSettingOption(36593, CMS_PRIMARIES_240M)); + std::shared_ptr<CSettingInt> settingCmsPrimaries = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSPRIMARIES, 36570, SettingLevel::Basic, currentPrimaries, entries); + settingCmsPrimaries->SetDependencies(depsCmsIcc); + + int currentGammaMode = settings->GetInt(SETTING_VIDEO_CMSGAMMAMODE); + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(36582, CMS_TRC_BT1886)); + entries.push_back(TranslatableIntegerSettingOption(36583, CMS_TRC_INPUT_OFFSET)); + entries.push_back(TranslatableIntegerSettingOption(36584, CMS_TRC_OUTPUT_OFFSET)); + entries.push_back(TranslatableIntegerSettingOption(36585, CMS_TRC_ABSOLUTE)); + std::shared_ptr<CSettingInt> settingCmsGammaMode = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSGAMMAMODE, 36572, SettingLevel::Basic, currentGammaMode, entries); + settingCmsGammaMode->SetDependencies(depsCmsIcc); + + float currentGamma = settings->GetInt(SETTING_VIDEO_CMSGAMMA)/100.0f; + if (currentGamma == 0.0f) + currentGamma = 2.20f; + std::shared_ptr<CSettingNumber> settingCmsGamma = AddSlider(groupColorManagement, SETTING_VIDEO_CMSGAMMA, 36574, SettingLevel::Basic, currentGamma, 36597, 1.6, 0.05, 2.8, 36574, usePopup); + settingCmsGamma->SetDependencies(depsCmsGamma); + + int currentLutSize = settings->GetInt(SETTING_VIDEO_CMSLUTSIZE); + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(36594, 4)); + entries.push_back(TranslatableIntegerSettingOption(36595, 6)); + entries.push_back(TranslatableIntegerSettingOption(36596, 8)); + std::shared_ptr<CSettingInt> settingCmsLutSize = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSLUTSIZE, 36576, SettingLevel::Basic, currentLutSize, entries); + settingCmsLutSize->SetDependencies(depsCmsIcc); +} + +void CGUIDialogCMSSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + return; + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_VIDEO_CMSENABLE) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(SETTING_VIDEO_CMSENABLE, (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())); + else if (settingId == SETTING_VIDEO_CMSMODE) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSMODE, std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_CMS3DLUT) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(SETTING_VIDEO_CMS3DLUT, std::static_pointer_cast<const CSettingString>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_CMSWHITEPOINT) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSWHITEPOINT, std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_CMSPRIMARIES) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSPRIMARIES, std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_CMSGAMMAMODE) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSGAMMAMODE, std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_CMSGAMMA) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSGAMMA, static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue())*100); + else if (settingId == SETTING_VIDEO_CMSLUTSIZE) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSLUTSIZE, std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); +} + +bool CGUIDialogCMSSettings::OnBack(int actionID) +{ + Save(); + return CGUIDialogSettingsBase::OnBack(actionID); +} + +bool CGUIDialogCMSSettings::Save() +{ + CLog::Log(LOGINFO, "CGUIDialogCMSSettings: Save() called"); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + return true; +} + +void CGUIDialogCMSSettings::Cms3dLutsFiller(const SettingConstPtr& setting, + std::vector<StringSettingOption>& list, + std::string& current, + void* data) +{ + // get 3dLut directory from settings + CFileItemList items; + + // list .3dlut files + std::string current3dlut = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(SETTING_VIDEO_CMS3DLUT); + if (!current3dlut.empty()) + current3dlut = URIUtils::GetDirectory(current3dlut); + XFILE::CDirectory::GetDirectory(current3dlut, items, ".3dlut", XFILE::DIR_FLAG_DEFAULTS); + + for (int i = 0; i < items.Size(); i++) + { + list.emplace_back(items[i]->GetLabel(), items[i]->GetPath()); + } +} diff --git a/xbmc/video/dialogs/GUIDialogCMSSettings.h b/xbmc/video/dialogs/GUIDialogCMSSettings.h new file mode 100644 index 0000000..e718b29 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogCMSSettings.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "settings/dialogs/GUIDialogSettingsManualBase.h" + +struct StringSettingOption; + +class CGUIDialogCMSSettings : public CGUIDialogSettingsManualBase +{ +public: + CGUIDialogCMSSettings(); + ~CGUIDialogCMSSettings() override; + +protected: + // implementations of ISettingCallback + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + + // specialization of CGUIDialogSettingsBase + bool AllowResettingSettings() const override { return false; } + bool OnBack(int actionID) override; + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + +private: + static void Cms3dLutsFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<StringSettingOption>& list, + std::string& current, + void* data); +}; diff --git a/xbmc/video/dialogs/GUIDialogFullScreenInfo.cpp b/xbmc/video/dialogs/GUIDialogFullScreenInfo.cpp new file mode 100644 index 0000000..70ebafe --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogFullScreenInfo.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogFullScreenInfo.h" + +#include "input/Key.h" + +CGUIDialogFullScreenInfo::CGUIDialogFullScreenInfo(void) + : CGUIDialog(WINDOW_DIALOG_FULLSCREEN_INFO, "DialogFullScreenInfo.xml") +{ + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogFullScreenInfo::~CGUIDialogFullScreenInfo(void) = default; + +bool CGUIDialogFullScreenInfo::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_SHOW_INFO) + { + Close(); + return true; + } + return CGUIDialog::OnAction(action); +} + diff --git a/xbmc/video/dialogs/GUIDialogFullScreenInfo.h b/xbmc/video/dialogs/GUIDialogFullScreenInfo.h new file mode 100644 index 0000000..ca5b5e8 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogFullScreenInfo.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "guilib/GUIDialog.h" + +class CGUIDialogFullScreenInfo : + public CGUIDialog +{ +public: + CGUIDialogFullScreenInfo(void); + ~CGUIDialogFullScreenInfo(void) override; + bool OnAction(const CAction &action) override; +}; + diff --git a/xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp b/xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp new file mode 100644 index 0000000..24f2618 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogSubtitleSettings.h" + +#include "FileItem.h" +#include "GUIDialogSubtitles.h" +#include "GUIPassword.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "addons/Skin.h" +#include "addons/VFSEntry.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "cores/IPlayer.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "settings/lib/SettingDefinitions.h" +#include "settings/lib/SettingsManager.h" +#include "utils/FileUtils.h" +#include "utils/LangCodeExpander.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <string> +#include <vector> + +#define SETTING_SUBTITLE_ENABLE "subtitles.enable" +#define SETTING_SUBTITLE_DELAY "subtitles.delay" +#define SETTING_SUBTITLE_STREAM "subtitles.stream" +#define SETTING_SUBTITLE_BROWSER "subtitles.browser" +#define SETTING_SUBTITLE_SEARCH "subtitles.search" +#define SETTING_MAKE_DEFAULT "audio.makedefault" + +CGUIDialogSubtitleSettings::CGUIDialogSubtitleSettings() + : CGUIDialogSettingsManualBase(WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS, "DialogSettings.xml") +{ } + +CGUIDialogSubtitleSettings::~CGUIDialogSubtitleSettings() = default; + +void CGUIDialogSubtitleSettings::FrameMove() +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->HasPlayer()) + { + const CVideoSettings videoSettings = appPlayer->GetVideoSettings(); + + // these settings can change on the fly + //! @todo m_settingsManager->SetBool(SETTING_SUBTITLE_ENABLE, g_application.GetAppPlayer().GetSubtitleVisible()); + // \-> Unless subtitle visibility can change on the fly, while Dialog is up, this code should be removed. + GetSettingsManager()->SetNumber(SETTING_SUBTITLE_DELAY, + static_cast<double>(videoSettings.m_SubtitleDelay)); + //! @todo (needs special handling): m_settingsManager->SetInt(SETTING_SUBTITLE_STREAM, g_application.GetAppPlayer().GetSubtitle()); + } + + CGUIDialogSettingsManualBase::FrameMove(); +} + +bool CGUIDialogSubtitleSettings::OnMessage(CGUIMessage& message) +{ + if (message.GetMessage() == GUI_MSG_SUBTITLE_DOWNLOADED) + { + Close(); + } + return CGUIDialogSettingsManualBase::OnMessage(message); +} + +void CGUIDialogSubtitleSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + return; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_SUBTITLE_ENABLE) + { + bool value = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + if (value) + { + // Ensure that we use/store the subtitle stream the user currently sees in the dialog. + appPlayer->SetSubtitle(m_subtitleStream); + } + appPlayer->SetSubtitleVisible(value); + } + else if (settingId == SETTING_SUBTITLE_DELAY) + { + float value = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + appPlayer->SetSubTitleDelay(value); + } + else if (settingId == SETTING_SUBTITLE_STREAM) + { + m_subtitleStream = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + appPlayer->SetSubtitle(m_subtitleStream); + } +} + +std::string CGUIDialogSubtitleSettings::BrowseForSubtitle() +{ + std::string extras; + for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances()) + { + if (vfsAddon->ID() == "vfs.rar" || vfsAddon->ID() == "vfs.libarchive") + extras += '|' + vfsAddon->GetExtensions(); + } + + std::string strPath; + if (URIUtils::IsInRAR(g_application.CurrentFileItem().GetPath()) || URIUtils::IsInZIP(g_application.CurrentFileItem().GetPath())) + { + strPath = CURL(g_application.CurrentFileItem().GetPath()).GetHostName(); + } + else if (!URIUtils::IsPlugin(g_application.CurrentFileItem().GetPath())) + { + strPath = g_application.CurrentFileItem().GetPath(); + } + + std::string strMask = + ".utf|.utf8|.utf-8|.sub|.srt|.smi|.rt|.txt|.ssa|.aqt|.jss|.ass|.vtt|.idx|.zip|.sup"; + + if (g_application.GetCurrentPlayer() == "VideoPlayer") + strMask = ".srt|.zip|.ifo|.smi|.sub|.idx|.ass|.ssa|.vtt|.txt|.sup"; + + strMask += extras; + + VECSOURCES shares(*CMediaSourceSettings::GetInstance().GetSources("video")); + if (CMediaSettings::GetInstance().GetAdditionalSubtitleDirectoryChecked() != -1 && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH).empty()) + { + CMediaSource share; + std::vector<std::string> paths; + if (!strPath.empty()) + { + paths.push_back(URIUtils::GetDirectory(strPath)); + } + paths.push_back(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH)); + share.FromNameAndPaths("video",g_localizeStrings.Get(21367),paths); + shares.push_back(share); + strPath = share.strPath; + URIUtils::AddSlashAtEnd(strPath); + } + + if (CGUIDialogFileBrowser::ShowAndGetFile(shares, strMask, g_localizeStrings.Get(293), strPath, false, true)) // "subtitles" + { + if (URIUtils::HasExtension(strPath, ".sub")) + { + if (CFileUtils::Exists(URIUtils::ReplaceExtension(strPath, ".idx"))) + strPath = URIUtils::ReplaceExtension(strPath, ".idx"); + } + + return strPath; + } + + return ""; +} + +void CGUIDialogSubtitleSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + return; + + CGUIDialogSettingsManualBase::OnSettingAction(setting); + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_SUBTITLE_BROWSER) + { + std::string strPath = BrowseForSubtitle(); + if (!strPath.empty()) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->AddSubtitle(strPath); + Close(); + } + } + else if (settingId == SETTING_SUBTITLE_SEARCH) + { + auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSubtitles>(WINDOW_DIALOG_SUBTITLES); + if (dialog) + { + dialog->Open(); + m_subtitleStreamSetting->UpdateDynamicOptions(); + } + } + else if (settingId == SETTING_MAKE_DEFAULT) + Save(); +} + +bool CGUIDialogSubtitleSettings::Save() +{ + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (!g_passwordManager.CheckSettingLevelLock(SettingLevel::Expert) && + profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE) + return true; + + // prompt user if they are sure + if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{12376}, CVariant{12377})) + return true; + + // reset the settings + CVideoDatabase db; + if (!db.Open()) + return true; + + db.EraseAllVideoSettings(); + db.Close(); + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + CMediaSettings::GetInstance().GetDefaultVideoSettings() = appPlayer->GetVideoSettings(); + CMediaSettings::GetInstance().GetDefaultVideoSettings().m_SubtitleStream = -1; + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + return true; +} + +void CGUIDialogSubtitleSettings::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + + SetHeading(24133); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067); +} + +void CGUIDialogSubtitleSettings::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + const std::shared_ptr<CSettingCategory> category = AddCategory("audiosubtitlesettings", -1); + if (category == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings"); + return; + } + + // get all necessary setting groups + const std::shared_ptr<CSettingGroup> groupAudio = AddGroup(category); + if (groupAudio == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings"); + return; + } + const std::shared_ptr<CSettingGroup> groupSubtitles = AddGroup(category); + if (groupSubtitles == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings"); + return; + } + const std::shared_ptr<CSettingGroup> groupSaveAsDefault = AddGroup(category); + if (groupSaveAsDefault == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings"); + return; + } + + bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml"); + + const CVideoSettings videoSettings = appPlayer->GetVideoSettings(); + + if (appPlayer->HasPlayer()) + { + appPlayer->GetSubtitleCapabilities(m_subtitleCapabilities); + } + + // subtitle settings + m_subtitleVisible = appPlayer->GetSubtitleVisible(); + + // subtitle enabled setting + AddToggle(groupSubtitles, SETTING_SUBTITLE_ENABLE, 13397, SettingLevel::Basic, m_subtitleVisible); + + // subtitle delay setting + if (SupportsSubtitleFeature(IPC_SUBS_OFFSET)) + { + std::shared_ptr<CSettingNumber> settingSubtitleDelay = AddSlider(groupSubtitles, SETTING_SUBTITLE_DELAY, 22006, SettingLevel::Basic, videoSettings.m_SubtitleDelay, 0, -CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange, 0.1f, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange, 22006, usePopup); + std::static_pointer_cast<CSettingControlSlider>(settingSubtitleDelay->GetControl())->SetFormatter(SettingFormatterDelay); + } + + // subtitle stream setting + if (SupportsSubtitleFeature(IPC_SUBS_SELECT)) + AddSubtitleStreams(groupSubtitles, SETTING_SUBTITLE_STREAM); + + // subtitle browser setting + if (SupportsSubtitleFeature(IPC_SUBS_EXTERNAL)) + AddButton(groupSubtitles, SETTING_SUBTITLE_BROWSER, 13250, SettingLevel::Basic); + + AddButton(groupSubtitles, SETTING_SUBTITLE_SEARCH, 24134, SettingLevel::Basic); + + // subtitle stream setting + AddButton(groupSaveAsDefault, SETTING_MAKE_DEFAULT, 12376, SettingLevel::Basic); +} + +bool CGUIDialogSubtitleSettings::SupportsSubtitleFeature(int feature) +{ + for (auto item : m_subtitleCapabilities) + { + if (item == feature || item == IPC_SUBS_ALL) + return true; + } + return false; +} + +void CGUIDialogSubtitleSettings::AddSubtitleStreams(const std::shared_ptr<CSettingGroup>& group, + const std::string& settingId) +{ + if (group == NULL || settingId.empty()) + return; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + m_subtitleStream = appPlayer->GetSubtitle(); + if (m_subtitleStream < 0) + m_subtitleStream = 0; + + m_subtitleStreamSetting = AddList(group, settingId, 462, SettingLevel::Basic, m_subtitleStream, SubtitleStreamsOptionFiller, 462); +} + +void CGUIDialogSubtitleSettings::SubtitleStreamsOptionFiller( + const SettingConstPtr& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + int subtitleStreamCount = appPlayer->GetSubtitleCount(); + + // cycle through each subtitle and add it to our entry list + for (int i = 0; i < subtitleStreamCount; ++i) + { + SubtitleStreamInfo info; + appPlayer->GetSubtitleStreamInfo(i, info); + + std::string strItem; + std::string strLanguage; + + if (!g_LangCodeExpander.Lookup(info.language, strLanguage)) + strLanguage = g_localizeStrings.Get(13205); // Unknown + + if (info.name.length() == 0) + strItem = strLanguage; + else + strItem = StringUtils::Format("{} - {}", strLanguage, info.name); + + strItem += FormatFlags(info.flags); + strItem += StringUtils::Format(" ({}/{})", i + 1, subtitleStreamCount); + + list.emplace_back(strItem, i); + } + + // no subtitle streams - just add a "None" entry + if (list.empty()) + { + list.emplace_back(g_localizeStrings.Get(231), -1); + current = -1; + } +} + +std::string CGUIDialogSubtitleSettings::SettingFormatterDelay( + const std::shared_ptr<const CSettingControlSlider>& control, + const CVariant& value, + const CVariant& minimum, + const CVariant& step, + const CVariant& maximum) +{ + if (!value.isDouble()) + return ""; + + float fValue = value.asFloat(); + float fStep = step.asFloat(); + + if (fabs(fValue) < 0.5f * fStep) + return StringUtils::Format(g_localizeStrings.Get(22003), 0.0); + if (fValue < 0) + return StringUtils::Format(g_localizeStrings.Get(22004), fabs(fValue)); + + return StringUtils::Format(g_localizeStrings.Get(22005), fValue); +} + +std::string CGUIDialogSubtitleSettings::FormatFlags(StreamFlags flags) +{ + std::vector<std::string> localizedFlags; + if (flags & StreamFlags::FLAG_DEFAULT) + localizedFlags.emplace_back(g_localizeStrings.Get(39105)); + if (flags & StreamFlags::FLAG_FORCED) + localizedFlags.emplace_back(g_localizeStrings.Get(39106)); + if (flags & StreamFlags::FLAG_HEARING_IMPAIRED) + localizedFlags.emplace_back(g_localizeStrings.Get(39107)); + if (flags & StreamFlags::FLAG_VISUAL_IMPAIRED) + localizedFlags.emplace_back(g_localizeStrings.Get(39108)); + + std::string formated = StringUtils::Join(localizedFlags, ", "); + + if (!formated.empty()) + formated = StringUtils::Format(" [{}]", formated); + + return formated; +} diff --git a/xbmc/video/dialogs/GUIDialogSubtitleSettings.h b/xbmc/video/dialogs/GUIDialogSubtitleSettings.h new file mode 100644 index 0000000..65216ed --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogSubtitleSettings.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "cores/VideoPlayer/Interface/StreamInfo.h" +#include "settings/dialogs/GUIDialogSettingsManualBase.h" + +#include <string> +#include <utility> +#include <vector> + +class CVariant; +struct IntegerSettingOption; + +class CGUIDialogSubtitleSettings : public CGUIDialogSettingsManualBase +{ +public: + CGUIDialogSubtitleSettings(); + ~CGUIDialogSubtitleSettings() override; + bool OnMessage(CGUIMessage& message) override; + + // specialization of CGUIWindow + void FrameMove() override; + + static std::string BrowseForSubtitle(); + +protected: + // implementations of ISettingCallback + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + + // specialization of CGUIDialogSettingsBase + bool AllowResettingSettings() const override { return false; } + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + +private: + bool SupportsSubtitleFeature(int feature); + + void AddSubtitleStreams(const std::shared_ptr<CSettingGroup>& group, + const std::string& settingId); + + int m_subtitleStream; + bool m_subtitleVisible; + std::shared_ptr<CSettingInt> m_subtitleStreamSetting; + + std::vector<int> m_subtitleCapabilities; + static std::string FormatFlags(StreamFlags flags); + + static void SubtitleStreamsOptionFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + + static std::string SettingFormatterDelay( + const std::shared_ptr<const CSettingControlSlider>& control, + const CVariant& value, + const CVariant& minimum, + const CVariant& step, + const CVariant& maximum); +}; diff --git a/xbmc/video/dialogs/GUIDialogSubtitles.cpp b/xbmc/video/dialogs/GUIDialogSubtitles.cpp new file mode 100644 index 0000000..600c6da --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogSubtitles.cpp @@ -0,0 +1,703 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogSubtitles.h" + +#include "FileItem.h" +#include "LangInfo.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "addons/AddonManager.h" +#include "addons/addoninfo/AddonInfo.h" +#include "addons/addoninfo/AddonType.h" +#include "addons/gui/GUIDialogAddonSettings.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "cores/IPlayer.h" +#include "dialogs/GUIDialogContextMenu.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "filesystem/AddonsDirectory.h" +#include "filesystem/Directory.h" +#include "filesystem/File.h" +#include "filesystem/SpecialProtocol.h" +#include "filesystem/StackDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/ActionIDs.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "utils/JobManager.h" +#include "utils/LangCodeExpander.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <mutex> + +using namespace ADDON; +using namespace XFILE; + +namespace +{ +constexpr int CONTROL_NAMELABEL = 100; +constexpr int CONTROL_NAMELOGO = 110; +constexpr int CONTROL_SUBLIST = 120; +constexpr int CONTROL_SUBSEXIST = 130; +constexpr int CONTROL_SUBSTATUS = 140; +constexpr int CONTROL_SERVICELIST = 150; +constexpr int CONTROL_MANUALSEARCH = 160; + +enum class SUBTITLE_SERVICE_CONTEXT_BUTTONS +{ + ADDON_SETTINGS, + ADDON_DISABLE +}; +} // namespace + +/*! \brief simple job to retrieve a directory and store a string (language) + */ +class CSubtitlesJob: public CJob +{ +public: + CSubtitlesJob(const CURL &url, const std::string &language) : m_url(url), m_language(language) + { + m_items = new CFileItemList; + } + ~CSubtitlesJob() override + { + delete m_items; + } + bool DoWork() override + { + CDirectory::GetDirectory(m_url.Get(), *m_items, "", DIR_FLAG_DEFAULTS); + return true; + } + bool operator==(const CJob *job) const override + { + if (strcmp(job->GetType(),GetType()) == 0) + { + const CSubtitlesJob* rjob = dynamic_cast<const CSubtitlesJob*>(job); + if (rjob) + { + return m_url.Get() == rjob->m_url.Get() && + m_language == rjob->m_language; + } + } + return false; + } + const CFileItemList *GetItems() const { return m_items; } + const CURL &GetURL() const { return m_url; } + const std::string &GetLanguage() const { return m_language; } +private: + CURL m_url; + CFileItemList *m_items; + std::string m_language; +}; + +CGUIDialogSubtitles::CGUIDialogSubtitles(void) + : CGUIDialog(WINDOW_DIALOG_SUBTITLES, "DialogSubtitles.xml") + , m_subtitles(new CFileItemList) + , m_serviceItems(new CFileItemList) +{ + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogSubtitles::~CGUIDialogSubtitles(void) +{ + CancelJobs(); + delete m_subtitles; + delete m_serviceItems; +} + +bool CGUIDialogSubtitles::OnMessage(CGUIMessage& message) +{ + if (message.GetMessage() == GUI_MSG_CLICKED) + { + int iControl = message.GetSenderId(); + bool selectAction = (message.GetParam1() == ACTION_SELECT_ITEM || + message.GetParam1() == ACTION_MOUSE_LEFT_CLICK); + + bool contextMenuAction = (message.GetParam1() == ACTION_CONTEXT_MENU || + message.GetParam1() == ACTION_MOUSE_RIGHT_CLICK); + + if (selectAction && iControl == CONTROL_SUBLIST) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SUBLIST); + OnMessage(msg); + + int item = msg.GetParam1(); + if (item >= 0 && item < m_subtitles->Size()) + Download(*m_subtitles->Get(item)); + return true; + } + else if (selectAction && iControl == CONTROL_SERVICELIST) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SERVICELIST); + OnMessage(msg); + + int item = msg.GetParam1(); + if (item >= 0 && item < m_serviceItems->Size()) + { + SetService(m_serviceItems->Get(item)->GetProperty("Addon.ID").asString()); + Search(); + } + return true; + } + else if (contextMenuAction && iControl == CONTROL_SERVICELIST) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SERVICELIST); + OnMessage(msg); + + const int itemIdx = msg.GetParam1(); + if (itemIdx >= 0 && itemIdx < m_serviceItems->Size()) + { + OnSubtitleServiceContextMenu(itemIdx); + } + } + else if (iControl == CONTROL_MANUALSEARCH) + { + //manual search + if (CGUIKeyboardFactory::ShowAndGetInput(m_strManualSearch, CVariant{g_localizeStrings.Get(24121)}, true)) + { + Search(m_strManualSearch); + return true; + } + } + } + else if (message.GetMessage() == GUI_MSG_WINDOW_DEINIT) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + // Resume the video if the user has requested it + if (appPlayer->IsPaused() && m_pausedOnRun) + appPlayer->Pause(); + + CGUIDialog::OnMessage(message); + + ClearSubtitles(); + ClearServices(); + return true; + } + return CGUIDialog::OnMessage(message); +} + +void CGUIDialogSubtitles::OnInitWindow() +{ + // Pause the video if the user has requested it + m_pausedOnRun = false; + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_SUBTITLES_PAUSEONSEARCH) && + !appPlayer->IsPaused()) + { + appPlayer->Pause(); + m_pausedOnRun = true; + } + + FillServices(); + CGUIWindow::OnInitWindow(); + Search(); +} + +void CGUIDialogSubtitles::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) +{ + if (m_bInvalidated) + { + // take copies of our variables to ensure we don't hold the lock for long. + std::string status; + CFileItemList subs; + { + std::unique_lock<CCriticalSection> lock(m_critsection); + status = m_status; + subs.Assign(*m_subtitles); + } + SET_CONTROL_LABEL(CONTROL_SUBSTATUS, status); + + if (m_updateSubsList) + { + CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_SUBLIST, 0, 0, &subs); + OnMessage(message); + if (!subs.IsEmpty()) + { + // focus subtitles list + CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), CONTROL_SUBLIST); + OnMessage(msg); + } + m_updateSubsList = false; + } + + int control = GetFocusedControlID(); + // nothing has focus + if (!control) + { + CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), m_subtitles->IsEmpty() ? + CONTROL_SERVICELIST : CONTROL_SUBLIST); + OnMessage(msg); + } + // subs list is focused but we have no subs + else if (control == CONTROL_SUBLIST && m_subtitles->IsEmpty()) + { + CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), CONTROL_SERVICELIST); + OnMessage(msg); + } + } + CGUIDialog::Process(currentTime, dirtyregions); +} + +void CGUIDialogSubtitles::FillServices() +{ + ClearServices(); + + VECADDONS addons; + CServiceBroker::GetAddonMgr().GetAddons(addons, AddonType::SUBTITLE_MODULE); + + if (addons.empty()) + { + UpdateStatus(NO_SERVICES); + return; + } + + std::string defaultService; + const CFileItem &item = g_application.CurrentUnstackedItem(); + if (item.GetVideoContentType() == VideoDbContentType::TVSHOWS || + item.GetVideoContentType() == VideoDbContentType::EPISODES) + // Set default service for tv shows + defaultService = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_TV); + else + // Set default service for filemode and movies + defaultService = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_MOVIE); + + std::string service = addons.front()->ID(); + for (VECADDONS::const_iterator addonIt = addons.begin(); addonIt != addons.end(); ++addonIt) + { + CFileItemPtr item(CAddonsDirectory::FileItemFromAddon(*addonIt, "plugin://" + (*addonIt)->ID(), false)); + m_serviceItems->Add(item); + if ((*addonIt)->ID() == defaultService) + service = (*addonIt)->ID(); + } + + // Bind our services to the UI + CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_SERVICELIST, 0, 0, m_serviceItems); + OnMessage(msg); + + SetService(service); +} + +bool CGUIDialogSubtitles::SetService(const std::string &service) +{ + if (service != m_currentService) + { + m_currentService = service; + CLog::Log(LOGDEBUG, "New Service [{}] ", m_currentService); + + CFileItemPtr currentService = GetService(); + // highlight this item in the skin + for (int i = 0; i < m_serviceItems->Size(); i++) + { + CFileItemPtr pItem = m_serviceItems->Get(i); + pItem->Select(pItem == currentService); + } + + SET_CONTROL_LABEL(CONTROL_NAMELABEL, currentService->GetLabel()); + + if (currentService->HasAddonInfo()) + { + std::string icon = URIUtils::AddFileToFolder(currentService->GetAddonInfo()->Path(), "logo.png"); + SET_CONTROL_FILENAME(CONTROL_NAMELOGO, icon); + } + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->GetSubtitleCount() == 0) + SET_CONTROL_HIDDEN(CONTROL_SUBSEXIST); + else + SET_CONTROL_VISIBLE(CONTROL_SUBSEXIST); + + return true; + } + return false; +} + +const CFileItemPtr CGUIDialogSubtitles::GetService() const +{ + for (int i = 0; i < m_serviceItems->Size(); i++) + { + if (m_serviceItems->Get(i)->GetProperty("Addon.ID") == m_currentService) + return m_serviceItems->Get(i); + } + return CFileItemPtr(); +} + +void CGUIDialogSubtitles::Search(const std::string &search/*=""*/) +{ + if (m_currentService.empty()) + return; // no services available + + UpdateStatus(SEARCHING); + ClearSubtitles(); + + CURL url("plugin://" + m_currentService + "/"); + if (!search.empty()) + { + url.SetOption("action", "manualsearch"); + url.SetOption("searchstring", search); + } + else + url.SetOption("action", "search"); + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + SettingConstPtr setting = settings->GetSetting(CSettings::SETTING_SUBTITLES_LANGUAGES); + if (setting) + url.SetOption("languages", setting->ToString()); + + // Check for stacking + if (g_application.CurrentFileItem().IsStack()) + url.SetOption("stack", "1"); + + std::string preferredLanguage = settings->GetString(CSettings::SETTING_LOCALE_SUBTITLELANGUAGE); + + if (StringUtils::EqualsNoCase(preferredLanguage, "original")) + { + AudioStreamInfo info; + std::string strLanguage; + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->GetAudioStreamInfo(CURRENT_STREAM, info); + + if (!g_LangCodeExpander.Lookup(info.language, strLanguage)) + strLanguage = "Unknown"; + + preferredLanguage = strLanguage; + } + else if (StringUtils::EqualsNoCase(preferredLanguage, "default")) + preferredLanguage = g_langInfo.GetEnglishLanguageName(); + + url.SetOption("preferredlanguage", preferredLanguage); + + AddJob(new CSubtitlesJob(url, "")); +} + +void CGUIDialogSubtitles::OnJobComplete(unsigned int jobID, bool success, CJob *job) +{ + const CURL &url = static_cast<CSubtitlesJob*>(job)->GetURL(); + const CFileItemList *items = static_cast<CSubtitlesJob*>(job)->GetItems(); + const std::string &language = static_cast<CSubtitlesJob*>(job)->GetLanguage(); + if (url.GetOption("action") == "search" || url.GetOption("action") == "manualsearch") + OnSearchComplete(items); + else + OnDownloadComplete(items, language); + CJobQueue::OnJobComplete(jobID, success, job); +} + +void CGUIDialogSubtitles::OnSearchComplete(const CFileItemList *items) +{ + std::unique_lock<CCriticalSection> lock(m_critsection); + m_subtitles->Assign(*items); + UpdateStatus(SEARCH_COMPLETE); + m_updateSubsList = true; + MarkDirtyRegion(); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!items->IsEmpty() && appPlayer->GetSubtitleCount() == 0 && + m_LastAutoDownloaded != g_application.CurrentFile() && + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_SUBTITLES_DOWNLOADFIRST)) + { + CFileItemPtr item = items->Get(0); + CLog::Log(LOGDEBUG, "{} - Automatically download first subtitle: {}", __FUNCTION__, + item->GetLabel2()); + m_LastAutoDownloaded = g_application.CurrentFile(); + Download(*item); + } + + SetInvalid(); +} + +void CGUIDialogSubtitles::OnSubtitleServiceContextMenu(int itemIdx) +{ + const auto service = m_serviceItems->Get(itemIdx); + + CContextButtons buttons; + // Subtitle addon settings + buttons.Add(static_cast<int>(SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_SETTINGS), + g_localizeStrings.Get(21417)); + // Disable addon + buttons.Add(static_cast<int>(SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_DISABLE), + g_localizeStrings.Get(24021)); + + auto idx = static_cast<SUBTITLE_SERVICE_CONTEXT_BUTTONS>(CGUIDialogContextMenu::Show(buttons)); + switch (idx) + { + case SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_SETTINGS: + { + AddonPtr addon; + if (CServiceBroker::GetAddonMgr().GetAddon(service->GetProperty("Addon.ID").asString(), addon, + AddonType::SUBTITLE_MODULE, + OnlyEnabled::CHOICE_YES)) + { + CGUIDialogAddonSettings::ShowForAddon(addon); + } + else + { + CLog::Log(LOGERROR, "{} - Could not open settings for addon: {}", __FUNCTION__, + service->GetProperty("Addon.ID").asString()); + } + break; + } + case SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_DISABLE: + { + CServiceBroker::GetAddonMgr().DisableAddon(service->GetProperty("Addon.ID").asString(), + AddonDisabledReason::USER); + const bool currentActiveServiceWasDisabled = + m_currentService == service->GetProperty("Addon.ID").asString(); + FillServices(); + // restart search if the current active service was disabled + if (currentActiveServiceWasDisabled && !m_serviceItems->IsEmpty()) + { + Search(); + } + // if no more services are available make sure the subtitle list is cleaned up + else if (m_serviceItems->IsEmpty()) + { + ClearSubtitles(); + } + break; + } + default: + break; + } +} + +void CGUIDialogSubtitles::UpdateStatus(STATUS status) +{ + std::unique_lock<CCriticalSection> lock(m_critsection); + std::string label; + switch (status) + { + case NO_SERVICES: + label = g_localizeStrings.Get(24114); + break; + case SEARCHING: + label = g_localizeStrings.Get(24107); + break; + case SEARCH_COMPLETE: + if (!m_subtitles->IsEmpty()) + label = StringUtils::Format(g_localizeStrings.Get(24108), m_subtitles->Size()); + else + label = g_localizeStrings.Get(24109); + break; + case DOWNLOADING: + label = g_localizeStrings.Get(24110); + break; + default: + break; + } + if (label != m_status) + { + m_status = label; + SetInvalid(); + } +} + +void CGUIDialogSubtitles::Download(const CFileItem &subtitle) +{ + UpdateStatus(DOWNLOADING); + + // subtitle URL should be of the form plugin://<addonid>/?param=foo¶m=bar + // we just append (if not already present) the action=download parameter. + CURL url(subtitle.GetURL()); + if (url.GetOption("action").empty()) + url.SetOption("action", "download"); + + AddJob(new CSubtitlesJob(url, subtitle.GetLabel())); +} + +void CGUIDialogSubtitles::OnDownloadComplete(const CFileItemList *items, const std::string &language) +{ + if (items->IsEmpty()) + { + CFileItemPtr service = GetService(); + if (service) + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, service->GetLabel(), g_localizeStrings.Get(24113)); + UpdateStatus(SEARCH_COMPLETE); + return; + } + + SUBTITLE_STORAGEMODE storageMode = (SUBTITLE_STORAGEMODE) CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SUBTITLES_STORAGEMODE); + + // Get (unstacked) path + std::string strCurrentFile = g_application.CurrentUnstackedItem().GetDynPath(); + + std::string strDownloadPath = "special://temp"; + std::string strDestPath; + std::vector<std::string> vecFiles; + + std::string strCurrentFilePath; + if (StringUtils::StartsWith(strCurrentFilePath, "http://")) + { + strCurrentFile = "TempSubtitle"; + vecFiles.push_back(strCurrentFile); + } + else + { + std::string subPath = CSpecialProtocol::TranslatePath("special://subtitles"); + if (!subPath.empty()) + strDownloadPath = subPath; + + /** Get item's folder for sub storage, special case for RAR/ZIP items + * @todo We need some way to avoid special casing this all over the place + * for rar/zip (perhaps modify GetDirectory?) + */ + if (URIUtils::IsInRAR(strCurrentFile) || URIUtils::IsInZIP(strCurrentFile)) + strCurrentFilePath = URIUtils::GetDirectory(CURL(strCurrentFile).GetHostName()); + else + strCurrentFilePath = URIUtils::GetDirectory(strCurrentFile); + + // Handle stacks + if (g_application.CurrentFileItem().IsStack() && items->Size() > 1) + { + CStackDirectory::GetPaths(g_application.CurrentFileItem().GetPath(), vecFiles); + // Make sure (stack) size is the same as the size of the items handed to us, else fallback to single item + if (items->Size() != (int) vecFiles.size()) + { + vecFiles.clear(); + vecFiles.push_back(strCurrentFile); + } + } + else + { + vecFiles.push_back(strCurrentFile); + } + + if (storageMode == SUBTITLE_STORAGEMODE_MOVIEPATH && + CUtil::SupportsWriteFileOperations(strCurrentFilePath)) + { + strDestPath = strCurrentFilePath; + } + } + + // Use fallback? + if (strDestPath.empty()) + strDestPath = strDownloadPath; + + // Extract the language and appropriate extension + std::string strSubLang; + g_LangCodeExpander.ConvertToISO6391(language, strSubLang); + + // Iterate over all items to transfer + for (unsigned int i = 0; i < vecFiles.size() && i < (unsigned int) items->Size(); i++) + { + std::string strUrl = items->Get(i)->GetPath(); + std::string strFileName = URIUtils::GetFileName(vecFiles[i]); + URIUtils::RemoveExtension(strFileName); + + // construct subtitle path + std::string strSubExt = URIUtils::GetExtension(strUrl); + std::string strSubName = StringUtils::Format("{}.{}{}", strFileName, strSubLang, strSubExt); + + // Handle URL encoding: + std::string strDownloadFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubName, strDownloadPath); + std::string strDestFile = strDownloadFile; + + if (!CFile::Copy(strUrl, strDownloadFile)) + { + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, strSubName, g_localizeStrings.Get(24113)); + CLog::Log(LOGERROR, "{} - Saving of subtitle {} to {} failed", __FUNCTION__, strUrl, + strDownloadFile); + } + else + { + if (strDestPath != strDownloadPath) + { + // Handle URL encoding: + std::string strTryDestFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubName, strDestPath); + + /* Copy the file from temp to our final destination, if that fails fallback to download path + * (ie. special://subtitles or use special://temp). Note that after the first item strDownloadPath equals strDestpath + * so that all remaining items (including the .idx below) are copied directly to their final destination and thus all + * items end up in the same folder + */ + CLog::Log(LOGDEBUG, "{} - Saving subtitle {} to {}", __FUNCTION__, strDownloadFile, + strTryDestFile); + if (CFile::Copy(strDownloadFile, strTryDestFile)) + { + CFile::Delete(strDownloadFile); + strDestFile = strTryDestFile; + strDownloadPath = strDestPath; // Update download path so all the other items get directly downloaded to our final destination + } + else + { + CLog::Log(LOGWARNING, "{} - Saving of subtitle {} to {} failed. Falling back to {}", + __FUNCTION__, strDownloadFile, strTryDestFile, strDownloadPath); + strDestPath = strDownloadPath; // Copy failed, use fallback for the rest of the items + } + } + else + { + CLog::Log(LOGDEBUG, "{} - Saved subtitle {} to {}", __FUNCTION__, strUrl, strDownloadFile); + } + + // for ".sub" subtitles we check if ".idx" counterpart exists and copy that as well + if (StringUtils::EqualsNoCase(strSubExt, ".sub")) + { + strUrl = URIUtils::ReplaceExtension(strUrl, ".idx"); + if(CFile::Exists(strUrl)) + { + std::string strSubNameIdx = StringUtils::Format("{}.{}.idx", strFileName, strSubLang); + // Handle URL encoding: + strDestFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubNameIdx, strDestPath); + CFile::Copy(strUrl, strDestFile); + } + } + + // Set sub for currently playing (stack) item + if (vecFiles[i] == strCurrentFile) + SetSubtitles(strDestFile); + } + } + + // Notify window manager that a subtitle was downloaded + CGUIMessage msg(GUI_MSG_SUBTITLE_DOWNLOADED, 0, 0); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); + + // Close the window + Close(); +} + +void CGUIDialogSubtitles::ClearSubtitles() +{ + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_SUBLIST); + OnMessage(msg); + std::unique_lock<CCriticalSection> lock(m_critsection); + m_subtitles->Clear(); +} + +void CGUIDialogSubtitles::ClearServices() +{ + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_SERVICELIST); + OnMessage(msg); + m_serviceItems->Clear(); + m_currentService.clear(); +} + +void CGUIDialogSubtitles::SetSubtitles(const std::string &subtitle) +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->AddSubtitle(subtitle); +} diff --git a/xbmc/video/dialogs/GUIDialogSubtitles.h b/xbmc/video/dialogs/GUIDialogSubtitles.h new file mode 100644 index 0000000..043a613 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogSubtitles.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "guilib/GUIDialog.h" +#include "threads/CriticalSection.h" +#include "utils/JobManager.h" + +#include <string> + +enum SUBTITLE_STORAGEMODE +{ + SUBTITLE_STORAGEMODE_MOVIEPATH = 0, + SUBTITLE_STORAGEMODE_CUSTOMPATH +}; + +class CFileItem; +class CFileItemList; + +class CGUIDialogSubtitles : public CGUIDialog, CJobQueue +{ +public: + CGUIDialogSubtitles(void); + ~CGUIDialogSubtitles(void) override; + bool OnMessage(CGUIMessage& message) override; + void OnInitWindow() override; + +protected: + void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override; + void OnJobComplete(unsigned int jobID, bool success, CJob *job) override; + + bool SetService(const std::string &service); + const CFileItemPtr GetService() const; + void FillServices(); + void ClearServices(); + void ClearSubtitles(); + + enum STATUS { NO_SERVICES = 0, SEARCHING, SEARCH_COMPLETE, DOWNLOADING }; + void UpdateStatus(STATUS status); + + void Search(const std::string &search=""); + void OnSearchComplete(const CFileItemList *items); + + void Download(const CFileItem &subtitle); + void OnDownloadComplete(const CFileItemList *items, const std::string &language); + + + /*! + \brief Called when the context menu is requested on a subtitle service + present on the list of installed subtitle addons + \param itemIdx the index of the selected subtitle service on the list + */ + void OnSubtitleServiceContextMenu(int itemIdx); + + void SetSubtitles(const std::string &subtitle); + + CCriticalSection m_critsection; + CFileItemList* m_subtitles; + CFileItemList* m_serviceItems; + std::string m_currentService; + std::string m_status; + std::string m_strManualSearch; + bool m_pausedOnRun = false; + bool m_updateSubsList = false; ///< true if we need to update our subs list + std::string m_LastAutoDownloaded; ///< Last video file path which automatically downloaded subtitle +}; diff --git a/xbmc/video/dialogs/GUIDialogTeletext.cpp b/xbmc/video/dialogs/GUIDialogTeletext.cpp new file mode 100644 index 0000000..01e86cb --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogTeletext.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogTeletext.h" + +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "guilib/GUIMessage.h" +#include "guilib/GUITexture.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/Texture.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/ColorUtils.h" +#include "utils/log.h" + +static int teletextFadeAmount = 0; + +CGUIDialogTeletext::CGUIDialogTeletext() + : CGUIDialog(WINDOW_DIALOG_OSD_TELETEXT, ""), m_pTxtTexture(nullptr) +{ + m_renderOrder = RENDER_ORDER_DIALOG_TELETEXT; +} + +CGUIDialogTeletext::~CGUIDialogTeletext() = default; + +bool CGUIDialogTeletext::OnAction(const CAction& action) +{ + if (m_TextDecoder.HandleAction(action)) + { + MarkDirtyRegion(); + return true; + } + + return CGUIDialog::OnAction(action); +} + +bool CGUIDialogTeletext::OnBack(int actionID) +{ + m_bClose = true; + MarkDirtyRegion(); + return true; +} + +bool CGUIDialogTeletext::OnMessage(CGUIMessage& message) +{ + if (message.GetMessage() == GUI_MSG_WINDOW_INIT) + { + /* Do not open if no teletext is available */ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!appPlayer->HasTeletextCache()) + { + Close(); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(23049), "", 1500, false); + return true; + } + } + else if (message.GetMessage() == GUI_MSG_NOTIFY_ALL) + { + if (message.GetParam1() == GUI_MSG_WINDOW_RESIZE) + { + SetCoordinates(); + } + } + return CGUIDialog::OnMessage(message); +} + +void CGUIDialogTeletext::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) +{ + CGUIDialog::Process(currentTime, dirtyregions); + m_renderRegion = m_vertCoords; +} + +void CGUIDialogTeletext::Render() +{ + // Do not render if we have no texture + if (!m_pTxtTexture) + { + CLog::Log(LOGERROR, "CGUITeletextBox::Render called without texture"); + return; + } + + m_TextDecoder.RenderPage(); + + if (!m_bClose) + { + if (teletextFadeAmount < 100) + { + teletextFadeAmount = std::min(100, teletextFadeAmount + 5); + MarkDirtyRegion(); + } + } + else + { + if (teletextFadeAmount > 0) + { + teletextFadeAmount = std::max(0, teletextFadeAmount - 10); + MarkDirtyRegion(); + } + + if (teletextFadeAmount == 0) + Close(); + } + + unsigned char* textureBuffer = (unsigned char*)m_TextDecoder.GetTextureBuffer(); + if (!m_bClose && m_TextDecoder.NeedRendering() && textureBuffer) + { + m_pTxtTexture->Update(m_TextDecoder.GetWidth(), m_TextDecoder.GetHeight(), m_TextDecoder.GetWidth()*4, XB_FMT_A8R8G8B8, textureBuffer, false); + m_TextDecoder.RenderingDone(); + MarkDirtyRegion(); + } + + UTILS::COLOR::Color color = + (static_cast<UTILS::COLOR::Color>(teletextFadeAmount * 2.55f) & 0xff) << 24 | 0xFFFFFF; + CGUITexture::DrawQuad(m_vertCoords, color, m_pTxtTexture.get()); + + CGUIDialog::Render(); +} + +void CGUIDialogTeletext::OnInitWindow() +{ + teletextFadeAmount = 0; + m_bClose = false; + m_windowLoaded = true; + + SetCoordinates(); + + if (!m_TextDecoder.InitDecoder()) + { + CLog::Log(LOGERROR, "{}: failed to init teletext decoder", __FUNCTION__); + Close(); + } + + m_pTxtTexture = + CTexture::CreateTexture(m_TextDecoder.GetWidth(), m_TextDecoder.GetHeight(), XB_FMT_A8R8G8B8); + if (!m_pTxtTexture) + { + CLog::Log(LOGERROR, "{}: failed to create texture", __FUNCTION__); + Close(); + } + + CGUIDialog::OnInitWindow(); +} + +void CGUIDialogTeletext::OnDeinitWindow(int nextWindowID) +{ + m_windowLoaded = false; + m_TextDecoder.EndDecoder(); + + m_pTxtTexture.reset(); + + CGUIDialog::OnDeinitWindow(nextWindowID); +} + +void CGUIDialogTeletext::SetCoordinates() +{ + float left, right, top, bottom; + + CServiceBroker::GetWinSystem()->GetGfxContext().SetScalingResolution(m_coordsRes, m_needsScaling); + + left = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(0, 0); + right = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord((float)m_coordsRes.iWidth, 0); + top = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(0, 0); + bottom = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(0, (float)m_coordsRes.iHeight); + + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_TELETEXTSCALE)) + { + /* Fixed aspect ratio to 4:3 for teletext */ + float width = right - left; + float height = bottom - top; + if (width / 4 > height / 3) + { + left = (width - height * 4 / 3) / 2; + right = width - left; + } + else + { + top = (height - width * 3 / 4) / 2; + bottom = height - top; + } + } + + m_vertCoords.SetRect(left, + top, + right, + bottom); + + MarkDirtyRegion(); +} diff --git a/xbmc/video/dialogs/GUIDialogTeletext.h b/xbmc/video/dialogs/GUIDialogTeletext.h new file mode 100644 index 0000000..4a60ec0 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogTeletext.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "guilib/GUIDialog.h" +#include "video/Teletext.h" + +#include <memory> + +class CTexture; + +class CGUIDialogTeletext : public CGUIDialog +{ +public: + CGUIDialogTeletext(void); + ~CGUIDialogTeletext(void) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction& action) override; + bool OnBack(int actionID) override; + void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override; + void Render() override; + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + +protected: + bool m_bClose; /* Close sendet, needed for fade out */ + std::unique_ptr<CTexture> m_pTxtTexture; /* Texture info class to render to screen */ + CRect m_vertCoords; /* Coordinates of teletext field on screen */ + CTeletextDecoder m_TextDecoder; /* Decoding class for teletext code */ + +private: + void SetCoordinates(); +}; diff --git a/xbmc/video/dialogs/GUIDialogVideoBookmarks.cpp b/xbmc/video/dialogs/GUIDialogVideoBookmarks.cpp new file mode 100644 index 0000000..f2669d3 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoBookmarks.cpp @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogVideoBookmarks.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "Util.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogContextMenu.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pictures/Picture.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Crc32.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" +#include "video/VideoThumbLoader.h" +#include "view/ViewState.h" + +#include <mutex> +#include <string> +#include <vector> + +#define BOOKMARK_THUMB_WIDTH CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes + +#define CONTROL_ADD_BOOKMARK 2 +#define CONTROL_CLEAR_BOOKMARKS 3 +#define CONTROL_ADD_EPISODE_BOOKMARK 4 + +#define CONTROL_THUMBS 11 + +CGUIDialogVideoBookmarks::CGUIDialogVideoBookmarks() + : CGUIDialog(WINDOW_DIALOG_VIDEO_BOOKMARKS, "VideoOSDBookmarks.xml"), + CJobQueue(false, 1, CJob::PRIORITY_NORMAL) +{ + m_vecItems = new CFileItemList; + m_loadType = LOAD_EVERY_TIME; + m_jobsStarted = 0; +} + +CGUIDialogVideoBookmarks::~CGUIDialogVideoBookmarks() +{ + delete m_vecItems; +} + +bool CGUIDialogVideoBookmarks::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_DEINIT: + { + CUtil::DeleteVideoDatabaseDirectoryCache(); + Clear(); + } + break; + + case GUI_MSG_WINDOW_INIT: + { + // don't init this dialog if we don't playback a file + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!appPlayer->IsPlaying()) + return false; + + CGUIWindow::OnMessage(message); + Update(); + return true; + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_ADD_BOOKMARK) + { + AddBookmark(); + Update(); + } + else if (iControl == CONTROL_CLEAR_BOOKMARKS) + { + ClearBookmarks(); + } + else if (iControl == CONTROL_ADD_EPISODE_BOOKMARK) + { + AddEpisodeBookmark(); + Update(); + } + else if (m_viewControl.HasControl(iControl)) // list/thumb control + { + int iItem = m_viewControl.GetSelectedItem(); + int iAction = message.GetParam1(); + if (iAction == ACTION_DELETE_ITEM) + { + Delete(iItem); + } + else if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK) + { + GotoBookmark(iItem); + } + } + } + break; + case GUI_MSG_SETFOCUS: + { + if (m_viewControl.HasControl(message.GetControlId()) && m_viewControl.GetCurrentControl() != message.GetControlId()) + { + m_viewControl.SetFocused(); + return true; + } + } + break; + case GUI_MSG_REFRESH_LIST: + { + switch (message.GetParam1()) + { + case 0: + OnRefreshList(); + break; + case 1: + UpdateItem(message.GetParam2()); + break; + default: + break; + } + } + break; + } + + return CGUIDialog::OnMessage(message); +} + +bool CGUIDialogVideoBookmarks::OnAction(const CAction &action) +{ + switch(action.GetID()) + { + case ACTION_CONTEXT_MENU: + case ACTION_MOUSE_RIGHT_CLICK: + { + OnPopupMenu(m_viewControl.GetSelectedItem()); + return true; + } + } + return CGUIDialog::OnAction(action); +} + + +void CGUIDialogVideoBookmarks::OnPopupMenu(int item) +{ + if (item < 0 || item >= (int) m_bookmarks.size()) + return; + + // highlight the item + (*m_vecItems)[item]->Select(true); + + CContextButtons choices; + choices.Add(1, (m_bookmarks[item].type == CBookmark::EPISODE ? 20405 : 20404)); // "Remove episode bookmark" or "Remove bookmark" + + int button = CGUIDialogContextMenu::ShowAndGetChoice(choices); + + // unhighlight the item + (*m_vecItems)[item]->Select(false); + + if (button == 1) + Delete(item); +} + +void CGUIDialogVideoBookmarks::Delete(int item) +{ + if ( item>=0 && (unsigned)item < m_bookmarks.size() ) + { + CVideoDatabase videoDatabase; + videoDatabase.Open(); + std::string path(g_application.CurrentFile()); + if (g_application.CurrentFileItem().HasProperty("original_listitem_url") && + !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString())) + path = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString(); + videoDatabase.ClearBookMarkOfFile(path, m_bookmarks[item], m_bookmarks[item].type); + videoDatabase.Close(); + CUtil::DeleteVideoDatabaseDirectoryCache(); + } + Update(); +} + +void CGUIDialogVideoBookmarks::UpdateItem(unsigned int chapterIdx) +{ + std::unique_lock<CCriticalSection> lock(m_refreshSection); + + int itemPos = 0; + for (const auto& item : *m_vecItems) + { + if (chapterIdx == item->GetProperty("chapter").asInteger()) + break; + itemPos++; + } + + if (itemPos < m_vecItems->Size()) + { + std::string time = StringUtils::Format("chapter://{}/{}", m_filePath, chapterIdx); + std::string cachefile = CServiceBroker::GetTextureCache()->GetCachedPath( + CServiceBroker::GetTextureCache()->GetCacheFile(time) + ".jpg"); + if (CFileUtils::Exists(cachefile)) + { + (*m_vecItems)[itemPos]->SetArt("thumb", cachefile); + } + } +} + +void CGUIDialogVideoBookmarks::OnRefreshList() +{ + m_bookmarks.clear(); + std::vector<CFileItemPtr> items; + + // open the d/b and retrieve the bookmarks for the current movie + m_filePath = g_application.CurrentFile(); + if (g_application.CurrentFileItem().HasProperty("original_listitem_url") && + !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString())) + m_filePath = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString(); + + CVideoDatabase videoDatabase; + videoDatabase.Open(); + videoDatabase.GetBookMarksForFile(m_filePath, m_bookmarks); + videoDatabase.GetBookMarksForFile(m_filePath, m_bookmarks, CBookmark::EPISODE, true); + videoDatabase.Close(); + + std::unique_lock<CCriticalSection> lock(m_refreshSection); + m_vecItems->Clear(); + + // cycle through each stored bookmark and add it to our list control + for (unsigned int i = 0; i < m_bookmarks.size(); ++i) + { + std::string bookmarkTime; + if (m_bookmarks[i].type == CBookmark::EPISODE) + bookmarkTime = StringUtils::Format("{} {} {} {}", g_localizeStrings.Get(20373), + m_bookmarks[i].seasonNumber, g_localizeStrings.Get(20359), + m_bookmarks[i].episodeNumber); + else + bookmarkTime = StringUtils::SecondsToTimeString((long)m_bookmarks[i].timeInSeconds, TIME_FORMAT_HH_MM_SS); + + CFileItemPtr item(new CFileItem(StringUtils::Format(g_localizeStrings.Get(299), i + 1))); + item->SetLabel2(bookmarkTime); + item->SetArt("thumb", m_bookmarks[i].thumbNailImage); + item->SetProperty("resumepoint", m_bookmarks[i].timeInSeconds); + item->SetProperty("playerstate", m_bookmarks[i].playerState); + item->SetProperty("isbookmark", "true"); + items.push_back(item); + } + + // add chapters if around + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + for (int i = 1; i <= appPlayer->GetChapterCount(); ++i) + { + std::string chapterName; + appPlayer->GetChapterName(chapterName, i); + + int64_t pos = appPlayer->GetChapterPos(i); + std::string time = StringUtils::SecondsToTimeString((long) pos, TIME_FORMAT_HH_MM_SS); + + if (chapterName.empty() || + StringUtils::StartsWithNoCase(chapterName, time) || + StringUtils::IsNaturalNumber(chapterName)) + chapterName = StringUtils::Format(g_localizeStrings.Get(25010), i); + + CFileItemPtr item(new CFileItem(chapterName)); + item->SetLabel2(time); + + std::string chapterPath = StringUtils::Format("chapter://{}/{}", m_filePath, i); + std::string cachefile = CServiceBroker::GetTextureCache()->GetCachedPath( + CServiceBroker::GetTextureCache()->GetCacheFile(chapterPath) + ".jpg"); + if (CFileUtils::Exists(cachefile)) + item->SetArt("thumb", cachefile); + else if (i > m_jobsStarted && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTCHAPTERTHUMBS)) + { + CFileItem item(m_filePath, false); + CJob* job = new CThumbExtractor(item, m_filePath, true, chapterPath, pos * 1000, false); + AddJob(job); + m_mapJobsChapter[job] = i; + m_jobsStarted++; + } + + item->SetProperty("chapter", i); + item->SetProperty("resumepoint", static_cast<double>(pos)); + item->SetProperty("ischapter", "true"); + items.push_back(item); + } + + // sort items by resume point + std::sort(items.begin(), items.end(), [](const CFileItemPtr &item1, const CFileItemPtr &item2) { + return item1->GetProperty("resumepoint").asDouble() < item2->GetProperty("resumepoint").asDouble(); + }); + + // add items to file list and mark the proper item as selected if the current playtime is above + int selectedItemIndex = 0; + double playTime = g_application.GetTime(); + for (auto& item : items) + { + m_vecItems->Add(item); + if (playTime >= item->GetProperty("resumepoint").asDouble()) + selectedItemIndex = m_vecItems->Size() - 1; + } + + m_viewControl.SetItems(*m_vecItems); + m_viewControl.SetSelectedItem(selectedItemIndex); +} + +void CGUIDialogVideoBookmarks::Update() +{ + CVideoDatabase videoDatabase; + videoDatabase.Open(); + + if (g_application.CurrentFileItem().HasVideoInfoTag() && g_application.CurrentFileItem().GetVideoInfoTag()->m_iEpisode > -1) + { + std::vector<CVideoInfoTag> episodes; + videoDatabase.GetEpisodesByFile(g_application.CurrentFile(),episodes); + if (episodes.size() > 1) + { + CONTROL_ENABLE(CONTROL_ADD_EPISODE_BOOKMARK); + } + else + { + CONTROL_DISABLE(CONTROL_ADD_EPISODE_BOOKMARK); + } + } + else + { + CONTROL_DISABLE(CONTROL_ADD_EPISODE_BOOKMARK); + } + + + m_viewControl.SetCurrentView(DEFAULT_VIEW_ICONS); + + // empty the list ready for population + Clear(); + + OnRefreshList(); + + videoDatabase.Close(); +} + +void CGUIDialogVideoBookmarks::Clear() +{ + m_viewControl.Clear(); + m_vecItems->Clear(); +} + +void CGUIDialogVideoBookmarks::GotoBookmark(int item) +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (item < 0 || item >= m_vecItems->Size() || !appPlayer->HasPlayer()) + return; + + CFileItemPtr fileItem = m_vecItems->Get(item); + int chapter = static_cast<int>(fileItem->GetProperty("chapter").asInteger()); + if (chapter <= 0) + { + appPlayer->SetPlayerState(fileItem->GetProperty("playerstate").asString()); + g_application.SeekTime(fileItem->GetProperty("resumepoint").asDouble()); + } + else + appPlayer->SeekChapter(chapter); + + Close(); +} + +void CGUIDialogVideoBookmarks::ClearBookmarks() +{ + CVideoDatabase videoDatabase; + videoDatabase.Open(); + std::string path = g_application.CurrentFile(); + if (g_application.CurrentFileItem().HasProperty("original_listitem_url") && + !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString())) + path = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString(); + videoDatabase.ClearBookMarksOfFile(path, CBookmark::STANDARD); + videoDatabase.ClearBookMarksOfFile(path, CBookmark::RESUME); + videoDatabase.ClearBookMarksOfFile(path, CBookmark::EPISODE); + videoDatabase.Close(); + Update(); +} + +bool CGUIDialogVideoBookmarks::AddBookmark(CVideoInfoTag* tag) +{ + CVideoDatabase videoDatabase; + CBookmark bookmark; + bookmark.timeInSeconds = (int)g_application.GetTime(); + bookmark.totalTimeInSeconds = (int)g_application.GetTotalTime(); + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + if (appPlayer->HasPlayer()) + bookmark.playerState = appPlayer->GetPlayerState(); + else + bookmark.playerState.clear(); + + bookmark.player = g_application.GetCurrentPlayer(); + + // create the thumbnail image + float aspectRatio = appPlayer->GetRenderAspectRatio(); + int width = BOOKMARK_THUMB_WIDTH; + int height = (int)(BOOKMARK_THUMB_WIDTH / aspectRatio); + if (height > (int)BOOKMARK_THUMB_WIDTH) + { + height = BOOKMARK_THUMB_WIDTH; + width = (int)(BOOKMARK_THUMB_WIDTH * aspectRatio); + } + + + uint8_t *pixels = (uint8_t*)malloc(height * width * 4); + unsigned int captureId = appPlayer->RenderCaptureAlloc(); + + appPlayer->RenderCapture(captureId, width, height, CAPTUREFLAG_IMMEDIATELY); + bool hasImage = appPlayer->RenderCaptureGetPixels(captureId, 1000, pixels, height * width * 4); + + if (hasImage) + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + auto crc = Crc32::ComputeFromLowerCase(g_application.CurrentFile()); + bookmark.thumbNailImage = + StringUtils::Format("{:08x}_{}.jpg", crc, (int)bookmark.timeInSeconds); + bookmark.thumbNailImage = URIUtils::AddFileToFolder(profileManager->GetBookmarksThumbFolder(), bookmark.thumbNailImage); + + if (!CPicture::CreateThumbnailFromSurface(pixels, width, height, width * 4, + bookmark.thumbNailImage)) + { + bookmark.thumbNailImage.clear(); + } + else + CLog::Log(LOGERROR,"CGUIDialogVideoBookmarks: failed to create thumbnail"); + + appPlayer->RenderCaptureRelease(captureId); + } + else + CLog::Log(LOGERROR,"CGUIDialogVideoBookmarks: failed to create thumbnail 2"); + + free(pixels); + + videoDatabase.Open(); + if (tag) + videoDatabase.AddBookMarkForEpisode(*tag, bookmark); + else + { + std::string path = g_application.CurrentFile(); + if (g_application.CurrentFileItem().HasProperty("original_listitem_url") && + !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString())) + path = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString(); + videoDatabase.AddBookMarkToFile(path, bookmark, CBookmark::STANDARD); + } + videoDatabase.Close(); + return true; +} + +void CGUIDialogVideoBookmarks::OnWindowLoaded() +{ + CGUIDialog::OnWindowLoaded(); + m_viewControl.Reset(); + m_viewControl.SetParentWindow(GetID()); + m_viewControl.AddView(GetControl(CONTROL_THUMBS)); + m_jobsStarted = 0; + m_mapJobsChapter.clear(); + m_vecItems->Clear(); +} + +void CGUIDialogVideoBookmarks::OnWindowUnload() +{ + //stop running thumb extraction jobs + CancelJobs(); + m_mapJobsChapter.clear(); + m_vecItems->Clear(); + CGUIDialog::OnWindowUnload(); + m_viewControl.Reset(); +} + +CGUIControl *CGUIDialogVideoBookmarks::GetFirstFocusableControl(int id) +{ + if (m_viewControl.HasControl(id)) + id = m_viewControl.GetCurrentControl(); + return CGUIWindow::GetFirstFocusableControl(id); +} + +bool CGUIDialogVideoBookmarks::AddEpisodeBookmark() +{ + std::vector<CVideoInfoTag> episodes; + CVideoDatabase videoDatabase; + videoDatabase.Open(); + videoDatabase.GetEpisodesByFile(g_application.CurrentFile(), episodes); + videoDatabase.Close(); + if (!episodes.empty()) + { + CContextButtons choices; + for (unsigned int i=0; i < episodes.size(); ++i) + { + std::string strButton = + StringUtils::Format("{} {}, {} {}", g_localizeStrings.Get(20373), episodes[i].m_iSeason, + g_localizeStrings.Get(20359), episodes[i].m_iEpisode); + choices.Add(i, strButton); + } + + int pressed = CGUIDialogContextMenu::ShowAndGetChoice(choices); + if (pressed >= 0) + { + AddBookmark(&episodes[pressed]); + return true; + } + } + return false; +} + + + +bool CGUIDialogVideoBookmarks::OnAddBookmark() +{ + if (!g_application.CurrentFileItem().IsVideo()) + return false; + + if (CGUIDialogVideoBookmarks::AddBookmark()) + { + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_REFRESH_LIST, 0, WINDOW_DIALOG_VIDEO_BOOKMARKS); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, + g_localizeStrings.Get(298), // "Bookmarks" + g_localizeStrings.Get(21362));// "Bookmark created" + return true; + } + return false; +} + +bool CGUIDialogVideoBookmarks::OnAddEpisodeBookmark() +{ + bool bReturn = false; + if (g_application.CurrentFileItem().HasVideoInfoTag() && g_application.CurrentFileItem().GetVideoInfoTag()->m_iEpisode > -1) + { + CVideoDatabase videoDatabase; + videoDatabase.Open(); + std::vector<CVideoInfoTag> episodes; + videoDatabase.GetEpisodesByFile(g_application.CurrentFile(),episodes); + if (episodes.size() > 1) + { + bReturn = CGUIDialogVideoBookmarks::AddEpisodeBookmark(); + if(bReturn) + { + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_REFRESH_LIST, 0, WINDOW_DIALOG_VIDEO_BOOKMARKS); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, + g_localizeStrings.Get(298), // "Bookmarks" + g_localizeStrings.Get(21363));// "Episode Bookmark created" + + } + } + videoDatabase.Close(); + } + return bReturn; +} + +void CGUIDialogVideoBookmarks::OnJobComplete(unsigned int jobID, + bool success, CJob* job) +{ + if (success && IsActive()) + { + MAPJOBSCHAPS::iterator iter = m_mapJobsChapter.find(job); + if (iter != m_mapJobsChapter.end()) + { + unsigned int chapterIdx = (*iter).second; + CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, 1, chapterIdx); + CServiceBroker::GetAppMessenger()->SendGUIMessage(m); + m_mapJobsChapter.erase(iter); + } + } + CJobQueue::OnJobComplete(jobID, success, job); +} diff --git a/xbmc/video/dialogs/GUIDialogVideoBookmarks.h b/xbmc/video/dialogs/GUIDialogVideoBookmarks.h new file mode 100644 index 0000000..801afcd --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoBookmarks.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "guilib/GUIDialog.h" +#include "utils/JobManager.h" +#include "video/VideoDatabase.h" +#include "view/GUIViewControl.h" + +class CFileItemList; + +class CGUIDialogVideoBookmarks : public CGUIDialog, public CJobQueue +{ + typedef std::map<CJob*, unsigned int> MAPJOBSCHAPS; + +public: + CGUIDialogVideoBookmarks(void); + ~CGUIDialogVideoBookmarks(void) override; + bool OnMessage(CGUIMessage& message) override; + void OnWindowLoaded() override; + void OnWindowUnload() override; + bool OnAction(const CAction &action) override; + + /*! + \brief Creates a bookmark of the currently playing video file. + + NOTE: sends a GUI_MSG_REFRESH_LIST message to DialogVideoBookmark on success + \return True if creation of bookmark was successful + \sa OnAddEpisodeBookmark + */ + static bool OnAddBookmark(); + + /*! + \brief Creates an episode bookmark of the currently playing file + + An episode bookmark specifies the end/beginning of episodes on files like: S01E01E02 + Fails if the current video isn't a multi-episode file + NOTE: sends a GUI_MSG_REFRESH_LIST message to DialogVideoBookmark on success + \return True, if bookmark was successfully created + \sa OnAddBookmark + **/ + static bool OnAddEpisodeBookmark(); + + + void Update(); +protected: + void GotoBookmark(int iItem); + void ClearBookmarks(); + static bool AddEpisodeBookmark(); + static bool AddBookmark(CVideoInfoTag *tag=NULL); + void Delete(int item); + void Clear(); + void OnRefreshList(); + void OnPopupMenu(int item); + CGUIControl *GetFirstFocusableControl(int id) override; + + void OnJobComplete(unsigned int jobID, bool success, CJob* job) override; + + CFileItemList* m_vecItems; + CGUIViewControl m_viewControl; + VECBOOKMARKS m_bookmarks; + +private: + void UpdateItem(unsigned int chapterIdx); + + int m_jobsStarted; + std::string m_filePath; + CCriticalSection m_refreshSection; + MAPJOBSCHAPS m_mapJobsChapter; +}; diff --git a/xbmc/video/dialogs/GUIDialogVideoInfo.cpp b/xbmc/video/dialogs/GUIDialogVideoInfo.cpp new file mode 100644 index 0000000..240d672 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoInfo.cpp @@ -0,0 +1,2398 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogVideoInfo.h" + +#include "ContextMenuManager.h" +#include "FileItem.h" +#include "GUIPassword.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "Util.h" +#include "dialogs/GUIDialogFileBrowser.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/Directory.h" +#include "filesystem/VideoDatabaseDirectory.h" +#include "filesystem/VideoDatabaseDirectory/QueryParams.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIImage.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindow.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/MusicDatabase.h" +#include "music/dialogs/GUIDialogMusicInfo.h" +#include "playlists/PlayListTypes.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/SettingUtils.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "storage/MediaManager.h" +#include "utils/FileExtensionProvider.h" +#include "utils/FileUtils.h" +#include "utils/SortUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "video/VideoDbUrl.h" +#include "video/VideoInfoScanner.h" +#include "video/VideoInfoTag.h" +#include "video/VideoLibraryQueue.h" +#include "video/VideoThumbLoader.h" +#include "video/tags/VideoTagLoaderFFmpeg.h" +#include "video/windows/GUIWindowVideoNav.h" + +#include <iterator> +#include <string> + +using namespace XFILE::VIDEODATABASEDIRECTORY; +using namespace XFILE; +using namespace KODI::MESSAGING; + +#define CONTROL_IMAGE 3 +#define CONTROL_TEXTAREA 4 +#define CONTROL_BTN_TRACKS 5 +#define CONTROL_BTN_REFRESH 6 +#define CONTROL_BTN_USERRATING 7 +#define CONTROL_BTN_PLAY 8 +#define CONTROL_BTN_RESUME 9 +#define CONTROL_BTN_GET_THUMB 10 +#define CONTROL_BTN_PLAY_TRAILER 11 +#define CONTROL_BTN_GET_FANART 12 +#define CONTROL_BTN_DIRECTOR 13 + +#define CONTROL_LIST 50 + +// predicate used by sorting and set_difference +bool compFileItemsByDbId(const CFileItemPtr& lhs, const CFileItemPtr& rhs) +{ + return lhs->HasVideoInfoTag() && rhs->HasVideoInfoTag() && lhs->GetVideoInfoTag()->m_iDbId < rhs->GetVideoInfoTag()->m_iDbId; +} + +CGUIDialogVideoInfo::CGUIDialogVideoInfo(void) + : CGUIDialog(WINDOW_DIALOG_VIDEO_INFO, "DialogVideoInfo.xml"), + m_movieItem(new CFileItem), + m_castList(new CFileItemList) +{ + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogVideoInfo::~CGUIDialogVideoInfo(void) +{ + delete m_castList; +} + +bool CGUIDialogVideoInfo::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_DEINIT: + { + ClearCastList(); + + if (m_startUserrating != m_movieItem->GetVideoInfoTag()->m_iUserRating) + { + CVideoDatabase db; + if (db.Open()) + { + m_hasUpdatedUserrating = true; + db.SetVideoUserRating(m_movieItem->GetVideoInfoTag()->m_iDbId, m_movieItem->GetVideoInfoTag()->m_iUserRating, m_movieItem->GetVideoInfoTag()->m_type); + db.Close(); + } + } + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTN_REFRESH) + { + if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeTvShow) + { + bool bCanceled=false; + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20377}, CVariant{20378}, bCanceled, CVariant{ "" }, CVariant{ "" }, CGUIDialogYesNo::NO_TIMEOUT)) + { + m_bRefreshAll = true; + CVideoDatabase db; + if (db.Open()) + { + db.SetPathHash(m_movieItem->GetVideoInfoTag()->m_strPath,""); + db.Close(); + } + } + else + m_bRefreshAll = false; + + if (bCanceled) + return false; + } + m_bRefresh = true; + Close(); + return true; + } + else if (iControl == CONTROL_BTN_TRACKS) + { + m_bViewReview = !m_bViewReview; + Update(); + } + else if (iControl == CONTROL_BTN_PLAY) + { + Play(); + } + else if (iControl == CONTROL_BTN_USERRATING) + { + OnSetUserrating(); + } + else if (iControl == CONTROL_BTN_RESUME) + { + Play(true); + } + else if (iControl == CONTROL_BTN_GET_THUMB) + { + OnGetArt(); + } + else if (iControl == CONTROL_BTN_PLAY_TRAILER) + { + PlayTrailer(); + } + else if (iControl == CONTROL_BTN_GET_FANART) + { + OnGetFanart(); + } + else if (iControl == CONTROL_BTN_DIRECTOR) + { + auto directors = m_movieItem->GetVideoInfoTag()->m_director; + if (directors.size() == 0) + return true; + if (directors.size() == 1) + OnSearch(directors[0]); + else + { + auto pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (pDlgSelect) + { + pDlgSelect->Reset(); + pDlgSelect->SetHeading(CVariant{22080}); + for (const auto &director: directors) + pDlgSelect->Add(director); + pDlgSelect->Open(); + + int iItem = pDlgSelect->GetSelectedItem(); + if (iItem < 0) + return true; + OnSearch(directors[iItem]); + } + } + } + else if (iControl == CONTROL_LIST) + { + int iAction = message.GetParam1(); + if (ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction) + { + CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl); + OnMessage(msg); + int iItem = msg.GetParam1(); + if (iItem < 0 || iItem >= m_castList->Size()) + break; + std::string strItem = m_castList->Get(iItem)->GetLabel(); + if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection) + { + SetMovie(m_castList->Get(iItem).get()); + Close(); + Open(); + } + else + OnSearch(strItem); + } + } + } + break; + case GUI_MSG_NOTIFY_ALL: + { + if (IsActive() && message.GetParam1() == GUI_MSG_UPDATE_ITEM && message.GetItem()) + { + CFileItemPtr item = std::static_pointer_cast<CFileItem>(message.GetItem()); + if (item && m_movieItem->IsPath(item->GetPath())) + { // Just copy over the stream details and the thumb if we don't already have one + if (!m_movieItem->HasArt("thumb")) + m_movieItem->SetArt("thumb", item->GetArt("thumb")); + m_movieItem->GetVideoInfoTag()->m_streamDetails = item->GetVideoInfoTag()->m_streamDetails; + } + return true; + } + } + } + + return CGUIDialog::OnMessage(message); +} + +void CGUIDialogVideoInfo::OnInitWindow() +{ + m_bRefresh = false; + m_bRefreshAll = true; + m_hasUpdatedThumb = false; + m_hasUpdatedUserrating = false; + m_bViewReview = true; + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + const std::string uniqueId = m_movieItem->GetProperty("xxuniqueid").asString(); + if (uniqueId.empty() || !StringUtils::StartsWithNoCase(uniqueId.c_str(), "xx")) + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_REFRESH, + (profileManager->GetCurrentProfile().canWriteDatabases() || + g_passwordManager.bMasterUser)); + else + CONTROL_DISABLE(CONTROL_BTN_REFRESH); + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB, + (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) && + !StringUtils::StartsWithNoCase(m_movieItem->GetVideoInfoTag()-> + GetUniqueID().c_str(), "plugin")); + // Disable video user rating button for plugins and sets as they don't have tables to save this + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_USERRATING, !m_movieItem->IsPlugin() && m_movieItem->GetVideoInfoTag()->m_type != MediaTypeVideoCollection); + + VideoDbContentType type = m_movieItem->GetVideoContentType(); + if (type == VideoDbContentType::TVSHOWS || type == VideoDbContentType::MOVIES) + CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_FANART, (profileManager-> + GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) && + !StringUtils::StartsWithNoCase(m_movieItem->GetVideoInfoTag()-> + GetUniqueID().c_str(), "plugin")); + else + CONTROL_DISABLE(CONTROL_BTN_GET_FANART); + + Update(); + + CGUIDialog::OnInitWindow(); +} + +bool CGUIDialogVideoInfo::OnAction(const CAction &action) +{ + int userrating = m_movieItem->GetVideoInfoTag()->m_iUserRating; + if (action.GetID() == ACTION_INCREASE_RATING) + { + SetUserrating(userrating + 1); + return true; + } + else if (action.GetID() == ACTION_DECREASE_RATING) + { + SetUserrating(userrating - 1); + return true; + } + else if (action.GetID() == ACTION_SHOW_INFO) + { + Close(); + return true; + } + return CGUIDialog::OnAction(action); +} + +void CGUIDialogVideoInfo::SetUserrating(int userrating) const +{ + userrating = std::max(userrating, 0); + userrating = std::min(userrating, 10); + if (userrating != m_movieItem->GetVideoInfoTag()->m_iUserRating) + { + m_movieItem->GetVideoInfoTag()->SetUserrating(userrating); + + // send a message to all windows to tell them to update the fileitem (eg playlistplayer, media windows) + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_movieItem); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + } +} + +void CGUIDialogVideoInfo::SetMovie(const CFileItem *item) +{ + *m_movieItem = *item; + + // setup cast list + ClearCastList(); + + // When the scraper throws an error, the video tag can be null here + if (!item->HasVideoInfoTag()) + return; + + MediaType type = item->GetVideoInfoTag()->m_type; + + m_startUserrating = m_movieItem->GetVideoInfoTag()->m_iUserRating; + + if (type == MediaTypeMusicVideo) + { // music video + CMusicDatabase database; + database.Open(); + const std::vector<std::string> &artists = m_movieItem->GetVideoInfoTag()->m_artist; + for (std::vector<std::string>::const_iterator it = artists.begin(); it != artists.end(); ++it) + { + int idArtist = database.GetArtistByName(*it); + std::string thumb = database.GetArtForItem(idArtist, MediaTypeArtist, "thumb"); + CFileItemPtr item(new CFileItem(*it)); + if (!thumb.empty()) + item->SetArt("thumb", thumb); + item->SetArt("icon", "DefaultArtist.png"); + item->SetLabel2(g_localizeStrings.Get(29904)); + m_castList->Add(item); + } + // get performers in the music video (added as actors) + for (CVideoInfoTag::iCast it = m_movieItem->GetVideoInfoTag()->m_cast.begin(); + it != m_movieItem->GetVideoInfoTag()->m_cast.end(); ++it) + { + // Check to see if we have already added this performer as the artist and skip adding if so + auto haveArtist = std::find(std::begin(artists), std::end(artists), it->strName); + if (haveArtist == artists.end()) // artist or performer not already in the list + { + CFileItemPtr item(new CFileItem(it->strName)); + if (!it->thumb.empty()) + item->SetArt("thumb", it->thumb); + else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS)) + { // backward compatibility + std::string thumb = CScraperUrl::GetThumbUrl(it->thumbUrl.GetFirstUrlByType()); + if (!thumb.empty()) + { + item->SetArt("thumb", thumb); + CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb); + } + } + item->SetArt("icon", "DefaultActor.png"); + item->SetLabel(it->strName); + item->SetLabel2(it->strRole); + m_castList->Add(item); + } + } + } + else if (type == MediaTypeVideoCollection) + { + CVideoDatabase database; + database.Open(); + database.GetMoviesNav(m_movieItem->GetPath(), *m_castList, -1, -1, -1, -1, -1, -1, + m_movieItem->GetVideoInfoTag()->m_set.id, -1, + SortDescription(), VideoDbDetailsAll); + m_castList->Sort(SortBySortTitle, SortOrderDescending); + CVideoThumbLoader loader; + for (auto& item : *m_castList) + loader.LoadItem(item.get()); + } + else + { // movie/show/episode + for (CVideoInfoTag::iCast it = m_movieItem->GetVideoInfoTag()->m_cast.begin(); it != m_movieItem->GetVideoInfoTag()->m_cast.end(); ++it) + { + CFileItemPtr item(new CFileItem(it->strName)); + if (!it->thumb.empty()) + item->SetArt("thumb", it->thumb); + else + { + const std::shared_ptr<CSettings> settings = + CServiceBroker::GetSettingsComponent()->GetSettings(); + if (settings->GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL) != + CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE && + settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS)) + { // backward compatibility + std::string thumb = CScraperUrl::GetThumbUrl(it->thumbUrl.GetFirstUrlByType()); + if (!thumb.empty()) + { + item->SetArt("thumb", thumb); + CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb); + } + } + } + item->SetArt("icon", "DefaultActor.png"); + item->SetLabel(it->strName); + item->SetLabel2(it->strRole); + m_castList->Add(item); + } + } + + if (type == MediaTypeMovie) + { + // local trailers should always override non-local, so check + // for a local one if the registered trailer is online + if (m_movieItem->GetVideoInfoTag()->m_strTrailer.empty() || + URIUtils::IsInternetStream(m_movieItem->GetVideoInfoTag()->m_strTrailer)) + { + std::string localTrailer = m_movieItem->FindTrailer(); + if (!localTrailer.empty()) + { + m_movieItem->GetVideoInfoTag()->m_strTrailer = localTrailer; + CVideoDatabase database; + if (database.Open()) + { + database.SetSingleValue(VideoDbContentType::MOVIES, VIDEODB_ID_TRAILER, + m_movieItem->GetVideoInfoTag()->m_iDbId, + m_movieItem->GetVideoInfoTag()->m_strTrailer); + database.Close(); + CUtil::DeleteVideoDatabaseDirectoryCache(); + } + } + } + } + + m_castList->SetContent(CMediaTypes::ToPlural(type)); + + CVideoThumbLoader loader; + loader.LoadItem(m_movieItem.get()); +} + +void CGUIDialogVideoInfo::Update() +{ + // setup plot text area + std::shared_ptr<CSettingList> setting(std::dynamic_pointer_cast<CSettingList>( + CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS))); + std::string strTmp = m_movieItem->GetVideoInfoTag()->m_strPlot; + if (m_movieItem->GetVideoInfoTag()->m_type != MediaTypeTvShow) + if (m_movieItem->GetVideoInfoTag()->GetPlayCount() == 0 && setting && + ((m_movieItem->GetVideoInfoTag()->m_type == MediaTypeMovie && + !CSettingUtils::FindIntInList(setting, + CSettings::VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_MOVIES)) || + (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeEpisode && + !CSettingUtils::FindIntInList( + setting, CSettings::VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_TVSHOWEPISODES)))) + strTmp = g_localizeStrings.Get(20370); + + StringUtils::Trim(strTmp); + SetLabel(CONTROL_TEXTAREA, strTmp); + + CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_castList); + OnMessage(msg); + + if (GetControl(CONTROL_BTN_TRACKS)) // if no CONTROL_BTN_TRACKS found - allow skinner full visibility control over CONTROL_TEXTAREA and CONTROL_LIST + { + if (m_bViewReview) + { + if (!m_movieItem->GetVideoInfoTag()->m_artist.empty()) + { + SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 133); + } + else if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection) + { + SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 20342); + } + else + { + SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 206); + } + + SET_CONTROL_HIDDEN(CONTROL_LIST); + SET_CONTROL_VISIBLE(CONTROL_TEXTAREA); + } + else + { + SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 207); + + SET_CONTROL_HIDDEN(CONTROL_TEXTAREA); + SET_CONTROL_VISIBLE(CONTROL_LIST); + } + } + + // Check for resumability + if (m_movieItem->GetVideoInfoTag()->GetResumePoint().timeInSeconds > 0.0) + CONTROL_ENABLE(CONTROL_BTN_RESUME); + else + CONTROL_DISABLE(CONTROL_BTN_RESUME); + + CONTROL_ENABLE(CONTROL_BTN_PLAY); + + // update the thumbnail + CGUIControl* pControl = GetControl(CONTROL_IMAGE); + if (pControl) + { + CGUIImage* pImageControl = static_cast<CGUIImage*>(pControl); + pImageControl->FreeResources(); + pImageControl->SetFileName(m_movieItem->GetArt("thumb")); + } + // tell our GUI to completely reload all controls (as some of them + // are likely to have had this image in use so will need refreshing) + if (m_hasUpdatedThumb) + { + CGUIMessage reload(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(reload); + } +} + +bool CGUIDialogVideoInfo::NeedRefresh() const +{ + return m_bRefresh; +} + +bool CGUIDialogVideoInfo::RefreshAll() const +{ + return m_bRefreshAll; +} + +void CGUIDialogVideoInfo::OnSearch(std::string& strSearch) +{ + CGUIDialogProgress *progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (progress) + { + progress->SetHeading(CVariant{194}); + progress->SetLine(0, CVariant{strSearch}); + progress->SetLine(1, CVariant{""}); + progress->SetLine(2, CVariant{""}); + progress->Open(); + progress->Progress(); + } + CFileItemList items; + DoSearch(strSearch, items); + + if (progress) + progress->Close(); + + if (items.Size()) + { + CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (pDlgSelect) + { + pDlgSelect->Reset(); + pDlgSelect->SetHeading(CVariant{283}); + + CVideoThumbLoader loader; + for (int i = 0; i < items.Size(); i++) + { + if (items[i]->HasVideoInfoTag() && + items[i]->GetVideoInfoTag()->GetPlayCount() > 0) + items[i]->SetLabel2(g_localizeStrings.Get(16102)); + + loader.LoadItem(items[i].get()); + pDlgSelect->Add(*items[i]); + } + + pDlgSelect->SetUseDetails(true); + pDlgSelect->Open(); + + int iItem = pDlgSelect->GetSelectedItem(); + if (iItem < 0) + return; + + OnSearchItemFound(items[iItem].get()); + } + } + else + { + HELPERS::ShowOKDialogText(CVariant{194}, CVariant{284}); + } +} + +void CGUIDialogVideoInfo::DoSearch(std::string& strSearch, CFileItemList& items) const +{ + CVideoDatabase db; + if (!db.Open()) + return; + + CFileItemList movies; + db.GetMoviesByActor(strSearch, movies); + for (int i = 0; i < movies.Size(); ++i) + { + std::string label = movies[i]->GetVideoInfoTag()->m_strTitle; + if (movies[i]->GetVideoInfoTag()->HasYear()) + label += StringUtils::Format(" ({})", movies[i]->GetVideoInfoTag()->GetYear()); + movies[i]->SetLabel(label); + } + CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20338) + "] ", items); + + db.GetTvShowsByActor(strSearch, movies); + for (int i = 0; i < movies.Size(); ++i) + { + std::string label = movies[i]->GetVideoInfoTag()->m_strShowTitle; + if (movies[i]->GetVideoInfoTag()->HasYear()) + label += StringUtils::Format(" ({})", movies[i]->GetVideoInfoTag()->GetYear()); + movies[i]->SetLabel(label); + } + CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20364) + "] ", items); + + db.GetEpisodesByActor(strSearch, movies); + for (int i = 0; i < movies.Size(); ++i) + { + std::string label = movies[i]->GetVideoInfoTag()->m_strTitle + " (" + movies[i]->GetVideoInfoTag()->m_strShowTitle + ")"; + movies[i]->SetLabel(label); + } + CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20359) + "] ", items); + + db.GetMusicVideosByArtist(strSearch, movies); + for (int i = 0; i < movies.Size(); ++i) + { + std::string label = StringUtils::Join(movies[i]->GetVideoInfoTag()->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator) + " - " + movies[i]->GetVideoInfoTag()->m_strTitle; + if (movies[i]->GetVideoInfoTag()->HasYear()) + label += StringUtils::Format(" ({})", movies[i]->GetVideoInfoTag()->GetYear()); + movies[i]->SetLabel(label); + } + CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20391) + "] ", items); + db.Close(); + + // Search for music albums by artist with name matching search string + CMusicDatabase music_database; + if (!music_database.Open()) + return; + + if (music_database.SearchAlbumsByArtistName(strSearch, movies)) + { + for (int i = 0; i < movies.Size(); ++i) + { + // Set type so that video thumbloader handles album art + movies[i]->GetVideoInfoTag()->m_type = MediaTypeAlbum; + } + CGUIWindowVideoBase::AppendAndClearSearchItems( + movies, "[" + g_localizeStrings.Get(36918) + "] ", items); + } + music_database.Close(); +} + +void CGUIDialogVideoInfo::OnSearchItemFound(const CFileItem* pItem) +{ + VideoDbContentType type = pItem->GetVideoContentType(); + + CVideoDatabase db; + if (!db.Open()) + return; + + CVideoInfoTag movieDetails; + if (type == VideoDbContentType::MOVIES) + db.GetMovieInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId); + if (type == VideoDbContentType::EPISODES) + db.GetEpisodeInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId); + if (type == VideoDbContentType::TVSHOWS) + db.GetTvShowInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId); + if (type == VideoDbContentType::MUSICVIDEOS) + db.GetMusicVideoInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId); + db.Close(); + if (type == VideoDbContentType::MUSICALBUMS) + { + Close(); + CGUIDialogMusicInfo::ShowFor(const_cast<CFileItem*>(pItem)); + return; // No video info to refresh so just close the window and go back to the fileitem list + } + + CFileItem item(*pItem); + *item.GetVideoInfoTag() = movieDetails; + SetMovie(&item); + // refresh our window entirely + Close(); + Open(); +} + +void CGUIDialogVideoInfo::ClearCastList() +{ + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST); + OnMessage(msg); + m_castList->Clear(); +} + +void CGUIDialogVideoInfo::Play(bool resume) +{ + if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeTvShow) + { + std::string strPath; + if (m_movieItem->IsPlugin()) + { + strPath = m_movieItem->GetPath(); + Close(); + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV) + { + CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()-> + GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE, 0); + message.SetStringParam(strPath); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message); + } + else + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV,strPath); + } + else + { + strPath = StringUtils::Format("videodb://tvshows/titles/{}/", + m_movieItem->GetVideoInfoTag()->m_iDbId); + Close(); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV,strPath); + } + return; + } + + if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection) + { + std::string strPath = StringUtils::Format("videodb://movies/sets/{}/?setid={}", + m_movieItem->GetVideoInfoTag()->m_iDbId, + m_movieItem->GetVideoInfoTag()->m_iDbId); + Close(); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV, strPath); + return; + } + + CGUIWindowVideoNav* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowVideoNav>(WINDOW_VIDEO_NAV); + if (pWindow) + { + // close our dialog + Close(true); + if (resume) + m_movieItem->SetStartOffset(STARTOFFSET_RESUME); + else if (!CGUIWindowVideoBase::ShowResumeMenu(*m_movieItem)) + { + // The Resume dialog was closed without any choice + Open(); + return; + } + m_movieItem->SetProperty("playlist_type_hint", PLAYLIST::TYPE_VIDEO); + + pWindow->PlayMovie(m_movieItem.get()); + } +} + +namespace +{ +// Add art types required in Kodi and configured by the user +void AddHardCodedAndExtendedArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag) +{ + for (const auto& artType : CVideoThumbLoader::GetArtTypes(tag.m_type)) + { + if (find(artTypes.cbegin(), artTypes.cend(), artType) == artTypes.cend()) + artTypes.emplace_back(artType); + } +} + +// Add art types currently assigned to the media item +void AddCurrentArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag, + CVideoDatabase& db) +{ + std::map<std::string, std::string> currentArt; + db.GetArtForItem(tag.m_iDbId, tag.m_type, currentArt); + for (const auto& art : currentArt) + { + if (!art.second.empty() && find(artTypes.cbegin(), artTypes.cend(), art.first) == artTypes.cend()) + artTypes.push_back(art.first); + } +} + +// Add art types that exist for other media items of the same type +void AddMediaTypeArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag, + CVideoDatabase& db) +{ + std::vector<std::string> dbArtTypes; + db.GetArtTypes(tag.m_type, dbArtTypes); + for (const auto& artType : dbArtTypes) + { + if (find(artTypes.cbegin(), artTypes.cend(), artType) == artTypes.cend()) + artTypes.push_back(artType); + } +} + +// Add art types from available but unassigned artwork for this media item +void AddAvailableArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag, + CVideoDatabase& db) +{ + for (const auto& artType : db.GetAvailableArtTypesForItem(tag.m_iDbId, tag.m_type)) + { + if (find(artTypes.cbegin(), artTypes.cend(), artType) == artTypes.cend()) + artTypes.push_back(artType); + } +} + +std::vector<std::string> GetArtTypesList(const CVideoInfoTag& tag) +{ + CVideoDatabase db; + db.Open(); + + std::vector<std::string> artTypes; + + AddHardCodedAndExtendedArtTypes(artTypes, tag); + AddCurrentArtTypes(artTypes, tag, db); + AddMediaTypeArtTypes(artTypes, tag, db); + AddAvailableArtTypes(artTypes, tag, db); + + db.Close(); + return artTypes; +} +} + +std::string CGUIDialogVideoInfo::ChooseArtType(const CFileItem &videoItem) +{ + // prompt for choice + CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (!dialog || !videoItem.HasVideoInfoTag()) + return ""; + + CFileItemList items; + dialog->SetHeading(CVariant{13511}); + dialog->Reset(); + dialog->SetUseDetails(true); + dialog->EnableButton(true, 13516); + + std::vector<std::string> artTypes = GetArtTypesList(*videoItem.GetVideoInfoTag()); + + for (std::vector<std::string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i) + { + const std::string& type = *i; + CFileItemPtr item(new CFileItem(type, false)); + if (type == "banner") + item->SetLabel(g_localizeStrings.Get(20020)); + else if (type == "fanart") + item->SetLabel(g_localizeStrings.Get(20445)); + else if (type == "poster") + item->SetLabel(g_localizeStrings.Get(20021)); + else if (type == "thumb") + item->SetLabel(g_localizeStrings.Get(21371)); + else + item->SetLabel(type); + item->SetProperty("type", type); + if (videoItem.HasArt(type)) + item->SetArt("thumb", videoItem.GetArt(type)); + items.Add(item); + } + + dialog->SetItems(items); + dialog->Open(); + + if (dialog->IsButtonPressed()) + { + // Get the new artwork name + std::string strArtworkName; + if (!CGUIKeyboardFactory::ShowAndGetInput(strArtworkName, CVariant{g_localizeStrings.Get(13516)}, false)) + return ""; + + return strArtworkName; + } + + return dialog->GetSelectedFileItem()->GetProperty("type").asString(); +} + +void CGUIDialogVideoInfo::OnGetArt() +{ + if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection) + { + ManageVideoItemArtwork(m_movieItem, m_movieItem->GetVideoInfoTag()->m_type); + return; + } + std::string type = ChooseArtType(*m_movieItem); + if (type.empty()) + return; // cancelled + + //! @todo this can be removed once these are unified. + if (type == "fanart") + OnGetFanart(); + else + { + CFileItemList items; + + // Current thumb + if (m_movieItem->HasArt(type)) + { + CFileItemPtr item(new CFileItem("thumb://Current", false)); + item->SetArt("thumb", m_movieItem->GetArt(type)); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13512)); + items.Add(item); + } + else if ((type == "poster" || type == "banner") && m_movieItem->HasArt("thumb")) + { // add the 'thumb' type in + CFileItemPtr item(new CFileItem("thumb://Thumb", false)); + item->SetArt("thumb", m_movieItem->GetArt("thumb")); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13512)); + items.Add(item); + } + + std::string embeddedArt; + if (URIUtils::HasExtension(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, ".mkv")) + { + CFileItem item(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, false); + CVideoTagLoaderFFmpeg loader(item, nullptr, false); + CVideoInfoTag tag; + loader.Load(tag, false, nullptr); + for (const auto& it : tag.m_coverArt) + { + if (it.m_type == type) + { + CFileItemPtr itemF(new CFileItem("thumb://Embedded", false)); + embeddedArt = CTextureUtils::GetWrappedImageURL(item.GetPath(), "video_" + type); + itemF->SetArt("thumb", embeddedArt); + itemF->SetLabel(g_localizeStrings.Get(13519)); + items.Add(itemF); + } + } + } + + // Grab the thumbnails from the web + m_movieItem->GetVideoInfoTag()->m_strPictureURL.Parse(); + std::vector<std::string> thumbs; + int season = (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeSeason) ? m_movieItem->GetVideoInfoTag()->m_iSeason : -1; + m_movieItem->GetVideoInfoTag()->m_strPictureURL.GetThumbUrls(thumbs, type, season); + + for (unsigned int i = 0; i < thumbs.size(); ++i) + { + std::string strItemPath = StringUtils::Format("thumb://Remote{}", i); + CFileItemPtr item(new CFileItem(strItemPath, false)); + item->SetArt("thumb", thumbs[i]); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13513)); + + //! @todo Do we need to clear the cached image? + // CServiceBroker::GetTextureCache()->ClearCachedImage(thumb); + items.Add(item); + } + + std::string localThumb = CVideoThumbLoader::GetLocalArt(*m_movieItem, type); + if (!localThumb.empty()) + { + CFileItemPtr item(new CFileItem("thumb://Local", false)); + item->SetArt("thumb", localThumb); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13514)); + items.Add(item); + } + else + { // no local thumb exists, so we are just using the IMDb thumb or cached thumb + // which is probably the IMDb thumb. These could be wrong, so allow the user + // to delete the incorrect thumb + CFileItemPtr item(new CFileItem("thumb://None", false)); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13515)); + items.Add(item); + } + + std::string result; + VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("video")); + AddItemPathToFileBrowserSources(sources, *m_movieItem); + CServiceBroker::GetMediaManager().GetLocalDrives(sources); + if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) && + result != "thumb://Current") // user didn't choose the one they have + { + std::string newThumb; + if (StringUtils::StartsWith(result, "thumb://Remote")) + { + int number = atoi(result.substr(14).c_str()); + newThumb = thumbs[number]; + } + else if (result == "thumb://Thumb") + newThumb = m_movieItem->GetArt("thumb"); + else if (result == "thumb://Local") + newThumb = localThumb; + else if (result == "thumb://Embedded") + newThumb = embeddedArt; + else if (CFileUtils::Exists(result)) + newThumb = result; + else // none + newThumb.clear(); + + // update thumb in the database + CVideoDatabase db; + if (db.Open()) + { + db.SetArtForItem(m_movieItem->GetVideoInfoTag()->m_iDbId, m_movieItem->GetVideoInfoTag()->m_type, type, newThumb); + db.Close(); + } + CUtil::DeleteVideoDatabaseDirectoryCache(); // to get them new thumbs to show + m_movieItem->SetArt(type, newThumb); + if (m_movieItem->HasProperty("set_folder_thumb")) + { // have a folder thumb to set as well + VIDEO::CVideoInfoScanner::ApplyThumbToFolder(m_movieItem->GetProperty("set_folder_thumb").asString(), newThumb); + } + m_hasUpdatedThumb = true; + } + } + + // Update our screen + Update(); + + // re-open the art selection dialog as we come back from + // the image selection dialog + OnGetArt(); +} + +// Allow user to select a Fanart +void CGUIDialogVideoInfo::OnGetFanart() +{ + CFileItemList items; + + // Ensure the fanart is unpacked + m_movieItem->GetVideoInfoTag()->m_fanart.Unpack(); + + if (m_movieItem->HasArt("fanart")) + { + CFileItemPtr itemCurrent(new CFileItem("fanart://Current",false)); + itemCurrent->SetArt("thumb", m_movieItem->GetArt("fanart")); + itemCurrent->SetArt("icon", "DefaultPicture.png"); + itemCurrent->SetLabel(g_localizeStrings.Get(20440)); + items.Add(itemCurrent); + } + + std::string embeddedArt; + if (URIUtils::HasExtension(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, ".mkv")) + { + CFileItem item(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, false); + CVideoTagLoaderFFmpeg loader(item, nullptr, false); + CVideoInfoTag tag; + loader.Load(tag, false, nullptr); + for (const auto& it : tag.m_coverArt) + { + if (it.m_type == "fanart") + { + CFileItemPtr itemF(new CFileItem("fanart://Embedded", false)); + embeddedArt = CTextureUtils::GetWrappedImageURL(item.GetPath(), "video_fanart"); + itemF->SetArt("thumb", embeddedArt); + itemF->SetLabel(g_localizeStrings.Get(13520)); + items.Add(itemF); + } + } + } + + // Grab the thumbnails from the web + for (unsigned int i = 0; i < m_movieItem->GetVideoInfoTag()->m_fanart.GetNumFanarts(); i++) + { + if (URIUtils::IsProtocol(m_movieItem->GetVideoInfoTag()->m_fanart.GetPreviewURL(i), "image")) + continue; + std::string strItemPath = StringUtils::Format("fanart://Remote{}", i); + CFileItemPtr item(new CFileItem(strItemPath, false)); + std::string thumb = m_movieItem->GetVideoInfoTag()->m_fanart.GetPreviewURL(i); + item->SetArt("thumb", CTextureUtils::GetWrappedThumbURL(thumb)); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(20441)); + + //! @todo Do we need to clear the cached image? + // CServiceBroker::GetTextureCache()->ClearCachedImage(thumb); + items.Add(item); + } + + CFileItem item(*m_movieItem->GetVideoInfoTag()); + std::string strLocal = item.GetLocalFanart(); + if (!strLocal.empty()) + { + CFileItemPtr itemLocal(new CFileItem("fanart://Local",false)); + itemLocal->SetArt("thumb", strLocal); + itemLocal->SetArt("icon", "DefaultPicture.png"); + itemLocal->SetLabel(g_localizeStrings.Get(20438)); + + //! @todo Do we need to clear the cached image? + CServiceBroker::GetTextureCache()->ClearCachedImage(strLocal); + items.Add(itemLocal); + } + else + { + CFileItemPtr itemNone(new CFileItem("fanart://None", false)); + itemNone->SetArt("icon", "DefaultPicture.png"); + itemNone->SetLabel(g_localizeStrings.Get(20439)); + items.Add(itemNone); + } + + std::string result; + VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("video")); + AddItemPathToFileBrowserSources(sources, item); + CServiceBroker::GetMediaManager().GetLocalDrives(sources); + bool flip=false; + if (!CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(20437), result, &flip, 20445) || + StringUtils::EqualsNoCase(result, "fanart://Current")) + return; // user cancelled + + if (StringUtils::EqualsNoCase(result, "fanart://Local")) + result = strLocal; + + if (StringUtils::EqualsNoCase(result, "fanart://Embedded")) + { + unsigned int current = m_movieItem->GetVideoInfoTag()->m_fanart.GetNumFanarts(); + int found = -1; + for (size_t i = 0; i < current; ++i) + if (URIUtils::IsProtocol(m_movieItem->GetVideoInfoTag()->m_fanart.GetImageURL(), "image")) + found = i; + if (found != -1) + { + m_movieItem->GetVideoInfoTag()->m_fanart.AddFanart(embeddedArt, "", ""); + found = current; + } + + m_movieItem->GetVideoInfoTag()->m_fanart.SetPrimaryFanart(found); + + CVideoDatabase db; + if (db.Open()) + { + db.UpdateFanart(*m_movieItem, m_movieItem->GetVideoContentType()); + db.Close(); + } + result = embeddedArt; + } + + if (StringUtils::StartsWith(result, "fanart://Remote")) + { + int iFanart = atoi(result.substr(15).c_str()); + // set new primary fanart, and update our database accordingly + m_movieItem->GetVideoInfoTag()->m_fanart.SetPrimaryFanart(iFanart); + CVideoDatabase db; + if (db.Open()) + { + db.UpdateFanart(*m_movieItem, m_movieItem->GetVideoContentType()); + db.Close(); + } + result = m_movieItem->GetVideoInfoTag()->m_fanart.GetImageURL(); + } + else if (StringUtils::EqualsNoCase(result, "fanart://None") || !CFileUtils::Exists(result)) + result.clear(); + + // set the fanart image + if (flip && !result.empty()) + result = CTextureUtils::GetWrappedImageURL(result, "", "flipped"); + CVideoDatabase db; + if (db.Open()) + { + db.SetArtForItem(m_movieItem->GetVideoInfoTag()->m_iDbId, m_movieItem->GetVideoInfoTag()->m_type, "fanart", result); + db.Close(); + } + + CUtil::DeleteVideoDatabaseDirectoryCache(); // to get them new thumbs to show + m_movieItem->SetArt("fanart", result); + m_hasUpdatedThumb = true; + + // Update our screen + Update(); +} + +void CGUIDialogVideoInfo::OnSetUserrating() const +{ + CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (dialog) + { + dialog->SetHeading(CVariant{ 38023 }); + dialog->Add(g_localizeStrings.Get(38022)); + for (int i = 1; i <= 10; i++) + dialog->Add(StringUtils::Format("{}: {}", g_localizeStrings.Get(563), i)); + + dialog->SetSelected(m_movieItem->GetVideoInfoTag()->m_iUserRating); + + dialog->Open(); + + int iItem = dialog->GetSelectedItem(); + if (iItem < 0) + return; + + SetUserrating(iItem); + } +} + +void CGUIDialogVideoInfo::PlayTrailer() +{ + Close(true); + CGUIMessage msg(GUI_MSG_PLAY_TRAILER, 0, 0, 0, 0, m_movieItem); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); +} + +void CGUIDialogVideoInfo::SetLabel(int iControl, const std::string &strLabel) +{ + if (strLabel.empty()) + { + SET_CONTROL_LABEL(iControl, 416); // "Not available" + } + else + { + SET_CONTROL_LABEL(iControl, strLabel); + } +} + +std::string CGUIDialogVideoInfo::GetThumbnail() const +{ + return m_movieItem->GetArt("thumb"); +} + +namespace +{ +std::string GetItemPathForBrowserSource(const CFileItem& item) +{ + if (!item.HasVideoInfoTag()) + return ""; + + std::string itemDir = item.GetVideoInfoTag()->m_basePath; + //season + if (itemDir.empty()) + itemDir = item.GetVideoInfoTag()->GetPath(); + + CFileItem itemTmp(itemDir, false); + if (itemTmp.IsVideo()) + itemDir = URIUtils::GetParentPath(itemDir); + + return itemDir; +} + +void AddItemPathStringToFileBrowserSources(VECSOURCES& sources, + const std::string& itemDir, const std::string& label) +{ + if (!itemDir.empty() && CDirectory::Exists(itemDir)) + { + CMediaSource itemSource; + itemSource.strName = label; + itemSource.strPath = itemDir; + sources.push_back(itemSource); + } +} +} // namespace + +void CGUIDialogVideoInfo::AddItemPathToFileBrowserSources(VECSOURCES& sources, + const CFileItem& item) +{ + std::string itemDir = GetItemPathForBrowserSource(item); + AddItemPathStringToFileBrowserSources(sources, itemDir, g_localizeStrings.Get(36041)); +} + +int CGUIDialogVideoInfo::ManageVideoItem(const std::shared_ptr<CFileItem>& item) +{ + if (item == nullptr || !item->IsVideoDb() || !item->HasVideoInfoTag() || item->GetVideoInfoTag()->m_iDbId < 0) + return -1; + + CVideoDatabase database; + if (!database.Open()) + return -1; + + const std::string &type = item->GetVideoInfoTag()->m_type; + int dbId = item->GetVideoInfoTag()->m_iDbId; + + CContextButtons buttons; + if (type == MediaTypeMovie || type == MediaTypeVideoCollection || + type == MediaTypeTvShow || type == MediaTypeEpisode || + (type == MediaTypeSeason && item->GetVideoInfoTag()->m_iSeason > 0) || // seasons without "all seasons" and "specials" + type == MediaTypeMusicVideo) + buttons.Add(CONTEXT_BUTTON_EDIT, 16105); + + if (type == MediaTypeMovie || type == MediaTypeTvShow) + buttons.Add(CONTEXT_BUTTON_EDIT_SORTTITLE, 16107); + + if (type == MediaTypeMovie) + { + // only show link/unlink if there are tvshows available + if (database.HasContent(VideoDbContentType::TVSHOWS)) + { + buttons.Add(CONTEXT_BUTTON_LINK_MOVIE, 20384); + if (database.IsLinkedToTvshow(dbId)) + buttons.Add(CONTEXT_BUTTON_UNLINK_MOVIE, 20385); + } + + // set or change movie set the movie belongs to + buttons.Add(CONTEXT_BUTTON_SET_MOVIESET, 20465); + } + + if (type == MediaTypeEpisode && + item->GetVideoInfoTag()->m_iBookmarkId > 0) + buttons.Add(CONTEXT_BUTTON_UNLINK_BOOKMARK, 20405); + + // movie sets + if (item->m_bIsFolder && type == MediaTypeVideoCollection) + { + buttons.Add(CONTEXT_BUTTON_SET_MOVIESET_ART, 13511); + buttons.Add(CONTEXT_BUTTON_MOVIESET_ADD_REMOVE_ITEMS, 20465); + } + + // seasons + if (item->m_bIsFolder && type == MediaTypeSeason) + buttons.Add(CONTEXT_BUTTON_SET_SEASON_ART, 13511); + + // tags + if (item->m_bIsFolder && type == "tag") + { + CVideoDbUrl videoUrl; + if (videoUrl.FromString(item->GetPath())) + { + const std::string &mediaType = videoUrl.GetItemType(); + + buttons.Add( + CONTEXT_BUTTON_TAGS_ADD_ITEMS, + StringUtils::Format(g_localizeStrings.Get(20460), GetLocalizedVideoType(mediaType))); + buttons.Add(CONTEXT_BUTTON_TAGS_REMOVE_ITEMS, StringUtils::Format(g_localizeStrings.Get(20461).c_str(), GetLocalizedVideoType(mediaType).c_str())); + } + } + + if (type != MediaTypeSeason) + buttons.Add(CONTEXT_BUTTON_DELETE, 646); + + //temporary workaround until the context menu ids are removed + const int addonItemOffset = 10000; + + auto addonItems = CServiceBroker::GetContextMenuManager().GetAddonItems(*item, CContextMenuManager::MANAGE); + for (size_t i = 0; i < addonItems.size(); ++i) + buttons.Add(addonItemOffset + i, addonItems[i]->GetLabel(*item)); + + bool result = false; + int button = CGUIDialogContextMenu::ShowAndGetChoice(buttons); + if (button >= 0) + { + switch (static_cast<CONTEXT_BUTTON>(button)) + { + case CONTEXT_BUTTON_EDIT: + result = UpdateVideoItemTitle(item); + break; + + case CONTEXT_BUTTON_EDIT_SORTTITLE: + result = UpdateVideoItemSortTitle(item); + break; + + case CONTEXT_BUTTON_LINK_MOVIE: + result = LinkMovieToTvShow(item, false, database); + break; + + case CONTEXT_BUTTON_UNLINK_MOVIE: + result = LinkMovieToTvShow(item, true, database); + break; + + case CONTEXT_BUTTON_SET_MOVIESET: + { + CFileItemPtr selectedSet; + if (GetSetForMovie(item.get(), selectedSet)) + result = SetMovieSet(item.get(), selectedSet.get()); + break; + } + + case CONTEXT_BUTTON_UNLINK_BOOKMARK: + database.DeleteBookMarkForEpisode(*item->GetVideoInfoTag()); + result = true; + break; + + case CONTEXT_BUTTON_DELETE: + result = DeleteVideoItem(item); + break; + + case CONTEXT_BUTTON_SET_MOVIESET_ART: + case CONTEXT_BUTTON_SET_SEASON_ART: + result = ManageVideoItemArtwork(item, type); + break; + + case CONTEXT_BUTTON_MOVIESET_ADD_REMOVE_ITEMS: + result = ManageMovieSets(item); + break; + + case CONTEXT_BUTTON_TAGS_ADD_ITEMS: + result = AddItemsToTag(item); + break; + + case CONTEXT_BUTTON_TAGS_REMOVE_ITEMS: + result = RemoveItemsFromTag(item); + break; + + default: + if (button >= addonItemOffset) + result = CONTEXTMENU::LoopFrom(*addonItems[button - addonItemOffset], item); + break; + } + } + + database.Close(); + + if (result) + return button; + + return -1; +} + +//Add change a title's name +bool CGUIDialogVideoInfo::UpdateVideoItemTitle(const std::shared_ptr<CFileItem>& pItem) +{ + if (pItem == nullptr || !pItem->HasVideoInfoTag()) + return false; + + // dont allow update while scanning + if (CVideoLibraryQueue::GetInstance().IsScanningLibrary()) + { + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057}); + return false; + } + + CVideoDatabase database; + if (!database.Open()) + return false; + + int iDbId = pItem->GetVideoInfoTag()->m_iDbId; + MediaType mediaType = pItem->GetVideoInfoTag()->m_type; + + CVideoInfoTag detail; + std::string title; + if (mediaType == MediaTypeMovie) + { + database.GetMovieInfo("", detail, iDbId, VideoDbDetailsNone); + title = detail.m_strTitle; + } + else if (mediaType == MediaTypeVideoCollection) + { + database.GetSetInfo(iDbId, detail); + title = detail.m_strTitle; + } + else if (mediaType == MediaTypeEpisode) + { + database.GetEpisodeInfo(pItem->GetPath(), detail, iDbId, VideoDbDetailsNone); + title = detail.m_strTitle; + } + else if (mediaType == MediaTypeSeason) + { + database.GetSeasonInfo(iDbId, detail); + title = detail.m_strSortTitle.empty() ? detail.m_strTitle : detail.m_strSortTitle; + } + else if (mediaType == MediaTypeTvShow) + { + database.GetTvShowInfo(pItem->GetVideoInfoTag()->m_strFileNameAndPath, detail, iDbId, 0, VideoDbDetailsNone); + title = detail.m_strTitle; + } + else if (mediaType == MediaTypeMusicVideo) + { + database.GetMusicVideoInfo(pItem->GetVideoInfoTag()->m_strFileNameAndPath, detail, iDbId, VideoDbDetailsNone); + title = detail.m_strTitle; + } + + // get the new title + if (!CGUIKeyboardFactory::ShowAndGetInput(title, CVariant{ g_localizeStrings.Get(16105) }, false)) + return false; + + if (mediaType == MediaTypeSeason) + { + detail.m_strSortTitle = title; + std::map<std::string, std::string> artwork; + database.SetDetailsForSeason(detail, artwork, detail.m_iIdShow, detail.m_iDbId); + } + else + { + detail.m_strTitle = title; + VideoDbContentType iType = pItem->GetVideoContentType(); + database.UpdateMovieTitle(iDbId, detail.m_strTitle, iType); + } + + return true; +} + +bool CGUIDialogVideoInfo::CanDeleteVideoItem(const std::shared_ptr<CFileItem>& item) +{ + if (item == nullptr || !item->HasVideoInfoTag()) + return false; + + if (item->GetVideoInfoTag()->m_type == "tag") + return true; + + CQueryParams params; + CVideoDatabaseDirectory::GetQueryParams(item->GetPath(), params); + + return params.GetMovieId() != -1 || + params.GetEpisodeId() != -1 || + params.GetMVideoId() != -1 || + params.GetSetId() != -1 || + (params.GetTvShowId() != -1 && params.GetSeason() <= -1 && + !CVideoDatabaseDirectory::IsAllItem(item->GetPath())); +} + +bool CGUIDialogVideoInfo::DeleteVideoItemFromDatabase(const std::shared_ptr<CFileItem>& item, + bool unavailable /* = false */) +{ + if (item == nullptr || !item->HasVideoInfoTag() || + !CanDeleteVideoItem(item)) + return false; + + // dont allow update while scanning + if (CVideoLibraryQueue::GetInstance().IsScanningLibrary()) + { + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057}); + return false; + } + + CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO); + if (pDialog == nullptr) + return false; + + int heading = -1; + VideoDbContentType type = item->GetVideoContentType(); + const std::string& subtype = item->GetVideoInfoTag()->m_type; + if (subtype != "tag") + { + switch (type) + { + case VideoDbContentType::MOVIES: + heading = 432; + break; + case VideoDbContentType::EPISODES: + heading = 20362; + break; + case VideoDbContentType::TVSHOWS: + heading = 20363; + break; + case VideoDbContentType::MUSICVIDEOS: + heading = 20392; + break; + case VideoDbContentType::MOVIE_SETS: + heading = 646; + break; + default: + return false; + } + } + else + { + heading = 10058; + } + + pDialog->SetHeading(CVariant{heading}); + + if (unavailable) + { + pDialog->SetLine(0, CVariant{g_localizeStrings.Get(662)}); + pDialog->SetLine(1, CVariant{g_localizeStrings.Get(663)}); + } + else + { + pDialog->SetLine(0, + CVariant{StringUtils::Format(g_localizeStrings.Get(433), item->GetLabel())}); + pDialog->SetLine(1, CVariant{""}); + } + pDialog->SetLine(2, CVariant{""}); + pDialog->Open(); + + if (!pDialog->IsConfirmed()) + return false; + + CVideoDatabase database; + database.Open(); + + if (item->GetVideoInfoTag()->m_iDbId < 0) + return false; + + if (subtype == "tag") + { + database.DeleteTag(item->GetVideoInfoTag()->m_iDbId, type); + return true; + } + + switch (type) + { + case VideoDbContentType::MOVIES: + database.DeleteMovie(item->GetVideoInfoTag()->m_iDbId); + break; + case VideoDbContentType::EPISODES: + database.DeleteEpisode(item->GetVideoInfoTag()->m_iDbId); + break; + case VideoDbContentType::TVSHOWS: + database.DeleteTvShow(item->GetVideoInfoTag()->m_iDbId); + break; + case VideoDbContentType::MUSICVIDEOS: + database.DeleteMusicVideo(item->GetVideoInfoTag()->m_iDbId); + break; + case VideoDbContentType::MOVIE_SETS: + database.DeleteSet(item->GetVideoInfoTag()->m_iDbId); + break; + default: + return false; + } + return true; +} + +bool CGUIDialogVideoInfo::DeleteVideoItem(const std::shared_ptr<CFileItem>& item, + bool unavailable /* = false */) +{ + if (item == nullptr) + return false; + + // delete the video item from the database + if (!DeleteVideoItemFromDatabase(item, unavailable)) + return false; + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + // check if the user is allowed to delete the actual file as well + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) && + (profileManager->GetCurrentProfile().getLockMode() == LOCK_MODE_EVERYONE || + !profileManager->GetCurrentProfile().filesLocked() || + g_passwordManager.IsMasterLockUnlocked(true))) + { + std::string strDeletePath = item->GetVideoInfoTag()->GetPath(); + + if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strDeletePath), "VIDEO_TS.IFO")) + { + strDeletePath = URIUtils::GetDirectory(strDeletePath); + if (StringUtils::EndsWithNoCase(strDeletePath, "video_ts/")) + { + URIUtils::RemoveSlashAtEnd(strDeletePath); + strDeletePath = URIUtils::GetDirectory(strDeletePath); + } + } + if (URIUtils::HasSlashAtEnd(strDeletePath)) + item->m_bIsFolder = true; + + // check if the file/directory can be deleted + if (CUtil::SupportsWriteFileOperations(strDeletePath)) + { + item->SetPath(strDeletePath); + + // HACK: stacked files need to be treated as folders in order to be deleted + if (item->IsStack()) + item->m_bIsFolder = true; + + CGUIComponent *gui = CServiceBroker::GetGUI(); + if (gui && gui->ConfirmDelete(item->GetPath())) + CFileUtils::DeleteItem(item); + } + } + + CUtil::DeleteVideoDatabaseDirectoryCache(); + + return true; +} + +bool CGUIDialogVideoInfo::ManageMovieSets(const std::shared_ptr<CFileItem>& item) +{ + if (item == nullptr) + return false; + + CFileItemList originalItems; + CFileItemList selectedItems; + + if (!GetMoviesForSet(item.get(), originalItems, selectedItems) || + selectedItems.Size() == 0) // need at least one item selected + return false; + + VECFILEITEMS original = originalItems.GetList(); + std::sort(original.begin(), original.end(), compFileItemsByDbId); + VECFILEITEMS selected = selectedItems.GetList(); + std::sort(selected.begin(), selected.end(), compFileItemsByDbId); + + bool refreshNeeded = false; + // update the "added" items + VECFILEITEMS addedItems; + set_difference(selected.begin(),selected.end(), original.begin(),original.end(), std::back_inserter(addedItems), compFileItemsByDbId); + for (VECFILEITEMS::const_iterator it = addedItems.begin(); it != addedItems.end(); ++it) + { + if (SetMovieSet(it->get(), item.get())) + refreshNeeded = true; + } + + // update the "deleted" items + CFileItemPtr clearItem(new CFileItem()); + clearItem->GetVideoInfoTag()->m_iDbId = -1; // -1 will be used to clear set + VECFILEITEMS deletedItems; + set_difference(original.begin(),original.end(), selected.begin(),selected.end(), std::back_inserter(deletedItems), compFileItemsByDbId); + for (VECFILEITEMS::iterator it = deletedItems.begin(); it != deletedItems.end(); ++it) + { + if (SetMovieSet(it->get(), clearItem.get())) + refreshNeeded = true; + } + + return refreshNeeded; +} + +bool CGUIDialogVideoInfo::GetMoviesForSet(const CFileItem *setItem, CFileItemList &originalMovies, CFileItemList &selectedMovies) +{ + if (setItem == nullptr || !setItem->HasVideoInfoTag()) + return false; + + CVideoDatabase videodb; + if (!videodb.Open()) + return false; + + std::string baseDir = + StringUtils::Format("videodb://movies/sets/{}", setItem->GetVideoInfoTag()->m_iDbId); + + if (!CDirectory::GetDirectory(baseDir, originalMovies, "", DIR_FLAG_DEFAULTS) || + originalMovies.Size() <= 0) // keep a copy of the original members of the set + return false; + + CFileItemList listItems; + if (!videodb.GetSortedVideos(MediaTypeMovie, "videodb://movies", SortDescription(), listItems) || listItems.Size() <= 0) + return false; + + CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (dialog == nullptr) + return false; + + listItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + + dialog->Reset(); + dialog->SetMultiSelection(true); + dialog->SetHeading(CVariant{g_localizeStrings.Get(20457)}); + dialog->SetItems(listItems); + std::vector<int> selectedIndices; + for (int i = 0; i < originalMovies.Size(); i++) + { + for (int listIndex = 0; listIndex < listItems.Size(); listIndex++) + { + if (listItems.Get(listIndex)->GetVideoInfoTag()->m_iDbId == originalMovies[i]->GetVideoInfoTag()->m_iDbId) + { + selectedIndices.push_back(listIndex); + break; + } + } + } + dialog->SetSelected(selectedIndices); + dialog->EnableButton(true, 186); + dialog->Open(); + + if (dialog->IsConfirmed()) + { + for (int i : dialog->GetSelectedItems()) + selectedMovies.Add(listItems.Get(i)); + return (selectedMovies.Size() > 0); + } + else + return false; +} + +bool CGUIDialogVideoInfo::GetSetForMovie(const CFileItem* movieItem, + std::shared_ptr<CFileItem>& selectedSet) +{ + if (movieItem == nullptr || !movieItem->HasVideoInfoTag()) + return false; + + CVideoDatabase videodb; + if (!videodb.Open()) + return false; + + CFileItemList listItems; + + // " ignoreSingleMovieSets=false " as an option in the url is needed here + // to override the gui-setting "Include sets containing a single movie" + // and retrieve all moviesets + + std::string baseDir = "videodb://movies/sets/?ignoreSingleMovieSets=false"; + + if (!CDirectory::GetDirectory(baseDir, listItems, "", DIR_FLAG_DEFAULTS)) + return false; + listItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + + int currentSetId = 0; + std::string currentSetLabel; + + if (movieItem->GetVideoInfoTag()->m_set.id > currentSetId) + { + currentSetId = movieItem->GetVideoInfoTag()->m_set.id; + currentSetLabel = videodb.GetSetById(currentSetId); + } + + if (currentSetId > 0) + { + // remove duplicate entry + for (int listIndex = 0; listIndex < listItems.Size(); listIndex++) + { + if (listItems.Get(listIndex)->GetVideoInfoTag()->m_iDbId == currentSetId) + { + listItems.Remove(listIndex); + break; + } + } + // add clear item + std::string strClear = StringUtils::Format(g_localizeStrings.Get(20467), currentSetLabel); + CFileItemPtr clearItem(new CFileItem(strClear)); + clearItem->GetVideoInfoTag()->m_iDbId = -1; // -1 will be used to clear set + listItems.AddFront(clearItem, 0); + // add keep current set item + std::string strKeep = StringUtils::Format(g_localizeStrings.Get(20469), currentSetLabel); + CFileItemPtr keepItem(new CFileItem(strKeep)); + keepItem->GetVideoInfoTag()->m_iDbId = currentSetId; + listItems.AddFront(keepItem, 1); + } + + CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (dialog == nullptr) + return false; + + dialog->Reset(); + dialog->SetHeading(CVariant{g_localizeStrings.Get(20466)}); + dialog->SetItems(listItems); + if (currentSetId >= 0) + { + for (int listIndex = 0; listIndex < listItems.Size(); listIndex++) + { + if (listItems.Get(listIndex)->GetVideoInfoTag()->m_iDbId == currentSetId) + { + dialog->SetSelected(listIndex); + break; + } + } + } + dialog->EnableButton(true, 20468); // new set via button + dialog->Open(); + + if (dialog->IsButtonPressed()) + { // creating new set + std::string newSetTitle; + if (!CGUIKeyboardFactory::ShowAndGetInput(newSetTitle, CVariant{g_localizeStrings.Get(20468)}, false)) + return false; + int idSet = videodb.AddSet(newSetTitle); + std::map<std::string, std::string> movieArt, setArt; + if (!videodb.GetArtForItem(idSet, MediaTypeVideoCollection, setArt)) + { + videodb.GetArtForItem(movieItem->GetVideoInfoTag()->m_iDbId, MediaTypeMovie, movieArt); + videodb.SetArtForItem(idSet, MediaTypeVideoCollection, movieArt); + } + CFileItemPtr newSet(new CFileItem(newSetTitle)); + newSet->GetVideoInfoTag()->m_iDbId = idSet; + selectedSet = newSet; + return true; + } + else if (dialog->IsConfirmed()) + { + selectedSet = dialog->GetSelectedFileItem(); + return (selectedSet != nullptr); + } + else + return false; +} + +bool CGUIDialogVideoInfo::SetMovieSet(const CFileItem *movieItem, const CFileItem *selectedSet) +{ + if (movieItem == nullptr || !movieItem->HasVideoInfoTag() || + selectedSet == nullptr || !selectedSet->HasVideoInfoTag()) + return false; + + CVideoDatabase videodb; + if (!videodb.Open()) + return false; + + videodb.SetMovieSet(movieItem->GetVideoInfoTag()->m_iDbId, selectedSet->GetVideoInfoTag()->m_iDbId); + return true; +} + +bool CGUIDialogVideoInfo::GetItemsForTag(const std::string &strHeading, const std::string &type, CFileItemList &items, int idTag /* = -1 */, bool showAll /* = true */) +{ + CVideoDatabase videodb; + if (!videodb.Open()) + return false; + + MediaType mediaType = MediaTypeNone; + std::string baseDir = "videodb://"; + std::string idColumn; + if (type.compare(MediaTypeMovie) == 0) + { + mediaType = MediaTypeMovie; + baseDir += "movies"; + idColumn = "idMovie"; + } + else if (type.compare(MediaTypeTvShow) == 0) + { + mediaType = MediaTypeTvShow; + baseDir += "tvshows"; + idColumn = "idShow"; + } + else if (type.compare(MediaTypeMusicVideo) == 0) + { + mediaType = MediaTypeMusicVideo; + baseDir += "musicvideos"; + idColumn = "idMVideo"; + } + + baseDir += "/titles/"; + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(baseDir)) + return false; + + CVideoDatabase::Filter filter; + if (idTag > 0) + { + if (!showAll) + videoUrl.AddOption("tagid", idTag); + else + filter.where = videodb.PrepareSQL("%s_view.%s NOT IN (SELECT tag_link.media_id FROM tag_link WHERE tag_link.tag_id = %d AND tag_link.media_type = '%s')", type.c_str(), idColumn.c_str(), idTag, type.c_str()); + } + + CFileItemList listItems; + if (!videodb.GetSortedVideos(mediaType, videoUrl.ToString(), SortDescription(), listItems, filter) || listItems.Size() <= 0) + return false; + + CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (dialog == nullptr) + return false; + + listItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + + dialog->Reset(); + dialog->SetMultiSelection(true); + dialog->SetHeading(CVariant{strHeading}); + dialog->SetItems(listItems); + dialog->EnableButton(true, 186); + dialog->Open(); + + for (int i : dialog->GetSelectedItems()) + items.Add(listItems.Get(i)); + return items.Size() > 0; +} + +bool CGUIDialogVideoInfo::AddItemsToTag(const std::shared_ptr<CFileItem>& tagItem) +{ + if (tagItem == nullptr || !tagItem->HasVideoInfoTag()) + return false; + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(tagItem->GetPath())) + return false; + + CVideoDatabase videodb; + if (!videodb.Open()) + return true; + + std::string mediaType = videoUrl.GetItemType(); + mediaType = mediaType.substr(0, mediaType.length() - 1); + + CFileItemList items; + std::string localizedType = GetLocalizedVideoType(mediaType); + std::string strLabel = StringUtils::Format(g_localizeStrings.Get(20464), localizedType); + if (!GetItemsForTag(strLabel, mediaType, items, tagItem->GetVideoInfoTag()->m_iDbId)) + return true; + + for (int index = 0; index < items.Size(); index++) + { + if (!items[index]->HasVideoInfoTag() || items[index]->GetVideoInfoTag()->m_iDbId <= 0) + continue; + + videodb.AddTagToItem(items[index]->GetVideoInfoTag()->m_iDbId, tagItem->GetVideoInfoTag()->m_iDbId, mediaType); + } + + return true; +} + +bool CGUIDialogVideoInfo::RemoveItemsFromTag(const std::shared_ptr<CFileItem>& tagItem) +{ + if (tagItem == nullptr || !tagItem->HasVideoInfoTag()) + return false; + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(tagItem->GetPath())) + return false; + + CVideoDatabase videodb; + if (!videodb.Open()) + return true; + + std::string mediaType = videoUrl.GetItemType(); + mediaType = mediaType.substr(0, mediaType.length() - 1); + + CFileItemList items; + std::string localizedType = GetLocalizedVideoType(mediaType); + std::string strLabel = StringUtils::Format(g_localizeStrings.Get(20464), localizedType); + if (!GetItemsForTag(strLabel, mediaType, items, tagItem->GetVideoInfoTag()->m_iDbId, false)) + return true; + + for (int index = 0; index < items.Size(); index++) + { + if (!items[index]->HasVideoInfoTag() || items[index]->GetVideoInfoTag()->m_iDbId <= 0) + continue; + + videodb.RemoveTagFromItem(items[index]->GetVideoInfoTag()->m_iDbId, tagItem->GetVideoInfoTag()->m_iDbId, mediaType); + } + + return true; +} + +namespace +{ +std::string FindLocalMovieSetArtworkFile(const CFileItemPtr& item, const std::string& artType) +{ + std::string infoFolder = VIDEO::CVideoInfoScanner::GetMovieSetInfoFolder(item->GetLabel()); + if (infoFolder.empty()) + return ""; + + CFileItemList availableArtFiles; + CDirectory::GetDirectory(infoFolder, availableArtFiles, + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), + DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO); + for (const auto& artFile : availableArtFiles) + { + std::string candidate = URIUtils::GetFileName(artFile->GetPath()); + URIUtils::RemoveExtension(candidate); + if (StringUtils::EqualsNoCase(candidate, artType)) + return artFile->GetPath(); + } + return ""; +} +} // namespace + +bool CGUIDialogVideoInfo::ManageVideoItemArtwork(const std::shared_ptr<CFileItem>& item, + const MediaType& type) +{ + if (item == nullptr || !item->HasVideoInfoTag() || type.empty()) + return false; + + CVideoDatabase videodb; + if (!videodb.Open()) + return true; + + // Grab the thumbnails from the web + CFileItemList items; + CFileItemPtr noneitem(new CFileItem("thumb://None", false)); + std::string currentThumb; + int idArtist = -1; + std::string artistPath; + std::string artistOldPath; + std::string artType = "thumb"; + if (type == MediaTypeArtist) + { + CMusicDatabase musicdb; + if (musicdb.Open()) + { + idArtist = musicdb.GetArtistByName(item->GetLabel()); // Fails when name not unique + if (idArtist >= 0 ) + { + // Get artist paths - possible locations for thumb - while music db open + musicdb.GetOldArtistPath(idArtist, artistOldPath); // Old artist path, local to music files + CArtist artist; + musicdb.GetArtist(idArtist, artist); // Need name and mbid for artist folder name + musicdb.GetArtistPath(artist, artistPath); // Artist path in artist info folder + + currentThumb = musicdb.GetArtForItem(idArtist, MediaTypeArtist, "thumb"); + if (currentThumb.empty()) + currentThumb = videodb.GetArtForItem(item->GetVideoInfoTag()->m_iDbId, item->GetVideoInfoTag()->m_type, artType); + } + } + } + else if (type == "actor") + currentThumb = videodb.GetArtForItem(item->GetVideoInfoTag()->m_iDbId, item->GetVideoInfoTag()->m_type, artType); + else + { // SEASON, SET + artType = ChooseArtType(*item); + if (artType.empty()) + return false; + + if (artType == "fanart" && type != MediaTypeVideoCollection) + return OnGetFanart(item); + + if (item->HasArt(artType)) + currentThumb = item->GetArt(artType); + else if ((artType == "poster" || artType == "banner") && item->HasArt("thumb")) + currentThumb = item->GetArt("thumb"); + } + + if (!currentThumb.empty()) + { + CFileItemPtr item(new CFileItem("thumb://Current", false)); + item->SetArt("thumb", currentThumb); + item->SetLabel(g_localizeStrings.Get(13512)); + items.Add(item); + } + noneitem->SetArt("icon", "DefaultFolder.png"); + noneitem->SetLabel(g_localizeStrings.Get(13515)); + + bool local = false; + std::vector<std::string> thumbs; + if (type != MediaTypeArtist) + { + CVideoInfoTag tag; + if (type == MediaTypeSeason) + { + videodb.GetTvShowInfo("", tag, item->GetVideoInfoTag()->m_iIdShow); + tag.m_strPictureURL.Parse(); + tag.m_strPictureURL.GetThumbUrls(thumbs, artType, item->GetVideoInfoTag()->m_iSeason); + } + else if (type == MediaTypeVideoCollection) + { + CFileItemList items; + std::string baseDir = + StringUtils::Format("videodb://movies/sets/{}", item->GetVideoInfoTag()->m_iDbId); + if (videodb.GetMoviesNav(baseDir, items)) + { + for (int i=0; i < items.Size(); i++) + { + CVideoInfoTag* pTag = items[i]->GetVideoInfoTag(); + pTag->m_strPictureURL.Parse(); + pTag->m_strPictureURL.GetThumbUrls(thumbs, "set." + artType, -1, true); + } + } + } + else + { + tag = *item->GetVideoInfoTag(); + tag.m_strPictureURL.Parse(); + tag.m_strPictureURL.GetThumbUrls(thumbs, artType); + } + + for (size_t i = 0; i < thumbs.size(); i++) + { + CFileItemPtr item(new CFileItem(StringUtils::Format("thumb://Remote{0}", i), false)); + item->SetArt("thumb", thumbs[i]); + item->SetArt("icon", "DefaultPicture.png"); + item->SetLabel(g_localizeStrings.Get(13513)); + items.Add(item); + + //! @todo Do we need to clear the cached image? + // CServiceBroker::GetTextureCache()->ClearCachedImage(thumbs[i]); + } + + if (type == "actor") + { + std::string picturePath; + std::string strThumb = URIUtils::AddFileToFolder(picturePath, "folder.jpg"); + if (CFileUtils::Exists(strThumb)) + { + CFileItemPtr pItem(new CFileItem(strThumb,false)); + pItem->SetLabel(g_localizeStrings.Get(13514)); + pItem->SetArt("thumb", strThumb); + items.Add(pItem); + local = true; + } + else + noneitem->SetArt("icon", "DefaultActor.png"); + } + + if (type == MediaTypeVideoCollection) + { + std::string localFile = FindLocalMovieSetArtworkFile(item, artType); + if (!localFile.empty()) + { + CFileItemPtr pItem(new CFileItem(localFile, false)); + pItem->SetLabel(g_localizeStrings.Get(13514)); + pItem->SetArt("thumb", localFile); + items.Add(pItem); + local = true; + } + else + noneitem->SetArt("icon", "DefaultVideo.png"); + } + } + else + { + std::string strThumb; + bool existsThumb = false; + // First look for artist thumb in the primary location + if (!artistPath.empty()) + { + strThumb = URIUtils::AddFileToFolder(artistPath, "folder.jpg"); + existsThumb = CFileUtils::Exists(strThumb); + } + // If not there fall back local to music files (historic location for those album artists with a unique folder) + if (!existsThumb && !artistOldPath.empty()) + { + strThumb = URIUtils::AddFileToFolder(artistOldPath, "folder.jpg"); + existsThumb = CFileUtils::Exists(strThumb); + } + + if (existsThumb) + { + CFileItemPtr pItem(new CFileItem(strThumb, false)); + pItem->SetLabel(g_localizeStrings.Get(13514)); + pItem->SetArt("thumb", strThumb); + items.Add(pItem); + local = true; + } + else + noneitem->SetArt("icon", "DefaultArtist.png"); + } + + if (!local) + items.Add(noneitem); + + std::string result; + VECSOURCES sources=*CMediaSourceSettings::GetInstance().GetSources("video"); + CServiceBroker::GetMediaManager().GetLocalDrives(sources); + if (type == MediaTypeVideoCollection) + { + AddItemPathStringToFileBrowserSources(sources, + VIDEO::CVideoInfoScanner::GetMovieSetInfoFolder(item->GetLabel()), + g_localizeStrings.Get(36041)); + AddItemPathStringToFileBrowserSources(sources, + CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER), + "* " + g_localizeStrings.Get(20226)); + } + else + AddItemPathToFileBrowserSources(sources, *item); + if (!CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result)) + return false; // user cancelled + + if (result == "thumb://Current") + result = currentThumb; // user chose the one they have + + // delete the thumbnail if that's what the user wants, else overwrite with the + // new thumbnail + if (result == "thumb://None") + result.clear(); + else if (StringUtils::StartsWith(result, "thumb://Remote")) + { + int number = atoi(StringUtils::Mid(result, 14).c_str()); + result = thumbs[number]; + } + + // write the selected artwork to the database + if (type == MediaTypeVideoCollection || + type == "actor" || + type == MediaTypeSeason || + (type == MediaTypeArtist && idArtist < 0)) + videodb.SetArtForItem(item->GetVideoInfoTag()->m_iDbId, item->GetVideoInfoTag()->m_type, artType, result); + else + { + CMusicDatabase musicdb; + if (musicdb.Open()) + musicdb.SetArtForItem(idArtist, MediaTypeArtist, artType, result); + } + + item->SetArt(artType, result); + + CUtil::DeleteVideoDatabaseDirectoryCache(); + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + + return true; +} + +std::string CGUIDialogVideoInfo::GetLocalizedVideoType(const std::string &strType) +{ + if (CMediaTypes::IsMediaType(strType, MediaTypeMovie)) + return g_localizeStrings.Get(20342); + else if (CMediaTypes::IsMediaType(strType, MediaTypeTvShow)) + return g_localizeStrings.Get(20343); + else if (CMediaTypes::IsMediaType(strType, MediaTypeEpisode)) + return g_localizeStrings.Get(20359); + else if (CMediaTypes::IsMediaType(strType, MediaTypeMusicVideo)) + return g_localizeStrings.Get(20391); + + return ""; +} + +bool CGUIDialogVideoInfo::UpdateVideoItemSortTitle(const std::shared_ptr<CFileItem>& pItem) +{ + // dont allow update while scanning + if (CVideoLibraryQueue::GetInstance().IsScanningLibrary()) + { + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057}); + return false; + } + + CVideoDatabase database; + if (!database.Open()) + return false; + + int iDbId = pItem->GetVideoInfoTag()->m_iDbId; + CVideoInfoTag detail; + VideoDbContentType iType = pItem->GetVideoContentType(); + if (iType == VideoDbContentType::MOVIES) + database.GetMovieInfo("", detail, iDbId, VideoDbDetailsNone); + else if (iType == VideoDbContentType::TVSHOWS) + database.GetTvShowInfo(pItem->GetVideoInfoTag()->m_strFileNameAndPath, detail, iDbId, 0, VideoDbDetailsNone); + + std::string currentTitle; + if (detail.m_strSortTitle.empty()) + currentTitle = detail.m_strTitle; + else + currentTitle = detail.m_strSortTitle; + + // get the new sort title + if (!CGUIKeyboardFactory::ShowAndGetInput(currentTitle, CVariant{g_localizeStrings.Get(16107)}, false)) + return false; + + return database.UpdateVideoSortTitle(iDbId, currentTitle, iType); +} + +bool CGUIDialogVideoInfo::LinkMovieToTvShow(const std::shared_ptr<CFileItem>& item, + bool bRemove, + CVideoDatabase& database) +{ + int dbId = item->GetVideoInfoTag()->m_iDbId; + + CFileItemList list; + if (bRemove) + { + std::vector<int> ids; + if (!database.GetLinksToTvShow(dbId, ids)) + return false; + + for (unsigned int i = 0; i < ids.size(); ++i) + { + CVideoInfoTag tag; + database.GetTvShowInfo("", tag, ids[i], 0 , VideoDbDetailsNone); + CFileItemPtr show(new CFileItem(tag)); + list.Add(show); + } + } + else + { + database.GetTvShowsNav("videodb://tvshows/titles", list); + + // remove already linked shows + std::vector<int> ids; + if (!database.GetLinksToTvShow(dbId, ids)) + return false; + + for (int i = 0; i < list.Size(); ) + { + size_t j; + for (j = 0; j < ids.size(); ++j) + { + if (list[i]->GetVideoInfoTag()->m_iDbId == ids[j]) + break; + } + if (j == ids.size()) + i++; + else + list.Remove(i); + } + } + + int iSelectedLabel = 0; + if (list.Size() > 1 || (!bRemove && !list.IsEmpty())) + { + list.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + CGUIDialogSelect* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + if (pDialog) + { + pDialog->Reset(); + pDialog->SetItems(list); + pDialog->SetHeading(CVariant{20356}); + pDialog->Open(); + iSelectedLabel = pDialog->GetSelectedItem(); + } + } + + if (iSelectedLabel > -1 && iSelectedLabel < list.Size()) + return database.LinkMovieToTvshow(dbId, list[iSelectedLabel]->GetVideoInfoTag()->m_iDbId, bRemove); + + return false; +} + +bool CGUIDialogVideoInfo::OnGetFanart(const std::shared_ptr<CFileItem>& videoItem) +{ + if (videoItem == nullptr || !videoItem->HasVideoInfoTag()) + return false; + + // update the db + CVideoDatabase videodb; + if (!videodb.Open()) + return false; + + CVideoThumbLoader loader; + CFileItem item(*videoItem); + loader.LoadItem(&item); + + CFileItemList items; + if (item.HasArt("fanart")) + { + CFileItemPtr itemCurrent(new CFileItem("fanart://Current", false)); + itemCurrent->SetArt("thumb", item.GetArt("fanart")); + itemCurrent->SetLabel(g_localizeStrings.Get(20440)); + items.Add(itemCurrent); + } + + // add the none option + { + CFileItemPtr itemNone(new CFileItem("fanart://None", false)); + itemNone->SetArt("icon", "DefaultVideo.png"); + itemNone->SetLabel(g_localizeStrings.Get(20439)); + items.Add(itemNone); + } + + std::string result; + VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("video")); + CServiceBroker::GetMediaManager().GetLocalDrives(sources); + AddItemPathToFileBrowserSources(sources, item); + bool flip = false; + if (!CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(20437), result, &flip, 20445) || + StringUtils::EqualsNoCase(result, "fanart://Current")) + return false; + + else if (StringUtils::EqualsNoCase(result, "fanart://None") || !CFileUtils::Exists(result)) + result.clear(); + if (!result.empty() && flip) + result = CTextureUtils::GetWrappedImageURL(result, "", "flipped"); + + videodb.SetArtForItem(item.GetVideoInfoTag()->m_iDbId, item.GetVideoInfoTag()->m_type, "fanart", result); + + // clear view cache and reload images + CUtil::DeleteVideoDatabaseDirectoryCache(); + + return true; +} + +void CGUIDialogVideoInfo::ShowFor(const CFileItem& item) +{ + auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowVideoNav>(WINDOW_VIDEO_NAV); + if (window) + { + ADDON::ScraperPtr info; + window->OnItemInfo(item, info); + } +} diff --git a/xbmc/video/dialogs/GUIDialogVideoInfo.h b/xbmc/video/dialogs/GUIDialogVideoInfo.h new file mode 100644 index 0000000..ac19e98 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoInfo.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "MediaSource.h" +#include "guilib/GUIDialog.h" +#include "media/MediaType.h" + +#include <memory> + +class CFileItem; +class CFileItemList; +class CVideoDatabase; + +class CGUIDialogVideoInfo : + public CGUIDialog +{ +public: + CGUIDialogVideoInfo(void); + ~CGUIDialogVideoInfo(void) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + void SetMovie(const CFileItem *item); + bool NeedRefresh() const; + bool RefreshAll() const; + bool HasUpdatedThumb() const { return m_hasUpdatedThumb; } + bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; } + + std::string GetThumbnail() const; + std::shared_ptr<CFileItem> GetCurrentListItem(int offset = 0) override { return m_movieItem; } + const CFileItemList& CurrentDirectory() const { return *m_castList; } + bool HasListItems() const override { return true; } + + static void AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item); + + static int ManageVideoItem(const std::shared_ptr<CFileItem>& item); + static bool UpdateVideoItemTitle(const std::shared_ptr<CFileItem>& pItem); + static bool CanDeleteVideoItem(const std::shared_ptr<CFileItem>& item); + static bool DeleteVideoItemFromDatabase(const std::shared_ptr<CFileItem>& item, + bool unavailable = false); + static bool DeleteVideoItem(const std::shared_ptr<CFileItem>& item, bool unavailable = false); + + static bool ManageMovieSets(const std::shared_ptr<CFileItem>& item); + static bool GetMoviesForSet(const CFileItem *setItem, CFileItemList &originalMovies, CFileItemList &selectedMovies); + static bool GetSetForMovie(const CFileItem* movieItem, std::shared_ptr<CFileItem>& selectedSet); + static bool SetMovieSet(const CFileItem *movieItem, const CFileItem *selectedSet); + + static bool GetItemsForTag(const std::string &strHeading, const std::string &type, CFileItemList &items, int idTag = -1, bool showAll = true); + static bool AddItemsToTag(const std::shared_ptr<CFileItem>& tagItem); + static bool RemoveItemsFromTag(const std::shared_ptr<CFileItem>& tagItem); + + static bool ManageVideoItemArtwork(const std::shared_ptr<CFileItem>& item, const MediaType& type); + + static std::string GetLocalizedVideoType(const std::string &strType); + + static void ShowFor(const CFileItem& item); + +protected: + void OnInitWindow() override; + void Update(); + void SetLabel(int iControl, const std::string& strLabel); + void SetUserrating(int userrating) const; + + // link cast to movies + void ClearCastList(); + /** + * \brief Search the current directory for a string got from the virtual keyboard + * \param strSearch The search string + */ + void OnSearch(std::string& strSearch); + /** + * \brief Make the actual search for the OnSearch function. + * \param strSearch The search string + * \param items Items Found + */ + void DoSearch(std::string& strSearch, CFileItemList& items) const; + /** + * \brief React on the selected search item + * \param pItem Search result item + */ + void OnSearchItemFound(const CFileItem* pItem); + void Play(bool resume = false); + void OnGetArt(); + void OnGetFanart(); + void OnSetUserrating() const; + void PlayTrailer(); + + static bool UpdateVideoItemSortTitle(const std::shared_ptr<CFileItem>& pItem); + static bool LinkMovieToTvShow(const std::shared_ptr<CFileItem>& item, + bool bRemove, + CVideoDatabase& database); + + /*! \brief Pop up a fanart chooser. Does not utilise remote URLs. + \param videoItem the item to choose fanart for. + */ + static bool OnGetFanart(const std::shared_ptr<CFileItem>& videoItem); + + std::shared_ptr<CFileItem> m_movieItem; + CFileItemList *m_castList; + bool m_bViewReview = false; + bool m_bRefresh = false; + bool m_bRefreshAll = true; + bool m_hasUpdatedThumb = false; + bool m_hasUpdatedUserrating = false; + int m_startUserrating = -1; + +private: + static std::string ChooseArtType(const CFileItem& item); +}; diff --git a/xbmc/video/dialogs/GUIDialogVideoOSD.cpp b/xbmc/video/dialogs/GUIDialogVideoOSD.cpp new file mode 100644 index 0000000..3b8009d --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoOSD.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogVideoOSD.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "input/InputManager.h" +#include "input/actions/ActionIDs.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +using namespace PVR; + +CGUIDialogVideoOSD::CGUIDialogVideoOSD(void) + : CGUIDialog(WINDOW_DIALOG_VIDEO_OSD, "VideoOSD.xml") +{ + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogVideoOSD::~CGUIDialogVideoOSD(void) = default; + +void CGUIDialogVideoOSD::FrameMove() +{ + if (m_autoClosing) + { + // check for movement of mouse or a submenu open + if (CServiceBroker::GetInputManager().IsMouseActive() + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_AUDIO_OSD_SETTINGS) + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS) + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIDEO_OSD_SETTINGS) + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_CMS_OSD_SETTINGS) + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIDEO_BOOKMARKS) + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_PVR_OSD_CHANNELS) + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_PVR_CHANNEL_GUIDE) + || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_OSD_TELETEXT)) + // extend show time by original value + SetAutoClose(m_showDuration); + } + CGUIDialog::FrameMove(); +} + +bool CGUIDialogVideoOSD::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_SHOW_OSD) + { + Close(); + return true; + } + + return CGUIDialog::OnAction(action); +} + +EVENT_RESULT CGUIDialogVideoOSD::OnMouseEvent(const CPoint &point, const CMouseEvent &event) +{ + if (event.m_id == ACTION_MOUSE_WHEEL_UP) + { + return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_FORWARD, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED; + } + if (event.m_id == ACTION_MOUSE_WHEEL_DOWN) + { + return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_BACK, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED; + } + + return CGUIDialog::OnMouseEvent(point, event); +} + +bool CGUIDialogVideoOSD::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_VIDEO_MENU_STARTED: + { + // We have gone to the DVD menu, so close the OSD. + Close(); + } + break; + case GUI_MSG_WINDOW_DEINIT: // fired when OSD is hidden + { + // Remove our subdialogs if visible + CGUIDialog *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_AUDIO_OSD_SETTINGS); + if (pDialog && pDialog->IsDialogRunning()) + pDialog->Close(true); + pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS); + if (pDialog && pDialog->IsDialogRunning()) + pDialog->Close(true); + } + break; + } + return CGUIDialog::OnMessage(message); +} + diff --git a/xbmc/video/dialogs/GUIDialogVideoOSD.h b/xbmc/video/dialogs/GUIDialogVideoOSD.h new file mode 100644 index 0000000..95cfc9f --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoOSD.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "guilib/GUIDialog.h" + +class CGUIDialogVideoOSD : public CGUIDialog +{ +public: + + CGUIDialogVideoOSD(void); + ~CGUIDialogVideoOSD(void) override; + + void FrameMove() override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; +protected: + EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override; +}; diff --git a/xbmc/video/dialogs/GUIDialogVideoSettings.cpp b/xbmc/video/dialogs/GUIDialogVideoSettings.cpp new file mode 100644 index 0000000..24d9d22 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoSettings.cpp @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2005-2018 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 "GUIDialogVideoSettings.h" + +#include "GUIPassword.h" +#include "ServiceBroker.h" +#include "addons/Skin.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "profiles/ProfileManager.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "settings/lib/SettingDefinitions.h" +#include "settings/lib/SettingsManager.h" +#include "utils/LangCodeExpander.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" +#include "video/ViewModeSettings.h" + +#include <utility> + +#define SETTING_VIDEO_VIEW_MODE "video.viewmode" +#define SETTING_VIDEO_ZOOM "video.zoom" +#define SETTING_VIDEO_PIXEL_RATIO "video.pixelratio" +#define SETTING_VIDEO_BRIGHTNESS "video.brightness" +#define SETTING_VIDEO_CONTRAST "video.contrast" +#define SETTING_VIDEO_GAMMA "video.gamma" +#define SETTING_VIDEO_NONLIN_STRETCH "video.nonlinearstretch" +#define SETTING_VIDEO_POSTPROCESS "video.postprocess" +#define SETTING_VIDEO_VERTICAL_SHIFT "video.verticalshift" +#define SETTING_VIDEO_TONEMAP_METHOD "video.tonemapmethod" +#define SETTING_VIDEO_TONEMAP_PARAM "video.tonemapparam" +#define SETTING_VIDEO_ORIENTATION "video.orientation" + +#define SETTING_VIDEO_VDPAU_NOISE "vdpau.noise" +#define SETTING_VIDEO_VDPAU_SHARPNESS "vdpau.sharpness" + +#define SETTING_VIDEO_INTERLACEMETHOD "video.interlacemethod" +#define SETTING_VIDEO_SCALINGMETHOD "video.scalingmethod" + +#define SETTING_VIDEO_STEREOSCOPICMODE "video.stereoscopicmode" +#define SETTING_VIDEO_STEREOSCOPICINVERT "video.stereoscopicinvert" + +#define SETTING_VIDEO_MAKE_DEFAULT "video.save" +#define SETTING_VIDEO_CALIBRATION "video.calibration" +#define SETTING_VIDEO_STREAM "video.stream" + +CGUIDialogVideoSettings::CGUIDialogVideoSettings() + : CGUIDialogSettingsManualBase(WINDOW_DIALOG_VIDEO_OSD_SETTINGS, "DialogSettings.xml") +{ } + +CGUIDialogVideoSettings::~CGUIDialogVideoSettings() = default; + +void CGUIDialogVideoSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + return; + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_VIDEO_INTERLACEMETHOD) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_InterlaceMethod = static_cast<EINTERLACEMETHOD>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_SCALINGMETHOD) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_ScalingMethod = static_cast<ESCALINGMETHOD>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_STREAM) + { + m_videoStream = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + // only change the video stream if a different one has been asked for + if (appPlayer->GetVideoStream() != m_videoStream) + { + appPlayer->SetVideoStream(m_videoStream); // Set the video stream to the one selected + } + } + else if (settingId == SETTING_VIDEO_VIEW_MODE) + { + int value = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + const CVideoSettings vs = appPlayer->GetVideoSettings(); + + appPlayer->SetRenderViewMode(value, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + + m_viewModeChanged = true; + GetSettingsManager()->SetNumber(SETTING_VIDEO_ZOOM, static_cast<double>(vs.m_CustomZoomAmount)); + GetSettingsManager()->SetNumber(SETTING_VIDEO_PIXEL_RATIO, + static_cast<double>(vs.m_CustomPixelRatio)); + GetSettingsManager()->SetNumber(SETTING_VIDEO_VERTICAL_SHIFT, + static_cast<double>(vs.m_CustomVerticalShift)); + GetSettingsManager()->SetBool(SETTING_VIDEO_NONLIN_STRETCH, vs.m_CustomNonLinStretch); + m_viewModeChanged = false; + } + else if (settingId == SETTING_VIDEO_ZOOM || + settingId == SETTING_VIDEO_VERTICAL_SHIFT || + settingId == SETTING_VIDEO_PIXEL_RATIO || + settingId == SETTING_VIDEO_NONLIN_STRETCH) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + if (settingId == SETTING_VIDEO_ZOOM) + vs.m_CustomZoomAmount = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_VERTICAL_SHIFT) + vs.m_CustomVerticalShift = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_PIXEL_RATIO) + vs.m_CustomPixelRatio = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + else if (settingId == SETTING_VIDEO_NONLIN_STRETCH) + vs.m_CustomNonLinStretch = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + + // try changing the view mode to custom. If it already is set to custom + // manually call the render manager + if (GetSettingsManager()->GetInt(SETTING_VIDEO_VIEW_MODE) != ViewModeCustom) + GetSettingsManager()->SetInt(SETTING_VIDEO_VIEW_MODE, ViewModeCustom); + else + appPlayer->SetRenderViewMode(vs.m_ViewMode, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + } + else if (settingId == SETTING_VIDEO_POSTPROCESS) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_PostProcess = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_BRIGHTNESS) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_Brightness = static_cast<float>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_CONTRAST) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_Contrast = static_cast<float>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_GAMMA) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_Gamma = static_cast<float>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_VDPAU_NOISE) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_NoiseReduction = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_VDPAU_SHARPNESS) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_Sharpness = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_TONEMAP_METHOD) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_ToneMapMethod = static_cast<ETONEMAPMETHOD>( + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_TONEMAP_PARAM) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_ToneMapParam = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue()); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_ORIENTATION) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_Orientation = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_STEREOSCOPICMODE) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_StereoMode = std::static_pointer_cast<const CSettingInt>(setting)->GetValue(); + appPlayer->SetVideoSettings(vs); + } + else if (settingId == SETTING_VIDEO_STEREOSCOPICINVERT) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_StereoInvert = std::static_pointer_cast<const CSettingBool>(setting)->GetValue(); + appPlayer->SetVideoSettings(vs); + } +} + +void CGUIDialogVideoSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == NULL) + return; + + CGUIDialogSettingsManualBase::OnSettingChanged(setting); + + const std::string &settingId = setting->GetId(); + if (settingId == SETTING_VIDEO_CALIBRATION) + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return; + + auto settings = settingsComponent->GetSettings(); + if (!settings) + return; + + auto calibsetting = settings->GetSetting(CSettings::SETTING_VIDEOSCREEN_GUICALIBRATION); + if (!calibsetting) + { + CLog::Log(LOGERROR, "Failed to load setting for: {}", + CSettings::SETTING_VIDEOSCREEN_GUICALIBRATION); + return; + } + + // launch calibration window + if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && + g_passwordManager.CheckSettingLevelLock(calibsetting->GetLevel())) + return; + + CServiceBroker::GetGUI()->GetWindowManager().ForceActivateWindow(WINDOW_SCREEN_CALIBRATION); + } + //! @todo implement + else if (settingId == SETTING_VIDEO_MAKE_DEFAULT) + Save(); +} + +bool CGUIDialogVideoSettings::Save() +{ + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && + !g_passwordManager.CheckSettingLevelLock(::SettingLevel::Expert)) + return true; + + // prompt user if they are sure + if (CGUIDialogYesNo::ShowAndGetInput(CVariant(12376), CVariant(12377))) + { // reset the settings + CVideoDatabase db; + if (!db.Open()) + return true; + db.EraseAllVideoSettings(); + db.Close(); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + CMediaSettings::GetInstance().GetDefaultVideoSettings() = appPlayer->GetVideoSettings(); + CMediaSettings::GetInstance().GetDefaultVideoSettings().m_SubtitleStream = -1; + CMediaSettings::GetInstance().GetDefaultVideoSettings().m_AudioStream = -1; + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + } + + return true; +} + +void CGUIDialogVideoSettings::SetupView() +{ + CGUIDialogSettingsManualBase::SetupView(); + + SetHeading(13395); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON); + SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON); + SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067); +} + +void CGUIDialogVideoSettings::InitializeSettings() +{ + CGUIDialogSettingsManualBase::InitializeSettings(); + + const std::shared_ptr<CSettingCategory> category = AddCategory("videosettings", -1); + if (category == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings"); + return; + } + + // get all necessary setting groups + const std::shared_ptr<CSettingGroup> groupVideoStream = AddGroup(category); + if (groupVideoStream == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings"); + return; + } + const std::shared_ptr<CSettingGroup> groupVideo = AddGroup(category); + if (groupVideo == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings"); + return; + } + const std::shared_ptr<CSettingGroup> groupStereoscopic = AddGroup(category); + if (groupStereoscopic == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings"); + return; + } + const std::shared_ptr<CSettingGroup> groupSaveAsDefault = AddGroup(category); + if (groupSaveAsDefault == NULL) + { + CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings"); + return; + } + + bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml"); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + const CVideoSettings videoSettings = appPlayer->GetVideoSettings(); + + TranslatableIntegerSettingOptions entries; + + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(16039, VS_INTERLACEMETHOD_NONE)); + entries.push_back(TranslatableIntegerSettingOption(16019, VS_INTERLACEMETHOD_AUTO)); + entries.push_back(TranslatableIntegerSettingOption(20131, VS_INTERLACEMETHOD_RENDER_BLEND)); + entries.push_back(TranslatableIntegerSettingOption(20129, VS_INTERLACEMETHOD_RENDER_WEAVE)); + entries.push_back(TranslatableIntegerSettingOption(16021, VS_INTERLACEMETHOD_RENDER_BOB)); + entries.push_back(TranslatableIntegerSettingOption(16020, VS_INTERLACEMETHOD_DEINTERLACE)); + entries.push_back(TranslatableIntegerSettingOption(16036, VS_INTERLACEMETHOD_DEINTERLACE_HALF)); + entries.push_back( + TranslatableIntegerSettingOption(16311, VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL)); + entries.push_back(TranslatableIntegerSettingOption(16310, VS_INTERLACEMETHOD_VDPAU_TEMPORAL)); + entries.push_back(TranslatableIntegerSettingOption(16325, VS_INTERLACEMETHOD_VDPAU_BOB)); + entries.push_back( + TranslatableIntegerSettingOption(16318, VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF)); + entries.push_back( + TranslatableIntegerSettingOption(16317, VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF)); + entries.push_back(TranslatableIntegerSettingOption(16327, VS_INTERLACEMETHOD_VAAPI_BOB)); + entries.push_back(TranslatableIntegerSettingOption(16328, VS_INTERLACEMETHOD_VAAPI_MADI)); + entries.push_back(TranslatableIntegerSettingOption(16329, VS_INTERLACEMETHOD_VAAPI_MACI)); + entries.push_back(TranslatableIntegerSettingOption(16320, VS_INTERLACEMETHOD_DXVA_AUTO)); + + /* remove unsupported methods */ + for (TranslatableIntegerSettingOptions::iterator it = entries.begin(); it != entries.end(); ) + { + if (appPlayer->Supports(static_cast<EINTERLACEMETHOD>(it->value))) + ++it; + else + it = entries.erase(it); + } + + if (!entries.empty()) + { + EINTERLACEMETHOD method = videoSettings.m_InterlaceMethod; + if (!appPlayer->Supports(method)) + { + method = appPlayer->GetDeinterlacingMethodDefault(); + } + AddSpinner(groupVideo, SETTING_VIDEO_INTERLACEMETHOD, 16038, SettingLevel::Basic, static_cast<int>(method), entries); + } + + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(16301, VS_SCALINGMETHOD_NEAREST)); + entries.push_back(TranslatableIntegerSettingOption(16302, VS_SCALINGMETHOD_LINEAR)); + entries.push_back(TranslatableIntegerSettingOption(16303, VS_SCALINGMETHOD_CUBIC_B_SPLINE)); + entries.push_back(TranslatableIntegerSettingOption(16314, VS_SCALINGMETHOD_CUBIC_MITCHELL)); + entries.push_back(TranslatableIntegerSettingOption(16321, VS_SCALINGMETHOD_CUBIC_CATMULL)); + entries.push_back(TranslatableIntegerSettingOption(16326, VS_SCALINGMETHOD_CUBIC_0_075)); + entries.push_back(TranslatableIntegerSettingOption(16330, VS_SCALINGMETHOD_CUBIC_0_1)); + entries.push_back(TranslatableIntegerSettingOption(16304, VS_SCALINGMETHOD_LANCZOS2)); + entries.push_back(TranslatableIntegerSettingOption(16323, VS_SCALINGMETHOD_SPLINE36_FAST)); + entries.push_back(TranslatableIntegerSettingOption(16315, VS_SCALINGMETHOD_LANCZOS3_FAST)); + entries.push_back(TranslatableIntegerSettingOption(16322, VS_SCALINGMETHOD_SPLINE36)); + entries.push_back(TranslatableIntegerSettingOption(16305, VS_SCALINGMETHOD_LANCZOS3)); + entries.push_back(TranslatableIntegerSettingOption(16306, VS_SCALINGMETHOD_SINC8)); + entries.push_back(TranslatableIntegerSettingOption(16307, VS_SCALINGMETHOD_BICUBIC_SOFTWARE)); + entries.push_back(TranslatableIntegerSettingOption(16308, VS_SCALINGMETHOD_LANCZOS_SOFTWARE)); + entries.push_back(TranslatableIntegerSettingOption(16309, VS_SCALINGMETHOD_SINC_SOFTWARE)); + entries.push_back(TranslatableIntegerSettingOption(13120, VS_SCALINGMETHOD_VDPAU_HARDWARE)); + entries.push_back(TranslatableIntegerSettingOption(16319, VS_SCALINGMETHOD_DXVA_HARDWARE)); + entries.push_back(TranslatableIntegerSettingOption(16316, VS_SCALINGMETHOD_AUTO)); + + /* remove unsupported methods */ + for(TranslatableIntegerSettingOptions::iterator it = entries.begin(); it != entries.end(); ) + { + if (appPlayer->Supports(static_cast<ESCALINGMETHOD>(it->value))) + ++it; + else + it = entries.erase(it); + } + + AddSpinner(groupVideo, SETTING_VIDEO_SCALINGMETHOD, 16300, SettingLevel::Basic, static_cast<int>(videoSettings.m_ScalingMethod), entries); + + AddVideoStreams(groupVideoStream, SETTING_VIDEO_STREAM); + + if (appPlayer->Supports(RENDERFEATURE_STRETCH) || appPlayer->Supports(RENDERFEATURE_PIXEL_RATIO)) + { + AddList(groupVideo, SETTING_VIDEO_VIEW_MODE, 629, SettingLevel::Basic, videoSettings.m_ViewMode, CViewModeSettings::ViewModesFiller, 629); + } + if (appPlayer->Supports(RENDERFEATURE_ZOOM)) + AddSlider(groupVideo, SETTING_VIDEO_ZOOM, 216, SettingLevel::Basic, + videoSettings.m_CustomZoomAmount, "{:2.2f}", 0.5f, 0.01f, 2.0f, 216, usePopup); + if (appPlayer->Supports(RENDERFEATURE_VERTICAL_SHIFT)) + AddSlider(groupVideo, SETTING_VIDEO_VERTICAL_SHIFT, 225, SettingLevel::Basic, + videoSettings.m_CustomVerticalShift, "{:2.2f}", -2.0f, 0.01f, 2.0f, 225, usePopup); + if (appPlayer->Supports(RENDERFEATURE_PIXEL_RATIO)) + AddSlider(groupVideo, SETTING_VIDEO_PIXEL_RATIO, 217, SettingLevel::Basic, + videoSettings.m_CustomPixelRatio, "{:2.2f}", 0.5f, 0.01f, 2.0f, 217, usePopup); + + AddList(groupVideo, SETTING_VIDEO_ORIENTATION, 21843, SettingLevel::Basic, videoSettings.m_Orientation, CGUIDialogVideoSettings::VideoOrientationFiller, 21843); + + if (appPlayer->Supports(RENDERFEATURE_POSTPROCESS)) + AddToggle(groupVideo, SETTING_VIDEO_POSTPROCESS, 16400, SettingLevel::Basic, videoSettings.m_PostProcess); + if (appPlayer->Supports(RENDERFEATURE_BRIGHTNESS)) + AddPercentageSlider(groupVideo, SETTING_VIDEO_BRIGHTNESS, 464, SettingLevel::Basic, static_cast<int>(videoSettings.m_Brightness), 14047, 1, 464, usePopup); + if (appPlayer->Supports(RENDERFEATURE_CONTRAST)) + AddPercentageSlider(groupVideo, SETTING_VIDEO_CONTRAST, 465, SettingLevel::Basic, static_cast<int>(videoSettings.m_Contrast), 14047, 1, 465, usePopup); + if (appPlayer->Supports(RENDERFEATURE_GAMMA)) + AddPercentageSlider(groupVideo, SETTING_VIDEO_GAMMA, 466, SettingLevel::Basic, static_cast<int>(videoSettings.m_Gamma), 14047, 1, 466, usePopup); + if (appPlayer->Supports(RENDERFEATURE_NOISE)) + AddSlider(groupVideo, SETTING_VIDEO_VDPAU_NOISE, 16312, SettingLevel::Basic, + videoSettings.m_NoiseReduction, "{:2.2f}", 0.0f, 0.01f, 1.0f, 16312, usePopup); + if (appPlayer->Supports(RENDERFEATURE_SHARPNESS)) + AddSlider(groupVideo, SETTING_VIDEO_VDPAU_SHARPNESS, 16313, SettingLevel::Basic, + videoSettings.m_Sharpness, "{:2.2f}", -1.0f, 0.02f, 1.0f, 16313, usePopup); + if (appPlayer->Supports(RENDERFEATURE_NONLINSTRETCH)) + AddToggle(groupVideo, SETTING_VIDEO_NONLIN_STRETCH, 659, SettingLevel::Basic, videoSettings.m_CustomNonLinStretch); + + // tone mapping + if (appPlayer->Supports(RENDERFEATURE_TONEMAP)) + { + bool visible = !(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CServiceBroker::GetWinSystem()->SETTING_WINSYSTEM_IS_HDR_DISPLAY) && + CServiceBroker::GetWinSystem()->IsHDRDisplay()); + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(36554, VS_TONEMAPMETHOD_OFF)); + entries.push_back(TranslatableIntegerSettingOption(36555, VS_TONEMAPMETHOD_REINHARD)); + entries.push_back(TranslatableIntegerSettingOption(36557, VS_TONEMAPMETHOD_ACES)); + entries.push_back(TranslatableIntegerSettingOption(36558, VS_TONEMAPMETHOD_HABLE)); + + AddSpinner(groupVideo, SETTING_VIDEO_TONEMAP_METHOD, 36553, SettingLevel::Basic, + videoSettings.m_ToneMapMethod, entries, false, visible); + AddSlider(groupVideo, SETTING_VIDEO_TONEMAP_PARAM, 36556, SettingLevel::Basic, + videoSettings.m_ToneMapParam, "{:2.2f}", 0.1f, 0.1f, 5.0f, 36556, usePopup, false, + visible); + } + + // stereoscopic settings + entries.clear(); + entries.push_back(TranslatableIntegerSettingOption(16316, RENDER_STEREO_MODE_OFF)); + entries.push_back(TranslatableIntegerSettingOption(36503, RENDER_STEREO_MODE_SPLIT_HORIZONTAL)); + entries.push_back(TranslatableIntegerSettingOption(36504, RENDER_STEREO_MODE_SPLIT_VERTICAL)); + AddSpinner(groupStereoscopic, SETTING_VIDEO_STEREOSCOPICMODE, 36535, SettingLevel::Basic, videoSettings.m_StereoMode, entries); + AddToggle(groupStereoscopic, SETTING_VIDEO_STEREOSCOPICINVERT, 36536, SettingLevel::Basic, videoSettings.m_StereoInvert); + + // general settings + AddButton(groupSaveAsDefault, SETTING_VIDEO_MAKE_DEFAULT, 12376, SettingLevel::Basic); + AddButton(groupSaveAsDefault, SETTING_VIDEO_CALIBRATION, 214, SettingLevel::Basic); +} + +void CGUIDialogVideoSettings::AddVideoStreams(const std::shared_ptr<CSettingGroup>& group, + const std::string& settingId) +{ + if (group == NULL || settingId.empty()) + return; + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + m_videoStream = appPlayer->GetVideoStream(); + if (m_videoStream < 0) + m_videoStream = 0; + + AddList(group, settingId, 38031, SettingLevel::Basic, m_videoStream, VideoStreamsOptionFiller, 38031); +} + +void CGUIDialogVideoSettings::VideoStreamsOptionFiller( + const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + int videoStreamCount = appPlayer->GetVideoStreamCount(); + // cycle through each video stream and add it to our list control + for (int i = 0; i < videoStreamCount; ++i) + { + std::string strItem; + std::string strLanguage; + + VideoStreamInfo info; + appPlayer->GetVideoStreamInfo(i, info); + + g_LangCodeExpander.Lookup(info.language, strLanguage); + + if (!info.name.empty()) + { + if (!strLanguage.empty()) + strItem = StringUtils::Format("{} - {}", strLanguage, info.name); + else + strItem = info.name; + } + else if (!strLanguage.empty()) + { + strItem = strLanguage; + } + + if (info.codecName.empty()) + strItem += StringUtils::Format(" ({}x{}", info.width, info.height); + else + strItem += StringUtils::Format(" ({}, {}x{}", info.codecName, info.width, info.height); + + if (info.bitrate) + strItem += StringUtils::Format(", {} bps)", info.bitrate); + else + strItem += ")"; + + strItem += FormatFlags(info.flags); + strItem += StringUtils::Format(" ({}/{})", i + 1, videoStreamCount); + list.emplace_back(strItem, i); + } + + if (list.empty()) + { + list.emplace_back(g_localizeStrings.Get(231), -1); + current = -1; + } +} + +void CGUIDialogVideoSettings::VideoOrientationFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data) +{ + list.emplace_back(g_localizeStrings.Get(687), 0); + list.emplace_back(g_localizeStrings.Get(35229), 90); + list.emplace_back(g_localizeStrings.Get(35230), 180); + list.emplace_back(g_localizeStrings.Get(35231), 270); +} + +std::string CGUIDialogVideoSettings::FormatFlags(StreamFlags flags) +{ + std::vector<std::string> localizedFlags; + if (flags & StreamFlags::FLAG_DEFAULT) + localizedFlags.emplace_back(g_localizeStrings.Get(39105)); + if (flags & StreamFlags::FLAG_FORCED) + localizedFlags.emplace_back(g_localizeStrings.Get(39106)); + if (flags & StreamFlags::FLAG_HEARING_IMPAIRED) + localizedFlags.emplace_back(g_localizeStrings.Get(39107)); + if (flags & StreamFlags::FLAG_VISUAL_IMPAIRED) + localizedFlags.emplace_back(g_localizeStrings.Get(39108)); + + std::string formated = StringUtils::Join(localizedFlags, ", "); + + if (!formated.empty()) + formated = StringUtils::Format(" [{}]", formated); + + return formated; +} diff --git a/xbmc/video/dialogs/GUIDialogVideoSettings.h b/xbmc/video/dialogs/GUIDialogVideoSettings.h new file mode 100644 index 0000000..5314e40 --- /dev/null +++ b/xbmc/video/dialogs/GUIDialogVideoSettings.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "cores/VideoPlayer/Interface/StreamInfo.h" +#include "settings/dialogs/GUIDialogSettingsManualBase.h" + +#include <string> +#include <utility> +#include <vector> + +struct IntegerSettingOption; + +class CGUIDialogVideoSettings : public CGUIDialogSettingsManualBase +{ +public: + CGUIDialogVideoSettings(); + ~CGUIDialogVideoSettings() override; + +protected: + // implementations of ISettingCallback + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + + void AddVideoStreams(const std::shared_ptr<CSettingGroup>& group, const std::string& settingId); + static void VideoStreamsOptionFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + + static void VideoOrientationFiller(const std::shared_ptr<const CSetting>& setting, + std::vector<IntegerSettingOption>& list, + int& current, + void* data); + + static std::string FormatFlags(StreamFlags flags); + + // specialization of CGUIDialogSettingsBase + bool AllowResettingSettings() const override { return false; } + bool Save() override; + void SetupView() override; + + // specialization of CGUIDialogSettingsManualBase + void InitializeSettings() override; + +private: + int m_videoStream; + bool m_viewModeChanged = false; +}; diff --git a/xbmc/video/jobs/CMakeLists.txt b/xbmc/video/jobs/CMakeLists.txt new file mode 100644 index 0000000..b63d8fe --- /dev/null +++ b/xbmc/video/jobs/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES VideoLibraryCleaningJob.cpp + VideoLibraryJob.cpp + VideoLibraryMarkWatchedJob.cpp + VideoLibraryProgressJob.cpp + VideoLibraryRefreshingJob.cpp + VideoLibraryScanningJob.cpp + VideoLibraryResetResumePointJob.cpp) + +set(HEADERS VideoLibraryCleaningJob.h + VideoLibraryJob.h + VideoLibraryMarkWatchedJob.h + VideoLibraryProgressJob.h + VideoLibraryRefreshingJob.h + VideoLibraryScanningJob.h + VideoLibraryResetResumePointJob.h) + +core_add_library(video_jobs) diff --git a/xbmc/video/jobs/VideoLibraryCleaningJob.cpp b/xbmc/video/jobs/VideoLibraryCleaningJob.cpp new file mode 100644 index 0000000..7f4adfd --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryCleaningJob.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2014-2018 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 "VideoLibraryCleaningJob.h" + +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "video/VideoDatabase.h" + +CVideoLibraryCleaningJob::CVideoLibraryCleaningJob(const std::set<int>& paths /* = std::set<int>() */, bool showDialog /* = false */) + : CVideoLibraryProgressJob(NULL), + m_paths(paths), + m_showDialog(showDialog) +{ } + +CVideoLibraryCleaningJob::CVideoLibraryCleaningJob(const std::set<int>& paths, CGUIDialogProgressBarHandle* progressBar) + : CVideoLibraryProgressJob(progressBar), + m_paths(paths), + m_showDialog(false) +{ } + +CVideoLibraryCleaningJob::~CVideoLibraryCleaningJob() = default; + +bool CVideoLibraryCleaningJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CVideoLibraryCleaningJob* cleaningJob = dynamic_cast<const CVideoLibraryCleaningJob*>(job); + if (cleaningJob == NULL) + return false; + + return m_paths == cleaningJob->m_paths && + m_showDialog == cleaningJob->m_showDialog; +} + +bool CVideoLibraryCleaningJob::Work(CVideoDatabase &db) +{ + db.CleanDatabase(GetProgressBar(), m_paths, m_showDialog); + return true; +} diff --git a/xbmc/video/jobs/VideoLibraryCleaningJob.h b/xbmc/video/jobs/VideoLibraryCleaningJob.h new file mode 100644 index 0000000..f85682c --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryCleaningJob.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014-2018 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. + */ + +#pragma once + +#include "video/jobs/VideoLibraryProgressJob.h" + +#include <set> + +class CGUIDialogProgressBarHandle; + +/*! + \brief Video library job implementation for cleaning the video library. +*/ +class CVideoLibraryCleaningJob : public CVideoLibraryProgressJob +{ +public: + /*! + \brief Creates a new video library cleaning job for the given paths. + + \param[in] paths Set with database IDs of paths to be cleaned + \param[in] showDialog Whether to show a modal dialog or not + */ + CVideoLibraryCleaningJob(const std::set<int>& paths = std::set<int>(), bool showDialog = false); + + /*! + \brief Creates a new video library cleaning job for the given paths. + + \param[in] paths Set with database IDs of paths to be cleaned + \param[in] progressBar Progress bar to be used to display the cleaning progress + */ + CVideoLibraryCleaningJob(const std::set<int>& paths, CGUIDialogProgressBarHandle* progressBar); + ~CVideoLibraryCleaningJob() override; + + // specialization of CJob + const char *GetType() const override { return "VideoLibraryCleaningJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CVideoLibraryJob + bool Work(CVideoDatabase &db) override; + +private: + std::set<int> m_paths; + bool m_showDialog; +}; diff --git a/xbmc/video/jobs/VideoLibraryJob.cpp b/xbmc/video/jobs/VideoLibraryJob.cpp new file mode 100644 index 0000000..efbbdb4 --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryJob.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014-2018 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 "VideoLibraryJob.h" + +#include "video/VideoDatabase.h" + +CVideoLibraryJob::CVideoLibraryJob() = default; + +CVideoLibraryJob::~CVideoLibraryJob() = default; + +bool CVideoLibraryJob::DoWork() +{ + CVideoDatabase db; + if (!db.Open()) + return false; + + return Work(db); +} diff --git a/xbmc/video/jobs/VideoLibraryJob.h b/xbmc/video/jobs/VideoLibraryJob.h new file mode 100644 index 0000000..a8f7851 --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryJob.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014-2018 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. + */ + +#pragma once + +#include "utils/Job.h" + +class CVideoDatabase; + +/*! + \brief Basic implementation/interface of a CJob which interacts with the + video database. + */ +class CVideoLibraryJob : public CJob +{ +public: + ~CVideoLibraryJob() override; + + /*! + \brief Whether the job can be cancelled or not. + */ + virtual bool CanBeCancelled() const { return false; } + + /*! + \brief Tries to cancel the running job. + + \return True if the job was cancelled, false otherwise + */ + virtual bool Cancel() { return false; } + + // implementation of CJob + bool DoWork() override; + const char *GetType() const override { return "VideoLibraryJob"; } + bool operator==(const CJob* job) const override { return false; } + +protected: + CVideoLibraryJob(); + + /*! + \brief Worker method to be implemented by an actual implementation. + + \param[in] db Already open video database to be used for interaction + \return True if the process succeeded, false otherwise + */ + virtual bool Work(CVideoDatabase &db) = 0; +}; diff --git a/xbmc/video/jobs/VideoLibraryMarkWatchedJob.cpp b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.cpp new file mode 100644 index 0000000..25381bb --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014-2018 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 <vector> + +#include "VideoLibraryMarkWatchedJob.h" +#include "FileItem.h" +#include "Util.h" +#include "filesystem/Directory.h" +#ifdef HAS_UPNP +#include "network/upnp/UPnP.h" +#endif +#include "pvr/PVRManager.h" +#include "pvr/recordings/PVRRecordings.h" +#include "profiles/ProfileManager.h" +#include "settings/SettingsComponent.h" +#include "ServiceBroker.h" +#include "utils/URIUtils.h" +#include "video/VideoDatabase.h" + +CVideoLibraryMarkWatchedJob::CVideoLibraryMarkWatchedJob(const std::shared_ptr<CFileItem>& item, + bool mark) + : m_item(item), m_mark(mark) +{ } + +CVideoLibraryMarkWatchedJob::~CVideoLibraryMarkWatchedJob() = default; + +bool CVideoLibraryMarkWatchedJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CVideoLibraryMarkWatchedJob* markJob = dynamic_cast<const CVideoLibraryMarkWatchedJob*>(job); + if (markJob == NULL) + return false; + + return m_item->IsSamePath(markJob->m_item.get()) && markJob->m_mark == m_mark; +} + +bool CVideoLibraryMarkWatchedJob::Work(CVideoDatabase &db) +{ + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (!profileManager->GetCurrentProfile().canWriteDatabases()) + return false; + + CFileItemList items; + items.Add(CFileItemPtr(new CFileItem(*m_item))); + + if (m_item->m_bIsFolder) + CUtil::GetRecursiveListing(m_item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO); + + std::vector<CFileItemPtr> markItems; + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr item = items.Get(i); + if (item->HasVideoInfoTag() && m_mark == (item->GetVideoInfoTag()->GetPlayCount() > 0)) + continue; + +#ifdef HAS_UPNP + if (URIUtils::IsUPnP(item->GetPath()) && UPNP::CUPnP::MarkWatched(*item, m_mark)) + continue; +#endif + + if (item->HasPVRRecordingInfoTag() && + CServiceBroker::GetPVRManager().Recordings()->MarkWatched(item->GetPVRRecordingInfoTag(), m_mark)) + { + CDateTime newLastPlayed; + if (m_mark) + newLastPlayed = db.IncrementPlayCount(*item); + else + newLastPlayed = db.SetPlayCount(*item, 0); + + if (newLastPlayed.IsValid()) + item->GetVideoInfoTag()->m_lastPlayed = newLastPlayed; + + continue; + } + + markItems.push_back(item); + } + + if (markItems.empty()) + return true; + + db.BeginTransaction(); + + for (std::vector<CFileItemPtr>::const_iterator iter = markItems.begin(); iter != markItems.end(); ++iter) + { + const CFileItemPtr& item = *iter; + + std::string path(item->GetPath()); + if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->GetPath().empty()) + path = item->GetVideoInfoTag()->GetPath(); + + // With both mark as watched and unwatched we want the resume bookmarks to be reset + db.ClearBookMarksOfFile(path, CBookmark::RESUME); + + CDateTime newLastPlayed; + if (m_mark) + newLastPlayed = db.IncrementPlayCount(*item); + else + newLastPlayed = db.SetPlayCount(*item, 0); + + if (newLastPlayed.IsValid() && item->HasVideoInfoTag()) + item->GetVideoInfoTag()->m_lastPlayed = newLastPlayed; + } + + db.CommitTransaction(); + db.Close(); + + return true; +} diff --git a/xbmc/video/jobs/VideoLibraryMarkWatchedJob.h b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.h new file mode 100644 index 0000000..e683b3f --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014-2018 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. + */ + +#pragma once + +#include "video/jobs/VideoLibraryJob.h" + +#include <memory> + +class CFileItem; + +/*! + \brief Video library job implementation for marking items as watched/unwatched. + */ +class CVideoLibraryMarkWatchedJob : public CVideoLibraryJob +{ +public: + /*! + \brief Creates a new video library scanning job. + + \param[in] item Item to be marked as watched/unwatched + \param[in] mark Whether to mark the item as watched or unwatched + */ + CVideoLibraryMarkWatchedJob(const std::shared_ptr<CFileItem>& item, bool mark); + ~CVideoLibraryMarkWatchedJob() override; + + const char *GetType() const override { return "CVideoLibraryMarkWatchedJob"; } + bool operator==(const CJob* job) const override; + +protected: + bool Work(CVideoDatabase &db) override; + +private: + std::shared_ptr<CFileItem> m_item; + bool m_mark; +}; diff --git a/xbmc/video/jobs/VideoLibraryProgressJob.cpp b/xbmc/video/jobs/VideoLibraryProgressJob.cpp new file mode 100644 index 0000000..0aab524 --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryProgressJob.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014-2018 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 "VideoLibraryProgressJob.h" + +CVideoLibraryProgressJob::CVideoLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar) + : CProgressJob(progressBar) +{ } + +CVideoLibraryProgressJob::~CVideoLibraryProgressJob() = default; + +bool CVideoLibraryProgressJob::DoWork() +{ + bool result = CVideoLibraryJob::DoWork(); + + MarkFinished(); + + return result; +} diff --git a/xbmc/video/jobs/VideoLibraryProgressJob.h b/xbmc/video/jobs/VideoLibraryProgressJob.h new file mode 100644 index 0000000..278072b --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryProgressJob.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014-2018 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. + */ + +#pragma once + +#include "utils/ProgressJob.h" +#include "video/jobs/VideoLibraryJob.h" + +/*! + \brief Combined base implementation of a video library job with a progress bar. + */ +class CVideoLibraryProgressJob : public CProgressJob, public CVideoLibraryJob +{ +public: + ~CVideoLibraryProgressJob() override; + + // implementation of CJob + bool DoWork() override; + const char *GetType() const override { return "CVideoLibraryProgressJob"; } + bool operator==(const CJob* job) const override { return false; } + +protected: + explicit CVideoLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar); +}; diff --git a/xbmc/video/jobs/VideoLibraryRefreshingJob.cpp b/xbmc/video/jobs/VideoLibraryRefreshingJob.cpp new file mode 100644 index 0000000..dbac5ed --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryRefreshingJob.cpp @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2014-2018 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 "VideoLibraryRefreshingJob.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "TextureDatabase.h" +#include "URL.h" +#include "addons/Scraper.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/PluginDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "media/MediaType.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" +#include "video/VideoInfoDownloader.h" +#include "video/VideoInfoScanner.h" +#include "video/tags/IVideoInfoTagLoader.h" +#include "video/tags/VideoInfoTagLoaderFactory.h" +#include "video/tags/VideoTagLoaderPlugin.h" + +#include <utility> + +using namespace KODI::MESSAGING; +using namespace VIDEO; + +CVideoLibraryRefreshingJob::CVideoLibraryRefreshingJob(std::shared_ptr<CFileItem> item, + bool forceRefresh, + bool refreshAll, + bool ignoreNfo /* = false */, + const std::string& searchTitle /* = "" */) + : CVideoLibraryProgressJob(nullptr), + m_item(std::move(item)), + m_forceRefresh(forceRefresh), + m_refreshAll(refreshAll), + m_ignoreNfo(ignoreNfo), + m_searchTitle(searchTitle) +{ } + +CVideoLibraryRefreshingJob::~CVideoLibraryRefreshingJob() = default; + +bool CVideoLibraryRefreshingJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CVideoLibraryRefreshingJob* refreshingJob = dynamic_cast<const CVideoLibraryRefreshingJob*>(job); + if (refreshingJob == nullptr) + return false; + + return m_item->GetPath() == refreshingJob->m_item->GetPath(); +} + +bool CVideoLibraryRefreshingJob::Work(CVideoDatabase &db) +{ + if (m_item == nullptr) + return false; + + // determine the scraper for the item's path + VIDEO::SScanSettings scanSettings; + ADDON::ScraperPtr scraper = db.GetScraperForPath(m_item->GetPath(), scanSettings); + if (scraper == nullptr) + return false; + + if (URIUtils::IsPlugin(m_item->GetPath()) && !XFILE::CPluginDirectory::IsMediaLibraryScanningAllowed(ADDON::TranslateContent(scraper->Content()), m_item->GetPath())) + { + CLog::Log(LOGINFO, + "CVideoLibraryRefreshingJob: Plugin '{}' does not support media library scanning and " + "refreshing", + CURL::GetRedacted(m_item->GetPath())); + return false; + } + + // copy the scraper in case we need it again + ADDON::ScraperPtr originalScraper(scraper); + + // get the item's correct title + std::string itemTitle = m_searchTitle; + if (itemTitle.empty()) + itemTitle = m_item->GetMovieName(scanSettings.parent_name); + + CScraperUrl scraperUrl; + bool needsRefresh = m_forceRefresh; + bool hasDetails = false; + bool ignoreNfo = m_ignoreNfo; + + // run this in a loop in case we need to refresh again + bool failure = false; + do + { + std::unique_ptr<CVideoInfoTag> pluginTag; + std::unique_ptr<CGUIListItem::ArtMap> pluginArt; + + if (!ignoreNfo) + { + std::unique_ptr<IVideoInfoTagLoader> loader; + loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*m_item, scraper, + scanSettings.parent_name_root, m_forceRefresh)); + // check if there's an NFO for the item + CInfoScanner::INFO_TYPE nfoResult = CInfoScanner::NO_NFO; + if (loader) + { + std::unique_ptr<CVideoInfoTag> tag(new CVideoInfoTag()); + nfoResult = loader->Load(*tag, false); + if (nfoResult == CInfoScanner::FULL_NFO && m_item->IsPlugin() && scraper->ID() == "metadata.local") + { + // get video info and art from plugin source with metadata.local scraper + if (scraper->Content() == CONTENT_TVSHOWS && !m_item->m_bIsFolder && tag->m_iIdShow < 0) + // preserve show_id for episode + tag->m_iIdShow = m_item->GetVideoInfoTag()->m_iIdShow; + pluginTag = std::move(tag); + CVideoTagLoaderPlugin* nfo = dynamic_cast<CVideoTagLoaderPlugin*>(loader.get()); + if (nfo && nfo->GetArt()) + pluginArt = std::move(nfo->GetArt()); + } + else if (nfoResult == CInfoScanner::URL_NFO) + scraperUrl = loader->ScraperUrl(); + } + + // if there's no NFO remember it in case we have to refresh again + if (nfoResult == CInfoScanner::ERROR_NFO) + ignoreNfo = true; + else if (nfoResult != CInfoScanner::NO_NFO) + hasDetails = true; + + // if we are performing a forced refresh ask the user to choose between using a valid NFO and a valid scraper + if (needsRefresh && IsModal() && !scraper->IsNoop() + && nfoResult != CInfoScanner::ERROR_NFO) + { + int heading = 20159; + if (scraper->Content() == CONTENT_MOVIES) + heading = 13346; + else if (scraper->Content() == CONTENT_TVSHOWS) + heading = m_item->m_bIsFolder ? 20351 : 20352; + else if (scraper->Content() == CONTENT_MUSICVIDEOS) + heading = 20393; + if (CGUIDialogYesNo::ShowAndGetInput(heading, 20446)) + { + hasDetails = false; + ignoreNfo = true; + scraperUrl.Clear(); + scraper = originalScraper; + } + } + } + + // no need to re-fetch the episode guide for episodes + if (scraper->Content() == CONTENT_TVSHOWS && !m_item->m_bIsFolder) + hasDetails = true; + + // if we don't have an url or need to refresh anyway do the web search + if (!hasDetails && (needsRefresh || !scraperUrl.HasUrls())) + { + SetTitle(StringUtils::Format(g_localizeStrings.Get(197), scraper->Name())); + SetText(itemTitle); + SetProgress(0); + + // clear any cached data from the scraper + scraper->ClearCache(); + + // create the info downloader for the scraper + CVideoInfoDownloader infoDownloader(scraper); + + // try to find a matching item + MOVIELIST itemResultList; + int result = infoDownloader.FindMovie(itemTitle, -1, itemResultList, GetProgressDialog()); + + // close the progress dialog + MarkFinished(); + + if (result > 0) + { + // there are multiple matches for the item + if (!itemResultList.empty()) + { + // choose the first match + if (!IsModal()) + scraperUrl = itemResultList.at(0); + else + { + // ask the user what to do + CGUIDialogSelect* selectDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + selectDialog->Reset(); + selectDialog->SetHeading(scraper->Content() == CONTENT_TVSHOWS ? 20356 : 196); + for (const auto& itemResult : itemResultList) + selectDialog->Add(itemResult.GetTitle()); + selectDialog->EnableButton(true, 413); // "Manual" + selectDialog->Open(); + + // check if the user has chosen one of the results + int selectedItem = selectDialog->GetSelectedItem(); + if (selectedItem >= 0) + scraperUrl = itemResultList.at(selectedItem); + // the user hasn't chosen one of the results and but has chosen to manually enter a title to use + else if (selectDialog->IsButtonPressed()) + { + // ask the user to input a title to use + if (!CGUIKeyboardFactory::ShowAndGetInput(itemTitle, g_localizeStrings.Get(scraper->Content() == CONTENT_TVSHOWS ? 20357 : 16009), false)) + return false; + + // go through the whole process again + needsRefresh = true; + continue; + } + // nothing else we can do + else + return false; + } + + CLog::Log(LOGDEBUG, "CVideoLibraryRefreshingJob: user selected item '{}' with URL '{}'", + scraperUrl.GetTitle(), scraperUrl.GetFirstThumbUrl()); + } + } + else if (result < 0 || !VIDEO::CVideoInfoScanner::DownloadFailed(GetProgressDialog())) + { + failure = true; + break; + } + } + + // if the URL is still empty, check whether or not we're allowed + // to prompt and ask the user to input a new search title + if (!hasDetails && !scraperUrl.HasUrls()) + { + if (IsModal()) + { + // ask the user to input a title to use + if (!CGUIKeyboardFactory::ShowAndGetInput(itemTitle, g_localizeStrings.Get(scraper->Content() == CONTENT_TVSHOWS ? 20357 : 16009), false)) + return false; + + // go through the whole process again + needsRefresh = true; + continue; + } + + // nothing else we can do + failure = true; + break; + } + + // before we start downloading all the necessary information cleanup any existing artwork and hashes + CTextureDatabase textureDb; + if (textureDb.Open()) + { + for (const auto& artwork : m_item->GetArt()) + textureDb.InvalidateCachedTexture(artwork.second); + + textureDb.Close(); + } + m_item->ClearArt(); + + // put together the list of items to refresh + std::string path = m_item->GetPath(); + CFileItemList items; + if (m_item->HasVideoInfoTag() && m_item->GetVideoInfoTag()->m_iDbId > 0) + { + // for a tvshow we need to handle all paths of it + std::vector<std::string> tvshowPaths; + if (CMediaTypes::IsMediaType(m_item->GetVideoInfoTag()->m_type, MediaTypeTvShow) && m_refreshAll && + db.GetPathsLinkedToTvShow(m_item->GetVideoInfoTag()->m_iDbId, tvshowPaths)) + { + for (const auto& tvshowPath : tvshowPaths) + { + CFileItemPtr tvshowItem(new CFileItem(*m_item->GetVideoInfoTag())); + tvshowItem->SetPath(tvshowPath); + items.Add(tvshowItem); + } + } + // otherwise just add a copy of the item + else + items.Add(CFileItemPtr(new CFileItem(*m_item->GetVideoInfoTag()))); + + // update the path to the real path (instead of a videodb:// one) + path = m_item->GetVideoInfoTag()->m_strPath; + } + else + items.Add(CFileItemPtr(new CFileItem(*m_item))); + + // set the proper path of the list of items to lookup + items.SetPath(m_item->m_bIsFolder ? URIUtils::GetParentPath(path) : URIUtils::GetDirectory(path)); + + int headingLabel = 198; + if (scraper->Content() == CONTENT_TVSHOWS) + { + if (m_item->m_bIsFolder) + headingLabel = 20353; + else + headingLabel = 20361; + } + else if (scraper->Content() == CONTENT_MUSICVIDEOS) + headingLabel = 20394; + + // prepare the progress dialog for downloading all the necessary information + SetTitle(g_localizeStrings.Get(headingLabel)); + SetText(scraperUrl.GetTitle()); + SetProgress(0); + + // remove any existing data for the item we're going to refresh + if (m_item->GetVideoInfoTag()->m_iDbId > 0) + { + int dbId = m_item->GetVideoInfoTag()->m_iDbId; + if (scraper->Content() == CONTENT_MOVIES) + db.DeleteMovie(dbId); + else if (scraper->Content() == CONTENT_MUSICVIDEOS) + db.DeleteMusicVideo(dbId); + else if (scraper->Content() == CONTENT_TVSHOWS) + { + if (!m_item->m_bIsFolder) + db.DeleteEpisode(dbId); + else if (m_refreshAll) + db.DeleteTvShow(dbId); + else + db.DeleteDetailsForTvShow(dbId); + } + } + + if (pluginTag || pluginArt) + // set video info and art from plugin source with metadata.local scraper to items + for (auto &i: items) + { + if (pluginTag) + *i->GetVideoInfoTag() = *pluginTag; + if (pluginArt) + i->SetArt(*pluginArt); + } + + // finally download the information for the item + CVideoInfoScanner scanner; + if (!scanner.RetrieveVideoInfo(items, scanSettings.parent_name, + scraper->Content(), !ignoreNfo, + scraperUrl.HasUrls() ? &scraperUrl : nullptr, + m_refreshAll, GetProgressDialog())) + { + // something went wrong + MarkFinished(); + + // check if the user cancelled + if (!IsCancelled() && IsModal()) + HELPERS::ShowOKDialogText(CVariant{195}, CVariant{itemTitle}); + + return false; + } + + // retrieve the updated information from the database + if (scraper->Content() == CONTENT_MOVIES) + db.GetMovieInfo(m_item->GetPath(), *m_item->GetVideoInfoTag()); + else if (scraper->Content() == CONTENT_MUSICVIDEOS) + db.GetMusicVideoInfo(m_item->GetPath(), *m_item->GetVideoInfoTag()); + else if (scraper->Content() == CONTENT_TVSHOWS) + { + // update tvshow info to get updated episode numbers + if (m_item->m_bIsFolder) + db.GetTvShowInfo(m_item->GetPath(), *m_item->GetVideoInfoTag()); + else + db.GetEpisodeInfo(m_item->GetPath(), *m_item->GetVideoInfoTag()); + } + + // we're finally done + MarkFinished(); + break; + } while (needsRefresh); + + if (failure && IsModal()) + HELPERS::ShowOKDialogText(CVariant{195}, CVariant{itemTitle}); + + return true; +} diff --git a/xbmc/video/jobs/VideoLibraryRefreshingJob.h b/xbmc/video/jobs/VideoLibraryRefreshingJob.h new file mode 100644 index 0000000..e98f4c0 --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryRefreshingJob.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014-2018 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. + */ + +#pragma once + +#include "video/jobs/VideoLibraryProgressJob.h" + +#include <memory> +#include <string> + +class CFileItem; + +/*! + \brief Video library job implementation for refreshing a single item. +*/ +class CVideoLibraryRefreshingJob : public CVideoLibraryProgressJob +{ +public: + /*! + \brief Creates a new video library cleaning job for the given paths. + + \param[inout] item Video item to be refreshed + \param[in] forceRefresh Whether to force a complete refresh (including NFO or internet lookup) + \param[in] refreshAll Whether to refresh all sub-items (in case of a tvshow) + \param[in] ignoreNfo Whether or not to ignore local NFO files + \param[in] searchTitle Title to use for the search (instead of determining it from the item's filename/path) + */ + CVideoLibraryRefreshingJob(std::shared_ptr<CFileItem> item, + bool forceRefresh, + bool refreshAll, + bool ignoreNfo = false, + const std::string& searchTitle = ""); + + ~CVideoLibraryRefreshingJob() override; + + // specialization of CJob + const char *GetType() const override { return "VideoLibraryRefreshingJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CVideoLibraryJob + bool Work(CVideoDatabase &db) override; + +private: + std::shared_ptr<CFileItem> m_item; + bool m_forceRefresh; + bool m_refreshAll; + bool m_ignoreNfo; + std::string m_searchTitle; +}; diff --git a/xbmc/video/jobs/VideoLibraryResetResumePointJob.cpp b/xbmc/video/jobs/VideoLibraryResetResumePointJob.cpp new file mode 100644 index 0000000..15b71a2 --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryResetResumePointJob.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017-2018 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 "VideoLibraryResetResumePointJob.h" + +#include <vector> + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "filesystem/IDirectory.h" +#ifdef HAS_UPNP +#include "network/upnp/UPnP.h" +#endif +#include "profiles/ProfileManager.h" +#include "pvr/PVRManager.h" +#include "pvr/recordings/PVRRecordings.h" +#include "settings/SettingsComponent.h" +#include "utils/URIUtils.h" +#include "video/VideoDatabase.h" + +CVideoLibraryResetResumePointJob::CVideoLibraryResetResumePointJob( + const std::shared_ptr<CFileItem>& item) + : m_item(item) +{ +} + +bool CVideoLibraryResetResumePointJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CVideoLibraryResetResumePointJob* resetJob = dynamic_cast<const CVideoLibraryResetResumePointJob*>(job); + if (!resetJob) + return false; + + return m_item->IsSamePath(resetJob->m_item.get()); +} + +bool CVideoLibraryResetResumePointJob::Work(CVideoDatabase &db) +{ + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (!profileManager->GetCurrentProfile().canWriteDatabases()) + return false; + + CFileItemList items; + items.Add(std::make_shared<CFileItem>(*m_item)); + + if (m_item->m_bIsFolder) + CUtil::GetRecursiveListing(m_item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO); + + std::vector<CFileItemPtr> resetItems; + for (const auto& item : items) + { +#ifdef HAS_UPNP + if (URIUtils::IsUPnP(item->GetPath()) && UPNP::CUPnP::SaveFileState(*item, CBookmark(), false /* updatePlayCount */)) + continue; +#endif + + if (item->HasPVRRecordingInfoTag() && + CServiceBroker::GetPVRManager().Recordings()->ResetResumePoint(item->GetPVRRecordingInfoTag())) + continue; + + resetItems.emplace_back(item); + } + + if (resetItems.empty()) + return true; + + db.BeginTransaction(); + + for (const auto& resetItem : resetItems) + { + db.DeleteResumeBookMark(*resetItem); + } + + db.CommitTransaction(); + db.Close(); + + return true; +} diff --git a/xbmc/video/jobs/VideoLibraryResetResumePointJob.h b/xbmc/video/jobs/VideoLibraryResetResumePointJob.h new file mode 100644 index 0000000..1e2d69a --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryResetResumePointJob.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017-2018 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. + */ + +#pragma once + +#include "FileItem.h" +#include "video/jobs/VideoLibraryJob.h" + +#include <memory> + +/*! + \brief Video library job implementation for resetting a resume point. + */ +class CVideoLibraryResetResumePointJob : public CVideoLibraryJob +{ +public: + /*! + \brief Creates a new job for resetting a given item's resume point. + + \param[in] item Item for that the resume point shall be reset. + */ + CVideoLibraryResetResumePointJob(const std::shared_ptr<CFileItem>& item); + ~CVideoLibraryResetResumePointJob() override = default; + + const char *GetType() const override { return "CVideoLibraryResetResumePointJob"; } + bool operator==(const CJob* job) const override; + +protected: + bool Work(CVideoDatabase &db) override; + +private: + std::shared_ptr<CFileItem> m_item; +}; diff --git a/xbmc/video/jobs/VideoLibraryScanningJob.cpp b/xbmc/video/jobs/VideoLibraryScanningJob.cpp new file mode 100644 index 0000000..f938420 --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryScanningJob.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014-2018 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 "VideoLibraryScanningJob.h" + +#include "video/VideoDatabase.h" + +CVideoLibraryScanningJob::CVideoLibraryScanningJob(const std::string& directory, bool scanAll /* = false */, bool showProgress /* = true */) + : m_scanner(), + m_directory(directory), + m_showProgress(showProgress), + m_scanAll(scanAll) +{ } + +CVideoLibraryScanningJob::~CVideoLibraryScanningJob() = default; + +bool CVideoLibraryScanningJob::Cancel() +{ + if (!m_scanner.IsScanning()) + return true; + + m_scanner.Stop(); + return true; +} + +bool CVideoLibraryScanningJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CVideoLibraryScanningJob* scanningJob = dynamic_cast<const CVideoLibraryScanningJob*>(job); + if (scanningJob == NULL) + return false; + + return m_directory == scanningJob->m_directory && + m_scanAll == scanningJob->m_scanAll; +} + +bool CVideoLibraryScanningJob::Work(CVideoDatabase &db) +{ + m_scanner.ShowDialog(m_showProgress); + m_scanner.Start(m_directory, m_scanAll); + + return true; +} diff --git a/xbmc/video/jobs/VideoLibraryScanningJob.h b/xbmc/video/jobs/VideoLibraryScanningJob.h new file mode 100644 index 0000000..3df98a7 --- /dev/null +++ b/xbmc/video/jobs/VideoLibraryScanningJob.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014-2018 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. + */ + +#pragma once + +#include "video/VideoInfoScanner.h" +#include "video/jobs/VideoLibraryJob.h" + +#include <string> + +/*! + \brief Video library job implementation for scanning items. + + Uses CVideoInfoScanner for the whole filesystem scanning and can be run with + or without a visible progress bar. + */ +class CVideoLibraryScanningJob : public CVideoLibraryJob +{ +public: + /*! + \brief Creates a new video library scanning job. + + \param[in] directory Directory to be scanned for new items + \param[in] scanAll Whether to scan all items or not + \param[in] showProgress Whether to show a progress bar or not + */ + CVideoLibraryScanningJob(const std::string& directory, bool scanAll = false, bool showProgress = true); + ~CVideoLibraryScanningJob() override; + + // specialization of CVideoLibraryJob + bool CanBeCancelled() const override { return true; } + bool Cancel() override; + + // specialization of CJob + const char *GetType() const override { return "VideoLibraryScanningJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CVideoLibraryJob + bool Work(CVideoDatabase &db) override; + +private: + VIDEO::CVideoInfoScanner m_scanner; + std::string m_directory; + bool m_showProgress; + bool m_scanAll; +}; diff --git a/xbmc/video/tags/CMakeLists.txt b/xbmc/video/tags/CMakeLists.txt new file mode 100644 index 0000000..ab289f8 --- /dev/null +++ b/xbmc/video/tags/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCES VideoInfoTagLoaderFactory.cpp + VideoTagLoaderFFmpeg.cpp + VideoTagLoaderNFO.cpp + VideoTagLoaderPlugin.cpp) + +set(HEADERS IVideoInfoTagLoader.h + VideoInfoTagLoaderFactory.h + VideoTagLoaderFFmpeg.h + VideoTagLoaderNFO.h + VideoTagLoaderPlugin.h) + +core_add_library(video_tags) diff --git a/xbmc/video/tags/IVideoInfoTagLoader.h b/xbmc/video/tags/IVideoInfoTagLoader.h new file mode 100644 index 0000000..8afc31e --- /dev/null +++ b/xbmc/video/tags/IVideoInfoTagLoader.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017-2018 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. + */ + +#pragma once + +#include "InfoScanner.h" +#include "addons/Scraper.h" + +#include <string> +#include <utility> + +class CFileItem; +class CVideoInfoTag; +class EmbeddedArt; + +namespace VIDEO +{ + +//! \brief Base class for video tag loaders. +class IVideoInfoTagLoader +{ +public: + //! \brief Constructor + //! \param item The item to load info for + //! \param info Scraper info + //! \param llokInFolder True to look in folder holding file + IVideoInfoTagLoader(const CFileItem& item, ADDON::ScraperPtr info, bool lookInFolder) + : m_item(item), m_info(std::move(info)) + { + } + virtual ~IVideoInfoTagLoader() = default; + + //! \brief Returns true if we have info to provide. + virtual bool HasInfo() const = 0; + + //! \brief Load tag from file. + //! \brief tag Tag to load info into + //! \brief prioritise True to prioritise data over existing data in tag + //! \returns True if tag was read, false otherwise + virtual CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag, + bool prioritise, + std::vector<EmbeddedArt>* art = nullptr) = 0; + + //! \brief Returns url associated with obtained URL (NFO_URL et al). + const CScraperUrl& ScraperUrl() const { return m_url; } + + //! \brief Returns current scaper info. + const ADDON::ScraperPtr GetAddonInfo() const { return m_info; } + +protected: + const CFileItem& m_item; //!< Reference to item to load for + ADDON::ScraperPtr m_info; //!< Scraper info + CScraperUrl m_url; //!< URL for scraper +}; + +} diff --git a/xbmc/video/tags/VideoInfoTagLoaderFactory.cpp b/xbmc/video/tags/VideoInfoTagLoaderFactory.cpp new file mode 100644 index 0000000..00ed67a --- /dev/null +++ b/xbmc/video/tags/VideoInfoTagLoaderFactory.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017-2018 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 "VideoInfoTagLoaderFactory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "VideoTagLoaderFFmpeg.h" +#include "VideoTagLoaderNFO.h" +#include "VideoTagLoaderPlugin.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +using namespace VIDEO; + +IVideoInfoTagLoader* CVideoInfoTagLoaderFactory::CreateLoader(const CFileItem& item, + const ADDON::ScraperPtr& info, + bool lookInFolder, + bool forceRefresh) +{ + if (item.IsPlugin() && info && info->ID() == "metadata.local") + { + // Direct loading from plugin source with metadata.local scraper + CVideoTagLoaderPlugin* plugin = new CVideoTagLoaderPlugin(item, forceRefresh); + if (plugin->HasInfo()) + return plugin; + delete plugin; + } + + CVideoTagLoaderNFO* nfo = new CVideoTagLoaderNFO(item, info, lookInFolder); + if (nfo->HasInfo()) + return nfo; + delete nfo; + + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_USETAGS) && + (item.IsType(".mkv") || item.IsType(".mp4") || item.IsType(".avi") || item.IsType(".m4v"))) + { + CVideoTagLoaderFFmpeg* ff = new CVideoTagLoaderFFmpeg(item, info, lookInFolder); + if (ff->HasInfo()) + return ff; + delete ff; + } + + return nullptr; +} diff --git a/xbmc/video/tags/VideoInfoTagLoaderFactory.h b/xbmc/video/tags/VideoInfoTagLoaderFactory.h new file mode 100644 index 0000000..9b8861f --- /dev/null +++ b/xbmc/video/tags/VideoInfoTagLoaderFactory.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017-2018 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. + */ + +#pragma once + +#include "IVideoInfoTagLoader.h" +#include "addons/Scraper.h" + +class CFileItem; // forward + +namespace VIDEO +{ + class CVideoInfoTagLoaderFactory + { + public: + //! \brief Returns a tag loader for the given item. + //! \param item The item to find tag loader for + //! \param type Type of tag loader. In particular used for tvshows + static IVideoInfoTagLoader* CreateLoader(const CFileItem& item, + const ADDON::ScraperPtr& info, + bool lookInFolder, + bool forceRefresh = false); + + protected: + // No instancing of this class + CVideoInfoTagLoaderFactory(void) = delete; + virtual ~CVideoInfoTagLoaderFactory() = delete; + }; +} diff --git a/xbmc/video/tags/VideoTagLoaderFFmpeg.cpp b/xbmc/video/tags/VideoTagLoaderFFmpeg.cpp new file mode 100644 index 0000000..c1b0495 --- /dev/null +++ b/xbmc/video/tags/VideoTagLoaderFFmpeg.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2017-2018 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 "VideoTagLoaderFFmpeg.h" + +#include "FileItem.h" +#include "NfoFile.h" +#include "addons/Scraper.h" +#include "cores/FFmpeg.h" +#include "filesystem/File.h" +#include "filesystem/StackDirectory.h" +#include "utils/StringUtils.h" +#include "video/VideoInfoTag.h" + +using namespace XFILE; + +static int vfs_file_read(void *h, uint8_t* buf, int size) +{ + CFile* pFile = static_cast<CFile*>(h); + return pFile->Read(buf, size); +} + +static int64_t vfs_file_seek(void *h, int64_t pos, int whence) +{ + CFile* pFile = static_cast<CFile*>(h); + if (whence == AVSEEK_SIZE) + return pFile->GetLength(); + else + return pFile->Seek(pos, whence & ~AVSEEK_FORCE); +} + +CVideoTagLoaderFFmpeg::CVideoTagLoaderFFmpeg(const CFileItem& item, + const ADDON::ScraperPtr& info, + bool lookInFolder) + : IVideoInfoTagLoader(item, info, lookInFolder) + , m_info(info) +{ + std::string filename = + item.IsStack() ? CStackDirectory::GetFirstStackedFile(item.GetPath()) : item.GetPath(); + + m_file = new CFile; + + if (!m_file->Open(filename)) + { + delete m_file; + m_file = nullptr; + return; + } + + int blockSize = m_file->GetChunkSize(); + int bufferSize = blockSize > 1 ? blockSize : 4096; + uint8_t* buffer = (uint8_t*)av_malloc(bufferSize); + m_ioctx = avio_alloc_context(buffer, bufferSize, 0, + m_file, vfs_file_read, nullptr, + vfs_file_seek); + + m_fctx = avformat_alloc_context(); + m_fctx->pb = m_ioctx; + + if (m_file->IoControl(IOCTRL_SEEK_POSSIBLE, nullptr) != 1) + m_ioctx->seekable = 0; + + AVInputFormat* iformat = nullptr; + av_probe_input_buffer(m_ioctx, &iformat, m_item.GetPath().c_str(), nullptr, 0, 0); + if (avformat_open_input(&m_fctx, m_item.GetPath().c_str(), iformat, nullptr) < 0) + { + delete m_file; + m_file = nullptr; + } +} + +CVideoTagLoaderFFmpeg::~CVideoTagLoaderFFmpeg() +{ + if (m_fctx) + avformat_close_input(&m_fctx); + if (m_ioctx) + { + av_free(m_ioctx->buffer); + av_free(m_ioctx); + } + delete m_file; +} + +bool CVideoTagLoaderFFmpeg::HasInfo() const +{ + if (!m_file) + return false; + + for (size_t i = 0; i < m_fctx->nb_streams; ++i) + { + AVDictionaryEntry* avtag; + avtag = av_dict_get(m_fctx->streams[i]->metadata, "filename", nullptr, AV_DICT_IGNORE_SUFFIX); + if (avtag && strcmp(avtag->value,"kodi-metadata") == 0) + { + m_metadata_stream = i; + return true; + } + else if (avtag && strcmp(avtag->value,"kodi-override-metadata") == 0) + { + m_metadata_stream = i; + m_override_data = true; + return true; + } + } + + AVDictionaryEntry* avtag = nullptr; + if (m_item.IsType(".mkv")) + { + avtag = av_dict_get(m_fctx->metadata, "IMDBURL", nullptr, AV_DICT_IGNORE_SUFFIX); + if (!avtag) + avtag = av_dict_get(m_fctx->metadata, "TMDBURL", nullptr, AV_DICT_IGNORE_SUFFIX); + if (!avtag) + avtag = av_dict_get(m_fctx->metadata, "TITLE", nullptr, AV_DICT_IGNORE_SUFFIX); + } else if (m_item.IsType(".mp4") || m_item.IsType(".avi")) + avtag = av_dict_get(m_fctx->metadata, "title", nullptr, AV_DICT_IGNORE_SUFFIX); + + return avtag != nullptr; +} + +CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::Load(CVideoInfoTag& tag, + bool, std::vector<EmbeddedArt>* art) +{ + if (m_item.IsType(".mkv")) + return LoadMKV(tag, art); + else if (m_item.IsType(".mp4")) + return LoadMP4(tag, art); + else if (m_item.IsType(".avi")) + return LoadAVI(tag, art); + else + return CInfoScanner::NO_NFO; + +} + +CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::LoadMKV(CVideoInfoTag& tag, + std::vector<EmbeddedArt>* art) +{ + // embedded art + for (size_t i = 0; i < m_fctx->nb_streams; ++i) + { + if ((m_fctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) == 0) + continue; + AVDictionaryEntry* avtag; + avtag = av_dict_get(m_fctx->streams[i]->metadata, "filename", nullptr, AV_DICT_IGNORE_SUFFIX); + std::string value; + if (avtag) + value = avtag->value; + avtag = av_dict_get(m_fctx->streams[i]->metadata, "mimetype", nullptr, AV_DICT_IGNORE_SUFFIX); + if (!value.empty() && avtag) + { + std::string type; + if (value == "fanart.png" || value == "fanart.jpg") + type = "fanart"; + else if (value == "cover.png" || value == "cover.jpg") + type = "poster"; + else if (value == "small_cover.png" || value == "small_cover.jpg") + type = "thumb"; + if (type.empty()) + continue; + size_t size = m_fctx->streams[i]->attached_pic.size; + if (art) + art->emplace_back(EmbeddedArt(m_fctx->streams[i]->attached_pic.data, + size, avtag->value, type)); + else + tag.m_coverArt.emplace_back(EmbeddedArtInfo(size, avtag->value, type)); + } + } + + if (m_metadata_stream != -1) + { + CNfoFile nfo; + auto* data = m_fctx->streams[m_metadata_stream]->codecpar->extradata; + const char* content = reinterpret_cast<const char*>(data); + if (!m_override_data) + { + nfo.GetDetails(tag, content); + return CInfoScanner::FULL_NFO; + } + else + { + nfo.Create(content, m_info); + m_url = nfo.ScraperUrl(); + return CInfoScanner::URL_NFO; + } + } + + AVDictionaryEntry* avtag = nullptr; + bool hastag = false; + while ((avtag = av_dict_get(m_fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX))) + { + if (StringUtils::CompareNoCase(avtag->key, "imdburl") == 0 || + StringUtils::CompareNoCase(avtag->key, "tmdburl") == 0) + { + CNfoFile nfo; + nfo.Create(avtag->value, m_info); + m_url = nfo.ScraperUrl(); + return CInfoScanner::URL_NFO; + } + else if (StringUtils::CompareNoCase(avtag->key, "title") == 0) + tag.SetTitle(avtag->value); + else if (StringUtils::CompareNoCase(avtag->key, "director") == 0) + { + std::vector<std::string> dirs = StringUtils::Split(avtag->value, " / "); + tag.SetDirector(dirs); + } + else if (StringUtils::CompareNoCase(avtag->key, "date_released") == 0) + tag.SetYear(atoi(avtag->value)); + hastag = true; + } + + return hastag ? CInfoScanner::TITLE_NFO : CInfoScanner::NO_NFO; +} + +// https://wiki.multimedia.cx/index.php/FFmpeg_Metadata +CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::LoadMP4(CVideoInfoTag& tag, + std::vector<EmbeddedArt>* art) +{ + bool hasfull = false; + AVDictionaryEntry* avtag = nullptr; + // If either description or synopsis is found, assume user wants to use the tag info only + while ((avtag = av_dict_get(m_fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX))) + { + if (strcmp(avtag->key, "title") == 0) + tag.SetTitle(avtag->value); + else if (strcmp(avtag->key, "composer") == 0) + tag.SetWritingCredits(StringUtils::Split(avtag->value, " / ")); + else if (strcmp(avtag->key, "genre") == 0) + tag.SetGenre(StringUtils::Split(avtag->value, " / ")); + else if (strcmp(avtag->key,"date") == 0) + tag.SetYear(atoi(avtag->value)); + else if (strcmp(avtag->key, "description") == 0) + { + tag.SetPlotOutline(avtag->value); + hasfull = true; + } + else if (strcmp(avtag->key, "synopsis") == 0) + { + tag.SetPlot(avtag->value); + hasfull = true; + } + else if (strcmp(avtag->key, "track") == 0) + tag.m_iTrack = std::stoi(avtag->value); + else if (strcmp(avtag->key, "album") == 0) + tag.SetAlbum(avtag->value); + else if (strcmp(avtag->key, "artist") == 0) + tag.SetArtist(StringUtils::Split(avtag->value, " / ")); + } + + for (size_t i = 0; i < m_fctx->nb_streams; ++i) + { + if ((m_fctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) == 0) + continue; + + size_t size = m_fctx->streams[i]->attached_pic.size; + const std::string type = "poster"; + if (art) + art->emplace_back(EmbeddedArt(m_fctx->streams[i]->attached_pic.data, + size, "image/png", type)); + else + tag.m_coverArt.emplace_back(EmbeddedArtInfo(size, "image/png", type)); + } + + return hasfull ? CInfoScanner::FULL_NFO : CInfoScanner::TITLE_NFO; +} + +// https://wiki.multimedia.cx/index.php/FFmpeg_Metadata#AVI +CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::LoadAVI(CVideoInfoTag& tag, + std::vector<EmbeddedArt>* art) +{ + AVDictionaryEntry* avtag = nullptr; + while ((avtag = av_dict_get(m_fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX))) + { + if (strcmp(avtag->key, "title") == 0) + tag.SetTitle(avtag->value); + else if (strcmp(avtag->key,"date") == 0) + tag.SetYear(atoi(avtag->value)); + } + + return CInfoScanner::TITLE_NFO; +} diff --git a/xbmc/video/tags/VideoTagLoaderFFmpeg.h b/xbmc/video/tags/VideoTagLoaderFFmpeg.h new file mode 100644 index 0000000..f6b50df --- /dev/null +++ b/xbmc/video/tags/VideoTagLoaderFFmpeg.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017-2018 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. + */ + +#pragma once + +#include "IVideoInfoTagLoader.h" + +#include <string> +#include <vector> + +struct AVFormatContext; +struct AVIOContext; +namespace XFILE +{ + class CFile; +} + +//! \brief Video tag loader using FFMPEG. +class CVideoTagLoaderFFmpeg : public VIDEO::IVideoInfoTagLoader +{ +public: + //! \brief Constructor. + CVideoTagLoaderFFmpeg(const CFileItem& item, + const ADDON::ScraperPtr& info, + bool lookInFolder); + + //! \brief Destructor. + ~CVideoTagLoaderFFmpeg() override; + + //! \brief Returns whether or not reader has info. + bool HasInfo() const override; + + //! \brief Load "tag" from nfo file. + //! \brief tag Tag to load info into + CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag, bool, + std::vector<EmbeddedArt>* art = nullptr) override; + + ADDON::ScraperPtr GetScraperInfo() const { return m_info; } + +protected: + ADDON::ScraperPtr m_info; //!< Passed scraper info + AVIOContext* m_ioctx = nullptr; //!< IO context for file + AVFormatContext* m_fctx = nullptr; //!< Format context for file + XFILE::CFile* m_file = nullptr; //!< VFS file handle for file + mutable int m_metadata_stream = -1; //!< Stream holding kodi metadata (mkv) + mutable bool m_override_data = false; //!< Data is for overriding + + //! \brief Load tags from MKV file. + CInfoScanner::INFO_TYPE LoadMKV(CVideoInfoTag& tag, std::vector<EmbeddedArt>* art); + + //! \brief Load tags from MP4 file. + CInfoScanner::INFO_TYPE LoadMP4(CVideoInfoTag& tag, std::vector<EmbeddedArt>* art); + + //! \brief Load tags from AVI file. + CInfoScanner::INFO_TYPE LoadAVI(CVideoInfoTag& tag, std::vector<EmbeddedArt>* art); +}; + diff --git a/xbmc/video/tags/VideoTagLoaderNFO.cpp b/xbmc/video/tags/VideoTagLoaderNFO.cpp new file mode 100644 index 0000000..f85ce39 --- /dev/null +++ b/xbmc/video/tags/VideoTagLoaderNFO.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2005-2018 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 "VideoTagLoaderNFO.h" + +#include "FileItem.h" +#include "NfoFile.h" +#include "URL.h" +#include "filesystem/Directory.h" +#include "filesystem/StackDirectory.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoInfoTag.h" + +#include <utility> + +using namespace XFILE; + +CVideoTagLoaderNFO::CVideoTagLoaderNFO(const CFileItem& item, + ADDON::ScraperPtr info, + bool lookInFolder) + : IVideoInfoTagLoader(item, std::move(info), lookInFolder) +{ + if (m_info && m_info->Content() == CONTENT_TVSHOWS && m_item.m_bIsFolder) + m_path = URIUtils::AddFileToFolder(m_item.GetPath(), "tvshow.nfo"); + else + m_path = FindNFO(m_item, lookInFolder); +} + +bool CVideoTagLoaderNFO::HasInfo() const +{ + return !m_path.empty() && CFileUtils::Exists(m_path); +} + +CInfoScanner::INFO_TYPE CVideoTagLoaderNFO::Load(CVideoInfoTag& tag, + bool prioritise, + std::vector<EmbeddedArt>*) +{ + CNfoFile nfoReader; + CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO; + if (m_info && m_info->Content() == CONTENT_TVSHOWS && !m_item.m_bIsFolder) + result = nfoReader.Create(m_path, m_info, m_item.GetVideoInfoTag()->m_iEpisode); + else if (m_info) + result = nfoReader.Create(m_path, m_info); + + if (result == CInfoScanner::FULL_NFO || result == CInfoScanner::COMBINED_NFO) + nfoReader.GetDetails(tag, nullptr, prioritise); + + if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO) + { + m_url = nfoReader.ScraperUrl(); + m_info = nfoReader.GetScraperInfo(); + } + + std::string type; + switch(result) + { + case CInfoScanner::COMBINED_NFO: + type = "mixed"; + break; + case CInfoScanner::FULL_NFO: + type = "full"; + break; + case CInfoScanner::URL_NFO: + type = "URL"; + break; + case CInfoScanner::NO_NFO: + type = ""; + break; + case CInfoScanner::OVERRIDE_NFO: + type = "override"; + break; + default: + type = "malformed"; + } + if (result != CInfoScanner::NO_NFO) + CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching {} NFO file: {}", type, + CURL::GetRedacted(m_path)); + else + CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '{}'", + CURL::GetRedacted(m_item.GetPath())); + + return result; +} + +std::string CVideoTagLoaderNFO::FindNFO(const CFileItem& item, + bool movieFolder) const +{ + std::string nfoFile; + // Find a matching .nfo file + if (!item.m_bIsFolder) + { + if (URIUtils::IsInRAR(item.GetPath())) // we have a rarred item - we want to check outside the rars + { + CFileItem item2(item); + CURL url(item.GetPath()); + std::string strPath = URIUtils::GetDirectory(url.GetHostName()); + item2.SetPath(URIUtils::AddFileToFolder(strPath, + URIUtils::GetFileName(item.GetPath()))); + return FindNFO(item2, movieFolder); + } + + // grab the folder path + std::string strPath = URIUtils::GetDirectory(item.GetPath()); + + if (movieFolder && !item.IsStack()) + { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below) + nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo"); + if (CFileUtils::Exists(nfoFile)) + return nfoFile; + } + + // try looking for .nfo file for a stacked item + if (item.IsStack()) + { + // first try .nfo file matching first file in stack + CStackDirectory dir; + std::string firstFile = dir.GetFirstStackedFile(item.GetPath()); + CFileItem item2; + item2.SetPath(firstFile); + nfoFile = FindNFO(item2, movieFolder); + // else try .nfo file matching stacked title + if (nfoFile.empty()) + { + std::string stackedTitlePath = dir.GetStackedTitlePath(item.GetPath()); + item2.SetPath(stackedTitlePath); + nfoFile = FindNFO(item2, movieFolder); + } + } + else + { + // already an .nfo file? + if (URIUtils::HasExtension(item.GetPath(), ".nfo")) + nfoFile = item.GetPath(); + // no, create .nfo file + else + nfoFile = URIUtils::ReplaceExtension(item.GetPath(), ".nfo"); + } + + // test file existence + if (!nfoFile.empty() && !CFileUtils::Exists(nfoFile)) + nfoFile.clear(); + + if (nfoFile.empty()) // final attempt - strip off any cd1 folders + { + URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows + CFileItem item2; + if (StringUtils::EndsWithNoCase(strPath, "cd1")) + { + strPath.erase(strPath.size() - 3); + item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item.GetPath()))); + return FindNFO(item2, movieFolder); + } + } + + if (nfoFile.empty() && item.IsOpticalMediaFile()) + { + CFileItem parentDirectory(item.GetLocalMetadataPath(), true); + nfoFile = FindNFO(parentDirectory, true); + } + } + // folders (or stacked dvds) can take any nfo file if there's a unique one + if (item.m_bIsFolder || item.IsOpticalMediaFile() || (movieFolder && nfoFile.empty())) + { + // see if there is a unique nfo file in this folder, and if so, use that + CFileItemList items; + CDirectory dir; + std::string strPath; + if (item.m_bIsFolder) + strPath = item.GetPath(); + else + strPath = URIUtils::GetDirectory(item.GetPath()); + + if (dir.GetDirectory(strPath, items, ".nfo", DIR_FLAG_DEFAULTS) && items.Size()) + { + int numNFO = -1; + for (int i = 0; i < items.Size(); i++) + { + if (items[i]->IsNFO()) + { + if (numNFO == -1) + numNFO = i; + else + { + numNFO = -1; + break; + } + } + } + if (numNFO > -1) + return items[numNFO]->GetPath(); + } + } + + return nfoFile; +} + diff --git a/xbmc/video/tags/VideoTagLoaderNFO.h b/xbmc/video/tags/VideoTagLoaderNFO.h new file mode 100644 index 0000000..e6124c8 --- /dev/null +++ b/xbmc/video/tags/VideoTagLoaderNFO.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "IVideoInfoTagLoader.h" + +#include <string> +#include <vector> + +//! \brief Video tag loader using nfo files. +class CVideoTagLoaderNFO : public VIDEO::IVideoInfoTagLoader +{ +public: + CVideoTagLoaderNFO(const CFileItem& item, + ADDON::ScraperPtr info, + bool lookInFolder); + + ~CVideoTagLoaderNFO() override = default; + + //! \brief Returns whether or not read has info. + bool HasInfo() const override; + + //! \brief Load "tag" from nfo file. + //! \brief tag Tag to load info into + CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag, bool prioritise, + std::vector<EmbeddedArt>* = nullptr) override; + +protected: + //! \brief Find nfo file for item + //! \param item The item to find NFO file for + //! \param movieFolder If true, look for movie.nfo + std::string FindNFO(const CFileItem& item, bool movieFolder) const; + + std::string m_path; //!< Path to nfo file +}; diff --git a/xbmc/video/tags/VideoTagLoaderPlugin.cpp b/xbmc/video/tags/VideoTagLoaderPlugin.cpp new file mode 100644 index 0000000..407e0ee --- /dev/null +++ b/xbmc/video/tags/VideoTagLoaderPlugin.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005-2018 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 "VideoTagLoaderPlugin.h" + +#include "FileItem.h" +#include "URL.h" +#include "filesystem/PluginDirectory.h" + +using namespace XFILE; + +CVideoTagLoaderPlugin::CVideoTagLoaderPlugin(const CFileItem& item, bool forceRefresh) + : IVideoInfoTagLoader(item, nullptr, false), m_force_refresh(forceRefresh) +{ + if (forceRefresh) + return; + // Preserve CFileItem video info and art to avoid info loss between creating VideoInfoTagLoaderFactory and calling Load() + if (m_item.HasVideoInfoTag()) + m_tag.reset(new CVideoInfoTag(*m_item.GetVideoInfoTag())); + auto& art = item.GetArt(); + if (!art.empty()) + m_art.reset(new CGUIListItem::ArtMap(art)); +} + +bool CVideoTagLoaderPlugin::HasInfo() const +{ + return m_tag || m_force_refresh; +} + +CInfoScanner::INFO_TYPE CVideoTagLoaderPlugin::Load(CVideoInfoTag& tag, bool, std::vector<EmbeddedArt>*) +{ + if (m_force_refresh) + { + // In case of force refresh call our plugin with option "kodi_action=refresh_info" + // Plugin must do all refreshing work at specified path and return directory containing one ListItem with video tag and art + // We cannot obtain all info from setResolvedUrl, because CPluginDirectory::GetPluginResult doesn't copy full art + CURL url(m_item.GetPath()); + url.SetOption("kodi_action", "refresh_info"); + CPluginDirectory plugin; + CFileItemList items; + if (!plugin.GetDirectory(url, items)) + return CInfoScanner::ERROR_NFO; + if (!items.IsEmpty()) + { + const CFileItemPtr &item = items[0]; + m_art.reset(new CGUIListItem::ArtMap(item->GetArt())); + if (item->HasVideoInfoTag()) + { + tag = *item->GetVideoInfoTag(); + return CInfoScanner::FULL_NFO; + } + } + } + else if (m_tag) + { + // Otherwise just copy CFileItem video info to tag + tag = *m_tag; + return CInfoScanner::FULL_NFO; + } + return CInfoScanner::NO_NFO; +} diff --git a/xbmc/video/tags/VideoTagLoaderPlugin.h b/xbmc/video/tags/VideoTagLoaderPlugin.h new file mode 100644 index 0000000..7da92c1 --- /dev/null +++ b/xbmc/video/tags/VideoTagLoaderPlugin.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "IVideoInfoTagLoader.h" +#include "video/VideoInfoTag.h" + +#include <map> +#include <memory> +#include <string> +#include <vector> + +//! \brief Video tag loader from plugin source. +class CVideoTagLoaderPlugin : public VIDEO::IVideoInfoTagLoader +{ +public: + CVideoTagLoaderPlugin(const CFileItem& item, bool forceRefresh); + + ~CVideoTagLoaderPlugin() override = default; + + //! \brief Returns whether or not read has info. + bool HasInfo() const override; + + //! \brief Load "tag" from plugin. + //! \param tag Tag to load info into + CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag, bool prioritise, + std::vector<EmbeddedArt>* = nullptr) override; + + inline std::unique_ptr<std::map<std::string, std::string>>& GetArt() + { + return m_art; + } +protected: + std::unique_ptr<CVideoInfoTag> m_tag; + std::unique_ptr<std::map<std::string, std::string>> m_art; + bool m_force_refresh; +}; diff --git a/xbmc/video/test/CMakeLists.txt b/xbmc/video/test/CMakeLists.txt new file mode 100644 index 0000000..e03acfb --- /dev/null +++ b/xbmc/video/test/CMakeLists.txt @@ -0,0 +1,4 @@ +set(SOURCES TestStacks.cpp + TestVideoInfoScanner.cpp) + +core_add_test_library(video_test) diff --git a/xbmc/video/test/TestStacks.cpp b/xbmc/video/test/TestStacks.cpp new file mode 100644 index 0000000..f5d92ea --- /dev/null +++ b/xbmc/video/test/TestStacks.cpp @@ -0,0 +1,74 @@ +/* + * 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 "FileItem.h" +#include "filesystem/Directory.h" +#include "test/TestUtils.h" + +#include <string> + +#include <gtest/gtest.h> + +using namespace XFILE; + +namespace +{ +const std::string VIDEO_EXTENSIONS = ".mpg|.mpeg|.mp4|.mkv|.mk3d|.iso"; +} + +class TestStacks : public ::testing::Test +{ +protected: + TestStacks() = default; + ~TestStacks() override = default; +}; + +TEST_F(TestStacks, TestMovieFilesStackFilesAB) +{ + const std::string movieFolder = + XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_ab/Movie-(2001)"); + CFileItemList items; + CDirectory::GetDirectory(movieFolder, items, VIDEO_EXTENSIONS, DIR_FLAG_DEFAULTS); + // make sure items has 2 items (the two movie parts) + EXPECT_EQ(items.Size(), 2); + // stack the items and make sure we end up with a single movie + items.Stack(); + EXPECT_EQ(items.Size(), 1); + // check the single item in the stack is a stack:// + EXPECT_EQ(items.Get(0)->IsStack(), true); +} + +TEST_F(TestStacks, TestMovieFilesStackFilesPart) +{ + const std::string movieFolder = + XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_part/Movie_(2001)"); + CFileItemList items; + CDirectory::GetDirectory(movieFolder, items, VIDEO_EXTENSIONS, DIR_FLAG_DEFAULTS); + // make sure items has 3 items (the three movie parts) + EXPECT_EQ(items.Size(), 3); + // stack the items and make sure we end up with a single movie + items.Stack(); + EXPECT_EQ(items.Size(), 1); + // check the single item in the stack is a stack:// + EXPECT_EQ(items.Get(0)->IsStack(), true); +} + +TEST_F(TestStacks, TestMovieFilesStackDvdIso) +{ + const std::string movieFolder = + XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)"); + CFileItemList items; + CDirectory::GetDirectory(movieFolder, items, VIDEO_EXTENSIONS, DIR_FLAG_DEFAULTS); + // make sure items has 2 items (the two dvd isos) + EXPECT_EQ(items.Size(), 2); + // stack the items and make sure we end up with a single movie + items.Stack(); + EXPECT_EQ(items.Size(), 1); + // check the single item in the stack is a stack:// + EXPECT_EQ(items.Get(0)->IsStack(), true); +} diff --git a/xbmc/video/test/TestVideoInfoScanner.cpp b/xbmc/video/test/TestVideoInfoScanner.cpp new file mode 100644 index 0000000..981d749 --- /dev/null +++ b/xbmc/video/test/TestVideoInfoScanner.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005-2018 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 "FileItem.h" +#include "video/VideoInfoScanner.h" + +#include <gtest/gtest.h> + +using namespace VIDEO; +using ::testing::Test; +using ::testing::WithParamInterface; +using ::testing::ValuesIn; + +typedef struct +{ + const char* path; + int season; + int episode[4]; // for multi-episodes +} TestEntry; + +static const TestEntry TestData[] = { + //season+episode + {"foo.S02E03.mkv", 2, {3} }, + {"foo.203.mkv", 2, {3} }, + //episode only + {"foo.Ep03.mkv", 1, {3} }, + {"foo.Ep_03.mkv", 1, {3} }, + {"foo.Part.III.mkv", 1, {3} }, + {"foo.Part.3.mkv", 1, {3} }, + {"foo.E03.mkv", 1, {3} }, + {"foo.2009.E03.mkv", 1, {3} }, + // multi-episode + {"The Legend of Korra - S01E01-02 - Welcome to Republic City & A Leaf in the Wind.mkv", 1, { 1, 2 } }, + {"foo.S01E01E02.mkv", 1, {1,2} }, + {"foo.S01E03E04E05.mkv", 1, {3,4,5} } +}; + +class TestVideoInfoScanner : public Test, + public WithParamInterface<TestEntry> +{ +}; + +TEST_P(TestVideoInfoScanner, EnumerateEpisodeItem) +{ + const TestEntry& entry = GetParam(); + CVideoInfoScanner scanner; + CFileItem item(entry.path, false); + EPISODELIST expected; + for (int i = 0; i < 3 && entry.episode[i]; i++) + expected.push_back(EPISODE(entry.season, entry.episode[i], 0, false)); + + EPISODELIST result; + ASSERT_TRUE(scanner.EnumerateEpisodeItem(&item, result)); + EXPECT_EQ(expected.size(), result.size()); + for (size_t i = 0; i < expected.size(); i++) + EXPECT_EQ(expected[i], result[i]); +} + +INSTANTIATE_TEST_SUITE_P(VideoInfoScanner, TestVideoInfoScanner, ValuesIn(TestData)); + +TEST(TestVideoInfoScanner, EnumerateEpisodeItemByTitle) +{ + CVideoInfoScanner scanner; + CFileItem item("/foo.special.mp4", false); + EPISODELIST result; + ASSERT_TRUE(scanner.EnumerateEpisodeItem(&item, result)); + ASSERT_EQ(result.size(), 1); + ASSERT_EQ(result[0].strTitle, "foo"); +} diff --git a/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)A.mp4 b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)A.mp4 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)A.mp4 diff --git a/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)B.mp4 b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)B.mp4 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)B.mp4 diff --git a/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso diff --git a/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso diff --git a/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part1.mkv b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part1.mkv new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part1.mkv diff --git a/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv diff --git a/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv diff --git a/xbmc/video/windows/CMakeLists.txt b/xbmc/video/windows/CMakeLists.txt new file mode 100644 index 0000000..0695663 --- /dev/null +++ b/xbmc/video/windows/CMakeLists.txt @@ -0,0 +1,14 @@ +set(SOURCES GUIWindowFullScreen.cpp + GUIWindowVideoBase.cpp + GUIWindowVideoNav.cpp + GUIWindowVideoPlaylist.cpp + VideoFileItemListModifier.cpp) + +set(HEADERS GUIWindowFullScreen.h + GUIWindowFullScreenDefines.h + GUIWindowVideoBase.h + GUIWindowVideoNav.h + GUIWindowVideoPlaylist.h + VideoFileItemListModifier.h) + +core_add_library(video_windows) diff --git a/xbmc/video/windows/GUIWindowFullScreen.cpp b/xbmc/video/windows/GUIWindowFullScreen.cpp new file mode 100644 index 0000000..7cebc5f --- /dev/null +++ b/xbmc/video/windows/GUIWindowFullScreen.cpp @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2005-2018 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 "GUIWindowFullScreen.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "GUIWindowFullScreenDefines.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "cores/IPlayer.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "video/ViewModeSettings.h" +#include "video/dialogs/GUIDialogFullScreenInfo.h" +#include "video/dialogs/GUIDialogSubtitleSettings.h" +#include "windowing/WinSystem.h" + +#include <algorithm> +#include <stdio.h> +#if defined(TARGET_DARWIN) +#include "platform/posix/PosixResourceCounter.h" +#endif + +using namespace KODI::GUILIB; +using namespace KODI::MESSAGING; + +#if defined(TARGET_DARWIN) +static CPosixResourceCounter m_resourceCounter; +#endif + +CGUIWindowFullScreen::CGUIWindowFullScreen() + : CGUIWindow(WINDOW_FULLSCREEN_VIDEO, "VideoFullScreen.xml") +{ + m_viewModeChanged = true; + m_dwShowViewModeTimeout = {}; + m_bShowCurrentTime = false; + m_loadType = KEEP_IN_MEMORY; + // audio + // - language + // - volume + // - stream + + // video + // - Create Bookmark (294) + // - Cycle bookmarks (295) + // - Clear bookmarks (296) + // - jump to specific time + // - slider + // - av delay + + // subtitles + // - delay + // - language + + m_controlStats = new GUICONTROLSTATS; +} + +CGUIWindowFullScreen::~CGUIWindowFullScreen(void) +{ + delete m_controlStats; +} + +bool CGUIWindowFullScreen::OnAction(const CAction &action) +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + switch (action.GetID()) + { + case ACTION_SHOW_OSD: + ToggleOSD(); + return true; + + case ACTION_TRIGGER_OSD: + TriggerOSD(); + return true; + + case ACTION_MOUSE_MOVE: + if (action.GetAmount(2) || action.GetAmount(3)) + { + if (!appPlayer->IsInMenu()) + { + TriggerOSD(); + return true; + } + } + break; + + case ACTION_MOUSE_LEFT_CLICK: + if (!appPlayer->IsInMenu()) + { + TriggerOSD(); + return true; + } + break; + + case ACTION_SHOW_GUI: + { + // switch back to the menu + CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow(); + return true; + } + break; + + case ACTION_SHOW_OSD_TIME: + m_bShowCurrentTime = !m_bShowCurrentTime; + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowTime(m_bShowCurrentTime); + return true; + break; + + case ACTION_SHOW_INFO: + { + CGUIDialogFullScreenInfo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogFullScreenInfo>(WINDOW_DIALOG_FULLSCREEN_INFO); + if (pDialog) + { + CFileItem item(g_application.CurrentFileItem()); + pDialog->Open(); + return true; + } + break; + } + + case ACTION_ASPECT_RATIO: + { // toggle the aspect ratio mode (only if the info is onscreen) + if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0) + { + CVideoSettings vs = appPlayer->GetVideoSettings(); + vs.m_ViewMode = CViewModeSettings::GetNextQuickCycleViewMode(vs.m_ViewMode); + appPlayer->SetRenderViewMode(vs.m_ViewMode, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio, + vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch); + } + else + m_viewModeChanged = true; + m_dwShowViewModeTimeout = std::chrono::steady_clock::now(); + } + return true; + break; + case ACTION_SHOW_PLAYLIST: + { + CFileItem item(g_application.CurrentFileItem()); + if (item.HasPVRChannelInfoTag()) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_PVR_OSD_CHANNELS); + else if (item.HasVideoInfoTag()) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_PLAYLIST); + else if (item.HasMusicInfoTag()) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST); + } + return true; + break; + case ACTION_BROWSE_SUBTITLE: + { + std::string path = CGUIDialogSubtitleSettings::BrowseForSubtitle(); + if (!path.empty()) + appPlayer->AddSubtitle(path); + return true; + } + default: + break; + } + + return CGUIWindow::OnAction(action); +} + +void CGUIWindowFullScreen::ClearBackground() +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsRenderingVideoLayer()) + CServiceBroker::GetWinSystem()->GetGfxContext().Clear(0); +} + +void CGUIWindowFullScreen::OnWindowLoaded() +{ + CGUIWindow::OnWindowLoaded(); + // override the clear colour - we must never clear fullscreen + m_clearBackground = 0; +} + +bool CGUIWindowFullScreen::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_WINDOW_INIT: + { + // check whether we've come back here from a window during which time we've actually + // stopped playing videos + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (message.GetParam1() == WINDOW_INVALID && !appPlayer->IsPlayingVideo()) + { // why are we here if nothing is playing??? + CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow(); + return true; + } + + GUIINFO::CPlayerGUIInfo& guiInfo = CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider(); + guiInfo.SetShowInfo(false); + m_bShowCurrentTime = false; + + // switch resolution + CServiceBroker::GetWinSystem()->GetGfxContext().SetFullScreenVideo(true); + + // now call the base class to load our windows + CGUIWindow::OnMessage(message); + + m_dwShowViewModeTimeout = {}; + m_viewModeChanged = true; + + + return true; + } + case GUI_MSG_WINDOW_DEINIT: + { + // close all active modal dialogs + CServiceBroker::GetGUI()->GetWindowManager().CloseInternalModalDialogs(true); + + CGUIWindow::OnMessage(message); + + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + CServiceBroker::GetWinSystem()->GetGfxContext().SetFullScreenVideo(false); + + return true; + } + case GUI_MSG_SETFOCUS: + case GUI_MSG_LOSTFOCUS: + if (message.GetSenderId() != WINDOW_FULLSCREEN_VIDEO) return true; + break; + } + + return CGUIWindow::OnMessage(message); +} + +EVENT_RESULT CGUIWindowFullScreen::OnMouseEvent(const CPoint &point, const CMouseEvent &event) +{ + if (event.m_id == ACTION_MOUSE_RIGHT_CLICK) + { // no control found to absorb this click - go back to GUI + OnAction(CAction(ACTION_SHOW_GUI)); + return EVENT_RESULT_HANDLED; + } + if (event.m_id == ACTION_MOUSE_WHEEL_UP) + { + return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_FORWARD, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED; + } + if (event.m_id == ACTION_MOUSE_WHEEL_DOWN) + { + return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_BACK, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED; + } + if (event.m_id >= ACTION_GESTURE_NOTIFY && event.m_id <= ACTION_GESTURE_END) // gestures + return EVENT_RESULT_UNHANDLED; + return EVENT_RESULT_UNHANDLED; +} + +void CGUIWindowFullScreen::FrameMove() +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!appPlayer->HasPlayer()) + return; + + //---------------------- + // ViewMode Information + //---------------------- + + auto now = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::milliseconds>(now - m_dwShowViewModeTimeout); + + if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0 && duration.count() > 2500) + { + m_dwShowViewModeTimeout = {}; + m_viewModeChanged = true; + } + + if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0) + { + RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(); + + { + // get the "View Mode" string + const std::string& strTitle = g_localizeStrings.Get(629); + const auto& vs = appPlayer->GetVideoSettings(); + int sId = CViewModeSettings::GetViewModeStringIndex(vs.m_ViewMode); + const std::string& strMode = g_localizeStrings.Get(sId); + std::string strInfo = StringUtils::Format("{} : {}", strTitle, strMode); + CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), LABEL_ROW1); + msg.SetLabel(strInfo); + OnMessage(msg); + } + // show sizing information + VideoStreamInfo info; + appPlayer->GetVideoStreamInfo(CURRENT_STREAM, info); + { + // Splitres scaling factor + float xscale = (float)res.iScreenWidth / (float)res.iWidth; + float yscale = (float)res.iScreenHeight / (float)res.iHeight; + + std::string strSizing = StringUtils::Format( + g_localizeStrings.Get(245), (int)info.SrcRect.Width(), (int)info.SrcRect.Height(), + (int)(info.DestRect.Width() * xscale), (int)(info.DestRect.Height() * yscale), + CDisplaySettings::GetInstance().GetZoomAmount(), + info.videoAspectRatio * CDisplaySettings::GetInstance().GetPixelRatio(), + CDisplaySettings::GetInstance().GetPixelRatio(), + CDisplaySettings::GetInstance().GetVerticalShift()); + CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), LABEL_ROW2); + msg.SetLabel(strSizing); + OnMessage(msg); + } + // show resolution information + { + std::string strStatus; + if (CServiceBroker::GetWinSystem()->IsFullScreen()) + strStatus = StringUtils::Format("{} {}x{}@{:.2f}Hz - {}", g_localizeStrings.Get(13287), + res.iScreenWidth, res.iScreenHeight, res.fRefreshRate, + g_localizeStrings.Get(244)); + else + strStatus = + StringUtils::Format("{} {}x{} - {}", g_localizeStrings.Get(13287), res.iScreenWidth, + res.iScreenHeight, g_localizeStrings.Get(242)); + + CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), LABEL_ROW3); + msg.SetLabel(strStatus); + OnMessage(msg); + } + } + + if (m_viewModeChanged) + { + if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0) + { + SET_CONTROL_VISIBLE(LABEL_ROW1); + SET_CONTROL_VISIBLE(LABEL_ROW2); + SET_CONTROL_VISIBLE(LABEL_ROW3); + SET_CONTROL_VISIBLE(BLUE_BAR); + } + else + { + SET_CONTROL_HIDDEN(LABEL_ROW1); + SET_CONTROL_HIDDEN(LABEL_ROW2); + SET_CONTROL_HIDDEN(LABEL_ROW3); + SET_CONTROL_HIDDEN(BLUE_BAR); + } + m_viewModeChanged = false; + } +} + +void CGUIWindowFullScreen::Process(unsigned int currentTime, CDirtyRegionList &dirtyregion) +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsRenderingGuiLayer()) + MarkDirtyRegion(); + + m_controlStats->Reset(); + + CGUIWindow::Process(currentTime, dirtyregion); + + //! @todo This isn't quite optimal - ideally we'd only be dirtying up the actual video render rect + //! which is probably the job of the renderer as it can more easily track resizing etc. + m_renderRegion.SetRect(0, 0, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()); +} + +void CGUIWindowFullScreen::Render() +{ + CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(), false); + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->Render(true, 255); + CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(m_coordsRes, m_needsScaling); + CGUIWindow::Render(); +} + +void CGUIWindowFullScreen::RenderEx() +{ + CGUIWindow::RenderEx(); + CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(), false); + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->Render(false, 255, false); + CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(m_coordsRes, m_needsScaling); +} + +void CGUIWindowFullScreen::SeekChapter(int iChapter) +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->SeekChapter(iChapter); +} + +void CGUIWindowFullScreen::ToggleOSD() +{ + CGUIDialog *pOSD = GetOSD(); + if (pOSD) + { + if (pOSD->IsDialogRunning()) + pOSD->Close(); + else + pOSD->Open(); + } + + MarkDirtyRegion(); +} + +void CGUIWindowFullScreen::TriggerOSD() +{ + CGUIDialog *pOSD = GetOSD(); + if (pOSD && !pOSD->IsDialogRunning()) + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!appPlayer->IsPlayingGame()) + pOSD->SetAutoClose(3000); + pOSD->Open(); + } +} + +bool CGUIWindowFullScreen::HasVisibleControls() +{ + return m_controlStats->nCountVisible > 0; +} + +CGUIDialog* CGUIWindowFullScreen::GetOSD() +{ + return CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_VIDEO_OSD); +} diff --git a/xbmc/video/windows/GUIWindowFullScreen.h b/xbmc/video/windows/GUIWindowFullScreen.h new file mode 100644 index 0000000..de4e38d --- /dev/null +++ b/xbmc/video/windows/GUIWindowFullScreen.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "guilib/GUIWindow.h" + +#include <chrono> + +class CGUIDialog; + +class CGUIWindowFullScreen : public CGUIWindow +{ +public: + CGUIWindowFullScreen(); + ~CGUIWindowFullScreen(void) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + void ClearBackground() override; + void FrameMove() override; + void Process(unsigned int currentTime, CDirtyRegionList &dirtyregion) override; + void Render() override; + void RenderEx() override; + void OnWindowLoaded() override; + bool HasVisibleControls() override; + +protected: + EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override; + +private: + void SeekChapter(int iChapter); + void ToggleOSD(); + void TriggerOSD(); + CGUIDialog *GetOSD(); + + bool m_viewModeChanged; + std::chrono::time_point<std::chrono::steady_clock> m_dwShowViewModeTimeout; + + bool m_bShowCurrentTime; +}; diff --git a/xbmc/video/windows/GUIWindowFullScreenDefines.h b/xbmc/video/windows/GUIWindowFullScreenDefines.h new file mode 100644 index 0000000..cf67af5 --- /dev/null +++ b/xbmc/video/windows/GUIWindowFullScreenDefines.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2017-2018 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. + */ + +#pragma once + +#define BLUE_BAR 0 +#define LABEL_ROW1 10 +#define LABEL_ROW2 11 +#define LABEL_ROW3 12 diff --git a/xbmc/video/windows/GUIWindowVideoBase.cpp b/xbmc/video/windows/GUIWindowVideoBase.cpp new file mode 100644 index 0000000..94164ef --- /dev/null +++ b/xbmc/video/windows/GUIWindowVideoBase.cpp @@ -0,0 +1,1589 @@ +/* + * Copyright (C) 2005-2018 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 "GUIWindowVideoBase.h" + +#include "Autorun.h" +#include "GUIPassword.h" +#include "GUIUserMessages.h" +#include "PartyModeManager.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "addons/gui/GUIDialogAddonInfo.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "cores/playercorefactory/PlayerCoreFactory.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogSmartPlaylistEditor.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/Directory.h" +#include "filesystem/MultiPathDirectory.h" +#include "filesystem/StackDirectory.h" +#include "filesystem/VideoDatabaseDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/dialogs/GUIDialogMusicInfo.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingUtils.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/dialogs/GUIDialogContentSettings.h" +#include "settings/lib/Setting.h" +#include "storage/MediaManager.h" +#include "utils/FileExtensionProvider.h" +#include "utils/FileUtils.h" +#include "utils/GroupUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoInfoScanner.h" +#include "video/VideoLibraryQueue.h" +#include "video/VideoUtils.h" +#include "video/dialogs/GUIDialogVideoInfo.h" +#include "view/GUIViewState.h" + +using namespace XFILE; +using namespace VIDEODATABASEDIRECTORY; +using namespace VIDEO; +using namespace ADDON; +using namespace PVR; +using namespace KODI::MESSAGING; + +#define CONTROL_BTNVIEWASICONS 2 +#define CONTROL_BTNSORTBY 3 +#define CONTROL_BTNSORTASC 4 +#define CONTROL_LABELFILES 12 + +#define CONTROL_PLAY_DVD 6 + +#define PROPERTY_GROUP_BY "group.by" +#define PROPERTY_GROUP_MIXED "group.mixed" + +static constexpr int SETTING_AUTOPLAYNEXT_MUSICVIDEOS = 0; +static constexpr int SETTING_AUTOPLAYNEXT_EPISODES = 2; +static constexpr int SETTING_AUTOPLAYNEXT_MOVIES = 3; +static constexpr int SETTING_AUTOPLAYNEXT_UNCATEGORIZED = 4; + +CGUIWindowVideoBase::CGUIWindowVideoBase(int id, const std::string &xmlFile) + : CGUIMediaWindow(id, xmlFile.c_str()) +{ + m_thumbLoader.SetObserver(this); + m_stackingAvailable = true; + m_dlgProgress = NULL; +} + +CGUIWindowVideoBase::~CGUIWindowVideoBase() = default; + +bool CGUIWindowVideoBase::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_SCAN_ITEM) + return OnContextButton(m_viewControl.GetSelectedItem(),CONTEXT_BUTTON_SCAN); + else if (action.GetID() == ACTION_SHOW_PLAYLIST) + { + if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO || + CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_VIDEO).size() > 0) + { + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_PLAYLIST); + return true; + } + } + + return CGUIMediaWindow::OnAction(action); +} + +bool CGUIWindowVideoBase::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_DEINIT: + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + m_database.Close(); + break; + + case GUI_MSG_WINDOW_INIT: + { + m_database.Open(); + m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + return CGUIMediaWindow::OnMessage(message); + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); +#if defined(HAS_DVD_DRIVE) + if (iControl == CONTROL_PLAY_DVD) + { + // play movie... + MEDIA_DETECT::CAutorun::PlayDiscAskResume( + CServiceBroker::GetMediaManager().TranslateDevicePath("")); + } + else +#endif + if (m_viewControl.HasControl(iControl)) // list/thumb control + { + // get selected item + int iItem = m_viewControl.GetSelectedItem(); + int iAction = message.GetParam1(); + + // iItem is checked for validity inside these routines + if (iAction == ACTION_QUEUE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK) + { + OnQueueItem(iItem); + return true; + } + else if (iAction == ACTION_QUEUE_ITEM_NEXT) + { + OnQueueItem(iItem, true); + return true; + } + else if (iAction == ACTION_SHOW_INFO) + { + return OnItemInfo(iItem); + } + else if (iAction == ACTION_PLAYER_PLAY) + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + // if playback is paused or playback speed != 1, return + if (appPlayer->IsPlayingVideo()) + { + if (appPlayer->IsPausedPlayback()) + return false; + if (appPlayer->GetPlaySpeed() != 1) + return false; + } + + // not playing video, or playback speed == 1 + return OnResumeItem(iItem); + } + else if (iAction == ACTION_DELETE_ITEM) + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + // is delete allowed? + if (profileManager->GetCurrentProfile().canWriteDatabases()) + { + // must be at the title window + if (GetID() == WINDOW_VIDEO_NAV) + OnDeleteItem(iItem); + + // or be at the video playlists location + else if (m_vecItems->IsPath("special://videoplaylists/")) + OnDeleteItem(iItem); + else + return false; + + return true; + } + } + } + } + break; + case GUI_MSG_SEARCH: + OnSearch(); + break; + } + return CGUIMediaWindow::OnMessage(message); +} + +bool CGUIWindowVideoBase::OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& scraper) +{ + if (fileItem.IsParentFolder() || fileItem.m_bIsShareOrDrive || fileItem.IsPath("add") || + (fileItem.IsPlayList() && !URIUtils::HasExtension(fileItem.GetDynPath(), ".strm"))) + return false; + + CFileItem item(fileItem); + bool fromDB = false; + if ((item.IsVideoDb() && item.HasVideoInfoTag()) || + (item.HasVideoInfoTag() && item.GetVideoInfoTag()->m_iDbId != -1)) + { + if (item.GetVideoInfoTag()->m_type == MediaTypeSeason) + { // clear out the art - we're really grabbing the info on the show here + item.ClearArt(); + item.GetVideoInfoTag()->m_iDbId = item.GetVideoInfoTag()->m_iIdShow; + } + item.SetPath(item.GetVideoInfoTag()->GetPath()); + fromDB = true; + } + else + { + if (item.m_bIsFolder && scraper && scraper->Content() != CONTENT_TVSHOWS) + { + CFileItemList items; + CDirectory::GetDirectory(item.GetPath(), items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), + DIR_FLAG_DEFAULTS); + + // Check for cases 1_dir/1_dir/.../file (e.g. by packages where have a extra folder) + while (items.Size() == 1 && items[0]->m_bIsFolder) + { + const std::string path = items[0]->GetPath(); + items.Clear(); + CDirectory::GetDirectory(path, items, + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), + DIR_FLAG_DEFAULTS); + } + + items.Stack(); + + // check for media files + bool bFoundFile(false); + for (int i = 0; i < items.Size(); ++i) + { + CFileItemPtr item2 = items[i]; + + if (item2->IsVideo() && !item2->IsPlayList() && + !CUtil::ExcludeFileOrFolder(item2->GetPath(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps)) + { + item.SetPath(item2->GetPath()); + item.m_bIsFolder = false; + bFoundFile = true; + break; + } + } + + // no video file in this folder + if (!bFoundFile) + { + HELPERS::ShowOKDialogText(CVariant{13346}, CVariant{20349}); + return false; + } + } + } + + // we need to also request any thumbs be applied to the folder item + if (fileItem.m_bIsFolder) + item.SetProperty("set_folder_thumb", fileItem.GetPath()); + + bool modified = ShowIMDB(CFileItemPtr(new CFileItem(item)), scraper, fromDB); + if (modified && + (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV)) // since we can be called from the music library we need this check + { + int itemNumber = m_viewControl.GetSelectedItem(); + Refresh(); + m_viewControl.SetSelectedItem(itemNumber); + } + return true; +} + +// ShowIMDB is called as follows: +// 1. To lookup info on a file. +// 2. To lookup info on a folder (which may or may not contain a file) +// 3. To lookup info just for fun (no file or folder related) + +// We just need the item object for this. +// A "blank" item object is sent for 3. +// If a folder is sent, currently it sets strFolder and bFolder +// this is only used for setting the folder thumb, however. + +// Steps should be: + +// 1. Check database to see if we have this information already +// 2. Else, check for a nfoFile to get the URL +// 3. Run a loop to check for refresh +// 4. If no URL is present do a search to get the URL +// 4. Once we have the URL, download the details +// 5. Once we have the details, add to the database if necessary (case 1,2) +// and show the information. +// 6. Check for a refresh, and if so, go to 3. + +bool CGUIWindowVideoBase::ShowIMDB(CFileItemPtr item, const ScraperPtr &info2, bool fromDB) +{ + /* + CLog::Log(LOGDEBUG,"CGUIWindowVideoBase::ShowIMDB"); + CLog::Log(LOGDEBUG," strMovie = [{}]", strMovie); + CLog::Log(LOGDEBUG," strFile = [{}]", strFile); + CLog::Log(LOGDEBUG," strFolder = [{}]", strFolder); + CLog::Log(LOGDEBUG," bFolder = [{}]", ((int)bFolder ? "true" : "false")); + */ + + CGUIDialogProgress* pDlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + CGUIDialogVideoInfo* pDlgInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogVideoInfo>(WINDOW_DIALOG_VIDEO_INFO); + + const ScraperPtr& info(info2); // use this as nfo might change it.. + + if (!pDlgProgress) return false; + if (!pDlgSelect) return false; + if (!pDlgInfo) return false; + + // 1. Check for already downloaded information, and if we have it, display our dialog + // Return if no Refresh is needed. + bool bHasInfo=false; + + CVideoInfoTag movieDetails; + if (info) + { + m_database.Open(); // since we can be called from the music library + + int dbId = item->HasVideoInfoTag() ? item->GetVideoInfoTag()->m_iDbId : -1; + if (info->Content() == CONTENT_MOVIES) + { + bHasInfo = m_database.GetMovieInfo(item->GetPath(), movieDetails, dbId); + } + if (info->Content() == CONTENT_TVSHOWS) + { + if (item->m_bIsFolder) + { + bHasInfo = m_database.GetTvShowInfo(item->GetPath(), movieDetails, dbId); + } + else + { + bHasInfo = m_database.GetEpisodeInfo(item->GetPath(), movieDetails, dbId); + if (!bHasInfo) + { + // !! WORKAROUND !! + // As we cannot add an episode to a non-existing tvshow entry, we have to check the parent directory + // to see if it`s already in our video database. If it's not yet part of the database we will exit here. + // (Ticket #4764) + // + // NOTE: This will fail for episodes on multipath shares, as the parent path isn't what is stored in the + // database. Possible solutions are to store the paths in the db separately and rely on the show + // stacking stuff, or to modify GetTvShowId to do support multipath:// shares + std::string strParentDirectory; + URIUtils::GetParentPath(item->GetPath(), strParentDirectory); + if (m_database.GetTvShowId(strParentDirectory) < 0) + { + CLog::Log(LOGERROR, "{}: could not add episode [{}]. tvshow does not exist yet..", + __FUNCTION__, item->GetPath()); + return false; + } + } + } + } + if (info->Content() == CONTENT_MUSICVIDEOS) + { + bHasInfo = m_database.GetMusicVideoInfo(item->GetPath(), movieDetails); + } + m_database.Close(); + } + else if(item->HasVideoInfoTag()) + { + bHasInfo = true; + movieDetails = *item->GetVideoInfoTag(); + } + + bool needsRefresh = false; + if (bHasInfo) + { + if (!info || info->Content() == CONTENT_NONE) // disable refresh button + item->SetProperty("xxuniqueid", "xx" + movieDetails.GetUniqueID()); + *item->GetVideoInfoTag() = movieDetails; + pDlgInfo->SetMovie(item.get()); + pDlgInfo->Open(); + if (pDlgInfo->HasUpdatedUserrating()) + return true; + needsRefresh = pDlgInfo->NeedRefresh(); + if (!needsRefresh) + return pDlgInfo->HasUpdatedThumb(); + // check if the item in the video info dialog has changed and if so, get the new item + else if (pDlgInfo->GetCurrentListItem() != NULL) + { + item = pDlgInfo->GetCurrentListItem(); + + if (item->IsVideoDb() && item->HasVideoInfoTag()) + item->SetPath(item->GetVideoInfoTag()->GetPath()); + } + } + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + // quietly return if Internet lookups are disabled + if (!profileManager->GetCurrentProfile().canWriteDatabases() && !g_passwordManager.bMasterUser) + return false; + + if (!info) + return false; + + if (CVideoLibraryQueue::GetInstance().IsScanningLibrary()) + { + HELPERS::ShowOKDialogText(CVariant{13346}, CVariant{14057}); + return false; + } + + bool listNeedsUpdating = false; + // 3. Run a loop so that if we Refresh we re-run this block + do + { + if (!CVideoLibraryQueue::GetInstance().RefreshItemModal(item, needsRefresh, pDlgInfo->RefreshAll())) + return listNeedsUpdating; + + // remove directory caches and reload images + CUtil::DeleteVideoDatabaseDirectoryCache(); + CGUIMessage reload(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS); + OnMessage(reload); + + pDlgInfo->SetMovie(item.get()); + pDlgInfo->Open(); + item->SetArt("thumb", pDlgInfo->GetThumbnail()); + needsRefresh = pDlgInfo->NeedRefresh(); + listNeedsUpdating = true; + } while (needsRefresh); + + return listNeedsUpdating; +} + +void CGUIWindowVideoBase::OnQueueItem(int iItem, bool first) +{ + // don't re-queue items from playlist window + if (GetID() == WINDOW_VIDEO_PLAYLIST) + return; + + if (iItem < 0 || iItem >= m_vecItems->Size()) + return; + + // add item 2 playlist + const auto item = m_vecItems->Get(iItem); + + if (item->IsRAR() || item->IsZIP()) + return; + + VIDEO_UTILS::QueueItem(item, first ? VIDEO_UTILS::QueuePosition::POSITION_BEGIN + : VIDEO_UTILS::QueuePosition::POSITION_END); + + // select next item + m_viewControl.SetSelectedItem(iItem + 1); +} + +bool CGUIWindowVideoBase::OnClick(int iItem, const std::string &player) +{ + return CGUIMediaWindow::OnClick(iItem, player); +} + +bool CGUIWindowVideoBase::OnSelect(int iItem) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) + return false; + + CFileItemPtr item = m_vecItems->Get(iItem); + + std::string path = item->GetPath(); + if (!item->m_bIsFolder && path != "add" && + !StringUtils::StartsWith(path, "newsmartplaylist://") && + !StringUtils::StartsWith(path, "newplaylist://") && + !StringUtils::StartsWith(path, "newtag://") && + !StringUtils::StartsWith(path, "script://")) + return OnFileAction(iItem, CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MYVIDEOS_SELECTACTION), ""); + + return CGUIMediaWindow::OnSelect(iItem); +} + +bool CGUIWindowVideoBase::OnFileAction(int iItem, int action, const std::string& player) +{ + CFileItemPtr item = m_vecItems->Get(iItem); + if (!item) + { + return false; + } + + // Reset the current start offset. The actual resume + // option is set in the switch, based on the action passed. + item->SetStartOffset(0); + + switch (action) + { + case SELECT_ACTION_CHOOSE: + { + CContextButtons choices; + + if (item->IsVideoDb()) + { + std::string itemPath(item->GetPath()); + itemPath = item->GetVideoInfoTag()->m_strFileNameAndPath; + if (URIUtils::IsStack(itemPath) && CFileItem(CStackDirectory::GetFirstStackedFile(itemPath),false).IsDiscImage()) + choices.Add(SELECT_ACTION_PLAYPART, 20324); // Play Part + } + + std::string resumeString = GetResumeString(*item); + if (!resumeString.empty()) + { + choices.Add(SELECT_ACTION_RESUME, resumeString); + choices.Add(SELECT_ACTION_PLAY, 12021); // Play from beginning + } + else + choices.Add(SELECT_ACTION_PLAY, 208); // Play + + choices.Add(SELECT_ACTION_INFO, 22081); // Info + choices.Add(SELECT_ACTION_MORE, 22082); // More + int value = CGUIDialogContextMenu::ShowAndGetChoice(choices); + if (value < 0) + return true; + + return OnFileAction(iItem, value, player); + } + break; + case SELECT_ACTION_PLAY_OR_RESUME: + return OnResumeItem(iItem, player); + case SELECT_ACTION_INFO: + return OnItemInfo(iItem); + case SELECT_ACTION_MORE: + OnPopupMenu(iItem); + return true; + case SELECT_ACTION_RESUME: + item->SetStartOffset(STARTOFFSET_RESUME); + if (item->m_bIsFolder) + { + PlayItem(iItem, player); + return true; + } + break; + case SELECT_ACTION_PLAYPART: + if (!OnPlayStackPart(iItem)) + return false; + break; + case SELECT_ACTION_QUEUE: + OnQueueItem(iItem); + return true; + case SELECT_ACTION_PLAY: + if (item->m_bIsFolder) + { + PlayItem(iItem, player); + return true; + } + break; + default: + break; + } + return OnClick(iItem, player); +} + +bool CGUIWindowVideoBase::OnItemInfo(int iItem) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) + return false; + + CFileItemPtr item = m_vecItems->Get(iItem); + + if (item->IsPath("add") || item->IsParentFolder() || + (item->IsPlayList() && !URIUtils::HasExtension(item->GetDynPath(), ".strm"))) + return false; + + if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript())) + return CGUIDialogAddonInfo::ShowForItem(item); + + if (item->m_bIsFolder && + item->IsVideoDb() && + StringUtils::StartsWith(item->GetPath(), "videodb://movies/sets/")) + return ShowIMDB(item, nullptr, true); + + ADDON::ScraperPtr scraper; + + // Match visibility test of CMusicInfo::IsVisible + if (item->IsVideoDb() && item->HasVideoInfoTag() && + (item->HasProperty("artist_musicid") || item->HasProperty("album_musicid"))) + { + CGUIDialogMusicInfo::ShowFor(item.get()); + return true; + } + if (!m_vecItems->IsPlugin() && !m_vecItems->IsRSS() && !m_vecItems->IsLiveTV()) + { + std::string strDir; + if (item->IsVideoDb() && + item->HasVideoInfoTag() && + !item->GetVideoInfoTag()->m_strPath.empty()) + { + strDir = item->GetVideoInfoTag()->m_strPath; + } + else + strDir = URIUtils::GetDirectory(item->GetPath()); + + SScanSettings settings; + bool foundDirectly = false; + scraper = m_database.GetScraperForPath(strDir, settings, foundDirectly); + + if (!scraper && + !(m_database.HasMovieInfo(item->GetDynPath()) || m_database.HasTvShowInfo(strDir) || + m_database.HasEpisodeInfo(item->GetDynPath()))) + { + HELPERS::ShowOKDialogText(CVariant{20176}, // Show video information + CVariant{19055}); // no information available + return false; + } + + if (scraper && scraper->Content() == CONTENT_TVSHOWS && foundDirectly && !settings.parent_name_root) // dont lookup on root tvshow folder + return true; + } + + return OnItemInfo(*item, scraper); +} + +void CGUIWindowVideoBase::OnRestartItem(int iItem, const std::string &player) +{ + CGUIMediaWindow::OnClick(iItem, player); +} + +void CGUIWindowVideoBase::LoadVideoInfo(CFileItemList& items, + CVideoDatabase& database, + bool allowReplaceLabels) +{ + //! @todo this could possibly be threaded as per the music info loading, + //! we could also cache the info + if (!items.GetContent().empty() && !items.IsPlugin()) + return; // don't load for listings that have content set and weren't created from plugins + + std::string content = items.GetContent(); + // determine content only if it isn't set + if (content.empty()) + { + content = database.GetContentForPath(items.GetPath()); + items.SetContent((content.empty() && !items.IsPlugin()) ? "files" : content); + } + + /* + If we have a matching item in the library, so we can assign the metadata to it. In addition, we can choose + * whether the item is stacked down (eg in the case of folders representing a single item) + * whether or not we assign the library's labels to the item, or leave the item as is. + + As certain users (read: certain developers) don't want either of these to occur, we compromise by stacking + items down only if stacking is available and enabled. + + Similarly, we assign the "clean" library labels to the item only if the "Replace filenames with library titles" + setting is enabled. + */ + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + const bool stackItems = + items.GetProperty("isstacked").asBoolean() || + (StackingAvailable(items) && settings->GetBool(CSettings::SETTING_MYVIDEOS_STACKVIDEOS)); + const bool replaceLabels = + allowReplaceLabels && settings->GetBool(CSettings::SETTING_MYVIDEOS_REPLACELABELS); + + CFileItemList dbItems; + /* NOTE: In the future when GetItemsForPath returns all items regardless of whether they're "in the library" + we won't need the fetchedPlayCounts code, and can "simply" do this directly on absence of content. */ + bool fetchedPlayCounts = false; + if (!content.empty()) + { + database.GetItemsForPath(content, items.GetPath(), dbItems); + dbItems.SetFastLookup(true); + } + + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr pItem = items[i]; + CFileItemPtr match; + + if (pItem->m_bIsFolder && !pItem->IsParentFolder()) + { + // we need this for enabling the right context menu entries, like mark watched / unwatched + pItem->SetProperty("IsVideoFolder", true); + } + + if (!content + .empty()) /* optical media will be stacked down, so it's path won't match the base path */ + { + std::string pathToMatch = + pItem->IsOpticalMediaFile() ? pItem->GetLocalMetadataPath() : pItem->GetPath(); + if (URIUtils::IsMultiPath(pathToMatch)) + pathToMatch = CMultiPathDirectory::GetFirstPath(pathToMatch); + match = dbItems.Get(pathToMatch); + } + if (match) + { + pItem->UpdateInfo(*match, replaceLabels); + + if (stackItems) + { + if (match->m_bIsFolder) + pItem->SetPath(match->GetVideoInfoTag()->m_strPath); + else + pItem->SetPath(match->GetVideoInfoTag()->m_strFileNameAndPath); + // if we switch from a file to a folder item it means we really shouldn't be sorting files and + // folders separately + if (pItem->m_bIsFolder != match->m_bIsFolder) + { + items.SetSortIgnoreFolders(true); + pItem->m_bIsFolder = match->m_bIsFolder; + } + } + } + else + { + /* NOTE: Currently we GetPlayCounts on our items regardless of whether content is set + as if content is set, GetItemsForPaths doesn't return anything not in the content tables. + This code can be removed once the content tables are always filled */ + if (!pItem->m_bIsFolder && !fetchedPlayCounts) + { + database.GetPlayCounts(items.GetPath(), items); + fetchedPlayCounts = true; + } + + // set the watched overlay + if (pItem->IsVideo()) + pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, + pItem->HasVideoInfoTag() && + pItem->GetVideoInfoTag()->GetPlayCount() > 0); + } + } +} + +std::string CGUIWindowVideoBase::GetResumeString(const CFileItem &item) +{ + const VIDEO_UTILS::ResumeInformation resumeInfo = VIDEO_UTILS::GetItemResumeInformation(item); + if (resumeInfo.isResumable) + { + if (resumeInfo.startOffset > 0) + { + std::string resumeString = StringUtils::Format( + g_localizeStrings.Get(12022), + StringUtils::SecondsToTimeString( + static_cast<long>(CUtil::ConvertMilliSecsToSecsInt(resumeInfo.startOffset)), + TIME_FORMAT_HH_MM_SS)); + if (resumeInfo.partNumber > 0) + { + const std::string partString = + StringUtils::Format(g_localizeStrings.Get(23051), resumeInfo.partNumber); + resumeString += " (" + partString + ")"; + } + return resumeString; + } + else + { + return g_localizeStrings.Get(13362); // Continue watching + } + } + return {}; +} + +bool CGUIWindowVideoBase::ShowResumeMenu(CFileItem &item) +{ + if (!item.IsLiveTV()) + { + std::string resumeString = GetResumeString(item); + if (!resumeString.empty()) + { // prompt user whether they wish to resume + CContextButtons choices; + choices.Add(1, resumeString); + choices.Add(2, 12021); // Play from beginning + int retVal = CGUIDialogContextMenu::ShowAndGetChoice(choices); + if (retVal < 0) + return false; // don't do anything + if (retVal == 1) + item.SetStartOffset(STARTOFFSET_RESUME); + } + } + return true; +} + +bool CGUIWindowVideoBase::OnResumeItem(int iItem, const std::string &player) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) return true; + CFileItemPtr item = m_vecItems->Get(iItem); + + std::string resumeString = GetResumeString(*item); + + if (!resumeString.empty()) + { + CContextButtons choices; + choices.Add(SELECT_ACTION_RESUME, resumeString); + choices.Add(SELECT_ACTION_PLAY, 12021); // Play from beginning + int value = CGUIDialogContextMenu::ShowAndGetChoice(choices); + if (value < 0) + return true; + return OnFileAction(iItem, value, player); + } + + if (item->m_bIsFolder) + { + // resuming directories isn't fully supported yet. play all of its content. + PlayItem(iItem, player); + return true; + } + + return OnFileAction(iItem, SELECT_ACTION_PLAY, player); +} + +void CGUIWindowVideoBase::GetContextButtons(int itemNumber, CContextButtons &buttons) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + + // contextual buttons + if (item) + { + if (!item->IsParentFolder()) + { + std::string path(item->GetPath()); + if (item->IsVideoDb() && item->HasVideoInfoTag()) + path = item->GetVideoInfoTag()->m_strFileNameAndPath; + + if (!item->IsPath("add") && !item->IsPlugin() && + !item->IsScript() && !item->IsAddonsPath() && !item->IsLiveTV()) + { + if (URIUtils::IsStack(path)) + { + std::vector<uint64_t> times; + if (m_database.GetStackTimes(path,times) || CFileItem(CStackDirectory::GetFirstStackedFile(path),false).IsDiscImage()) + buttons.Add(CONTEXT_BUTTON_PLAY_PART, 20324); + } + } + + if (!item->m_bIsFolder && !(item->IsPlayList() && !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders)) + { + const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + // get players + std::vector<std::string> players; + if (item->IsVideoDb()) + { + CFileItem item2(item->GetVideoInfoTag()->m_strFileNameAndPath, false); + playerCoreFactory.GetPlayers(item2, players); + } + else + playerCoreFactory.GetPlayers(*item, players); + + if (players.size() > 1) + buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); + } + if (item->IsSmartPlayList()) + { + buttons.Add(CONTEXT_BUTTON_PLAY_PARTYMODE, 15216); // Play in Partymode + } + + // if the item isn't a folder or script, is not explicitly marked as not playable, + // is a member of a list rather than a single item and we're not on the last element of the list, + // then add either 'play from here' or 'play only this' depending on default behaviour + if (!(item->m_bIsFolder || item->IsScript()) && + (!item->HasProperty("IsPlayable") || item->GetProperty("IsPlayable").asBoolean()) && + m_vecItems->Size() > 1 && itemNumber < m_vecItems->Size() - 1) + { + int settingValue = SETTING_AUTOPLAYNEXT_UNCATEGORIZED; + + if (item->IsVideoDb() && item->HasVideoInfoTag()) + { + const std::string mediaType = item->GetVideoInfoTag()->m_type; + + if (mediaType == MediaTypeMusicVideo) + settingValue = SETTING_AUTOPLAYNEXT_MUSICVIDEOS; + else if (mediaType == MediaTypeEpisode) + settingValue = SETTING_AUTOPLAYNEXT_EPISODES; + else if (mediaType == MediaTypeMovie) + settingValue = SETTING_AUTOPLAYNEXT_MOVIES; + } + + const auto setting = std::dynamic_pointer_cast<CSettingList>( + CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting( + CSettings::SETTING_VIDEOPLAYER_AUTOPLAYNEXTITEM)); + + if (setting && CSettingUtils::FindIntInList(setting, settingValue)) + buttons.Add(CONTEXT_BUTTON_PLAY_ONLY_THIS, 13434); + else + buttons.Add(CONTEXT_BUTTON_PLAY_AND_QUEUE, 13412); + } + if (item->IsSmartPlayList() || m_vecItems->IsSmartPlayList()) + buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586); + } + } + CGUIMediaWindow::GetContextButtons(itemNumber, buttons); +} + +bool CGUIWindowVideoBase::OnPlayStackPart(int iItem) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) + return false; + + CFileItemPtr stack = m_vecItems->Get(iItem); + std::string path(stack->GetPath()); + if (stack->IsVideoDb()) + path = stack->GetVideoInfoTag()->m_strFileNameAndPath; + + if (!URIUtils::IsStack(path)) + return false; + + CFileItemList parts; + CDirectory::GetDirectory(path, parts, "", DIR_FLAG_DEFAULTS); + + for (int i = 0; i < parts.Size(); i++) + parts[i]->SetLabel(StringUtils::Format(g_localizeStrings.Get(23051), i + 1)); + + CGUIDialogSelect* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + + pDialog->Reset(); + pDialog->SetHeading(CVariant{20324}); + pDialog->SetItems(parts); + pDialog->Open(); + + if (!pDialog->IsConfirmed()) + return false; + + int selectedFile = pDialog->GetSelectedItem(); + if (selectedFile >= 0) + { + // ISO stack + if (CFileItem(CStackDirectory::GetFirstStackedFile(path),false).IsDiscImage()) + { + std::string resumeString = CGUIWindowVideoBase::GetResumeString(*(parts[selectedFile].get())); + stack->SetStartOffset(0); + if (!resumeString.empty()) + { + CContextButtons choices; + choices.Add(SELECT_ACTION_RESUME, resumeString); + choices.Add(SELECT_ACTION_PLAY, 12021); // Play from beginning + int value = CGUIDialogContextMenu::ShowAndGetChoice(choices); + if (value == SELECT_ACTION_RESUME) + { + const VIDEO_UTILS::ResumeInformation resumeInfo = + VIDEO_UTILS::GetItemResumeInformation(*parts[selectedFile]); + stack->SetStartOffset(resumeInfo.startOffset); + stack->m_lStartPartNumber = resumeInfo.partNumber; + } + else if (value != SELECT_ACTION_PLAY) + return false; // if not selected PLAY, then we changed our mind so return + } + stack->m_lStartPartNumber = selectedFile + 1; + } + // regular stack + else + { + if (selectedFile > 0) + { + std::vector<uint64_t> times; + if (m_database.GetStackTimes(path,times)) + stack->SetStartOffset(times[selectedFile - 1]); + } + else + stack->SetStartOffset(0); + } + + + } + + return true; +} + +bool CGUIWindowVideoBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + switch (button) + { + case CONTEXT_BUTTON_SET_CONTENT: + { + OnAssignContent(item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strPath.empty() ? item->GetVideoInfoTag()->m_strPath : item->GetPath()); + return true; + } + case CONTEXT_BUTTON_PLAY_PART: + { + if (OnPlayStackPart(itemNumber)) + { + // call CGUIMediaWindow::OnClick() as otherwise autoresume will kick in + CGUIMediaWindow::OnClick(itemNumber); + return true; + } + else + return false; + } + case CONTEXT_BUTTON_PLAY_WITH: + { + const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + std::vector<std::string> players; + if (item->IsVideoDb()) + { + CFileItem item2(*item->GetVideoInfoTag()); + playerCoreFactory.GetPlayers(item2, players); + } + else + playerCoreFactory.GetPlayers(*item, players); + + std:: string player = playerCoreFactory.SelectPlayerDialog(players); + if (!player.empty()) + { + // any other select actions but play or resume, resume, play or playpart + // don't make any sense here since the user already decided that he'd + // like to play the item (just with a specific player) + VideoSelectAction selectAction = (VideoSelectAction)CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MYVIDEOS_SELECTACTION); + if (selectAction != SELECT_ACTION_PLAY_OR_RESUME && + selectAction != SELECT_ACTION_RESUME && + selectAction != SELECT_ACTION_PLAY && + selectAction != SELECT_ACTION_PLAYPART) + selectAction = SELECT_ACTION_PLAY_OR_RESUME; + return OnFileAction(itemNumber, selectAction, player); + } + return true; + } + + case CONTEXT_BUTTON_PLAY_PARTYMODE: + g_partyModeManager.Enable(PARTYMODECONTEXT_VIDEO, m_vecItems->Get(itemNumber)->GetPath()); + return true; + + case CONTEXT_BUTTON_SCAN: + { + if( !item) + return false; + ADDON::ScraperPtr info; + SScanSettings settings; + GetScraperForItem(item.get(), info, settings); + std::string strPath = item->GetPath(); + if (item->IsVideoDb() && (!item->m_bIsFolder || item->GetVideoInfoTag()->m_strPath.empty())) + return false; + + if (item->IsVideoDb()) + strPath = item->GetVideoInfoTag()->m_strPath; + + if (!info || info->Content() == CONTENT_NONE) + return false; + + if (item->m_bIsFolder) + { + OnScan(strPath, true); + } + else + OnItemInfo(*item, info); + + return true; + } + case CONTEXT_BUTTON_DELETE: + OnDeleteItem(itemNumber); + return true; + case CONTEXT_BUTTON_EDIT_SMART_PLAYLIST: + { + std::string playlist = m_vecItems->Get(itemNumber)->IsSmartPlayList() ? m_vecItems->Get(itemNumber)->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items + if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist, "video")) + Refresh(true); // need to update + return true; + } + case CONTEXT_BUTTON_RENAME: + OnRenameItem(itemNumber); + return true; + case CONTEXT_BUTTON_PLAY_AND_QUEUE: + return OnPlayAndQueueMedia(item); + case CONTEXT_BUTTON_PLAY_ONLY_THIS: + return OnPlayMedia(itemNumber); + default: + break; + } + return CGUIMediaWindow::OnContextButton(itemNumber, button); +} + +bool CGUIWindowVideoBase::OnPlayMedia(int iItem, const std::string &player) +{ + if ( iItem < 0 || iItem >= m_vecItems->Size() ) + return false; + + CFileItemPtr pItem = m_vecItems->Get(iItem); + + // party mode + if (g_partyModeManager.IsEnabled(PARTYMODECONTEXT_VIDEO)) + { + PLAYLIST::CPlayList playlistTemp; + playlistTemp.Add(pItem); + g_partyModeManager.AddUserSongs(playlistTemp, true); + return true; + } + + // Reset Playlistplayer, playback started now does + // not use the playlistplayer. + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE); + + CFileItem item(*pItem); + if (pItem->IsVideoDb()) + { + item.SetPath(pItem->GetVideoInfoTag()->m_strFileNameAndPath); + item.SetProperty("original_listitem_url", pItem->GetPath()); + } + CLog::Log(LOGDEBUG, "{} {}", __FUNCTION__, CURL::GetRedacted(item.GetPath())); + + item.SetProperty("playlist_type_hint", m_guiState->GetPlaylist()); + + PlayMovie(&item, player); + + return true; +} + +bool CGUIWindowVideoBase::OnPlayAndQueueMedia(const CFileItemPtr& item, const std::string& player) +{ + // Get the current playlist and make sure it is not shuffled + PLAYLIST::Id playlistId = m_guiState->GetPlaylist(); + if (playlistId != PLAYLIST::TYPE_NONE && + CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId)) + { + CServiceBroker::GetPlaylistPlayer().SetShuffle(playlistId, false); + } + + CFileItemPtr movieItem(new CFileItem(*item)); + + // Call the base method to actually queue the items + // and start playing the given item + return CGUIMediaWindow::OnPlayAndQueueMedia(movieItem, player); +} + +void CGUIWindowVideoBase::PlayMovie(const CFileItem *item, const std::string &player) +{ + if(m_thumbLoader.IsLoading()) + m_thumbLoader.StopAsync(); + + CServiceBroker::GetPlaylistPlayer().Play(std::make_shared<CFileItem>(*item), player); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!appPlayer->IsPlayingVideo()) + m_thumbLoader.Load(*m_vecItems); +} + +void CGUIWindowVideoBase::OnDeleteItem(int iItem) +{ + if ( iItem < 0 || iItem >= m_vecItems->Size()) + return; + + OnDeleteItem(m_vecItems->Get(iItem)); + + Refresh(true); + m_viewControl.SetSelectedItem(iItem); +} + +void CGUIWindowVideoBase::OnDeleteItem(const CFileItemPtr& item) +{ + // HACK: stacked files need to be treated as folders in order to be deleted + if (item->IsStack()) + item->m_bIsFolder = true; + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (profileManager->GetCurrentProfile().getLockMode() != LOCK_MODE_EVERYONE && + profileManager->GetCurrentProfile().filesLocked()) + { + if (!g_passwordManager.IsMasterLockUnlocked(true)) + return; + } + + if ((CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) || + m_vecItems->IsPath("special://videoplaylists/")) && + CUtil::SupportsWriteFileOperations(item->GetPath())) + { + CGUIComponent *gui = CServiceBroker::GetGUI(); + if (gui && gui->ConfirmDelete(item->GetPath())) + CFileUtils::DeleteItem(item); + } +} + +void CGUIWindowVideoBase::LoadPlayList(const std::string& strPlayList, + PLAYLIST::Id playlistId /* = PLAYLIST::TYPE_VIDEO */) +{ + // if partymode is active, we disable it + if (g_partyModeManager.IsEnabled()) + g_partyModeManager.Disable(); + + // load a playlist like .m3u, .pls + // first get correct factory to load playlist + std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList)); + if (pPlayList) + { + // load it + if (!pPlayList->Load(strPlayList)) + { + HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477}); + return; //hmmm unable to load playlist? + } + } + + if (g_application.ProcessAndStartPlaylist(strPlayList, *pPlayList, playlistId)) + { + if (m_guiState) + m_guiState->SetPlaylistDirectory("playlistvideo://"); + } +} + +void CGUIWindowVideoBase::PlayItem(int iItem, const std::string &player) +{ + // restrictions should be placed in the appropriate window code + // only call the base code if the item passes since this clears + // the currently playing temp playlist + + const CFileItemPtr pItem = m_vecItems->Get(iItem); + // if its a folder, build a temp playlist + if (pItem->m_bIsFolder && !pItem->IsPlugin()) + { + // take a copy so we can alter the queue state + CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem))); + + // Allow queuing of unqueueable items + // when we try to queue them directly + if (!item->CanQueue()) + item->SetCanQueue(true); + + // skip ".." + if (item->IsParentFolder()) + return; + + // recursively add items to list + CFileItemList queuedItems; + VIDEO_UTILS::GetItemsForPlayList(item, queuedItems); + + CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO); + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_VIDEO, queuedItems); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO); + CServiceBroker::GetPlaylistPlayer().Play(); + } + else if (pItem->IsPlayList()) + { + // load the playlist the old way + LoadPlayList(pItem->GetPath(), PLAYLIST::TYPE_VIDEO); + } + else + { + // single item, play it + OnClick(iItem, player); + } +} + +bool CGUIWindowVideoBase::Update(const std::string &strDirectory, bool updateFilterPath /* = true */) +{ + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath)) + return false; + + // might already be running from GetGroupedItems + if (!m_thumbLoader.IsLoading()) + m_thumbLoader.Load(*m_vecItems); + + return true; +} + +bool CGUIWindowVideoBase::GetDirectory(const std::string &strDirectory, CFileItemList &items) +{ + bool bResult = CGUIMediaWindow::GetDirectory(strDirectory, items); + + // add in the "New Playlist" item if we're in the playlists folder + if ((items.GetPath() == "special://videoplaylists/") && !items.Contains("newplaylist://")) + { + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + CFileItemPtr newPlaylist(new CFileItem(profileManager->GetUserDataItem("PartyMode-Video.xsp"),false)); + newPlaylist->SetLabel(g_localizeStrings.Get(16035)); + newPlaylist->SetLabelPreformatted(true); + newPlaylist->SetArt("icon", "DefaultPartyMode.png"); + newPlaylist->m_bIsFolder = true; + items.Add(newPlaylist); + +/* newPlaylist.reset(new CFileItem("newplaylist://", false)); + newPlaylist->SetLabel(g_localizeStrings.Get(525)); + newPlaylist->SetLabelPreformatted(true); + items.Add(newPlaylist); +*/ + newPlaylist.reset(new CFileItem("newsmartplaylist://video", false)); + newPlaylist->SetLabel(g_localizeStrings.Get(21437)); // "new smart playlist..." + newPlaylist->SetArt("icon", "DefaultAddSource.png"); + newPlaylist->SetLabelPreformatted(true); + items.Add(newPlaylist); + } + + m_stackingAvailable = StackingAvailable(items); + // we may also be in a tvshow files listing + // (ideally this should be removed, and our stack regexps tidied up if necessary + // No "normal" episodes should stack, and multi-parts should be supported) + ADDON::ScraperPtr info = m_database.GetScraperForPath(strDirectory); + if (info && info->Content() == CONTENT_TVSHOWS) + m_stackingAvailable = false; + + if (m_stackingAvailable && !items.IsStack() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_STACKVIDEOS)) + items.Stack(); + + return bResult; +} + +bool CGUIWindowVideoBase::StackingAvailable(const CFileItemList &items) +{ + CURL url(items.GetPath()); + return !(items.IsPlugin() || items.IsAddonsPath() || + items.IsRSS() || items.IsInternetStream() || + items.IsVideoDb() || url.IsProtocol("playlistvideo")); +} + +void CGUIWindowVideoBase::GetGroupedItems(CFileItemList &items) +{ + CGUIMediaWindow::GetGroupedItems(items); + + std::string group; + bool mixed = false; + if (items.HasProperty(PROPERTY_GROUP_BY)) + group = items.GetProperty(PROPERTY_GROUP_BY).asString(); + if (items.HasProperty(PROPERTY_GROUP_MIXED)) + mixed = items.GetProperty(PROPERTY_GROUP_MIXED).asBoolean(); + + // group == "none" completely suppresses any grouping + if (!StringUtils::EqualsNoCase(group, "none")) + { + CQueryParams params; + CVideoDatabaseDirectory dir; + dir.GetQueryParams(items.GetPath(), params); + VIDEODATABASEDIRECTORY::NODE_TYPE nodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_strFilterPath); + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (items.GetContent() == "movies" && params.GetSetId() <= 0 && + nodeType == NODE_TYPE_TITLE_MOVIES && + (settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPMOVIESETS) || (StringUtils::EqualsNoCase(group, "sets") && mixed))) + { + CFileItemList groupedItems; + GroupAttribute groupAttributes = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPSINGLEITEMSETS) ? GroupAttributeNone : GroupAttributeIgnoreSingleItems; + if (GroupUtils::GroupAndMix(GroupBySet, m_strFilterPath, items, groupedItems, groupAttributes)) + { + items.ClearItems(); + items.Append(groupedItems); + } + } + } + + // reload thumbs after filtering and grouping + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + m_thumbLoader.Load(items); +} + +bool CGUIWindowVideoBase::CheckFilterAdvanced(CFileItemList &items) const +{ + const std::string& content = items.GetContent(); + if ((items.IsVideoDb() || CanContainFilter(m_strFilterPath)) && + (StringUtils::EqualsNoCase(content, "movies") || + StringUtils::EqualsNoCase(content, "tvshows") || + StringUtils::EqualsNoCase(content, "episodes") || + StringUtils::EqualsNoCase(content, "musicvideos"))) + return true; + + return false; +} + +bool CGUIWindowVideoBase::CanContainFilter(const std::string &strDirectory) const +{ + return URIUtils::IsProtocol(strDirectory, "videodb://"); +} + +/// \brief Search the current directory for a string got from the virtual keyboard +void CGUIWindowVideoBase::OnSearch() +{ + std::string strSearch; + if (!CGUIKeyboardFactory::ShowAndGetInput(strSearch, CVariant{g_localizeStrings.Get(16017)}, false)) + return ; + + StringUtils::ToLower(strSearch); + if (m_dlgProgress) + { + m_dlgProgress->SetHeading(CVariant{194}); + m_dlgProgress->SetLine(0, CVariant{strSearch}); + m_dlgProgress->SetLine(1, CVariant{""}); + m_dlgProgress->SetLine(2, CVariant{""}); + m_dlgProgress->Open(); + m_dlgProgress->Progress(); + } + CFileItemList items; + DoSearch(strSearch, items); + + if (m_dlgProgress) + m_dlgProgress->Close(); + + if (items.Size()) + { + CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT); + pDlgSelect->Reset(); + pDlgSelect->SetHeading(CVariant{283}); + + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr pItem = items[i]; + pDlgSelect->Add(pItem->GetLabel()); + } + + pDlgSelect->Open(); + + int iItem = pDlgSelect->GetSelectedItem(); + if (iItem < 0) + return; + + OnSearchItemFound(items[iItem].get()); + } + else + { + HELPERS::ShowOKDialogText(CVariant{194}, CVariant{284}); + } +} + +/// \brief React on the selected search item +/// \param pItem Search result item +void CGUIWindowVideoBase::OnSearchItemFound(const CFileItem* pSelItem) +{ + if (pSelItem->m_bIsFolder) + { + std::string strPath = pSelItem->GetPath(); + std::string strParentPath; + URIUtils::GetParentPath(strPath, strParentPath); + + Update(strParentPath); + + if (pSelItem->IsVideoDb() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + SetHistoryForPath(""); + else + SetHistoryForPath(strParentPath); + + strPath = pSelItem->GetPath(); + CURL url(strPath); + if (pSelItem->IsSmb() && !URIUtils::HasSlashAtEnd(strPath)) + strPath += "/"; + + for (int i = 0; i < m_vecItems->Size(); i++) + { + CFileItemPtr pItem = m_vecItems->Get(i); + if (pItem->GetPath() == strPath) + { + m_viewControl.SetSelectedItem(i); + break; + } + } + } + else + { + std::string strPath = URIUtils::GetDirectory(pSelItem->GetPath()); + + Update(strPath); + + if (pSelItem->IsVideoDb() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + SetHistoryForPath(""); + else + SetHistoryForPath(strPath); + + for (int i = 0; i < m_vecItems->Size(); i++) + { + CFileItemPtr pItem = m_vecItems->Get(i); + CURL url(pItem->GetPath()); + if (pSelItem->IsVideoDb()) + url.SetOptions(""); + if (url.Get() == pSelItem->GetPath()) + { + m_viewControl.SetSelectedItem(i); + break; + } + } + } + m_viewControl.SetFocused(); +} + +int CGUIWindowVideoBase::GetScraperForItem(CFileItem *item, ADDON::ScraperPtr &info, SScanSettings& settings) +{ + if (!item) + return 0; + + if (m_vecItems->IsPlugin() || m_vecItems->IsRSS()) + { + info.reset(); + return 0; + } + else if(m_vecItems->IsLiveTV()) + { + info.reset(); + return 0; + } + + bool foundDirectly = false; + info = m_database.GetScraperForPath(item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strPath.empty() ? item->GetVideoInfoTag()->m_strPath : item->GetPath(), settings, foundDirectly); + return foundDirectly ? 1 : 0; +} + +void CGUIWindowVideoBase::OnScan(const std::string& strPath, bool scanAll) +{ + CVideoLibraryQueue::GetInstance().ScanLibrary(strPath, scanAll, true); +} + +std::string CGUIWindowVideoBase::GetStartFolder(const std::string &dir) +{ + std::string lower(dir); StringUtils::ToLower(lower); + if (lower == "$playlists" || lower == "playlists") + return "special://videoplaylists/"; + else if (lower == "plugins" || lower == "addons") + return "addons://sources/video/"; + return CGUIMediaWindow::GetStartFolder(dir); +} + +void CGUIWindowVideoBase::AppendAndClearSearchItems(CFileItemList &searchItems, const std::string &prependLabel, CFileItemList &results) +{ + if (!searchItems.Size()) + return; + + searchItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + for (int i = 0; i < searchItems.Size(); i++) + searchItems[i]->SetLabel(prependLabel + searchItems[i]->GetLabel()); + results.Append(searchItems); + + searchItems.Clear(); +} + +bool CGUIWindowVideoBase::OnUnAssignContent(const std::string &path, int header, int text) +{ + bool bCanceled; + CVideoDatabase db; + db.Open(); + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{header}, CVariant{text}, bCanceled, CVariant{ "" }, CVariant{ "" }, CGUIDialogYesNo::NO_TIMEOUT)) + { + CGUIDialogProgress *progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + db.RemoveContentForPath(path, progress); + db.Close(); + CUtil::DeleteVideoDatabaseDirectoryCache(); + return true; + } + else + { + if (!bCanceled) + { + ADDON::ScraperPtr info; + SScanSettings settings; + settings.exclude = true; + db.SetScraperForPath(path,info,settings); + } + } + db.Close(); + + return false; +} + +void CGUIWindowVideoBase::OnAssignContent(const std::string &path) +{ + bool bScan=false; + CVideoDatabase db; + db.Open(); + + SScanSettings settings; + ADDON::ScraperPtr info = db.GetScraperForPath(path, settings); + + ADDON::ScraperPtr info2(info); + + if (CGUIDialogContentSettings::Show(info, settings)) + { + if(settings.exclude || (!info && info2)) + { + OnUnAssignContent(path, 20375, 20340); + } + else if (info != info2) + { + if (OnUnAssignContent(path, 20442, 20443)) + bScan = true; + } + db.SetScraperForPath(path, info, settings); + } + + if (bScan) + { + CVideoLibraryQueue::GetInstance().ScanLibrary(path, true, true); + } +} diff --git a/xbmc/video/windows/GUIWindowVideoBase.h b/xbmc/video/windows/GUIWindowVideoBase.h new file mode 100644 index 0000000..0c5acf4 --- /dev/null +++ b/xbmc/video/windows/GUIWindowVideoBase.h @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "playlists/PlayListTypes.h" +#include "video/VideoDatabase.h" +#include "video/VideoThumbLoader.h" +#include "windows/GUIMediaWindow.h" + +enum VideoSelectAction +{ + SELECT_ACTION_CHOOSE = 0, + SELECT_ACTION_PLAY_OR_RESUME, + SELECT_ACTION_RESUME, + SELECT_ACTION_INFO, + SELECT_ACTION_MORE, + SELECT_ACTION_PLAY, + SELECT_ACTION_PLAYPART, + SELECT_ACTION_QUEUE +}; + +class CGUIWindowVideoBase : public CGUIMediaWindow, public IBackgroundLoaderObserver +{ +public: + CGUIWindowVideoBase(int id, const std::string &xmlFile); + ~CGUIWindowVideoBase(void) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + + void PlayMovie(const CFileItem* item, const std::string& player = ""); + + /*! \brief Gets called to process the "info" action for the given file item + Default implementation shows a dialog containing information for the movie/episode/... + represented by the file item. + \param fileItem the item for which information is to be presented. + \param scraper a scraper addon instance that can be used to obtain additional information for + the given item + \return true if information was presented, false otherwise. + */ + virtual bool OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& scraper); + + /*! \brief Show the resume menu for this item (if it has a resume bookmark) + If a resume bookmark is found, we set the item's m_lStartOffset to STARTOFFSET_RESUME. + Note that we do this in favour of setting the resume point, as we need additional + information from the database (in particular, the playerState) when resuming some items + (eg ISO/VIDEO_TS). + \param item item to check for a resume bookmark + \return true if an option was chosen, false if the resume menu was cancelled. + */ + static bool ShowResumeMenu(CFileItem &item); + + /*! \brief Append a set of search items to a results list using a specific prepend label + Sorts the search items first, then appends with the given prependLabel to the results list. + Then empty the search item list so it can be refilled. + \param searchItems The search items to append. + \param prependLabel the label that should be prepended to all search results. + \param results the fileitemlist to append the search results to. + \sa DoSearch + */ + static void AppendAndClearSearchItems(CFileItemList &searchItems, const std::string &prependLabel, CFileItemList &results); + + /*! \brief Prompt the user for assigning content to a path. + Based on changes, we then call OnUnassignContent, update or refresh scraper information in the database + and optionally start a scan + \param path the path to assign content for + */ + static void OnAssignContent(const std::string &path); + + /*! \brief checks the database for a resume position and puts together a string + \param item selected item + \return string containing the resume position or an empty string if there is no resume position + */ + static std::string GetResumeString(const CFileItem &item); + + /*! \brief Load video information from the database for these items (public static version) + Useful for grabbing information for file listings, from watched status to full metadata + \param items the items to load information for. + \param database open database object to retrieve the data from + \param allowReplaceLabels allow label replacement if according GUI setting is enabled + */ + static void LoadVideoInfo(CFileItemList& items, + CVideoDatabase& database, + bool allowReplaceLabels = true); + +protected: + void OnScan(const std::string& strPath, bool scanAll = false); + bool Update(const std::string &strDirectory, bool updateFilterPath = true) override; + bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override; + void OnItemLoaded(CFileItem* pItem) override {}; + void GetGroupedItems(CFileItemList &items) override; + + bool CheckFilterAdvanced(CFileItemList &items) const override; + bool CanContainFilter(const std::string &strDirectory) const override; + + void GetContextButtons(int itemNumber, CContextButtons &buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + virtual void OnQueueItem(int iItem, bool first = false); + virtual void OnDeleteItem(const CFileItemPtr& pItem); + void OnDeleteItem(int iItem) override; + virtual void DoSearch(const std::string& strSearch, CFileItemList& items) {} + std::string GetStartFolder(const std::string &dir) override; + + bool OnClick(int iItem, const std::string &player = "") override; + bool OnSelect(int iItem) override; + /*! \brief react to an Info action on a view item + \param item the selected item + \return true if the action is performed, false otherwise + */ + bool OnItemInfo(int item); + /*! \brief perform a given action on a file + \param item the selected item + \param action the action to perform + \return true if the action is performed, false otherwise + */ + bool OnFileAction(int item, int action, const std::string& player); + + void OnRestartItem(int iItem, const std::string &player = ""); + bool OnResumeItem(int iItem, const std::string &player = ""); + void PlayItem(int iItem, const std::string &player = ""); + bool OnPlayMedia(int iItem, const std::string &player = "") override; + bool OnPlayAndQueueMedia(const CFileItemPtr& item, const std::string& player = "") override; + using CGUIMediaWindow::LoadPlayList; + void LoadPlayList(const std::string& strPlayList, PLAYLIST::Id playlistId = PLAYLIST::TYPE_VIDEO); + + bool ShowIMDB(CFileItemPtr item, const ADDON::ScraperPtr& content, bool fromDB); + + void OnSearch(); + void OnSearchItemFound(const CFileItem* pSelItem); + int GetScraperForItem(CFileItem *item, ADDON::ScraperPtr &info, VIDEO::SScanSettings& settings); + + static bool OnUnAssignContent(const std::string &path, int header, int text); + + static bool StackingAvailable(const CFileItemList &items); + + bool OnPlayStackPart(int item); + + CGUIDialogProgress* m_dlgProgress; + CVideoDatabase m_database; + + CVideoThumbLoader m_thumbLoader; + bool m_stackingAvailable; +}; diff --git a/xbmc/video/windows/GUIWindowVideoNav.cpp b/xbmc/video/windows/GUIWindowVideoNav.cpp new file mode 100644 index 0000000..d53c105 --- /dev/null +++ b/xbmc/video/windows/GUIWindowVideoNav.cpp @@ -0,0 +1,1171 @@ +/* + * Copyright (C) 2016-2018 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 "GUIWindowVideoNav.h" + +#include "FileItem.h" +#include "GUIPassword.h" +#include "PartyModeManager.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "dialogs/GUIDialogMediaSource.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/Directory.h" +#include "filesystem/VideoDatabaseDirectory.h" +#include "filesystem/VideoDatabaseFile.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/Key.h" +#include "messaging/ApplicationMessenger.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "music/MusicDatabase.h" +#include "profiles/ProfileManager.h" +#include "pvr/recordings/PVRRecording.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDbUrl.h" +#include "video/VideoInfoScanner.h" +#include "video/VideoLibraryQueue.h" +#include "video/dialogs/GUIDialogVideoInfo.h" +#include "view/GUIViewState.h" + +#include <utility> + +using namespace XFILE; +using namespace VIDEODATABASEDIRECTORY; +using namespace KODI::MESSAGING; + +#define CONTROL_BTNVIEWASICONS 2 +#define CONTROL_BTNSORTBY 3 +#define CONTROL_BTNSORTASC 4 +#define CONTROL_BTNSEARCH 8 +#define CONTROL_LABELFILES 12 + +#define CONTROL_BTN_FILTER 19 +#define CONTROL_BTNSHOWMODE 10 +#define CONTROL_BTNSHOWALL 14 +#define CONTROL_UNLOCK 11 + +#define CONTROL_FILTER 15 +#define CONTROL_BTNPARTYMODE 16 +#define CONTROL_LABELEMPTY 18 + +#define CONTROL_UPDATE_LIBRARY 20 + +CGUIWindowVideoNav::CGUIWindowVideoNav(void) + : CGUIWindowVideoBase(WINDOW_VIDEO_NAV, "MyVideoNav.xml") +{ + m_thumbLoader.SetObserver(this); +} + +CGUIWindowVideoNav::~CGUIWindowVideoNav(void) = default; + +bool CGUIWindowVideoNav::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_TOGGLE_WATCHED) + { + CFileItemPtr pItem = m_vecItems->Get(m_viewControl.GetSelectedItem()); + if (pItem->IsParentFolder()) + return false; + + if (pItem && pItem->HasVideoInfoTag()) + { + CVideoLibraryQueue::GetInstance().MarkAsWatched(pItem, pItem->GetVideoInfoTag()->GetPlayCount() == 0); + return true; + } + } + return CGUIWindowVideoBase::OnAction(action); +} + +bool CGUIWindowVideoNav::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage()) + { + case GUI_MSG_WINDOW_RESET: + m_vecItems->SetPath(""); + break; + case GUI_MSG_WINDOW_DEINIT: + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + break; + case GUI_MSG_WINDOW_INIT: + { + /* We don't want to show Autosourced items (ie removable pendrives, memorycards) in Library mode */ + m_rootDir.AllowNonLocalSources(false); + + SetProperty("flattened", CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)); + if (message.GetNumStringParams() && StringUtils::EqualsNoCase(message.GetStringParam(0), "Files") && + CMediaSourceSettings::GetInstance().GetSources("video")->empty()) + { + message.SetStringParam(""); + } + + if (!CGUIWindowVideoBase::OnMessage(message)) + return false; + + + if (message.GetStringParam(0) != "") + { + CURL url(message.GetStringParam(0)); + + int i = 0; + for (; i < m_vecItems->Size(); i++) + { + CFileItemPtr pItem = m_vecItems->Get(i); + + // skip ".." + if (pItem->IsParentFolder()) + continue; + + if (URIUtils::PathEquals(pItem->GetPath(), message.GetStringParam(0), true, true)) + { + m_viewControl.SetSelectedItem(i); + i = -1; + if (url.GetOption("showinfo") == "true") + { + ADDON::ScraperPtr scrapper; + OnItemInfo(*pItem, scrapper); + } + break; + } + } + if (i >= m_vecItems->Size()) + { + SelectFirstUnwatched(); + + if (url.GetOption("showinfo") == "true") + { + // We are here if the item is filtered out in the nav window + const std::string& path = message.GetStringParam(0); + CFileItem item(path, URIUtils::HasSlashAtEnd(path)); + if (item.IsVideoDb()) + { + *(item.GetVideoInfoTag()) = XFILE::CVideoDatabaseFile::GetVideoTag(CURL(item.GetPath())); + if (!item.GetVideoInfoTag()->IsEmpty()) + { + item.SetPath(item.GetVideoInfoTag()->m_strFileNameAndPath); + ADDON::ScraperPtr scrapper; + OnItemInfo(item, scrapper); + } + } + } + } + } + else + { + // This needs to be done again, because the initialization of CGUIWindow overwrites it with default values + // Mostly affects cases where GUIWindowVideoNav is constructed and we're already in a show, e.g. entering from the homescreen + SelectFirstUnwatched(); + } + + return true; + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTNPARTYMODE) + { + if (g_partyModeManager.IsEnabled()) + g_partyModeManager.Disable(); + else + { + if (!g_partyModeManager.Enable(PARTYMODECONTEXT_VIDEO)) + { + SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE,false); + return false; + } + + // Playlist directory is the root of the playlist window + if (m_guiState) + m_guiState->SetPlaylistDirectory("playlistvideo://"); + + return true; + } + UpdateButtons(); + } + + if (iControl == CONTROL_BTNSEARCH) + { + OnSearch(); + } + else if (iControl == CONTROL_BTNSHOWMODE) + { + CMediaSettings::GetInstance().CycleWatchedMode(m_vecItems->GetContent()); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + OnFilterItems(GetProperty("filter").asString()); + UpdateButtons(); + return true; + } + else if (iControl == CONTROL_BTNSHOWALL) + { + if (CMediaSettings::GetInstance().GetWatchedMode(m_vecItems->GetContent()) == WatchedModeAll) + CMediaSettings::GetInstance().SetWatchedMode(m_vecItems->GetContent(), WatchedModeUnwatched); + else + CMediaSettings::GetInstance().SetWatchedMode(m_vecItems->GetContent(), WatchedModeAll); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + OnFilterItems(GetProperty("filter").asString()); + UpdateButtons(); + return true; + } + else if (iControl == CONTROL_UPDATE_LIBRARY) + { + if (!CVideoLibraryQueue::GetInstance().IsScanningLibrary()) + OnScan(""); + else + CVideoLibraryQueue::GetInstance().StopLibraryScanning(); + return true; + } + } + break; + // update the display + case GUI_MSG_REFRESH_THUMBS: + Refresh(); + break; + } + return CGUIWindowVideoBase::OnMessage(message); +} + +SelectFirstUnwatchedItem CGUIWindowVideoNav::GetSettingSelectFirstUnwatchedItem() +{ + if (m_vecItems->IsVideoDb()) + { + NODE_TYPE nodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_vecItems->GetPath()); + + if (nodeType == NODE_TYPE_SEASONS || nodeType == NODE_TYPE_EPISODES) + { + int iValue = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOLIBRARY_TVSHOWSSELECTFIRSTUNWATCHEDITEM); + if (iValue >= SelectFirstUnwatchedItem::NEVER && iValue <= SelectFirstUnwatchedItem::ALWAYS) + return (SelectFirstUnwatchedItem)iValue; + } + } + + return SelectFirstUnwatchedItem::NEVER; +} + +IncludeAllSeasonsAndSpecials CGUIWindowVideoNav::GetSettingIncludeAllSeasonsAndSpecials() +{ + int iValue = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOLIBRARY_TVSHOWSINCLUDEALLSEASONSANDSPECIALS); + if (iValue >= IncludeAllSeasonsAndSpecials::NEITHER && iValue <= IncludeAllSeasonsAndSpecials::SPECIALS) + return (IncludeAllSeasonsAndSpecials)iValue; + + return IncludeAllSeasonsAndSpecials::NEITHER; +} + +int CGUIWindowVideoNav::GetFirstUnwatchedItemIndex(bool includeAllSeasons, bool includeSpecials) +{ + int iIndex = 0; + int iUnwatchedSeason = INT_MAX; + int iUnwatchedEpisode = INT_MAX; + NODE_TYPE nodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_vecItems->GetPath()); + + // Run through the list of items and find the first unwatched season/episode + for (int i = 0; i < m_vecItems->Size(); ++i) + { + CFileItemPtr pItem = m_vecItems->Get(i); + if (pItem->IsParentFolder() || !pItem->HasVideoInfoTag()) + continue; + + CVideoInfoTag *pTag = pItem->GetVideoInfoTag(); + + if ((!includeAllSeasons && pTag->m_iSeason < 0) || (!includeSpecials && pTag->m_iSeason == 0)) + continue; + + // Use the special sort values if they're available + int iSeason = pTag->m_iSpecialSortSeason >= 0 ? pTag->m_iSpecialSortSeason : pTag->m_iSeason; + int iEpisode = pTag->m_iSpecialSortEpisode >= 0 ? pTag->m_iSpecialSortEpisode : pTag->m_iEpisode; + + if (nodeType == NODE_TYPE::NODE_TYPE_SEASONS) + { + // Is the season unwatched, and is its season number lower than the currently identified + // first unwatched season + if (pTag->GetPlayCount() == 0 && iSeason < iUnwatchedSeason) + { + iUnwatchedSeason = iSeason; + iIndex = i; + } + } + + if (nodeType == NODE_TYPE::NODE_TYPE_EPISODES) + { + // Is the episode unwatched, and is its season number lower + // or is its episode number lower within the current season + if (pTag->GetPlayCount() == 0 && (iSeason < iUnwatchedSeason || (iSeason == iUnwatchedSeason && iEpisode < iUnwatchedEpisode))) + { + iUnwatchedSeason = iSeason; + iUnwatchedEpisode = iEpisode; + iIndex = i; + } + } + } + + return iIndex; +} + +bool CGUIWindowVideoNav::Update(const std::string &strDirectory, bool updateFilterPath /* = true */) +{ + if (!CGUIWindowVideoBase::Update(strDirectory, updateFilterPath)) + return false; + + SelectFirstUnwatched(); + + return true; +} + +void CGUIWindowVideoNav::SelectFirstUnwatched() { + // Check if we should select the first unwatched item + SelectFirstUnwatchedItem selectFirstUnwatched = GetSettingSelectFirstUnwatchedItem(); + if (selectFirstUnwatched != SelectFirstUnwatchedItem::NEVER) + { + bool bIsItemSelected = (m_viewControl.GetSelectedItem() > 0); + + if (selectFirstUnwatched == SelectFirstUnwatchedItem::ALWAYS || + (selectFirstUnwatched == SelectFirstUnwatchedItem::ON_FIRST_ENTRY && !bIsItemSelected)) + { + IncludeAllSeasonsAndSpecials incAllSeasonsSpecials = GetSettingIncludeAllSeasonsAndSpecials(); + + bool bIncludeAllSeasons = (incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::BOTH || incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::ALL_SEASONS); + bool bIncludeSpecials = (incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::BOTH || incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::SPECIALS); + + int iIndex = GetFirstUnwatchedItemIndex(bIncludeAllSeasons, bIncludeSpecials); + m_viewControl.SetSelectedItem(iIndex); + } + } +} + +bool CGUIWindowVideoNav::GetDirectory(const std::string &strDirectory, CFileItemList &items) +{ + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + items.ClearArt(); + items.ClearProperties(); + + bool bResult = CGUIWindowVideoBase::GetDirectory(strDirectory, items); + if (bResult) + { + if (items.IsVideoDb()) + { + XFILE::CVideoDatabaseDirectory dir; + CQueryParams params; + dir.GetQueryParams(items.GetPath(),params); + VIDEODATABASEDIRECTORY::NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath()); + + int iFlatten = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOLIBRARY_FLATTENTVSHOWS); + int itemsSize = items.GetObjectCount(); + int firstIndex = items.Size() - itemsSize; + + // perform the flattening logic for tvshows with a single (unwatched) season (+ optional special season) + if (node == NODE_TYPE_SEASONS && !items.IsEmpty()) + { + // check if the last item is the "All seasons" item which should be ignored for flattening + if (!items[items.Size() - 1]->HasVideoInfoTag() || items[items.Size() - 1]->GetVideoInfoTag()->m_iSeason < 0) + itemsSize -= 1; + + bool bFlatten = (itemsSize == 1 && iFlatten == 1) || iFlatten == 2 || // flatten if one one season or if always flatten is enabled + (itemsSize == 2 && iFlatten == 1 && // flatten if one season + specials + (items[firstIndex]->GetVideoInfoTag()->m_iSeason == 0 || items[firstIndex + 1]->GetVideoInfoTag()->m_iSeason == 0)); + + if (iFlatten > 0 && !bFlatten && (WatchedMode)CMediaSettings::GetInstance().GetWatchedMode("tvshows") == WatchedModeUnwatched) + { + int count = 0; + for(int i = 0; i < items.Size(); i++) + { + const CFileItemPtr item = items.Get(i); + if (item->GetProperty("unwatchedepisodes").asInteger() != 0 && item->GetVideoInfoTag()->m_iSeason > 0) + count++; + } + bFlatten = (count < 2); // flatten if there is only 1 unwatched season (not counting specials) + } + + if (bFlatten) + { // flatten if one season or flatten always + items.Clear(); + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(items.GetPath())) + return false; + + videoUrl.AppendPath("-2/"); + return GetDirectory(videoUrl.ToString(), items); + } + } + + if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES || + node == NODE_TYPE_SEASONS || + node == NODE_TYPE_RECENTLY_ADDED_EPISODES) + { + CLog::Log(LOGDEBUG, "WindowVideoNav::GetDirectory"); + // grab the show thumb + CVideoInfoTag details; + m_database.GetTvShowInfo("", details, params.GetTvShowId()); + std::map<std::string, std::string> art; + if (m_database.GetArtForItem(details.m_iDbId, details.m_type, art)) + { + items.AppendArt(art, details.m_type); + items.SetArtFallback("fanart", "tvshow.fanart"); + if (node == NODE_TYPE_SEASONS) + { // set an art fallback for "thumb" + if (items.HasArt("tvshow.poster")) + items.SetArtFallback("thumb", "tvshow.poster"); + else if (items.HasArt("tvshow.banner")) + items.SetArtFallback("thumb", "tvshow.banner"); + } + } + + // Grab fanart data + items.SetProperty("fanart_color1", details.m_fanart.GetColor(0)); + items.SetProperty("fanart_color2", details.m_fanart.GetColor(1)); + items.SetProperty("fanart_color3", details.m_fanart.GetColor(2)); + + // save the show description (showplot) + items.SetProperty("showplot", details.m_strPlot); + items.SetProperty("showtitle", details.m_strShowTitle); + + // the container folder thumb is the parent (i.e. season or show) + if (itemsSize && (node == NODE_TYPE_EPISODES || node == NODE_TYPE_RECENTLY_ADDED_EPISODES)) + { + int seasonID = -1; + int seasonParam = params.GetSeason(); + + // grab all season art when flatten always + if (seasonParam == -2 && iFlatten == 2) + seasonParam = -1; + + if (seasonParam >= -1) + seasonID = m_database.GetSeasonId(details.m_iDbId, seasonParam); + else + seasonID = items[firstIndex]->GetVideoInfoTag()->m_iIdSeason; + + CGUIListItem::ArtMap seasonArt; + if (seasonID > -1 && m_database.GetArtForItem(seasonID, MediaTypeSeason, seasonArt)) + { + items.AppendArt(seasonArt, MediaTypeSeason); + // set an art fallback for "thumb" + if (items.HasArt("season.poster")) + items.SetArtFallback("thumb", "season.poster"); + else if (items.HasArt("season.banner")) + items.SetArtFallback("thumb", "season.banner"); + } + } + } + else if (node == NODE_TYPE_TITLE_MOVIES || + node == NODE_TYPE_RECENTLY_ADDED_MOVIES) + { + if (params.GetSetId() > 0) + { + CGUIListItem::ArtMap setArt; + if (m_database.GetArtForItem(params.GetSetId(), MediaTypeVideoCollection, setArt)) + { + items.AppendArt(setArt, MediaTypeVideoCollection); + items.SetArtFallback("fanart", "set.fanart"); + if (items.HasArt("set.poster")) + items.SetArtFallback("thumb", "set.poster"); + } + } + } + } + else if (URIUtils::PathEquals(items.GetPath(), "special://videoplaylists/")) + items.SetContent("playlists"); + else if (!items.IsVirtualDirectoryRoot()) + { // load info from the database + std::string label; + if (items.GetLabel().empty() && m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("video"), &label)) + items.SetLabel(label); + if (!items.IsSourcesPath() && !items.IsLibraryFolder()) + LoadVideoInfo(items, m_database); + } + + CVideoDbUrl videoUrl; + if (videoUrl.FromString(items.GetPath()) && items.GetContent() == "tags" && + !items.Contains("newtag://" + videoUrl.GetType())) + { + CFileItemPtr newTag(new CFileItem("newtag://" + videoUrl.GetType(), false)); + newTag->SetLabel(g_localizeStrings.Get(20462)); + newTag->SetLabelPreformatted(true); + newTag->SetSpecialSort(SortSpecialOnTop); + items.Add(newTag); + } + } + return bResult; +} + +void CGUIWindowVideoNav::UpdateButtons() +{ + CGUIWindowVideoBase::UpdateButtons(); + + // Update object count + int iItems = m_vecItems->Size(); + if (iItems) + { + // check for parent dir and "all" items + // should always be the first two items + for (int i = 0; i <= (iItems>=2 ? 1 : 0); i++) + { + CFileItemPtr pItem = m_vecItems->Get(i); + if (pItem->IsParentFolder()) iItems--; + if (StringUtils::StartsWith(pItem->GetPath(), "/-1/")) iItems--; + } + // or the last item + if (m_vecItems->Size() > 2 && + StringUtils::StartsWith(m_vecItems->Get(m_vecItems->Size()-1)->GetPath(), "/-1/")) + iItems--; + } + std::string items = StringUtils::Format("{} {}", iItems, g_localizeStrings.Get(127)); + SET_CONTROL_LABEL(CONTROL_LABELFILES, items); + + // set the filter label + std::string strLabel; + + // "Playlists" + if (m_vecItems->IsPath("special://videoplaylists/")) + strLabel = g_localizeStrings.Get(136); + // "{Playlist Name}" + else if (m_vecItems->IsPlayList()) + { + // get playlist name from path + std::string strDummy; + URIUtils::Split(m_vecItems->GetPath(), strDummy, strLabel); + } + else if (m_vecItems->IsPath("sources://video/")) + strLabel = g_localizeStrings.Get(744); + // everything else is from a videodb:// path + else if (m_vecItems->IsVideoDb()) + { + CVideoDatabaseDirectory dir; + dir.GetLabel(m_vecItems->GetPath(), strLabel); + } + else + strLabel = URIUtils::GetFileName(m_vecItems->GetPath()); + + SET_CONTROL_LABEL(CONTROL_FILTER, strLabel); + + int watchMode = CMediaSettings::GetInstance().GetWatchedMode(m_vecItems->GetContent()); + SET_CONTROL_LABEL(CONTROL_BTNSHOWMODE, g_localizeStrings.Get(16100 + watchMode)); + + SET_CONTROL_SELECTED(GetID(), CONTROL_BTNSHOWALL, watchMode != WatchedModeAll); + + SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled()); + + CONTROL_ENABLE_ON_CONDITION(CONTROL_UPDATE_LIBRARY, !m_vecItems->IsAddonsPath() && !m_vecItems->IsPlugin() && !m_vecItems->IsScript()); +} + +bool CGUIWindowVideoNav::GetFilteredItems(const std::string &filter, CFileItemList &items) +{ + bool listchanged = CGUIMediaWindow::GetFilteredItems(filter, items); + listchanged |= ApplyWatchedFilter(items); + + return listchanged; +} + +/// \brief Search for names, genres, artists, directors, and plots with search string \e strSearch in the +/// \brief video databases and return the found \e items +/// \param strSearch The search string +/// \param items Items Found +void CGUIWindowVideoNav::DoSearch(const std::string& strSearch, CFileItemList& items) +{ + CFileItemList tempItems; + const std::string& strGenre = g_localizeStrings.Get(515); // Genre + const std::string& strActor = g_localizeStrings.Get(20337); // Actor + const std::string& strDirector = g_localizeStrings.Get(20339); // Director + + //get matching names + m_database.GetMoviesByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20338) + "] ", items); + + m_database.GetEpisodesByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20359) + "] ", items); + + m_database.GetTvShowsByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20364) + "] ", items); + + m_database.GetMusicVideosByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20391) + "] ", items); + + m_database.GetMusicVideosByAlbum(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(558) + "] ", items); + + // get matching genres + m_database.GetMovieGenresByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strGenre + " - " + g_localizeStrings.Get(20342) + "] ", items); + + m_database.GetTvShowGenresByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strGenre + " - " + g_localizeStrings.Get(20343) + "] ", items); + + m_database.GetMusicVideoGenresByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strGenre + " - " + g_localizeStrings.Get(20389) + "] ", items); + + //get actors/artists + m_database.GetMovieActorsByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strActor + " - " + g_localizeStrings.Get(20342) + "] ", items); + + m_database.GetTvShowsActorsByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strActor + " - " + g_localizeStrings.Get(20343) + "] ", items); + + m_database.GetMusicVideoArtistsByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strActor + " - " + g_localizeStrings.Get(20389) + "] ", items); + + //directors + m_database.GetMovieDirectorsByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strDirector + " - " + g_localizeStrings.Get(20342) + "] ", items); + + m_database.GetTvShowsDirectorsByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strDirector + " - " + g_localizeStrings.Get(20343) + "] ", items); + + m_database.GetMusicVideoDirectorsByName(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + strDirector + " - " + g_localizeStrings.Get(20389) + "] ", items); + + //plot + m_database.GetEpisodesByPlot(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20365) + "] ", items); + + m_database.GetMoviesByPlot(strSearch, tempItems); + AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20323) + "] ", items); +} + +void CGUIWindowVideoNav::PlayItem(int iItem) +{ + // unlike additemtoplaylist, we need to check the items here + // before calling it since the current playlist will be stopped + // and cleared! + + // root is not allowed + if (m_vecItems->IsVirtualDirectoryRoot()) + return; + + CGUIWindowVideoBase::PlayItem(iItem); +} + +bool CGUIWindowVideoNav::OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& scraper) +{ + if (!scraper || scraper->Content() == CONTENT_NONE) + { + m_database.Open(); // since we can be called from the music library without being inited + if (fileItem.IsVideoDb()) + scraper = m_database.GetScraperForPath(fileItem.GetVideoInfoTag()->m_strPath); + else + { + std::string strPath,strFile; + URIUtils::Split(fileItem.GetPath(),strPath,strFile); + scraper = m_database.GetScraperForPath(strPath); + } + m_database.Close(); + } + return CGUIWindowVideoBase::OnItemInfo(fileItem, scraper); +} + +void CGUIWindowVideoNav::OnDeleteItem(const CFileItemPtr& pItem) +{ + if (m_vecItems->IsParentFolder()) + return; + + if (!m_vecItems->IsVideoDb() && !pItem->IsVideoDb()) + { + if (!pItem->IsPath("newsmartplaylist://video") && + !pItem->IsPath("special://videoplaylists/") && + !pItem->IsPath("sources://video/") && + !URIUtils::IsProtocol(pItem->GetPath(), "newtag")) + CGUIWindowVideoBase::OnDeleteItem(pItem); + } + else if (StringUtils::StartsWithNoCase(pItem->GetPath(), "videodb://movies/sets/") && + pItem->GetPath().size() > 22 && pItem->m_bIsFolder) + { + CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO); + + if (!pDialog) + return; + + pDialog->SetHeading(CVariant{432}); + std::string strLabel = StringUtils::Format(g_localizeStrings.Get(433), pItem->GetLabel()); + pDialog->SetLine(1, CVariant{std::move(strLabel)}); + pDialog->SetLine(2, CVariant{""}); + pDialog->Open(); + if (pDialog->IsConfirmed()) + { + CFileItemList items; + CDirectory::GetDirectory(pItem->GetPath(),items,"",DIR_FLAG_NO_FILE_DIRS); + for (int i=0;i<items.Size();++i) + OnDeleteItem(items[i]); + + CVideoDatabaseDirectory dir; + CQueryParams params; + dir.GetQueryParams(pItem->GetPath(),params); + m_database.DeleteSet(params.GetSetId()); + } + } + else if (m_vecItems->IsPath(CUtil::VideoPlaylistsLocation()) || + m_vecItems->IsPath("special://videoplaylists/")) + { + pItem->m_bIsFolder = false; + CGUIComponent *gui = CServiceBroker::GetGUI(); + if (gui && gui->ConfirmDelete(pItem->GetPath())) + CFileUtils::DeleteItem(pItem); + } + else + { + if (!CGUIDialogVideoInfo::DeleteVideoItem(pItem)) + return; + } + int itemNumber = m_viewControl.GetSelectedItem(); + int select = itemNumber >= m_vecItems->Size()-1 ? itemNumber-1 : itemNumber; + m_viewControl.SetSelectedItem(select); + + CUtil::DeleteVideoDatabaseDirectoryCache(); +} + +void CGUIWindowVideoNav::GetContextButtons(int itemNumber, CContextButtons &buttons) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + + CGUIWindowVideoBase::GetContextButtons(itemNumber, buttons); + + CVideoDatabaseDirectory dir; + NODE_TYPE node = dir.GetDirectoryChildType(m_vecItems->GetPath()); + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (!item) + { + // nothing to do here + } + else if (m_vecItems->IsPath("sources://video/")) + { + // get the usual shares + CGUIDialogContextMenu::GetContextButtons("video", item, buttons); + if (!item->IsDVD() && item->GetPath() != "add" && !item->IsParentFolder() && + (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser)) + { + CVideoDatabase database; + database.Open(); + ADDON::ScraperPtr info = database.GetScraperForPath(item->GetPath()); + + if (!item->IsLiveTV() && !item->IsAddonsPath() && !URIUtils::IsUPnP(item->GetPath())) + { + if (info && info->Content() != CONTENT_NONE) + { + buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20442); + buttons.Add(CONTEXT_BUTTON_SCAN, 13349); + } + else + buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20333); + } + } + } + else + { + // are we in the playlists location? + bool inPlaylists = m_vecItems->IsPath(CUtil::VideoPlaylistsLocation()) || + m_vecItems->IsPath("special://videoplaylists/"); + + if (item->HasVideoInfoTag() && item->HasProperty("artist_musicid")) + buttons.Add(CONTEXT_BUTTON_GO_TO_ARTIST, 20396); + + if (item->HasVideoInfoTag() && item->HasProperty("album_musicid")) + buttons.Add(CONTEXT_BUTTON_GO_TO_ALBUM, 20397); + + if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strAlbum.empty() && + !item->GetVideoInfoTag()->m_artist.empty() && + !item->GetVideoInfoTag()->m_strTitle.empty()) + { + CMusicDatabase database; + database.Open(); + if (database.GetSongByArtistAndAlbumAndTitle(StringUtils::Join(item->GetVideoInfoTag()->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator), + item->GetVideoInfoTag()->m_strAlbum, + item->GetVideoInfoTag()->m_strTitle) > -1) + { + buttons.Add(CONTEXT_BUTTON_PLAY_OTHER, 20398); + } + } + if (!item->IsParentFolder()) + { + ADDON::ScraperPtr info; + VIDEO::SScanSettings settings; + GetScraperForItem(item.get(), info, settings); + + // can we update the database? + if (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) + { + if (!CVideoLibraryQueue::GetInstance().IsScanningLibrary() && item->IsVideoDb() && + item->HasVideoInfoTag() && + (item->GetVideoInfoTag()->m_type == MediaTypeMovie || // movies + item->GetVideoInfoTag()->m_type == MediaTypeTvShow || // tvshows + item->GetVideoInfoTag()->m_type == MediaTypeSeason || // seasons + item->GetVideoInfoTag()->m_type == MediaTypeEpisode || // episodes + item->GetVideoInfoTag()->m_type == MediaTypeMusicVideo || // musicvideos + item->GetVideoInfoTag()->m_type == "tag" || // tags + item->GetVideoInfoTag()->m_type == MediaTypeVideoCollection)) // sets + { + buttons.Add(CONTEXT_BUTTON_EDIT, 16106); + } + if (node == NODE_TYPE_TITLE_TVSHOWS) + { + buttons.Add(CONTEXT_BUTTON_SCAN, 13349); + } + + if (node == NODE_TYPE_ACTOR && !dir.IsAllItem(item->GetPath()) && item->m_bIsFolder) + { + if (StringUtils::StartsWithNoCase(m_vecItems->GetPath(), "videodb://musicvideos")) // mvids + buttons.Add(CONTEXT_BUTTON_SET_ARTIST_THUMB, 13359); + else + buttons.Add(CONTEXT_BUTTON_SET_ACTOR_THUMB, 20403); + } + } + + if (!m_vecItems->IsVideoDb() && !m_vecItems->IsVirtualDirectoryRoot()) + { // non-video db items, file operations are allowed + if ((CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) && + CUtil::SupportsWriteFileOperations(item->GetPath())) || + (inPlaylists && URIUtils::GetFileName(item->GetPath()) != "PartyMode-Video.xsp" + && (item->IsPlayList() || item->IsSmartPlayList()))) + { + buttons.Add(CONTEXT_BUTTON_DELETE, 117); + buttons.Add(CONTEXT_BUTTON_RENAME, 118); + } + // add "Set/Change content" to folders + if (item->m_bIsFolder && !item->IsVideoDb() && !item->IsPlayList() && !item->IsSmartPlayList() && !item->IsLibraryFolder() && !item->IsLiveTV() && !item->IsPlugin() && !item->IsAddonsPath() && !URIUtils::IsUPnP(item->GetPath())) + { + if (info && info->Content() != CONTENT_NONE) + buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20442); + else + buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20333); + + if (info && info->Content() != CONTENT_NONE) + buttons.Add(CONTEXT_BUTTON_SCAN, 13349); + } + } + + if ((!item->HasVideoInfoTag() || item->GetVideoInfoTag()->m_iDbId == -1) && info && info->Content() != CONTENT_NONE) + buttons.Add(CONTEXT_BUTTON_SCAN_TO_LIBRARY, 21845); + + } + } +} + +bool CGUIWindowVideoNav::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + if (CGUIDialogContextMenu::OnContextButton("video", item, button)) + { + if (button == CONTEXT_BUTTON_REMOVE_SOURCE && !item->IsLiveTV() + &&!item->IsRSS() && !URIUtils::IsUPnP(item->GetPath())) + { + // if the source has been properly removed, remove the cached source list because the list has changed + if (OnUnAssignContent(item->GetPath(), 20375, 20340)) + m_vecItems->RemoveDiscCache(GetID()); + } + Refresh(); + return true; + } + switch (button) + { + case CONTEXT_BUTTON_EDIT: + { + CONTEXT_BUTTON ret = (CONTEXT_BUTTON)CGUIDialogVideoInfo::ManageVideoItem(item); + if (ret != CONTEXT_BUTTON_CANCELLED) + { + Refresh(true); + if (ret == CONTEXT_BUTTON_DELETE) + { + int select = itemNumber >= m_vecItems->Size()-1 ? itemNumber-1:itemNumber; + m_viewControl.SetSelectedItem(select); + } + } + return true; + } + + case CONTEXT_BUTTON_SET_ACTOR_THUMB: + case CONTEXT_BUTTON_SET_ARTIST_THUMB: + { + std::string type = MediaTypeSeason; + if (button == CONTEXT_BUTTON_SET_ACTOR_THUMB) + type = "actor"; + else if (button == CONTEXT_BUTTON_SET_ARTIST_THUMB) + type = MediaTypeArtist; + + bool result = CGUIDialogVideoInfo::ManageVideoItemArtwork(m_vecItems->Get(itemNumber), type); + Refresh(); + + return result; + } + case CONTEXT_BUTTON_GO_TO_ARTIST: + { + std::string strPath; + strPath = StringUtils::Format("musicdb://artists/{}/", + item->GetProperty("artist_musicid").asInteger()); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_NAV, strPath); + return true; + } + case CONTEXT_BUTTON_GO_TO_ALBUM: + { + std::string strPath; + strPath = StringUtils::Format("musicdb://albums/{}/", + item->GetProperty("album_musicid").asInteger()); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_NAV, strPath); + return true; + } + case CONTEXT_BUTTON_PLAY_OTHER: + { + CMusicDatabase database; + database.Open(); + CSong song; + if (database.GetSong(database.GetSongByArtistAndAlbumAndTitle(StringUtils::Join(m_vecItems->Get(itemNumber)->GetVideoInfoTag()->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator),m_vecItems->Get(itemNumber)->GetVideoInfoTag()->m_strAlbum, + m_vecItems->Get(itemNumber)->GetVideoInfoTag()->m_strTitle), + song)) + { + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, + static_cast<void*>(new CFileItem(song))); + } + return true; + } + case CONTEXT_BUTTON_SCAN_TO_LIBRARY: + CGUIDialogVideoInfo::ShowFor(*item); + return true; + + default: + break; + + } + return CGUIWindowVideoBase::OnContextButton(itemNumber, button); +} + +bool CGUIWindowVideoNav::OnAddMediaSource() +{ + return CGUIDialogMediaSource::ShowAndAddMediaSource("video"); +} + +bool CGUIWindowVideoNav::OnClick(int iItem, const std::string &player) +{ + CFileItemPtr item = m_vecItems->Get(iItem); + if (!item->m_bIsFolder && item->IsVideoDb() && !item->Exists()) + { + CLog::Log(LOGDEBUG, "{} called on '{}' but file doesn't exist", __FUNCTION__, item->GetPath()); + + const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager(); + + if (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) + { + if (!CGUIDialogVideoInfo::DeleteVideoItemFromDatabase(item, true)) + return true; + + // update list + Refresh(true); + m_viewControl.SetSelectedItem(iItem); + return true; + } + else + { + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{662}); + return true; + } + } + else if (StringUtils::StartsWithNoCase(item->GetPath(), "newtag://")) + { + // dont allow update while scanning + if (CVideoLibraryQueue::GetInstance().IsScanningLibrary()) + { + HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057}); + return true; + } + + //Get the new title + std::string strTag; + if (!CGUIKeyboardFactory::ShowAndGetInput(strTag, CVariant{g_localizeStrings.Get(20462)}, false)) + return true; + + CVideoDatabase videodb; + if (!videodb.Open()) + return true; + + // get the media type and convert from plural to singular (by removing the trailing "s") + std::string mediaType = item->GetPath().substr(9); + mediaType = mediaType.substr(0, mediaType.size() - 1); + std::string localizedType = CGUIDialogVideoInfo::GetLocalizedVideoType(mediaType); + if (localizedType.empty()) + return true; + + if (!videodb.GetSingleValue("tag", "tag.tag_id", videodb.PrepareSQL("tag.name = '%s' AND tag.tag_id IN (SELECT tag_link.tag_id FROM tag_link WHERE tag_link.media_type = '%s')", strTag.c_str(), mediaType.c_str())).empty()) + { + std::string strError = StringUtils::Format(g_localizeStrings.Get(20463), strTag); + HELPERS::ShowOKDialogText(CVariant{20462}, CVariant{std::move(strError)}); + return true; + } + + int idTag = videodb.AddTag(strTag); + CFileItemList items; + std::string strLabel = StringUtils::Format(g_localizeStrings.Get(20464), localizedType); + if (CGUIDialogVideoInfo::GetItemsForTag(strLabel, mediaType, items, idTag)) + { + for (int index = 0; index < items.Size(); index++) + { + if (!items[index]->HasVideoInfoTag() || items[index]->GetVideoInfoTag()->m_iDbId <= 0) + continue; + + videodb.AddTagToItem(items[index]->GetVideoInfoTag()->m_iDbId, idTag, mediaType); + } + } + + Refresh(true); + return true; + } + + return CGUIWindowVideoBase::OnClick(iItem, player); +} + +std::string CGUIWindowVideoNav::GetStartFolder(const std::string &dir) +{ + std::string lower(dir); StringUtils::ToLower(lower); + if (lower == "moviegenres") + return "videodb://movies/genres/"; + else if (lower == "movietitles") + return "videodb://movies/titles/"; + else if (lower == "movieyears") + return "videodb://movies/years/"; + else if (lower == "movieactors") + return "videodb://movies/actors/"; + else if (lower == "moviedirectors") + return "videodb://movies/directors/"; + else if (lower == "moviestudios") + return "videodb://movies/studios/"; + else if (lower == "moviesets") + return "videodb://movies/sets/"; + else if (lower == "moviecountries") + return "videodb://movies/countries/"; + else if (lower == "movietags") + return "videodb://movies/tags/"; + else if (lower == "movies") + return "videodb://movies/"; + else if (lower == "tvshowgenres") + return "videodb://tvshows/genres/"; + else if (lower == "tvshowtitles") + return "videodb://tvshows/titles/"; + else if (lower == "tvshowyears") + return "videodb://tvshows/years/"; + else if (lower == "tvshowactors") + return "videodb://tvshows/actors/"; + else if (lower == "tvshowstudios") + return "videodb://tvshows/studios/"; + else if (lower == "tvshowtags") + return "videodb://tvshows/tags/"; + else if (lower == "tvshows") + return "videodb://tvshows/"; + else if (lower == "musicvideogenres") + return "videodb://musicvideos/genres/"; + else if (lower == "musicvideotitles") + return "videodb://musicvideos/titles/"; + else if (lower == "musicvideoyears") + return "videodb://musicvideos/years/"; + else if (lower == "musicvideoartists") + return "videodb://musicvideos/artists/"; + else if (lower == "musicvideoalbums") + return "videodb://musicvideos/albums/"; + else if (lower == "musicvideodirectors") + return "videodb://musicvideos/directors/"; + else if (lower == "musicvideostudios") + return "videodb://musicvideos/studios/"; + else if (lower == "musicvideotags") + return "videodb://musicvideos/tags/"; + else if (lower == "musicvideos") + return "videodb://musicvideos/"; + else if (lower == "recentlyaddedmovies") + return "videodb://recentlyaddedmovies/"; + else if (lower == "recentlyaddedepisodes") + return "videodb://recentlyaddedepisodes/"; + else if (lower == "recentlyaddedmusicvideos") + return "videodb://recentlyaddedmusicvideos/"; + else if (lower == "inprogresstvshows") + return "videodb://inprogresstvshows/"; + else if (lower == "files") + return "sources://video/"; + return CGUIWindowVideoBase::GetStartFolder(dir); +} + +bool CGUIWindowVideoNav::ApplyWatchedFilter(CFileItemList &items) +{ + bool listchanged = false; + CVideoDatabaseDirectory dir; + NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath()); + + // now filter watched items as necessary + bool filterWatched=false; + if (node == NODE_TYPE_EPISODES + || node == NODE_TYPE_SEASONS + || node == NODE_TYPE_SETS + || node == NODE_TYPE_TAGS + || node == NODE_TYPE_TITLE_MOVIES + || node == NODE_TYPE_TITLE_TVSHOWS + || node == NODE_TYPE_TITLE_MUSICVIDEOS + || node == NODE_TYPE_RECENTLY_ADDED_EPISODES + || node == NODE_TYPE_RECENTLY_ADDED_MOVIES + || node == NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS) + filterWatched = true; + if (!items.IsVideoDb()) + filterWatched = true; + if (items.GetContent() == "tvshows" && + (items.IsSmartPlayList() || items.IsLibraryFolder())) + node = NODE_TYPE_TITLE_TVSHOWS; // so that the check below works + + int watchMode = CMediaSettings::GetInstance().GetWatchedMode(m_vecItems->GetContent()); + + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr item = items.Get(i); + + if(item->HasVideoInfoTag() && (node == NODE_TYPE_TITLE_TVSHOWS || node == NODE_TYPE_SEASONS)) + { + if (watchMode == WatchedModeUnwatched) + item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("unwatchedepisodes").asInteger(); + if (watchMode == WatchedModeWatched) + item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("watchedepisodes").asInteger(); + if (watchMode == WatchedModeAll) + item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger(); + item->SetProperty("numepisodes", item->GetVideoInfoTag()->m_iEpisode); + listchanged = true; + } + + if (filterWatched) + { + if(!item->IsParentFolder() && // Don't delete the go to parent folder + ((watchMode == WatchedModeWatched && item->GetVideoInfoTag()->GetPlayCount() == 0) || + (watchMode == WatchedModeUnwatched && item->GetVideoInfoTag()->GetPlayCount() > 0))) + { + items.Remove(i); + i--; + listchanged = true; + } + } + } + + // Remove the parent folder icon, if it's the only thing in the folder. This is needed for hiding seasons. + if (items.GetObjectCount() == 0 && items.GetFileCount() > 0 && items.Get(0)->IsParentFolder()) + items.Remove(0); + + if(node == NODE_TYPE_TITLE_TVSHOWS || node == NODE_TYPE_SEASONS) + { + // the watched filter may change the "numepisodes" property which is reflected in the TV_SHOWS and SEASONS nodes + // therefore, the items labels have to be refreshed, and possibly the list needs resorting as well. + items.ClearSortState(); // this is needed to force resorting even if sort method did not change + FormatAndSort(items); + } + + return listchanged; +} diff --git a/xbmc/video/windows/GUIWindowVideoNav.h b/xbmc/video/windows/GUIWindowVideoNav.h new file mode 100644 index 0000000..bdf4987 --- /dev/null +++ b/xbmc/video/windows/GUIWindowVideoNav.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "GUIWindowVideoBase.h" + +class CFileItemList; + +enum SelectFirstUnwatchedItem +{ + NEVER = 0, + ON_FIRST_ENTRY = 1, + ALWAYS = 2 +}; + +enum IncludeAllSeasonsAndSpecials +{ + NEITHER = 0, + BOTH = 1, + ALL_SEASONS = 2, + SPECIALS = 3 +}; + +class CGUIWindowVideoNav : public CGUIWindowVideoBase +{ +public: + + CGUIWindowVideoNav(void); + ~CGUIWindowVideoNav(void) override; + + bool OnAction(const CAction &action) override; + bool OnMessage(CGUIMessage& message) override; + + bool OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& info) override; + +protected: + bool ApplyWatchedFilter(CFileItemList &items); + bool GetFilteredItems(const std::string &filter, CFileItemList &items) override; + + void OnItemLoaded(CFileItem* pItem) override {}; + + // override base class methods + bool Update(const std::string &strDirectory, bool updateFilterPath = true) override; + bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override; + void UpdateButtons() override; + void DoSearch(const std::string& strSearch, CFileItemList& items) override; + virtual void PlayItem(int iItem); + void OnDeleteItem(const CFileItemPtr& pItem) override; + void GetContextButtons(int itemNumber, CContextButtons &buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + bool OnAddMediaSource() override; + bool OnClick(int iItem, const std::string &player = "") override; + std::string GetStartFolder(const std::string &dir) override; + + VECSOURCES m_shares; + +private: + virtual SelectFirstUnwatchedItem GetSettingSelectFirstUnwatchedItem(); + virtual IncludeAllSeasonsAndSpecials GetSettingIncludeAllSeasonsAndSpecials(); + virtual int GetFirstUnwatchedItemIndex(bool includeAllSeasons, bool includeSpecials); + void SelectFirstUnwatched(); +}; diff --git a/xbmc/video/windows/GUIWindowVideoPlaylist.cpp b/xbmc/video/windows/GUIWindowVideoPlaylist.cpp new file mode 100644 index 0000000..b27582d --- /dev/null +++ b/xbmc/video/windows/GUIWindowVideoPlaylist.cpp @@ -0,0 +1,609 @@ +/* + * Copyright (C) 2005-2018 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 "GUIWindowVideoPlaylist.h" + +#include "GUIUserMessages.h" +#include "PartyModeManager.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "cores/playercorefactory/PlayerCoreFactory.h" +#include "dialogs/GUIDialogSmartPlaylistEditor.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "playlists/PlayListM3U.h" +#include "settings/MediaSettings.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#define CONTROL_BTNVIEWASICONS 2 +#define CONTROL_BTNSORTBY 3 +#define CONTROL_BTNSORTASC 4 +#define CONTROL_LABELFILES 12 + +#define CONTROL_BTNSHUFFLE 20 +#define CONTROL_BTNSAVE 21 +#define CONTROL_BTNCLEAR 22 + +#define CONTROL_BTNPLAY 23 +#define CONTROL_BTNNEXT 24 +#define CONTROL_BTNPREVIOUS 25 +#define CONTROL_BTNREPEAT 26 + +CGUIWindowVideoPlaylist::CGUIWindowVideoPlaylist() +: CGUIWindowVideoBase(WINDOW_VIDEO_PLAYLIST, "MyPlaylist.xml") +{ + m_movingFrom = -1; +} + +CGUIWindowVideoPlaylist::~CGUIWindowVideoPlaylist() = default; + +void CGUIWindowVideoPlaylist::OnPrepareFileItems(CFileItemList& items) +{ + CGUIWindowVideoBase::OnPrepareFileItems(items); + + if (items.IsEmpty()) + return; + + if (!items.IsVideoDb() && !items.IsVirtualDirectoryRoot()) + { // load info from the database + std::string label; + if (items.GetLabel().empty() && + m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("video"), + &label)) + items.SetLabel(label); + if (!items.IsSourcesPath() && !items.IsLibraryFolder()) + LoadVideoInfo(items, m_database); + } +} + +bool CGUIWindowVideoPlaylist::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_PLAYLISTPLAYER_REPEAT: + { + UpdateButtons(); + } + break; + + case GUI_MSG_PLAYLISTPLAYER_RANDOM: + case GUI_MSG_PLAYLIST_CHANGED: + { + // global playlist changed outside playlist window + UpdateButtons(); + Refresh(true); + + if (m_viewControl.HasControl(m_iLastControl) && m_vecItems->Size() <= 0) + { + m_iLastControl = CONTROL_BTNVIEWASICONS; + SET_CONTROL_FOCUS(m_iLastControl, 0); + } + + } + break; + + case GUI_MSG_WINDOW_DEINIT: + { + m_movingFrom = -1; + } + break; + + case GUI_MSG_WINDOW_INIT: + { + m_vecItems->SetPath("playlistvideo://"); + + if (!CGUIWindowVideoBase::OnMessage(message)) + return false; + + if (m_vecItems->Size() <= 0) + { + m_iLastControl = CONTROL_BTNVIEWASICONS; + SET_CONTROL_FOCUS(m_iLastControl, 0); + } + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingVideo() && + CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO) + { + int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + if (iSong >= 0 && iSong <= m_vecItems->Size()) + m_viewControl.SetSelectedItem(iSong); + } + + return true; + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTNSHUFFLE) + { + if (!g_partyModeManager.IsEnabled()) + { + CServiceBroker::GetPlaylistPlayer().SetShuffle( + PLAYLIST::TYPE_VIDEO, + !(CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_VIDEO))); + CMediaSettings::GetInstance().SetVideoPlaylistShuffled( + CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_VIDEO)); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + UpdateButtons(); + Refresh(); + } + } + else if (iControl == CONTROL_BTNSAVE) + { + SavePlayList(); + } + else if (iControl == CONTROL_BTNCLEAR) + { + ClearPlayList(); + } + else if (iControl == CONTROL_BTNPLAY) + { + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO); + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().Play(m_viewControl.GetSelectedItem(), ""); + UpdateButtons(); + } + else if (iControl == CONTROL_BTNNEXT) + { + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO); + CServiceBroker::GetPlaylistPlayer().PlayNext(); + } + else if (iControl == CONTROL_BTNPREVIOUS) + { + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO); + CServiceBroker::GetPlaylistPlayer().PlayPrevious(); + } + else if (iControl == CONTROL_BTNREPEAT) + { + // increment repeat state + PLAYLIST::RepeatState state = + CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_VIDEO); + if (state == PLAYLIST::RepeatState::NONE) + CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO, + PLAYLIST::RepeatState::ALL); + else if (state == PLAYLIST::RepeatState::ALL) + CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO, + PLAYLIST::RepeatState::ONE); + else + CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO, + PLAYLIST::RepeatState::NONE); + + // save settings + CMediaSettings::GetInstance().SetVideoPlaylistRepeat( + CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_VIDEO) == + PLAYLIST::RepeatState::ALL); + CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); + + UpdateButtons(); + } + else if (m_viewControl.HasControl(iControl)) // list/thumb control + { + int iAction = message.GetParam1(); + int iItem = m_viewControl.GetSelectedItem(); + if (iAction == ACTION_DELETE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK) + { + RemovePlayListItem(iItem); + MarkPlaying(); + } + } + } + break; + } + return CGUIWindowVideoBase::OnMessage(message); +} + +bool CGUIWindowVideoPlaylist::OnAction(const CAction &action) +{ + if (action.GetID() == ACTION_PARENT_DIR) + { + // Playlist has no parent dirs + return true; + } + if (action.GetID() == ACTION_SHOW_PLAYLIST) + { + CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow(); + return true; + } + if ((action.GetID() == ACTION_MOVE_ITEM_UP) || (action.GetID() == ACTION_MOVE_ITEM_DOWN)) + { + int iItem = -1; + int iFocusedControl = GetFocusedControlID(); + if (m_viewControl.HasControl(iFocusedControl)) + iItem = m_viewControl.GetSelectedItem(); + OnMove(iItem, action.GetID()); + return true; + } + return CGUIWindowVideoBase::OnAction(action); +} + +bool CGUIWindowVideoPlaylist::OnBack(int actionID) +{ + if (actionID == ACTION_NAV_BACK) + return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up + return CGUIWindowVideoBase::OnBack(actionID); +} + +bool CGUIWindowVideoPlaylist::MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate /* = true */) +{ + int iSelected = iItem; + int iNew = iSelected; + if (iAction == ACTION_MOVE_ITEM_UP) + iNew--; + else + iNew++; + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + // is the currently playing item affected? + bool bFixCurrentSong = false; + if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO) && + appPlayer->IsPlayingVideo() && + ((CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iSelected) || + (CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iNew))) + bFixCurrentSong = true; + + PLAYLIST::CPlayList& playlist = + CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_VIDEO); + if (playlist.Swap(iSelected, iNew)) + { + // Correct the current playing song in playlistplayer + if (bFixCurrentSong) + { + int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + if (iSelected == iCurrentSong) + iCurrentSong = iNew; + else if (iNew == iCurrentSong) + iCurrentSong = iSelected; + CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong); + } + + if (bUpdate) + Refresh(); + return true; + } + + return false; +} + + +void CGUIWindowVideoPlaylist::ClearPlayList() +{ + ClearFileItems(); + CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO); + if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO) + { + CServiceBroker::GetPlaylistPlayer().Reset(); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE); + } + m_viewControl.SetItems(*m_vecItems); + UpdateButtons(); + SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0); +} + +void CGUIWindowVideoPlaylist::UpdateButtons() +{ + // Update playlist buttons + if (m_vecItems->Size() ) + { + CONTROL_ENABLE(CONTROL_BTNCLEAR); + CONTROL_ENABLE(CONTROL_BTNSAVE); + CONTROL_ENABLE(CONTROL_BTNPLAY); + CONTROL_ENABLE(CONTROL_BTNSHUFFLE); + CONTROL_ENABLE(CONTROL_BTNREPEAT); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingVideo() && + CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO) + { + CONTROL_ENABLE(CONTROL_BTNNEXT); + CONTROL_ENABLE(CONTROL_BTNPREVIOUS); + } + else + { + CONTROL_DISABLE(CONTROL_BTNNEXT); + CONTROL_DISABLE(CONTROL_BTNPREVIOUS); + } + } + else + { + CONTROL_DISABLE(CONTROL_BTNCLEAR); + CONTROL_DISABLE(CONTROL_BTNSAVE); + CONTROL_DISABLE(CONTROL_BTNSHUFFLE); + CONTROL_DISABLE(CONTROL_BTNPLAY); + CONTROL_DISABLE(CONTROL_BTNNEXT); + CONTROL_DISABLE(CONTROL_BTNPREVIOUS); + CONTROL_DISABLE(CONTROL_BTNREPEAT); + } + + CGUIMediaWindow::UpdateButtons(); + + // update buttons + CONTROL_DESELECT(CONTROL_BTNSHUFFLE); + if (CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_VIDEO)) + CONTROL_SELECT(CONTROL_BTNSHUFFLE); + + // update repeat button + PLAYLIST::RepeatState repState = + CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_VIDEO); + int iLocalizedString; + if (repState == PLAYLIST::RepeatState::NONE) + iLocalizedString = 595; // Repeat: Off + else if (repState == PLAYLIST::RepeatState::ONE) + iLocalizedString = 596; // Repeat: One + else + iLocalizedString = 597; // Repeat: All + + SET_CONTROL_LABEL(CONTROL_BTNREPEAT, g_localizeStrings.Get(iLocalizedString)); + + MarkPlaying(); +} + +bool CGUIWindowVideoPlaylist::OnPlayMedia(int iItem, const std::string &player) +{ + if ( iItem < 0 || iItem >= m_vecItems->Size() ) return false; + if (g_partyModeManager.IsEnabled()) + g_partyModeManager.Play(iItem); + else + { + CFileItemPtr pItem = m_vecItems->Get(iItem); + std::string strPath = pItem->GetPath(); + CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO); + // need to update Playlist FileItem's startOffset and resumePoint based on GUIWindowVideoPlaylist FileItem + if (pItem->GetStartOffset() == STARTOFFSET_RESUME) + { + CFileItemPtr pPlaylistItem = + CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_VIDEO)[iItem]; + pPlaylistItem->SetStartOffset(pItem->GetStartOffset()); + if (pPlaylistItem->HasVideoInfoTag() && pItem->HasVideoInfoTag()) + pPlaylistItem->GetVideoInfoTag()->SetResumePoint(pItem->GetVideoInfoTag()->GetResumePoint()); + } + // now play item + CServiceBroker::GetPlaylistPlayer().Play(iItem, player); + } + return true; +} + +void CGUIWindowVideoPlaylist::RemovePlayListItem(int iItem) +{ + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + // The current playing song can't be removed + if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO && + appPlayer->IsPlayingVideo() && CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iItem) + return ; + + CServiceBroker::GetPlaylistPlayer().Remove(PLAYLIST::TYPE_VIDEO, iItem); + + Refresh(); + + if (m_vecItems->Size() <= 0) + { + SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0); + } + else + { + m_viewControl.SetSelectedItem(iItem - 1); + } + + g_partyModeManager.OnSongChange(); +} + +/// \brief Save current playlist to playlist folder +void CGUIWindowVideoPlaylist::SavePlayList() +{ + std::string strNewFileName; + if (CGUIKeyboardFactory::ShowAndGetInput(strNewFileName, CVariant{g_localizeStrings.Get(16012)}, false)) + { + // need 2 rename it + strNewFileName = CUtil::MakeLegalFileName(strNewFileName); + strNewFileName += ".m3u"; + std::string strPath = URIUtils::AddFileToFolder( + CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH), + "video", + strNewFileName); + + PLAYLIST::CPlayListM3U playlist; + playlist.Add(*m_vecItems); + + CLog::Log(LOGDEBUG, "Saving video playlist: [{}]", strPath); + playlist.Save(strPath); + } +} + +void CGUIWindowVideoPlaylist::GetContextButtons(int itemNumber, CContextButtons &buttons) +{ + int itemPlaying = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + if (m_movingFrom >= 0) + { + if (itemNumber != m_movingFrom && (!g_partyModeManager.IsEnabled() || itemNumber > itemPlaying)) + buttons.Add(CONTEXT_BUTTON_MOVE_HERE, 13252); // move item here + buttons.Add(CONTEXT_BUTTON_CANCEL_MOVE, 13253); + + } + else + { + if (itemNumber > -1) + { + CFileItemPtr item = m_vecItems->Get(itemNumber); + + const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + // check what players we have, if we have multiple display play with option + std::vector<std::string> players; + if (item->IsVideoDb()) + { + CFileItem item2(item->GetVideoInfoTag()->m_strFileNameAndPath, false); + playerCoreFactory.GetPlayers(item2, players); + } + else + playerCoreFactory.GetPlayers(*item, players); + + if (players.size() > 1) + buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With... + } + if (itemNumber > (g_partyModeManager.IsEnabled() ? 1 : 0)) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332); + if (itemNumber + 1 < m_vecItems->Size()) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333); + if (!g_partyModeManager.IsEnabled() || itemNumber != itemPlaying) + buttons.Add(CONTEXT_BUTTON_MOVE_ITEM, 13251); + + if (itemNumber != itemPlaying) + buttons.Add(CONTEXT_BUTTON_DELETE, 15015); + } + if (g_partyModeManager.IsEnabled()) + { + buttons.Add(CONTEXT_BUTTON_EDIT_PARTYMODE, 21439); + buttons.Add(CONTEXT_BUTTON_CANCEL_PARTYMODE, 588); // cancel party mode + } +} + +bool CGUIWindowVideoPlaylist::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + switch (button) + { + case CONTEXT_BUTTON_PLAY_WITH: + { + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + if (!item) + break; + + const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory(); + + std::vector<std::string> players; + if (item->IsVideoDb()) + { + CFileItem item2(*item->GetVideoInfoTag()); + playerCoreFactory.GetPlayers(item2, players); + } + else + playerCoreFactory.GetPlayers(*item, players); + + std::string player = playerCoreFactory.SelectPlayerDialog(players); + if (!player.empty()) + OnClick(itemNumber, player); + return true; + } + + case CONTEXT_BUTTON_MOVE_ITEM: + m_movingFrom = itemNumber; + return true; + + case CONTEXT_BUTTON_MOVE_HERE: + if (m_movingFrom >= 0) + MoveItem(m_movingFrom, itemNumber); + m_movingFrom = -1; + return true; + + case CONTEXT_BUTTON_CANCEL_MOVE: + m_movingFrom = -1; + return true; + + case CONTEXT_BUTTON_MOVE_ITEM_UP: + OnMove(itemNumber, ACTION_MOVE_ITEM_UP); + return true; + + case CONTEXT_BUTTON_MOVE_ITEM_DOWN: + OnMove(itemNumber, ACTION_MOVE_ITEM_DOWN); + return true; + + case CONTEXT_BUTTON_DELETE: + RemovePlayListItem(itemNumber); + return true; + case CONTEXT_BUTTON_CANCEL_PARTYMODE: + g_partyModeManager.Disable(); + return true; + case CONTEXT_BUTTON_EDIT_PARTYMODE: + { + std::string playlist = "special://profile/PartyMode-Video.xsp"; + if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist)) + { + // apply new rules + g_partyModeManager.Disable(); + g_partyModeManager.Enable(PARTYMODECONTEXT_VIDEO); + } + return true; + } + default: + break; + } + + return CGUIWindowVideoBase::OnContextButton(itemNumber, button); +} + +void CGUIWindowVideoPlaylist::OnMove(int iItem, int iAction) +{ + if (iItem < 0 || iItem >= m_vecItems->Size()) return; + MoveCurrentPlayListItem(iItem, iAction); +} + +void CGUIWindowVideoPlaylist::MoveItem(int iStart, int iDest) +{ + if (iStart < 0 || iStart >= m_vecItems->Size()) return; + if (iDest < 0 || iDest >= m_vecItems->Size()) return; + + // default to move up + int iAction = ACTION_MOVE_ITEM_UP; + int iDirection = -1; + // are we moving down? + if (iStart < iDest) + { + iAction = ACTION_MOVE_ITEM_DOWN; + iDirection = 1; + } + + // keep swapping until you get to the destination or you + // hit the currently playing song + int i = iStart; + while (i != iDest) + { + // try to swap adjacent items + if (MoveCurrentPlayListItem(i, iAction, false)) + i = i + (1 * iDirection); + // we hit currently playing song, so abort + else + break; + } + Refresh(); +} + +void CGUIWindowVideoPlaylist::MarkPlaying() +{ + /* // clear markings + for (int i = 0; i < m_vecItems->Size(); i++) + m_vecItems->Get(i)->Select(false); + + // mark the currently playing item + if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == TYPE_VIDEO) && (g_application.GetAppPlayer().IsPlayingVideo())) + { + int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong(); + if (iSong >= 0 && iSong <= m_vecItems->Size()) + m_vecItems->Get(iSong)->Select(true); + }*/ +} + diff --git a/xbmc/video/windows/GUIWindowVideoPlaylist.h b/xbmc/video/windows/GUIWindowVideoPlaylist.h new file mode 100644 index 0000000..ea04fb0 --- /dev/null +++ b/xbmc/video/windows/GUIWindowVideoPlaylist.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2018 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. + */ + +#pragma once + +#include "GUIWindowVideoBase.h" + +class CGUIWindowVideoPlaylist : public CGUIWindowVideoBase +{ +public: + CGUIWindowVideoPlaylist(void); + ~CGUIWindowVideoPlaylist(void) override; + + void OnPrepareFileItems(CFileItemList& items) override; + bool OnMessage(CGUIMessage& message) override; + bool OnAction(const CAction &action) override; + bool OnBack(int actionID) override; + +protected: + bool OnPlayMedia(int iItem, const std::string &player = "") override; + void UpdateButtons() override; + void MarkPlaying(); + + void GetContextButtons(int itemNumber, CContextButtons &buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + + void OnMove(int iItem, int iAction); + + void ClearPlayList(); + void RemovePlayListItem(int iItem); + bool MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate = true); + void MoveItem(int iStart, int iDest); + + void SavePlayList(); + + int m_movingFrom; + VECSOURCES m_shares; +}; diff --git a/xbmc/video/windows/VideoFileItemListModifier.cpp b/xbmc/video/windows/VideoFileItemListModifier.cpp new file mode 100644 index 0000000..4d26af3 --- /dev/null +++ b/xbmc/video/windows/VideoFileItemListModifier.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2016-2018 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 "VideoFileItemListModifier.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "filesystem/VideoDatabaseDirectory/DirectoryNode.h" +#include "guilib/LocalizeStrings.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "video/VideoDatabase.h" +#include "video/VideoDbUrl.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +bool CVideoFileItemListModifier::CanModify(const CFileItemList &items) const +{ + if (items.IsVideoDb()) + return true; + + return false; +} + +bool CVideoFileItemListModifier::Modify(CFileItemList &items) const +{ + AddQueuingFolder(items); + return true; +} + +// Add an "* All ..." folder to the CFileItemList +// depending on the child node +void CVideoFileItemListModifier::AddQueuingFolder(CFileItemList& items) +{ + if (!items.IsVideoDb()) + return; + + auto directoryNode = CDirectoryNode::ParseURL(items.GetPath()); + + CFileItemPtr pItem; + + // always show "all" items by default + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_SHOWALLITEMS)) + return; + + // no need for "all" item when only one item + if (items.GetObjectCount() <= 1) + return; + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(directoryNode->BuildPath())) + return; + + // hack - as the season node might return episodes + std::unique_ptr<CDirectoryNode> pNode(directoryNode); + + switch (pNode->GetChildType()) + { + case NODE_TYPE_SEASONS: + { + const std::string& strLabel = g_localizeStrings.Get(20366); + pItem.reset(new CFileItem(strLabel)); // "All Seasons" + videoUrl.AppendPath("-1/"); + pItem->SetPath(videoUrl.ToString()); + // set the number of watched and unwatched items accordingly + int watched = 0; + int unwatched = 0; + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr item = items[i]; + watched += static_cast<int>(item->GetProperty("watchedepisodes").asInteger()); + unwatched += static_cast<int>(item->GetProperty("unwatchedepisodes").asInteger()); + } + const int totalEpisodes = watched + unwatched; + pItem->SetProperty("totalepisodes", totalEpisodes); + pItem->SetProperty("numepisodes", + totalEpisodes); // will be changed later to reflect watchmode setting + pItem->SetProperty("watchedepisodes", watched); + pItem->SetProperty("unwatchedepisodes", unwatched); + pItem->SetProperty("watchedepisodepercent", + totalEpisodes > 0 ? watched * 100 / totalEpisodes : 0); + + // @note: The items list may contain additional items that do not belong to the show. + // This is the case of the up directory (..) or movies linked to the tvshow. + // Iterate through the list till the first season type is found and the infotag can safely be copied. + + if (items.Size() > 1) + { + for (int i = 1; i < items.Size(); i++) + { + if (items[i]->GetVideoInfoTag() && items[i]->GetVideoInfoTag()->m_type == MediaTypeSeason && + items[i]->GetVideoInfoTag()->m_iSeason > 0) + { + *pItem->GetVideoInfoTag() = *items[i]->GetVideoInfoTag(); + pItem->GetVideoInfoTag()->m_iSeason = -1; + break; + } + } + } + + pItem->GetVideoInfoTag()->m_strTitle = strLabel; + pItem->GetVideoInfoTag()->m_iEpisode = watched + unwatched; + pItem->GetVideoInfoTag()->SetPlayCount((unwatched == 0) ? 1 : 0); + CVideoDatabase db; + if (db.Open()) + { + pItem->GetVideoInfoTag()->m_iDbId = db.GetSeasonId(pItem->GetVideoInfoTag()->m_iIdShow, -1); + db.Close(); + } + pItem->GetVideoInfoTag()->m_type = MediaTypeSeason; + } + break; + case NODE_TYPE_MUSICVIDEOS_ALBUM: + pItem.reset(new CFileItem("* " + g_localizeStrings.Get(16100))); // "* All Videos" + videoUrl.AppendPath("-1/"); + pItem->SetPath(videoUrl.ToString()); + break; + default: + break; + } + + if (pItem) + { + pItem->m_bIsFolder = true; + pItem->SetSpecialSort(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryAllItemsOnBottom ? SortSpecialOnBottom : SortSpecialOnTop); + pItem->SetCanQueue(false); + items.Add(pItem); + } +} diff --git a/xbmc/video/windows/VideoFileItemListModifier.h b/xbmc/video/windows/VideoFileItemListModifier.h new file mode 100644 index 0000000..c4727f9 --- /dev/null +++ b/xbmc/video/windows/VideoFileItemListModifier.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016-2018 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. + */ + +#pragma once + +#include "IFileItemListModifier.h" + +class CVideoFileItemListModifier : public IFileItemListModifier +{ +public: + CVideoFileItemListModifier() = default; + ~CVideoFileItemListModifier() override = default; + + bool CanModify(const CFileItemList &items) const override; + bool Modify(CFileItemList &items) const override; + +private: + static void AddQueuingFolder(CFileItemList & items); +}; |