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