Merge pull request #25083 from CrystalP/fix-versiondelete
[xbmc.git] / xbmc / video / VideoInfoScanner.cpp
blob4e9eabbbc2d9afca4944d0613dc96b5a05b4f99b
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 "interfaces/AnnouncementManager.h"
33 #include "messaging/helpers/DialogHelper.h"
34 #include "messaging/helpers/DialogOKHelper.h"
35 #include "settings/AdvancedSettings.h"
36 #include "settings/Settings.h"
37 #include "settings/SettingsComponent.h"
38 #include "tags/VideoInfoTagLoaderFactory.h"
39 #include "utils/Digest.h"
40 #include "utils/FileExtensionProvider.h"
41 #include "utils/RegExp.h"
42 #include "utils/StringUtils.h"
43 #include "utils/URIUtils.h"
44 #include "utils/Variant.h"
45 #include "utils/log.h"
46 #include "video/VideoFileItemClassify.h"
47 #include "video/VideoManagerTypes.h"
48 #include "video/VideoThumbLoader.h"
49 #include "video/dialogs/GUIDialogVideoManagerExtras.h"
50 #include "video/dialogs/GUIDialogVideoManagerVersions.h"
52 #include <algorithm>
53 #include <memory>
54 #include <utility>
56 using namespace XFILE;
57 using namespace ADDON;
58 using namespace KODI::MESSAGING;
59 using namespace KODI::VIDEO;
61 using KODI::MESSAGING::HELPERS::DialogResponse;
62 using KODI::UTILITY::CDigest;
64 namespace KODI::VIDEO
67 CVideoInfoScanner::CVideoInfoScanner()
69 m_bStop = false;
70 m_scanAll = false;
72 const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
74 m_ignoreVideoVersions = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_IGNOREVIDEOVERSIONS);
75 m_ignoreVideoExtras = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_IGNOREVIDEOEXTRAS);
78 CVideoInfoScanner::~CVideoInfoScanner()
79 = default;
81 void CVideoInfoScanner::Process()
83 m_bStop = false;
85 try
87 const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
89 if (m_showDialog && !settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE))
91 CGUIDialogExtendedProgressBar* dialog =
92 CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
93 if (dialog)
94 m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
97 // check if we only need to perform a cleaning
98 if (m_bClean && m_pathsToScan.empty())
100 std::set<int> paths;
101 m_database.CleanDatabase(m_handle, paths, false);
103 if (m_handle)
104 m_handle->MarkFinished();
105 m_handle = NULL;
107 m_bRunning = false;
109 return;
112 auto start = std::chrono::steady_clock::now();
114 m_database.Open();
116 m_bCanInterrupt = true;
118 CLog::Log(LOGINFO, "VideoInfoScanner: Starting scan ..");
119 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
120 "OnScanStarted");
122 // Database operations should not be canceled
123 // using Interrupt() while scanning as it could
124 // result in unexpected behaviour.
125 m_bCanInterrupt = false;
127 bool bCancelled = false;
128 while (!bCancelled && !m_pathsToScan.empty())
131 * A copy of the directory path is used because the path supplied is
132 * immediately removed from the m_pathsToScan set in DoScan(). If the
133 * reference points to the entry in the set a null reference error
134 * occurs.
136 std::string directory = *m_pathsToScan.begin();
137 if (m_bStop)
139 bCancelled = true;
141 else if (!CDirectory::Exists(directory))
144 * Note that this will skip clean (if m_bClean is enabled) if the directory really
145 * doesn't exist rather than a NAS being switched off. A manual clean from settings
146 * will still pick up and remove it though.
148 CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan{}.", __FUNCTION__,
149 CURL::GetRedacted(directory), m_bClean ? " and clean" : "");
150 m_pathsToScan.erase(m_pathsToScan.begin());
152 else if (!DoScan(directory))
153 bCancelled = true;
156 if (!bCancelled)
158 if (m_bClean)
159 m_database.CleanDatabase(m_handle, m_pathsToClean, false);
160 else
162 if (m_handle)
163 m_handle->SetTitle(g_localizeStrings.Get(331));
164 m_database.Compress(false);
168 CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
169 m_database.Close();
171 auto end = std::chrono::steady_clock::now();
172 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
174 CLog::Log(LOGINFO, "VideoInfoScanner: Finished scan. Scanning for video info took {} ms",
175 duration.count());
177 catch (...)
179 CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
182 m_bRunning = false;
183 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
184 "OnScanFinished");
186 if (m_handle)
187 m_handle->MarkFinished();
188 m_handle = NULL;
191 void CVideoInfoScanner::Start(const std::string& strDirectory, bool scanAll)
193 m_strStartDir = strDirectory;
194 m_scanAll = scanAll;
195 m_pathsToScan.clear();
196 m_pathsToClean.clear();
198 m_database.Open();
199 if (strDirectory.empty())
200 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
201 // we go.
202 m_database.GetPaths(m_pathsToScan);
204 else
205 { // scan all the paths of this subtree that is in the database
206 std::vector<std::string> rootDirs;
207 if (URIUtils::IsMultiPath(strDirectory))
208 CMultiPathDirectory::GetPaths(strDirectory, rootDirs);
209 else
210 rootDirs.push_back(strDirectory);
212 for (std::vector<std::string>::const_iterator it = rootDirs.begin(); it < rootDirs.end(); ++it)
214 m_pathsToScan.insert(*it);
215 std::vector<std::pair<int, std::string>> subpaths;
216 m_database.GetSubPaths(*it, subpaths);
217 for (std::vector<std::pair<int, std::string>>::iterator it = subpaths.begin(); it < subpaths.end(); ++it)
218 m_pathsToScan.insert(it->second);
221 m_database.Close();
222 m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryCleanOnUpdate;
224 m_bRunning = true;
225 Process();
228 void CVideoInfoScanner::Stop()
230 if (m_bCanInterrupt)
231 m_database.Interrupt();
233 m_bStop = true;
236 static void OnDirectoryScanned(const std::string& strDirectory)
238 CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
239 msg.SetStringParam(strDirectory);
240 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
243 bool CVideoInfoScanner::DoScan(const std::string& strDirectory)
245 if (m_handle)
247 m_handle->SetText(g_localizeStrings.Get(20415));
251 * Remove this path from the list we're processing. This must be done prior to
252 * the check for file or folder exclusion to prevent an infinite while loop
253 * in Process().
255 std::set<std::string>::iterator it = m_pathsToScan.find(strDirectory);
256 if (it != m_pathsToScan.end())
257 m_pathsToScan.erase(it);
259 // load subfolder
260 CFileItemList items;
261 bool foundDirectly = false;
262 bool bSkip = false;
264 SScanSettings settings;
265 ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
266 CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
268 // exclude folders that match our exclude regexps
269 const std::vector<std::string> &regexps = content == CONTENT_TVSHOWS ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
270 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps;
272 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
273 return true;
275 if (HasNoMedia(strDirectory))
276 return true;
278 bool ignoreFolder = !m_scanAll && settings.noupdate;
279 if (content == CONTENT_NONE || ignoreFolder)
280 return true;
282 if (URIUtils::IsPlugin(strDirectory) && !CPluginDirectory::IsMediaLibraryScanningAllowed(TranslateContent(content), strDirectory))
284 CLog::Log(
285 LOGINFO,
286 "VideoInfoScanner: Plugin '{}' does not support media library scanning for '{}' content",
287 CURL::GetRedacted(strDirectory), TranslateContent(content));
288 return true;
291 std::string hash, dbHash;
292 if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
294 if (m_handle)
296 int str = content == CONTENT_MOVIES ? 20317:20318;
297 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name()));
300 std::string fastHash;
301 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash && !URIUtils::IsPlugin(strDirectory))
302 fastHash = GetFastHash(strDirectory, regexps);
304 if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && StringUtils::EqualsNoCase(fastHash, dbHash))
305 { // fast hashes match - no need to process anything
306 hash = fastHash;
308 else
309 { // need to fetch the folder
310 CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
311 DIR_FLAG_DEFAULTS);
312 // do not consider inner folders with .nomedia
313 items.erase(std::remove_if(items.begin(), items.end(),
314 [this](const CFileItemPtr& item) {
315 return item->m_bIsFolder && HasNoMedia(item->GetPath());
317 items.end());
318 items.Stack();
320 // check whether to re-use previously computed fast hash
321 if (!CanFastHash(items, regexps) || fastHash.empty())
322 GetPathHash(items, hash);
323 else
324 hash = fastHash;
327 if (StringUtils::EqualsNoCase(hash, dbHash))
328 { // hash matches - skipping
329 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change{}",
330 CURL::GetRedacted(strDirectory), !fastHash.empty() ? " (fasthash)" : "");
331 bSkip = true;
333 else if (hash.empty())
334 { // directory empty or non-existent - add to clean list and skip
335 CLog::Log(LOGDEBUG,
336 "VideoInfoScanner: Skipping dir '{}' as it's empty or doesn't exist - adding to "
337 "clean list",
338 CURL::GetRedacted(strDirectory));
339 if (m_bClean)
340 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
341 bSkip = true;
343 else if (dbHash.empty())
344 { // new folder - scan
345 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
346 CURL::GetRedacted(strDirectory));
348 else
349 { // hash changed - rescan
350 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
351 CURL::GetRedacted(strDirectory), dbHash, hash);
354 else if (content == CONTENT_TVSHOWS)
356 if (m_handle)
357 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name()));
359 if (foundDirectly && !settings.parent_name_root)
361 CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
362 DIR_FLAG_DEFAULTS);
363 items.SetPath(strDirectory);
364 GetPathHash(items, hash);
365 bSkip = true;
366 if (!m_database.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash))
367 bSkip = false;
368 else
369 items.Clear();
371 else
373 CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
374 item->SetPath(strDirectory);
375 item->m_bIsFolder = true;
376 items.Add(item);
377 items.SetPath(URIUtils::GetParentPath(item->GetPath()));
380 bool foundSomething = false;
381 if (!bSkip)
383 foundSomething = RetrieveVideoInfo(items, settings.parent_name_root, content);
384 if (foundSomething)
386 if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
388 m_database.SetPathHash(strDirectory, hash);
389 if (m_bClean)
390 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
391 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir {}",
392 CURL::GetRedacted(strDirectory));
395 else
397 if (m_bClean)
398 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
399 CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir {}",
400 CURL::GetRedacted(strDirectory));
403 else if (!StringUtils::EqualsNoCase(hash, dbHash) && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
404 { // update the hash either way - we may have changed the hash to a fast version
405 m_database.SetPathHash(strDirectory, hash);
408 if (m_handle)
409 OnDirectoryScanned(strDirectory);
411 for (int i = 0; i < items.Size(); ++i)
413 CFileItemPtr pItem = items[i];
415 if (m_bStop)
416 break;
418 // add video extras to library
419 if (foundSomething && !m_ignoreVideoExtras && IsVideoExtrasFolder(*pItem))
421 if (AddVideoExtras(items, content, pItem->GetPath()))
423 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding video extras from dir {}",
424 CURL::GetRedacted(pItem->GetPath()));
427 // no further processing required
428 continue;
431 // if we have a directory item (non-playlist) we then recurse into that folder
432 // do not recurse for tv shows - we have already looked recursively for episodes
433 if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList() && settings.recurse > 0 && content != CONTENT_TVSHOWS)
435 if (!DoScan(pItem->GetPath()))
437 m_bStop = true;
441 return !m_bStop;
444 bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
446 if (pDlgProgress)
448 if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
450 pDlgProgress->ShowProgressBar(true);
451 pDlgProgress->SetPercentage(0);
453 else
454 pDlgProgress->ShowProgressBar(false);
456 pDlgProgress->Progress();
459 m_database.Open();
461 bool FoundSomeInfo = false;
462 std::vector<int> seenPaths;
463 for (int i = 0; i < items.Size(); ++i)
465 CFileItemPtr pItem = items[i];
467 // we do this since we may have a override per dir
468 ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath());
469 if (!info2) // skip
470 continue;
472 // Discard all .nomedia folders
473 if (pItem->m_bIsFolder && HasNoMedia(pItem->GetPath()))
474 continue;
476 // Discard all exclude files defined by regExExclude
477 if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
478 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps))
479 continue;
481 if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
483 if (m_handle)
484 m_handle->SetPercentage(i*100.f/items.Size());
487 // clear our scraper cache
488 info2->ClearCache();
490 INFO_RET ret = INFO_CANCELLED;
491 if (info2->Content() == CONTENT_TVSHOWS)
492 ret = RetrieveInfoForTvShow(pItem.get(), bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress);
493 else if (info2->Content() == CONTENT_MOVIES)
494 ret = RetrieveInfoForMovie(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
495 else if (info2->Content() == CONTENT_MUSICVIDEOS)
496 ret = RetrieveInfoForMusicVideo(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
497 else
499 CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type {} ({})", info2->Content(),
500 CURL::GetRedacted(pItem->GetPath()));
501 FoundSomeInfo = false;
502 break;
504 if (ret == INFO_CANCELLED || ret == INFO_ERROR)
506 CLog::Log(LOGWARNING,
507 "VideoInfoScanner: Error {} occurred while retrieving"
508 "information for {}.",
509 ret, CURL::GetRedacted(pItem->GetPath()));
510 FoundSomeInfo = false;
511 break;
513 if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
514 FoundSomeInfo = true;
515 else if (ret == INFO_NOT_FOUND)
517 CLog::Log(LOGWARNING,
518 "No information found for item '{}', it won't be added to the library.",
519 CURL::GetRedacted(pItem->GetPath()));
521 MediaType mediaType = MediaTypeMovie;
522 if (info2->Content() == CONTENT_TVSHOWS)
523 mediaType = MediaTypeTvShow;
524 else if (info2->Content() == CONTENT_MUSICVIDEOS)
525 mediaType = MediaTypeMusicVideo;
527 auto eventLog = CServiceBroker::GetEventLog();
528 if (eventLog)
530 const std::string itemlogpath = (info2->Content() == CONTENT_TVSHOWS)
531 ? CURL::GetRedacted(pItem->GetPath())
532 : URIUtils::GetFileName(pItem->GetPath());
534 eventLog->Add(EventPtr(new CMediaLibraryEvent(
535 mediaType, pItem->GetPath(), 24145,
536 StringUtils::Format(g_localizeStrings.Get(24147), mediaType, itemlogpath),
537 EventLevel::Warning)));
541 pURL = NULL;
543 // Keep track of directories we've seen
544 if (m_bClean && pItem->m_bIsFolder)
545 seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
548 if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
550 std::vector<std::pair<int, std::string>> libPaths;
551 m_database.GetSubPaths(items.GetPath(), libPaths);
552 for (std::vector<std::pair<int, std::string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
554 if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
555 m_pathsToClean.insert(i->first);
558 if(pDlgProgress)
559 pDlgProgress->ShowProgressBar(false);
561 m_database.Close();
562 return FoundSomeInfo;
565 CInfoScanner::INFO_RET
566 CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem,
567 bool bDirNames,
568 ScraperPtr &info2,
569 bool useLocal,
570 CScraperUrl* pURL,
571 bool fetchEpisodes,
572 CGUIDialogProgress* pDlgProgress)
574 const bool isSeason =
575 pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_type == MediaTypeSeason;
577 int idTvShow = -1;
578 int idSeason = -1;
579 std::string strPath = pItem->GetPath();
580 if (pItem->m_bIsFolder)
582 idTvShow = m_database.GetTvShowId(strPath);
583 if (isSeason && idTvShow > -1)
584 idSeason = m_database.GetSeasonId(idTvShow, pItem->GetVideoInfoTag()->m_iSeason);
586 else if (pItem->IsPlugin() && pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_iIdShow >= 0)
588 // for plugin source we cannot get idTvShow from episode path with URIUtils::GetDirectory() in all cases
589 // so use m_iIdShow from video info tag if possible
590 idTvShow = pItem->GetVideoInfoTag()->m_iIdShow;
591 CVideoInfoTag showInfo;
592 if (m_database.GetTvShowInfo(std::string(), showInfo, idTvShow, nullptr, 0))
593 strPath = showInfo.GetPath();
595 else
597 strPath = URIUtils::GetDirectory(strPath);
598 idTvShow = m_database.GetTvShowId(strPath);
599 if (isSeason && idTvShow > -1)
600 idSeason = m_database.GetSeasonId(idTvShow, pItem->GetVideoInfoTag()->m_iSeason);
602 if (idTvShow > -1 && (!isSeason || idSeason > -1) && (fetchEpisodes || !pItem->m_bIsFolder))
604 INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
605 if (ret == INFO_ADDED)
606 m_database.SetPathHash(strPath, pItem->GetProperty("hash").asString());
607 return ret;
610 if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361,
611 pItem->m_bIsFolder ? pItem->GetVideoInfoTag()->m_strShowTitle
612 : pItem->GetVideoInfoTag()->m_strTitle))
613 return INFO_CANCELLED;
615 if (m_handle)
616 m_handle->SetText(pItem->GetMovieName(bDirNames));
618 CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
619 CScraperUrl scrUrl;
620 // handle .nfo files
621 std::unique_ptr<IVideoInfoTagLoader> loader;
622 if (useLocal)
624 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
625 if (loader)
627 pItem->GetVideoInfoTag()->Reset();
628 result = loader->Load(*pItem->GetVideoInfoTag(), false);
632 if (result == CInfoScanner::FULL_NFO)
635 long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
636 if (lResult < 0)
637 return INFO_ERROR;
638 if (fetchEpisodes)
640 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
641 if (ret == INFO_ADDED)
642 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
643 return ret;
645 return INFO_ADDED;
647 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
649 scrUrl = loader->ScraperUrl();
650 pURL = &scrUrl;
653 CScraperUrl url;
654 int retVal = 0;
655 std::string movieTitle = pItem->GetMovieName(bDirNames);
656 int movieYear = -1; // hint that movie title was not found
657 if (result == CInfoScanner::TITLE_NFO)
659 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
660 movieTitle = tag->GetTitle();
661 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
664 std::string identifierType;
665 std::string identifier;
666 long lResult = -1;
667 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
669 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
670 if (GetDetails(pItem, uniqueIDs, url, info2,
671 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
672 ? loader.get()
673 : nullptr,
674 pDlgProgress))
676 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
677 return INFO_ERROR;
679 if (fetchEpisodes)
681 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
682 if (ret == INFO_ADDED)
684 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
685 return INFO_ADDED;
688 return INFO_ADDED;
692 if (pURL && pURL->HasUrls())
693 url = *pURL;
694 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
695 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
697 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
698 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
699 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
701 if (GetDetails(pItem, {}, url, info2,
702 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
703 ? loader.get()
704 : nullptr,
705 pDlgProgress))
707 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
708 return INFO_ERROR;
710 if (fetchEpisodes)
712 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
713 if (ret == INFO_ADDED)
714 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
716 return INFO_ADDED;
719 CInfoScanner::INFO_RET
720 CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem,
721 bool bDirNames,
722 ScraperPtr &info2,
723 bool useLocal,
724 CScraperUrl* pURL,
725 CGUIDialogProgress* pDlgProgress)
727 if (pItem->m_bIsFolder || !IsVideo(*pItem) || pItem->IsNFO() ||
728 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
729 return INFO_NOT_NEEDED;
731 if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
732 return INFO_CANCELLED;
734 if (m_database.HasMovieInfo(pItem->GetDynPath()))
735 return INFO_HAVE_ALREADY;
737 if (m_handle)
738 m_handle->SetText(pItem->GetMovieName(bDirNames));
740 CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
741 CScraperUrl scrUrl;
742 // handle .nfo files
743 std::unique_ptr<IVideoInfoTagLoader> loader;
744 if (useLocal)
746 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
747 if (loader)
749 pItem->GetVideoInfoTag()->Reset();
750 result = loader->Load(*pItem->GetVideoInfoTag(), false);
753 if (result == CInfoScanner::FULL_NFO)
755 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, true);
756 if (dbId < 0)
757 return INFO_ERROR;
758 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
759 return INFO_HAVE_ALREADY;
760 return INFO_ADDED;
762 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
764 scrUrl = loader->ScraperUrl();
765 pURL = &scrUrl;
768 CScraperUrl url;
769 int retVal = 0;
770 std::string movieTitle = pItem->GetMovieName(bDirNames);
771 int movieYear = -1; // hint that movie title was not found
772 if (result == CInfoScanner::TITLE_NFO)
774 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
775 movieTitle = tag->GetTitle();
776 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
779 std::string identifierType;
780 std::string identifier;
781 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
783 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
784 if (GetDetails(pItem, uniqueIDs, url, info2,
785 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
786 ? loader.get()
787 : nullptr,
788 pDlgProgress))
790 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
791 if (dbId < 0)
792 return INFO_ERROR;
793 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
794 return INFO_HAVE_ALREADY;
795 return INFO_ADDED;
799 if (pURL && pURL->HasUrls())
800 url = *pURL;
801 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
802 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
804 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
805 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
807 if (GetDetails(pItem, {}, url, info2,
808 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
809 ? loader.get()
810 : nullptr,
811 pDlgProgress))
813 const int dbId = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
814 if (dbId < 0)
815 return INFO_ERROR;
816 if (!m_ignoreVideoVersions && ProcessVideoVersion(VideoDbContentType::MOVIES, dbId))
817 return INFO_HAVE_ALREADY;
818 return INFO_ADDED;
820 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
821 return INFO_NOT_FOUND;
824 CInfoScanner::INFO_RET
825 CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem,
826 bool bDirNames,
827 ScraperPtr &info2,
828 bool useLocal,
829 CScraperUrl* pURL,
830 CGUIDialogProgress* pDlgProgress)
832 if (pItem->m_bIsFolder || !IsVideo(*pItem) || pItem->IsNFO() ||
833 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
834 return INFO_NOT_NEEDED;
836 if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
837 return INFO_CANCELLED;
839 if (m_database.HasMusicVideoInfo(pItem->GetPath()))
840 return INFO_HAVE_ALREADY;
842 if (m_handle)
843 m_handle->SetText(pItem->GetMovieName(bDirNames));
845 CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
846 CScraperUrl scrUrl;
847 // handle .nfo files
848 std::unique_ptr<IVideoInfoTagLoader> loader;
849 if (useLocal)
851 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
852 if (loader)
854 pItem->GetVideoInfoTag()->Reset();
855 result = loader->Load(*pItem->GetVideoInfoTag(), false);
858 if (result == CInfoScanner::FULL_NFO)
860 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
861 return INFO_ERROR;
862 return INFO_ADDED;
864 if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
866 scrUrl = loader->ScraperUrl();
867 pURL = &scrUrl;
870 CScraperUrl url;
871 int retVal = 0;
872 std::string movieTitle = pItem->GetMovieName(bDirNames);
873 int movieYear = -1; // hint that movie title was not found
874 if (result == CInfoScanner::TITLE_NFO)
876 CVideoInfoTag* tag = pItem->GetVideoInfoTag();
877 movieTitle = tag->GetTitle();
878 movieYear = tag->GetYear(); // movieYear is expected to be >= 0
881 std::string identifierType;
882 std::string identifier;
883 if (info2->IsPython() && CUtil::GetFilenameIdentifier(movieTitle, identifierType, identifier))
885 const std::unordered_map<std::string, std::string> uniqueIDs{{identifierType, identifier}};
886 if (GetDetails(pItem, uniqueIDs, url, info2,
887 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
888 ? loader.get()
889 : nullptr,
890 pDlgProgress))
892 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
893 return INFO_ERROR;
894 return INFO_ADDED;
898 if (pURL && pURL->HasUrls())
899 url = *pURL;
900 else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
901 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
903 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
904 url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
906 if (GetDetails(pItem, {}, url, info2,
907 (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
908 ? loader.get()
909 : nullptr,
910 pDlgProgress))
912 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
913 return INFO_ERROR;
914 return INFO_ADDED;
916 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
917 return INFO_NOT_FOUND;
920 CInfoScanner::INFO_RET
921 CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item,
922 long showID,
923 const ADDON::ScraperPtr &scraper,
924 bool useLocal,
925 CGUIDialogProgress *progress)
927 // enumerate episodes
928 EPISODELIST files;
929 if (!EnumerateSeriesFolder(item, files))
930 return INFO_HAVE_ALREADY;
931 if (files.empty()) // no update or no files
932 return INFO_NOT_NEEDED;
934 if (m_bStop || (progress && progress->IsCanceled()))
935 return INFO_CANCELLED;
937 CVideoInfoTag showInfo;
938 m_database.GetTvShowInfo("", showInfo, showID);
939 INFO_RET ret = OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
941 if (ret == INFO_ADDED)
943 std::map<int, std::map<std::string, std::string>> seasonArt;
944 m_database.GetTvShowSeasonArt(showID, seasonArt);
946 bool updateSeasonArt = false;
947 for (std::map<int, std::map<std::string, std::string>>::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
949 if (i->second.empty())
951 updateSeasonArt = true;
952 break;
956 if (updateSeasonArt)
958 if (!item->IsPlugin() || scraper->ID() != "metadata.local")
960 CVideoInfoDownloader loader(scraper);
961 loader.GetArtwork(showInfo);
963 GetSeasonThumbs(showInfo, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !item->IsPlugin());
964 for (std::map<int, std::map<std::string, std::string> >::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
966 int seasonID = m_database.AddSeason(showID, i->first);
967 m_database.SetArtForItem(seasonID, MediaTypeSeason, i->second);
971 return ret;
974 bool CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
976 CFileItemList items;
977 const std::vector<std::string> &regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps;
979 bool bSkip = false;
981 if (item->m_bIsFolder)
984 * Note: DoScan() will not remove this path as it's not recursing for tvshows.
985 * Remove this path from the list we're processing in order to avoid hitting
986 * it twice in the main loop.
988 std::set<std::string>::iterator it = m_pathsToScan.find(item->GetPath());
989 if (it != m_pathsToScan.end())
990 m_pathsToScan.erase(it);
992 if (HasNoMedia(item->GetPath()))
993 return true;
995 std::string hash, dbHash;
996 bool allowEmptyHash = false;
997 if (item->IsPlugin())
999 // if plugin has already calculated a hash for directory contents - use it
1000 // in this case we don't need to get directory listing from plugin for hash checking
1001 if (item->HasProperty("hash"))
1003 hash = item->GetProperty("hash").asString();
1004 allowEmptyHash = true;
1007 else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash)
1008 hash = GetRecursiveFastHash(item->GetPath(), regexps);
1010 if (m_database.GetPathHash(item->GetPath(), dbHash) && (allowEmptyHash || !hash.empty()) && StringUtils::EqualsNoCase(dbHash, hash))
1012 // fast hashes match - no need to process anything
1013 bSkip = true;
1016 // fast hash cannot be computed or we need to rescan. fetch the listing.
1017 if (!bSkip)
1019 int flags = DIR_FLAG_DEFAULTS;
1020 if (!hash.empty())
1021 flags |= DIR_FLAG_NO_FILE_INFO;
1023 // Listing that ignores files inside and below folders containing .nomedia files.
1024 CDirectory::EnumerateDirectory(
1025 item->GetPath(), [&items](const std::shared_ptr<CFileItem>& item) { items.Add(item); },
1026 [this](const std::shared_ptr<CFileItem>& folder)
1027 { return !HasNoMedia(folder->GetPath()); },
1028 true, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), flags);
1030 // fast hash failed - compute slow one
1031 if (hash.empty())
1033 GetPathHash(items, hash);
1034 if (StringUtils::EqualsNoCase(dbHash, hash))
1036 // slow hashes match - no need to process anything
1037 bSkip = true;
1042 if (bSkip)
1044 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change",
1045 CURL::GetRedacted(item->GetPath()));
1046 // update our dialog with our progress
1047 if (m_handle)
1048 OnDirectoryScanned(item->GetPath());
1049 return false;
1052 if (dbHash.empty())
1053 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
1054 CURL::GetRedacted(item->GetPath()));
1055 else
1056 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
1057 CURL::GetRedacted(item->GetPath()), dbHash, hash);
1059 if (m_bClean)
1061 m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
1062 m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
1064 item->SetProperty("hash", hash);
1066 else
1068 CFileItemPtr newItem(new CFileItem(*item));
1069 items.Add(newItem);
1073 stack down any dvd folders
1074 need to sort using the full path since this is a collapsed recursive listing of all subdirs
1075 video_ts.ifo files should sort at the top of a dvd folder in ascending order
1077 /foo/bar/video_ts.ifo
1078 /foo/bar/vts_x_y.ifo
1079 /foo/bar/vts_x_y.vob
1082 // since we're doing this now anyway, should other items be stacked?
1083 items.Sort(SortByPath, SortOrderAscending);
1085 // If found VIDEO_TS.IFO or INDEX.BDMV then we are dealing with Blu-ray or DVD files on disc
1086 // somewhere in the directory tree. Assume that all other files/folders in the same folder
1087 // with VIDEO_TS or BDMV can be ignored.
1088 // THere can be a BACKUP/INDEX.BDMV which needs to be ignored (and broke the old while loop here)
1090 // Get folders to remove
1091 std::vector<std::string> foldersToRemove;
1092 for (const auto& item : items)
1094 const std::string file = StringUtils::ToUpper(item->GetPath());
1095 if (file.find("VIDEO_TS.IFO") != std::string::npos)
1096 foldersToRemove.emplace_back(StringUtils::ToUpper(URIUtils::GetDirectory(file)));
1097 if (file.find("INDEX.BDMV") != std::string::npos &&
1098 file.find("BACKUP/INDEX.BDMV") == std::string::npos)
1099 foldersToRemove.emplace_back(
1100 StringUtils::ToUpper(URIUtils::GetParentPath(URIUtils::GetDirectory(file))));
1103 // Remove folders
1104 items.erase(
1105 std::remove_if(items.begin(), items.end(),
1106 [&](const CFileItemPtr& i)
1108 const std::string fileAndPath(StringUtils::ToUpper(i->GetPath()));
1109 std::string file;
1110 std::string path;
1111 URIUtils::Split(fileAndPath, path, file);
1112 return (std::count_if(foldersToRemove.begin(), foldersToRemove.end(),
1113 [&](const std::string& removePath)
1114 { return path.rfind(removePath, 0) == 0; }) > 0) &&
1115 file != "VIDEO_TS.IFO" &&
1116 (file != "INDEX.BDMV" ||
1117 fileAndPath.find("BACKUP/INDEX.BDMV") != std::string::npos);
1119 items.end());
1121 // enumerate
1122 for (int i=0;i<items.Size();++i)
1124 if (items[i]->m_bIsFolder)
1125 continue;
1126 std::string strPath = URIUtils::GetDirectory(items[i]->GetPath());
1127 URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
1129 if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strPath), "sample"))
1130 continue;
1132 // Discard all exclude files defined by regExExcludes
1133 if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
1134 continue;
1137 * Check if the media source has already set the season and episode or original air date in
1138 * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
1139 * any false positive matches.
1141 if (ProcessItemByVideoInfoTag(items[i].get(), episodeList))
1142 continue;
1144 if (!EnumerateEpisodeItem(items[i].get(), episodeList))
1145 CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file {}", CURL::GetRedacted(items[i]->GetPath()));
1147 return true;
1150 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
1152 if (!item->HasVideoInfoTag())
1153 return false;
1155 const CVideoInfoTag* tag = item->GetVideoInfoTag();
1156 bool isValid = false;
1158 * First check the season and episode number. This takes precedence over the original air
1159 * date and episode title. Must be a valid season and episode number combination.
1161 if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
1162 isValid = true;
1164 // episode 0 with non-zero season is valid! (e.g. prequel episode)
1165 if (item->IsPlugin() && tag->m_iSeason > 0 && tag->m_iEpisode >= 0)
1166 isValid = true;
1168 if (isValid)
1170 EPISODE episode;
1171 episode.strPath = item->GetPath();
1172 episode.iSeason = tag->m_iSeason;
1173 episode.iEpisode = tag->m_iEpisode;
1174 episode.isFolder = false;
1175 // save full item for plugin source
1176 if (item->IsPlugin())
1177 episode.item = std::make_shared<CFileItem>(*item);
1178 episodeList.push_back(episode);
1179 CLog::Log(LOGDEBUG, "{} - found match for: {}. Season {}, Episode {}", __FUNCTION__,
1180 CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode);
1181 return true;
1185 * Next preference is the first aired date. If it exists use that for matching the TV Show
1186 * information. Also set the title in case there are multiple matches for the first aired date.
1188 if (tag->m_firstAired.IsValid())
1190 EPISODE episode;
1191 episode.strPath = item->GetPath();
1192 episode.strTitle = tag->m_strTitle;
1193 episode.isFolder = false;
1195 * Set season and episode to -1 to indicate to use the aired date.
1197 episode.iSeason = -1;
1198 episode.iEpisode = -1;
1200 * The first aired date string must be parseable.
1202 episode.cDate = item->GetVideoInfoTag()->m_firstAired;
1203 episodeList.push_back(episode);
1204 CLog::Log(LOGDEBUG, "{} - found match for: '{}', firstAired: '{}' = '{}', title: '{}'",
1205 __FUNCTION__, CURL::GetRedacted(episode.strPath),
1206 tag->m_firstAired.GetAsDBDateTime(), episode.cDate.GetAsLocalizedDate(),
1207 episode.strTitle);
1208 return true;
1212 * Next preference is the episode title. If it exists use that for matching the TV Show
1213 * information.
1215 if (!tag->m_strTitle.empty())
1217 EPISODE episode;
1218 episode.strPath = item->GetPath();
1219 episode.strTitle = tag->m_strTitle;
1220 episode.isFolder = false;
1222 * Set season and episode to -1 to indicate to use the title.
1224 episode.iSeason = -1;
1225 episode.iEpisode = -1;
1226 episodeList.push_back(episode);
1227 CLog::Log(LOGDEBUG, "{} - found match for: '{}', title: '{}'", __FUNCTION__,
1228 CURL::GetRedacted(episode.strPath), episode.strTitle);
1229 return true;
1233 * There is no further episode information available if both the season and episode number have
1234 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
1235 * to the episode list.
1237 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
1239 CLog::Log(LOGDEBUG,
1240 "{} - found exclusion match for: {}. Both Season and Episode are 0. Item will be "
1241 "ignored for scanning.",
1242 __FUNCTION__, CURL::GetRedacted(item->GetPath()));
1243 return true;
1246 return false;
1249 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
1251 SETTINGS_TVSHOWLIST expression = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowEnumRegExps;
1253 std::string strLabel;
1255 // remove path to main file if it's a bd or dvd folder to regex the right (folder) name
1256 if (item->IsOpticalMediaFile())
1258 strLabel = item->GetLocalMetadataPath();
1259 URIUtils::RemoveSlashAtEnd(strLabel);
1261 else
1262 strLabel = item->GetPath();
1264 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
1265 strLabel = CURL::Decode(CURL::GetRedacted(strLabel));
1267 for (unsigned int i=0;i<expression.size();++i)
1269 CRegExp reg(true, CRegExp::autoUtf8);
1270 if (!reg.RegComp(expression[i].regexp))
1271 continue;
1273 int regexppos, regexp2pos;
1274 //CLog::Log(LOGDEBUG,"running expression {} on {}",expression[i].regexp,strLabel);
1275 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
1276 continue;
1278 EPISODE episode;
1279 episode.strPath = item->GetPath();
1280 episode.iSeason = -1;
1281 episode.iEpisode = -1;
1282 episode.cDate.SetValid(false);
1283 episode.isFolder = false;
1285 bool byDate = expression[i].byDate ? true : false;
1286 bool byTitle = expression[i].byTitle;
1287 int defaultSeason = expression[i].defaultSeason;
1289 if (byDate)
1291 if (!GetAirDateFromRegExp(reg, episode))
1292 continue;
1294 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match {} ({}) [{}]",
1295 CURL::GetRedacted(episode.strPath), episode.cDate.GetAsLocalizedDate(),
1296 expression[i].regexp);
1298 else if (byTitle)
1300 if (!GetEpisodeTitleFromRegExp(reg, episode))
1301 continue;
1303 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found title based match {} ({}) [{}]",
1304 CURL::GetRedacted(episode.strPath), episode.strTitle, expression[i].regexp);
1306 else
1308 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
1309 continue;
1311 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match {} (s{}e{}) [{}]",
1312 CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode,
1313 expression[i].regexp);
1316 // Grab the remainder from first regexp run
1317 // as second run might modify or empty it.
1318 std::string remainder(reg.GetMatch(3));
1321 * Check if the files base path is a dedicated folder that contains
1322 * only this single episode. If season and episode match with the
1323 * actual media file, we set episode.isFolder to true.
1325 std::string strBasePath = item->GetBaseMoviePath(true);
1326 URIUtils::RemoveSlashAtEnd(strBasePath);
1327 strBasePath = URIUtils::GetFileName(strBasePath);
1329 if (reg.RegFind(strBasePath.c_str()) > -1)
1331 EPISODE parent;
1332 if (byDate)
1334 GetAirDateFromRegExp(reg, parent);
1335 if (episode.cDate == parent.cDate)
1336 episode.isFolder = true;
1338 else
1340 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
1341 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
1342 episode.isFolder = true;
1346 // add what we found by now
1347 episodeList.push_back(episode);
1349 CRegExp reg2(true, CRegExp::autoUtf8);
1350 // check the remainder of the string for any further episodes.
1351 if (!byDate && reg2.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowMultiPartEnumRegExp))
1353 int offset = 0;
1355 // we want "long circuit" OR below so that both offsets are evaluated
1356 while (static_cast<int>((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) |
1357 static_cast<int>((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
1359 if (((regexppos <= regexp2pos) && regexppos != -1) ||
1360 (regexppos >= 0 && regexp2pos == -1))
1362 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
1364 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season {}, multipart episode {} [{}]",
1365 episode.iSeason, episode.iEpisode,
1366 CServiceBroker::GetSettingsComponent()
1367 ->GetAdvancedSettings()
1368 ->m_tvshowMultiPartEnumRegExp);
1370 episodeList.push_back(episode);
1371 remainder = reg.GetMatch(3);
1372 offset = 0;
1374 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
1375 (regexp2pos >= 0 && regexppos == -1))
1377 episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
1378 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode {} [{}]",
1379 episode.iEpisode,
1380 CServiceBroker::GetSettingsComponent()
1381 ->GetAdvancedSettings()
1382 ->m_tvshowMultiPartEnumRegExp);
1383 episodeList.push_back(episode);
1384 offset += regexp2pos + reg2.GetFindLen();
1388 return true;
1390 return false;
1393 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp &reg, EPISODE &episodeInfo, int defaultSeason)
1395 std::string season(reg.GetMatch(1));
1396 std::string episode(reg.GetMatch(2));
1398 if (!season.empty() || !episode.empty())
1400 char* endptr = NULL;
1401 if (season.empty() && !episode.empty())
1402 { // no season specified -> assume defaultSeason
1403 episodeInfo.iSeason = defaultSeason;
1404 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
1405 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1407 else if (!season.empty() && episode.empty())
1408 { // no episode specification -> assume defaultSeason
1409 episodeInfo.iSeason = defaultSeason;
1410 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
1411 episodeInfo.iEpisode = atoi(season.c_str());
1413 else
1414 { // season and episode specified
1415 episodeInfo.iSeason = atoi(season.c_str());
1416 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1418 if (endptr)
1420 if (isalpha(*endptr))
1421 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1422 else if (*endptr == '.')
1423 episodeInfo.iSubepisode = atoi(endptr+1);
1425 return true;
1427 return false;
1430 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp &reg, EPISODE &episodeInfo)
1432 std::string param1(reg.GetMatch(1));
1433 std::string param2(reg.GetMatch(2));
1434 std::string param3(reg.GetMatch(3));
1436 if (!param1.empty() && !param2.empty() && !param3.empty())
1438 // regular expression by date
1439 int len1 = param1.size();
1440 int len2 = param2.size();
1441 int len3 = param3.size();
1443 if (len1==4 && len2==2 && len3==2)
1445 // yyyy mm dd format
1446 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1448 else if (len1==2 && len2==2 && len3==4)
1450 // mm dd yyyy format
1451 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1454 return episodeInfo.cDate.IsValid();
1457 bool CVideoInfoScanner::GetEpisodeTitleFromRegExp(CRegExp& reg, EPISODE& episodeInfo)
1459 std::string param1(reg.GetMatch(1));
1461 if (!param1.empty())
1463 episodeInfo.strTitle = param1;
1464 return true;
1466 return false;
1469 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1471 // ensure our database is open (this can get called via other classes)
1472 if (!m_database.Open())
1473 return -1;
1475 if (!libraryImport)
1476 GetArtwork(pItem, content, videoFolder, useLocal && !pItem->IsPlugin(), showInfo ? showInfo->m_strPath : "");
1478 // ensure the art map isn't completely empty by specifying an empty thumb
1479 std::map<std::string, std::string> art = pItem->GetArt();
1480 if (art.empty())
1481 art["thumb"] = "";
1483 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1484 if (movieDetails.m_basePath.empty())
1485 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1486 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1488 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1490 if (pItem->m_bIsFolder)
1491 movieDetails.m_strPath = pItem->GetPath();
1493 std::string strTitle(movieDetails.m_strTitle);
1495 if (showInfo && content == CONTENT_TVSHOWS)
1497 strTitle = StringUtils::Format("{} - {}x{} - {}", showInfo->m_strTitle,
1498 movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle);
1501 /* As HasStreamDetails() returns true for TV shows (because the scraper calls SetVideoInfoTag()
1502 * directly to set the duration) a better test is just to see if we have any common flag info
1503 * missing. If we have already read an nfo file then this data should be populated, otherwise
1504 * get it from the video file */
1506 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
1507 CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
1509 const auto& strmdetails = movieDetails.m_streamDetails;
1510 if (strmdetails.GetVideoCodec(1).empty() || strmdetails.GetVideoHeight(1) == 0 ||
1511 strmdetails.GetVideoWidth(1) == 0 || strmdetails.GetVideoDuration(1) == 0)
1514 CDVDFileInfo::GetFileStreamDetails(pItem);
1515 CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}",
1516 CURL::GetRedacted(pItem->GetPath()));
1520 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to {}:{}", TranslateContent(content), CURL::GetRedacted(pItem->GetPath()));
1521 long lResult = -1;
1523 if (content == CONTENT_MOVIES)
1525 // find local trailer first
1526 std::string strTrailer = pItem->FindTrailer();
1527 if (!strTrailer.empty())
1528 movieDetails.m_strTrailer = strTrailer;
1530 // Deal with 'Disc n' subdirectories
1531 const std::string discNum{
1532 CUtil::GetDiscNumberFromPath(URIUtils::GetParentPath(movieDetails.m_strFileNameAndPath))};
1533 if (!discNum.empty())
1535 if (movieDetails.m_set.title.empty())
1537 const std::string setName{m_database.GetSetByNameLike(movieDetails.m_strTitle)};
1538 if (!setName.empty())
1540 // Add movie to existing set
1541 movieDetails.SetSet(setName);
1543 else
1545 // Create set, then add movie to the set
1546 const int idSet{m_database.AddSet(movieDetails.m_strTitle)};
1547 m_database.SetArtForItem(idSet, MediaTypeVideoCollection, art);
1548 movieDetails.SetSet(movieDetails.m_strTitle);
1552 // Add '(Disc n)' to title (in local language)
1553 movieDetails.m_strTitle =
1554 StringUtils::Format(g_localizeStrings.Get(29995), movieDetails.m_strTitle, discNum);
1557 lResult = m_database.SetDetailsForMovie(movieDetails, art);
1558 movieDetails.m_iDbId = lResult;
1559 movieDetails.m_type = MediaTypeMovie;
1561 // setup links to shows if the linked shows are in the db
1562 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1564 CFileItemList items;
1565 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1566 if (items.Size())
1567 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1568 else
1569 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie {} to show {}",
1570 movieDetails.m_strTitle, movieDetails.m_showLink[i]);
1573 else if (content == CONTENT_TVSHOWS)
1575 if (pItem->m_bIsFolder)
1578 multipaths are not stored in the database, so in the case we have one,
1579 we split the paths, and compute the parent paths in each case.
1581 std::vector<std::string> multipath;
1582 if (!URIUtils::IsMultiPath(pItem->GetPath()) || !CMultiPathDirectory::GetPaths(pItem->GetPath(), multipath))
1583 multipath.push_back(pItem->GetPath());
1584 std::vector<std::pair<std::string, std::string> > paths;
1585 for (std::vector<std::string>::const_iterator i = multipath.begin(); i != multipath.end(); ++i)
1586 paths.emplace_back(*i, URIUtils::GetParentPath(*i));
1588 std::map<int, std::map<std::string, std::string> > seasonArt;
1590 if (!libraryImport)
1591 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !pItem->IsPlugin());
1593 lResult = m_database.SetDetailsForTvShow(paths, movieDetails, art, seasonArt);
1594 movieDetails.m_iDbId = lResult;
1595 movieDetails.m_type = MediaTypeTvShow;
1597 else
1599 // we add episode then set details, as otherwise set details will delete the
1600 // episode then add, which breaks multi-episode files.
1601 int idShow = showInfo ? showInfo->m_iDbId : -1;
1602 int idEpisode = m_database.AddNewEpisode(idShow, movieDetails);
1603 lResult = m_database.SetDetailsForEpisode(movieDetails, art, idShow, idEpisode);
1604 movieDetails.m_iDbId = lResult;
1605 movieDetails.m_type = MediaTypeEpisode;
1606 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1607 if (movieDetails.m_EpBookmark.timeInSeconds > 0)
1609 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1610 movieDetails.m_EpBookmark.seasonNumber = movieDetails.m_iSeason;
1611 movieDetails.m_EpBookmark.episodeNumber = movieDetails.m_iEpisode;
1612 m_database.AddBookMarkForEpisode(movieDetails, movieDetails.m_EpBookmark);
1616 else if (content == CONTENT_MUSICVIDEOS)
1618 lResult = m_database.SetDetailsForMusicVideo(movieDetails, art);
1619 movieDetails.m_iDbId = lResult;
1620 movieDetails.m_type = MediaTypeMusicVideo;
1623 if (!pItem->m_bIsFolder)
1625 const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
1626 if ((libraryImport || advancedSettings->m_bVideoLibraryImportWatchedState) &&
1627 (movieDetails.IsPlayCountSet() || movieDetails.m_lastPlayed.IsValid()))
1628 m_database.SetPlayCount(*pItem, movieDetails.GetPlayCount(), movieDetails.m_lastPlayed);
1630 if ((libraryImport || advancedSettings->m_bVideoLibraryImportResumePoint) &&
1631 movieDetails.GetResumePoint().IsSet())
1632 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.GetResumePoint(), CBookmark::RESUME);
1635 m_database.Close();
1637 CFileItemPtr itemCopy = std::make_shared<CFileItem>(*pItem);
1638 CVariant data;
1639 data["added"] = true;
1640 if (m_bRunning)
1641 data["transaction"] = true;
1642 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate",
1643 itemCopy, data);
1644 return lResult;
1647 std::string ContentToMediaType(CONTENT_TYPE content, bool folder)
1649 switch (content)
1651 case CONTENT_MOVIES:
1652 return MediaTypeMovie;
1653 case CONTENT_MUSICVIDEOS:
1654 return MediaTypeMusicVideo;
1655 case CONTENT_TVSHOWS:
1656 return folder ? MediaTypeTvShow : MediaTypeEpisode;
1657 default:
1658 return "";
1662 VideoDbContentType ContentToVideoDbType(CONTENT_TYPE content)
1664 switch (content)
1666 case CONTENT_MOVIES:
1667 return VideoDbContentType::MOVIES;
1668 case CONTENT_MUSICVIDEOS:
1669 return VideoDbContentType::MUSICVIDEOS;
1670 case CONTENT_TVSHOWS:
1671 return VideoDbContentType::EPISODES;
1672 default:
1673 return VideoDbContentType::UNKNOWN;
1677 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1679 std::string type = "thumb";
1680 if (width*5 < height*4)
1681 type = "poster";
1682 else if (width*1 > height*4)
1683 type = "banner";
1684 return type;
1687 std::string CVideoInfoScanner::GetMovieSetInfoFolder(const std::string& setTitle)
1689 if (setTitle.empty())
1690 return "";
1691 std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
1692 CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER);
1693 if (path.empty())
1694 return "";
1695 path = URIUtils::AddFileToFolder(path, CUtil::MakeLegalFileName(setTitle, LEGAL_WIN32_COMPAT));
1696 URIUtils::AddSlashAtEnd(path);
1697 CLog::Log(LOGDEBUG,
1698 "VideoInfoScanner: Looking for local artwork for movie set '{}' in folder '{}'",
1699 setTitle,
1700 CURL::GetRedacted(path));
1701 return CDirectory::Exists(path) ? path : "";
1704 void CVideoInfoScanner::AddLocalItemArtwork(CGUIListItem::ArtMap& itemArt,
1705 const std::vector<std::string>& wantedArtTypes, const std::string& itemPath,
1706 bool addAll, bool exactName)
1708 std::string path = URIUtils::GetDirectory(itemPath);
1709 if (path.empty())
1710 return;
1712 CFileItemList availableArtFiles;
1713 CDirectory::GetDirectory(path, availableArtFiles,
1714 CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
1715 DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
1717 std::string baseFilename = URIUtils::GetFileName(itemPath);
1718 if (!baseFilename.empty())
1720 URIUtils::RemoveExtension(baseFilename);
1721 baseFilename.append("-");
1724 for (const auto& artFile : availableArtFiles)
1726 std::string candidate = URIUtils::GetFileName(artFile->GetPath());
1728 bool matchesFilename =
1729 !baseFilename.empty() && StringUtils::StartsWith(candidate, baseFilename);
1730 if (!baseFilename.empty() && !matchesFilename)
1731 continue;
1733 if (matchesFilename)
1734 candidate.erase(0, baseFilename.length());
1735 URIUtils::RemoveExtension(candidate);
1736 StringUtils::ToLower(candidate);
1738 // move 'folder' to thumb / poster / banner based on aspect ratio
1739 // if such artwork doesn't already exist
1740 if (!matchesFilename && StringUtils::EqualsNoCase(candidate, "folder") &&
1741 !CVideoThumbLoader::IsArtTypeInWhitelist("folder", wantedArtTypes, exactName))
1743 // cache the image to determine sizing
1744 CTextureDetails details;
1745 if (CServiceBroker::GetTextureCache()->CacheImage(artFile->GetPath(), details))
1747 candidate = GetArtTypeFromSize(details.width, details.height);
1748 if (itemArt.find(candidate) != itemArt.end())
1749 continue;
1753 if ((addAll && CVideoThumbLoader::IsValidArtType(candidate)) ||
1754 CVideoThumbLoader::IsArtTypeInWhitelist(candidate, wantedArtTypes, exactName))
1756 itemArt[candidate] = artFile->GetPath();
1761 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1763 int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1764 CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
1765 if (artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE)
1766 return;
1768 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1769 movieDetails.m_fanart.Unpack();
1770 movieDetails.m_strPictureURL.Parse();
1772 CGUIListItem::ArtMap art = pItem->GetArt();
1774 // get and cache thumb images
1775 std::string mediaType = ContentToMediaType(content, pItem->m_bIsFolder);
1776 std::vector<std::string> artTypes = CVideoThumbLoader::GetArtTypes(mediaType);
1777 bool moviePartOfSet = content == CONTENT_MOVIES && !movieDetails.m_set.title.empty();
1778 std::vector<std::string> movieSetArtTypes;
1779 if (moviePartOfSet)
1781 movieSetArtTypes = CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection);
1782 for (const std::string& artType : movieSetArtTypes)
1783 artTypes.push_back("set." + artType);
1785 bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
1786 bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
1787 // find local art
1788 if (useLocal)
1790 if (!pItem->SkipLocalArt())
1792 if (bApplyToDir && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
1794 std::string filename = pItem->GetLocalArtBaseFilename();
1795 std::string directory = URIUtils::GetDirectory(filename);
1796 if (filename != directory)
1797 AddLocalItemArtwork(art, artTypes, directory, addAll, exactName);
1799 AddLocalItemArtwork(art, artTypes, pItem->GetLocalArtBaseFilename(), addAll, exactName);
1802 if (moviePartOfSet)
1804 std::string movieSetInfoPath = GetMovieSetInfoFolder(movieDetails.m_set.title);
1805 if (!movieSetInfoPath.empty())
1807 CGUIListItem::ArtMap movieSetArt;
1808 AddLocalItemArtwork(movieSetArt, movieSetArtTypes, movieSetInfoPath, addAll, exactName);
1809 for (const auto& artItem : movieSetArt)
1811 art["set." + artItem.first] = artItem.second;
1817 // find embedded art
1818 if (pItem->HasVideoInfoTag() && !pItem->GetVideoInfoTag()->m_coverArt.empty())
1820 for (auto& it : pItem->GetVideoInfoTag()->m_coverArt)
1822 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(it.m_type, artTypes, exactName)) &&
1823 art.find(it.m_type) == art.end())
1825 std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(),
1826 "video_" + it.m_type);
1827 art.insert(std::make_pair(it.m_type, thumb));
1832 // add online fanart (treated separately due to it being stored in m_fanart)
1833 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist("fanart", artTypes, exactName)) &&
1834 art.find("fanart") == art.end())
1836 std::string fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1837 if (!fanart.empty())
1838 art.insert(std::make_pair("fanart", fanart));
1841 // add online art
1842 for (const auto& url : pItem->GetVideoInfoTag()->m_strPictureURL.GetUrls())
1844 if (url.m_type != CScraperUrl::UrlType::General)
1845 continue;
1846 std::string aspect = url.m_aspect;
1847 if (aspect.empty())
1848 // Backward compatibility with Kodi 11 Eden NFO files
1849 aspect = mediaType == MediaTypeEpisode ? "thumb" : "poster";
1851 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
1852 art.find(aspect) == art.end())
1854 std::string image = GetImage(url, pItem->GetPath());
1855 if (!image.empty())
1856 art.insert(std::make_pair(aspect, image));
1860 if (art.find("thumb") == art.end() &&
1861 CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
1862 CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB) &&
1863 CDVDFileInfo::CanExtract(*pItem))
1865 art["thumb"] = CVideoThumbLoader::GetEmbeddedThumbURL(*pItem);
1868 for (const auto& artType : artTypes)
1870 if (art.find(artType) != art.end())
1871 CServiceBroker::GetTextureCache()->BackgroundCacheImage(art[artType]);
1874 pItem->SetArt(art);
1876 // parent folder to apply the thumb to and to search for local actor thumbs
1877 std::string parentDir = URIUtils::GetBasePath(pItem->GetPath());
1878 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS))
1879 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1880 if (bApplyToDir)
1881 ApplyThumbToFolder(parentDir, art["thumb"]);
1884 std::string CVideoInfoScanner::GetImage(const CScraperUrl::SUrlEntry &image, const std::string& itemPath)
1886 std::string thumb = CScraperUrl::GetThumbUrl(image);
1887 if (!thumb.empty() && thumb.find('/') == std::string::npos &&
1888 thumb.find('\\') == std::string::npos)
1890 std::string strPath = URIUtils::GetDirectory(itemPath);
1891 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1893 return thumb;
1896 CInfoScanner::INFO_RET
1897 CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files,
1898 const ADDON::ScraperPtr &scraper,
1899 bool useLocal,
1900 const CVideoInfoTag& showInfo,
1901 CGUIDialogProgress* pDlgProgress /* = NULL */)
1903 if (pDlgProgress)
1905 pDlgProgress->SetLine(1, CVariant{20361}); // Loading episode details
1906 pDlgProgress->SetPercentage(0);
1907 pDlgProgress->ShowProgressBar(true);
1908 pDlgProgress->Progress();
1911 EPISODELIST episodes;
1912 bool hasEpisodeGuide = false;
1914 int iMax = files.size();
1915 int iCurr = 1;
1916 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1918 if (pDlgProgress)
1920 pDlgProgress->SetLine(1, CVariant{20361}); // Loading episode details
1921 pDlgProgress->SetLine(2, StringUtils::Format("{} {}", g_localizeStrings.Get(20373),
1922 file->iSeason)); // Season x
1923 pDlgProgress->SetLine(3, StringUtils::Format("{} {}", g_localizeStrings.Get(20359),
1924 file->iEpisode)); // Episode y
1925 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1926 pDlgProgress->Progress();
1928 if (m_handle)
1929 m_handle->SetPercentage(100.f*iCurr++/iMax);
1931 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1932 return INFO_CANCELLED;
1934 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1936 if (m_handle)
1937 m_handle->SetText(g_localizeStrings.Get(20415));
1938 continue;
1941 CFileItem item;
1942 if (file->item)
1943 item = *file->item;
1944 else
1946 item.SetPath(file->strPath);
1947 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1950 // handle .nfo files
1951 CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
1952 CScraperUrl scrUrl;
1953 const ScraperPtr& info(scraper);
1954 std::unique_ptr<IVideoInfoTagLoader> loader;
1955 if (useLocal)
1957 loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(item, info, false));
1958 if (loader)
1960 // no reset here on purpose
1961 result = loader->Load(*item.GetVideoInfoTag(), false);
1964 if (result == CInfoScanner::FULL_NFO)
1966 // override with episode and season number from file if available
1967 if (file->iEpisode > -1)
1969 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1970 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1972 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1973 return INFO_ERROR;
1974 continue;
1977 if (!hasEpisodeGuide)
1979 // fetch episode guide
1980 if (!showInfo.m_strEpisodeGuide.empty() && scraper->ID() != "metadata.local")
1982 CScraperUrl url;
1983 url.ParseAndAppendUrlsFromEpisodeGuide(showInfo.m_strEpisodeGuide);
1985 if (pDlgProgress)
1987 pDlgProgress->SetLine(1, CVariant{20354}); // Fetching episode guide
1988 pDlgProgress->Progress();
1991 CVideoInfoDownloader imdb(scraper);
1992 if (!imdb.GetEpisodeList(url, episodes))
1993 return INFO_NOT_FOUND;
1995 hasEpisodeGuide = true;
1999 if (episodes.empty())
2001 CLog::Log(LOGERROR,
2002 "VideoInfoScanner: Asked to lookup episode {}"
2003 " online, but we have either no episode guide or"
2004 " we are using the local scraper. Check your tvshow.nfo and make"
2005 " sure the <episodeguide> tag is in place and/or use an online"
2006 " scraper.",
2007 CURL::GetRedacted(file->strPath));
2008 continue;
2011 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
2012 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
2013 bool bFound = false;
2014 EPISODELIST::iterator guide = episodes.begin();
2015 EPISODELIST matches;
2017 for (; guide != episodes.end(); ++guide )
2019 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
2021 if (key==*guide)
2023 bFound = true;
2024 break;
2026 else if ((file->iSubepisode!=0) && (backupkey==*guide))
2028 matches.push_back(*guide);
2029 continue;
2032 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
2034 matches.push_back(*guide);
2035 continue;
2037 if (!guide->cScraperUrl.GetTitle().empty() &&
2038 StringUtils::EqualsNoCase(guide->cScraperUrl.GetTitle(), file->strTitle))
2040 bFound = true;
2041 break;
2043 if (!guide->strTitle.empty() && StringUtils::EqualsNoCase(guide->strTitle, file->strTitle))
2045 bFound = true;
2046 break;
2050 if (!bFound)
2053 * If there is only one match or there are matches but no title to compare with to help
2054 * identify the best match, then pick the first match as the best possible candidate.
2056 * Otherwise, use the title to further refine the best match.
2058 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
2060 guide = matches.begin();
2061 bFound = true;
2063 else if (!file->strTitle.empty())
2065 CLog::Log(LOGDEBUG, "VideoInfoScanner: analyzing parsed title '{}'", file->strTitle);
2066 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
2068 EPISODELIST *candidates;
2069 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
2071 minscore = 0.8; // 80% should ensure a good match.
2072 candidates = &episodes;
2074 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
2075 candidates = &matches;
2077 std::vector<std::string> titles;
2078 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
2080 auto title = guide->cScraperUrl.GetTitle();
2081 if (title.empty())
2083 title = guide->strTitle;
2085 StringUtils::ToLower(title);
2086 guide->cScraperUrl.SetTitle(title);
2087 titles.push_back(title);
2090 double matchscore;
2091 std::string loweredTitle(file->strTitle);
2092 StringUtils::ToLower(loweredTitle);
2093 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
2094 if (index >= 0 && matchscore >= minscore)
2096 guide = candidates->begin() + index;
2097 bFound = true;
2098 CLog::Log(LOGDEBUG,
2099 "{} fuzzy title match for show: '{}', title: '{}', match: '{}', score: {:f} "
2100 ">= {:f}",
2101 __FUNCTION__, showInfo.m_strTitle, file->strTitle, titles[index], matchscore,
2102 minscore);
2107 if (bFound)
2109 CVideoInfoDownloader imdb(scraper);
2110 CFileItem item;
2111 item.SetPath(file->strPath);
2112 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
2113 return INFO_NOT_FOUND; //! @todo should we just skip to the next episode?
2115 // Only set season/epnum from filename when it is not already set by a scraper
2116 if (item.GetVideoInfoTag()->m_iSeason == -1)
2117 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
2118 if (item.GetVideoInfoTag()->m_iEpisode == -1)
2119 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
2121 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
2122 return INFO_ERROR;
2124 else
2126 CLog::Log(
2127 LOGDEBUG,
2128 "{} - no match for show: '{}', season: {}, episode: {}.{}, airdate: '{}', title: '{}'",
2129 __FUNCTION__, showInfo.m_strTitle, file->iSeason, file->iEpisode, file->iSubepisode,
2130 file->cDate.GetAsLocalizedDate(), file->strTitle);
2133 return INFO_ADDED;
2136 bool CVideoInfoScanner::GetDetails(CFileItem* pItem,
2137 const std::unordered_map<std::string, std::string>& uniqueIDs,
2138 CScraperUrl& url,
2139 const ScraperPtr& scraper,
2140 IVideoInfoTagLoader* loader,
2141 CGUIDialogProgress* pDialog /* = NULL */)
2143 CVideoInfoTag movieDetails;
2145 if (m_handle && !url.GetTitle().empty())
2146 m_handle->SetText(url.GetTitle());
2148 CVideoInfoDownloader imdb(scraper);
2149 bool ret = imdb.GetDetails(uniqueIDs, url, movieDetails, pDialog);
2151 if (ret)
2153 if (loader)
2154 loader->Load(movieDetails, true);
2156 if (m_handle && url.GetTitle().empty())
2157 m_handle->SetText(movieDetails.m_strTitle);
2159 if (pDialog)
2161 if (!pDialog->HasText())
2162 pDialog->SetLine(0, CVariant{movieDetails.m_strTitle});
2163 pDialog->Progress();
2166 *pItem->GetVideoInfoTag() = movieDetails;
2167 return true;
2169 return false; // no info found, or cancelled
2172 void CVideoInfoScanner::ApplyThumbToFolder(const std::string &folder, const std::string &imdbThumb)
2174 // copy icon to folder also;
2175 if (!imdbThumb.empty())
2177 CFileItem folderItem(folder, true);
2178 CThumbLoader loader;
2179 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
2183 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash)
2185 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
2186 if (0 == items.Size()) return 0;
2187 CDigest digest{CDigest::Type::MD5};
2188 int count = 0;
2189 for (int i = 0; i < items.Size(); ++i)
2191 const CFileItemPtr pItem = items[i];
2192 digest.Update(pItem->GetPath());
2193 if (pItem->IsPlugin())
2195 // allow plugin to calculate hash itself using strings rather than binary data for size and date
2196 // according to ListItem.setInfo() documentation date format should be "d.m.Y"
2197 if (pItem->m_dwSize)
2198 digest.Update(std::to_string(pItem->m_dwSize));
2199 if (pItem->m_dateTime.IsValid())
2200 digest.Update(StringUtils::Format("{:02}.{:02}.{:04}", pItem->m_dateTime.GetDay(),
2201 pItem->m_dateTime.GetMonth(),
2202 pItem->m_dateTime.GetYear()));
2204 else
2206 digest.Update(&pItem->m_dwSize, sizeof(pItem->m_dwSize));
2207 KODI::TIME::FileTime time = pItem->m_dateTime;
2208 digest.Update(&time, sizeof(KODI::TIME::FileTime));
2210 if (IsVideo(*pItem) && !pItem->IsPlayList() && !pItem->IsNFO())
2211 count++;
2213 hash = digest.Finalize();
2214 return count;
2217 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items, const std::vector<std::string> &excludes) const
2219 if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash || items.IsPlugin())
2220 return false;
2222 for (int i = 0; i < items.Size(); ++i)
2224 if (items[i]->m_bIsFolder && !CUtil::ExcludeFileOrFolder(items[i]->GetPath(), excludes))
2225 return false;
2227 return true;
2230 std::string CVideoInfoScanner::GetFastHash(const std::string &directory,
2231 const std::vector<std::string> &excludes) const
2233 CDigest digest{CDigest::Type::MD5};
2235 if (excludes.size())
2236 digest.Update(StringUtils::Join(excludes, "|"));
2238 struct __stat64 buffer;
2239 if (XFILE::CFile::Stat(directory, &buffer) == 0)
2241 int64_t time = buffer.st_mtime;
2242 if (!time)
2243 time = buffer.st_ctime;
2244 if (time)
2246 digest.Update((unsigned char *)&time, sizeof(time));
2247 return digest.Finalize();
2250 return "";
2253 std::string CVideoInfoScanner::GetRecursiveFastHash(const std::string &directory,
2254 const std::vector<std::string> &excludes) const
2256 CFileItemList items;
2257 items.Add(std::make_shared<CFileItem>(directory, true));
2258 CUtil::GetRecursiveDirsListing(directory, items, DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
2260 CDigest digest{CDigest::Type::MD5};
2262 if (excludes.size())
2263 digest.Update(StringUtils::Join(excludes, "|"));
2265 int64_t time = 0;
2266 for (int i=0; i < items.Size(); ++i)
2268 int64_t stat_time = 0;
2269 struct __stat64 buffer;
2270 if (XFILE::CFile::Stat(items[i]->GetPath(), &buffer) == 0)
2272 //! @todo some filesystems may return the mtime/ctime inline, in which case this is
2273 //! unnecessarily expensive. Consider supporting Stat() in our directory cache?
2274 stat_time = buffer.st_mtime ? buffer.st_mtime : buffer.st_ctime;
2275 time += stat_time;
2278 if (!stat_time)
2279 return "";
2282 if (time)
2284 digest.Update((unsigned char *)&time, sizeof(time));
2285 return digest.Finalize();
2287 return "";
2290 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show,
2291 std::map<int, std::map<std::string, std::string>> &seasonArt, const std::vector<std::string> &artTypes, bool useLocal)
2293 int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->
2294 GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
2295 bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
2296 bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
2297 if (useLocal)
2299 // find the maximum number of seasons we have local thumbs for
2300 int maxSeasons = 0;
2301 CFileItemList items;
2302 std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
2303 if (!show.m_strPath.empty())
2305 CDirectory::GetDirectory(show.m_strPath, items, extensions,
2306 DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE |
2307 DIR_FLAG_NO_FILE_INFO);
2309 extensions.erase(std::remove(extensions.begin(), extensions.end(), '.'), extensions.end());
2310 CRegExp reg;
2311 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z0-9]+)?\\.(" + extensions + ")"))
2313 for (const auto& item : items)
2315 std::string name = URIUtils::GetFileName(item->GetPath());
2316 if (reg.RegFind(name) > -1)
2318 int season = atoi(reg.GetMatch(1).c_str());
2319 if (season > maxSeasons)
2320 maxSeasons = season;
2324 for (int season = -1; season <= maxSeasons; season++)
2326 // skip if we already have some art
2327 std::map<int, std::map<std::string, std::string>>::const_iterator it = seasonArt.find(season);
2328 if (it != seasonArt.end() && !it->second.empty())
2329 continue;
2331 std::map<std::string, std::string> art;
2332 std::string basePath;
2333 if (season == -1)
2334 basePath = "season-all";
2335 else if (season == 0)
2336 basePath = "season-specials";
2337 else
2338 basePath = StringUtils::Format("season{:02}", season);
2340 AddLocalItemArtwork(art, artTypes,
2341 URIUtils::AddFileToFolder(show.m_strPath, basePath),
2342 addAll, exactName);
2344 seasonArt[season] = art;
2347 // add online art
2348 for (const auto& url : show.m_strPictureURL.GetUrls())
2350 if (url.m_type != CScraperUrl::UrlType::Season)
2351 continue;
2352 std::string aspect = url.m_aspect;
2353 if (aspect.empty())
2354 aspect = "thumb";
2355 std::map<std::string, std::string>& art = seasonArt[url.m_season];
2356 if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
2357 art.find(aspect) == art.end())
2359 std::string image = CScraperUrl::GetThumbUrl(url);
2360 if (!image.empty())
2361 art.insert(std::make_pair(aspect, image));
2366 void CVideoInfoScanner::FetchActorThumbs(std::vector<SActorInfo>& actors, const std::string& strPath)
2368 CFileItemList items;
2369 // don't try to fetch anything local with plugin source
2370 if (!URIUtils::IsPlugin(strPath))
2372 std::string actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
2373 if (CDirectory::Exists(actorsDir))
2374 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
2375 DIR_FLAG_NO_FILE_INFO);
2377 for (std::vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
2379 if (i->thumb.empty())
2381 std::string thumbFile = i->strName;
2382 StringUtils::Replace(thumbFile, ' ', '_');
2383 for (int j = 0; j < items.Size(); j++)
2385 std::string compare = URIUtils::GetFileName(items[j]->GetPath());
2386 URIUtils::RemoveExtension(compare);
2387 if (!items[j]->m_bIsFolder && compare == thumbFile)
2389 i->thumb = items[j]->GetPath();
2390 break;
2393 if (i->thumb.empty() && !i->thumbUrl.GetFirstUrlByType().m_url.empty())
2394 i->thumb = CScraperUrl::GetThumbUrl(i->thumbUrl.GetFirstUrlByType());
2395 if (!i->thumb.empty())
2396 CServiceBroker::GetTextureCache()->BackgroundCacheImage(i->thumb);
2401 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
2403 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoScannerIgnoreErrors)
2404 return true;
2406 if (pDialog)
2408 HELPERS::ShowOKDialogText(CVariant{20448}, CVariant{20449});
2409 return false;
2411 return HELPERS::ShowYesNoDialogText(CVariant{20448}, CVariant{20450}) ==
2412 DialogResponse::CHOICE_YES;
2415 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const std::string &line1)
2417 if (progress)
2419 progress->SetHeading(CVariant{heading});
2420 progress->SetLine(0, CVariant{line1});
2421 progress->Progress();
2422 return progress->IsCanceled();
2424 return m_bStop;
2427 int CVideoInfoScanner::FindVideo(const std::string &title, int year, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
2429 MOVIELIST movielist;
2430 CVideoInfoDownloader imdb(scraper);
2431 int returncode = imdb.FindMovie(title, year, movielist, progress);
2432 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
2433 { // scraper reported an error, or we had an error and user wants to cancel the scan
2434 m_bStop = true;
2435 return -1; // cancelled
2437 if (returncode > 0 && movielist.size())
2439 url = movielist[0];
2440 return 1; // found a movie
2442 return 0; // didn't find anything
2445 bool CVideoInfoScanner::AddVideoExtras(CFileItemList& items,
2446 const CONTENT_TYPE& content,
2447 const std::string& path)
2449 int dbId = -1;
2451 // get the library item which was added previously with the specified conent type
2452 for (const auto& item : items)
2454 if (content == CONTENT_MOVIES)
2456 dbId = m_database.GetMovieId(item->GetPath());
2457 if (dbId != -1)
2459 break;
2464 if (dbId == -1)
2466 CLog::Log(LOGERROR, "VideoInfoScanner: Failed to find the library item for video extras {}",
2467 CURL::GetRedacted(path));
2468 return false;
2471 // Add video extras to library
2472 CDirectory::EnumerateDirectory(
2473 path,
2474 [this, content, dbId, path](const std::shared_ptr<CFileItem>& item)
2476 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
2477 CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
2479 CDVDFileInfo::GetFileStreamDetails(item.get());
2480 CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}",
2481 CURL::GetRedacted(item->GetPath()));
2484 const std::string typeVideoVersion =
2485 CGUIDialogVideoManagerExtras::GenerateVideoExtra(path, item->GetPath());
2487 const int idVideoVersion = m_database.AddVideoVersionType(
2488 typeVideoVersion, VideoAssetTypeOwner::AUTO, VideoAssetType::EXTRA);
2490 m_database.AddVideoAsset(ContentToVideoDbType(content), dbId, idVideoVersion,
2491 VideoAssetType::EXTRA, *item.get());
2493 CLog::Log(LOGDEBUG, "VideoInfoScanner: Added video extras {}",
2494 CURL::GetRedacted(item->GetPath()));
2496 [](auto) { return true; }, true,
2497 CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), DIR_FLAG_DEFAULTS);
2499 return true;
2502 bool CVideoInfoScanner::ProcessVideoVersion(VideoDbContentType itemType, int dbId)
2504 return CGUIDialogVideoManagerVersions::ProcessVideoVersion(itemType, dbId);