[cosmetic] AddVideoAsset function cleanup
[xbmc.git] / xbmc / video / VideoInfoScanner.cpp
blob40f967ac9e11dfbe9effa03efe239e3a0c93af81
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 "FileItemList.h"
13 #include "GUIInfoManager.h"
14 #include "GUIUserMessages.h"
15 #include "ServiceBroker.h"
16 #include "TextureCache.h"
17 #include "URL.h"
18 #include "Util.h"
19 #include "VideoInfoDownloader.h"
20 #include "cores/VideoPlayer/DVDFileInfo.h"
21 #include "dialogs/GUIDialogExtendedProgressBar.h"
22 #include "dialogs/GUIDialogProgress.h"
23 #include "events/EventLog.h"
24 #include "events/MediaLibraryEvent.h"
25 #include "filesystem/Directory.h"
26 #include "filesystem/File.h"
27 #include "filesystem/MultiPathDirectory.h"
28 #include "filesystem/PluginDirectory.h"
29 #include "guilib/GUIComponent.h"
30 #include "guilib/GUIWindowManager.h"
31 #include "guilib/LocalizeStrings.h"
32 #include "imagefiles/ImageFileURL.h"
33 #include "interfaces/AnnouncementManager.h"
34 #include "messaging/helpers/DialogHelper.h"
35 #include "messaging/helpers/DialogOKHelper.h"
36 #include "playlists/PlayListFileItemClassify.h"
37 #include "settings/AdvancedSettings.h"
38 #include "settings/Settings.h"
39 #include "settings/SettingsComponent.h"
40 #include "tags/VideoInfoTagLoaderFactory.h"
41 #include "utils/ArtUtils.h"
42 #include "utils/Digest.h"
43 #include "utils/FileExtensionProvider.h"
44 #include "utils/RegExp.h"
45 #include "utils/StringUtils.h"
46 #include "utils/URIUtils.h"
47 #include "utils/Variant.h"
48 #include "utils/log.h"
49 #include "video/VideoFileItemClassify.h"
50 #include "video/VideoManagerTypes.h"
51 #include "video/VideoThumbLoader.h"
52 #include "video/VideoUtils.h"
53 #include "video/dialogs/GUIDialogVideoManagerExtras.h"
54 #include "video/dialogs/GUIDialogVideoManagerVersions.h"
56 #include <algorithm>
57 #include <memory>
58 #include <utility>
60 using namespace XFILE;
61 using namespace ADDON;
62 using namespace KODI::MESSAGING;
63 using namespace KODI;
65 using KODI::MESSAGING::HELPERS::DialogResponse;
66 using KODI::UTILITY::CDigest;
68 namespace KODI::VIDEO
71 CVideoInfoScanner::CVideoInfoScanner()
73 m_bStop = false;
74 m_scanAll = false;
76 const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
78 m_ignoreVideoVersions = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_IGNOREVIDEOVERSIONS);
79 m_ignoreVideoExtras = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_IGNOREVIDEOEXTRAS);
82 CVideoInfoScanner::~CVideoInfoScanner()
83 = default;
85 void CVideoInfoScanner::Process()
87 m_bStop = false;
89 try
91 const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
93 if (m_showDialog && !settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE))
95 CGUIDialogExtendedProgressBar* dialog =
96 CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
97 if (dialog)
98 m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
101 // check if we only need to perform a cleaning
102 if (m_bClean && m_pathsToScan.empty())
104 std::set<int> paths;
105 m_database.CleanDatabase(m_handle, paths, false);
107 if (m_handle)
108 m_handle->MarkFinished();
109 m_handle = NULL;
111 m_bRunning = false;
113 return;
116 auto start = std::chrono::steady_clock::now();
118 m_database.Open();
120 m_bCanInterrupt = true;
122 CLog::Log(LOGINFO, "VideoInfoScanner: Starting scan ..");
123 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
124 "OnScanStarted");
126 // Database operations should not be canceled
127 // using Interrupt() while scanning as it could
128 // result in unexpected behaviour.
129 m_bCanInterrupt = false;
131 bool bCancelled = false;
132 while (!bCancelled && !m_pathsToScan.empty())
135 * A copy of the directory path is used because the path supplied is
136 * immediately removed from the m_pathsToScan set in DoScan(). If the
137 * reference points to the entry in the set a null reference error
138 * occurs.
140 std::string directory = *m_pathsToScan.begin();
141 if (m_bStop)
143 bCancelled = true;
145 else if (!CDirectory::Exists(directory))
148 * Note that this will skip clean (if m_bClean is enabled) if the directory really
149 * doesn't exist rather than a NAS being switched off. A manual clean from settings
150 * will still pick up and remove it though.
152 CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan{}.", __FUNCTION__,
153 CURL::GetRedacted(directory), m_bClean ? " and clean" : "");
154 m_pathsToScan.erase(m_pathsToScan.begin());
156 else if (!DoScan(directory))
157 bCancelled = true;
160 if (!bCancelled)
162 if (m_bClean)
163 m_database.CleanDatabase(m_handle, m_pathsToClean, false);
164 else
166 if (m_handle)
167 m_handle->SetTitle(g_localizeStrings.Get(331));
168 m_database.Compress(false);
172 CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
173 m_database.Close();
175 auto end = std::chrono::steady_clock::now();
176 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
178 CLog::Log(LOGINFO, "VideoInfoScanner: Finished scan. Scanning for video info took {} ms",
179 duration.count());
181 catch (...)
183 CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
186 m_bRunning = false;
187 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
188 "OnScanFinished");
190 if (m_handle)
191 m_handle->MarkFinished();
192 m_handle = NULL;
195 void CVideoInfoScanner::Start(const std::string& strDirectory, bool scanAll)
197 m_strStartDir = strDirectory;
198 m_scanAll = scanAll;
199 m_pathsToScan.clear();
200 m_pathsToClean.clear();
202 m_database.Open();
203 if (strDirectory.empty())
204 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
205 // we go.
206 m_database.GetPaths(m_pathsToScan);
208 else
209 { // scan all the paths of this subtree that is in the database
210 std::vector<std::string> rootDirs;
211 if (URIUtils::IsMultiPath(strDirectory))
212 CMultiPathDirectory::GetPaths(strDirectory, rootDirs);
213 else
214 rootDirs.push_back(strDirectory);
216 for (std::vector<std::string>::const_iterator it = rootDirs.begin(); it < rootDirs.end(); ++it)
218 m_pathsToScan.insert(*it);
219 std::vector<std::pair<int, std::string>> subpaths;
220 m_database.GetSubPaths(*it, subpaths);
221 for (std::vector<std::pair<int, std::string>>::iterator it = subpaths.begin(); it < subpaths.end(); ++it)
222 m_pathsToScan.insert(it->second);
225 m_database.Close();
226 m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryCleanOnUpdate;
228 m_bRunning = true;
229 Process();
232 void CVideoInfoScanner::Stop()
234 if (m_bCanInterrupt)
235 m_database.Interrupt();
237 m_bStop = true;
240 static void OnDirectoryScanned(const std::string& strDirectory)
242 CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
243 msg.SetStringParam(strDirectory);
244 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
247 bool CVideoInfoScanner::DoScan(const std::string& strDirectory)
249 if (m_handle)
251 m_handle->SetText(g_localizeStrings.Get(20415));
255 * Remove this path from the list we're processing. This must be done prior to
256 * the check for file or folder exclusion to prevent an infinite while loop
257 * in Process().
259 std::set<std::string>::iterator it = m_pathsToScan.find(strDirectory);
260 if (it != m_pathsToScan.end())
261 m_pathsToScan.erase(it);
263 // load subfolder
264 CFileItemList items;
265 bool foundDirectly = false;
266 bool bSkip = false;
268 SScanSettings settings;
269 ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
270 CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
272 // exclude folders that match our exclude regexps
273 const std::vector<std::string> &regexps = content == CONTENT_TVSHOWS ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
274 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps;
276 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
277 return true;
279 if (HasNoMedia(strDirectory))
280 return true;
282 bool ignoreFolder = !m_scanAll && settings.noupdate;
283 if (content == CONTENT_NONE || ignoreFolder)
284 return true;
286 if (URIUtils::IsPlugin(strDirectory) && !CPluginDirectory::IsMediaLibraryScanningAllowed(TranslateContent(content), strDirectory))
288 CLog::Log(
289 LOGINFO,
290 "VideoInfoScanner: Plugin '{}' does not support media library scanning for '{}' content",
291 CURL::GetRedacted(strDirectory), TranslateContent(content));
292 return true;
295 std::string hash, dbHash;
296 if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
298 if (m_handle)
300 int str = content == CONTENT_MOVIES ? 20317:20318;
301 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name()));
304 std::string fastHash;
305 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash && !URIUtils::IsPlugin(strDirectory))
306 fastHash = GetFastHash(strDirectory, regexps);
308 if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && StringUtils::EqualsNoCase(fastHash, dbHash))
309 { // fast hashes match - no need to process anything
310 hash = fastHash;
312 else
313 { // need to fetch the folder
314 CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
315 DIR_FLAG_DEFAULTS);
316 // do not consider inner folders with .nomedia
317 items.erase(std::remove_if(items.begin(), items.end(),
318 [this](const CFileItemPtr& item) {
319 return item->m_bIsFolder && HasNoMedia(item->GetPath());
321 items.end());
322 items.Stack();
324 // check whether to re-use previously computed fast hash
325 if (!CanFastHash(items, regexps) || fastHash.empty())
326 GetPathHash(items, hash);
327 else
328 hash = fastHash;
331 if (StringUtils::EqualsNoCase(hash, dbHash))
332 { // hash matches - skipping
333 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change{}",
334 CURL::GetRedacted(strDirectory), !fastHash.empty() ? " (fasthash)" : "");
335 bSkip = true;
337 else if (hash.empty())
338 { // directory empty or non-existent - add to clean list and skip
339 CLog::Log(LOGDEBUG,
340 "VideoInfoScanner: Skipping dir '{}' as it's empty or doesn't exist - adding to "
341 "clean list",
342 CURL::GetRedacted(strDirectory));
343 if (m_bClean)
344 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
345 bSkip = true;
347 else if (dbHash.empty())
348 { // new folder - scan
349 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
350 CURL::GetRedacted(strDirectory));
352 else
353 { // hash changed - rescan
354 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
355 CURL::GetRedacted(strDirectory), dbHash, hash);
358 else if (content == CONTENT_TVSHOWS)
360 if (m_handle)
361 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name()));
363 if (foundDirectly && !settings.parent_name_root)
365 CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
366 DIR_FLAG_DEFAULTS);
367 items.SetPath(strDirectory);
368 GetPathHash(items, hash);
369 bSkip = true;
370 if (!m_database.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash))
371 bSkip = false;
372 else
373 items.Clear();
375 else
377 CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
378 item->SetPath(strDirectory);
379 item->m_bIsFolder = true;
380 items.Add(item);
381 items.SetPath(URIUtils::GetParentPath(item->GetPath()));
384 bool foundSomething = false;
385 if (!bSkip)
387 foundSomething = RetrieveVideoInfo(items, settings.parent_name_root, content);
388 if (foundSomething)
390 if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
392 m_database.SetPathHash(strDirectory, hash);
393 if (m_bClean)
394 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
395 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir {}",
396 CURL::GetRedacted(strDirectory));
399 else
401 if (m_bClean)
402 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
403 CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir {}",
404 CURL::GetRedacted(strDirectory));
407 else if (!StringUtils::EqualsNoCase(hash, dbHash) && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
408 { // update the hash either way - we may have changed the hash to a fast version
409 m_database.SetPathHash(strDirectory, hash);
412 if (m_handle)
413 OnDirectoryScanned(strDirectory);
415 for (int i = 0; i < items.Size(); ++i)
417 CFileItemPtr pItem = items[i];
419 if (m_bStop)
420 break;
422 // add video extras to library
423 if (foundSomething && !m_ignoreVideoExtras && IsVideoExtrasFolder(*pItem))
425 if (AddVideoExtras(items, content, pItem->GetPath()))
427 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding video extras from dir {}",
428 CURL::GetRedacted(pItem->GetPath()));
431 // no further processing required
432 continue;
435 // if we have a directory item (non-playlist) we then recurse into that folder
436 // do not recurse for tv shows - we have already looked recursively for episodes
437 if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !PLAYLIST::IsPlayList(*pItem) &&
438 settings.recurse > 0 && content != CONTENT_TVSHOWS)
440 if (!DoScan(pItem->GetPath()))
442 m_bStop = true;
446 return !m_bStop;
449 bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
451 if (pDlgProgress)
453 if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
455 pDlgProgress->ShowProgressBar(true);
456 pDlgProgress->SetPercentage(0);
458 else
459 pDlgProgress->ShowProgressBar(false);
461 pDlgProgress->Progress();
464 m_database.Open();
466 bool FoundSomeInfo = false;
467 std::vector<int> seenPaths;
468 for (int i = 0; i < items.Size(); ++i)
470 CFileItemPtr pItem = items[i];
472 // we do this since we may have a override per dir
473 ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath());
474 if (!info2) // skip
475 continue;
477 // Discard all .nomedia folders
478 if (pItem->m_bIsFolder && HasNoMedia(pItem->GetPath()))
479 continue;
481 // Discard all exclude files defined by regExExclude
482 if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
483 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps))
484 continue;
486 if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
488 if (m_handle)
489 m_handle->SetPercentage(i*100.f/items.Size());
492 // clear our scraper cache
493 info2->ClearCache();
495 INFO_RET ret = INFO_CANCELLED;
496 if (info2->Content() == CONTENT_TVSHOWS)
497 ret = RetrieveInfoForTvShow(pItem.get(), bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress);
498 else if (info2->Content() == CONTENT_MOVIES)
499 ret = RetrieveInfoForMovie(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
500 else if (info2->Content() == CONTENT_MUSICVIDEOS)
501 ret = RetrieveInfoForMusicVideo(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
502 else
504 CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type {} ({})", info2->Content(),
505 CURL::GetRedacted(pItem->GetPath()));
506 FoundSomeInfo = false;
507 break;
509 if (ret == INFO_CANCELLED || ret == INFO_ERROR)
511 CLog::Log(LOGWARNING,
512 "VideoInfoScanner: Error {} occurred while retrieving"
513 "information for {}.",
514 ret, CURL::GetRedacted(pItem->GetPath()));
515 FoundSomeInfo = false;
516 break;
518 if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
519 FoundSomeInfo = true;
520 else if (ret == INFO_NOT_FOUND)
522 CLog::Log(LOGWARNING,
523 "No information found for item '{}', it won't be added to the library.",
524 CURL::GetRedacted(pItem->GetPath()));
526 MediaType mediaType = MediaTypeMovie;
527 if (info2->Content() == CONTENT_TVSHOWS)
528 mediaType = MediaTypeTvShow;
529 else if (info2->Content() == CONTENT_MUSICVIDEOS)
530 mediaType = MediaTypeMusicVideo;
532 auto eventLog = CServiceBroker::GetEventLog();
533 if (eventLog)
535 const std::string itemlogpath = (info2->Content() == CONTENT_TVSHOWS)
536 ? CURL::GetRedacted(pItem->GetPath())
537 : URIUtils::GetFileName(pItem->GetPath());
539 eventLog->Add(EventPtr(new CMediaLibraryEvent(
540 mediaType, pItem->GetPath(), 24145,
541 StringUtils::Format(g_localizeStrings.Get(24147), mediaType, itemlogpath),
542 EventLevel::Warning)));
546 pURL = NULL;
548 // Keep track of directories we've seen
549 if (m_bClean && pItem->m_bIsFolder)
550 seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
553 if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
555 std::vector<std::pair<int, std::string>> libPaths;
556 m_database.GetSubPaths(items.GetPath(), libPaths);
557 for (std::vector<std::pair<int, std::string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
559 if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
560 m_pathsToClean.insert(i->first);
563 if(pDlgProgress)
564 pDlgProgress->ShowProgressBar(false);
566 m_database.Close();
567 return FoundSomeInfo;
570 CInfoScanner::INFO_RET
571 CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem,
572 bool bDirNames,
573 ScraperPtr &info2,
574 bool useLocal,
575 CScraperUrl* pURL,
576 bool fetchEpisodes,
577 CGUIDialogProgress* pDlgProgress)
579 const bool isSeason =
580 pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_type == MediaTypeSeason;
582 int idTvShow = -1;
583 int idSeason = -1;
584 std::string strPath = pItem->GetPath();
585 if (pItem->m_bIsFolder)
587 idTvShow = m_database.GetTvShowId(strPath);
588 if (isSeason && idTvShow > -1)
589 idSeason = m_database.GetSeasonId(idTvShow, pItem->GetVideoInfoTag()->m_iSeason);
591 else if (pItem->IsPlugin() && pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_iIdShow >= 0)
593 // for plugin source we cannot get idTvShow from episode path with URIUtils::GetDirectory() in all cases
594 // so use m_iIdShow from video info tag if possible
595 idTvShow = pItem->GetVideoInfoTag()->m_iIdShow;
596 CVideoInfoTag showInfo;
597 if (m_database.GetTvShowInfo(std::string(), showInfo, idTvShow, nullptr, 0))
598 strPath = showInfo.GetPath();
600 else
602 strPath = URIUtils::GetDirectory(strPath);
603 idTvShow = m_database.GetTvShowId(strPath);
604 if (isSeason && idTvShow > -1)
605 idSeason = m_database.GetSeasonId(idTvShow, pItem->GetVideoInfoTag()->m_iSeason);
607 if (idTvShow > -1 && (!isSeason || idSeason > -1) && (fetchEpisodes || !pItem->m_bIsFolder))
609 INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
610 if (ret == INFO_ADDED)
611 m_database.SetPathHash(strPath, pItem->GetProperty("hash").asString());
612 return ret;
615 if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361,
616 pItem->m_bIsFolder ? pItem->GetVideoInfoTag()->m_strShowTitle
617 : pItem->GetVideoInfoTag()->m_strTitle))
618 return INFO_CANCELLED;
620 if (m_handle)
621 m_handle->SetText(pItem->GetMovieName(bDirNames));
623 CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
624 CScraperUrl scrUrl;
625 // handle .nfo files
626 std::unique_ptr<IVideoInfoTagLoader> loader;
627 if (useLocal)
629 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
630 if (loader)
632 pItem->GetVideoInfoTag()->Reset();
633 result = loader->Load(*pItem->GetVideoInfoTag(), false);
637 if (result == CInfoScanner::FULL_NFO)
640 long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
641 if (lResult < 0)
642 return INFO_ERROR;
643 if (fetchEpisodes)
645 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
646 if (ret == INFO_ADDED)
647 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
648 return ret;
650 return INFO_ADDED;
652 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
654 scrUrl = loader->ScraperUrl();
655 pURL = &scrUrl;
658 CScraperUrl url;
659 int retVal = 0;
660 std::string movieTitle = pItem->GetMovieName(bDirNames);
661 int movieYear = -1; // hint that movie title was not found
662 if (result == CInfoScanner::TITLE_NFO)
664 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
665 movieTitle = tag->GetTitle();
666 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
669 std::string identifierType;
670 std::string identifier;
671 long lResult = -1;
672 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
674 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
675 if (GetDetails(pItem, uniqueIDs, url, info2,
676 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
677 ? loader.get()
678 : nullptr,
679 pDlgProgress))
681 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
682 return INFO_ERROR;
684 if (fetchEpisodes)
686 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
687 if (ret == INFO_ADDED)
689 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
690 return INFO_ADDED;
693 return INFO_ADDED;
697 if (pURL && pURL->HasUrls())
698 url = *pURL;
699 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
700 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
702 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
703 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
704 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
706 if (GetDetails(pItem, {}, url, info2,
707 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
708 ? loader.get()
709 : nullptr,
710 pDlgProgress))
712 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
713 return INFO_ERROR;
715 if (fetchEpisodes)
717 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
718 if (ret == INFO_ADDED)
719 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
721 return INFO_ADDED;
724 CInfoScanner::INFO_RET
725 CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem,
726 bool bDirNames,
727 ScraperPtr &info2,
728 bool useLocal,
729 CScraperUrl* pURL,
730 CGUIDialogProgress* pDlgProgress)
732 if (pItem->m_bIsFolder || !IsVideo(*pItem) || pItem->IsNFO() ||
733 (PLAYLIST::IsPlayList(*pItem) && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
734 return INFO_NOT_NEEDED;
736 if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
737 return INFO_CANCELLED;
739 if (m_database.HasMovieInfo(pItem->GetDynPath()))
740 return INFO_HAVE_ALREADY;
742 if (m_handle)
743 m_handle->SetText(pItem->GetMovieName(bDirNames));
745 CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
746 CScraperUrl scrUrl;
747 // handle .nfo files
748 std::unique_ptr<IVideoInfoTagLoader> loader;
749 if (useLocal)
751 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
752 if (loader)
754 pItem->GetVideoInfoTag()->Reset();
755 result = loader->Load(*pItem->GetVideoInfoTag(), false);
758 if (result == CInfoScanner::FULL_NFO)
760 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, true);
761 if (dbId < 0)
762 return INFO_ERROR;
763 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
764 return INFO_HAVE_ALREADY;
765 return INFO_ADDED;
767 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
769 scrUrl = loader->ScraperUrl();
770 pURL = &scrUrl;
773 CScraperUrl url;
774 int retVal = 0;
775 std::string movieTitle = pItem->GetMovieName(bDirNames);
776 int movieYear = -1; // hint that movie title was not found
777 if (result == CInfoScanner::TITLE_NFO)
779 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
780 movieTitle = tag->GetTitle();
781 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
784 std::string identifierType;
785 std::string identifier;
786 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
788 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
789 if (GetDetails(pItem, uniqueIDs, url, info2,
790 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
791 ? loader.get()
792 : nullptr,
793 pDlgProgress))
795 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
796 if (dbId < 0)
797 return INFO_ERROR;
798 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
799 return INFO_HAVE_ALREADY;
800 return INFO_ADDED;
804 if (pURL && pURL->HasUrls())
805 url = *pURL;
806 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
807 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
809 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
810 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
812 if (GetDetails(pItem, {}, url, info2,
813 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
814 ? loader.get()
815 : nullptr,
816 pDlgProgress))
818 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
819 if (dbId < 0)
820 return INFO_ERROR;
821 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
822 return INFO_HAVE_ALREADY;
823 return INFO_ADDED;
825 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
826 return INFO_NOT_FOUND;
829 CInfoScanner::INFO_RET
830 CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem,
831 bool bDirNames,
832 ScraperPtr &info2,
833 bool useLocal,
834 CScraperUrl* pURL,
835 CGUIDialogProgress* pDlgProgress)
837 if (pItem->m_bIsFolder || !IsVideo(*pItem) || pItem->IsNFO() ||
838 (PLAYLIST::IsPlayList(*pItem) && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
839 return INFO_NOT_NEEDED;
841 if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
842 return INFO_CANCELLED;
844 if (m_database.HasMusicVideoInfo(pItem->GetPath()))
845 return INFO_HAVE_ALREADY;
847 if (m_handle)
848 m_handle->SetText(pItem->GetMovieName(bDirNames));
850 CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
851 CScraperUrl scrUrl;
852 // handle .nfo files
853 std::unique_ptr<IVideoInfoTagLoader> loader;
854 if (useLocal)
856 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
857 if (loader)
859 pItem->GetVideoInfoTag()->Reset();
860 result = loader->Load(*pItem->GetVideoInfoTag(), false);
863 if (result == CInfoScanner::FULL_NFO)
865 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
866 return INFO_ERROR;
867 return INFO_ADDED;
869 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
871 scrUrl = loader->ScraperUrl();
872 pURL = &scrUrl;
875 CScraperUrl url;
876 int retVal = 0;
877 std::string movieTitle = pItem->GetMovieName(bDirNames);
878 int movieYear = -1; // hint that movie title was not found
879 if (result == CInfoScanner::TITLE_NFO)
881 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
882 movieTitle = tag->GetTitle();
883 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
886 std::string identifierType;
887 std::string identifier;
888 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
890 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
891 if (GetDetails(pItem, uniqueIDs, url, info2,
892 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
893 ? loader.get()
894 : nullptr,
895 pDlgProgress))
897 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
898 return INFO_ERROR;
899 return INFO_ADDED;
903 if (pURL && pURL->HasUrls())
904 url = *pURL;
905 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
906 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
908 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
909 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
911 if (GetDetails(pItem, {}, url, info2,
912 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
913 ? loader.get()
914 : nullptr,
915 pDlgProgress))
917 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
918 return INFO_ERROR;
919 return INFO_ADDED;
921 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
922 return INFO_NOT_FOUND;
925 CInfoScanner::INFO_RET
926 CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item,
927 long showID,
928 const ADDON::ScraperPtr &scraper,
929 bool useLocal,
930 CGUIDialogProgress *progress)
932 // enumerate episodes
933 EPISODELIST files;
934 if (!EnumerateSeriesFolder(item, files))
935 return INFO_HAVE_ALREADY;
936 if (files.empty()) // no update or no files
937 return INFO_NOT_NEEDED;
939 if (m_bStop || (progress && progress->IsCanceled()))
940 return INFO_CANCELLED;
942 CVideoInfoTag showInfo;
943 m_database.GetTvShowInfo("", showInfo, showID);
944 INFO_RET ret = OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
946 if (ret == INFO_ADDED)
948 std::map<int, std::map<std::string, std::string>> seasonArt;
949 m_database.GetTvShowSeasonArt(showID, seasonArt);
951 bool updateSeasonArt = false;
952 for (std::map<int, std::map<std::string, std::string>>::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
954 if (i->second.empty())
956 updateSeasonArt = true;
957 break;
961 if (updateSeasonArt)
963 if (!item->IsPlugin() || scraper->ID() != "metadata.local")
965 CVideoInfoDownloader loader(scraper);
966 loader.GetArtwork(showInfo);
968 GetSeasonThumbs(showInfo, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !item->IsPlugin());
969 for (std::map<int, std::map<std::string, std::string> >::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
971 int seasonID = m_database.AddSeason(showID, i->first);
972 m_database.SetArtForItem(seasonID, MediaTypeSeason, i->second);
976 return ret;
979 bool CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
981 CFileItemList items;
982 const std::vector<std::string> &regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps;
984 bool bSkip = false;
986 if (item->m_bIsFolder)
989 * Note: DoScan() will not remove this path as it's not recursing for tvshows.
990 * Remove this path from the list we're processing in order to avoid hitting
991 * it twice in the main loop.
993 std::set<std::string>::iterator it = m_pathsToScan.find(item->GetPath());
994 if (it != m_pathsToScan.end())
995 m_pathsToScan.erase(it);
997 if (HasNoMedia(item->GetPath()))
998 return true;
1000 std::string hash, dbHash;
1001 bool allowEmptyHash = false;
1002 if (item->IsPlugin())
1004 // if plugin has already calculated a hash for directory contents - use it
1005 // in this case we don't need to get directory listing from plugin for hash checking
1006 if (item->HasProperty("hash"))
1008 hash = item->GetProperty("hash").asString();
1009 allowEmptyHash = true;
1012 else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash)
1013 hash = GetRecursiveFastHash(item->GetPath(), regexps);
1015 if (m_database.GetPathHash(item->GetPath(), dbHash) && (allowEmptyHash || !hash.empty()) && StringUtils::EqualsNoCase(dbHash, hash))
1017 // fast hashes match - no need to process anything
1018 bSkip = true;
1021 // fast hash cannot be computed or we need to rescan. fetch the listing.
1022 if (!bSkip)
1024 int flags = DIR_FLAG_DEFAULTS;
1025 if (!hash.empty())
1026 flags |= DIR_FLAG_NO_FILE_INFO;
1028 // Listing that ignores files inside and below folders containing .nomedia files.
1029 CDirectory::EnumerateDirectory(
1030 item->GetPath(), [&items](const std::shared_ptr<CFileItem>& item) { items.Add(item); },
1031 [this](const std::shared_ptr<CFileItem>& folder)
1032 { return !HasNoMedia(folder->GetPath()); },
1033 true, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), flags);
1035 // fast hash failed - compute slow one
1036 if (hash.empty())
1038 GetPathHash(items, hash);
1039 if (StringUtils::EqualsNoCase(dbHash, hash))
1041 // slow hashes match - no need to process anything
1042 bSkip = true;
1047 if (bSkip)
1049 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change",
1050 CURL::GetRedacted(item->GetPath()));
1051 // update our dialog with our progress
1052 if (m_handle)
1053 OnDirectoryScanned(item->GetPath());
1054 return false;
1057 if (dbHash.empty())
1058 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
1059 CURL::GetRedacted(item->GetPath()));
1060 else
1061 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
1062 CURL::GetRedacted(item->GetPath()), dbHash, hash);
1064 if (m_bClean)
1066 m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
1067 m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
1069 item->SetProperty("hash", hash);
1071 else
1073 CFileItemPtr newItem(new CFileItem(*item));
1074 items.Add(newItem);
1078 stack down any dvd folders
1079 need to sort using the full path since this is a collapsed recursive listing of all subdirs
1080 video_ts.ifo files should sort at the top of a dvd folder in ascending order
1082 /foo/bar/video_ts.ifo
1083 /foo/bar/vts_x_y.ifo
1084 /foo/bar/vts_x_y.vob
1087 // since we're doing this now anyway, should other items be stacked?
1088 items.Sort(SortByPath, SortOrderAscending);
1090 // If found VIDEO_TS.IFO or INDEX.BDMV then we are dealing with Blu-ray or DVD files on disc
1091 // somewhere in the directory tree. Assume that all other files/folders in the same folder
1092 // with VIDEO_TS or BDMV can be ignored.
1093 // THere can be a BACKUP/INDEX.BDMV which needs to be ignored (and broke the old while loop here)
1095 // Get folders to remove
1096 std::vector<std::string> foldersToRemove;
1097 for (const auto& item : items)
1099 const std::string file = StringUtils::ToUpper(item->GetPath());
1100 if (file.find("VIDEO_TS.IFO") != std::string::npos)
1101 foldersToRemove.emplace_back(StringUtils::ToUpper(URIUtils::GetDirectory(file)));
1102 if (file.find("INDEX.BDMV") != std::string::npos &&
1103 file.find("BACKUP/INDEX.BDMV") == std::string::npos)
1104 foldersToRemove.emplace_back(
1105 StringUtils::ToUpper(URIUtils::GetParentPath(URIUtils::GetDirectory(file))));
1108 // Remove folders
1109 items.erase(
1110 std::remove_if(items.begin(), items.end(),
1111 [&](const CFileItemPtr& i)
1113 const std::string fileAndPath(StringUtils::ToUpper(i->GetPath()));
1114 std::string file;
1115 std::string path;
1116 URIUtils::Split(fileAndPath, path, file);
1117 return (std::count_if(foldersToRemove.begin(), foldersToRemove.end(),
1118 [&](const std::string& removePath)
1119 { return path.rfind(removePath, 0) == 0; }) > 0) &&
1120 file != "VIDEO_TS.IFO" &&
1121 (file != "INDEX.BDMV" ||
1122 fileAndPath.find("BACKUP/INDEX.BDMV") != std::string::npos);
1124 items.end());
1126 // enumerate
1127 for (int i=0;i<items.Size();++i)
1129 if (items[i]->m_bIsFolder)
1130 continue;
1131 std::string strPath = URIUtils::GetDirectory(items[i]->GetPath());
1132 URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
1134 if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strPath), "sample"))
1135 continue;
1137 // Discard all exclude files defined by regExExcludes
1138 if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
1139 continue;
1142 * Check if the media source has already set the season and episode or original air date in
1143 * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
1144 * any false positive matches.
1146 if (ProcessItemByVideoInfoTag(items[i].get(), episodeList))
1147 continue;
1149 if (!EnumerateEpisodeItem(items[i].get(), episodeList))
1150 CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file {}", CURL::GetRedacted(items[i]->GetPath()));
1152 return true;
1155 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
1157 if (!item->HasVideoInfoTag())
1158 return false;
1160 const CVideoInfoTag* tag = item->GetVideoInfoTag();
1161 bool isValid = false;
1163 * First check the season and episode number. This takes precedence over the original air
1164 * date and episode title. Must be a valid season and episode number combination.
1166 if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
1167 isValid = true;
1169 // episode 0 with non-zero season is valid! (e.g. prequel episode)
1170 if (item->IsPlugin() && tag->m_iSeason > 0 && tag->m_iEpisode >= 0)
1171 isValid = true;
1173 if (isValid)
1175 EPISODE episode;
1176 episode.strPath = item->GetPath();
1177 episode.iSeason = tag->m_iSeason;
1178 episode.iEpisode = tag->m_iEpisode;
1179 episode.isFolder = false;
1180 // save full item for plugin source
1181 if (item->IsPlugin())
1182 episode.item = std::make_shared<CFileItem>(*item);
1183 episodeList.push_back(episode);
1184 CLog::Log(LOGDEBUG, "{} - found match for: {}. Season {}, Episode {}", __FUNCTION__,
1185 CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode);
1186 return true;
1190 * Next preference is the first aired date. If it exists use that for matching the TV Show
1191 * information. Also set the title in case there are multiple matches for the first aired date.
1193 if (tag->m_firstAired.IsValid())
1195 EPISODE episode;
1196 episode.strPath = item->GetPath();
1197 episode.strTitle = tag->m_strTitle;
1198 episode.isFolder = false;
1200 * Set season and episode to -1 to indicate to use the aired date.
1202 episode.iSeason = -1;
1203 episode.iEpisode = -1;
1205 * The first aired date string must be parseable.
1207 episode.cDate = item->GetVideoInfoTag()->m_firstAired;
1208 episodeList.push_back(episode);
1209 CLog::Log(LOGDEBUG, "{} - found match for: '{}', firstAired: '{}' = '{}', title: '{}'",
1210 __FUNCTION__, CURL::GetRedacted(episode.strPath),
1211 tag->m_firstAired.GetAsDBDateTime(), episode.cDate.GetAsLocalizedDate(),
1212 episode.strTitle);
1213 return true;
1217 * Next preference is the episode title. If it exists use that for matching the TV Show
1218 * information.
1220 if (!tag->m_strTitle.empty())
1222 EPISODE episode;
1223 episode.strPath = item->GetPath();
1224 episode.strTitle = tag->m_strTitle;
1225 episode.isFolder = false;
1227 * Set season and episode to -1 to indicate to use the title.
1229 episode.iSeason = -1;
1230 episode.iEpisode = -1;
1231 episodeList.push_back(episode);
1232 CLog::Log(LOGDEBUG, "{} - found match for: '{}', title: '{}'", __FUNCTION__,
1233 CURL::GetRedacted(episode.strPath), episode.strTitle);
1234 return true;
1238 * There is no further episode information available if both the season and episode number have
1239 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
1240 * to the episode list.
1242 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
1244 CLog::Log(LOGDEBUG,
1245 "{} - found exclusion match for: {}. Both Season and Episode are 0. Item will be "
1246 "ignored for scanning.",
1247 __FUNCTION__, CURL::GetRedacted(item->GetPath()));
1248 return true;
1251 return false;
1254 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
1256 SETTINGS_TVSHOWLIST expression = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowEnumRegExps;
1258 std::string strLabel;
1260 // remove path to main file if it's a bd or dvd folder to regex the right (folder) name
1261 if (item->IsOpticalMediaFile())
1263 strLabel = item->GetLocalMetadataPath();
1264 URIUtils::RemoveSlashAtEnd(strLabel);
1266 else
1267 strLabel = item->GetPath();
1269 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
1270 strLabel = CURL::Decode(CURL::GetRedacted(strLabel));
1272 for (unsigned int i=0;i<expression.size();++i)
1274 CRegExp reg(true, CRegExp::autoUtf8);
1275 if (!reg.RegComp(expression[i].regexp))
1276 continue;
1278 int regexppos, regexp2pos;
1279 //CLog::Log(LOGDEBUG,"running expression {} on {}",expression[i].regexp,strLabel);
1280 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
1281 continue;
1283 EPISODE episode;
1284 episode.strPath = item->GetPath();
1285 episode.iSeason = -1;
1286 episode.iEpisode = -1;
1287 episode.cDate.SetValid(false);
1288 episode.isFolder = false;
1290 bool byDate = expression[i].byDate ? true : false;
1291 bool byTitle = expression[i].byTitle;
1292 int defaultSeason = expression[i].defaultSeason;
1294 if (byDate)
1296 if (!GetAirDateFromRegExp(reg, episode))
1297 continue;
1299 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match {} ({}) [{}]",
1300 CURL::GetRedacted(episode.strPath), episode.cDate.GetAsLocalizedDate(),
1301 expression[i].regexp);
1303 else if (byTitle)
1305 if (!GetEpisodeTitleFromRegExp(reg, episode))
1306 continue;
1308 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found title based match {} ({}) [{}]",
1309 CURL::GetRedacted(episode.strPath), episode.strTitle, expression[i].regexp);
1311 else
1313 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
1314 continue;
1316 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match {} (s{}e{}) [{}]",
1317 CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode,
1318 expression[i].regexp);
1321 // Grab the remainder from first regexp run
1322 // as second run might modify or empty it.
1323 std::string remainder(reg.GetMatch(3));
1326 * Check if the files base path is a dedicated folder that contains
1327 * only this single episode. If season and episode match with the
1328 * actual media file, we set episode.isFolder to true.
1330 std::string strBasePath = item->GetBaseMoviePath(true);
1331 URIUtils::RemoveSlashAtEnd(strBasePath);
1332 strBasePath = URIUtils::GetFileName(strBasePath);
1334 if (reg.RegFind(strBasePath.c_str()) > -1)
1336 EPISODE parent;
1337 if (byDate)
1339 GetAirDateFromRegExp(reg, parent);
1340 if (episode.cDate == parent.cDate)
1341 episode.isFolder = true;
1343 else
1345 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
1346 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
1347 episode.isFolder = true;
1351 // add what we found by now
1352 episodeList.push_back(episode);
1354 CRegExp reg2(true, CRegExp::autoUtf8);
1355 // check the remainder of the string for any further episodes.
1356 if (!byDate && reg2.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowMultiPartEnumRegExp))
1358 int offset = 0;
1360 // we want "long circuit" OR below so that both offsets are evaluated
1361 while (static_cast<int>((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) |
1362 static_cast<int>((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
1364 if (((regexppos <= regexp2pos) && regexppos != -1) ||
1365 (regexppos >= 0 && regexp2pos == -1))
1367 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
1369 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season {}, multipart episode {} [{}]",
1370 episode.iSeason, episode.iEpisode,
1371 CServiceBroker::GetSettingsComponent()
1372 ->GetAdvancedSettings()
1373 ->m_tvshowMultiPartEnumRegExp);
1375 episodeList.push_back(episode);
1376 remainder = reg.GetMatch(3);
1377 offset = 0;
1379 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
1380 (regexp2pos >= 0 && regexppos == -1))
1382 episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
1383 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode {} [{}]",
1384 episode.iEpisode,
1385 CServiceBroker::GetSettingsComponent()
1386 ->GetAdvancedSettings()
1387 ->m_tvshowMultiPartEnumRegExp);
1388 episodeList.push_back(episode);
1389 offset += regexp2pos + reg2.GetFindLen();
1393 return true;
1395 return false;
1398 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp &reg, EPISODE &episodeInfo, int defaultSeason)
1400 std::string season(reg.GetMatch(1));
1401 std::string episode(reg.GetMatch(2));
1403 if (!season.empty() || !episode.empty())
1405 char* endptr = NULL;
1406 if (season.empty() && !episode.empty())
1407 { // no season specified -> assume defaultSeason
1408 episodeInfo.iSeason = defaultSeason;
1409 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
1410 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1412 else if (!season.empty() && episode.empty())
1413 { // no episode specification -> assume defaultSeason
1414 episodeInfo.iSeason = defaultSeason;
1415 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
1416 episodeInfo.iEpisode = atoi(season.c_str());
1418 else
1419 { // season and episode specified
1420 episodeInfo.iSeason = atoi(season.c_str());
1421 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1423 if (endptr)
1425 if (isalpha(*endptr))
1426 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1427 else if (*endptr == '.')
1428 episodeInfo.iSubepisode = atoi(endptr+1);
1430 return true;
1432 return false;
1435 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp &reg, EPISODE &episodeInfo)
1437 std::string param1(reg.GetMatch(1));
1438 std::string param2(reg.GetMatch(2));
1439 std::string param3(reg.GetMatch(3));
1441 if (!param1.empty() && !param2.empty() && !param3.empty())
1443 // regular expression by date
1444 int len1 = param1.size();
1445 int len2 = param2.size();
1446 int len3 = param3.size();
1448 if (len1==4 && len2==2 && len3==2)
1450 // yyyy mm dd format
1451 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1453 else if (len1==2 && len2==2 && len3==4)
1455 // mm dd yyyy format
1456 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1459 return episodeInfo.cDate.IsValid();
1462 bool CVideoInfoScanner::GetEpisodeTitleFromRegExp(CRegExp& reg, EPISODE& episodeInfo)
1464 std::string param1(reg.GetMatch(1));
1466 if (!param1.empty())
1468 episodeInfo.strTitle = param1;
1469 return true;
1471 return false;
1474 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1476 // ensure our database is open (this can get called via other classes)
1477 if (!m_database.Open())
1478 return -1;
1480 if (!libraryImport)
1481 GetArtwork(pItem, content, videoFolder, useLocal && !pItem->IsPlugin(), showInfo ? showInfo->m_strPath : "");
1483 // ensure the art map isn't completely empty by specifying an empty thumb
1484 std::map<std::string, std::string> art = pItem->GetArt();
1485 if (art.empty())
1486 art["thumb"] = "";
1488 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1489 if (movieDetails.m_basePath.empty())
1490 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1491 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1493 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1495 if (pItem->m_bIsFolder)
1496 movieDetails.m_strPath = pItem->GetPath();
1498 std::string strTitle(movieDetails.m_strTitle);
1500 if (showInfo && content == CONTENT_TVSHOWS)
1502 strTitle = StringUtils::Format("{} - {}x{} - {}", showInfo->m_strTitle,
1503 movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle);
1506 /* As HasStreamDetails() returns true for TV shows (because the scraper calls SetVideoInfoTag()
1507 * directly to set the duration) a better test is just to see if we have any common flag info
1508 * missing. If we have already read an nfo file then this data should be populated, otherwise
1509 * get it from the video file */
1511 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
1512 CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
1514 const auto& strmdetails = movieDetails.m_streamDetails;
1515 if (strmdetails.GetVideoCodec(1).empty() || strmdetails.GetVideoHeight(1) == 0 ||
1516 strmdetails.GetVideoWidth(1) == 0 || strmdetails.GetVideoDuration(1) == 0)
1519 CDVDFileInfo::GetFileStreamDetails(pItem);
1520 CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}",
1521 CURL::GetRedacted(pItem->GetPath()));
1525 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to {}:{}", TranslateContent(content), CURL::GetRedacted(pItem->GetPath()));
1526 long lResult = -1;
1528 if (content == CONTENT_MOVIES)
1530 // find local trailer first
1531 std::string strTrailer = UTILS::FindTrailer(*pItem);
1532 if (!strTrailer.empty())
1533 movieDetails.m_strTrailer = strTrailer;
1535 // Deal with 'Disc n' subdirectories
1536 const std::string discNum{
1537 CUtil::GetDiscNumberFromPath(URIUtils::GetParentPath(movieDetails.m_strFileNameAndPath))};
1538 if (!discNum.empty())
1540 if (movieDetails.m_set.title.empty())
1542 const std::string setName{m_database.GetSetByNameLike(movieDetails.m_strTitle)};
1543 if (!setName.empty())
1545 // Add movie to existing set
1546 movieDetails.SetSet(setName);
1548 else
1550 // Create set, then add movie to the set
1551 const int idSet{m_database.AddSet(movieDetails.m_strTitle)};
1552 m_database.SetArtForItem(idSet, MediaTypeVideoCollection, art);
1553 movieDetails.SetSet(movieDetails.m_strTitle);
1557 // Add '(Disc n)' to title (in local language)
1558 movieDetails.m_strTitle =
1559 StringUtils::Format(g_localizeStrings.Get(29995), movieDetails.m_strTitle, discNum);
1562 lResult = m_database.SetDetailsForMovie(movieDetails, art);
1563 movieDetails.m_iDbId = lResult;
1564 movieDetails.m_type = MediaTypeMovie;
1566 // setup links to shows if the linked shows are in the db
1567 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1569 CFileItemList items;
1570 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1571 if (items.Size())
1572 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1573 else
1574 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie {} to show {}",
1575 movieDetails.m_strTitle, movieDetails.m_showLink[i]);
1578 else if (content == CONTENT_TVSHOWS)
1580 if (pItem->m_bIsFolder)
1583 multipaths are not stored in the database, so in the case we have one,
1584 we split the paths, and compute the parent paths in each case.
1586 std::vector<std::string> multipath;
1587 if (!URIUtils::IsMultiPath(pItem->GetPath()) || !CMultiPathDirectory::GetPaths(pItem->GetPath(), multipath))
1588 multipath.push_back(pItem->GetPath());
1589 std::vector<std::pair<std::string, std::string> > paths;
1590 for (std::vector<std::string>::const_iterator i = multipath.begin(); i != multipath.end(); ++i)
1591 paths.emplace_back(*i, URIUtils::GetParentPath(*i));
1593 std::map<int, std::map<std::string, std::string> > seasonArt;
1595 if (!libraryImport)
1596 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !pItem->IsPlugin());
1598 lResult = m_database.SetDetailsForTvShow(paths, movieDetails, art, seasonArt);
1599 movieDetails.m_iDbId = lResult;
1600 movieDetails.m_type = MediaTypeTvShow;
1602 else
1604 // we add episode then set details, as otherwise set details will delete the
1605 // episode then add, which breaks multi-episode files.
1606 int idShow = showInfo ? showInfo->m_iDbId : -1;
1607 int idEpisode = m_database.AddNewEpisode(idShow, movieDetails);
1608 lResult = m_database.SetDetailsForEpisode(movieDetails, art, idShow, idEpisode);
1609 movieDetails.m_iDbId = lResult;
1610 movieDetails.m_type = MediaTypeEpisode;
1611 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1612 if (movieDetails.m_EpBookmark.timeInSeconds > 0)
1614 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1615 movieDetails.m_EpBookmark.seasonNumber = movieDetails.m_iSeason;
1616 movieDetails.m_EpBookmark.episodeNumber = movieDetails.m_iEpisode;
1617 m_database.AddBookMarkForEpisode(movieDetails, movieDetails.m_EpBookmark);
1621 else if (content == CONTENT_MUSICVIDEOS)
1623 lResult = m_database.SetDetailsForMusicVideo(movieDetails, art);
1624 movieDetails.m_iDbId = lResult;
1625 movieDetails.m_type = MediaTypeMusicVideo;
1628 if (!pItem->m_bIsFolder)
1630 const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
1631 if ((libraryImport || advancedSettings->m_bVideoLibraryImportWatchedState) &&
1632 (movieDetails.IsPlayCountSet() || movieDetails.m_lastPlayed.IsValid()))
1633 m_database.SetPlayCount(*pItem, movieDetails.GetPlayCount(), movieDetails.m_lastPlayed);
1635 if ((libraryImport || advancedSettings->m_bVideoLibraryImportResumePoint) &&
1636 movieDetails.GetResumePoint().IsSet())
1637 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.GetResumePoint(), CBookmark::RESUME);
1640 m_database.Close();
1642 CFileItemPtr itemCopy = std::make_shared<CFileItem>(*pItem);
1643 CVariant data;
1644 data["added"] = true;
1645 if (m_bRunning)
1646 data["transaction"] = true;
1647 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate",
1648 itemCopy, data);
1649 return lResult;
1652 std::string ContentToMediaType(CONTENT_TYPE content, bool folder)
1654 switch (content)
1656 case CONTENT_MOVIES:
1657 return MediaTypeMovie;
1658 case CONTENT_MUSICVIDEOS:
1659 return MediaTypeMusicVideo;
1660 case CONTENT_TVSHOWS:
1661 return folder ? MediaTypeTvShow : MediaTypeEpisode;
1662 default:
1663 return "";
1667 VideoDbContentType ContentToVideoDbType(CONTENT_TYPE content)
1669 switch (content)
1671 case CONTENT_MOVIES:
1672 return VideoDbContentType::MOVIES;
1673 case CONTENT_MUSICVIDEOS:
1674 return VideoDbContentType::MUSICVIDEOS;
1675 case CONTENT_TVSHOWS:
1676 return VideoDbContentType::EPISODES;
1677 default:
1678 return VideoDbContentType::UNKNOWN;
1682 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1684 std::string type = "thumb";
1685 if (width*5 < height*4)
1686 type = "poster";
1687 else if (width*1 > height*4)
1688 type = "banner";
1689 return type;
1692 std::string CVideoInfoScanner::GetMovieSetInfoFolder(const std::string& setTitle)
1694 if (setTitle.empty())
1695 return "";
1696 std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
1697 CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER);
1698 if (path.empty())
1699 return "";
1700 path = URIUtils::AddFileToFolder(path, CUtil::MakeLegalFileName(setTitle, LEGAL_WIN32_COMPAT));
1701 URIUtils::AddSlashAtEnd(path);
1702 CLog::Log(LOGDEBUG,
1703 "VideoInfoScanner: Looking for local artwork for movie set '{}' in folder '{}'",
1704 setTitle,
1705 CURL::GetRedacted(path));
1706 return CDirectory::Exists(path) ? path : "";
1709 void CVideoInfoScanner::AddLocalItemArtwork(CGUIListItem::ArtMap& itemArt,
1710 const std::vector<std::string>& wantedArtTypes, const std::string& itemPath,
1711 bool addAll, bool exactName)
1713 std::string path = URIUtils::GetDirectory(itemPath);
1714 if (path.empty())
1715 return;
1717 CFileItemList availableArtFiles;
1718 CDirectory::GetDirectory(path, availableArtFiles,
1719 CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
1720 DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
1722 std::string baseFilename = URIUtils::GetFileName(itemPath);
1723 if (!baseFilename.empty())
1725 URIUtils::RemoveExtension(baseFilename);
1726 baseFilename.append("-");
1729 for (const auto& artFile : availableArtFiles)
1731 std::string candidate = URIUtils::GetFileName(artFile->GetPath());
1733 bool matchesFilename =
1734 !baseFilename.empty() && StringUtils::StartsWith(candidate, baseFilename);
1735 if (!baseFilename.empty() && !matchesFilename)
1736 continue;
1738 if (matchesFilename)
1739 candidate.erase(0, baseFilename.length());
1740 URIUtils::RemoveExtension(candidate);
1741 StringUtils::ToLower(candidate);
1743 // move 'folder' to thumb / poster / banner based on aspect ratio
1744 // if such artwork doesn't already exist
1745 if (!matchesFilename && StringUtils::EqualsNoCase(candidate, "folder") &&
1746 !CVideoThumbLoader::IsArtTypeInWhitelist("folder", wantedArtTypes, exactName))
1748 // cache the image to determine sizing
1749 CTextureDetails details;
1750 if (CServiceBroker::GetTextureCache()->CacheImage(artFile->GetPath(), details))
1752 candidate = GetArtTypeFromSize(details.width, details.height);
1753 if (itemArt.find(candidate) != itemArt.end())
1754 continue;
1758 if ((addAll && CVideoThumbLoader::IsValidArtType(candidate)) ||
1759 CVideoThumbLoader::IsArtTypeInWhitelist(candidate, wantedArtTypes, exactName))
1761 itemArt[candidate] = artFile->GetPath();
1766 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1768 int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1769 CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
1770 if (artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE)
1771 return;
1773 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1774 movieDetails.m_fanart.Unpack();
1775 movieDetails.m_strPictureURL.Parse();
1777 CGUIListItem::ArtMap art = pItem->GetArt();
1779 // get and cache thumb images
1780 std::string mediaType = ContentToMediaType(content, pItem->m_bIsFolder);
1781 std::vector<std::string> artTypes = CVideoThumbLoader::GetArtTypes(mediaType);
1782 bool moviePartOfSet = content == CONTENT_MOVIES && !movieDetails.m_set.title.empty();
1783 std::vector<std::string> movieSetArtTypes;
1784 if (moviePartOfSet)
1786 movieSetArtTypes = CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection);
1787 for (const std::string& artType : movieSetArtTypes)
1788 artTypes.push_back("set." + artType);
1790 bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
1791 bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
1792 // find local art
1793 if (useLocal)
1795 if (!pItem->SkipLocalArt())
1797 bool useFolder = false;
1798 if (bApplyToDir && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
1800 std::string filename = ART::GetLocalArtBaseFilename(*pItem, useFolder);
1801 std::string directory = URIUtils::GetDirectory(filename);
1802 if (filename != directory)
1803 AddLocalItemArtwork(art, artTypes, directory, addAll, exactName);
1806 // Reset useFolder to false as GetLocalArtBaseFilename may modify it in
1807 // the previous call.
1808 useFolder = false;
1810 AddLocalItemArtwork(art, artTypes, ART::GetLocalArtBaseFilename(*pItem, useFolder), addAll,
1811 exactName);
1814 if (moviePartOfSet)
1816 std::string movieSetInfoPath = GetMovieSetInfoFolder(movieDetails.m_set.title);
1817 if (!movieSetInfoPath.empty())
1819 CGUIListItem::ArtMap movieSetArt;
1820 AddLocalItemArtwork(movieSetArt, movieSetArtTypes, movieSetInfoPath, addAll, exactName);
1821 for (const auto& artItem : movieSetArt)
1823 art["set." + artItem.first] = artItem.second;
1829 // find embedded art
1830 if (pItem->HasVideoInfoTag() && !pItem->GetVideoInfoTag()->m_coverArt.empty())
1832 for (auto& it : pItem->GetVideoInfoTag()->m_coverArt)
1834 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(it.m_type, artTypes, exactName)) &&
1835 art.find(it.m_type) == art.end())
1837 std::string thumb = IMAGE_FILES::URLFromFile(pItem->GetPath(), "video_" + it.m_type);
1838 art.insert(std::make_pair(it.m_type, thumb));
1843 // add online fanart (treated separately due to it being stored in m_fanart)
1844 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist("fanart", artTypes, exactName)) &&
1845 art.find("fanart") == art.end())
1847 std::string fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1848 if (!fanart.empty())
1849 art.insert(std::make_pair("fanart", fanart));
1852 // add online art
1853 for (const auto& url : pItem->GetVideoInfoTag()->m_strPictureURL.GetUrls())
1855 if (url.m_type != CScraperUrl::UrlType::General)
1856 continue;
1857 std::string aspect = url.m_aspect;
1858 if (aspect.empty())
1859 // Backward compatibility with Kodi 11 Eden NFO files
1860 aspect = mediaType == MediaTypeEpisode ? "thumb" : "poster";
1862 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
1863 art.find(aspect) == art.end())
1865 std::string image = GetImage(url, pItem->GetPath());
1866 if (!image.empty())
1867 art.insert(std::make_pair(aspect, image));
1871 if (art.find("thumb") == art.end() &&
1872 CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
1873 CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB) &&
1874 CDVDFileInfo::CanExtract(*pItem))
1876 art["thumb"] = CVideoThumbLoader::GetEmbeddedThumbURL(*pItem);
1879 for (const auto& artType : artTypes)
1881 if (art.find(artType) != art.end())
1882 CServiceBroker::GetTextureCache()->BackgroundCacheImage(art[artType]);
1885 pItem->SetArt(art);
1887 // parent folder to apply the thumb to and to search for local actor thumbs
1888 std::string parentDir = URIUtils::GetBasePath(pItem->GetPath());
1889 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS))
1890 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1891 if (bApplyToDir)
1892 ApplyThumbToFolder(parentDir, art["thumb"]);
1895 std::string CVideoInfoScanner::GetImage(const CScraperUrl::SUrlEntry &image, const std::string& itemPath)
1897 std::string thumb = CScraperUrl::GetThumbUrl(image);
1898 if (!thumb.empty() && thumb.find('/') == std::string::npos &&
1899 thumb.find('\\') == std::string::npos)
1901 std::string strPath = URIUtils::GetDirectory(itemPath);
1902 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1904 return thumb;
1907 CInfoScanner::INFO_RET
1908 CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files,
1909 const ADDON::ScraperPtr &scraper,
1910 bool useLocal,
1911 const CVideoInfoTag& showInfo,
1912 CGUIDialogProgress* pDlgProgress /* = NULL */)
1914 if (pDlgProgress)
1916 pDlgProgress->SetLine(1, CVariant{20361}); // Loading episode details
1917 pDlgProgress->SetPercentage(0);
1918 pDlgProgress->ShowProgressBar(true);
1919 pDlgProgress->Progress();
1922 EPISODELIST episodes;
1923 bool hasEpisodeGuide = false;
1925 int iMax = files.size();
1926 int iCurr = 1;
1927 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1929 if (pDlgProgress)
1931 pDlgProgress->SetLine(1, CVariant{20361}); // Loading episode details
1932 pDlgProgress->SetLine(2, StringUtils::Format("{} {}", g_localizeStrings.Get(20373),
1933 file->iSeason)); // Season x
1934 pDlgProgress->SetLine(3, StringUtils::Format("{} {}", g_localizeStrings.Get(20359),
1935 file->iEpisode)); // Episode y
1936 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1937 pDlgProgress->Progress();
1939 if (m_handle)
1940 m_handle->SetPercentage(100.f*iCurr++/iMax);
1942 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1943 return INFO_CANCELLED;
1945 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1947 if (m_handle)
1948 m_handle->SetText(g_localizeStrings.Get(20415));
1949 continue;
1952 CFileItem item;
1953 if (file->item)
1954 item = *file->item;
1955 else
1957 item.SetPath(file->strPath);
1958 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1961 // handle .nfo files
1962 CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
1963 CScraperUrl scrUrl;
1964 const ScraperPtr& info(scraper);
1965 std::unique_ptr<IVideoInfoTagLoader> loader;
1966 if (useLocal)
1968 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(item, info, false));
1969 if (loader)
1971 // no reset here on purpose
1972 result = loader->Load(*item.GetVideoInfoTag(), false);
1975 if (result == CInfoScanner::FULL_NFO)
1977 // override with episode and season number from file if available
1978 if (file->iEpisode > -1)
1980 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1981 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1983 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1984 return INFO_ERROR;
1985 continue;
1988 if (!hasEpisodeGuide)
1990 // fetch episode guide
1991 if (!showInfo.m_strEpisodeGuide.empty() && scraper->ID() != "metadata.local")
1993 CScraperUrl url;
1994 url.ParseAndAppendUrlsFromEpisodeGuide(showInfo.m_strEpisodeGuide);
1996 if (pDlgProgress)
1998 pDlgProgress->SetLine(1, CVariant{20354}); // Fetching episode guide
1999 pDlgProgress->Progress();
2002 CVideoInfoDownloader imdb(scraper);
2003 if (!imdb.GetEpisodeList(url, episodes))
2004 return INFO_NOT_FOUND;
2006 hasEpisodeGuide = true;
2010 if (episodes.empty())
2012 CLog::Log(LOGERROR,
2013 "VideoInfoScanner: Asked to lookup episode {}"
2014 " online, but we have either no episode guide or"
2015 " we are using the local scraper. Check your tvshow.nfo and make"
2016 " sure the <episodeguide> tag is in place and/or use an online"
2017 " scraper.",
2018 CURL::GetRedacted(file->strPath));
2019 continue;
2022 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
2023 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
2024 bool bFound = false;
2025 EPISODELIST::iterator guide = episodes.begin();
2026 EPISODELIST matches;
2028 for (; guide != episodes.end(); ++guide )
2030 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
2032 if (key==*guide)
2034 bFound = true;
2035 break;
2037 else if ((file->iSubepisode!=0) && (backupkey==*guide))
2039 matches.push_back(*guide);
2040 continue;
2043 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
2045 matches.push_back(*guide);
2046 continue;
2048 if (!guide->cScraperUrl.GetTitle().empty() &&
2049 StringUtils::EqualsNoCase(guide->cScraperUrl.GetTitle(), file->strTitle))
2051 bFound = true;
2052 break;
2054 if (!guide->strTitle.empty() && StringUtils::EqualsNoCase(guide->strTitle, file->strTitle))
2056 bFound = true;
2057 break;
2061 if (!bFound)
2064 * If there is only one match or there are matches but no title to compare with to help
2065 * identify the best match, then pick the first match as the best possible candidate.
2067 * Otherwise, use the title to further refine the best match.
2069 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
2071 guide = matches.begin();
2072 bFound = true;
2074 else if (!file->strTitle.empty())
2076 CLog::Log(LOGDEBUG, "VideoInfoScanner: analyzing parsed title '{}'", file->strTitle);
2077 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
2079 EPISODELIST *candidates;
2080 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
2082 minscore = 0.8; // 80% should ensure a good match.
2083 candidates = &episodes;
2085 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
2086 candidates = &matches;
2088 std::vector<std::string> titles;
2089 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
2091 auto title = guide->cScraperUrl.GetTitle();
2092 if (title.empty())
2094 title = guide->strTitle;
2096 StringUtils::ToLower(title);
2097 guide->cScraperUrl.SetTitle(title);
2098 titles.push_back(title);
2101 double matchscore;
2102 std::string loweredTitle(file->strTitle);
2103 StringUtils::ToLower(loweredTitle);
2104 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
2105 if (index >= 0 && matchscore >= minscore)
2107 guide = candidates->begin() + index;
2108 bFound = true;
2109 CLog::Log(LOGDEBUG,
2110 "{} fuzzy title match for show: '{}', title: '{}', match: '{}', score: {:f} "
2111 ">= {:f}",
2112 __FUNCTION__, showInfo.m_strTitle, file->strTitle, titles[index], matchscore,
2113 minscore);
2118 if (bFound)
2120 CVideoInfoDownloader imdb(scraper);
2121 CFileItem item;
2122 item.SetPath(file->strPath);
2123 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
2124 return INFO_NOT_FOUND; //! @todo should we just skip to the next episode?
2126 // Only set season/epnum from filename when it is not already set by a scraper
2127 if (item.GetVideoInfoTag()->m_iSeason == -1)
2128 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
2129 if (item.GetVideoInfoTag()->m_iEpisode == -1)
2130 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
2132 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
2133 return INFO_ERROR;
2135 else
2137 CLog::Log(
2138 LOGDEBUG,
2139 "{} - no match for show: '{}', season: {}, episode: {}.{}, airdate: '{}', title: '{}'",
2140 __FUNCTION__, showInfo.m_strTitle, file->iSeason, file->iEpisode, file->iSubepisode,
2141 file->cDate.GetAsLocalizedDate(), file->strTitle);
2144 return INFO_ADDED;
2147 bool CVideoInfoScanner::GetDetails(CFileItem* pItem,
2148 const std::unordered_map<std::string, std::string>& uniqueIDs,
2149 CScraperUrl& url,
2150 const ScraperPtr& scraper,
2151 IVideoInfoTagLoader* loader,
2152 CGUIDialogProgress* pDialog /* = NULL */)
2154 CVideoInfoTag movieDetails;
2156 if (m_handle && !url.GetTitle().empty())
2157 m_handle->SetText(url.GetTitle());
2159 CVideoInfoDownloader imdb(scraper);
2160 bool ret = imdb.GetDetails(uniqueIDs, url, movieDetails, pDialog);
2162 if (ret)
2164 if (loader)
2165 loader->Load(movieDetails, true);
2167 if (m_handle && url.GetTitle().empty())
2168 m_handle->SetText(movieDetails.m_strTitle);
2170 if (pDialog)
2172 if (!pDialog->HasText())
2173 pDialog->SetLine(0, CVariant{movieDetails.m_strTitle});
2174 pDialog->Progress();
2177 *pItem->GetVideoInfoTag() = movieDetails;
2178 return true;
2180 return false; // no info found, or cancelled
2183 void CVideoInfoScanner::ApplyThumbToFolder(const std::string &folder, const std::string &imdbThumb)
2185 // copy icon to folder also;
2186 if (!imdbThumb.empty())
2188 CFileItem folderItem(folder, true);
2189 CThumbLoader loader;
2190 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
2194 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash)
2196 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
2197 if (0 == items.Size()) return 0;
2198 CDigest digest{CDigest::Type::MD5};
2199 int count = 0;
2200 for (int i = 0; i < items.Size(); ++i)
2202 const CFileItemPtr pItem = items[i];
2203 digest.Update(pItem->GetPath());
2204 if (pItem->IsPlugin())
2206 // allow plugin to calculate hash itself using strings rather than binary data for size and date
2207 // according to ListItem.setInfo() documentation date format should be "d.m.Y"
2208 if (pItem->m_dwSize)
2209 digest.Update(std::to_string(pItem->m_dwSize));
2210 if (pItem->m_dateTime.IsValid())
2211 digest.Update(StringUtils::Format("{:02}.{:02}.{:04}", pItem->m_dateTime.GetDay(),
2212 pItem->m_dateTime.GetMonth(),
2213 pItem->m_dateTime.GetYear()));
2215 else
2217 digest.Update(&pItem->m_dwSize, sizeof(pItem->m_dwSize));
2218 KODI::TIME::FileTime time = pItem->m_dateTime;
2219 digest.Update(&time, sizeof(KODI::TIME::FileTime));
2221 if (IsVideo(*pItem) && !PLAYLIST::IsPlayList(*pItem) && !pItem->IsNFO())
2222 count++;
2224 hash = digest.Finalize();
2225 return count;
2228 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items, const std::vector<std::string> &excludes) const
2230 if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash || items.IsPlugin())
2231 return false;
2233 for (int i = 0; i < items.Size(); ++i)
2235 if (items[i]->m_bIsFolder && !CUtil::ExcludeFileOrFolder(items[i]->GetPath(), excludes))
2236 return false;
2238 return true;
2241 std::string CVideoInfoScanner::GetFastHash(const std::string &directory,
2242 const std::vector<std::string> &excludes) const
2244 CDigest digest{CDigest::Type::MD5};
2246 if (excludes.size())
2247 digest.Update(StringUtils::Join(excludes, "|"));
2249 struct __stat64 buffer;
2250 if (XFILE::CFile::Stat(directory, &buffer) == 0)
2252 int64_t time = buffer.st_mtime;
2253 if (!time)
2254 time = buffer.st_ctime;
2255 if (time)
2257 digest.Update((unsigned char *)&time, sizeof(time));
2258 return digest.Finalize();
2261 return "";
2264 std::string CVideoInfoScanner::GetRecursiveFastHash(const std::string &directory,
2265 const std::vector<std::string> &excludes) const
2267 CFileItemList items;
2268 items.Add(std::make_shared<CFileItem>(directory, true));
2269 CUtil::GetRecursiveDirsListing(directory, items, DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
2271 CDigest digest{CDigest::Type::MD5};
2273 if (excludes.size())
2274 digest.Update(StringUtils::Join(excludes, "|"));
2276 int64_t time = 0;
2277 for (int i=0; i < items.Size(); ++i)
2279 int64_t stat_time = 0;
2280 struct __stat64 buffer;
2281 if (XFILE::CFile::Stat(items[i]->GetPath(), &buffer) == 0)
2283 //! @todo some filesystems may return the mtime/ctime inline, in which case this is
2284 //! unnecessarily expensive. Consider supporting Stat() in our directory cache?
2285 stat_time = buffer.st_mtime ? buffer.st_mtime : buffer.st_ctime;
2286 time += stat_time;
2289 if (!stat_time)
2290 return "";
2293 if (time)
2295 digest.Update((unsigned char *)&time, sizeof(time));
2296 return digest.Finalize();
2298 return "";
2301 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show,
2302 std::map<int, std::map<std::string, std::string>> &seasonArt, const std::vector<std::string> &artTypes, bool useLocal)
2304 int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->
2305 GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
2306 bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
2307 bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
2308 if (useLocal)
2310 // find the maximum number of seasons we have local thumbs for
2311 int maxSeasons = 0;
2312 CFileItemList items;
2313 std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
2314 if (!show.m_strPath.empty())
2316 CDirectory::GetDirectory(show.m_strPath, items, extensions,
2317 DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE |
2318 DIR_FLAG_NO_FILE_INFO);
2320 extensions.erase(std::remove(extensions.begin(), extensions.end(), '.'), extensions.end());
2321 CRegExp reg;
2322 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z0-9]+)?\\.(" + extensions + ")"))
2324 for (const auto& item : items)
2326 std::string name = URIUtils::GetFileName(item->GetPath());
2327 if (reg.RegFind(name) > -1)
2329 int season = atoi(reg.GetMatch(1).c_str());
2330 if (season > maxSeasons)
2331 maxSeasons = season;
2335 for (int season = -1; season <= maxSeasons; season++)
2337 // skip if we already have some art
2338 std::map<int, std::map<std::string, std::string>>::const_iterator it = seasonArt.find(season);
2339 if (it != seasonArt.end() && !it->second.empty())
2340 continue;
2342 std::map<std::string, std::string> art;
2343 std::string basePath;
2344 if (season == -1)
2345 basePath = "season-all";
2346 else if (season == 0)
2347 basePath = "season-specials";
2348 else
2349 basePath = StringUtils::Format("season{:02}", season);
2351 AddLocalItemArtwork(art, artTypes,
2352 URIUtils::AddFileToFolder(show.m_strPath, basePath),
2353 addAll, exactName);
2355 seasonArt[season] = art;
2358 // add online art
2359 for (const auto& url : show.m_strPictureURL.GetUrls())
2361 if (url.m_type != CScraperUrl::UrlType::Season)
2362 continue;
2363 std::string aspect = url.m_aspect;
2364 if (aspect.empty())
2365 aspect = "thumb";
2366 std::map<std::string, std::string>& art = seasonArt[url.m_season];
2367 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
2368 art.find(aspect) == art.end())
2370 std::string image = CScraperUrl::GetThumbUrl(url);
2371 if (!image.empty())
2372 art.insert(std::make_pair(aspect, image));
2377 void CVideoInfoScanner::FetchActorThumbs(std::vector<SActorInfo>& actors, const std::string& strPath)
2379 CFileItemList items;
2380 // don't try to fetch anything local with plugin source
2381 if (!URIUtils::IsPlugin(strPath))
2383 std::string actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
2384 if (CDirectory::Exists(actorsDir))
2385 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
2386 DIR_FLAG_NO_FILE_INFO);
2388 for (std::vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
2390 if (i->thumb.empty())
2392 std::string thumbFile = i->strName;
2393 StringUtils::Replace(thumbFile, ' ', '_');
2394 for (int j = 0; j < items.Size(); j++)
2396 std::string compare = URIUtils::GetFileName(items[j]->GetPath());
2397 URIUtils::RemoveExtension(compare);
2398 if (!items[j]->m_bIsFolder && compare == thumbFile)
2400 i->thumb = items[j]->GetPath();
2401 break;
2404 if (i->thumb.empty() && !i->thumbUrl.GetFirstUrlByType().m_url.empty())
2405 i->thumb = CScraperUrl::GetThumbUrl(i->thumbUrl.GetFirstUrlByType());
2406 if (!i->thumb.empty())
2407 CServiceBroker::GetTextureCache()->BackgroundCacheImage(i->thumb);
2412 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
2414 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoScannerIgnoreErrors)
2415 return true;
2417 if (pDialog)
2419 HELPERS::ShowOKDialogText(CVariant{20448}, CVariant{20449});
2420 return false;
2422 return HELPERS::ShowYesNoDialogText(CVariant{20448}, CVariant{20450}) ==
2423 DialogResponse::CHOICE_YES;
2426 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const std::string &line1)
2428 if (progress)
2430 progress->SetHeading(CVariant{heading});
2431 progress->SetLine(0, CVariant{line1});
2432 progress->Progress();
2433 return progress->IsCanceled();
2435 return m_bStop;
2438 int CVideoInfoScanner::FindVideo(const std::string &title, int year, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
2440 MOVIELIST movielist;
2441 CVideoInfoDownloader imdb(scraper);
2442 int returncode = imdb.FindMovie(title, year, movielist, progress);
2443 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
2444 { // scraper reported an error, or we had an error and user wants to cancel the scan
2445 m_bStop = true;
2446 return -1; // cancelled
2448 if (returncode > 0 && movielist.size())
2450 url = movielist[0];
2451 return 1; // found a movie
2453 return 0; // didn't find anything
2456 bool CVideoInfoScanner::AddVideoExtras(CFileItemList& items,
2457 const CONTENT_TYPE& content,
2458 const std::string& path)
2460 int dbId = -1;
2462 // get the library item which was added previously with the specified conent type
2463 for (const auto& item : items)
2465 if (content == CONTENT_MOVIES)
2467 dbId = m_database.GetMovieId(item->GetPath());
2468 if (dbId != -1)
2470 break;
2475 if (dbId == -1)
2477 CLog::Log(LOGERROR, "VideoInfoScanner: Failed to find the library item for video extras {}",
2478 CURL::GetRedacted(path));
2479 return false;
2482 // Add video extras to library
2483 CDirectory::EnumerateDirectory(
2484 path,
2485 [this, content, dbId, path](const std::shared_ptr<CFileItem>& item)
2487 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2488 CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
2490 CDVDFileInfo::GetFileStreamDetails(item.get());
2491 CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}",
2492 CURL::GetRedacted(item->GetPath()));
2495 const std::string typeVideoVersion =
2496 CGUIDialogVideoManagerExtras::GenerateVideoExtra(path, item->GetPath());
2498 const int idVideoVersion = m_database.AddVideoVersionType(
2499 typeVideoVersion, VideoAssetTypeOwner::AUTO, VideoAssetType::EXTRA);
2501 GetArtwork(item.get(), content, true, true, "");
2503 if (m_database.AddVideoAsset(ContentToVideoDbType(content), dbId, idVideoVersion,
2504 VideoAssetType::EXTRA, *item.get()))
2506 CLog::Log(LOGDEBUG, "VideoInfoScanner: Added video extra {}",
2507 CURL::GetRedacted(item->GetPath()));
2509 else
2511 CLog::Log(LOGERROR, "VideoInfoScanner: Failed to add video extra {}",
2512 CURL::GetRedacted(item->GetPath()));
2515 [](auto) { return true; }, true,
2516 CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), DIR_FLAG_DEFAULTS);
2518 return true;
2521 bool CVideoInfoScanner::ProcessVideoVersion(VideoDbContentType itemType, int dbId)
2523 return CGUIDialogVideoManagerVersions::ProcessVideoVersion(itemType, dbId);