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