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.IsEmpty())
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.IsEmpty() && fastHash == dbHash)
268 { // fast hashes match - no need to process anything
269 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change (fasthash)", 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.IsEmpty())
281 if (dbHash.IsEmpty())
282 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '%s' as not in the database", strDirectory.c_str());
284 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '%s' due to change (%s != %s)", 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.IsEmpty() && !dbHash.IsEmpty())
290 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' as it's empty or doesn't exist - adding to clean list", strDirectory.c_str());
291 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
294 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change", strDirectory.c_str());
297 OnDirectoryScanned(strDirectory);
299 // update the hash to a fast hash if needed
300 if (CanFastHash(items) && !fastHash.IsEmpty())
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", strDirectory.c_str());
346 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
347 CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir %s", 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(), 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)
442 CLog::Log(LOGWARNING, "No information found for item '%s', it won't be added to the library.", pItem->GetPath().c_str());
446 // Keep track of directories we've seen
447 if (pItem->m_bIsFolder)
448 seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
451 if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
453 vector< pair<int,string> > libPaths;
454 m_database.GetSubPaths(items.GetPath(), libPaths);
455 for (vector< pair<int,string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
457 if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
458 m_pathsToClean.insert(i->first);
462 pDlgProgress->ShowProgressBar(false);
464 g_infoManager.ResetLibraryBools();
466 return FoundSomeInfo;
469 INFO_RET CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
472 if (pItem->m_bIsFolder)
473 idTvShow = m_database.GetTvShowId(pItem->GetPath());
477 URIUtils::GetDirectory(pItem->GetPath(),strPath);
478 idTvShow = m_database.GetTvShowId(strPath);
480 if (idTvShow > -1 && (fetchEpisodes || !pItem->m_bIsFolder))
482 INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
483 if (ret == INFO_ADDED)
484 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
488 if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361, pItem->GetLabel()))
489 return INFO_CANCELLED;
492 m_handle->SetText(pItem->GetMovieName(bDirNames));
494 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
498 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
499 if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
500 { // check for preconfigured scraper; if found, overwrite with interpreted scraper (from Nfofile)
501 // but keep current scan settings
502 SScanSettings settings;
503 if (m_database.GetScraperForPath(pItem->GetPath(), settings))
504 m_database.SetScraperForPath(pItem->GetPath(), info2, settings);
506 if (result == CNfoFile::FULL_NFO)
508 pItem->GetVideoInfoTag()->Reset();
509 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
511 long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
516 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
517 if (ret == INFO_ADDED)
518 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
523 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
530 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
531 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
534 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
536 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
541 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
542 if (ret == INFO_ADDED)
543 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
548 INFO_RET CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
550 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
551 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
552 return INFO_NOT_NEEDED;
554 if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
555 return INFO_CANCELLED;
557 if (m_database.HasMovieInfo(pItem->GetPath()))
558 return INFO_HAVE_ALREADY;
561 m_handle->SetText(pItem->GetMovieName(bDirNames));
563 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
567 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
568 if (result == CNfoFile::FULL_NFO)
570 pItem->GetVideoInfoTag()->Reset();
571 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
573 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
577 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
584 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
585 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
587 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
589 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
593 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
594 return INFO_NOT_FOUND;
597 INFO_RET CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
599 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
600 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
601 return INFO_NOT_NEEDED;
603 if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
604 return INFO_CANCELLED;
606 if (m_database.HasMusicVideoInfo(pItem->GetPath()))
607 return INFO_HAVE_ALREADY;
610 m_handle->SetText(pItem->GetMovieName(bDirNames));
612 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
616 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
617 if (result == CNfoFile::FULL_NFO)
619 pItem->GetVideoInfoTag()->Reset();
620 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
622 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
626 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
633 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
634 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
636 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
638 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
642 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
643 return INFO_NOT_FOUND;
646 INFO_RET CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item, long showID, const ADDON::ScraperPtr &scraper, bool useLocal, CGUIDialogProgress *progress)
648 // enumerate episodes
650 EnumerateSeriesFolder(item, files);
651 if (files.size() == 0) // no update or no files
652 return INFO_NOT_NEEDED;
654 if (m_bStop || (progress && progress->IsCanceled()))
655 return INFO_CANCELLED;
657 CVideoInfoTag showInfo;
658 m_database.GetTvShowInfo("", showInfo, showID);
659 return OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
662 void CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
666 if (item->m_bIsFolder)
668 CUtil::GetRecursiveListing(item->GetPath(), items, g_advancedSettings.m_videoExtensions, true);
669 CStdString hash, dbHash;
670 int numFilesInFolder = GetPathHash(items, hash);
672 if (m_database.GetPathHash(item->GetPath(), dbHash) && dbHash == hash)
674 m_currentItem += numFilesInFolder;
676 // update our dialog with our progress
680 m_handle->SetPercentage(m_currentItem*100.f/m_itemCount);
682 OnDirectoryScanned(item->GetPath());
686 m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
687 m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
688 item->SetProperty("hash", hash);
692 CFileItemPtr newItem(new CFileItem(*item));
697 stack down any dvd folders
698 need to sort using the full path since this is a collapsed recursive listing of all subdirs
699 video_ts.ifo files should sort at the top of a dvd folder in ascending order
701 /foo/bar/video_ts.ifo
706 // since we're doing this now anyway, should other items be stacked?
707 items.Sort(SORT_METHOD_FULLPATH, SortOrderAscending);
709 while (x < items.Size())
711 if (items[x]->m_bIsFolder)
715 CStdString strPathX, strFileX;
716 URIUtils::Split(items[x]->GetPath(), strPathX, strFileX);
717 //CLog::Log(LOGDEBUG,"%i:%s:%s", x, strPathX.c_str(), strFileX.c_str());
720 if (strFileX.Equals("VIDEO_TS.IFO"))
722 while (y < items.Size())
724 CStdString strPathY, strFileY;
725 URIUtils::Split(items[y]->GetPath(), strPathY, strFileY);
726 //CLog::Log(LOGDEBUG," %i:%s:%s", y, strPathY.c_str(), strFileY.c_str());
728 if (strPathY.Equals(strPathX))
730 remove everything sorted below the video_ts.ifo file in the same path.
731 understandbly this wont stack correctly if there are other files in the the dvd folder.
732 this should be unlikely and thus is being ignored for now but we can monitor the
733 where the path changes and potentially remove the items above the video_ts.ifo file.
744 CStdStringArray regexps = g_advancedSettings.m_tvshowExcludeFromScanRegExps;
746 for (int i=0;i<items.Size();++i)
748 if (items[i]->m_bIsFolder)
751 URIUtils::GetDirectory(items[i]->GetPath(), strPath);
752 URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
754 if (URIUtils::GetFileName(strPath).Equals("sample"))
757 // Discard all exclude files defined by regExExcludes
758 if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
762 * Check if the media source has already set the season and episode or original air date in
763 * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
764 * any false positive matches.
766 if (ProcessItemByVideoInfoTag(items[i].get(), episodeList))
769 if (!EnumerateEpisodeItem(items[i].get(), episodeList))
771 CStdString decode(items[i]->GetPath());
772 CURL::Decode(decode);
773 CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file %s", decode.c_str());
778 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
780 if (!item->HasVideoInfoTag())
783 const CVideoInfoTag* tag = item->GetVideoInfoTag();
785 * First check the season and episode number. This takes precedence over the original air
786 * date and episode title. Must be a valid season and episode number combination.
788 if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
791 episode.strPath = item->GetPath();
792 episode.iSeason = tag->m_iSeason;
793 episode.iEpisode = tag->m_iEpisode;
794 episode.isFolder = false;
795 episodeList.push_back(episode);
796 CLog::Log(LOGDEBUG, "%s - found match for: %s. Season %d, Episode %d", __FUNCTION__,
797 episode.strPath.c_str(), episode.iSeason, episode.iEpisode);
802 * Next preference is the first aired date. If it exists use that for matching the TV Show
803 * information. Also set the title in case there are multiple matches for the first aired date.
805 if (tag->m_firstAired.IsValid())
808 episode.strPath = item->GetPath();
809 episode.strTitle = tag->m_strTitle;
810 episode.isFolder = false;
812 * Set season and episode to -1 to indicate to use the aired date.
814 episode.iSeason = -1;
815 episode.iEpisode = -1;
817 * The first aired date string must be parseable.
819 episode.cDate = item->GetVideoInfoTag()->m_firstAired;
820 episodeList.push_back(episode);
821 CLog::Log(LOGDEBUG, "%s - found match for: '%s', firstAired: '%s' = '%s', title: '%s'",
822 __FUNCTION__, episode.strPath.c_str(), tag->m_firstAired.GetAsDBDateTime().c_str(),
823 episode.cDate.GetAsLocalizedDate().c_str(), episode.strTitle.c_str());
828 * Next preference is the episode title. If it exists use that for matching the TV Show
831 if (!tag->m_strTitle.IsEmpty())
834 episode.strPath = item->GetPath();
835 episode.strTitle = tag->m_strTitle;
836 episode.isFolder = false;
838 * Set season and episode to -1 to indicate to use the title.
840 episode.iSeason = -1;
841 episode.iEpisode = -1;
842 episodeList.push_back(episode);
843 CLog::Log(LOGDEBUG,"%s - found match for: '%s', title: '%s'", __FUNCTION__,
844 episode.strPath.c_str(), episode.strTitle.c_str());
849 * There is no further episode information available if both the season and episode number have
850 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
851 * to the episode list.
853 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
855 CLog::Log(LOGDEBUG,"%s - found exclusion match for: %s. Both Season and Episode are 0. Item will be ignored for scanning.",
856 __FUNCTION__, item->GetPath().c_str());
863 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
865 SETTINGS_TVSHOWLIST expression = g_advancedSettings.m_tvshowEnumRegExps;
867 CStdString strLabel=item->GetPath();
868 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
869 CURL::Decode(strLabel);
870 strLabel.MakeLower();
872 for (unsigned int i=0;i<expression.size();++i)
875 if (!reg.RegComp(expression[i].regexp))
878 int regexppos, regexp2pos;
879 //CLog::Log(LOGDEBUG,"running expression %s on %s",expression[i].regexp.c_str(),strLabel.c_str());
880 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
884 episode.strPath = item->GetPath();
885 episode.iSeason = -1;
886 episode.iEpisode = -1;
887 episode.cDate.SetValid(false);
888 episode.isFolder = false;
890 bool byDate = expression[i].byDate ? true : false;
891 int defaultSeason = expression[i].defaultSeason;
895 if (!GetAirDateFromRegExp(reg, episode))
898 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match %s (%s) [%s]", strLabel.c_str(),
899 episode.cDate.GetAsLocalizedDate().c_str(), expression[i].regexp.c_str());
903 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
906 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match %s (s%ie%i) [%s]", strLabel.c_str(),
907 episode.iSeason, episode.iEpisode, expression[i].regexp.c_str());
910 // Grab the remainder from first regexp run
911 // as second run might modify or empty it.
912 std::string remainder = reg.GetReplaceString("\\3");
915 * Check if the files base path is a dedicated folder that contains
916 * only this single episode. If season and episode match with the
917 * actual media file, we set episode.isFolder to true.
919 CStdString strBasePath = item->GetBaseMoviePath(true);
920 URIUtils::RemoveSlashAtEnd(strBasePath);
921 strBasePath = URIUtils::GetFileName(strBasePath);
923 if (reg.RegFind(strBasePath.c_str()) > -1)
928 GetAirDateFromRegExp(reg, parent);
929 if (episode.cDate == parent.cDate)
930 episode.isFolder = true;
934 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
935 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
936 episode.isFolder = true;
940 // add what we found by now
941 episodeList.push_back(episode);
944 // check the remainder of the string for any further episodes.
945 if (!byDate && reg2.RegComp(g_advancedSettings.m_tvshowMultiPartEnumRegExp))
949 // we want "long circuit" OR below so that both offsets are evaluated
950 while (((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) | ((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
952 if (((regexppos <= regexp2pos) && regexppos != -1) ||
953 (regexppos >= 0 && regexp2pos == -1))
955 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
957 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season %u, multipart episode %u [%s]",
958 episode.iSeason, episode.iEpisode,
959 g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
961 episodeList.push_back(episode);
962 remainder = reg.GetReplaceString("\\3");
965 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
966 (regexp2pos >= 0 && regexppos == -1))
968 episode.iEpisode = atoi(reg2.GetReplaceString("\\1").c_str());
969 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode %u [%s]",
970 episode.iEpisode, g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
971 episodeList.push_back(episode);
972 offset += regexp2pos + reg2.GetFindLen();
981 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp ®, EPISODE &episodeInfo, int defaultSeason)
983 std::string season = reg.GetReplaceString("\\1");
984 std::string episode = reg.GetReplaceString("\\2");
986 if (!season.empty() || !episode.empty())
989 if (season.empty() && !episode.empty())
990 { // no season specified -> assume defaultSeason
991 episodeInfo.iSeason = defaultSeason;
992 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
993 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
995 else if (!season.empty() && episode.empty())
996 { // no episode specification -> assume defaultSeason
997 episodeInfo.iSeason = defaultSeason;
998 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
999 episodeInfo.iEpisode = atoi(season.c_str());
1002 { // season and episode specified
1003 episodeInfo.iSeason = atoi(season.c_str());
1004 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1008 if (isalpha(*endptr))
1009 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1010 else if (*endptr == '.')
1011 episodeInfo.iSubepisode = atoi(endptr+1);
1018 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp ®, EPISODE &episodeInfo)
1020 std::string param1 = reg.GetReplaceString("\\1");
1021 std::string param2 = reg.GetReplaceString("\\2");
1022 std::string param3 = reg.GetReplaceString("\\3");
1024 if (!param1.empty() && !param2.empty() && !param3.empty())
1026 // regular expression by date
1027 int len1 = param1.size();
1028 int len2 = param2.size();
1029 int len3 = param3.size();
1031 if (len1==4 && len2==2 && len3==2)
1033 // yyyy mm dd format
1034 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1036 else if (len1==2 && len2==2 && len3==4)
1038 // mm dd yyyy format
1039 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1042 return episodeInfo.cDate.IsValid();
1045 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1047 // ensure our database is open (this can get called via other classes)
1048 if (!m_database.Open())
1052 GetArtwork(pItem, content, videoFolder, useLocal, showInfo ? showInfo->m_strPath : "");
1054 // ensure the art map isn't completely empty by specifying an empty thumb
1055 map<string, string> art = pItem->GetArt();
1059 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1060 if (movieDetails.m_basePath.IsEmpty())
1061 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1062 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1064 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1066 if (pItem->m_bIsFolder)
1067 movieDetails.m_strPath = pItem->GetPath();
1069 CStdString strTitle(movieDetails.m_strTitle);
1071 if (showInfo && content == CONTENT_TVSHOWS)
1073 strTitle.Format("%s - %ix%i - %s", showInfo->m_strTitle.c_str(), movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle.c_str());
1076 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to %s:%s", TranslateContent(content).c_str(), pItem->GetPath().c_str());
1079 if (content == CONTENT_MOVIES)
1081 // find local trailer first
1082 CStdString strTrailer = pItem->FindTrailer();
1083 if (!strTrailer.IsEmpty())
1084 movieDetails.m_strTrailer = strTrailer;
1086 lResult = m_database.SetDetailsForMovie(pItem->GetPath(), movieDetails, art);
1087 movieDetails.m_iDbId = lResult;
1088 movieDetails.m_type = "movie";
1090 // setup links to shows if the linked shows are in the db
1091 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1093 CFileItemList items;
1094 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1096 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1098 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie %s to show %s", movieDetails.m_strTitle.c_str(), movieDetails.m_showLink[i].c_str());
1101 else if (content == CONTENT_TVSHOWS)
1103 if (pItem->m_bIsFolder)
1105 map<int, map<string, string> > seasonArt;
1107 { // get and cache season thumbs
1108 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes("season"), useLocal);
1109 for (map<int, map<string, string> >::iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
1110 for (map<string, string>::iterator j = i->second.begin(); j != i->second.end(); ++j)
1111 CTextureCache::Get().BackgroundCacheImage(j->second);
1113 lResult = m_database.SetDetailsForTvShow(pItem->GetPath(), movieDetails, art, seasonArt);
1114 movieDetails.m_iDbId = lResult;
1115 movieDetails.m_type = "tvshow";
1119 // we add episode then set details, as otherwise set details will delete the
1120 // episode then add, which breaks multi-episode files.
1121 int idShow = showInfo ? showInfo->m_iDbId : -1;
1122 int idEpisode = m_database.AddEpisode(idShow, pItem->GetPath());
1123 lResult = m_database.SetDetailsForEpisode(pItem->GetPath(), movieDetails, art, idShow, idEpisode);
1124 movieDetails.m_iDbId = lResult;
1125 movieDetails.m_type = "episode";
1126 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1127 if (movieDetails.m_fEpBookmark > 0)
1129 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1131 bookmark.timeInSeconds = movieDetails.m_fEpBookmark;
1132 bookmark.seasonNumber = movieDetails.m_iSeason;
1133 bookmark.episodeNumber = movieDetails.m_iEpisode;
1134 m_database.AddBookMarkForEpisode(movieDetails, bookmark);
1138 else if (content == CONTENT_MUSICVIDEOS)
1140 lResult = m_database.SetDetailsForMusicVideo(pItem->GetPath(), movieDetails, art);
1141 movieDetails.m_iDbId = lResult;
1142 movieDetails.m_type = "musicvideo";
1145 if (g_advancedSettings.m_bVideoLibraryImportWatchedState || libraryImport)
1146 m_database.SetPlayCount(*pItem, movieDetails.m_playCount, movieDetails.m_lastPlayed);
1148 if ((g_advancedSettings.m_bVideoLibraryImportResumePoint || libraryImport) &&
1149 movieDetails.m_resumePoint.IsSet())
1150 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.m_resumePoint, CBookmark::RESUME);
1154 CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
1155 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", itemCopy);
1159 string ContentToMediaType(CONTENT_TYPE content, bool folder)
1163 case CONTENT_MOVIES:
1165 case CONTENT_MUSICVIDEOS:
1166 return "musicvideo";
1167 case CONTENT_TVSHOWS:
1168 return folder ? "tvshow" : "episode";
1174 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1176 std::string type = "thumb";
1177 if (width*5 < height*4)
1179 else if (width*1 > height*4)
1184 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1186 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1187 movieDetails.m_fanart.Unpack();
1188 movieDetails.m_strPictureURL.Parse();
1190 CGUIListItem::ArtMap art = pItem->GetArt();
1192 // get and cache thumb images
1193 vector<string> artTypes = CVideoThumbLoader::GetArtTypes(ContentToMediaType(content, pItem->m_bIsFolder));
1194 vector<string>::iterator i = find(artTypes.begin(), artTypes.end(), "fanart");
1195 if (i != artTypes.end())
1196 artTypes.erase(i); // fanart is handled below
1197 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end() &&
1198 art.find("thumb") == art.end();
1202 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1204 if (art.find(*i) == art.end())
1206 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, *i, bApplyToDir);
1208 art.insert(make_pair(*i, image));
1211 // find and classify the local thumb (backcompat) if available
1214 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, "thumb", bApplyToDir);
1216 { // cache the image and determine sizing
1217 CTextureDetails details;
1218 if (CTextureCache::Get().CacheImage(image, details))
1220 std::string type = GetArtTypeFromSize(details.width, details.height);
1221 if (art.find(type) == art.end())
1222 art.insert(make_pair(type, image));
1229 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1231 if (art.find(*i) == art.end())
1233 std::string image = GetImage(pItem, false, bApplyToDir, *i);
1235 art.insert(make_pair(*i, image));
1239 // use the first piece of online art as the first art type if no thumb type is available yet
1240 if (art.empty() && lookForThumb)
1242 std::string image = GetImage(pItem, false, bApplyToDir, "thumb");
1244 art.insert(make_pair(artTypes.front(), image));
1247 // get & save fanart image (treated separately due to it being stored in m_fanart)
1248 bool isEpisode = (content == CONTENT_TVSHOWS && !pItem->m_bIsFolder);
1249 if (!isEpisode && art.find("fanart") == art.end())
1251 string fanart = GetFanart(pItem, useLocal);
1252 if (!fanart.empty())
1253 art.insert(make_pair("fanart", fanart));
1256 for (CGUIListItem::ArtMap::const_iterator i = art.begin(); i != art.end(); ++i)
1257 CTextureCache::Get().BackgroundCacheImage(i->second);
1261 // parent folder to apply the thumb to and to search for local actor thumbs
1262 CStdString parentDir = GetParentDir(*pItem);
1263 if (CSettings::Get().GetBool("videolibrary.actorthumbs"))
1264 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1266 ApplyThumbToFolder(parentDir, art["thumb"]);
1269 std::string CVideoInfoScanner::GetImage(CFileItem *pItem, bool useLocal, bool bApplyToDir, const std::string &type)
1273 thumb = CVideoThumbLoader::GetLocalArt(*pItem, type, bApplyToDir);
1277 thumb = CScraperUrl::GetThumbURL(pItem->GetVideoInfoTag()->m_strPictureURL.GetFirstThumb(type));
1280 if (thumb.find("http://") == string::npos &&
1281 thumb.find("/") == string::npos &&
1282 thumb.find("\\") == string::npos)
1285 URIUtils::GetDirectory(pItem->GetPath(), strPath);
1286 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1293 std::string CVideoInfoScanner::GetFanart(CFileItem *pItem, bool useLocal)
1295 std::string fanart = pItem->GetArt("fanart");
1296 if (fanart.empty() && useLocal)
1297 fanart = pItem->FindLocalArt("fanart.jpg", true);
1299 fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1303 INFO_RET CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress /* = NULL */)
1307 pDlgProgress->SetLine(1, showInfo.m_strTitle);
1308 pDlgProgress->SetLine(2, 20361);
1309 pDlgProgress->SetPercentage(0);
1310 pDlgProgress->ShowProgressBar(true);
1311 pDlgProgress->Progress();
1314 EPISODELIST episodes;
1315 bool hasEpisodeGuide = false;
1317 int iMax = files.size();
1319 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1321 m_nfoReader.Close();
1324 pDlgProgress->SetLine(2, 20361);
1325 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1326 pDlgProgress->Progress();
1329 m_handle->SetPercentage(100.f*iCurr++/iMax);
1331 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1332 return INFO_CANCELLED;
1334 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1337 m_handle->SetText(g_localizeStrings.Get(20415));
1342 item.SetPath(file->strPath);
1344 // handle .nfo files
1345 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1347 ScraperPtr info(scraper);
1348 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1350 result = CheckForNFOFile(&item, false, info,scrUrl);
1351 if (result == CNfoFile::FULL_NFO)
1353 m_nfoReader.GetDetails(*item.GetVideoInfoTag());
1354 // override with episode and season number from file if available
1355 if (file->iEpisode > -1)
1357 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1358 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1360 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1365 if (!hasEpisodeGuide)
1367 // fetch episode guide
1368 if (!showInfo.m_strEpisodeGuide.IsEmpty())
1371 url.ParseEpisodeGuide(showInfo.m_strEpisodeGuide);
1375 pDlgProgress->SetLine(2, 20354);
1376 pDlgProgress->Progress();
1379 CVideoInfoDownloader imdb(scraper);
1380 if (!imdb.GetEpisodeList(url, episodes))
1381 return INFO_NOT_FOUND;
1383 hasEpisodeGuide = true;
1387 if (episodes.empty())
1389 CLog::Log(LOGERROR, "VideoInfoScanner: Asked to lookup episode %s"
1390 " online, but we have no episode guide. Check your tvshow.nfo and make"
1391 " sure the <episodeguide> tag is in place.", file->strPath.c_str());
1395 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
1396 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
1397 bool bFound = false;
1398 EPISODELIST::iterator guide = episodes.begin();;
1399 EPISODELIST matches;
1401 for (; guide != episodes.end(); ++guide )
1403 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
1410 else if ((file->iSubepisode!=0) && (backupkey==*guide))
1412 matches.push_back(*guide);
1416 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1418 matches.push_back(*guide);
1421 if (!guide->cScraperUrl.strTitle.IsEmpty() && guide->cScraperUrl.strTitle.CompareNoCase(file->strTitle.c_str()) == 0)
1431 * If there is only one match or there are matches but no title to compare with to help
1432 * identify the best match, then pick the first match as the best possible candidate.
1434 * Otherwise, use the title to further refine the best match.
1436 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
1438 guide = matches.begin();
1441 else if (!file->strTitle.empty())
1443 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
1445 EPISODELIST *candidates;
1446 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1448 minscore = 0.8; // 80% should ensure a good match.
1449 candidates = &episodes;
1451 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1452 candidates = &matches;
1454 CStdStringArray titles;
1455 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
1456 titles.push_back(guide->cScraperUrl.strTitle.ToLower());
1459 std::string loweredTitle(file->strTitle);
1460 StringUtils::ToLower(loweredTitle);
1461 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
1462 if (matchscore >= minscore)
1464 guide = candidates->begin() + index;
1466 CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1467 __FUNCTION__, showInfo.m_strTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1474 CVideoInfoDownloader imdb(scraper);
1476 item.SetPath(file->strPath);
1477 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1478 return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1480 // Only set season/epnum from filename when it is not already set by a scraper
1481 if (item.GetVideoInfoTag()->m_iSeason == -1)
1482 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
1483 if (item.GetVideoInfoTag()->m_iEpisode == -1)
1484 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
1486 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
1491 CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d.%d, airdate: '%s', title: '%s'",
1492 __FUNCTION__, showInfo.m_strTitle.c_str(), file->iSeason, file->iEpisode, file->iSubepisode,
1493 file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1499 CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1502 // Find a matching .nfo file
1503 if (!item->m_bIsFolder)
1505 if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1507 CFileItem item2(*item);
1508 CURL url(item->GetPath());
1510 URIUtils::GetDirectory(url.GetHostName(), strPath);
1511 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1512 return GetnfoFile(&item2, bGrabAny);
1515 // grab the folder path
1517 URIUtils::GetDirectory(item->GetPath(), strPath);
1519 if (bGrabAny && !item->IsStack())
1520 { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1521 nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1522 if (CFile::Exists(nfoFile))
1526 // try looking for .nfo file for a stacked item
1527 if (item->IsStack())
1529 // first try .nfo file matching first file in stack
1530 CStackDirectory dir;
1531 CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1533 item2.SetPath(firstFile);
1534 nfoFile = GetnfoFile(&item2, bGrabAny);
1535 // else try .nfo file matching stacked title
1536 if (nfoFile.IsEmpty())
1538 CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1539 item2.SetPath(stackedTitlePath);
1540 nfoFile = GetnfoFile(&item2, bGrabAny);
1545 // already an .nfo file?
1546 if (URIUtils::HasExtension(item->GetPath(), ".nfo"))
1547 nfoFile = item->GetPath();
1548 // no, create .nfo file
1550 nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1553 // test file existence
1554 if (!nfoFile.IsEmpty() && !CFile::Exists(nfoFile))
1557 if (nfoFile.IsEmpty()) // final attempt - strip off any cd1 folders
1559 URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1561 if (strPath.Mid(strPath.size()-3).Equals("cd1"))
1563 strPath = strPath.Mid(0,strPath.size()-3);
1564 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1565 return GetnfoFile(&item2, bGrabAny);
1569 if (nfoFile.IsEmpty() && item->IsOpticalMediaFile())
1571 CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1572 nfoFile = GetnfoFile(&parentDirectory, true);
1575 // folders (or stacked dvds) can take any nfo file if there's a unique one
1576 if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.IsEmpty()))
1578 // see if there is a unique nfo file in this folder, and if so, use that
1579 CFileItemList items;
1581 CStdString strPath = item->GetPath();
1582 if (!item->m_bIsFolder)
1583 URIUtils::GetDirectory(item->GetPath(), strPath);
1584 if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1587 for (int i = 0; i < items.Size(); i++)
1589 if (items[i]->IsNFO())
1601 return items[numNFO]->GetPath();
1608 bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1610 CVideoInfoTag movieDetails;
1612 if (m_handle && !url.strTitle.IsEmpty())
1613 m_handle->SetText(url.strTitle);
1615 CVideoInfoDownloader imdb(scraper);
1616 bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1621 nfoFile->GetDetails(movieDetails,NULL,true);
1623 if (m_handle && url.strTitle.IsEmpty())
1624 m_handle->SetText(movieDetails.m_strTitle);
1628 pDialog->SetLine(1, movieDetails.m_strTitle);
1629 pDialog->Progress();
1632 *pItem->GetVideoInfoTag() = movieDetails;
1635 return false; // no info found, or cancelled
1638 void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1640 // copy icon to folder also;
1641 if (!imdbThumb.IsEmpty())
1643 CFileItem folderItem(folder, true);
1644 CThumbLoader loader;
1645 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
1649 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1651 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1652 if (0 == items.Size()) return 0;
1653 XBMC::XBMC_MD5 md5state;
1655 for (int i = 0; i < items.Size(); ++i)
1657 const CFileItemPtr pItem = items[i];
1658 md5state.append(pItem->GetPath());
1659 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1660 FILETIME time = pItem->m_dateTime;
1661 md5state.append((unsigned char *)&time, sizeof(FILETIME));
1662 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1665 md5state.getDigest(hash);
1669 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1671 // TODO: Probably should account for excluded folders here (eg samples), though that then
1672 // introduces possible problems if the user then changes the exclude regexps and
1673 // expects excluded folders that are inside a fast-hashed folder to then be picked
1674 // up. The chances that the user has a folder which contains only excluded folders
1675 // where some of those folders should be scanned recursively is pretty small.
1676 return items.GetFolderCount() == 0;
1679 CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1681 struct __stat64 buffer;
1682 if (XFILE::CFile::Stat(directory, &buffer) == 0)
1684 int64_t time = buffer.st_mtime;
1686 time = buffer.st_ctime;
1690 hash.Format("fast%"PRId64, time);
1697 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, map<int, map<string, string> > &seasonArt, const vector<string> &artTypes, bool useLocal)
1699 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end();
1701 // find the maximum number of seasons we have thumbs for (local + remote)
1702 int maxSeasons = show.m_strPictureURL.GetMaxSeasonThumb();
1704 CFileItemList items;
1705 CDirectory::GetDirectory(show.m_strPath, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
1707 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z]+)?\\.(tbn|jpg|png)"))
1709 for (int i = 0; i < items.Size(); i++)
1711 CStdString name = URIUtils::GetFileName(items[i]->GetPath());
1712 if (reg.RegFind(name) > -1)
1714 int season = atoi(reg.GetReplaceString("\\1").c_str());
1715 if (season > maxSeasons)
1716 maxSeasons = season;
1720 for (int season = -1; season <= maxSeasons; season++)
1722 map<string, string> art;
1727 basePath = "season-all";
1728 else if (season == 0)
1729 basePath = "season-specials";
1731 basePath = StringUtils::Format("season%02i", season);
1732 CFileItem artItem(URIUtils::AddFileToFolder(show.m_strPath, basePath), false);
1734 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1736 std::string image = CVideoThumbLoader::GetLocalArt(artItem, *i, false);
1738 art.insert(make_pair(*i, image));
1740 // find and classify the local thumb (backcompat) if available
1743 std::string image = CVideoThumbLoader::GetLocalArt(artItem, "thumb", false);
1745 { // cache the image and determine sizing
1746 CTextureDetails details;
1747 if (CTextureCache::Get().CacheImage(image, details))
1749 std::string type = GetArtTypeFromSize(details.width, details.height);
1750 if (art.find(type) == art.end())
1751 art.insert(make_pair(type, image));
1758 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1760 if (art.find(*i) == art.end())
1762 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, *i));
1764 art.insert(make_pair(*i, image));
1767 // use the first piece of online art as the first art type if no thumb type is available yet
1768 if (art.empty() && lookForThumb)
1770 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, "thumb"));
1772 art.insert(make_pair(artTypes.front(), image));
1775 seasonArt.insert(make_pair(season, art));
1779 void CVideoInfoScanner::FetchActorThumbs(vector<SActorInfo>& actors, const CStdString& strPath)
1781 CFileItemList items;
1782 CStdString actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
1783 if (CDirectory::Exists(actorsDir))
1784 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
1785 DIR_FLAG_NO_FILE_INFO);
1786 for (vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
1788 if (i->thumb.IsEmpty())
1790 CStdString thumbFile = i->strName;
1791 thumbFile.Replace(" ","_");
1792 for (int j = 0; j < items.Size(); j++)
1794 CStdString compare = URIUtils::GetFileName(items[j]->GetPath());
1795 URIUtils::RemoveExtension(compare);
1796 if (!items[j]->m_bIsFolder && compare == thumbFile)
1798 i->thumb = items[j]->GetPath();
1802 if (i->thumb.IsEmpty() && !i->thumbUrl.GetFirstThumb().m_url.IsEmpty())
1803 i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1804 if (!i->thumb.IsEmpty())
1805 CTextureCache::Get().BackgroundCacheImage(i->thumb);
1810 CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1812 CStdString strNfoFile;
1813 if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1814 || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1815 strNfoFile = GetnfoFile(pItem, bGrabAny);
1816 if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1817 strNfoFile = URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo");
1819 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1820 if (!strNfoFile.IsEmpty() && CFile::Exists(strNfoFile))
1822 if (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder)
1823 result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1825 result = m_nfoReader.Create(strNfoFile,info);
1830 case CNfoFile::COMBINED_NFO:
1833 case CNfoFile::FULL_NFO:
1836 case CNfoFile::URL_NFO:
1839 case CNfoFile::NO_NFO:
1845 if (result != CNfoFile::NO_NFO)
1846 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), strNfoFile.c_str());
1847 if (result == CNfoFile::FULL_NFO)
1849 if (info->Content() == CONTENT_TVSHOWS)
1850 info = m_nfoReader.GetScraperInfo();
1852 else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1854 scrUrl = m_nfoReader.ScraperUrl();
1855 info = m_nfoReader.GetScraperInfo();
1857 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1858 scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1860 if (result == CNfoFile::COMBINED_NFO)
1861 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1865 CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", pItem->GetPath().c_str());
1870 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1872 if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1877 CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1880 return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1883 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1887 progress->SetHeading(heading);
1888 progress->SetLine(0, line1);
1889 progress->SetLine(2, "");
1890 progress->Progress();
1891 return progress->IsCanceled();
1896 int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1898 MOVIELIST movielist;
1899 CVideoInfoDownloader imdb(scraper);
1900 int returncode = imdb.FindMovie(videoName, movielist, progress);
1901 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
1902 { // scraper reported an error, or we had an error and user wants to cancel the scan
1904 return -1; // cancelled
1906 if (returncode > 0 && movielist.size())
1909 return 1; // found a movie
1911 return 0; // didn't find anything
1914 CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1916 CStdString strCheck = item.GetPath();
1918 strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1920 CStdString strDirectory;
1921 URIUtils::GetDirectory(strCheck, strDirectory);
1922 if (URIUtils::IsInRAR(strCheck))
1924 CStdString strPath=strDirectory;
1925 URIUtils::GetParentPath(strPath, strDirectory);
1929 strCheck = strDirectory;
1930 URIUtils::RemoveSlashAtEnd(strCheck);
1931 if (URIUtils::GetFileName(strCheck).size() == 3 && URIUtils::GetFileName(strCheck).Left(2).Equals("cd"))
1932 URIUtils::GetDirectory(strCheck, strDirectory);
1934 return strDirectory;