2 * Copyright (C) 2005-2013 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
21 #include "threads/SystemClock.h"
23 #include "VideoInfoScanner.h"
24 #include "addons/AddonManager.h"
25 #include "filesystem/DirectoryCache.h"
28 #include "utils/RegExp.h"
29 #include "utils/md5.h"
30 #include "filesystem/StackDirectory.h"
31 #include "VideoInfoDownloader.h"
32 #include "GUIInfoManager.h"
33 #include "filesystem/File.h"
34 #include "dialogs/GUIDialogExtendedProgressBar.h"
35 #include "dialogs/GUIDialogProgress.h"
36 #include "dialogs/GUIDialogYesNo.h"
37 #include "dialogs/GUIDialogOK.h"
38 #include "interfaces/AnnouncementManager.h"
39 #include "settings/AdvancedSettings.h"
40 #include "settings/Settings.h"
41 #include "utils/StringUtils.h"
42 #include "guilib/LocalizeStrings.h"
43 #include "guilib/GUIWindowManager.h"
44 #include "utils/TimeUtils.h"
45 #include "utils/log.h"
46 #include "utils/URIUtils.h"
47 #include "utils/Variant.h"
48 #include "video/VideoThumbLoader.h"
49 #include "TextureCache.h"
50 #include "GUIUserMessages.h"
54 using namespace XFILE;
55 using namespace ADDON;
60 CVideoInfoScanner::CVideoInfoScanner() : CThread("VideoInfoScanner")
65 m_bCanInterrupt = false;
72 CVideoInfoScanner::~CVideoInfoScanner()
76 void CVideoInfoScanner::Process()
80 unsigned int tick = XbmcThreads::SystemClockMillis();
84 if (m_showDialog && !CSettings::Get().GetBool("videolibrary.backgroundupdate"))
86 CGUIDialogExtendedProgressBar* dialog =
87 (CGUIDialogExtendedProgressBar*)g_windowManager.GetWindow(WINDOW_DIALOG_EXT_PROGRESS);
89 m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
92 m_bCanInterrupt = true;
94 CLog::Log(LOGNOTICE, "VideoInfoScanner: Starting scan ..");
95 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanStarted");
97 // Reset progress vars
101 SetPriority(GetMinPriority());
103 // Database operations should not be canceled
104 // using Interupt() while scanning as it could
105 // result in unexpected behaviour.
106 m_bCanInterrupt = false;
108 bool bCancelled = false;
109 while (!bCancelled && m_pathsToScan.size())
112 * A copy of the directory path is used because the path supplied is
113 * immediately removed from the m_pathsToScan set in DoScan(). If the
114 * reference points to the entry in the set a null reference error
117 CStdString directory = *m_pathsToScan.begin();
118 if (!CDirectory::Exists(directory))
121 * Note that this will skip clean (if m_bClean is enabled) if the directory really
122 * doesn't exist rather than a NAS being switched off. A manual clean from settings
123 * will still pick up and remove it though.
125 CLog::Log(LOGWARNING, "%s directory '%s' does not exist - skipping scan%s.", __FUNCTION__, directory.c_str(), m_bClean ? " and clean" : "");
126 m_pathsToScan.erase(m_pathsToScan.begin());
128 else if (!DoScan(directory))
135 CleanDatabase(m_handle,&m_pathsToClean, false);
139 m_handle->SetTitle(g_localizeStrings.Get(331));
140 m_database.Compress(false);
146 tick = XbmcThreads::SystemClockMillis() - tick;
147 CLog::Log(LOGNOTICE, "VideoInfoScanner: Finished scan. Scanning for video info took %s", StringUtils::SecondsToTimeString(tick / 1000).c_str());
151 CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
155 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanFinished");
157 // we need to clear the videodb cache and update any active lists
158 CUtil::DeleteVideoDatabaseDirectoryCache();
159 CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0);
160 g_windowManager.SendThreadMessage(msg);
163 m_handle->MarkFinished();
167 void CVideoInfoScanner::Start(const CStdString& strDirectory, bool scanAll)
169 m_strStartDir = strDirectory;
171 m_pathsToScan.clear();
172 m_pathsToClean.clear();
174 if (strDirectory.empty())
175 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
178 m_database.GetPaths(m_pathsToScan);
183 m_pathsToScan.insert(strDirectory);
185 m_bClean = g_advancedSettings.m_bVideoLibraryCleanOnUpdate;
192 bool CVideoInfoScanner::IsScanning()
197 void CVideoInfoScanner::Stop()
200 m_database.Interupt();
205 void CVideoInfoScanner::CleanDatabase(CGUIDialogProgressBarHandle* handle /*= NULL */, const set<int>* paths /*= NULL */, bool showProgress /*= true */)
209 m_database.CleanDatabase(handle, paths, showProgress);
214 static void OnDirectoryScanned(const CStdString& strDirectory)
216 CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
217 msg.SetStringParam(strDirectory);
218 g_windowManager.SendThreadMessage(msg);
221 bool CVideoInfoScanner::DoScan(const CStdString& strDirectory)
225 m_handle->SetText(g_localizeStrings.Get(20415));
229 * Remove this path from the list we're processing. This must be done prior to
230 * the check for file or folder exclusion to prevent an infinite while loop
233 set<CStdString>::iterator it = m_pathsToScan.find(strDirectory);
234 if (it != m_pathsToScan.end())
235 m_pathsToScan.erase(it);
239 bool foundDirectly = false;
242 SScanSettings settings;
243 ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
244 CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
246 // exclude folders that match our exclude regexps
247 CStdStringArray regexps = content == CONTENT_TVSHOWS ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
248 : g_advancedSettings.m_moviesExcludeFromScanRegExps;
250 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
253 bool ignoreFolder = !m_scanAll && settings.noupdate;
254 if (content == CONTENT_NONE || ignoreFolder)
257 CStdString hash, dbHash;
258 if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
262 int str = content == CONTENT_MOVIES ? 20317:20318;
263 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name().c_str()));
266 CStdString fastHash = GetFastHash(strDirectory);
267 if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && fastHash == dbHash)
268 { // fast hashes match - no need to process anything
269 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change (fasthash)", CURL::GetRedacted(strDirectory).c_str());
274 { // need to fetch the folder
275 CDirectory::GetDirectory(strDirectory, items, g_advancedSettings.m_videoExtensions);
278 GetPathHash(items, hash);
279 if (hash != dbHash && !hash.empty())
282 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '%s' as not in the database", CURL::GetRedacted(strDirectory).c_str());
284 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '%s' due to change (%s != %s)", CURL::GetRedacted(strDirectory).c_str(), dbHash.c_str(), hash.c_str());
287 { // they're the same or the hash is empty (dir empty/dir not retrievable)
288 if (hash.empty() && !dbHash.empty())
290 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' as it's empty or doesn't exist - adding to clean list", CURL::GetRedacted(strDirectory).c_str());
291 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
294 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change", CURL::GetRedacted(strDirectory).c_str());
297 OnDirectoryScanned(strDirectory);
299 // update the hash to a fast hash if needed
300 if (CanFastHash(items) && !fastHash.empty())
304 else if (content == CONTENT_TVSHOWS)
307 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name().c_str()));
309 if (foundDirectly && !settings.parent_name_root)
311 CDirectory::GetDirectory(strDirectory, items, g_advancedSettings.m_videoExtensions);
312 items.SetPath(strDirectory);
313 GetPathHash(items, hash);
315 if (!m_database.GetPathHash(strDirectory, dbHash) || dbHash != hash)
317 m_database.SetPathHash(strDirectory, hash);
325 CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
326 item->SetPath(strDirectory);
327 item->m_bIsFolder = true;
329 items.SetPath(URIUtils::GetParentPath(item->GetPath()));
335 if (RetrieveVideoInfo(items, settings.parent_name_root, content))
337 if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
339 m_database.SetPathHash(strDirectory, hash);
340 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
341 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir %s", CURL::GetRedacted(strDirectory).c_str());
346 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
347 CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir %s", CURL::GetRedacted(strDirectory).c_str());
350 else if (hash != dbHash && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
351 { // update the hash either way - we may have changed the hash to a fast version
352 m_database.SetPathHash(strDirectory, hash);
356 OnDirectoryScanned(strDirectory);
358 for (int i = 0; i < items.Size(); ++i)
360 CFileItemPtr pItem = items[i];
365 // if we have a directory item (non-playlist) we then recurse into that folder
366 // do not recurse for tv shows - we have already looked recursively for episodes
367 if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList() && settings.recurse > 0 && content != CONTENT_TVSHOWS)
369 if (!DoScan(pItem->GetPath()))
378 bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
382 if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
384 pDlgProgress->ShowProgressBar(true);
385 pDlgProgress->SetPercentage(0);
388 pDlgProgress->ShowProgressBar(false);
390 pDlgProgress->Progress();
395 bool FoundSomeInfo = false;
396 vector<int> seenPaths;
397 for (int i = 0; i < (int)items.Size(); ++i)
400 CFileItemPtr pItem = items[i];
402 // we do this since we may have a override per dir
403 ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath());
407 // Discard all exclude files defined by regExExclude
408 if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
409 : g_advancedSettings.m_moviesExcludeFromScanRegExps))
412 if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
415 m_handle->SetPercentage(i*100.f/items.Size());
418 // clear our scraper cache
421 INFO_RET ret = INFO_CANCELLED;
422 if (info2->Content() == CONTENT_TVSHOWS)
423 ret = RetrieveInfoForTvShow(pItem.get(), bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress);
424 else if (info2->Content() == CONTENT_MOVIES)
425 ret = RetrieveInfoForMovie(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
426 else if (info2->Content() == CONTENT_MUSICVIDEOS)
427 ret = RetrieveInfoForMusicVideo(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
430 CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type %d (%s)", info2->Content(), CURL::GetRedacted(pItem->GetPath()).c_str());
431 FoundSomeInfo = false;
434 if (ret == INFO_CANCELLED || ret == INFO_ERROR)
436 FoundSomeInfo = false;
439 if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
440 FoundSomeInfo = true;
441 else if (ret == INFO_NOT_FOUND)
443 CLog::Log(LOGWARNING, "No information found for item '%s', it won't be added to the library.", CURL::GetRedacted(pItem->GetPath()).c_str());
448 // Keep track of directories we've seen
449 if (pItem->m_bIsFolder)
450 seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
453 if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
455 vector< pair<int,string> > libPaths;
456 m_database.GetSubPaths(items.GetPath(), libPaths);
457 for (vector< pair<int,string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
459 if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
460 m_pathsToClean.insert(i->first);
464 pDlgProgress->ShowProgressBar(false);
466 g_infoManager.ResetLibraryBools();
468 return FoundSomeInfo;
471 INFO_RET CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
474 if (pItem->m_bIsFolder)
475 idTvShow = m_database.GetTvShowId(pItem->GetPath());
478 CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
479 idTvShow = m_database.GetTvShowId(strPath);
481 if (idTvShow > -1 && (fetchEpisodes || !pItem->m_bIsFolder))
483 INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
484 if (ret == INFO_ADDED)
485 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
489 if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361, pItem->GetLabel()))
490 return INFO_CANCELLED;
493 m_handle->SetText(pItem->GetMovieName(bDirNames));
495 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
499 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
500 if (result == CNfoFile::FULL_NFO)
502 pItem->GetVideoInfoTag()->Reset();
503 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
505 long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
510 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
511 if (ret == INFO_ADDED)
512 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
517 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
524 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
525 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
528 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
530 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
535 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
536 if (ret == INFO_ADDED)
537 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
542 INFO_RET CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
544 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
545 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
546 return INFO_NOT_NEEDED;
548 if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
549 return INFO_CANCELLED;
551 if (m_database.HasMovieInfo(pItem->GetPath()))
552 return INFO_HAVE_ALREADY;
555 m_handle->SetText(pItem->GetMovieName(bDirNames));
557 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
561 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
562 if (result == CNfoFile::FULL_NFO)
564 pItem->GetVideoInfoTag()->Reset();
565 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
567 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
571 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
578 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
579 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
581 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
583 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
587 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
588 return INFO_NOT_FOUND;
591 INFO_RET CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
593 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
594 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
595 return INFO_NOT_NEEDED;
597 if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
598 return INFO_CANCELLED;
600 if (m_database.HasMusicVideoInfo(pItem->GetPath()))
601 return INFO_HAVE_ALREADY;
604 m_handle->SetText(pItem->GetMovieName(bDirNames));
606 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
610 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
611 if (result == CNfoFile::FULL_NFO)
613 pItem->GetVideoInfoTag()->Reset();
614 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
616 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
620 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
627 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
628 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
630 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
632 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
636 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
637 return INFO_NOT_FOUND;
640 INFO_RET CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item, long showID, const ADDON::ScraperPtr &scraper, bool useLocal, CGUIDialogProgress *progress)
642 // enumerate episodes
644 EnumerateSeriesFolder(item, files);
645 if (files.size() == 0) // no update or no files
646 return INFO_NOT_NEEDED;
648 if (m_bStop || (progress && progress->IsCanceled()))
649 return INFO_CANCELLED;
651 CVideoInfoTag showInfo;
652 m_database.GetTvShowInfo("", showInfo, showID);
653 return OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
656 void CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
660 if (item->m_bIsFolder)
662 CUtil::GetRecursiveListing(item->GetPath(), items, g_advancedSettings.m_videoExtensions, true);
663 CStdString hash, dbHash;
664 int numFilesInFolder = GetPathHash(items, hash);
666 if (m_database.GetPathHash(item->GetPath(), dbHash) && dbHash == hash)
668 m_currentItem += numFilesInFolder;
670 // update our dialog with our progress
674 m_handle->SetPercentage(m_currentItem*100.f/m_itemCount);
676 OnDirectoryScanned(item->GetPath());
680 m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
681 m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
682 item->SetProperty("hash", hash);
686 CFileItemPtr newItem(new CFileItem(*item));
691 stack down any dvd folders
692 need to sort using the full path since this is a collapsed recursive listing of all subdirs
693 video_ts.ifo files should sort at the top of a dvd folder in ascending order
695 /foo/bar/video_ts.ifo
700 // since we're doing this now anyway, should other items be stacked?
701 items.Sort(SortByPath, SortOrderAscending);
703 while (x < items.Size())
705 if (items[x]->m_bIsFolder)
709 CStdString strPathX, strFileX;
710 URIUtils::Split(items[x]->GetPath(), strPathX, strFileX);
711 //CLog::Log(LOGDEBUG,"%i:%s:%s", x, strPathX.c_str(), strFileX.c_str());
714 if (strFileX.Equals("VIDEO_TS.IFO"))
716 while (y < items.Size())
718 CStdString strPathY, strFileY;
719 URIUtils::Split(items[y]->GetPath(), strPathY, strFileY);
720 //CLog::Log(LOGDEBUG," %i:%s:%s", y, strPathY.c_str(), strFileY.c_str());
722 if (strPathY.Equals(strPathX))
724 remove everything sorted below the video_ts.ifo file in the same path.
725 understandbly this wont stack correctly if there are other files in the the dvd folder.
726 this should be unlikely and thus is being ignored for now but we can monitor the
727 where the path changes and potentially remove the items above the video_ts.ifo file.
738 CStdStringArray regexps = g_advancedSettings.m_tvshowExcludeFromScanRegExps;
740 for (int i=0;i<items.Size();++i)
742 if (items[i]->m_bIsFolder)
744 CStdString strPath = URIUtils::GetDirectory(items[i]->GetPath());
745 URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
747 if (URIUtils::GetFileName(strPath).Equals("sample"))
750 // Discard all exclude files defined by regExExcludes
751 if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
755 * Check if the media source has already set the season and episode or original air date in
756 * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
757 * any false positive matches.
759 if (ProcessItemByVideoInfoTag(items[i].get(), episodeList))
762 if (!EnumerateEpisodeItem(items[i].get(), episodeList))
763 CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file %s", CURL::GetRedacted(CURL::Decode(items[i]->GetPath())).c_str());
767 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
769 if (!item->HasVideoInfoTag())
772 const CVideoInfoTag* tag = item->GetVideoInfoTag();
774 * First check the season and episode number. This takes precedence over the original air
775 * date and episode title. Must be a valid season and episode number combination.
777 if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
780 episode.strPath = item->GetPath();
781 episode.iSeason = tag->m_iSeason;
782 episode.iEpisode = tag->m_iEpisode;
783 episode.isFolder = false;
784 episodeList.push_back(episode);
785 CLog::Log(LOGDEBUG, "%s - found match for: %s. Season %d, Episode %d", __FUNCTION__,
786 episode.strPath.c_str(), episode.iSeason, episode.iEpisode);
791 * Next preference is the first aired date. If it exists use that for matching the TV Show
792 * information. Also set the title in case there are multiple matches for the first aired date.
794 if (tag->m_firstAired.IsValid())
797 episode.strPath = item->GetPath();
798 episode.strTitle = tag->m_strTitle;
799 episode.isFolder = false;
801 * Set season and episode to -1 to indicate to use the aired date.
803 episode.iSeason = -1;
804 episode.iEpisode = -1;
806 * The first aired date string must be parseable.
808 episode.cDate = item->GetVideoInfoTag()->m_firstAired;
809 episodeList.push_back(episode);
810 CLog::Log(LOGDEBUG, "%s - found match for: '%s', firstAired: '%s' = '%s', title: '%s'",
811 __FUNCTION__, episode.strPath.c_str(), tag->m_firstAired.GetAsDBDateTime().c_str(),
812 episode.cDate.GetAsLocalizedDate().c_str(), episode.strTitle.c_str());
817 * Next preference is the episode title. If it exists use that for matching the TV Show
820 if (!tag->m_strTitle.empty())
823 episode.strPath = item->GetPath();
824 episode.strTitle = tag->m_strTitle;
825 episode.isFolder = false;
827 * Set season and episode to -1 to indicate to use the title.
829 episode.iSeason = -1;
830 episode.iEpisode = -1;
831 episodeList.push_back(episode);
832 CLog::Log(LOGDEBUG,"%s - found match for: '%s', title: '%s'", __FUNCTION__,
833 episode.strPath.c_str(), episode.strTitle.c_str());
838 * There is no further episode information available if both the season and episode number have
839 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
840 * to the episode list.
842 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
844 CLog::Log(LOGDEBUG,"%s - found exclusion match for: %s. Both Season and Episode are 0. Item will be ignored for scanning.",
845 __FUNCTION__, item->GetPath().c_str());
852 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
854 SETTINGS_TVSHOWLIST expression = g_advancedSettings.m_tvshowEnumRegExps;
856 CStdString strLabel=item->GetPath();
857 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
858 strLabel = CURL::Decode(strLabel);
860 for (unsigned int i=0;i<expression.size();++i)
862 CRegExp reg(true, CRegExp::autoUtf8);
863 if (!reg.RegComp(expression[i].regexp))
866 int regexppos, regexp2pos;
867 //CLog::Log(LOGDEBUG,"running expression %s on %s",expression[i].regexp.c_str(),strLabel.c_str());
868 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
872 episode.strPath = item->GetPath();
873 episode.iSeason = -1;
874 episode.iEpisode = -1;
875 episode.cDate.SetValid(false);
876 episode.isFolder = false;
878 bool byDate = expression[i].byDate ? true : false;
879 int defaultSeason = expression[i].defaultSeason;
883 if (!GetAirDateFromRegExp(reg, episode))
886 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match %s (%s) [%s]", strLabel.c_str(),
887 episode.cDate.GetAsLocalizedDate().c_str(), expression[i].regexp.c_str());
891 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
894 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match %s (s%ie%i) [%s]", strLabel.c_str(),
895 episode.iSeason, episode.iEpisode, expression[i].regexp.c_str());
898 // Grab the remainder from first regexp run
899 // as second run might modify or empty it.
900 std::string remainder(reg.GetMatch(3));
903 * Check if the files base path is a dedicated folder that contains
904 * only this single episode. If season and episode match with the
905 * actual media file, we set episode.isFolder to true.
907 CStdString strBasePath = item->GetBaseMoviePath(true);
908 URIUtils::RemoveSlashAtEnd(strBasePath);
909 strBasePath = URIUtils::GetFileName(strBasePath);
911 if (reg.RegFind(strBasePath.c_str()) > -1)
916 GetAirDateFromRegExp(reg, parent);
917 if (episode.cDate == parent.cDate)
918 episode.isFolder = true;
922 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
923 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
924 episode.isFolder = true;
928 // add what we found by now
929 episodeList.push_back(episode);
931 CRegExp reg2(true, CRegExp::autoUtf8);
932 // check the remainder of the string for any further episodes.
933 if (!byDate && reg2.RegComp(g_advancedSettings.m_tvshowMultiPartEnumRegExp))
937 // we want "long circuit" OR below so that both offsets are evaluated
938 while (((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) | ((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
940 if (((regexppos <= regexp2pos) && regexppos != -1) ||
941 (regexppos >= 0 && regexp2pos == -1))
943 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
945 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season %u, multipart episode %u [%s]",
946 episode.iSeason, episode.iEpisode,
947 g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
949 episodeList.push_back(episode);
950 remainder = reg.GetMatch(3);
953 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
954 (regexp2pos >= 0 && regexppos == -1))
956 episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
957 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode %u [%s]",
958 episode.iEpisode, g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
959 episodeList.push_back(episode);
960 offset += regexp2pos + reg2.GetFindLen();
969 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp ®, EPISODE &episodeInfo, int defaultSeason)
971 std::string season(reg.GetMatch(1));
972 std::string episode(reg.GetMatch(2));
974 if (!season.empty() || !episode.empty())
977 if (season.empty() && !episode.empty())
978 { // no season specified -> assume defaultSeason
979 episodeInfo.iSeason = defaultSeason;
980 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
981 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
983 else if (!season.empty() && episode.empty())
984 { // no episode specification -> assume defaultSeason
985 episodeInfo.iSeason = defaultSeason;
986 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
987 episodeInfo.iEpisode = atoi(season.c_str());
990 { // season and episode specified
991 episodeInfo.iSeason = atoi(season.c_str());
992 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
996 if (isalpha(*endptr))
997 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
998 else if (*endptr == '.')
999 episodeInfo.iSubepisode = atoi(endptr+1);
1006 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp ®, EPISODE &episodeInfo)
1008 std::string param1(reg.GetMatch(1));
1009 std::string param2(reg.GetMatch(2));
1010 std::string param3(reg.GetMatch(3));
1012 if (!param1.empty() && !param2.empty() && !param3.empty())
1014 // regular expression by date
1015 int len1 = param1.size();
1016 int len2 = param2.size();
1017 int len3 = param3.size();
1019 if (len1==4 && len2==2 && len3==2)
1021 // yyyy mm dd format
1022 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1024 else if (len1==2 && len2==2 && len3==4)
1026 // mm dd yyyy format
1027 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1030 return episodeInfo.cDate.IsValid();
1033 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1035 // ensure our database is open (this can get called via other classes)
1036 if (!m_database.Open())
1040 GetArtwork(pItem, content, videoFolder, useLocal, showInfo ? showInfo->m_strPath : "");
1042 // ensure the art map isn't completely empty by specifying an empty thumb
1043 map<string, string> art = pItem->GetArt();
1047 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1048 if (movieDetails.m_basePath.empty())
1049 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1050 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1052 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1054 if (pItem->m_bIsFolder)
1055 movieDetails.m_strPath = pItem->GetPath();
1057 CStdString strTitle(movieDetails.m_strTitle);
1059 if (showInfo && content == CONTENT_TVSHOWS)
1061 strTitle = StringUtils::Format("%s - %ix%i - %s", showInfo->m_strTitle.c_str(), movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle.c_str());
1064 std::string redactPath(CURL::GetRedacted(CURL::Decode(pItem->GetPath())));
1066 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to %s:%s", TranslateContent(content).c_str(), redactPath.c_str());
1069 if (content == CONTENT_MOVIES)
1071 // find local trailer first
1072 CStdString strTrailer = pItem->FindTrailer();
1073 if (!strTrailer.empty())
1074 movieDetails.m_strTrailer = strTrailer;
1076 lResult = m_database.SetDetailsForMovie(pItem->GetPath(), movieDetails, art);
1077 movieDetails.m_iDbId = lResult;
1078 movieDetails.m_type = "movie";
1080 // setup links to shows if the linked shows are in the db
1081 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1083 CFileItemList items;
1084 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1086 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1088 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie %s to show %s", movieDetails.m_strTitle.c_str(), movieDetails.m_showLink[i].c_str());
1091 else if (content == CONTENT_TVSHOWS)
1093 if (pItem->m_bIsFolder)
1095 map<int, map<string, string> > seasonArt;
1097 { // get and cache season thumbs
1098 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes("season"), useLocal);
1099 for (map<int, map<string, string> >::iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
1100 for (map<string, string>::iterator j = i->second.begin(); j != i->second.end(); ++j)
1101 CTextureCache::Get().BackgroundCacheImage(j->second);
1103 lResult = m_database.SetDetailsForTvShow(pItem->GetPath(), movieDetails, art, seasonArt);
1104 movieDetails.m_iDbId = lResult;
1105 movieDetails.m_type = "tvshow";
1109 // we add episode then set details, as otherwise set details will delete the
1110 // episode then add, which breaks multi-episode files.
1111 int idShow = showInfo ? showInfo->m_iDbId : -1;
1112 int idEpisode = m_database.AddEpisode(idShow, pItem->GetPath());
1113 lResult = m_database.SetDetailsForEpisode(pItem->GetPath(), movieDetails, art, idShow, idEpisode);
1114 movieDetails.m_iDbId = lResult;
1115 movieDetails.m_type = "episode";
1116 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1117 if (movieDetails.m_fEpBookmark > 0)
1119 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1121 bookmark.timeInSeconds = movieDetails.m_fEpBookmark;
1122 bookmark.seasonNumber = movieDetails.m_iSeason;
1123 bookmark.episodeNumber = movieDetails.m_iEpisode;
1124 m_database.AddBookMarkForEpisode(movieDetails, bookmark);
1128 else if (content == CONTENT_MUSICVIDEOS)
1130 lResult = m_database.SetDetailsForMusicVideo(pItem->GetPath(), movieDetails, art);
1131 movieDetails.m_iDbId = lResult;
1132 movieDetails.m_type = "musicvideo";
1135 if (g_advancedSettings.m_bVideoLibraryImportWatchedState || libraryImport)
1136 m_database.SetPlayCount(*pItem, movieDetails.m_playCount, movieDetails.m_lastPlayed);
1138 if ((g_advancedSettings.m_bVideoLibraryImportResumePoint || libraryImport) &&
1139 movieDetails.m_resumePoint.IsSet())
1140 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.m_resumePoint, CBookmark::RESUME);
1144 CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
1145 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", itemCopy);
1149 string ContentToMediaType(CONTENT_TYPE content, bool folder)
1153 case CONTENT_MOVIES:
1155 case CONTENT_MUSICVIDEOS:
1156 return "musicvideo";
1157 case CONTENT_TVSHOWS:
1158 return folder ? "tvshow" : "episode";
1164 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1166 std::string type = "thumb";
1167 if (width*5 < height*4)
1169 else if (width*1 > height*4)
1174 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1176 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1177 movieDetails.m_fanart.Unpack();
1178 movieDetails.m_strPictureURL.Parse();
1180 CGUIListItem::ArtMap art = pItem->GetArt();
1182 // get and cache thumb images
1183 vector<string> artTypes = CVideoThumbLoader::GetArtTypes(ContentToMediaType(content, pItem->m_bIsFolder));
1184 vector<string>::iterator i = find(artTypes.begin(), artTypes.end(), "fanart");
1185 if (i != artTypes.end())
1186 artTypes.erase(i); // fanart is handled below
1187 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end() &&
1188 art.find("thumb") == art.end();
1192 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1194 if (art.find(*i) == art.end())
1196 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, *i, bApplyToDir);
1198 art.insert(make_pair(*i, image));
1201 // find and classify the local thumb (backcompat) if available
1204 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, "thumb", bApplyToDir);
1206 { // cache the image and determine sizing
1207 CTextureDetails details;
1208 if (CTextureCache::Get().CacheImage(image, details))
1210 std::string type = GetArtTypeFromSize(details.width, details.height);
1211 if (art.find(type) == art.end())
1212 art.insert(make_pair(type, image));
1219 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1221 if (art.find(*i) == art.end())
1223 std::string image = GetImage(pItem, false, bApplyToDir, *i);
1225 art.insert(make_pair(*i, image));
1229 // use the first piece of online art as the first art type if no thumb type is available yet
1230 if (art.empty() && lookForThumb)
1232 std::string image = GetImage(pItem, false, bApplyToDir, "thumb");
1234 art.insert(make_pair(artTypes.front(), image));
1237 // get & save fanart image (treated separately due to it being stored in m_fanart)
1238 bool isEpisode = (content == CONTENT_TVSHOWS && !pItem->m_bIsFolder);
1239 if (!isEpisode && art.find("fanart") == art.end())
1241 string fanart = GetFanart(pItem, useLocal);
1242 if (!fanart.empty())
1243 art.insert(make_pair("fanart", fanart));
1246 for (CGUIListItem::ArtMap::const_iterator i = art.begin(); i != art.end(); ++i)
1247 CTextureCache::Get().BackgroundCacheImage(i->second);
1251 // parent folder to apply the thumb to and to search for local actor thumbs
1252 CStdString parentDir = GetParentDir(*pItem);
1253 if (CSettings::Get().GetBool("videolibrary.actorthumbs"))
1254 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1256 ApplyThumbToFolder(parentDir, art["thumb"]);
1259 std::string CVideoInfoScanner::GetImage(CFileItem *pItem, bool useLocal, bool bApplyToDir, const std::string &type)
1263 thumb = CVideoThumbLoader::GetLocalArt(*pItem, type, bApplyToDir);
1267 thumb = CScraperUrl::GetThumbURL(pItem->GetVideoInfoTag()->m_strPictureURL.GetFirstThumb(type));
1270 if (thumb.find("http://") == string::npos &&
1271 thumb.find("/") == string::npos &&
1272 thumb.find("\\") == string::npos)
1274 CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
1275 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1282 std::string CVideoInfoScanner::GetFanart(CFileItem *pItem, bool useLocal)
1286 std::string fanart = pItem->GetArt("fanart");
1287 if (fanart.empty() && useLocal)
1288 fanart = pItem->FindLocalArt("fanart.jpg", true);
1290 fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1294 INFO_RET CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress /* = NULL */)
1298 pDlgProgress->SetLine(1, showInfo.m_strTitle);
1299 pDlgProgress->SetLine(2, 20361);
1300 pDlgProgress->SetPercentage(0);
1301 pDlgProgress->ShowProgressBar(true);
1302 pDlgProgress->Progress();
1305 EPISODELIST episodes;
1306 bool hasEpisodeGuide = false;
1308 int iMax = files.size();
1310 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1312 m_nfoReader.Close();
1315 pDlgProgress->SetLine(2, 20361);
1316 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1317 pDlgProgress->Progress();
1320 m_handle->SetPercentage(100.f*iCurr++/iMax);
1322 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1323 return INFO_CANCELLED;
1325 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1328 m_handle->SetText(g_localizeStrings.Get(20415));
1333 item.SetPath(file->strPath);
1335 // handle .nfo files
1336 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1338 ScraperPtr info(scraper);
1339 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1341 result = CheckForNFOFile(&item, false, info,scrUrl);
1342 if (result == CNfoFile::FULL_NFO)
1344 m_nfoReader.GetDetails(*item.GetVideoInfoTag());
1345 // override with episode and season number from file if available
1346 if (file->iEpisode > -1)
1348 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1349 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1351 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1356 if (!hasEpisodeGuide)
1358 // fetch episode guide
1359 if (!showInfo.m_strEpisodeGuide.empty())
1362 url.ParseEpisodeGuide(showInfo.m_strEpisodeGuide);
1366 pDlgProgress->SetLine(2, 20354);
1367 pDlgProgress->Progress();
1370 CVideoInfoDownloader imdb(scraper);
1371 if (!imdb.GetEpisodeList(url, episodes))
1372 return INFO_NOT_FOUND;
1374 hasEpisodeGuide = true;
1378 if (episodes.empty())
1380 CLog::Log(LOGERROR, "VideoInfoScanner: Asked to lookup episode %s"
1381 " online, but we have no episode guide. Check your tvshow.nfo and make"
1382 " sure the <episodeguide> tag is in place.", file->strPath.c_str());
1386 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
1387 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
1388 bool bFound = false;
1389 EPISODELIST::iterator guide = episodes.begin();;
1390 EPISODELIST matches;
1392 for (; guide != episodes.end(); ++guide )
1394 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
1401 else if ((file->iSubepisode!=0) && (backupkey==*guide))
1403 matches.push_back(*guide);
1407 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1409 matches.push_back(*guide);
1412 if (!guide->cScraperUrl.strTitle.empty() && StringUtils::EqualsNoCase(guide->cScraperUrl.strTitle, file->strTitle))
1422 * If there is only one match or there are matches but no title to compare with to help
1423 * identify the best match, then pick the first match as the best possible candidate.
1425 * Otherwise, use the title to further refine the best match.
1427 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
1429 guide = matches.begin();
1432 else if (!file->strTitle.empty())
1434 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
1436 EPISODELIST *candidates;
1437 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1439 minscore = 0.8; // 80% should ensure a good match.
1440 candidates = &episodes;
1442 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1443 candidates = &matches;
1445 CStdStringArray titles;
1446 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
1448 StringUtils::ToLower(guide->cScraperUrl.strTitle);
1449 titles.push_back(guide->cScraperUrl.strTitle);
1453 std::string loweredTitle(file->strTitle);
1454 StringUtils::ToLower(loweredTitle);
1455 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
1456 if (matchscore >= minscore)
1458 guide = candidates->begin() + index;
1460 CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1461 __FUNCTION__, showInfo.m_strTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1468 CVideoInfoDownloader imdb(scraper);
1470 item.SetPath(file->strPath);
1471 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1472 return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1474 // Only set season/epnum from filename when it is not already set by a scraper
1475 if (item.GetVideoInfoTag()->m_iSeason == -1)
1476 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
1477 if (item.GetVideoInfoTag()->m_iEpisode == -1)
1478 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
1480 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
1485 CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d.%d, airdate: '%s', title: '%s'",
1486 __FUNCTION__, showInfo.m_strTitle.c_str(), file->iSeason, file->iEpisode, file->iSubepisode,
1487 file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1493 CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1496 // Find a matching .nfo file
1497 if (!item->m_bIsFolder)
1499 if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1501 CFileItem item2(*item);
1502 CURL url(item->GetPath());
1503 CStdString strPath = URIUtils::GetDirectory(url.GetHostName());
1504 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1505 return GetnfoFile(&item2, bGrabAny);
1508 // grab the folder path
1509 CStdString strPath = URIUtils::GetDirectory(item->GetPath());
1511 if (bGrabAny && !item->IsStack())
1512 { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1513 nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1514 if (CFile::Exists(nfoFile))
1518 // try looking for .nfo file for a stacked item
1519 if (item->IsStack())
1521 // first try .nfo file matching first file in stack
1522 CStackDirectory dir;
1523 CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1525 item2.SetPath(firstFile);
1526 nfoFile = GetnfoFile(&item2, bGrabAny);
1527 // else try .nfo file matching stacked title
1528 if (nfoFile.empty())
1530 CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1531 item2.SetPath(stackedTitlePath);
1532 nfoFile = GetnfoFile(&item2, bGrabAny);
1537 // already an .nfo file?
1538 if (URIUtils::HasExtension(item->GetPath(), ".nfo"))
1539 nfoFile = item->GetPath();
1540 // no, create .nfo file
1542 nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1545 // test file existence
1546 if (!nfoFile.empty() && !CFile::Exists(nfoFile))
1549 if (nfoFile.empty()) // final attempt - strip off any cd1 folders
1551 URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1553 if (StringUtils::EndsWithNoCase(strPath, "cd1"))
1555 strPath.erase(strPath.size() - 3);
1556 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1557 return GetnfoFile(&item2, bGrabAny);
1561 if (nfoFile.empty() && item->IsOpticalMediaFile())
1563 CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1564 nfoFile = GetnfoFile(&parentDirectory, true);
1567 // folders (or stacked dvds) can take any nfo file if there's a unique one
1568 if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.empty()))
1570 // see if there is a unique nfo file in this folder, and if so, use that
1571 CFileItemList items;
1574 if (item->m_bIsFolder)
1575 strPath = item->GetPath();
1577 strPath = URIUtils::GetDirectory(item->GetPath());
1579 if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1582 for (int i = 0; i < items.Size(); i++)
1584 if (items[i]->IsNFO())
1596 return items[numNFO]->GetPath();
1603 bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1605 CVideoInfoTag movieDetails;
1607 if (m_handle && !url.strTitle.empty())
1608 m_handle->SetText(url.strTitle);
1610 CVideoInfoDownloader imdb(scraper);
1611 bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1616 nfoFile->GetDetails(movieDetails,NULL,true);
1618 if (m_handle && url.strTitle.empty())
1619 m_handle->SetText(movieDetails.m_strTitle);
1623 pDialog->SetLine(1, movieDetails.m_strTitle);
1624 pDialog->Progress();
1627 *pItem->GetVideoInfoTag() = movieDetails;
1630 return false; // no info found, or cancelled
1633 void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1635 // copy icon to folder also;
1636 if (!imdbThumb.empty())
1638 CFileItem folderItem(folder, true);
1639 CThumbLoader loader;
1640 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
1644 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1646 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1647 if (0 == items.Size()) return 0;
1648 XBMC::XBMC_MD5 md5state;
1650 for (int i = 0; i < items.Size(); ++i)
1652 const CFileItemPtr pItem = items[i];
1653 md5state.append(pItem->GetPath());
1654 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1655 FILETIME time = pItem->m_dateTime;
1656 md5state.append((unsigned char *)&time, sizeof(FILETIME));
1657 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1660 md5state.getDigest(hash);
1664 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1666 // TODO: Probably should account for excluded folders here (eg samples), though that then
1667 // introduces possible problems if the user then changes the exclude regexps and
1668 // expects excluded folders that are inside a fast-hashed folder to then be picked
1669 // up. The chances that the user has a folder which contains only excluded folders
1670 // where some of those folders should be scanned recursively is pretty small.
1671 return items.GetFolderCount() == 0;
1674 CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1676 struct __stat64 buffer;
1677 if (XFILE::CFile::Stat(directory, &buffer) == 0)
1679 int64_t time = buffer.st_mtime;
1681 time = buffer.st_ctime;
1683 return StringUtils::Format("fast%"PRId64, time);
1688 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, map<int, map<string, string> > &seasonArt, const vector<string> &artTypes, bool useLocal)
1690 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end();
1692 // find the maximum number of seasons we have thumbs for (local + remote)
1693 int maxSeasons = show.m_strPictureURL.GetMaxSeasonThumb();
1695 CFileItemList items;
1696 CDirectory::GetDirectory(show.m_strPath, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
1698 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z]+)?\\.(tbn|jpg|png)"))
1700 for (int i = 0; i < items.Size(); i++)
1702 CStdString name = URIUtils::GetFileName(items[i]->GetPath());
1703 if (reg.RegFind(name) > -1)
1705 int season = atoi(reg.GetMatch(1).c_str());
1706 if (season > maxSeasons)
1707 maxSeasons = season;
1711 for (int season = -1; season <= maxSeasons; season++)
1713 map<string, string> art;
1718 basePath = "season-all";
1719 else if (season == 0)
1720 basePath = "season-specials";
1722 basePath = StringUtils::Format("season%02i", season);
1723 CFileItem artItem(URIUtils::AddFileToFolder(show.m_strPath, basePath), false);
1725 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1727 std::string image = CVideoThumbLoader::GetLocalArt(artItem, *i, false);
1729 art.insert(make_pair(*i, image));
1731 // find and classify the local thumb (backcompat) if available
1734 std::string image = CVideoThumbLoader::GetLocalArt(artItem, "thumb", false);
1736 { // cache the image and determine sizing
1737 CTextureDetails details;
1738 if (CTextureCache::Get().CacheImage(image, details))
1740 std::string type = GetArtTypeFromSize(details.width, details.height);
1741 if (art.find(type) == art.end())
1742 art.insert(make_pair(type, image));
1749 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1751 if (art.find(*i) == art.end())
1753 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, *i));
1755 art.insert(make_pair(*i, image));
1758 // use the first piece of online art as the first art type if no thumb type is available yet
1759 if (art.empty() && lookForThumb)
1761 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, "thumb"));
1763 art.insert(make_pair(artTypes.front(), image));
1766 seasonArt.insert(make_pair(season, art));
1770 void CVideoInfoScanner::FetchActorThumbs(vector<SActorInfo>& actors, const CStdString& strPath)
1772 CFileItemList items;
1773 CStdString actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
1774 if (CDirectory::Exists(actorsDir))
1775 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
1776 DIR_FLAG_NO_FILE_INFO);
1777 for (vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
1779 if (i->thumb.empty())
1781 CStdString thumbFile = i->strName;
1782 StringUtils::Replace(thumbFile, ' ', '_');
1783 for (int j = 0; j < items.Size(); j++)
1785 CStdString compare = URIUtils::GetFileName(items[j]->GetPath());
1786 URIUtils::RemoveExtension(compare);
1787 if (!items[j]->m_bIsFolder && compare == thumbFile)
1789 i->thumb = items[j]->GetPath();
1793 if (i->thumb.empty() && !i->thumbUrl.GetFirstThumb().m_url.empty())
1794 i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1795 if (!i->thumb.empty())
1796 CTextureCache::Get().BackgroundCacheImage(i->thumb);
1801 CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1803 CStdString strNfoFile;
1804 if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1805 || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1806 strNfoFile = GetnfoFile(pItem, bGrabAny);
1807 if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1808 strNfoFile = URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo");
1810 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1811 if (!strNfoFile.empty() && CFile::Exists(strNfoFile))
1813 if (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder)
1814 result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1816 result = m_nfoReader.Create(strNfoFile,info);
1821 case CNfoFile::COMBINED_NFO:
1824 case CNfoFile::FULL_NFO:
1827 case CNfoFile::URL_NFO:
1830 case CNfoFile::NO_NFO:
1836 if (result != CNfoFile::NO_NFO)
1837 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), CURL::GetRedacted(strNfoFile).c_str());
1838 if (result == CNfoFile::FULL_NFO)
1840 if (info->Content() == CONTENT_TVSHOWS)
1841 info = m_nfoReader.GetScraperInfo();
1843 else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1845 scrUrl = m_nfoReader.ScraperUrl();
1846 info = m_nfoReader.GetScraperInfo();
1848 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1849 scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1851 if (result == CNfoFile::COMBINED_NFO)
1852 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1856 CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", CURL::GetRedacted(pItem->GetPath()).c_str());
1861 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1863 if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1868 CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1871 return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1874 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1878 progress->SetHeading(heading);
1879 progress->SetLine(0, line1);
1880 progress->SetLine(2, "");
1881 progress->Progress();
1882 return progress->IsCanceled();
1887 int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1889 MOVIELIST movielist;
1890 CVideoInfoDownloader imdb(scraper);
1891 int returncode = imdb.FindMovie(videoName, movielist, progress);
1892 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
1893 { // scraper reported an error, or we had an error and user wants to cancel the scan
1895 return -1; // cancelled
1897 if (returncode > 0 && movielist.size())
1900 return 1; // found a movie
1902 return 0; // didn't find anything
1905 CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1907 CStdString strCheck = item.GetPath();
1909 strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1911 CStdString strDirectory = URIUtils::GetDirectory(strCheck);
1912 if (URIUtils::IsInRAR(strCheck))
1914 CStdString strPath=strDirectory;
1915 URIUtils::GetParentPath(strPath, strDirectory);
1919 strCheck = strDirectory;
1920 URIUtils::RemoveSlashAtEnd(strCheck);
1921 if (URIUtils::GetFileName(strCheck).size() == 3 && StringUtils::StartsWithNoCase(URIUtils::GetFileName(strCheck), "cd"))
1922 strDirectory = URIUtils::GetDirectory(strCheck);
1924 return strDirectory;