[video] fix selection after changing video or extra art
[xbmc.git] / xbmc / video / VideoInfoScanner.cpp
blobaca4162fd5d523e9e23417acc877548ce3c36a51
1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "VideoInfoScanner.h"
11 #include "FileItem.h"
12 #include "GUIInfoManager.h"
13 #include "GUIUserMessages.h"
14 #include "ServiceBroker.h"
15 #include "TextureCache.h"
16 #include "URL.h"
17 #include "Util.h"
18 #include "VideoInfoDownloader.h"
19 #include "cores/VideoPlayer/DVDFileInfo.h"
20 #include "dialogs/GUIDialogExtendedProgressBar.h"
21 #include "dialogs/GUIDialogProgress.h"
22 #include "events/EventLog.h"
23 #include "events/MediaLibraryEvent.h"
24 #include "filesystem/Directory.h"
25 #include "filesystem/File.h"
26 #include "filesystem/MultiPathDirectory.h"
27 #include "filesystem/PluginDirectory.h"
28 #include "guilib/GUIComponent.h"
29 #include "guilib/GUIWindowManager.h"
30 #include "guilib/LocalizeStrings.h"
31 #include "interfaces/AnnouncementManager.h"
32 #include "messaging/helpers/DialogHelper.h"
33 #include "messaging/helpers/DialogOKHelper.h"
34 #include "settings/AdvancedSettings.h"
35 #include "settings/Settings.h"
36 #include "settings/SettingsComponent.h"
37 #include "tags/VideoInfoTagLoaderFactory.h"
38 #include "utils/Digest.h"
39 #include "utils/FileExtensionProvider.h"
40 #include "utils/RegExp.h"
41 #include "utils/StringUtils.h"
42 #include "utils/URIUtils.h"
43 #include "utils/Variant.h"
44 #include "utils/log.h"
45 #include "video/VideoManagerTypes.h"
46 #include "video/VideoThumbLoader.h"
47 #include "video/dialogs/GUIDialogVideoManagerExtras.h"
48 #include "video/dialogs/GUIDialogVideoManagerVersions.h"
50 #include <algorithm>
51 #include <memory>
52 #include <utility>
54 using namespace XFILE;
55 using namespace ADDON;
56 using namespace KODI::MESSAGING;
58 using KODI::MESSAGING::HELPERS::DialogResponse;
59 using KODI::UTILITY::CDigest;
61 namespace VIDEO
64 CVideoInfoScanner::CVideoInfoScanner()
66 m_bStop = false;
67 m_scanAll = false;
69 const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
71 m_ignoreVideoVersions = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_IGNOREVIDEOVERSIONS);
72 m_ignoreVideoExtras = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_IGNOREVIDEOEXTRAS);
75 CVideoInfoScanner::~CVideoInfoScanner()
76 = default;
78 void CVideoInfoScanner::Process()
80 m_bStop = false;
82 try
84 const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
86 if (m_showDialog && !settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE))
88 CGUIDialogExtendedProgressBar* dialog =
89 CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
90 if (dialog)
91 m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
94 // check if we only need to perform a cleaning
95 if (m_bClean && m_pathsToScan.empty())
97 std::set<int> paths;
98 m_database.CleanDatabase(m_handle, paths, false);
100 if (m_handle)
101 m_handle->MarkFinished();
102 m_handle = NULL;
104 m_bRunning = false;
106 return;
109 auto start = std::chrono::steady_clock::now();
111 m_database.Open();
113 m_bCanInterrupt = true;
115 CLog::Log(LOGINFO, "VideoInfoScanner: Starting scan ..");
116 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
117 "OnScanStarted");
119 // Database operations should not be canceled
120 // using Interrupt() while scanning as it could
121 // result in unexpected behaviour.
122 m_bCanInterrupt = false;
124 bool bCancelled = false;
125 while (!bCancelled && !m_pathsToScan.empty())
128 * A copy of the directory path is used because the path supplied is
129 * immediately removed from the m_pathsToScan set in DoScan(). If the
130 * reference points to the entry in the set a null reference error
131 * occurs.
133 std::string directory = *m_pathsToScan.begin();
134 if (m_bStop)
136 bCancelled = true;
138 else if (!CDirectory::Exists(directory))
141 * Note that this will skip clean (if m_bClean is enabled) if the directory really
142 * doesn't exist rather than a NAS being switched off. A manual clean from settings
143 * will still pick up and remove it though.
145 CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan{}.", __FUNCTION__,
146 CURL::GetRedacted(directory), m_bClean ? " and clean" : "");
147 m_pathsToScan.erase(m_pathsToScan.begin());
149 else if (!DoScan(directory))
150 bCancelled = true;
153 if (!bCancelled)
155 if (m_bClean)
156 m_database.CleanDatabase(m_handle, m_pathsToClean, false);
157 else
159 if (m_handle)
160 m_handle->SetTitle(g_localizeStrings.Get(331));
161 m_database.Compress(false);
165 CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
166 m_database.Close();
168 auto end = std::chrono::steady_clock::now();
169 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
171 CLog::Log(LOGINFO, "VideoInfoScanner: Finished scan. Scanning for video info took {} ms",
172 duration.count());
174 catch (...)
176 CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
179 m_bRunning = false;
180 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
181 "OnScanFinished");
183 if (m_handle)
184 m_handle->MarkFinished();
185 m_handle = NULL;
188 void CVideoInfoScanner::Start(const std::string& strDirectory, bool scanAll)
190 m_strStartDir = strDirectory;
191 m_scanAll = scanAll;
192 m_pathsToScan.clear();
193 m_pathsToClean.clear();
195 m_database.Open();
196 if (strDirectory.empty())
197 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
198 // we go.
199 m_database.GetPaths(m_pathsToScan);
201 else
202 { // scan all the paths of this subtree that is in the database
203 std::vector<std::string> rootDirs;
204 if (URIUtils::IsMultiPath(strDirectory))
205 CMultiPathDirectory::GetPaths(strDirectory, rootDirs);
206 else
207 rootDirs.push_back(strDirectory);
209 for (std::vector<std::string>::const_iterator it = rootDirs.begin(); it < rootDirs.end(); ++it)
211 m_pathsToScan.insert(*it);
212 std::vector<std::pair<int, std::string>> subpaths;
213 m_database.GetSubPaths(*it, subpaths);
214 for (std::vector<std::pair<int, std::string>>::iterator it = subpaths.begin(); it < subpaths.end(); ++it)
215 m_pathsToScan.insert(it->second);
218 m_database.Close();
219 m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryCleanOnUpdate;
221 m_bRunning = true;
222 Process();
225 void CVideoInfoScanner::Stop()
227 if (m_bCanInterrupt)
228 m_database.Interrupt();
230 m_bStop = true;
233 static void OnDirectoryScanned(const std::string& strDirectory)
235 CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
236 msg.SetStringParam(strDirectory);
237 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
240 bool CVideoInfoScanner::DoScan(const std::string& strDirectory)
242 if (m_handle)
244 m_handle->SetText(g_localizeStrings.Get(20415));
248 * Remove this path from the list we're processing. This must be done prior to
249 * the check for file or folder exclusion to prevent an infinite while loop
250 * in Process().
252 std::set<std::string>::iterator it = m_pathsToScan.find(strDirectory);
253 if (it != m_pathsToScan.end())
254 m_pathsToScan.erase(it);
256 // load subfolder
257 CFileItemList items;
258 bool foundDirectly = false;
259 bool bSkip = false;
261 SScanSettings settings;
262 ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
263 CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
265 // exclude folders that match our exclude regexps
266 const std::vector<std::string> &regexps = content == CONTENT_TVSHOWS ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
267 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps;
269 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
270 return true;
272 if (HasNoMedia(strDirectory))
273 return true;
275 bool ignoreFolder = !m_scanAll && settings.noupdate;
276 if (content == CONTENT_NONE || ignoreFolder)
277 return true;
279 if (URIUtils::IsPlugin(strDirectory) && !CPluginDirectory::IsMediaLibraryScanningAllowed(TranslateContent(content), strDirectory))
281 CLog::Log(
282 LOGINFO,
283 "VideoInfoScanner: Plugin '{}' does not support media library scanning for '{}' content",
284 CURL::GetRedacted(strDirectory), TranslateContent(content));
285 return true;
288 std::string hash, dbHash;
289 if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
291 if (m_handle)
293 int str = content == CONTENT_MOVIES ? 20317:20318;
294 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name()));
297 std::string fastHash;
298 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash && !URIUtils::IsPlugin(strDirectory))
299 fastHash = GetFastHash(strDirectory, regexps);
301 if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && StringUtils::EqualsNoCase(fastHash, dbHash))
302 { // fast hashes match - no need to process anything
303 hash = fastHash;
305 else
306 { // need to fetch the folder
307 CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
308 DIR_FLAG_DEFAULTS);
309 // do not consider inner folders with .nomedia
310 items.erase(std::remove_if(items.begin(), items.end(),
311 [this](const CFileItemPtr& item) {
312 return item->m_bIsFolder && HasNoMedia(item->GetPath());
314 items.end());
315 items.Stack();
317 // check whether to re-use previously computed fast hash
318 if (!CanFastHash(items, regexps) || fastHash.empty())
319 GetPathHash(items, hash);
320 else
321 hash = fastHash;
324 if (StringUtils::EqualsNoCase(hash, dbHash))
325 { // hash matches - skipping
326 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change{}",
327 CURL::GetRedacted(strDirectory), !fastHash.empty() ? " (fasthash)" : "");
328 bSkip = true;
330 else if (hash.empty())
331 { // directory empty or non-existent - add to clean list and skip
332 CLog::Log(LOGDEBUG,
333 "VideoInfoScanner: Skipping dir '{}' as it's empty or doesn't exist - adding to "
334 "clean list",
335 CURL::GetRedacted(strDirectory));
336 if (m_bClean)
337 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
338 bSkip = true;
340 else if (dbHash.empty())
341 { // new folder - scan
342 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
343 CURL::GetRedacted(strDirectory));
345 else
346 { // hash changed - rescan
347 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
348 CURL::GetRedacted(strDirectory), dbHash, hash);
351 else if (content == CONTENT_TVSHOWS)
353 if (m_handle)
354 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name()));
356 if (foundDirectly && !settings.parent_name_root)
358 CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
359 DIR_FLAG_DEFAULTS);
360 items.SetPath(strDirectory);
361 GetPathHash(items, hash);
362 bSkip = true;
363 if (!m_database.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash))
364 bSkip = false;
365 else
366 items.Clear();
368 else
370 CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
371 item->SetPath(strDirectory);
372 item->m_bIsFolder = true;
373 items.Add(item);
374 items.SetPath(URIUtils::GetParentPath(item->GetPath()));
377 bool foundSomething = false;
378 if (!bSkip)
380 foundSomething = RetrieveVideoInfo(items, settings.parent_name_root, content);
381 if (foundSomething)
383 if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
385 m_database.SetPathHash(strDirectory, hash);
386 if (m_bClean)
387 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
388 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir {}",
389 CURL::GetRedacted(strDirectory));
392 else
394 if (m_bClean)
395 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
396 CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir {}",
397 CURL::GetRedacted(strDirectory));
400 else if (!StringUtils::EqualsNoCase(hash, dbHash) && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
401 { // update the hash either way - we may have changed the hash to a fast version
402 m_database.SetPathHash(strDirectory, hash);
405 if (m_handle)
406 OnDirectoryScanned(strDirectory);
408 for (int i = 0; i < items.Size(); ++i)
410 CFileItemPtr pItem = items[i];
412 if (m_bStop)
413 break;
415 // add video extras to library
416 if (foundSomething && !m_ignoreVideoExtras && pItem->IsVideoExtras())
418 if (AddVideoExtras(items, content, pItem->GetPath()))
420 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding video extras from dir {}",
421 CURL::GetRedacted(pItem->GetPath()));
424 // no further processing required
425 continue;
428 // if we have a directory item (non-playlist) we then recurse into that folder
429 // do not recurse for tv shows - we have already looked recursively for episodes
430 if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList() && settings.recurse > 0 && content != CONTENT_TVSHOWS)
432 if (!DoScan(pItem->GetPath()))
434 m_bStop = true;
438 return !m_bStop;
441 bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
443 if (pDlgProgress)
445 if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
447 pDlgProgress->ShowProgressBar(true);
448 pDlgProgress->SetPercentage(0);
450 else
451 pDlgProgress->ShowProgressBar(false);
453 pDlgProgress->Progress();
456 m_database.Open();
458 bool FoundSomeInfo = false;
459 std::vector<int> seenPaths;
460 for (int i = 0; i < items.Size(); ++i)
462 CFileItemPtr pItem = items[i];
464 // we do this since we may have a override per dir
465 ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath());
466 if (!info2) // skip
467 continue;
469 // Discard all .nomedia folders
470 if (pItem->m_bIsFolder && HasNoMedia(pItem->GetPath()))
471 continue;
473 // Discard all exclude files defined by regExExclude
474 if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
475 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps))
476 continue;
478 if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
480 if (m_handle)
481 m_handle->SetPercentage(i*100.f/items.Size());
484 // clear our scraper cache
485 info2->ClearCache();
487 INFO_RET ret = INFO_CANCELLED;
488 if (info2->Content() == CONTENT_TVSHOWS)
489 ret = RetrieveInfoForTvShow(pItem.get(), bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress);
490 else if (info2->Content() == CONTENT_MOVIES)
491 ret = RetrieveInfoForMovie(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
492 else if (info2->Content() == CONTENT_MUSICVIDEOS)
493 ret = RetrieveInfoForMusicVideo(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
494 else
496 CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type {} ({})", info2->Content(),
497 CURL::GetRedacted(pItem->GetPath()));
498 FoundSomeInfo = false;
499 break;
501 if (ret == INFO_CANCELLED || ret == INFO_ERROR)
503 CLog::Log(LOGWARNING,
504 "VideoInfoScanner: Error {} occurred while retrieving"
505 "information for {}.",
506 ret, CURL::GetRedacted(pItem->GetPath()));
507 FoundSomeInfo = false;
508 break;
510 if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
511 FoundSomeInfo = true;
512 else if (ret == INFO_NOT_FOUND)
514 CLog::Log(LOGWARNING,
515 "No information found for item '{}', it won't be added to the library.",
516 CURL::GetRedacted(pItem->GetPath()));
518 MediaType mediaType = MediaTypeMovie;
519 if (info2->Content() == CONTENT_TVSHOWS)
520 mediaType = MediaTypeTvShow;
521 else if (info2->Content() == CONTENT_MUSICVIDEOS)
522 mediaType = MediaTypeMusicVideo;
524 auto eventLog = CServiceBroker::GetEventLog();
525 if (eventLog)
527 const std::string itemlogpath = (info2->Content() == CONTENT_TVSHOWS)
528 ? CURL::GetRedacted(pItem->GetPath())
529 : URIUtils::GetFileName(pItem->GetPath());
531 eventLog->Add(EventPtr(new CMediaLibraryEvent(
532 mediaType, pItem->GetPath(), 24145,
533 StringUtils::Format(g_localizeStrings.Get(24147), mediaType, itemlogpath),
534 EventLevel::Warning)));
538 pURL = NULL;
540 // Keep track of directories we've seen
541 if (m_bClean && pItem->m_bIsFolder)
542 seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
545 if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
547 std::vector<std::pair<int, std::string>> libPaths;
548 m_database.GetSubPaths(items.GetPath(), libPaths);
549 for (std::vector<std::pair<int, std::string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
551 if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
552 m_pathsToClean.insert(i->first);
555 if(pDlgProgress)
556 pDlgProgress->ShowProgressBar(false);
558 m_database.Close();
559 return FoundSomeInfo;
562 CInfoScanner::INFO_RET
563 CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem,
564 bool bDirNames,
565 ScraperPtr &info2,
566 bool useLocal,
567 CScraperUrl* pURL,
568 bool fetchEpisodes,
569 CGUIDialogProgress* pDlgProgress)
571 const bool isSeason =
572 pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_type == MediaTypeSeason;
574 int idTvShow = -1;
575 int idSeason = -1;
576 std::string strPath = pItem->GetPath();
577 if (pItem->m_bIsFolder)
579 idTvShow = m_database.GetTvShowId(strPath);
580 if (isSeason && idTvShow > -1)
581 idSeason = m_database.GetSeasonId(idTvShow, pItem->GetVideoInfoTag()->m_iSeason);
583 else if (pItem->IsPlugin() && pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_iIdShow >= 0)
585 // for plugin source we cannot get idTvShow from episode path with URIUtils::GetDirectory() in all cases
586 // so use m_iIdShow from video info tag if possible
587 idTvShow = pItem->GetVideoInfoTag()->m_iIdShow;
588 CVideoInfoTag showInfo;
589 if (m_database.GetTvShowInfo(std::string(), showInfo, idTvShow, nullptr, 0))
590 strPath = showInfo.GetPath();
592 else
594 strPath = URIUtils::GetDirectory(strPath);
595 idTvShow = m_database.GetTvShowId(strPath);
596 if (isSeason && idTvShow > -1)
597 idSeason = m_database.GetSeasonId(idTvShow, pItem->GetVideoInfoTag()->m_iSeason);
599 if (idTvShow > -1 && (!isSeason || idSeason > -1) && (fetchEpisodes || !pItem->m_bIsFolder))
601 INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
602 if (ret == INFO_ADDED)
603 m_database.SetPathHash(strPath, pItem->GetProperty("hash").asString());
604 return ret;
607 if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361,
608 pItem->m_bIsFolder ? pItem->GetVideoInfoTag()->m_strShowTitle
609 : pItem->GetVideoInfoTag()->m_strTitle))
610 return INFO_CANCELLED;
612 if (m_handle)
613 m_handle->SetText(pItem->GetMovieName(bDirNames));
615 CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
616 CScraperUrl scrUrl;
617 // handle .nfo files
618 std::unique_ptr<IVideoInfoTagLoader> loader;
619 if (useLocal)
621 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
622 if (loader)
624 pItem->GetVideoInfoTag()->Reset();
625 result = loader->Load(*pItem->GetVideoInfoTag(), false);
629 if (result == CInfoScanner::FULL_NFO)
632 long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
633 if (lResult < 0)
634 return INFO_ERROR;
635 if (fetchEpisodes)
637 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
638 if (ret == INFO_ADDED)
639 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
640 return ret;
642 return INFO_ADDED;
644 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
646 scrUrl = loader->ScraperUrl();
647 pURL = &scrUrl;
650 CScraperUrl url;
651 int retVal = 0;
652 std::string movieTitle = pItem->GetMovieName(bDirNames);
653 int movieYear = -1; // hint that movie title was not found
654 if (result == CInfoScanner::TITLE_NFO)
656 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
657 movieTitle = tag->GetTitle();
658 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
661 std::string identifierType;
662 std::string identifier;
663 long lResult = -1;
664 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
666 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
667 if (GetDetails(pItem, uniqueIDs, url, info2,
668 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
669 ? loader.get()
670 : nullptr,
671 pDlgProgress))
673 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
674 return INFO_ERROR;
676 if (fetchEpisodes)
678 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
679 if (ret == INFO_ADDED)
681 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
682 return INFO_ADDED;
685 return INFO_ADDED;
689 if (pURL && pURL->HasUrls())
690 url = *pURL;
691 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
692 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
694 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
695 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
696 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
698 if (GetDetails(pItem, {}, url, info2,
699 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
700 ? loader.get()
701 : nullptr,
702 pDlgProgress))
704 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
705 return INFO_ERROR;
707 if (fetchEpisodes)
709 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
710 if (ret == INFO_ADDED)
711 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
713 return INFO_ADDED;
716 CInfoScanner::INFO_RET
717 CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem,
718 bool bDirNames,
719 ScraperPtr &info2,
720 bool useLocal,
721 CScraperUrl* pURL,
722 CGUIDialogProgress* pDlgProgress)
724 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
725 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
726 return INFO_NOT_NEEDED;
728 if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
729 return INFO_CANCELLED;
731 if (m_database.HasMovieInfo(pItem->GetDynPath()))
732 return INFO_HAVE_ALREADY;
734 if (m_handle)
735 m_handle->SetText(pItem->GetMovieName(bDirNames));
737 CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
738 CScraperUrl scrUrl;
739 // handle .nfo files
740 std::unique_ptr<IVideoInfoTagLoader> loader;
741 if (useLocal)
743 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
744 if (loader)
746 pItem->GetVideoInfoTag()->Reset();
747 result = loader->Load(*pItem->GetVideoInfoTag(), false);
750 if (result == CInfoScanner::FULL_NFO)
752 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, true);
753 if (dbId < 0)
754 return INFO_ERROR;
755 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
756 return INFO_HAVE_ALREADY;
757 return INFO_ADDED;
759 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
761 scrUrl = loader->ScraperUrl();
762 pURL = &scrUrl;
765 CScraperUrl url;
766 int retVal = 0;
767 std::string movieTitle = pItem->GetMovieName(bDirNames);
768 int movieYear = -1; // hint that movie title was not found
769 if (result == CInfoScanner::TITLE_NFO)
771 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
772 movieTitle = tag->GetTitle();
773 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
776 std::string identifierType;
777 std::string identifier;
778 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
780 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
781 if (GetDetails(pItem, uniqueIDs, url, info2,
782 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
783 ? loader.get()
784 : nullptr,
785 pDlgProgress))
787 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
788 if (dbId < 0)
789 return INFO_ERROR;
790 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
791 return INFO_HAVE_ALREADY;
792 return INFO_ADDED;
796 if (pURL && pURL->HasUrls())
797 url = *pURL;
798 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
799 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
801 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
802 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
804 if (GetDetails(pItem, {}, url, info2,
805 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
806 ? loader.get()
807 : nullptr,
808 pDlgProgress))
810 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
811 if (dbId < 0)
812 return INFO_ERROR;
813 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
814 return INFO_HAVE_ALREADY;
815 return INFO_ADDED;
817 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
818 return INFO_NOT_FOUND;
821 CInfoScanner::INFO_RET
822 CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem,
823 bool bDirNames,
824 ScraperPtr &info2,
825 bool useLocal,
826 CScraperUrl* pURL,
827 CGUIDialogProgress* pDlgProgress)
829 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
830 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
831 return INFO_NOT_NEEDED;
833 if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
834 return INFO_CANCELLED;
836 if (m_database.HasMusicVideoInfo(pItem->GetPath()))
837 return INFO_HAVE_ALREADY;
839 if (m_handle)
840 m_handle->SetText(pItem->GetMovieName(bDirNames));
842 CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
843 CScraperUrl scrUrl;
844 // handle .nfo files
845 std::unique_ptr<IVideoInfoTagLoader> loader;
846 if (useLocal)
848 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
849 if (loader)
851 pItem->GetVideoInfoTag()->Reset();
852 result = loader->Load(*pItem->GetVideoInfoTag(), false);
855 if (result == CInfoScanner::FULL_NFO)
857 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
858 return INFO_ERROR;
859 return INFO_ADDED;
861 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
863 scrUrl = loader->ScraperUrl();
864 pURL = &scrUrl;
867 CScraperUrl url;
868 int retVal = 0;
869 std::string movieTitle = pItem->GetMovieName(bDirNames);
870 int movieYear = -1; // hint that movie title was not found
871 if (result == CInfoScanner::TITLE_NFO)
873 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
874 movieTitle = tag->GetTitle();
875 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
878 std::string identifierType;
879 std::string identifier;
880 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
882 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
883 if (GetDetails(pItem, uniqueIDs, url, info2,
884 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
885 ? loader.get()
886 : nullptr,
887 pDlgProgress))
889 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
890 return INFO_ERROR;
891 return INFO_ADDED;
895 if (pURL && pURL->HasUrls())
896 url = *pURL;
897 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
898 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
900 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
901 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
903 if (GetDetails(pItem, {}, url, info2,
904 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
905 ? loader.get()
906 : nullptr,
907 pDlgProgress))
909 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
910 return INFO_ERROR;
911 return INFO_ADDED;
913 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
914 return INFO_NOT_FOUND;
917 CInfoScanner::INFO_RET
918 CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item,
919 long showID,
920 const ADDON::ScraperPtr &scraper,
921 bool useLocal,
922 CGUIDialogProgress *progress)
924 // enumerate episodes
925 EPISODELIST files;
926 if (!EnumerateSeriesFolder(item, files))
927 return INFO_HAVE_ALREADY;
928 if (files.empty()) // no update or no files
929 return INFO_NOT_NEEDED;
931 if (m_bStop || (progress && progress->IsCanceled()))
932 return INFO_CANCELLED;
934 CVideoInfoTag showInfo;
935 m_database.GetTvShowInfo("", showInfo, showID);
936 INFO_RET ret = OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
938 if (ret == INFO_ADDED)
940 std::map<int, std::map<std::string, std::string>> seasonArt;
941 m_database.GetTvShowSeasonArt(showID, seasonArt);
943 bool updateSeasonArt = false;
944 for (std::map<int, std::map<std::string, std::string>>::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
946 if (i->second.empty())
948 updateSeasonArt = true;
949 break;
953 if (updateSeasonArt)
955 if (!item->IsPlugin() || scraper->ID() != "metadata.local")
957 CVideoInfoDownloader loader(scraper);
958 loader.GetArtwork(showInfo);
960 GetSeasonThumbs(showInfo, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !item->IsPlugin());
961 for (std::map<int, std::map<std::string, std::string> >::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
963 int seasonID = m_database.AddSeason(showID, i->first);
964 m_database.SetArtForItem(seasonID, MediaTypeSeason, i->second);
968 return ret;
971 bool CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
973 CFileItemList items;
974 const std::vector<std::string> &regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps;
976 bool bSkip = false;
978 if (item->m_bIsFolder)
981 * Note: DoScan() will not remove this path as it's not recursing for tvshows.
982 * Remove this path from the list we're processing in order to avoid hitting
983 * it twice in the main loop.
985 std::set<std::string>::iterator it = m_pathsToScan.find(item->GetPath());
986 if (it != m_pathsToScan.end())
987 m_pathsToScan.erase(it);
989 if (HasNoMedia(item->GetPath()))
990 return true;
992 std::string hash, dbHash;
993 bool allowEmptyHash = false;
994 if (item->IsPlugin())
996 // if plugin has already calculated a hash for directory contents - use it
997 // in this case we don't need to get directory listing from plugin for hash checking
998 if (item->HasProperty("hash"))
1000 hash = item->GetProperty("hash").asString();
1001 allowEmptyHash = true;
1004 else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash)
1005 hash = GetRecursiveFastHash(item->GetPath(), regexps);
1007 if (m_database.GetPathHash(item->GetPath(), dbHash) && (allowEmptyHash || !hash.empty()) && StringUtils::EqualsNoCase(dbHash, hash))
1009 // fast hashes match - no need to process anything
1010 bSkip = true;
1013 // fast hash cannot be computed or we need to rescan. fetch the listing.
1014 if (!bSkip)
1016 int flags = DIR_FLAG_DEFAULTS;
1017 if (!hash.empty())
1018 flags |= DIR_FLAG_NO_FILE_INFO;
1020 // Listing that ignores files inside and below folders containing .nomedia files.
1021 CDirectory::EnumerateDirectory(
1022 item->GetPath(), [&items](const std::shared_ptr<CFileItem>& item) { items.Add(item); },
1023 [this](const std::shared_ptr<CFileItem>& folder)
1024 { return !HasNoMedia(folder->GetPath()); },
1025 true, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), flags);
1027 // fast hash failed - compute slow one
1028 if (hash.empty())
1030 GetPathHash(items, hash);
1031 if (StringUtils::EqualsNoCase(dbHash, hash))
1033 // slow hashes match - no need to process anything
1034 bSkip = true;
1039 if (bSkip)
1041 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change",
1042 CURL::GetRedacted(item->GetPath()));
1043 // update our dialog with our progress
1044 if (m_handle)
1045 OnDirectoryScanned(item->GetPath());
1046 return false;
1049 if (dbHash.empty())
1050 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
1051 CURL::GetRedacted(item->GetPath()));
1052 else
1053 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
1054 CURL::GetRedacted(item->GetPath()), dbHash, hash);
1056 if (m_bClean)
1058 m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
1059 m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
1061 item->SetProperty("hash", hash);
1063 else
1065 CFileItemPtr newItem(new CFileItem(*item));
1066 items.Add(newItem);
1070 stack down any dvd folders
1071 need to sort using the full path since this is a collapsed recursive listing of all subdirs
1072 video_ts.ifo files should sort at the top of a dvd folder in ascending order
1074 /foo/bar/video_ts.ifo
1075 /foo/bar/vts_x_y.ifo
1076 /foo/bar/vts_x_y.vob
1079 // since we're doing this now anyway, should other items be stacked?
1080 items.Sort(SortByPath, SortOrderAscending);
1082 // If found VIDEO_TS.IFO or INDEX.BDMV then we are dealing with Blu-ray or DVD files on disc
1083 // somewhere in the directory tree. Assume that all other files/folders in the same folder
1084 // with VIDEO_TS or BDMV can be ignored.
1085 // THere can be a BACKUP/INDEX.BDMV which needs to be ignored (and broke the old while loop here)
1087 // Get folders to remove
1088 std::vector<std::string> foldersToRemove;
1089 for (const auto& item : items)
1091 const std::string file = StringUtils::ToUpper(item->GetPath());
1092 if (file.find("VIDEO_TS.IFO") != std::string::npos)
1093 foldersToRemove.emplace_back(StringUtils::ToUpper(URIUtils::GetDirectory(file)));
1094 if (file.find("INDEX.BDMV") != std::string::npos &&
1095 file.find("BACKUP/INDEX.BDMV") == std::string::npos)
1096 foldersToRemove.emplace_back(
1097 StringUtils::ToUpper(URIUtils::GetParentPath(URIUtils::GetDirectory(file))));
1100 // Remove folders
1101 items.erase(
1102 std::remove_if(items.begin(), items.end(),
1103 [&](const CFileItemPtr& i)
1105 const std::string fileAndPath(StringUtils::ToUpper(i->GetPath()));
1106 std::string file;
1107 std::string path;
1108 URIUtils::Split(fileAndPath, path, file);
1109 return (std::count_if(foldersToRemove.begin(), foldersToRemove.end(),
1110 [&](const std::string& removePath)
1111 { return path.rfind(removePath, 0) == 0; }) > 0) &&
1112 file != "VIDEO_TS.IFO" &&
1113 (file != "INDEX.BDMV" ||
1114 fileAndPath.find("BACKUP/INDEX.BDMV") != std::string::npos);
1116 items.end());
1118 // enumerate
1119 for (int i=0;i<items.Size();++i)
1121 if (items[i]->m_bIsFolder)
1122 continue;
1123 std::string strPath = URIUtils::GetDirectory(items[i]->GetPath());
1124 URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
1126 if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strPath), "sample"))
1127 continue;
1129 // Discard all exclude files defined by regExExcludes
1130 if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
1131 continue;
1134 * Check if the media source has already set the season and episode or original air date in
1135 * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
1136 * any false positive matches.
1138 if (ProcessItemByVideoInfoTag(items[i].get(), episodeList))
1139 continue;
1141 if (!EnumerateEpisodeItem(items[i].get(), episodeList))
1142 CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file {}", CURL::GetRedacted(items[i]->GetPath()));
1144 return true;
1147 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
1149 if (!item->HasVideoInfoTag())
1150 return false;
1152 const CVideoInfoTag* tag = item->GetVideoInfoTag();
1153 bool isValid = false;
1155 * First check the season and episode number. This takes precedence over the original air
1156 * date and episode title. Must be a valid season and episode number combination.
1158 if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
1159 isValid = true;
1161 // episode 0 with non-zero season is valid! (e.g. prequel episode)
1162 if (item->IsPlugin() && tag->m_iSeason > 0 && tag->m_iEpisode >= 0)
1163 isValid = true;
1165 if (isValid)
1167 EPISODE episode;
1168 episode.strPath = item->GetPath();
1169 episode.iSeason = tag->m_iSeason;
1170 episode.iEpisode = tag->m_iEpisode;
1171 episode.isFolder = false;
1172 // save full item for plugin source
1173 if (item->IsPlugin())
1174 episode.item = std::make_shared<CFileItem>(*item);
1175 episodeList.push_back(episode);
1176 CLog::Log(LOGDEBUG, "{} - found match for: {}. Season {}, Episode {}", __FUNCTION__,
1177 CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode);
1178 return true;
1182 * Next preference is the first aired date. If it exists use that for matching the TV Show
1183 * information. Also set the title in case there are multiple matches for the first aired date.
1185 if (tag->m_firstAired.IsValid())
1187 EPISODE episode;
1188 episode.strPath = item->GetPath();
1189 episode.strTitle = tag->m_strTitle;
1190 episode.isFolder = false;
1192 * Set season and episode to -1 to indicate to use the aired date.
1194 episode.iSeason = -1;
1195 episode.iEpisode = -1;
1197 * The first aired date string must be parseable.
1199 episode.cDate = item->GetVideoInfoTag()->m_firstAired;
1200 episodeList.push_back(episode);
1201 CLog::Log(LOGDEBUG, "{} - found match for: '{}', firstAired: '{}' = '{}', title: '{}'",
1202 __FUNCTION__, CURL::GetRedacted(episode.strPath),
1203 tag->m_firstAired.GetAsDBDateTime(), episode.cDate.GetAsLocalizedDate(),
1204 episode.strTitle);
1205 return true;
1209 * Next preference is the episode title. If it exists use that for matching the TV Show
1210 * information.
1212 if (!tag->m_strTitle.empty())
1214 EPISODE episode;
1215 episode.strPath = item->GetPath();
1216 episode.strTitle = tag->m_strTitle;
1217 episode.isFolder = false;
1219 * Set season and episode to -1 to indicate to use the title.
1221 episode.iSeason = -1;
1222 episode.iEpisode = -1;
1223 episodeList.push_back(episode);
1224 CLog::Log(LOGDEBUG, "{} - found match for: '{}', title: '{}'", __FUNCTION__,
1225 CURL::GetRedacted(episode.strPath), episode.strTitle);
1226 return true;
1230 * There is no further episode information available if both the season and episode number have
1231 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
1232 * to the episode list.
1234 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
1236 CLog::Log(LOGDEBUG,
1237 "{} - found exclusion match for: {}. Both Season and Episode are 0. Item will be "
1238 "ignored for scanning.",
1239 __FUNCTION__, CURL::GetRedacted(item->GetPath()));
1240 return true;
1243 return false;
1246 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
1248 SETTINGS_TVSHOWLIST expression = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowEnumRegExps;
1250 std::string strLabel;
1252 // remove path to main file if it's a bd or dvd folder to regex the right (folder) name
1253 if (item->IsOpticalMediaFile())
1255 strLabel = item->GetLocalMetadataPath();
1256 URIUtils::RemoveSlashAtEnd(strLabel);
1258 else
1259 strLabel = item->GetPath();
1261 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
1262 strLabel = CURL::Decode(CURL::GetRedacted(strLabel));
1264 for (unsigned int i=0;i<expression.size();++i)
1266 CRegExp reg(true, CRegExp::autoUtf8);
1267 if (!reg.RegComp(expression[i].regexp))
1268 continue;
1270 int regexppos, regexp2pos;
1271 //CLog::Log(LOGDEBUG,"running expression {} on {}",expression[i].regexp,strLabel);
1272 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
1273 continue;
1275 EPISODE episode;
1276 episode.strPath = item->GetPath();
1277 episode.iSeason = -1;
1278 episode.iEpisode = -1;
1279 episode.cDate.SetValid(false);
1280 episode.isFolder = false;
1282 bool byDate = expression[i].byDate ? true : false;
1283 bool byTitle = expression[i].byTitle;
1284 int defaultSeason = expression[i].defaultSeason;
1286 if (byDate)
1288 if (!GetAirDateFromRegExp(reg, episode))
1289 continue;
1291 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match {} ({}) [{}]",
1292 CURL::GetRedacted(episode.strPath), episode.cDate.GetAsLocalizedDate(),
1293 expression[i].regexp);
1295 else if (byTitle)
1297 if (!GetEpisodeTitleFromRegExp(reg, episode))
1298 continue;
1300 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found title based match {} ({}) [{}]",
1301 CURL::GetRedacted(episode.strPath), episode.strTitle, expression[i].regexp);
1303 else
1305 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
1306 continue;
1308 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match {} (s{}e{}) [{}]",
1309 CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode,
1310 expression[i].regexp);
1313 // Grab the remainder from first regexp run
1314 // as second run might modify or empty it.
1315 std::string remainder(reg.GetMatch(3));
1318 * Check if the files base path is a dedicated folder that contains
1319 * only this single episode. If season and episode match with the
1320 * actual media file, we set episode.isFolder to true.
1322 std::string strBasePath = item->GetBaseMoviePath(true);
1323 URIUtils::RemoveSlashAtEnd(strBasePath);
1324 strBasePath = URIUtils::GetFileName(strBasePath);
1326 if (reg.RegFind(strBasePath.c_str()) > -1)
1328 EPISODE parent;
1329 if (byDate)
1331 GetAirDateFromRegExp(reg, parent);
1332 if (episode.cDate == parent.cDate)
1333 episode.isFolder = true;
1335 else
1337 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
1338 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
1339 episode.isFolder = true;
1343 // add what we found by now
1344 episodeList.push_back(episode);
1346 CRegExp reg2(true, CRegExp::autoUtf8);
1347 // check the remainder of the string for any further episodes.
1348 if (!byDate && reg2.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowMultiPartEnumRegExp))
1350 int offset = 0;
1352 // we want "long circuit" OR below so that both offsets are evaluated
1353 while (static_cast<int>((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) |
1354 static_cast<int>((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
1356 if (((regexppos <= regexp2pos) && regexppos != -1) ||
1357 (regexppos >= 0 && regexp2pos == -1))
1359 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
1361 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season {}, multipart episode {} [{}]",
1362 episode.iSeason, episode.iEpisode,
1363 CServiceBroker::GetSettingsComponent()
1364 ->GetAdvancedSettings()
1365 ->m_tvshowMultiPartEnumRegExp);
1367 episodeList.push_back(episode);
1368 remainder = reg.GetMatch(3);
1369 offset = 0;
1371 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
1372 (regexp2pos >= 0 && regexppos == -1))
1374 episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
1375 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode {} [{}]",
1376 episode.iEpisode,
1377 CServiceBroker::GetSettingsComponent()
1378 ->GetAdvancedSettings()
1379 ->m_tvshowMultiPartEnumRegExp);
1380 episodeList.push_back(episode);
1381 offset += regexp2pos + reg2.GetFindLen();
1385 return true;
1387 return false;
1390 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp &reg, EPISODE &episodeInfo, int defaultSeason)
1392 std::string season(reg.GetMatch(1));
1393 std::string episode(reg.GetMatch(2));
1395 if (!season.empty() || !episode.empty())
1397 char* endptr = NULL;
1398 if (season.empty() && !episode.empty())
1399 { // no season specified -> assume defaultSeason
1400 episodeInfo.iSeason = defaultSeason;
1401 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
1402 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1404 else if (!season.empty() && episode.empty())
1405 { // no episode specification -> assume defaultSeason
1406 episodeInfo.iSeason = defaultSeason;
1407 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
1408 episodeInfo.iEpisode = atoi(season.c_str());
1410 else
1411 { // season and episode specified
1412 episodeInfo.iSeason = atoi(season.c_str());
1413 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1415 if (endptr)
1417 if (isalpha(*endptr))
1418 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1419 else if (*endptr == '.')
1420 episodeInfo.iSubepisode = atoi(endptr+1);
1422 return true;
1424 return false;
1427 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp &reg, EPISODE &episodeInfo)
1429 std::string param1(reg.GetMatch(1));
1430 std::string param2(reg.GetMatch(2));
1431 std::string param3(reg.GetMatch(3));
1433 if (!param1.empty() && !param2.empty() && !param3.empty())
1435 // regular expression by date
1436 int len1 = param1.size();
1437 int len2 = param2.size();
1438 int len3 = param3.size();
1440 if (len1==4 && len2==2 && len3==2)
1442 // yyyy mm dd format
1443 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1445 else if (len1==2 && len2==2 && len3==4)
1447 // mm dd yyyy format
1448 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1451 return episodeInfo.cDate.IsValid();
1454 bool CVideoInfoScanner::GetEpisodeTitleFromRegExp(CRegExp& reg, EPISODE& episodeInfo)
1456 std::string param1(reg.GetMatch(1));
1458 if (!param1.empty())
1460 episodeInfo.strTitle = param1;
1461 return true;
1463 return false;
1466 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1468 // ensure our database is open (this can get called via other classes)
1469 if (!m_database.Open())
1470 return -1;
1472 if (!libraryImport)
1473 GetArtwork(pItem, content, videoFolder, useLocal && !pItem->IsPlugin(), showInfo ? showInfo->m_strPath : "");
1475 // ensure the art map isn't completely empty by specifying an empty thumb
1476 std::map<std::string, std::string> art = pItem->GetArt();
1477 if (art.empty())
1478 art["thumb"] = "";
1480 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1481 if (movieDetails.m_basePath.empty())
1482 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1483 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1485 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1487 if (pItem->m_bIsFolder)
1488 movieDetails.m_strPath = pItem->GetPath();
1490 std::string strTitle(movieDetails.m_strTitle);
1492 if (showInfo && content == CONTENT_TVSHOWS)
1494 strTitle = StringUtils::Format("{} - {}x{} - {}", showInfo->m_strTitle,
1495 movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle);
1498 /* As HasStreamDetails() returns true for TV shows (because the scraper calls SetVideoInfoTag()
1499 * directly to set the duration) a better test is just to see if we have any common flag info
1500 * missing. If we have already read an nfo file then this data should be populated, otherwise
1501 * get it from the video file */
1503 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
1504 CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
1506 const auto& strmdetails = movieDetails.m_streamDetails;
1507 if (strmdetails.GetVideoCodec(1).empty() || strmdetails.GetVideoHeight(1) == 0 ||
1508 strmdetails.GetVideoWidth(1) == 0 || strmdetails.GetVideoDuration(1) == 0)
1511 CDVDFileInfo::GetFileStreamDetails(pItem);
1512 CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}",
1513 CURL::GetRedacted(pItem->GetPath()));
1517 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to {}:{}", TranslateContent(content), CURL::GetRedacted(pItem->GetPath()));
1518 long lResult = -1;
1520 if (content == CONTENT_MOVIES)
1522 // find local trailer first
1523 std::string strTrailer = pItem->FindTrailer();
1524 if (!strTrailer.empty())
1525 movieDetails.m_strTrailer = strTrailer;
1527 // Deal with 'Disc n' subdirectories
1528 const std::string discNum{
1529 CUtil::GetDiscNumberFromPath(URIUtils::GetParentPath(movieDetails.m_strFileNameAndPath))};
1530 if (!discNum.empty())
1532 if (movieDetails.m_set.title.empty())
1534 const std::string setName{m_database.GetSetByNameLike(movieDetails.m_strTitle)};
1535 if (!setName.empty())
1537 // Add movie to existing set
1538 movieDetails.SetSet(setName);
1540 else
1542 // Create set, then add movie to the set
1543 const int idSet{m_database.AddSet(movieDetails.m_strTitle)};
1544 m_database.SetArtForItem(idSet, MediaTypeVideoCollection, art);
1545 movieDetails.SetSet(movieDetails.m_strTitle);
1549 // Add '(Disc n)' to title (in local language)
1550 movieDetails.m_strTitle =
1551 StringUtils::Format(g_localizeStrings.Get(29995), movieDetails.m_strTitle, discNum);
1554 lResult = m_database.SetDetailsForMovie(movieDetails, art);
1555 movieDetails.m_iDbId = lResult;
1556 movieDetails.m_type = MediaTypeMovie;
1558 // setup links to shows if the linked shows are in the db
1559 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1561 CFileItemList items;
1562 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1563 if (items.Size())
1564 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1565 else
1566 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie {} to show {}",
1567 movieDetails.m_strTitle, movieDetails.m_showLink[i]);
1570 else if (content == CONTENT_TVSHOWS)
1572 if (pItem->m_bIsFolder)
1575 multipaths are not stored in the database, so in the case we have one,
1576 we split the paths, and compute the parent paths in each case.
1578 std::vector<std::string> multipath;
1579 if (!URIUtils::IsMultiPath(pItem->GetPath()) || !CMultiPathDirectory::GetPaths(pItem->GetPath(), multipath))
1580 multipath.push_back(pItem->GetPath());
1581 std::vector<std::pair<std::string, std::string> > paths;
1582 for (std::vector<std::string>::const_iterator i = multipath.begin(); i != multipath.end(); ++i)
1583 paths.emplace_back(*i, URIUtils::GetParentPath(*i));
1585 std::map<int, std::map<std::string, std::string> > seasonArt;
1587 if (!libraryImport)
1588 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !pItem->IsPlugin());
1590 lResult = m_database.SetDetailsForTvShow(paths, movieDetails, art, seasonArt);
1591 movieDetails.m_iDbId = lResult;
1592 movieDetails.m_type = MediaTypeTvShow;
1594 else
1596 // we add episode then set details, as otherwise set details will delete the
1597 // episode then add, which breaks multi-episode files.
1598 int idShow = showInfo ? showInfo->m_iDbId : -1;
1599 int idEpisode = m_database.AddNewEpisode(idShow, movieDetails);
1600 lResult = m_database.SetDetailsForEpisode(movieDetails, art, idShow, idEpisode);
1601 movieDetails.m_iDbId = lResult;
1602 movieDetails.m_type = MediaTypeEpisode;
1603 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1604 if (movieDetails.m_EpBookmark.timeInSeconds > 0)
1606 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1607 movieDetails.m_EpBookmark.seasonNumber = movieDetails.m_iSeason;
1608 movieDetails.m_EpBookmark.episodeNumber = movieDetails.m_iEpisode;
1609 m_database.AddBookMarkForEpisode(movieDetails, movieDetails.m_EpBookmark);
1613 else if (content == CONTENT_MUSICVIDEOS)
1615 lResult = m_database.SetDetailsForMusicVideo(movieDetails, art);
1616 movieDetails.m_iDbId = lResult;
1617 movieDetails.m_type = MediaTypeMusicVideo;
1620 if (!pItem->m_bIsFolder)
1622 const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
1623 if ((libraryImport || advancedSettings->m_bVideoLibraryImportWatchedState) &&
1624 (movieDetails.IsPlayCountSet() || movieDetails.m_lastPlayed.IsValid()))
1625 m_database.SetPlayCount(*pItem, movieDetails.GetPlayCount(), movieDetails.m_lastPlayed);
1627 if ((libraryImport || advancedSettings->m_bVideoLibraryImportResumePoint) &&
1628 movieDetails.GetResumePoint().IsSet())
1629 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.GetResumePoint(), CBookmark::RESUME);
1632 m_database.Close();
1634 CFileItemPtr itemCopy = std::make_shared<CFileItem>(*pItem);
1635 CVariant data;
1636 data["added"] = true;
1637 if (m_bRunning)
1638 data["transaction"] = true;
1639 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate",
1640 itemCopy, data);
1641 return lResult;
1644 std::string ContentToMediaType(CONTENT_TYPE content, bool folder)
1646 switch (content)
1648 case CONTENT_MOVIES:
1649 return MediaTypeMovie;
1650 case CONTENT_MUSICVIDEOS:
1651 return MediaTypeMusicVideo;
1652 case CONTENT_TVSHOWS:
1653 return folder ? MediaTypeTvShow : MediaTypeEpisode;
1654 default:
1655 return "";
1659 VideoDbContentType ContentToVideoDbType(CONTENT_TYPE content)
1661 switch (content)
1663 case CONTENT_MOVIES:
1664 return VideoDbContentType::MOVIES;
1665 case CONTENT_MUSICVIDEOS:
1666 return VideoDbContentType::MUSICVIDEOS;
1667 case CONTENT_TVSHOWS:
1668 return VideoDbContentType::EPISODES;
1669 default:
1670 return VideoDbContentType::UNKNOWN;
1674 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1676 std::string type = "thumb";
1677 if (width*5 < height*4)
1678 type = "poster";
1679 else if (width*1 > height*4)
1680 type = "banner";
1681 return type;
1684 std::string CVideoInfoScanner::GetMovieSetInfoFolder(const std::string& setTitle)
1686 if (setTitle.empty())
1687 return "";
1688 std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
1689 CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER);
1690 if (path.empty())
1691 return "";
1692 path = URIUtils::AddFileToFolder(path, CUtil::MakeLegalFileName(setTitle, LEGAL_WIN32_COMPAT));
1693 URIUtils::AddSlashAtEnd(path);
1694 CLog::Log(LOGDEBUG,
1695 "VideoInfoScanner: Looking for local artwork for movie set '{}' in folder '{}'",
1696 setTitle,
1697 CURL::GetRedacted(path));
1698 return CDirectory::Exists(path) ? path : "";
1701 void CVideoInfoScanner::AddLocalItemArtwork(CGUIListItem::ArtMap& itemArt,
1702 const std::vector<std::string>& wantedArtTypes, const std::string& itemPath,
1703 bool addAll, bool exactName)
1705 std::string path = URIUtils::GetDirectory(itemPath);
1706 if (path.empty())
1707 return;
1709 CFileItemList availableArtFiles;
1710 CDirectory::GetDirectory(path, availableArtFiles,
1711 CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
1712 DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
1714 std::string baseFilename = URIUtils::GetFileName(itemPath);
1715 if (!baseFilename.empty())
1717 URIUtils::RemoveExtension(baseFilename);
1718 baseFilename.append("-");
1721 for (const auto& artFile : availableArtFiles)
1723 std::string candidate = URIUtils::GetFileName(artFile->GetPath());
1725 bool matchesFilename =
1726 !baseFilename.empty() && StringUtils::StartsWith(candidate, baseFilename);
1727 if (!baseFilename.empty() && !matchesFilename)
1728 continue;
1730 if (matchesFilename)
1731 candidate.erase(0, baseFilename.length());
1732 URIUtils::RemoveExtension(candidate);
1733 StringUtils::ToLower(candidate);
1735 // move 'folder' to thumb / poster / banner based on aspect ratio
1736 // if such artwork doesn't already exist
1737 if (!matchesFilename && StringUtils::EqualsNoCase(candidate, "folder") &&
1738 !CVideoThumbLoader::IsArtTypeInWhitelist("folder", wantedArtTypes, exactName))
1740 // cache the image to determine sizing
1741 CTextureDetails details;
1742 if (CServiceBroker::GetTextureCache()->CacheImage(artFile->GetPath(), details))
1744 candidate = GetArtTypeFromSize(details.width, details.height);
1745 if (itemArt.find(candidate) != itemArt.end())
1746 continue;
1750 if ((addAll && CVideoThumbLoader::IsValidArtType(candidate)) ||
1751 CVideoThumbLoader::IsArtTypeInWhitelist(candidate, wantedArtTypes, exactName))
1753 itemArt[candidate] = artFile->GetPath();
1758 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1760 int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1761 CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
1762 if (artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE)
1763 return;
1765 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1766 movieDetails.m_fanart.Unpack();
1767 movieDetails.m_strPictureURL.Parse();
1769 CGUIListItem::ArtMap art = pItem->GetArt();
1771 // get and cache thumb images
1772 std::string mediaType = ContentToMediaType(content, pItem->m_bIsFolder);
1773 std::vector<std::string> artTypes = CVideoThumbLoader::GetArtTypes(mediaType);
1774 bool moviePartOfSet = content == CONTENT_MOVIES && !movieDetails.m_set.title.empty();
1775 std::vector<std::string> movieSetArtTypes;
1776 if (moviePartOfSet)
1778 movieSetArtTypes = CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection);
1779 for (const std::string& artType : movieSetArtTypes)
1780 artTypes.push_back("set." + artType);
1782 bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
1783 bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
1784 // find local art
1785 if (useLocal)
1787 if (!pItem->SkipLocalArt())
1789 if (bApplyToDir && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
1791 std::string filename = pItem->GetLocalArtBaseFilename();
1792 std::string directory = URIUtils::GetDirectory(filename);
1793 if (filename != directory)
1794 AddLocalItemArtwork(art, artTypes, directory, addAll, exactName);
1796 AddLocalItemArtwork(art, artTypes, pItem->GetLocalArtBaseFilename(), addAll, exactName);
1799 if (moviePartOfSet)
1801 std::string movieSetInfoPath = GetMovieSetInfoFolder(movieDetails.m_set.title);
1802 if (!movieSetInfoPath.empty())
1804 CGUIListItem::ArtMap movieSetArt;
1805 AddLocalItemArtwork(movieSetArt, movieSetArtTypes, movieSetInfoPath, addAll, exactName);
1806 for (const auto& artItem : movieSetArt)
1808 art["set." + artItem.first] = artItem.second;
1814 // find embedded art
1815 if (pItem->HasVideoInfoTag() && !pItem->GetVideoInfoTag()->m_coverArt.empty())
1817 for (auto& it : pItem->GetVideoInfoTag()->m_coverArt)
1819 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(it.m_type, artTypes, exactName)) &&
1820 art.find(it.m_type) == art.end())
1822 std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(),
1823 "video_" + it.m_type);
1824 art.insert(std::make_pair(it.m_type, thumb));
1829 // add online fanart (treated separately due to it being stored in m_fanart)
1830 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist("fanart", artTypes, exactName)) &&
1831 art.find("fanart") == art.end())
1833 std::string fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1834 if (!fanart.empty())
1835 art.insert(std::make_pair("fanart", fanart));
1838 // add online art
1839 for (const auto& url : pItem->GetVideoInfoTag()->m_strPictureURL.GetUrls())
1841 if (url.m_type != CScraperUrl::UrlType::General)
1842 continue;
1843 std::string aspect = url.m_aspect;
1844 if (aspect.empty())
1845 // Backward compatibility with Kodi 11 Eden NFO files
1846 aspect = mediaType == MediaTypeEpisode ? "thumb" : "poster";
1848 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
1849 art.find(aspect) == art.end())
1851 std::string image = GetImage(url, pItem->GetPath());
1852 if (!image.empty())
1853 art.insert(std::make_pair(aspect, image));
1857 if (art.find("thumb") == art.end() &&
1858 CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
1859 CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB) &&
1860 CDVDFileInfo::CanExtract(*pItem))
1862 art["thumb"] = CVideoThumbLoader::GetEmbeddedThumbURL(*pItem);
1865 for (const auto& artType : artTypes)
1867 if (art.find(artType) != art.end())
1868 CServiceBroker::GetTextureCache()->BackgroundCacheImage(art[artType]);
1871 pItem->SetArt(art);
1873 // parent folder to apply the thumb to and to search for local actor thumbs
1874 std::string parentDir = URIUtils::GetBasePath(pItem->GetPath());
1875 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS))
1876 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1877 if (bApplyToDir)
1878 ApplyThumbToFolder(parentDir, art["thumb"]);
1881 std::string CVideoInfoScanner::GetImage(const CScraperUrl::SUrlEntry &image, const std::string& itemPath)
1883 std::string thumb = CScraperUrl::GetThumbUrl(image);
1884 if (!thumb.empty() && thumb.find('/') == std::string::npos &&
1885 thumb.find('\\') == std::string::npos)
1887 std::string strPath = URIUtils::GetDirectory(itemPath);
1888 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1890 return thumb;
1893 CInfoScanner::INFO_RET
1894 CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files,
1895 const ADDON::ScraperPtr &scraper,
1896 bool useLocal,
1897 const CVideoInfoTag& showInfo,
1898 CGUIDialogProgress* pDlgProgress /* = NULL */)
1900 if (pDlgProgress)
1902 pDlgProgress->SetLine(1, CVariant{20361}); // Loading episode details
1903 pDlgProgress->SetPercentage(0);
1904 pDlgProgress->ShowProgressBar(true);
1905 pDlgProgress->Progress();
1908 EPISODELIST episodes;
1909 bool hasEpisodeGuide = false;
1911 int iMax = files.size();
1912 int iCurr = 1;
1913 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1915 if (pDlgProgress)
1917 pDlgProgress->SetLine(1, CVariant{20361}); // Loading episode details
1918 pDlgProgress->SetLine(2, StringUtils::Format("{} {}", g_localizeStrings.Get(20373),
1919 file->iSeason)); // Season x
1920 pDlgProgress->SetLine(3, StringUtils::Format("{} {}", g_localizeStrings.Get(20359),
1921 file->iEpisode)); // Episode y
1922 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1923 pDlgProgress->Progress();
1925 if (m_handle)
1926 m_handle->SetPercentage(100.f*iCurr++/iMax);
1928 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1929 return INFO_CANCELLED;
1931 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1933 if (m_handle)
1934 m_handle->SetText(g_localizeStrings.Get(20415));
1935 continue;
1938 CFileItem item;
1939 if (file->item)
1940 item = *file->item;
1941 else
1943 item.SetPath(file->strPath);
1944 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1947 // handle .nfo files
1948 CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
1949 CScraperUrl scrUrl;
1950 const ScraperPtr& info(scraper);
1951 std::unique_ptr<IVideoInfoTagLoader> loader;
1952 if (useLocal)
1954 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(item, info, false));
1955 if (loader)
1957 // no reset here on purpose
1958 result = loader->Load(*item.GetVideoInfoTag(), false);
1961 if (result == CInfoScanner::FULL_NFO)
1963 // override with episode and season number from file if available
1964 if (file->iEpisode > -1)
1966 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1967 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1969 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1970 return INFO_ERROR;
1971 continue;
1974 if (!hasEpisodeGuide)
1976 // fetch episode guide
1977 if (!showInfo.m_strEpisodeGuide.empty() && scraper->ID() != "metadata.local")
1979 CScraperUrl url;
1980 url.ParseAndAppendUrlsFromEpisodeGuide(showInfo.m_strEpisodeGuide);
1982 if (pDlgProgress)
1984 pDlgProgress->SetLine(1, CVariant{20354}); // Fetching episode guide
1985 pDlgProgress->Progress();
1988 CVideoInfoDownloader imdb(scraper);
1989 if (!imdb.GetEpisodeList(url, episodes))
1990 return INFO_NOT_FOUND;
1992 hasEpisodeGuide = true;
1996 if (episodes.empty())
1998 CLog::Log(LOGERROR,
1999 "VideoInfoScanner: Asked to lookup episode {}"
2000 " online, but we have either no episode guide or"
2001 " we are using the local scraper. Check your tvshow.nfo and make"
2002 " sure the <episodeguide> tag is in place and/or use an online"
2003 " scraper.",
2004 CURL::GetRedacted(file->strPath));
2005 continue;
2008 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
2009 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
2010 bool bFound = false;
2011 EPISODELIST::iterator guide = episodes.begin();
2012 EPISODELIST matches;
2014 for (; guide != episodes.end(); ++guide )
2016 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
2018 if (key==*guide)
2020 bFound = true;
2021 break;
2023 else if ((file->iSubepisode!=0) && (backupkey==*guide))
2025 matches.push_back(*guide);
2026 continue;
2029 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
2031 matches.push_back(*guide);
2032 continue;
2034 if (!guide->cScraperUrl.GetTitle().empty() &&
2035 StringUtils::EqualsNoCase(guide->cScraperUrl.GetTitle(), file->strTitle))
2037 bFound = true;
2038 break;
2040 if (!guide->strTitle.empty() && StringUtils::EqualsNoCase(guide->strTitle, file->strTitle))
2042 bFound = true;
2043 break;
2047 if (!bFound)
2050 * If there is only one match or there are matches but no title to compare with to help
2051 * identify the best match, then pick the first match as the best possible candidate.
2053 * Otherwise, use the title to further refine the best match.
2055 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
2057 guide = matches.begin();
2058 bFound = true;
2060 else if (!file->strTitle.empty())
2062 CLog::Log(LOGDEBUG, "VideoInfoScanner: analyzing parsed title '{}'", file->strTitle);
2063 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
2065 EPISODELIST *candidates;
2066 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
2068 minscore = 0.8; // 80% should ensure a good match.
2069 candidates = &episodes;
2071 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
2072 candidates = &matches;
2074 std::vector<std::string> titles;
2075 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
2077 auto title = guide->cScraperUrl.GetTitle();
2078 if (title.empty())
2080 title = guide->strTitle;
2082 StringUtils::ToLower(title);
2083 guide->cScraperUrl.SetTitle(title);
2084 titles.push_back(title);
2087 double matchscore;
2088 std::string loweredTitle(file->strTitle);
2089 StringUtils::ToLower(loweredTitle);
2090 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
2091 if (index >= 0 && matchscore >= minscore)
2093 guide = candidates->begin() + index;
2094 bFound = true;
2095 CLog::Log(LOGDEBUG,
2096 "{} fuzzy title match for show: '{}', title: '{}', match: '{}', score: {:f} "
2097 ">= {:f}",
2098 __FUNCTION__, showInfo.m_strTitle, file->strTitle, titles[index], matchscore,
2099 minscore);
2104 if (bFound)
2106 CVideoInfoDownloader imdb(scraper);
2107 CFileItem item;
2108 item.SetPath(file->strPath);
2109 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
2110 return INFO_NOT_FOUND; //! @todo should we just skip to the next episode?
2112 // Only set season/epnum from filename when it is not already set by a scraper
2113 if (item.GetVideoInfoTag()->m_iSeason == -1)
2114 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
2115 if (item.GetVideoInfoTag()->m_iEpisode == -1)
2116 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
2118 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
2119 return INFO_ERROR;
2121 else
2123 CLog::Log(
2124 LOGDEBUG,
2125 "{} - no match for show: '{}', season: {}, episode: {}.{}, airdate: '{}', title: '{}'",
2126 __FUNCTION__, showInfo.m_strTitle, file->iSeason, file->iEpisode, file->iSubepisode,
2127 file->cDate.GetAsLocalizedDate(), file->strTitle);
2130 return INFO_ADDED;
2133 bool CVideoInfoScanner::GetDetails(CFileItem* pItem,
2134 const std::unordered_map<std::string, std::string>& uniqueIDs,
2135 CScraperUrl& url,
2136 const ScraperPtr& scraper,
2137 IVideoInfoTagLoader* loader,
2138 CGUIDialogProgress* pDialog /* = NULL */)
2140 CVideoInfoTag movieDetails;
2142 if (m_handle && !url.GetTitle().empty())
2143 m_handle->SetText(url.GetTitle());
2145 CVideoInfoDownloader imdb(scraper);
2146 bool ret = imdb.GetDetails(uniqueIDs, url, movieDetails, pDialog);
2148 if (ret)
2150 if (loader)
2151 loader->Load(movieDetails, true);
2153 if (m_handle && url.GetTitle().empty())
2154 m_handle->SetText(movieDetails.m_strTitle);
2156 if (pDialog)
2158 if (!pDialog->HasText())
2159 pDialog->SetLine(0, CVariant{movieDetails.m_strTitle});
2160 pDialog->Progress();
2163 *pItem->GetVideoInfoTag() = movieDetails;
2164 return true;
2166 return false; // no info found, or cancelled
2169 void CVideoInfoScanner::ApplyThumbToFolder(const std::string &folder, const std::string &imdbThumb)
2171 // copy icon to folder also;
2172 if (!imdbThumb.empty())
2174 CFileItem folderItem(folder, true);
2175 CThumbLoader loader;
2176 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
2180 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash)
2182 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
2183 if (0 == items.Size()) return 0;
2184 CDigest digest{CDigest::Type::MD5};
2185 int count = 0;
2186 for (int i = 0; i < items.Size(); ++i)
2188 const CFileItemPtr pItem = items[i];
2189 digest.Update(pItem->GetPath());
2190 if (pItem->IsPlugin())
2192 // allow plugin to calculate hash itself using strings rather than binary data for size and date
2193 // according to ListItem.setInfo() documentation date format should be "d.m.Y"
2194 if (pItem->m_dwSize)
2195 digest.Update(std::to_string(pItem->m_dwSize));
2196 if (pItem->m_dateTime.IsValid())
2197 digest.Update(StringUtils::Format("{:02}.{:02}.{:04}", pItem->m_dateTime.GetDay(),
2198 pItem->m_dateTime.GetMonth(),
2199 pItem->m_dateTime.GetYear()));
2201 else
2203 digest.Update(&pItem->m_dwSize, sizeof(pItem->m_dwSize));
2204 KODI::TIME::FileTime time = pItem->m_dateTime;
2205 digest.Update(&time, sizeof(KODI::TIME::FileTime));
2207 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
2208 count++;
2210 hash = digest.Finalize();
2211 return count;
2214 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items, const std::vector<std::string> &excludes) const
2216 if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash || items.IsPlugin())
2217 return false;
2219 for (int i = 0; i < items.Size(); ++i)
2221 if (items[i]->m_bIsFolder && !CUtil::ExcludeFileOrFolder(items[i]->GetPath(), excludes))
2222 return false;
2224 return true;
2227 std::string CVideoInfoScanner::GetFastHash(const std::string &directory,
2228 const std::vector<std::string> &excludes) const
2230 CDigest digest{CDigest::Type::MD5};
2232 if (excludes.size())
2233 digest.Update(StringUtils::Join(excludes, "|"));
2235 struct __stat64 buffer;
2236 if (XFILE::CFile::Stat(directory, &buffer) == 0)
2238 int64_t time = buffer.st_mtime;
2239 if (!time)
2240 time = buffer.st_ctime;
2241 if (time)
2243 digest.Update((unsigned char *)&time, sizeof(time));
2244 return digest.Finalize();
2247 return "";
2250 std::string CVideoInfoScanner::GetRecursiveFastHash(const std::string &directory,
2251 const std::vector<std::string> &excludes) const
2253 CFileItemList items;
2254 items.Add(std::make_shared<CFileItem>(directory, true));
2255 CUtil::GetRecursiveDirsListing(directory, items, DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
2257 CDigest digest{CDigest::Type::MD5};
2259 if (excludes.size())
2260 digest.Update(StringUtils::Join(excludes, "|"));
2262 int64_t time = 0;
2263 for (int i=0; i < items.Size(); ++i)
2265 int64_t stat_time = 0;
2266 struct __stat64 buffer;
2267 if (XFILE::CFile::Stat(items[i]->GetPath(), &buffer) == 0)
2269 //! @todo some filesystems may return the mtime/ctime inline, in which case this is
2270 //! unnecessarily expensive. Consider supporting Stat() in our directory cache?
2271 stat_time = buffer.st_mtime ? buffer.st_mtime : buffer.st_ctime;
2272 time += stat_time;
2275 if (!stat_time)
2276 return "";
2279 if (time)
2281 digest.Update((unsigned char *)&time, sizeof(time));
2282 return digest.Finalize();
2284 return "";
2287 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show,
2288 std::map<int, std::map<std::string, std::string>> &seasonArt, const std::vector<std::string> &artTypes, bool useLocal)
2290 int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->
2291 GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
2292 bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
2293 bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
2294 if (useLocal)
2296 // find the maximum number of seasons we have local thumbs for
2297 int maxSeasons = 0;
2298 CFileItemList items;
2299 std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
2300 if (!show.m_strPath.empty())
2302 CDirectory::GetDirectory(show.m_strPath, items, extensions,
2303 DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE |
2304 DIR_FLAG_NO_FILE_INFO);
2306 extensions.erase(std::remove(extensions.begin(), extensions.end(), '.'), extensions.end());
2307 CRegExp reg;
2308 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z0-9]+)?\\.(" + extensions + ")"))
2310 for (const auto& item : items)
2312 std::string name = URIUtils::GetFileName(item->GetPath());
2313 if (reg.RegFind(name) > -1)
2315 int season = atoi(reg.GetMatch(1).c_str());
2316 if (season > maxSeasons)
2317 maxSeasons = season;
2321 for (int season = -1; season <= maxSeasons; season++)
2323 // skip if we already have some art
2324 std::map<int, std::map<std::string, std::string>>::const_iterator it = seasonArt.find(season);
2325 if (it != seasonArt.end() && !it->second.empty())
2326 continue;
2328 std::map<std::string, std::string> art;
2329 std::string basePath;
2330 if (season == -1)
2331 basePath = "season-all";
2332 else if (season == 0)
2333 basePath = "season-specials";
2334 else
2335 basePath = StringUtils::Format("season{:02}", season);
2337 AddLocalItemArtwork(art, artTypes,
2338 URIUtils::AddFileToFolder(show.m_strPath, basePath),
2339 addAll, exactName);
2341 seasonArt[season] = art;
2344 // add online art
2345 for (const auto& url : show.m_strPictureURL.GetUrls())
2347 if (url.m_type != CScraperUrl::UrlType::Season)
2348 continue;
2349 std::string aspect = url.m_aspect;
2350 if (aspect.empty())
2351 aspect = "thumb";
2352 std::map<std::string, std::string>& art = seasonArt[url.m_season];
2353 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
2354 art.find(aspect) == art.end())
2356 std::string image = CScraperUrl::GetThumbUrl(url);
2357 if (!image.empty())
2358 art.insert(std::make_pair(aspect, image));
2363 void CVideoInfoScanner::FetchActorThumbs(std::vector<SActorInfo>& actors, const std::string& strPath)
2365 CFileItemList items;
2366 // don't try to fetch anything local with plugin source
2367 if (!URIUtils::IsPlugin(strPath))
2369 std::string actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
2370 if (CDirectory::Exists(actorsDir))
2371 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
2372 DIR_FLAG_NO_FILE_INFO);
2374 for (std::vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
2376 if (i->thumb.empty())
2378 std::string thumbFile = i->strName;
2379 StringUtils::Replace(thumbFile, ' ', '_');
2380 for (int j = 0; j < items.Size(); j++)
2382 std::string compare = URIUtils::GetFileName(items[j]->GetPath());
2383 URIUtils::RemoveExtension(compare);
2384 if (!items[j]->m_bIsFolder && compare == thumbFile)
2386 i->thumb = items[j]->GetPath();
2387 break;
2390 if (i->thumb.empty() && !i->thumbUrl.GetFirstUrlByType().m_url.empty())
2391 i->thumb = CScraperUrl::GetThumbUrl(i->thumbUrl.GetFirstUrlByType());
2392 if (!i->thumb.empty())
2393 CServiceBroker::GetTextureCache()->BackgroundCacheImage(i->thumb);
2398 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
2400 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoScannerIgnoreErrors)
2401 return true;
2403 if (pDialog)
2405 HELPERS::ShowOKDialogText(CVariant{20448}, CVariant{20449});
2406 return false;
2408 return HELPERS::ShowYesNoDialogText(CVariant{20448}, CVariant{20450}) ==
2409 DialogResponse::CHOICE_YES;
2412 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const std::string &line1)
2414 if (progress)
2416 progress->SetHeading(CVariant{heading});
2417 progress->SetLine(0, CVariant{line1});
2418 progress->Progress();
2419 return progress->IsCanceled();
2421 return m_bStop;
2424 int CVideoInfoScanner::FindVideo(const std::string &title, int year, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
2426 MOVIELIST movielist;
2427 CVideoInfoDownloader imdb(scraper);
2428 int returncode = imdb.FindMovie(title, year, movielist, progress);
2429 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
2430 { // scraper reported an error, or we had an error and user wants to cancel the scan
2431 m_bStop = true;
2432 return -1; // cancelled
2434 if (returncode > 0 && movielist.size())
2436 url = movielist[0];
2437 return 1; // found a movie
2439 return 0; // didn't find anything
2442 bool CVideoInfoScanner::AddVideoExtras(CFileItemList& items,
2443 const CONTENT_TYPE& content,
2444 const std::string& path)
2446 int dbId = -1;
2448 // get the library item which was added previously with the specified conent type
2449 for (const auto& item : items)
2451 if (content == CONTENT_MOVIES)
2453 dbId = m_database.GetMovieId(item->GetPath());
2454 if (dbId != -1)
2456 break;
2461 if (dbId == -1)
2463 CLog::Log(LOGERROR, "VideoInfoScanner: Failed to find the library item for video extras {}",
2464 CURL::GetRedacted(path));
2465 return false;
2468 // Add video extras to library
2469 CDirectory::EnumerateDirectory(
2470 path,
2471 [this, content, dbId, path](const std::shared_ptr<CFileItem>& item)
2473 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2474 CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
2476 CDVDFileInfo::GetFileStreamDetails(item.get());
2477 CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}",
2478 CURL::GetRedacted(item->GetPath()));
2481 const std::string typeVideoVersion =
2482 CGUIDialogVideoManagerExtras::GenerateVideoExtra(path, item->GetPath());
2484 const int idVideoVersion = m_database.AddVideoVersionType(
2485 typeVideoVersion, VideoAssetTypeOwner::AUTO, VideoAssetType::EXTRA);
2487 m_database.AddExtrasVideoVersion(ContentToVideoDbType(content), dbId, idVideoVersion,
2488 *item.get());
2489 CLog::Log(LOGDEBUG, "VideoInfoScanner: Added video extras {}",
2490 CURL::GetRedacted(item->GetPath()));
2492 [](auto) { return true; }, true,
2493 CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), DIR_FLAG_DEFAULTS);
2495 return true;
2498 bool CVideoInfoScanner::ProcessVideoVersion(VideoDbContentType itemType, int dbId)
2500 return CGUIDialogVideoManagerVersions::ProcessVideoVersion(itemType, dbId);