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