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"
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/DirectoryCache.h"
27 #include "filesystem/File.h"
28 #include "filesystem/MultiPathDirectory.h"
29 #include "filesystem/PluginDirectory.h"
30 #include "guilib/GUIComponent.h"
31 #include "guilib/GUIWindowManager.h"
32 #include "guilib/LocalizeStrings.h"
33 #include "interfaces/AnnouncementManager.h"
34 #include "messaging/helpers/DialogHelper.h"
35 #include "messaging/helpers/DialogOKHelper.h"
36 #include "settings/AdvancedSettings.h"
37 #include "settings/Settings.h"
38 #include "settings/SettingsComponent.h"
39 #include "tags/VideoInfoTagLoaderFactory.h"
40 #include "utils/Digest.h"
41 #include "utils/FileExtensionProvider.h"
42 #include "utils/RegExp.h"
43 #include "utils/StringUtils.h"
44 #include "utils/URIUtils.h"
45 #include "utils/Variant.h"
46 #include "utils/log.h"
47 #include "video/VideoThumbLoader.h"
52 using namespace XFILE
;
53 using namespace ADDON
;
54 using namespace KODI::MESSAGING
;
56 using KODI::MESSAGING::HELPERS::DialogResponse
;
57 using KODI::UTILITY::CDigest
;
62 CVideoInfoScanner::CVideoInfoScanner()
68 CVideoInfoScanner::~CVideoInfoScanner()
71 void CVideoInfoScanner::Process()
77 if (m_showDialog
&& !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE
))
79 CGUIDialogExtendedProgressBar
* dialog
=
80 CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogExtendedProgressBar
>(WINDOW_DIALOG_EXT_PROGRESS
);
82 m_handle
= dialog
->GetHandle(g_localizeStrings
.Get(314));
85 // check if we only need to perform a cleaning
86 if (m_bClean
&& m_pathsToScan
.empty())
89 m_database
.CleanDatabase(m_handle
, paths
, false);
92 m_handle
->MarkFinished();
100 auto start
= std::chrono::steady_clock::now();
104 m_bCanInterrupt
= true;
106 CLog::Log(LOGINFO
, "VideoInfoScanner: Starting scan ..");
107 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary
,
110 // Database operations should not be canceled
111 // using Interrupt() while scanning as it could
112 // result in unexpected behaviour.
113 m_bCanInterrupt
= false;
115 bool bCancelled
= false;
116 while (!bCancelled
&& !m_pathsToScan
.empty())
119 * A copy of the directory path is used because the path supplied is
120 * immediately removed from the m_pathsToScan set in DoScan(). If the
121 * reference points to the entry in the set a null reference error
124 std::string directory
= *m_pathsToScan
.begin();
129 else if (!CDirectory::Exists(directory
))
132 * Note that this will skip clean (if m_bClean is enabled) if the directory really
133 * doesn't exist rather than a NAS being switched off. A manual clean from settings
134 * will still pick up and remove it though.
136 CLog::Log(LOGWARNING
, "{} directory '{}' does not exist - skipping scan{}.", __FUNCTION__
,
137 CURL::GetRedacted(directory
), m_bClean
? " and clean" : "");
138 m_pathsToScan
.erase(m_pathsToScan
.begin());
140 else if (!DoScan(directory
))
147 m_database
.CleanDatabase(m_handle
, m_pathsToClean
, false);
151 m_handle
->SetTitle(g_localizeStrings
.Get(331));
152 m_database
.Compress(false);
156 CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
159 auto end
= std::chrono::steady_clock::now();
160 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
162 CLog::Log(LOGINFO
, "VideoInfoScanner: Finished scan. Scanning for video info took {} ms",
167 CLog::Log(LOGERROR
, "VideoInfoScanner: Exception while scanning.");
171 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary
,
175 m_handle
->MarkFinished();
179 void CVideoInfoScanner::Start(const std::string
& strDirectory
, bool scanAll
)
181 m_strStartDir
= strDirectory
;
183 m_pathsToScan
.clear();
184 m_pathsToClean
.clear();
187 if (strDirectory
.empty())
188 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
190 m_database
.GetPaths(m_pathsToScan
);
193 { // scan all the paths of this subtree that is in the database
194 std::vector
<std::string
> rootDirs
;
195 if (URIUtils::IsMultiPath(strDirectory
))
196 CMultiPathDirectory::GetPaths(strDirectory
, rootDirs
);
198 rootDirs
.push_back(strDirectory
);
200 for (std::vector
<std::string
>::const_iterator it
= rootDirs
.begin(); it
< rootDirs
.end(); ++it
)
202 m_pathsToScan
.insert(*it
);
203 std::vector
<std::pair
<int, std::string
>> subpaths
;
204 m_database
.GetSubPaths(*it
, subpaths
);
205 for (std::vector
<std::pair
<int, std::string
>>::iterator it
= subpaths
.begin(); it
< subpaths
.end(); ++it
)
206 m_pathsToScan
.insert(it
->second
);
210 m_bClean
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryCleanOnUpdate
;
216 void CVideoInfoScanner::Stop()
219 m_database
.Interrupt();
224 static void OnDirectoryScanned(const std::string
& strDirectory
)
226 CGUIMessage
msg(GUI_MSG_DIRECTORY_SCANNED
, 0, 0, 0);
227 msg
.SetStringParam(strDirectory
);
228 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg
);
231 bool CVideoInfoScanner::DoScan(const std::string
& strDirectory
)
235 m_handle
->SetText(g_localizeStrings
.Get(20415));
239 * Remove this path from the list we're processing. This must be done prior to
240 * the check for file or folder exclusion to prevent an infinite while loop
243 std::set
<std::string
>::iterator it
= m_pathsToScan
.find(strDirectory
);
244 if (it
!= m_pathsToScan
.end())
245 m_pathsToScan
.erase(it
);
249 bool foundDirectly
= false;
252 SScanSettings settings
;
253 ScraperPtr info
= m_database
.GetScraperForPath(strDirectory
, settings
, foundDirectly
);
254 CONTENT_TYPE content
= info
? info
->Content() : CONTENT_NONE
;
256 // exclude folders that match our exclude regexps
257 const std::vector
<std::string
> ®exps
= content
== CONTENT_TVSHOWS
? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
258 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps
;
260 if (CUtil::ExcludeFileOrFolder(strDirectory
, regexps
))
263 if (HasNoMedia(strDirectory
))
266 bool ignoreFolder
= !m_scanAll
&& settings
.noupdate
;
267 if (content
== CONTENT_NONE
|| ignoreFolder
)
270 if (URIUtils::IsPlugin(strDirectory
) && !CPluginDirectory::IsMediaLibraryScanningAllowed(TranslateContent(content
), strDirectory
))
274 "VideoInfoScanner: Plugin '{}' does not support media library scanning for '{}' content",
275 CURL::GetRedacted(strDirectory
), TranslateContent(content
));
279 std::string hash
, dbHash
;
280 if (content
== CONTENT_MOVIES
||content
== CONTENT_MUSICVIDEOS
)
284 int str
= content
== CONTENT_MOVIES
? 20317:20318;
285 m_handle
->SetTitle(StringUtils::Format(g_localizeStrings
.Get(str
), info
->Name()));
288 std::string fastHash
;
289 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash
&& !URIUtils::IsPlugin(strDirectory
))
290 fastHash
= GetFastHash(strDirectory
, regexps
);
292 if (m_database
.GetPathHash(strDirectory
, dbHash
) && !fastHash
.empty() && StringUtils::EqualsNoCase(fastHash
, dbHash
))
293 { // fast hashes match - no need to process anything
297 { // need to fetch the folder
298 CDirectory::GetDirectory(strDirectory
, items
, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
300 // do not consider inner folders with .nomedia
301 items
.erase(std::remove_if(items
.begin(), items
.end(),
302 [this](const CFileItemPtr
& item
) {
303 return item
->m_bIsFolder
&& HasNoMedia(item
->GetPath());
308 // check whether to re-use previously computed fast hash
309 if (!CanFastHash(items
, regexps
) || fastHash
.empty())
310 GetPathHash(items
, hash
);
315 if (StringUtils::EqualsNoCase(hash
, dbHash
))
316 { // hash matches - skipping
317 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Skipping dir '{}' due to no change{}",
318 CURL::GetRedacted(strDirectory
), !fastHash
.empty() ? " (fasthash)" : "");
321 else if (hash
.empty())
322 { // directory empty or non-existent - add to clean list and skip
324 "VideoInfoScanner: Skipping dir '{}' as it's empty or doesn't exist - adding to "
326 CURL::GetRedacted(strDirectory
));
328 m_pathsToClean
.insert(m_database
.GetPathId(strDirectory
));
331 else if (dbHash
.empty())
332 { // new folder - scan
333 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Scanning dir '{}' as not in the database",
334 CURL::GetRedacted(strDirectory
));
337 { // hash changed - rescan
338 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
339 CURL::GetRedacted(strDirectory
), dbHash
, hash
);
342 else if (content
== CONTENT_TVSHOWS
)
345 m_handle
->SetTitle(StringUtils::Format(g_localizeStrings
.Get(20319), info
->Name()));
347 if (foundDirectly
&& !settings
.parent_name_root
)
349 CDirectory::GetDirectory(strDirectory
, items
, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
351 items
.SetPath(strDirectory
);
352 GetPathHash(items
, hash
);
354 if (!m_database
.GetPathHash(strDirectory
, dbHash
) || !StringUtils::EqualsNoCase(dbHash
, hash
))
361 CFileItemPtr
item(new CFileItem(URIUtils::GetFileName(strDirectory
)));
362 item
->SetPath(strDirectory
);
363 item
->m_bIsFolder
= true;
365 items
.SetPath(URIUtils::GetParentPath(item
->GetPath()));
371 if (RetrieveVideoInfo(items
, settings
.parent_name_root
, content
))
373 if (!m_bStop
&& (content
== CONTENT_MOVIES
|| content
== CONTENT_MUSICVIDEOS
))
375 m_database
.SetPathHash(strDirectory
, hash
);
377 m_pathsToClean
.insert(m_database
.GetPathId(strDirectory
));
378 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Finished adding information from dir {}",
379 CURL::GetRedacted(strDirectory
));
385 m_pathsToClean
.insert(m_database
.GetPathId(strDirectory
));
386 CLog::Log(LOGDEBUG
, "VideoInfoScanner: No (new) information was found in dir {}",
387 CURL::GetRedacted(strDirectory
));
390 else if (!StringUtils::EqualsNoCase(hash
, dbHash
) && (content
== CONTENT_MOVIES
|| content
== CONTENT_MUSICVIDEOS
))
391 { // update the hash either way - we may have changed the hash to a fast version
392 m_database
.SetPathHash(strDirectory
, hash
);
396 OnDirectoryScanned(strDirectory
);
398 for (int i
= 0; i
< items
.Size(); ++i
)
400 CFileItemPtr pItem
= items
[i
];
405 // if we have a directory item (non-playlist) we then recurse into that folder
406 // do not recurse for tv shows - we have already looked recursively for episodes
407 if (pItem
->m_bIsFolder
&& !pItem
->IsParentFolder() && !pItem
->IsPlayList() && settings
.recurse
> 0 && content
!= CONTENT_TVSHOWS
)
409 if (!DoScan(pItem
->GetPath()))
418 bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList
& items
, bool bDirNames
, CONTENT_TYPE content
, bool useLocal
, CScraperUrl
* pURL
, bool fetchEpisodes
, CGUIDialogProgress
* pDlgProgress
)
422 if (items
.Size() > 1 || (items
[0]->m_bIsFolder
&& fetchEpisodes
))
424 pDlgProgress
->ShowProgressBar(true);
425 pDlgProgress
->SetPercentage(0);
428 pDlgProgress
->ShowProgressBar(false);
430 pDlgProgress
->Progress();
435 bool FoundSomeInfo
= false;
436 std::vector
<int> seenPaths
;
437 for (int i
= 0; i
< items
.Size(); ++i
)
439 CFileItemPtr pItem
= items
[i
];
441 // we do this since we may have a override per dir
442 ScraperPtr info2
= m_database
.GetScraperForPath(pItem
->m_bIsFolder
? pItem
->GetPath() : items
.GetPath());
446 // Discard all .nomedia folders
447 if (pItem
->m_bIsFolder
&& HasNoMedia(pItem
->GetPath()))
450 // Discard all exclude files defined by regExExclude
451 if (CUtil::ExcludeFileOrFolder(pItem
->GetPath(), (content
== CONTENT_TVSHOWS
) ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
452 : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps
))
455 if (info2
->Content() == CONTENT_MOVIES
|| info2
->Content() == CONTENT_MUSICVIDEOS
)
458 m_handle
->SetPercentage(i
*100.f
/items
.Size());
461 // clear our scraper cache
464 INFO_RET ret
= INFO_CANCELLED
;
465 if (info2
->Content() == CONTENT_TVSHOWS
)
466 ret
= RetrieveInfoForTvShow(pItem
.get(), bDirNames
, info2
, useLocal
, pURL
, fetchEpisodes
, pDlgProgress
);
467 else if (info2
->Content() == CONTENT_MOVIES
)
468 ret
= RetrieveInfoForMovie(pItem
.get(), bDirNames
, info2
, useLocal
, pURL
, pDlgProgress
);
469 else if (info2
->Content() == CONTENT_MUSICVIDEOS
)
470 ret
= RetrieveInfoForMusicVideo(pItem
.get(), bDirNames
, info2
, useLocal
, pURL
, pDlgProgress
);
473 CLog::Log(LOGERROR
, "VideoInfoScanner: Unknown content type {} ({})", info2
->Content(),
474 CURL::GetRedacted(pItem
->GetPath()));
475 FoundSomeInfo
= false;
478 if (ret
== INFO_CANCELLED
|| ret
== INFO_ERROR
)
480 CLog::Log(LOGWARNING
,
481 "VideoInfoScanner: Error {} occurred while retrieving"
482 "information for {}.",
483 ret
, CURL::GetRedacted(pItem
->GetPath()));
484 FoundSomeInfo
= false;
487 if (ret
== INFO_ADDED
|| ret
== INFO_HAVE_ALREADY
)
488 FoundSomeInfo
= true;
489 else if (ret
== INFO_NOT_FOUND
)
491 CLog::Log(LOGWARNING
,
492 "No information found for item '{}', it won't be added to the library.",
493 CURL::GetRedacted(pItem
->GetPath()));
495 MediaType mediaType
= MediaTypeMovie
;
496 if (info2
->Content() == CONTENT_TVSHOWS
)
497 mediaType
= MediaTypeTvShow
;
498 else if (info2
->Content() == CONTENT_MUSICVIDEOS
)
499 mediaType
= MediaTypeMusicVideo
;
501 auto eventLog
= CServiceBroker::GetEventLog();
503 eventLog
->Add(EventPtr(new CMediaLibraryEvent(
504 mediaType
, pItem
->GetPath(), 24145,
505 StringUtils::Format(g_localizeStrings
.Get(24147), mediaType
,
506 URIUtils::GetFileName(pItem
->GetPath())),
507 pItem
->GetArt("thumb"), CURL::GetRedacted(pItem
->GetPath()), EventLevel::Warning
)));
512 // Keep track of directories we've seen
513 if (m_bClean
&& pItem
->m_bIsFolder
)
514 seenPaths
.push_back(m_database
.GetPathId(pItem
->GetPath()));
517 if (content
== CONTENT_TVSHOWS
&& ! seenPaths
.empty())
519 std::vector
<std::pair
<int, std::string
>> libPaths
;
520 m_database
.GetSubPaths(items
.GetPath(), libPaths
);
521 for (std::vector
<std::pair
<int, std::string
> >::iterator i
= libPaths
.begin(); i
< libPaths
.end(); ++i
)
523 if (find(seenPaths
.begin(), seenPaths
.end(), i
->first
) == seenPaths
.end())
524 m_pathsToClean
.insert(i
->first
);
528 pDlgProgress
->ShowProgressBar(false);
531 return FoundSomeInfo
;
534 CInfoScanner::INFO_RET
535 CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem
*pItem
,
541 CGUIDialogProgress
* pDlgProgress
)
544 std::string strPath
= pItem
->GetPath();
545 if (pItem
->m_bIsFolder
)
546 idTvShow
= m_database
.GetTvShowId(strPath
);
547 else if (pItem
->IsPlugin() && pItem
->HasVideoInfoTag() && pItem
->GetVideoInfoTag()->m_iIdShow
>= 0)
549 // for plugin source we cannot get idTvShow from episode path with URIUtils::GetDirectory() in all cases
550 // so use m_iIdShow from video info tag if possible
551 idTvShow
= pItem
->GetVideoInfoTag()->m_iIdShow
;
552 CVideoInfoTag showInfo
;
553 if (m_database
.GetTvShowInfo(std::string(), showInfo
, idTvShow
, nullptr, 0))
554 strPath
= showInfo
.GetPath();
558 strPath
= URIUtils::GetDirectory(strPath
);
559 idTvShow
= m_database
.GetTvShowId(strPath
);
561 if (idTvShow
> -1 && (fetchEpisodes
|| !pItem
->m_bIsFolder
))
563 INFO_RET ret
= RetrieveInfoForEpisodes(pItem
, idTvShow
, info2
, useLocal
, pDlgProgress
);
564 if (ret
== INFO_ADDED
)
565 m_database
.SetPathHash(strPath
, pItem
->GetProperty("hash").asString());
569 if (ProgressCancelled(pDlgProgress
, pItem
->m_bIsFolder
? 20353 : 20361, pItem
->GetLabel()))
570 return INFO_CANCELLED
;
573 m_handle
->SetText(pItem
->GetMovieName(bDirNames
));
575 CInfoScanner::INFO_TYPE result
=CInfoScanner::NO_NFO
;
578 std::unique_ptr
<IVideoInfoTagLoader
> loader
;
581 loader
.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem
, info2
, bDirNames
));
584 pItem
->GetVideoInfoTag()->Reset();
585 result
= loader
->Load(*pItem
->GetVideoInfoTag(), false);
589 if (result
== CInfoScanner::FULL_NFO
)
592 long lResult
= AddVideo(pItem
, info2
->Content(), bDirNames
, useLocal
);
597 INFO_RET ret
= RetrieveInfoForEpisodes(pItem
, lResult
, info2
, useLocal
, pDlgProgress
);
598 if (ret
== INFO_ADDED
)
599 m_database
.SetPathHash(pItem
->GetPath(), pItem
->GetProperty("hash").asString());
604 if (result
== CInfoScanner::URL_NFO
|| result
== CInfoScanner::COMBINED_NFO
)
606 scrUrl
= loader
->ScraperUrl();
612 std::string movieTitle
= pItem
->GetMovieName(bDirNames
);
613 int movieYear
= -1; // hint that movie title was not found
614 if (result
== CInfoScanner::TITLE_NFO
)
616 CVideoInfoTag
* tag
= pItem
->GetVideoInfoTag();
617 movieTitle
= tag
->GetTitle();
618 movieYear
= tag
->GetYear(); // movieYear is expected to be >= 0
620 if (pURL
&& pURL
->HasUrls())
622 else if ((retVal
= FindVideo(movieTitle
, movieYear
, info2
, url
, pDlgProgress
)) <= 0)
623 return retVal
< 0 ? INFO_CANCELLED
: INFO_NOT_FOUND
;
625 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
626 url
.GetFirstThumbUrl(), info2
->Name(), TranslateContent(info2
->Content()));
629 if (GetDetails(pItem
, url
, info2
,
630 (result
== CInfoScanner::COMBINED_NFO
||
631 result
== CInfoScanner::OVERRIDE_NFO
) ? loader
.get() : nullptr,
634 if ((lResult
= AddVideo(pItem
, info2
->Content(), false, useLocal
)) < 0)
639 INFO_RET ret
= RetrieveInfoForEpisodes(pItem
, lResult
, info2
, useLocal
, pDlgProgress
);
640 if (ret
== INFO_ADDED
)
641 m_database
.SetPathHash(pItem
->GetPath(), pItem
->GetProperty("hash").asString());
646 CInfoScanner::INFO_RET
647 CVideoInfoScanner::RetrieveInfoForMovie(CFileItem
*pItem
,
652 CGUIDialogProgress
* pDlgProgress
)
654 if (pItem
->m_bIsFolder
|| !pItem
->IsVideo() || pItem
->IsNFO() ||
655 (pItem
->IsPlayList() && !URIUtils::HasExtension(pItem
->GetPath(), ".strm")))
656 return INFO_NOT_NEEDED
;
658 if (ProgressCancelled(pDlgProgress
, 198, pItem
->GetLabel()))
659 return INFO_CANCELLED
;
661 if (m_database
.HasMovieInfo(pItem
->GetPath()))
662 return INFO_HAVE_ALREADY
;
665 m_handle
->SetText(pItem
->GetMovieName(bDirNames
));
667 CInfoScanner::INFO_TYPE result
= CInfoScanner::NO_NFO
;
670 std::unique_ptr
<IVideoInfoTagLoader
> loader
;
673 loader
.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem
, info2
, bDirNames
));
676 pItem
->GetVideoInfoTag()->Reset();
677 result
= loader
->Load(*pItem
->GetVideoInfoTag(), false);
680 if (result
== CInfoScanner::FULL_NFO
)
682 if (AddVideo(pItem
, info2
->Content(), bDirNames
, true) < 0)
686 if (result
== CInfoScanner::URL_NFO
|| result
== CInfoScanner::COMBINED_NFO
)
688 scrUrl
= loader
->ScraperUrl();
694 std::string movieTitle
= pItem
->GetMovieName(bDirNames
);
695 int movieYear
= -1; // hint that movie title was not found
696 if (result
== CInfoScanner::TITLE_NFO
)
698 CVideoInfoTag
* tag
= pItem
->GetVideoInfoTag();
699 movieTitle
= tag
->GetTitle();
700 movieYear
= tag
->GetYear(); // movieYear is expected to be >= 0
702 if (pURL
&& pURL
->HasUrls())
704 else if ((retVal
= FindVideo(movieTitle
, movieYear
, info2
, url
, pDlgProgress
)) <= 0)
705 return retVal
< 0 ? INFO_CANCELLED
: INFO_NOT_FOUND
;
707 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
708 url
.GetFirstThumbUrl(), info2
->Name(), TranslateContent(info2
->Content()));
710 if (GetDetails(pItem
, url
, info2
,
711 (result
== CInfoScanner::COMBINED_NFO
||
712 result
== CInfoScanner::OVERRIDE_NFO
) ? loader
.get() : nullptr,
715 if (AddVideo(pItem
, info2
->Content(), bDirNames
, useLocal
) < 0)
719 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
720 return INFO_NOT_FOUND
;
723 CInfoScanner::INFO_RET
724 CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem
*pItem
,
729 CGUIDialogProgress
* pDlgProgress
)
731 if (pItem
->m_bIsFolder
|| !pItem
->IsVideo() || pItem
->IsNFO() ||
732 (pItem
->IsPlayList() && !URIUtils::HasExtension(pItem
->GetPath(), ".strm")))
733 return INFO_NOT_NEEDED
;
735 if (ProgressCancelled(pDlgProgress
, 20394, pItem
->GetLabel()))
736 return INFO_CANCELLED
;
738 if (m_database
.HasMusicVideoInfo(pItem
->GetPath()))
739 return INFO_HAVE_ALREADY
;
742 m_handle
->SetText(pItem
->GetMovieName(bDirNames
));
744 CInfoScanner::INFO_TYPE result
= CInfoScanner::NO_NFO
;
747 std::unique_ptr
<IVideoInfoTagLoader
> loader
;
750 loader
.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem
, info2
, bDirNames
));
753 pItem
->GetVideoInfoTag()->Reset();
754 result
= loader
->Load(*pItem
->GetVideoInfoTag(), false);
757 if (result
== CInfoScanner::FULL_NFO
)
759 if (AddVideo(pItem
, info2
->Content(), bDirNames
, true) < 0)
763 if (result
== CInfoScanner::URL_NFO
|| result
== CInfoScanner::COMBINED_NFO
)
765 scrUrl
= loader
->ScraperUrl();
771 std::string movieTitle
= pItem
->GetMovieName(bDirNames
);
772 int movieYear
= -1; // hint that movie title was not found
773 if (result
== CInfoScanner::TITLE_NFO
)
775 CVideoInfoTag
* tag
= pItem
->GetVideoInfoTag();
776 movieTitle
= tag
->GetTitle();
777 movieYear
= tag
->GetYear(); // movieYear is expected to be >= 0
779 if (pURL
&& pURL
->HasUrls())
781 else if ((retVal
= FindVideo(movieTitle
, movieYear
, info2
, url
, pDlgProgress
)) <= 0)
782 return retVal
< 0 ? INFO_CANCELLED
: INFO_NOT_FOUND
;
784 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
785 url
.GetFirstThumbUrl(), info2
->Name(), TranslateContent(info2
->Content()));
787 if (GetDetails(pItem
, url
, info2
,
788 (result
== CInfoScanner::COMBINED_NFO
||
789 result
== CInfoScanner::OVERRIDE_NFO
) ? loader
.get() : nullptr,
792 if (AddVideo(pItem
, info2
->Content(), bDirNames
, useLocal
) < 0)
796 //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
797 return INFO_NOT_FOUND
;
800 CInfoScanner::INFO_RET
801 CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem
*item
,
803 const ADDON::ScraperPtr
&scraper
,
805 CGUIDialogProgress
*progress
)
807 // enumerate episodes
809 if (!EnumerateSeriesFolder(item
, files
))
810 return INFO_HAVE_ALREADY
;
811 if (files
.empty()) // no update or no files
812 return INFO_NOT_NEEDED
;
814 if (m_bStop
|| (progress
&& progress
->IsCanceled()))
815 return INFO_CANCELLED
;
817 CVideoInfoTag showInfo
;
818 m_database
.GetTvShowInfo("", showInfo
, showID
);
819 INFO_RET ret
= OnProcessSeriesFolder(files
, scraper
, useLocal
, showInfo
, progress
);
821 if (ret
== INFO_ADDED
)
823 std::map
<int, std::map
<std::string
, std::string
>> seasonArt
;
824 m_database
.GetTvShowSeasonArt(showID
, seasonArt
);
826 bool updateSeasonArt
= false;
827 for (std::map
<int, std::map
<std::string
, std::string
>>::const_iterator i
= seasonArt
.begin(); i
!= seasonArt
.end(); ++i
)
829 if (i
->second
.empty())
831 updateSeasonArt
= true;
838 if (!item
->IsPlugin() || scraper
->ID() != "metadata.local")
840 CVideoInfoDownloader
loader(scraper
);
841 loader
.GetArtwork(showInfo
);
843 GetSeasonThumbs(showInfo
, seasonArt
, CVideoThumbLoader::GetArtTypes(MediaTypeSeason
), useLocal
&& !item
->IsPlugin());
844 for (std::map
<int, std::map
<std::string
, std::string
> >::const_iterator i
= seasonArt
.begin(); i
!= seasonArt
.end(); ++i
)
846 int seasonID
= m_database
.AddSeason(showID
, i
->first
);
847 m_database
.SetArtForItem(seasonID
, MediaTypeSeason
, i
->second
);
854 bool CVideoInfoScanner::EnumerateSeriesFolder(CFileItem
* item
, EPISODELIST
& episodeList
)
857 const std::vector
<std::string
> ®exps
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
;
861 if (item
->m_bIsFolder
)
864 * Note: DoScan() will not remove this path as it's not recursing for tvshows.
865 * Remove this path from the list we're processing in order to avoid hitting
866 * it twice in the main loop.
868 std::set
<std::string
>::iterator it
= m_pathsToScan
.find(item
->GetPath());
869 if (it
!= m_pathsToScan
.end())
870 m_pathsToScan
.erase(it
);
872 std::string hash
, dbHash
;
873 bool allowEmptyHash
= false;
874 if (item
->IsPlugin())
876 // if plugin has already calculated a hash for directory contents - use it
877 // in this case we don't need to get directory listing from plugin for hash checking
878 if (item
->HasProperty("hash"))
880 hash
= item
->GetProperty("hash").asString();
881 allowEmptyHash
= true;
884 else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash
)
885 hash
= GetRecursiveFastHash(item
->GetPath(), regexps
);
887 if (m_database
.GetPathHash(item
->GetPath(), dbHash
) && (allowEmptyHash
|| !hash
.empty()) && StringUtils::EqualsNoCase(dbHash
, hash
))
889 // fast hashes match - no need to process anything
893 // fast hash cannot be computed or we need to rescan. fetch the listing.
896 int flags
= DIR_FLAG_DEFAULTS
;
898 flags
|= DIR_FLAG_NO_FILE_INFO
;
900 CUtil::GetRecursiveListing(item
->GetPath(), items
, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), flags
);
902 // fast hash failed - compute slow one
905 GetPathHash(items
, hash
);
906 if (StringUtils::EqualsNoCase(dbHash
, hash
))
908 // slow hashes match - no need to process anything
916 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Skipping dir '{}' due to no change",
917 CURL::GetRedacted(item
->GetPath()));
918 // update our dialog with our progress
920 OnDirectoryScanned(item
->GetPath());
925 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Scanning dir '{}' as not in the database",
926 CURL::GetRedacted(item
->GetPath()));
928 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
929 CURL::GetRedacted(item
->GetPath()), dbHash
, hash
);
933 m_pathsToClean
.insert(m_database
.GetPathId(item
->GetPath()));
934 m_database
.GetPathsForTvShow(m_database
.GetTvShowId(item
->GetPath()), m_pathsToClean
);
936 item
->SetProperty("hash", hash
);
940 CFileItemPtr
newItem(new CFileItem(*item
));
945 stack down any dvd folders
946 need to sort using the full path since this is a collapsed recursive listing of all subdirs
947 video_ts.ifo files should sort at the top of a dvd folder in ascending order
949 /foo/bar/video_ts.ifo
954 // since we're doing this now anyway, should other items be stacked?
955 items
.Sort(SortByPath
, SortOrderAscending
);
957 while (x
< items
.Size())
959 if (items
[x
]->m_bIsFolder
)
965 std::string strPathX
, strFileX
;
966 URIUtils::Split(items
[x
]->GetPath(), strPathX
, strFileX
);
967 //CLog::Log(LOGDEBUG,"{}:{}:{}", x, strPathX, strFileX);
970 if (StringUtils::EqualsNoCase(strFileX
, "VIDEO_TS.IFO"))
972 while (y
< items
.Size())
974 std::string strPathY
, strFileY
;
975 URIUtils::Split(items
[y
]->GetPath(), strPathY
, strFileY
);
976 //CLog::Log(LOGDEBUG," {}:{}:{}", y, strPathY, strFileY);
978 if (StringUtils::EqualsNoCase(strPathY
, strPathX
))
980 remove everything sorted below the video_ts.ifo file in the same path.
981 understandably this wont stack correctly if there are other files in the the dvd folder.
982 this should be unlikely and thus is being ignored for now but we can monitor the
983 where the path changes and potentially remove the items above the video_ts.ifo file.
994 for (int i
=0;i
<items
.Size();++i
)
996 if (items
[i
]->m_bIsFolder
)
998 std::string strPath
= URIUtils::GetDirectory(items
[i
]->GetPath());
999 URIUtils::RemoveSlashAtEnd(strPath
); // want no slash for the test that follows
1001 if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strPath
), "sample"))
1004 // Discard all exclude files defined by regExExcludes
1005 if (CUtil::ExcludeFileOrFolder(items
[i
]->GetPath(), regexps
))
1009 * Check if the media source has already set the season and episode or original air date in
1010 * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
1011 * any false positive matches.
1013 if (ProcessItemByVideoInfoTag(items
[i
].get(), episodeList
))
1016 if (!EnumerateEpisodeItem(items
[i
].get(), episodeList
))
1017 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Could not enumerate file {}", CURL::GetRedacted(items
[i
]->GetPath()));
1022 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem
*item
, EPISODELIST
&episodeList
)
1024 if (!item
->HasVideoInfoTag())
1027 const CVideoInfoTag
* tag
= item
->GetVideoInfoTag();
1028 bool isValid
= false;
1030 * First check the season and episode number. This takes precedence over the original air
1031 * date and episode title. Must be a valid season and episode number combination.
1033 if (tag
->m_iSeason
> -1 && tag
->m_iEpisode
> 0)
1036 // episode 0 with non-zero season is valid! (e.g. prequel episode)
1037 if (item
->IsPlugin() && tag
->m_iSeason
> 0 && tag
->m_iEpisode
>= 0)
1043 episode
.strPath
= item
->GetPath();
1044 episode
.iSeason
= tag
->m_iSeason
;
1045 episode
.iEpisode
= tag
->m_iEpisode
;
1046 episode
.isFolder
= false;
1047 // save full item for plugin source
1048 if (item
->IsPlugin())
1049 episode
.item
= std::make_shared
<CFileItem
>(*item
);
1050 episodeList
.push_back(episode
);
1051 CLog::Log(LOGDEBUG
, "{} - found match for: {}. Season {}, Episode {}", __FUNCTION__
,
1052 CURL::GetRedacted(episode
.strPath
), episode
.iSeason
, episode
.iEpisode
);
1057 * Next preference is the first aired date. If it exists use that for matching the TV Show
1058 * information. Also set the title in case there are multiple matches for the first aired date.
1060 if (tag
->m_firstAired
.IsValid())
1063 episode
.strPath
= item
->GetPath();
1064 episode
.strTitle
= tag
->m_strTitle
;
1065 episode
.isFolder
= false;
1067 * Set season and episode to -1 to indicate to use the aired date.
1069 episode
.iSeason
= -1;
1070 episode
.iEpisode
= -1;
1072 * The first aired date string must be parseable.
1074 episode
.cDate
= item
->GetVideoInfoTag()->m_firstAired
;
1075 episodeList
.push_back(episode
);
1076 CLog::Log(LOGDEBUG
, "{} - found match for: '{}', firstAired: '{}' = '{}', title: '{}'",
1077 __FUNCTION__
, CURL::GetRedacted(episode
.strPath
),
1078 tag
->m_firstAired
.GetAsDBDateTime(), episode
.cDate
.GetAsLocalizedDate(),
1084 * Next preference is the episode title. If it exists use that for matching the TV Show
1087 if (!tag
->m_strTitle
.empty())
1090 episode
.strPath
= item
->GetPath();
1091 episode
.strTitle
= tag
->m_strTitle
;
1092 episode
.isFolder
= false;
1094 * Set season and episode to -1 to indicate to use the title.
1096 episode
.iSeason
= -1;
1097 episode
.iEpisode
= -1;
1098 episodeList
.push_back(episode
);
1099 CLog::Log(LOGDEBUG
, "{} - found match for: '{}', title: '{}'", __FUNCTION__
,
1100 CURL::GetRedacted(episode
.strPath
), episode
.strTitle
);
1105 * There is no further episode information available if both the season and episode number have
1106 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
1107 * to the episode list.
1109 if (tag
->m_iSeason
== 0 && tag
->m_iEpisode
== 0)
1112 "{} - found exclusion match for: {}. Both Season and Episode are 0. Item will be "
1113 "ignored for scanning.",
1114 __FUNCTION__
, CURL::GetRedacted(item
->GetPath()));
1121 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem
*item
, EPISODELIST
& episodeList
)
1123 SETTINGS_TVSHOWLIST expression
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowEnumRegExps
;
1125 std::string strLabel
;
1127 // remove path to main file if it's a bd or dvd folder to regex the right (folder) name
1128 if (item
->IsOpticalMediaFile())
1130 strLabel
= item
->GetLocalMetadataPath();
1131 URIUtils::RemoveSlashAtEnd(strLabel
);
1134 strLabel
= item
->GetPath();
1136 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
1137 strLabel
= CURL::Decode(CURL::GetRedacted(strLabel
));
1139 for (unsigned int i
=0;i
<expression
.size();++i
)
1141 CRegExp
reg(true, CRegExp::autoUtf8
);
1142 if (!reg
.RegComp(expression
[i
].regexp
))
1145 int regexppos
, regexp2pos
;
1146 //CLog::Log(LOGDEBUG,"running expression {} on {}",expression[i].regexp,strLabel);
1147 if ((regexppos
= reg
.RegFind(strLabel
.c_str())) < 0)
1151 episode
.strPath
= item
->GetPath();
1152 episode
.iSeason
= -1;
1153 episode
.iEpisode
= -1;
1154 episode
.cDate
.SetValid(false);
1155 episode
.isFolder
= false;
1157 bool byDate
= expression
[i
].byDate
? true : false;
1158 bool byTitle
= expression
[i
].byTitle
;
1159 int defaultSeason
= expression
[i
].defaultSeason
;
1163 if (!GetAirDateFromRegExp(reg
, episode
))
1166 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Found date based match {} ({}) [{}]",
1167 CURL::GetRedacted(episode
.strPath
), episode
.cDate
.GetAsLocalizedDate(),
1168 expression
[i
].regexp
);
1172 if (!GetEpisodeTitleFromRegExp(reg
, episode
))
1175 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Found title based match {} ({}) [{}]",
1176 CURL::GetRedacted(episode
.strPath
), episode
.strTitle
, expression
[i
].regexp
);
1180 if (!GetEpisodeAndSeasonFromRegExp(reg
, episode
, defaultSeason
))
1183 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Found episode match {} (s{}e{}) [{}]",
1184 CURL::GetRedacted(episode
.strPath
), episode
.iSeason
, episode
.iEpisode
,
1185 expression
[i
].regexp
);
1188 // Grab the remainder from first regexp run
1189 // as second run might modify or empty it.
1190 std::string
remainder(reg
.GetMatch(3));
1193 * Check if the files base path is a dedicated folder that contains
1194 * only this single episode. If season and episode match with the
1195 * actual media file, we set episode.isFolder to true.
1197 std::string strBasePath
= item
->GetBaseMoviePath(true);
1198 URIUtils::RemoveSlashAtEnd(strBasePath
);
1199 strBasePath
= URIUtils::GetFileName(strBasePath
);
1201 if (reg
.RegFind(strBasePath
.c_str()) > -1)
1206 GetAirDateFromRegExp(reg
, parent
);
1207 if (episode
.cDate
== parent
.cDate
)
1208 episode
.isFolder
= true;
1212 GetEpisodeAndSeasonFromRegExp(reg
, parent
, defaultSeason
);
1213 if (episode
.iSeason
== parent
.iSeason
&& episode
.iEpisode
== parent
.iEpisode
)
1214 episode
.isFolder
= true;
1218 // add what we found by now
1219 episodeList
.push_back(episode
);
1221 CRegExp
reg2(true, CRegExp::autoUtf8
);
1222 // check the remainder of the string for any further episodes.
1223 if (!byDate
&& reg2
.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowMultiPartEnumRegExp
))
1227 // we want "long circuit" OR below so that both offsets are evaluated
1228 while (static_cast<int>((regexp2pos
= reg2
.RegFind(remainder
.c_str() + offset
)) > -1) |
1229 static_cast<int>((regexppos
= reg
.RegFind(remainder
.c_str() + offset
)) > -1))
1231 if (((regexppos
<= regexp2pos
) && regexppos
!= -1) ||
1232 (regexppos
>= 0 && regexp2pos
== -1))
1234 GetEpisodeAndSeasonFromRegExp(reg
, episode
, defaultSeason
);
1236 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Adding new season {}, multipart episode {} [{}]",
1237 episode
.iSeason
, episode
.iEpisode
,
1238 CServiceBroker::GetSettingsComponent()
1239 ->GetAdvancedSettings()
1240 ->m_tvshowMultiPartEnumRegExp
);
1242 episodeList
.push_back(episode
);
1243 remainder
= reg
.GetMatch(3);
1246 else if (((regexp2pos
< regexppos
) && regexp2pos
!= -1) ||
1247 (regexp2pos
>= 0 && regexppos
== -1))
1249 episode
.iEpisode
= atoi(reg2
.GetMatch(1).c_str());
1250 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Adding multipart episode {} [{}]",
1252 CServiceBroker::GetSettingsComponent()
1253 ->GetAdvancedSettings()
1254 ->m_tvshowMultiPartEnumRegExp
);
1255 episodeList
.push_back(episode
);
1256 offset
+= regexp2pos
+ reg2
.GetFindLen();
1265 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp
®
, EPISODE
&episodeInfo
, int defaultSeason
)
1267 std::string
season(reg
.GetMatch(1));
1268 std::string
episode(reg
.GetMatch(2));
1270 if (!season
.empty() || !episode
.empty())
1272 char* endptr
= NULL
;
1273 if (season
.empty() && !episode
.empty())
1274 { // no season specified -> assume defaultSeason
1275 episodeInfo
.iSeason
= defaultSeason
;
1276 if ((episodeInfo
.iEpisode
= CUtil::TranslateRomanNumeral(episode
.c_str())) == -1)
1277 episodeInfo
.iEpisode
= strtol(episode
.c_str(), &endptr
, 10);
1279 else if (!season
.empty() && episode
.empty())
1280 { // no episode specification -> assume defaultSeason
1281 episodeInfo
.iSeason
= defaultSeason
;
1282 if ((episodeInfo
.iEpisode
= CUtil::TranslateRomanNumeral(season
.c_str())) == -1)
1283 episodeInfo
.iEpisode
= atoi(season
.c_str());
1286 { // season and episode specified
1287 episodeInfo
.iSeason
= atoi(season
.c_str());
1288 episodeInfo
.iEpisode
= strtol(episode
.c_str(), &endptr
, 10);
1292 if (isalpha(*endptr
))
1293 episodeInfo
.iSubepisode
= *endptr
- (islower(*endptr
) ? 'a' : 'A') + 1;
1294 else if (*endptr
== '.')
1295 episodeInfo
.iSubepisode
= atoi(endptr
+1);
1302 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp
®
, EPISODE
&episodeInfo
)
1304 std::string
param1(reg
.GetMatch(1));
1305 std::string
param2(reg
.GetMatch(2));
1306 std::string
param3(reg
.GetMatch(3));
1308 if (!param1
.empty() && !param2
.empty() && !param3
.empty())
1310 // regular expression by date
1311 int len1
= param1
.size();
1312 int len2
= param2
.size();
1313 int len3
= param3
.size();
1315 if (len1
==4 && len2
==2 && len3
==2)
1317 // yyyy mm dd format
1318 episodeInfo
.cDate
.SetDate(atoi(param1
.c_str()), atoi(param2
.c_str()), atoi(param3
.c_str()));
1320 else if (len1
==2 && len2
==2 && len3
==4)
1322 // mm dd yyyy format
1323 episodeInfo
.cDate
.SetDate(atoi(param3
.c_str()), atoi(param1
.c_str()), atoi(param2
.c_str()));
1326 return episodeInfo
.cDate
.IsValid();
1329 bool CVideoInfoScanner::GetEpisodeTitleFromRegExp(CRegExp
& reg
, EPISODE
& episodeInfo
)
1331 std::string
param1(reg
.GetMatch(1));
1333 if (!param1
.empty())
1335 episodeInfo
.strTitle
= param1
;
1341 long CVideoInfoScanner::AddVideo(CFileItem
*pItem
, const CONTENT_TYPE
&content
, bool videoFolder
/* = false */, bool useLocal
/* = true */, const CVideoInfoTag
*showInfo
/* = NULL */, bool libraryImport
/* = false */)
1343 // ensure our database is open (this can get called via other classes)
1344 if (!m_database
.Open())
1348 GetArtwork(pItem
, content
, videoFolder
, useLocal
&& !pItem
->IsPlugin(), showInfo
? showInfo
->m_strPath
: "");
1350 // ensure the art map isn't completely empty by specifying an empty thumb
1351 std::map
<std::string
, std::string
> art
= pItem
->GetArt();
1355 CVideoInfoTag
&movieDetails
= *pItem
->GetVideoInfoTag();
1356 if (movieDetails
.m_basePath
.empty())
1357 movieDetails
.m_basePath
= pItem
->GetBaseMoviePath(videoFolder
);
1358 movieDetails
.m_parentPathID
= m_database
.AddPath(URIUtils::GetParentPath(movieDetails
.m_basePath
));
1360 movieDetails
.m_strFileNameAndPath
= pItem
->GetPath();
1362 if (pItem
->m_bIsFolder
)
1363 movieDetails
.m_strPath
= pItem
->GetPath();
1365 std::string
strTitle(movieDetails
.m_strTitle
);
1367 if (showInfo
&& content
== CONTENT_TVSHOWS
)
1369 strTitle
= StringUtils::Format("{} - {}x{} - {}", showInfo
->m_strTitle
,
1370 movieDetails
.m_iSeason
, movieDetails
.m_iEpisode
, strTitle
);
1373 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
1374 CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS
) &&
1375 CDVDFileInfo::GetFileStreamDetails(pItem
))
1376 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Extracted filestream details from video file {}",
1377 CURL::GetRedacted(pItem
->GetPath()));
1379 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Adding new item to {}:{}", TranslateContent(content
), CURL::GetRedacted(pItem
->GetPath()));
1382 if (content
== CONTENT_MOVIES
)
1384 // find local trailer first
1385 std::string strTrailer
= pItem
->FindTrailer();
1386 if (!strTrailer
.empty())
1387 movieDetails
.m_strTrailer
= strTrailer
;
1389 lResult
= m_database
.SetDetailsForMovie(movieDetails
, art
);
1390 movieDetails
.m_iDbId
= lResult
;
1391 movieDetails
.m_type
= MediaTypeMovie
;
1393 // setup links to shows if the linked shows are in the db
1394 for (unsigned int i
=0; i
< movieDetails
.m_showLink
.size(); ++i
)
1396 CFileItemList items
;
1397 m_database
.GetTvShowsByName(movieDetails
.m_showLink
[i
], items
);
1399 m_database
.LinkMovieToTvshow(lResult
, items
[0]->GetVideoInfoTag()->m_iDbId
, false);
1401 CLog::Log(LOGDEBUG
, "VideoInfoScanner: Failed to link movie {} to show {}",
1402 movieDetails
.m_strTitle
, movieDetails
.m_showLink
[i
]);
1405 else if (content
== CONTENT_TVSHOWS
)
1407 if (pItem
->m_bIsFolder
)
1410 multipaths are not stored in the database, so in the case we have one,
1411 we split the paths, and compute the parent paths in each case.
1413 std::vector
<std::string
> multipath
;
1414 if (!URIUtils::IsMultiPath(pItem
->GetPath()) || !CMultiPathDirectory::GetPaths(pItem
->GetPath(), multipath
))
1415 multipath
.push_back(pItem
->GetPath());
1416 std::vector
<std::pair
<std::string
, std::string
> > paths
;
1417 for (std::vector
<std::string
>::const_iterator i
= multipath
.begin(); i
!= multipath
.end(); ++i
)
1418 paths
.emplace_back(*i
, URIUtils::GetParentPath(*i
));
1420 std::map
<int, std::map
<std::string
, std::string
> > seasonArt
;
1423 GetSeasonThumbs(movieDetails
, seasonArt
, CVideoThumbLoader::GetArtTypes(MediaTypeSeason
), useLocal
&& !pItem
->IsPlugin());
1425 lResult
= m_database
.SetDetailsForTvShow(paths
, movieDetails
, art
, seasonArt
);
1426 movieDetails
.m_iDbId
= lResult
;
1427 movieDetails
.m_type
= MediaTypeTvShow
;
1431 // we add episode then set details, as otherwise set details will delete the
1432 // episode then add, which breaks multi-episode files.
1433 int idShow
= showInfo
? showInfo
->m_iDbId
: -1;
1434 int idEpisode
= m_database
.AddNewEpisode(idShow
, movieDetails
);
1435 lResult
= m_database
.SetDetailsForEpisode(movieDetails
, art
, idShow
, idEpisode
);
1436 movieDetails
.m_iDbId
= lResult
;
1437 movieDetails
.m_type
= MediaTypeEpisode
;
1438 movieDetails
.m_strShowTitle
= showInfo
? showInfo
->m_strTitle
: "";
1439 if (movieDetails
.m_EpBookmark
.timeInSeconds
> 0)
1441 movieDetails
.m_strFileNameAndPath
= pItem
->GetPath();
1442 movieDetails
.m_EpBookmark
.seasonNumber
= movieDetails
.m_iSeason
;
1443 movieDetails
.m_EpBookmark
.episodeNumber
= movieDetails
.m_iEpisode
;
1444 m_database
.AddBookMarkForEpisode(movieDetails
, movieDetails
.m_EpBookmark
);
1448 else if (content
== CONTENT_MUSICVIDEOS
)
1450 lResult
= m_database
.SetDetailsForMusicVideo(movieDetails
, art
);
1451 movieDetails
.m_iDbId
= lResult
;
1452 movieDetails
.m_type
= MediaTypeMusicVideo
;
1455 if (!pItem
->m_bIsFolder
)
1457 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryImportWatchedState
|| libraryImport
)
1458 m_database
.SetPlayCount(*pItem
, movieDetails
.GetPlayCount(), movieDetails
.m_lastPlayed
);
1460 if ((CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryImportResumePoint
|| libraryImport
) &&
1461 movieDetails
.GetResumePoint().IsSet())
1462 m_database
.AddBookMarkToFile(pItem
->GetPath(), movieDetails
.GetResumePoint(), CBookmark::RESUME
);
1467 CFileItemPtr itemCopy
= CFileItemPtr(new CFileItem(*pItem
));
1469 data
["added"] = true;
1471 data
["transaction"] = true;
1472 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary
, "OnUpdate",
1477 std::string
ContentToMediaType(CONTENT_TYPE content
, bool folder
)
1481 case CONTENT_MOVIES
:
1482 return MediaTypeMovie
;
1483 case CONTENT_MUSICVIDEOS
:
1484 return MediaTypeMusicVideo
;
1485 case CONTENT_TVSHOWS
:
1486 return folder
? MediaTypeTvShow
: MediaTypeEpisode
;
1492 std::string
CVideoInfoScanner::GetArtTypeFromSize(unsigned int width
, unsigned int height
)
1494 std::string type
= "thumb";
1495 if (width
*5 < height
*4)
1497 else if (width
*1 > height
*4)
1502 std::string
CVideoInfoScanner::GetMovieSetInfoFolder(const std::string
& setTitle
)
1504 if (setTitle
.empty())
1506 std::string path
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
1507 CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER
);
1510 path
= URIUtils::AddFileToFolder(path
, CUtil::MakeLegalFileName(setTitle
, LEGAL_WIN32_COMPAT
));
1511 URIUtils::AddSlashAtEnd(path
);
1513 "VideoInfoScanner: Looking for local artwork for movie set '{}' in folder '{}'",
1515 CURL::GetRedacted(path
));
1516 return CDirectory::Exists(path
) ? path
: "";
1519 void CVideoInfoScanner::AddLocalItemArtwork(CGUIListItem::ArtMap
& itemArt
,
1520 const std::vector
<std::string
>& wantedArtTypes
, const std::string
& itemPath
,
1521 bool addAll
, bool exactName
)
1523 std::string path
= URIUtils::GetDirectory(itemPath
);
1527 CFileItemList availableArtFiles
;
1528 CDirectory::GetDirectory(path
, availableArtFiles
,
1529 CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
1530 DIR_FLAG_NO_FILE_DIRS
| DIR_FLAG_READ_CACHE
| DIR_FLAG_NO_FILE_INFO
);
1532 std::string baseFilename
= URIUtils::GetFileName(itemPath
);
1533 if (!baseFilename
.empty())
1535 URIUtils::RemoveExtension(baseFilename
);
1536 baseFilename
.append("-");
1539 for (const auto& artFile
: availableArtFiles
)
1541 std::string candidate
= URIUtils::GetFileName(artFile
->GetPath());
1543 bool matchesFilename
=
1544 !baseFilename
.empty() && StringUtils::StartsWith(candidate
, baseFilename
);
1545 if (!baseFilename
.empty() && !matchesFilename
)
1548 if (matchesFilename
)
1549 candidate
.erase(0, baseFilename
.length());
1550 URIUtils::RemoveExtension(candidate
);
1551 StringUtils::ToLower(candidate
);
1553 // move 'folder' to thumb / poster / banner based on aspect ratio
1554 // if such artwork doesn't already exist
1555 if (!matchesFilename
&& StringUtils::EqualsNoCase(candidate
, "folder") &&
1556 !CVideoThumbLoader::IsArtTypeInWhitelist("folder", wantedArtTypes
, exactName
))
1558 // cache the image to determine sizing
1559 CTextureDetails details
;
1560 if (CServiceBroker::GetTextureCache()->CacheImage(artFile
->GetPath(), details
))
1562 candidate
= GetArtTypeFromSize(details
.width
, details
.height
);
1563 if (itemArt
.find(candidate
) != itemArt
.end())
1568 if ((addAll
&& CVideoThumbLoader::IsValidArtType(candidate
)) ||
1569 CVideoThumbLoader::IsArtTypeInWhitelist(candidate
, wantedArtTypes
, exactName
))
1571 itemArt
[candidate
] = artFile
->GetPath();
1576 void CVideoInfoScanner::GetArtwork(CFileItem
*pItem
, const CONTENT_TYPE
&content
, bool bApplyToDir
, bool useLocal
, const std::string
&actorArtPath
)
1578 int artLevel
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
1579 CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL
);
1580 if (artLevel
== CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE
)
1583 CVideoInfoTag
&movieDetails
= *pItem
->GetVideoInfoTag();
1584 movieDetails
.m_fanart
.Unpack();
1585 movieDetails
.m_strPictureURL
.Parse();
1587 CGUIListItem::ArtMap art
= pItem
->GetArt();
1589 // get and cache thumb images
1590 std::string mediaType
= ContentToMediaType(content
, pItem
->m_bIsFolder
);
1591 std::vector
<std::string
> artTypes
= CVideoThumbLoader::GetArtTypes(mediaType
);
1592 bool moviePartOfSet
= content
== CONTENT_MOVIES
&& !movieDetails
.m_set
.title
.empty();
1593 std::vector
<std::string
> movieSetArtTypes
;
1596 movieSetArtTypes
= CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection
);
1597 for (const std::string
& artType
: movieSetArtTypes
)
1598 artTypes
.push_back("set." + artType
);
1600 bool addAll
= artLevel
== CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL
;
1601 bool exactName
= artLevel
== CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC
;
1605 if (!pItem
->SkipLocalArt())
1607 if (bApplyToDir
&& (content
== CONTENT_MOVIES
|| content
== CONTENT_MUSICVIDEOS
))
1609 std::string filename
= pItem
->GetLocalArtBaseFilename();
1610 std::string directory
= URIUtils::GetDirectory(filename
);
1611 if (filename
!= directory
)
1612 AddLocalItemArtwork(art
, artTypes
, directory
, addAll
, exactName
);
1614 AddLocalItemArtwork(art
, artTypes
, pItem
->GetLocalArtBaseFilename(), addAll
, exactName
);
1619 std::string movieSetInfoPath
= GetMovieSetInfoFolder(movieDetails
.m_set
.title
);
1620 if (!movieSetInfoPath
.empty())
1622 CGUIListItem::ArtMap movieSetArt
;
1623 AddLocalItemArtwork(movieSetArt
, movieSetArtTypes
, movieSetInfoPath
, addAll
, exactName
);
1624 for (const auto& artItem
: movieSetArt
)
1626 art
["set." + artItem
.first
] = artItem
.second
;
1632 // find embedded art
1633 if (pItem
->HasVideoInfoTag() && !pItem
->GetVideoInfoTag()->m_coverArt
.empty())
1635 for (auto& it
: pItem
->GetVideoInfoTag()->m_coverArt
)
1637 if ((addAll
|| CVideoThumbLoader::IsArtTypeInWhitelist(it
.m_type
, artTypes
, exactName
)) &&
1638 art
.find(it
.m_type
) == art
.end())
1640 std::string thumb
= CTextureUtils::GetWrappedImageURL(pItem
->GetPath(),
1641 "video_" + it
.m_type
);
1642 art
.insert(std::make_pair(it
.m_type
, thumb
));
1647 // add online fanart (treated separately due to it being stored in m_fanart)
1648 if ((addAll
|| CVideoThumbLoader::IsArtTypeInWhitelist("fanart", artTypes
, exactName
)) &&
1649 art
.find("fanart") == art
.end())
1651 std::string fanart
= pItem
->GetVideoInfoTag()->m_fanart
.GetImageURL();
1652 if (!fanart
.empty())
1653 art
.insert(std::make_pair("fanart", fanart
));
1657 for (const auto& url
: pItem
->GetVideoInfoTag()->m_strPictureURL
.GetUrls())
1659 if (url
.m_type
!= CScraperUrl::UrlType::General
)
1661 std::string aspect
= url
.m_aspect
;
1663 // Backward compatibility with Kodi 11 Eden NFO files
1664 aspect
= mediaType
== MediaTypeEpisode
? "thumb" : "poster";
1666 if ((addAll
|| CVideoThumbLoader::IsArtTypeInWhitelist(aspect
, artTypes
, exactName
)) &&
1667 art
.find(aspect
) == art
.end())
1669 std::string image
= GetImage(url
, pItem
->GetPath());
1671 art
.insert(std::make_pair(aspect
, image
));
1675 for (const auto& artType
: artTypes
)
1677 if (art
.find(artType
) != art
.end())
1678 CServiceBroker::GetTextureCache()->BackgroundCacheImage(art
[artType
]);
1683 // parent folder to apply the thumb to and to search for local actor thumbs
1684 std::string parentDir
= URIUtils::GetBasePath(pItem
->GetPath());
1685 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS
))
1686 FetchActorThumbs(movieDetails
.m_cast
, actorArtPath
.empty() ? parentDir
: actorArtPath
);
1688 ApplyThumbToFolder(parentDir
, art
["thumb"]);
1691 std::string
CVideoInfoScanner::GetImage(const CScraperUrl::SUrlEntry
&image
, const std::string
& itemPath
)
1693 std::string thumb
= CScraperUrl::GetThumbUrl(image
);
1694 if (!thumb
.empty() && thumb
.find('/') == std::string::npos
&&
1695 thumb
.find('\\') == std::string::npos
)
1697 std::string strPath
= URIUtils::GetDirectory(itemPath
);
1698 thumb
= URIUtils::AddFileToFolder(strPath
, thumb
);
1703 CInfoScanner::INFO_RET
1704 CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST
& files
,
1705 const ADDON::ScraperPtr
&scraper
,
1707 const CVideoInfoTag
& showInfo
,
1708 CGUIDialogProgress
* pDlgProgress
/* = NULL */)
1712 pDlgProgress
->SetLine(1, CVariant
{showInfo
.m_strTitle
});
1713 pDlgProgress
->SetLine(2, CVariant
{20361});
1714 pDlgProgress
->SetPercentage(0);
1715 pDlgProgress
->ShowProgressBar(true);
1716 pDlgProgress
->Progress();
1719 EPISODELIST episodes
;
1720 bool hasEpisodeGuide
= false;
1722 int iMax
= files
.size();
1724 for (EPISODELIST::iterator file
= files
.begin(); file
!= files
.end(); ++file
)
1728 pDlgProgress
->SetLine(2, CVariant
{20361});
1729 pDlgProgress
->SetPercentage((int)((float)(iCurr
++)/iMax
*100));
1730 pDlgProgress
->Progress();
1733 m_handle
->SetPercentage(100.f
*iCurr
++/iMax
);
1735 if ((pDlgProgress
&& pDlgProgress
->IsCanceled()) || m_bStop
)
1736 return INFO_CANCELLED
;
1738 if (m_database
.GetEpisodeId(file
->strPath
, file
->iEpisode
, file
->iSeason
) > -1)
1741 m_handle
->SetText(g_localizeStrings
.Get(20415));
1750 item
.SetPath(file
->strPath
);
1751 item
.GetVideoInfoTag()->m_iEpisode
= file
->iEpisode
;
1754 // handle .nfo files
1755 CInfoScanner::INFO_TYPE result
=CInfoScanner::NO_NFO
;
1757 const ScraperPtr
& info(scraper
);
1758 std::unique_ptr
<IVideoInfoTagLoader
> loader
;
1761 loader
.reset(CVideoInfoTagLoaderFactory::CreateLoader(item
, info
, false));
1764 // no reset here on purpose
1765 result
= loader
->Load(*item
.GetVideoInfoTag(), false);
1768 if (result
== CInfoScanner::FULL_NFO
)
1770 // override with episode and season number from file if available
1771 if (file
->iEpisode
> -1)
1773 item
.GetVideoInfoTag()->m_iEpisode
= file
->iEpisode
;
1774 item
.GetVideoInfoTag()->m_iSeason
= file
->iSeason
;
1776 if (AddVideo(&item
, CONTENT_TVSHOWS
, file
->isFolder
, true, &showInfo
) < 0)
1781 if (!hasEpisodeGuide
)
1783 // fetch episode guide
1784 if (!showInfo
.m_strEpisodeGuide
.empty())
1787 url
.ParseAndAppendUrlsFromEpisodeGuide(showInfo
.m_strEpisodeGuide
);
1791 pDlgProgress
->SetLine(2, CVariant
{20354});
1792 pDlgProgress
->Progress();
1795 CVideoInfoDownloader
imdb(scraper
);
1796 if (!imdb
.GetEpisodeList(url
, episodes
))
1797 return INFO_NOT_FOUND
;
1799 hasEpisodeGuide
= true;
1803 if (episodes
.empty())
1806 "VideoInfoScanner: Asked to lookup episode {}"
1807 " online, but we have no episode guide. Check your tvshow.nfo and make"
1808 " sure the <episodeguide> tag is in place.",
1809 CURL::GetRedacted(file
->strPath
));
1813 EPISODE
key(file
->iSeason
, file
->iEpisode
, file
->iSubepisode
);
1814 EPISODE
backupkey(file
->iSeason
, file
->iEpisode
, 0);
1815 bool bFound
= false;
1816 EPISODELIST::iterator guide
= episodes
.begin();
1817 EPISODELIST matches
;
1819 for (; guide
!= episodes
.end(); ++guide
)
1821 if ((file
->iEpisode
!=-1) && (file
->iSeason
!=-1))
1828 else if ((file
->iSubepisode
!=0) && (backupkey
==*guide
))
1830 matches
.push_back(*guide
);
1834 if (file
->cDate
.IsValid() && guide
->cDate
.IsValid() && file
->cDate
==guide
->cDate
)
1836 matches
.push_back(*guide
);
1839 if (!guide
->cScraperUrl
.GetTitle().empty() &&
1840 StringUtils::EqualsNoCase(guide
->cScraperUrl
.GetTitle(), file
->strTitle
))
1845 if (!guide
->strTitle
.empty() && StringUtils::EqualsNoCase(guide
->strTitle
, file
->strTitle
))
1855 * If there is only one match or there are matches but no title to compare with to help
1856 * identify the best match, then pick the first match as the best possible candidate.
1858 * Otherwise, use the title to further refine the best match.
1860 if (matches
.size() == 1 || (file
->strTitle
.empty() && matches
.size() > 1))
1862 guide
= matches
.begin();
1865 else if (!file
->strTitle
.empty())
1867 CLog::Log(LOGDEBUG
, "VideoInfoScanner: analyzing parsed title '{}'", file
->strTitle
);
1868 double minscore
= 0; // Default minimum score is 0 to find whatever is the best match.
1870 EPISODELIST
*candidates
;
1871 if (matches
.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1873 minscore
= 0.8; // 80% should ensure a good match.
1874 candidates
= &episodes
;
1876 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1877 candidates
= &matches
;
1879 std::vector
<std::string
> titles
;
1880 for (guide
= candidates
->begin(); guide
!= candidates
->end(); ++guide
)
1882 auto title
= guide
->cScraperUrl
.GetTitle();
1885 title
= guide
->strTitle
;
1887 StringUtils::ToLower(title
);
1888 guide
->cScraperUrl
.SetTitle(title
);
1889 titles
.push_back(title
);
1893 std::string
loweredTitle(file
->strTitle
);
1894 StringUtils::ToLower(loweredTitle
);
1895 int index
= StringUtils::FindBestMatch(loweredTitle
, titles
, matchscore
);
1896 if (index
>= 0 && matchscore
>= minscore
)
1898 guide
= candidates
->begin() + index
;
1901 "{} fuzzy title match for show: '{}', title: '{}', match: '{}', score: {:f} "
1903 __FUNCTION__
, showInfo
.m_strTitle
, file
->strTitle
, titles
[index
], matchscore
,
1911 CVideoInfoDownloader
imdb(scraper
);
1913 item
.SetPath(file
->strPath
);
1914 if (!imdb
.GetEpisodeDetails(guide
->cScraperUrl
, *item
.GetVideoInfoTag(), pDlgProgress
))
1915 return INFO_NOT_FOUND
; //! @todo should we just skip to the next episode?
1917 // Only set season/epnum from filename when it is not already set by a scraper
1918 if (item
.GetVideoInfoTag()->m_iSeason
== -1)
1919 item
.GetVideoInfoTag()->m_iSeason
= guide
->iSeason
;
1920 if (item
.GetVideoInfoTag()->m_iEpisode
== -1)
1921 item
.GetVideoInfoTag()->m_iEpisode
= guide
->iEpisode
;
1923 if (AddVideo(&item
, CONTENT_TVSHOWS
, file
->isFolder
, useLocal
, &showInfo
) < 0)
1930 "{} - no match for show: '{}', season: {}, episode: {}.{}, airdate: '{}', title: '{}'",
1931 __FUNCTION__
, showInfo
.m_strTitle
, file
->iSeason
, file
->iEpisode
, file
->iSubepisode
,
1932 file
->cDate
.GetAsLocalizedDate(), file
->strTitle
);
1938 bool CVideoInfoScanner::GetDetails(CFileItem
*pItem
, CScraperUrl
&url
,
1939 const ScraperPtr
& scraper
,
1940 IVideoInfoTagLoader
* loader
,
1941 CGUIDialogProgress
* pDialog
/* = NULL */)
1943 CVideoInfoTag movieDetails
;
1945 if (m_handle
&& !url
.GetTitle().empty())
1946 m_handle
->SetText(url
.GetTitle());
1948 CVideoInfoDownloader
imdb(scraper
);
1949 bool ret
= imdb
.GetDetails(url
, movieDetails
, pDialog
);
1954 loader
->Load(movieDetails
, true);
1956 if (m_handle
&& url
.GetTitle().empty())
1957 m_handle
->SetText(movieDetails
.m_strTitle
);
1961 pDialog
->SetLine(1, CVariant
{movieDetails
.m_strTitle
});
1962 pDialog
->Progress();
1965 *pItem
->GetVideoInfoTag() = movieDetails
;
1968 return false; // no info found, or cancelled
1971 void CVideoInfoScanner::ApplyThumbToFolder(const std::string
&folder
, const std::string
&imdbThumb
)
1973 // copy icon to folder also;
1974 if (!imdbThumb
.empty())
1976 CFileItem
folderItem(folder
, true);
1977 CThumbLoader loader
;
1978 loader
.SetCachedImage(folderItem
, "thumb", imdbThumb
);
1982 int CVideoInfoScanner::GetPathHash(const CFileItemList
&items
, std::string
&hash
)
1984 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1985 if (0 == items
.Size()) return 0;
1986 CDigest digest
{CDigest::Type::MD5
};
1988 for (int i
= 0; i
< items
.Size(); ++i
)
1990 const CFileItemPtr pItem
= items
[i
];
1991 digest
.Update(pItem
->GetPath());
1992 if (pItem
->IsPlugin())
1994 // allow plugin to calculate hash itself using strings rather than binary data for size and date
1995 // according to ListItem.setInfo() documentation date format should be "d.m.Y"
1996 if (pItem
->m_dwSize
)
1997 digest
.Update(std::to_string(pItem
->m_dwSize
));
1998 if (pItem
->m_dateTime
.IsValid())
1999 digest
.Update(StringUtils::Format("{:02}.{:02}.{:04}", pItem
->m_dateTime
.GetDay(),
2000 pItem
->m_dateTime
.GetMonth(),
2001 pItem
->m_dateTime
.GetYear()));
2005 digest
.Update(&pItem
->m_dwSize
, sizeof(pItem
->m_dwSize
));
2006 KODI::TIME::FileTime time
= pItem
->m_dateTime
;
2007 digest
.Update(&time
, sizeof(KODI::TIME::FileTime
));
2009 if (pItem
->IsVideo() && !pItem
->IsPlayList() && !pItem
->IsNFO())
2012 hash
= digest
.Finalize();
2016 bool CVideoInfoScanner::CanFastHash(const CFileItemList
&items
, const std::vector
<std::string
> &excludes
) const
2018 if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash
|| items
.IsPlugin())
2021 for (int i
= 0; i
< items
.Size(); ++i
)
2023 if (items
[i
]->m_bIsFolder
&& !CUtil::ExcludeFileOrFolder(items
[i
]->GetPath(), excludes
))
2029 std::string
CVideoInfoScanner::GetFastHash(const std::string
&directory
,
2030 const std::vector
<std::string
> &excludes
) const
2032 CDigest digest
{CDigest::Type::MD5
};
2034 if (excludes
.size())
2035 digest
.Update(StringUtils::Join(excludes
, "|"));
2037 struct __stat64 buffer
;
2038 if (XFILE::CFile::Stat(directory
, &buffer
) == 0)
2040 int64_t time
= buffer
.st_mtime
;
2042 time
= buffer
.st_ctime
;
2045 digest
.Update((unsigned char *)&time
, sizeof(time
));
2046 return digest
.Finalize();
2052 std::string
CVideoInfoScanner::GetRecursiveFastHash(const std::string
&directory
,
2053 const std::vector
<std::string
> &excludes
) const
2055 CFileItemList items
;
2056 items
.Add(CFileItemPtr(new CFileItem(directory
, true)));
2057 CUtil::GetRecursiveDirsListing(directory
, items
, DIR_FLAG_NO_FILE_DIRS
| DIR_FLAG_NO_FILE_INFO
);
2059 CDigest digest
{CDigest::Type::MD5
};
2061 if (excludes
.size())
2062 digest
.Update(StringUtils::Join(excludes
, "|"));
2065 for (int i
=0; i
< items
.Size(); ++i
)
2067 int64_t stat_time
= 0;
2068 struct __stat64 buffer
;
2069 if (XFILE::CFile::Stat(items
[i
]->GetPath(), &buffer
) == 0)
2071 //! @todo some filesystems may return the mtime/ctime inline, in which case this is
2072 //! unnecessarily expensive. Consider supporting Stat() in our directory cache?
2073 stat_time
= buffer
.st_mtime
? buffer
.st_mtime
: buffer
.st_ctime
;
2083 digest
.Update((unsigned char *)&time
, sizeof(time
));
2084 return digest
.Finalize();
2089 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag
&show
,
2090 std::map
<int, std::map
<std::string
, std::string
>> &seasonArt
, const std::vector
<std::string
> &artTypes
, bool useLocal
)
2092 int artLevel
= CServiceBroker::GetSettingsComponent()->GetSettings()->
2093 GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL
);
2094 bool addAll
= artLevel
== CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL
;
2095 bool exactName
= artLevel
== CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC
;
2098 // find the maximum number of seasons we have local thumbs for
2100 CFileItemList items
;
2101 std::string extensions
= CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
2102 if (!show
.m_strPath
.empty())
2104 CDirectory::GetDirectory(show
.m_strPath
, items
, extensions
,
2105 DIR_FLAG_NO_FILE_DIRS
| DIR_FLAG_READ_CACHE
|
2106 DIR_FLAG_NO_FILE_INFO
);
2108 extensions
.erase(std::remove(extensions
.begin(), extensions
.end(), '.'), extensions
.end());
2110 if (items
.Size() && reg
.RegComp("season([0-9]+)(-[a-z0-9]+)?\\.(" + extensions
+ ")"))
2112 for (const auto& item
: items
)
2114 std::string name
= URIUtils::GetFileName(item
->GetPath());
2115 if (reg
.RegFind(name
) > -1)
2117 int season
= atoi(reg
.GetMatch(1).c_str());
2118 if (season
> maxSeasons
)
2119 maxSeasons
= season
;
2123 for (int season
= -1; season
<= maxSeasons
; season
++)
2125 // skip if we already have some art
2126 std::map
<int, std::map
<std::string
, std::string
>>::const_iterator it
= seasonArt
.find(season
);
2127 if (it
!= seasonArt
.end() && !it
->second
.empty())
2130 std::map
<std::string
, std::string
> art
;
2131 std::string basePath
;
2133 basePath
= "season-all";
2134 else if (season
== 0)
2135 basePath
= "season-specials";
2137 basePath
= StringUtils::Format("season{:02}", season
);
2139 AddLocalItemArtwork(art
, artTypes
,
2140 URIUtils::AddFileToFolder(show
.m_strPath
, basePath
),
2143 seasonArt
[season
] = art
;
2147 for (const auto& url
: show
.m_strPictureURL
.GetUrls())
2149 if (url
.m_type
!= CScraperUrl::UrlType::Season
)
2151 std::string aspect
= url
.m_aspect
;
2154 std::map
<std::string
, std::string
>& art
= seasonArt
[url
.m_season
];
2155 if ((addAll
|| CVideoThumbLoader::IsArtTypeInWhitelist(aspect
, artTypes
, exactName
)) &&
2156 art
.find(aspect
) == art
.end())
2158 std::string image
= CScraperUrl::GetThumbUrl(url
);
2160 art
.insert(std::make_pair(aspect
, image
));
2165 void CVideoInfoScanner::FetchActorThumbs(std::vector
<SActorInfo
>& actors
, const std::string
& strPath
)
2167 CFileItemList items
;
2168 // don't try to fetch anything local with plugin source
2169 if (!URIUtils::IsPlugin(strPath
))
2171 std::string actorsDir
= URIUtils::AddFileToFolder(strPath
, ".actors");
2172 if (CDirectory::Exists(actorsDir
))
2173 CDirectory::GetDirectory(actorsDir
, items
, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS
|
2174 DIR_FLAG_NO_FILE_INFO
);
2176 for (std::vector
<SActorInfo
>::iterator i
= actors
.begin(); i
!= actors
.end(); ++i
)
2178 if (i
->thumb
.empty())
2180 std::string thumbFile
= i
->strName
;
2181 StringUtils::Replace(thumbFile
, ' ', '_');
2182 for (int j
= 0; j
< items
.Size(); j
++)
2184 std::string compare
= URIUtils::GetFileName(items
[j
]->GetPath());
2185 URIUtils::RemoveExtension(compare
);
2186 if (!items
[j
]->m_bIsFolder
&& compare
== thumbFile
)
2188 i
->thumb
= items
[j
]->GetPath();
2192 if (i
->thumb
.empty() && !i
->thumbUrl
.GetFirstUrlByType().m_url
.empty())
2193 i
->thumb
= CScraperUrl::GetThumbUrl(i
->thumbUrl
.GetFirstUrlByType());
2194 if (!i
->thumb
.empty())
2195 CServiceBroker::GetTextureCache()->BackgroundCacheImage(i
->thumb
);
2200 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress
* pDialog
)
2202 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoScannerIgnoreErrors
)
2207 HELPERS::ShowOKDialogText(CVariant
{20448}, CVariant
{20449});
2210 return HELPERS::ShowYesNoDialogText(CVariant
{20448}, CVariant
{20450}) ==
2211 DialogResponse::CHOICE_YES
;
2214 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress
* progress
, int heading
, const std::string
&line1
)
2218 progress
->SetHeading(CVariant
{heading
});
2219 progress
->SetLine(0, CVariant
{line1
});
2220 progress
->SetLine(2, CVariant
{""});
2221 progress
->Progress();
2222 return progress
->IsCanceled();
2227 int CVideoInfoScanner::FindVideo(const std::string
&title
, int year
, const ScraperPtr
&scraper
, CScraperUrl
&url
, CGUIDialogProgress
*progress
)
2229 MOVIELIST movielist
;
2230 CVideoInfoDownloader
imdb(scraper
);
2231 int returncode
= imdb
.FindMovie(title
, year
, movielist
, progress
);
2232 if (returncode
< 0 || (returncode
== 0 && (m_bStop
|| !DownloadFailed(progress
))))
2233 { // scraper reported an error, or we had an error and user wants to cancel the scan
2235 return -1; // cancelled
2237 if (returncode
> 0 && movielist
.size())
2240 return 1; // found a movie
2242 return 0; // didn't find anything