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/VideoInfoScanner.cpp | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/video/VideoInfoScanner.cpp')
-rw-r--r-- | xbmc/video/VideoInfoScanner.cpp | 2247 |
1 files changed, 2247 insertions, 0 deletions
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 + } + +} |