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 bool bFound = false;
1397 EPISODELIST::iterator guide = episodes.begin();;
1398 EPISODELIST matches;
1400 for (; guide != episodes.end(); ++guide )
1402 if ((file->iEpisode!=-1) && (file->iSeason!=-1) && (key==*guide))
1407 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1409 matches.push_back(*guide);
1412 if (!guide->cScraperUrl.strTitle.IsEmpty() && guide->cScraperUrl.strTitle.CompareNoCase(file->strTitle.c_str()) == 0)
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)
1447 titles.push_back(guide->cScraperUrl.strTitle.ToLower());
1450 std::string loweredTitle(file->strTitle);
1451 StringUtils::ToLower(loweredTitle);
1452 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
1453 if (matchscore >= minscore)
1455 guide = candidates->begin() + index;
1457 CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1458 __FUNCTION__, showInfo.m_strTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1465 CVideoInfoDownloader imdb(scraper);
1467 item.SetPath(file->strPath);
1468 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1469 return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1471 // Only set season/epnum from filename when it is not already set by a scraper
1472 if (item.GetVideoInfoTag()->m_iSeason == -1)
1473 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
1474 if (item.GetVideoInfoTag()->m_iEpisode == -1)
1475 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
1477 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
1482 CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d.%d, airdate: '%s', title: '%s'",
1483 __FUNCTION__, showInfo.m_strTitle.c_str(), file->iSeason, file->iEpisode, file->iSubepisode,
1484 file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1490 CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1493 // Find a matching .nfo file
1494 if (!item->m_bIsFolder)
1496 if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1498 CFileItem item2(*item);
1499 CURL url(item->GetPath());
1501 URIUtils::GetDirectory(url.GetHostName(), strPath);
1502 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1503 return GetnfoFile(&item2, bGrabAny);
1506 // grab the folder path
1508 URIUtils::GetDirectory(item->GetPath(), strPath);
1510 if (bGrabAny && !item->IsStack())
1511 { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1512 nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1513 if (CFile::Exists(nfoFile))
1517 // try looking for .nfo file for a stacked item
1518 if (item->IsStack())
1520 // first try .nfo file matching first file in stack
1521 CStackDirectory dir;
1522 CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1524 item2.SetPath(firstFile);
1525 nfoFile = GetnfoFile(&item2, bGrabAny);
1526 // else try .nfo file matching stacked title
1527 if (nfoFile.IsEmpty())
1529 CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1530 item2.SetPath(stackedTitlePath);
1531 nfoFile = GetnfoFile(&item2, bGrabAny);
1536 // already an .nfo file?
1537 if (URIUtils::HasExtension(item->GetPath(), ".nfo"))
1538 nfoFile = item->GetPath();
1539 // no, create .nfo file
1541 nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1544 // test file existence
1545 if (!nfoFile.IsEmpty() && !CFile::Exists(nfoFile))
1548 if (nfoFile.IsEmpty()) // final attempt - strip off any cd1 folders
1550 URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1552 if (strPath.Mid(strPath.size()-3).Equals("cd1"))
1554 strPath = strPath.Mid(0,strPath.size()-3);
1555 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1556 return GetnfoFile(&item2, bGrabAny);
1560 if (nfoFile.IsEmpty() && item->IsOpticalMediaFile())
1562 CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1563 nfoFile = GetnfoFile(&parentDirectory, true);
1566 // folders (or stacked dvds) can take any nfo file if there's a unique one
1567 if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.IsEmpty()))
1569 // see if there is a unique nfo file in this folder, and if so, use that
1570 CFileItemList items;
1572 CStdString strPath = item->GetPath();
1573 if (!item->m_bIsFolder)
1574 URIUtils::GetDirectory(item->GetPath(), strPath);
1575 if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1578 for (int i = 0; i < items.Size(); i++)
1580 if (items[i]->IsNFO())
1592 return items[numNFO]->GetPath();
1599 bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1601 CVideoInfoTag movieDetails;
1603 if (m_handle && !url.strTitle.IsEmpty())
1604 m_handle->SetText(url.strTitle);
1606 CVideoInfoDownloader imdb(scraper);
1607 bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1612 nfoFile->GetDetails(movieDetails,NULL,true);
1614 if (m_handle && url.strTitle.IsEmpty())
1615 m_handle->SetText(movieDetails.m_strTitle);
1619 pDialog->SetLine(1, movieDetails.m_strTitle);
1620 pDialog->Progress();
1623 *pItem->GetVideoInfoTag() = movieDetails;
1626 return false; // no info found, or cancelled
1629 void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1631 // copy icon to folder also;
1632 if (!imdbThumb.IsEmpty())
1634 CFileItem folderItem(folder, true);
1635 CThumbLoader loader;
1636 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
1640 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1642 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1643 if (0 == items.Size()) return 0;
1644 XBMC::XBMC_MD5 md5state;
1646 for (int i = 0; i < items.Size(); ++i)
1648 const CFileItemPtr pItem = items[i];
1649 md5state.append(pItem->GetPath());
1650 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1651 FILETIME time = pItem->m_dateTime;
1652 md5state.append((unsigned char *)&time, sizeof(FILETIME));
1653 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1656 md5state.getDigest(hash);
1660 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1662 // TODO: Probably should account for excluded folders here (eg samples), though that then
1663 // introduces possible problems if the user then changes the exclude regexps and
1664 // expects excluded folders that are inside a fast-hashed folder to then be picked
1665 // up. The chances that the user has a folder which contains only excluded folders
1666 // where some of those folders should be scanned recursively is pretty small.
1667 return items.GetFolderCount() == 0;
1670 CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1672 struct __stat64 buffer;
1673 if (XFILE::CFile::Stat(directory, &buffer) == 0)
1675 int64_t time = buffer.st_mtime;
1677 time = buffer.st_ctime;
1681 hash.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.GetReplaceString("\\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.IsEmpty())
1781 CStdString thumbFile = i->strName;
1782 thumbFile.Replace(" ","_");
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.IsEmpty() && !i->thumbUrl.GetFirstThumb().m_url.IsEmpty())
1794 i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1795 if (!i->thumb.IsEmpty())
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.IsEmpty() && 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(), 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'", 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;
1912 URIUtils::GetDirectory(strCheck, strDirectory);
1913 if (URIUtils::IsInRAR(strCheck))
1915 CStdString strPath=strDirectory;
1916 URIUtils::GetParentPath(strPath, strDirectory);
1920 strCheck = strDirectory;
1921 URIUtils::RemoveSlashAtEnd(strCheck);
1922 if (URIUtils::GetFileName(strCheck).size() == 3 && URIUtils::GetFileName(strCheck).Left(2).Equals("cd"))
1923 URIUtils::GetDirectory(strCheck, strDirectory);
1925 return strDirectory;