2 * Copyright (C) 2005-2013 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
21 #include "threads/SystemClock.h"
23 #include "VideoInfoScanner.h"
24 #include "addons/AddonManager.h"
25 #include "filesystem/DirectoryCache.h"
28 #include "utils/RegExp.h"
29 #include "utils/md5.h"
30 #include "filesystem/StackDirectory.h"
31 #include "VideoInfoDownloader.h"
32 #include "GUIInfoManager.h"
33 #include "filesystem/File.h"
34 #include "dialogs/GUIDialogExtendedProgressBar.h"
35 #include "dialogs/GUIDialogProgress.h"
36 #include "dialogs/GUIDialogYesNo.h"
37 #include "dialogs/GUIDialogOK.h"
38 #include "interfaces/AnnouncementManager.h"
39 #include "settings/AdvancedSettings.h"
40 #include "settings/Settings.h"
41 #include "utils/StringUtils.h"
42 #include "guilib/LocalizeStrings.h"
43 #include "guilib/GUIWindowManager.h"
44 #include "utils/TimeUtils.h"
45 #include "utils/log.h"
46 #include "utils/URIUtils.h"
47 #include "utils/Variant.h"
48 #include "video/VideoThumbLoader.h"
49 #include "TextureCache.h"
50 #include "GUIUserMessages.h"
54 using namespace XFILE;
55 using namespace ADDON;
60 CVideoInfoScanner::CVideoInfoScanner() : CThread("VideoInfoScanner")
65 m_bCanInterrupt = false;
72 CVideoInfoScanner::~CVideoInfoScanner()
76 void CVideoInfoScanner::Process()
80 unsigned int tick = XbmcThreads::SystemClockMillis();
84 if (m_showDialog && !CSettings::Get().GetBool("videolibrary.backgroundupdate"))
86 CGUIDialogExtendedProgressBar* dialog =
87 (CGUIDialogExtendedProgressBar*)g_windowManager.GetWindow(WINDOW_DIALOG_EXT_PROGRESS);
89 m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
92 m_bCanInterrupt = true;
94 CLog::Log(LOGNOTICE, "VideoInfoScanner: Starting scan ..");
95 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanStarted");
97 // Reset progress vars
101 SetPriority(GetMinPriority());
103 // Database operations should not be canceled
104 // using Interupt() while scanning as it could
105 // result in unexpected behaviour.
106 m_bCanInterrupt = false;
108 bool bCancelled = false;
109 while (!bCancelled && m_pathsToScan.size())
112 * A copy of the directory path is used because the path supplied is
113 * immediately removed from the m_pathsToScan set in DoScan(). If the
114 * reference points to the entry in the set a null reference error
117 CStdString directory = *m_pathsToScan.begin();
118 if (!CDirectory::Exists(directory))
121 * Note that this will skip clean (if m_bClean is enabled) if the directory really
122 * doesn't exist rather than a NAS being switched off. A manual clean from settings
123 * will still pick up and remove it though.
125 CLog::Log(LOGWARNING, "%s directory '%s' does not exist - skipping scan%s.", __FUNCTION__, directory.c_str(), m_bClean ? " and clean" : "");
126 m_pathsToScan.erase(m_pathsToScan.begin());
128 else if (!DoScan(directory))
135 CleanDatabase(m_handle,&m_pathsToClean, false);
139 m_handle->SetTitle(g_localizeStrings.Get(331));
140 m_database.Compress(false);
146 tick = XbmcThreads::SystemClockMillis() - tick;
147 CLog::Log(LOGNOTICE, "VideoInfoScanner: Finished scan. Scanning for video info took %s", StringUtils::SecondsToTimeString(tick / 1000).c_str());
151 CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
155 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanFinished");
157 // we need to clear the videodb cache and update any active lists
158 CUtil::DeleteVideoDatabaseDirectoryCache();
159 CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0);
160 g_windowManager.SendThreadMessage(msg);
163 m_handle->MarkFinished();
167 void CVideoInfoScanner::Start(const CStdString& strDirectory, bool scanAll)
169 m_strStartDir = strDirectory;
171 m_pathsToScan.clear();
172 m_pathsToClean.clear();
174 if (strDirectory.empty())
175 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
178 m_database.GetPaths(m_pathsToScan);
183 m_pathsToScan.insert(strDirectory);
185 m_bClean = g_advancedSettings.m_bVideoLibraryCleanOnUpdate;
192 bool CVideoInfoScanner::IsScanning()
197 void CVideoInfoScanner::Stop()
200 m_database.Interupt();
205 void CVideoInfoScanner::CleanDatabase(CGUIDialogProgressBarHandle* handle /*= NULL */, const set<int>* paths /*= NULL */, bool showProgress /*= true */)
209 m_database.CleanDatabase(handle, paths, showProgress);
214 static void OnDirectoryScanned(const CStdString& strDirectory)
216 CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
217 msg.SetStringParam(strDirectory);
218 g_windowManager.SendThreadMessage(msg);
221 bool CVideoInfoScanner::DoScan(const CStdString& strDirectory)
225 m_handle->SetText(g_localizeStrings.Get(20415));
229 * Remove this path from the list we're processing. This must be done prior to
230 * the check for file or folder exclusion to prevent an infinite while loop
233 set<CStdString>::iterator it = m_pathsToScan.find(strDirectory);
234 if (it != m_pathsToScan.end())
235 m_pathsToScan.erase(it);
239 bool foundDirectly = false;
242 SScanSettings settings;
243 ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
244 CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
246 // exclude folders that match our exclude regexps
247 CStdStringArray regexps = content == CONTENT_TVSHOWS ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
248 : g_advancedSettings.m_moviesExcludeFromScanRegExps;
250 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
253 bool ignoreFolder = !m_scanAll && settings.noupdate;
254 if (content == CONTENT_NONE || ignoreFolder)
257 CStdString hash, dbHash;
258 if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
262 int str = content == CONTENT_MOVIES ? 20317:20318;
263 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name().c_str()));
266 CStdString fastHash = GetFastHash(strDirectory);
267 if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && fastHash == dbHash)
268 { // fast hashes match - no need to process anything
269 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change (fasthash)", CURL::GetRedacted(strDirectory).c_str());
274 { // need to fetch the folder
275 CDirectory::GetDirectory(strDirectory, items, g_advancedSettings.m_videoExtensions);
278 GetPathHash(items, hash);
279 if (hash != dbHash && !hash.empty())
282 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '%s' as not in the database", CURL::GetRedacted(strDirectory).c_str());
284 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '%s' due to change (%s != %s)", CURL::GetRedacted(strDirectory).c_str(), dbHash.c_str(), hash.c_str());
287 { // they're the same or the hash is empty (dir empty/dir not retrievable)
288 if (hash.empty() && !dbHash.empty())
290 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' as it's empty or doesn't exist - adding to clean list", CURL::GetRedacted(strDirectory).c_str());
291 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
294 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change", CURL::GetRedacted(strDirectory).c_str());
297 OnDirectoryScanned(strDirectory);
299 // update the hash to a fast hash if needed
300 if (CanFastHash(items) && !fastHash.empty())
304 else if (content == CONTENT_TVSHOWS)
307 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name().c_str()));
309 if (foundDirectly && !settings.parent_name_root)
311 CDirectory::GetDirectory(strDirectory, items, g_advancedSettings.m_videoExtensions);
312 items.SetPath(strDirectory);
313 GetPathHash(items, hash);
315 if (!m_database.GetPathHash(strDirectory, dbHash) || dbHash != hash)
317 m_database.SetPathHash(strDirectory, hash);
325 CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
326 item->SetPath(strDirectory);
327 item->m_bIsFolder = true;
329 items.SetPath(URIUtils::GetParentPath(item->GetPath()));
335 if (RetrieveVideoInfo(items, settings.parent_name_root, content))
337 if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
339 m_database.SetPathHash(strDirectory, hash);
340 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
341 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir %s", CURL::GetRedacted(strDirectory).c_str());
346 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
347 CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir %s", CURL::GetRedacted(strDirectory).c_str());
350 else if (hash != dbHash && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
351 { // update the hash either way - we may have changed the hash to a fast version
352 m_database.SetPathHash(strDirectory, hash);
356 OnDirectoryScanned(strDirectory);
358 for (int i = 0; i < items.Size(); ++i)
360 CFileItemPtr pItem = items[i];
365 // if we have a directory item (non-playlist) we then recurse into that folder
366 // do not recurse for tv shows - we have already looked recursively for episodes
367 if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList() && settings.recurse > 0 && content != CONTENT_TVSHOWS)
369 if (!DoScan(pItem->GetPath()))
378 bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
382 if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
384 pDlgProgress->ShowProgressBar(true);
385 pDlgProgress->SetPercentage(0);
388 pDlgProgress->ShowProgressBar(false);
390 pDlgProgress->Progress();
395 bool FoundSomeInfo = false;
396 vector<int> seenPaths;
397 for (int i = 0; i < (int)items.Size(); ++i)
400 CFileItemPtr pItem = items[i];
402 // we do this since we may have a override per dir
403 ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath());
407 // Discard all exclude files defined by regExExclude
408 if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
409 : g_advancedSettings.m_moviesExcludeFromScanRegExps))
412 if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
415 m_handle->SetPercentage(i*100.f/items.Size());
418 // clear our scraper cache
421 INFO_RET ret = INFO_CANCELLED;
422 if (info2->Content() == CONTENT_TVSHOWS)
423 ret = RetrieveInfoForTvShow(pItem.get(), bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress);
424 else if (info2->Content() == CONTENT_MOVIES)
425 ret = RetrieveInfoForMovie(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
426 else if (info2->Content() == CONTENT_MUSICVIDEOS)
427 ret = RetrieveInfoForMusicVideo(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
430 CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type %d (%s)", info2->Content(), CURL::GetRedacted(pItem->GetPath()).c_str());
431 FoundSomeInfo = false;
434 if (ret == INFO_CANCELLED || ret == INFO_ERROR)
436 FoundSomeInfo = false;
439 if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
440 FoundSomeInfo = true;
441 else if (ret == INFO_NOT_FOUND)
443 CLog::Log(LOGWARNING, "No information found for item '%s', it won't be added to the library.", CURL::GetRedacted(pItem->GetPath()).c_str());
448 // Keep track of directories we've seen
449 if (pItem->m_bIsFolder)
450 seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
453 if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
455 vector< pair<int,string> > libPaths;
456 m_database.GetSubPaths(items.GetPath(), libPaths);
457 for (vector< pair<int,string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
459 if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
460 m_pathsToClean.insert(i->first);
464 pDlgProgress->ShowProgressBar(false);
466 g_infoManager.ResetLibraryBools();
468 return FoundSomeInfo;
471 INFO_RET CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
474 if (pItem->m_bIsFolder)
475 idTvShow = m_database.GetTvShowId(pItem->GetPath());
478 CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
479 idTvShow = m_database.GetTvShowId(strPath);
481 if (idTvShow > -1 && (fetchEpisodes || !pItem->m_bIsFolder))
483 INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
484 if (ret == INFO_ADDED)
485 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
489 if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361, pItem->GetLabel()))
490 return INFO_CANCELLED;
493 m_handle->SetText(pItem->GetMovieName(bDirNames));
495 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
499 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
500 if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
501 { // check for preconfigured scraper; if found, overwrite with interpreted scraper (from Nfofile)
502 // but keep current scan settings
503 SScanSettings settings;
504 if (m_database.GetScraperForPath(pItem->GetPath(), settings))
505 m_database.SetScraperForPath(pItem->GetPath(), info2, settings);
507 if (result == CNfoFile::FULL_NFO)
509 pItem->GetVideoInfoTag()->Reset();
510 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
512 long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
517 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
518 if (ret == INFO_ADDED)
519 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
524 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
531 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
532 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
535 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
537 if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
542 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
543 if (ret == INFO_ADDED)
544 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
549 INFO_RET CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
551 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
552 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
553 return INFO_NOT_NEEDED;
555 if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
556 return INFO_CANCELLED;
558 if (m_database.HasMovieInfo(pItem->GetPath()))
559 return INFO_HAVE_ALREADY;
562 m_handle->SetText(pItem->GetMovieName(bDirNames));
564 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
568 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
569 if (result == CNfoFile::FULL_NFO)
571 pItem->GetVideoInfoTag()->Reset();
572 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
574 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
578 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
585 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
586 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
588 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
590 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
594 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
595 return INFO_NOT_FOUND;
598 INFO_RET CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
600 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
601 (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
602 return INFO_NOT_NEEDED;
604 if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
605 return INFO_CANCELLED;
607 if (m_database.HasMusicVideoInfo(pItem->GetPath()))
608 return INFO_HAVE_ALREADY;
611 m_handle->SetText(pItem->GetMovieName(bDirNames));
613 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
617 result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
618 if (result == CNfoFile::FULL_NFO)
620 pItem->GetVideoInfoTag()->Reset();
621 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
623 if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
627 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
634 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
635 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
637 if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
639 if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
643 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
644 return INFO_NOT_FOUND;
647 INFO_RET CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item, long showID, const ADDON::ScraperPtr &scraper, bool useLocal, CGUIDialogProgress *progress)
649 // enumerate episodes
651 EnumerateSeriesFolder(item, files);
652 if (files.size() == 0) // no update or no files
653 return INFO_NOT_NEEDED;
655 if (m_bStop || (progress && progress->IsCanceled()))
656 return INFO_CANCELLED;
658 CVideoInfoTag showInfo;
659 m_database.GetTvShowInfo("", showInfo, showID);
660 return OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
663 void CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
667 if (item->m_bIsFolder)
669 CUtil::GetRecursiveListing(item->GetPath(), items, g_advancedSettings.m_videoExtensions, true);
670 CStdString hash, dbHash;
671 int numFilesInFolder = GetPathHash(items, hash);
673 if (m_database.GetPathHash(item->GetPath(), dbHash) && dbHash == hash)
675 m_currentItem += numFilesInFolder;
677 // update our dialog with our progress
681 m_handle->SetPercentage(m_currentItem*100.f/m_itemCount);
683 OnDirectoryScanned(item->GetPath());
687 m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
688 m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
689 item->SetProperty("hash", hash);
693 CFileItemPtr newItem(new CFileItem(*item));
698 stack down any dvd folders
699 need to sort using the full path since this is a collapsed recursive listing of all subdirs
700 video_ts.ifo files should sort at the top of a dvd folder in ascending order
702 /foo/bar/video_ts.ifo
707 // since we're doing this now anyway, should other items be stacked?
708 items.Sort(SortByPath, SortOrderAscending);
710 while (x < items.Size())
712 if (items[x]->m_bIsFolder)
716 CStdString strPathX, strFileX;
717 URIUtils::Split(items[x]->GetPath(), strPathX, strFileX);
718 //CLog::Log(LOGDEBUG,"%i:%s:%s", x, strPathX.c_str(), strFileX.c_str());
721 if (strFileX.Equals("VIDEO_TS.IFO"))
723 while (y < items.Size())
725 CStdString strPathY, strFileY;
726 URIUtils::Split(items[y]->GetPath(), strPathY, strFileY);
727 //CLog::Log(LOGDEBUG," %i:%s:%s", y, strPathY.c_str(), strFileY.c_str());
729 if (strPathY.Equals(strPathX))
731 remove everything sorted below the video_ts.ifo file in the same path.
732 understandbly this wont stack correctly if there are other files in the the dvd folder.
733 this should be unlikely and thus is being ignored for now but we can monitor the
734 where the path changes and potentially remove the items above the video_ts.ifo file.
745 CStdStringArray regexps = g_advancedSettings.m_tvshowExcludeFromScanRegExps;
747 for (int i=0;i<items.Size();++i)
749 if (items[i]->m_bIsFolder)
751 CStdString strPath = URIUtils::GetDirectory(items[i]->GetPath());
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", CURL::GetRedacted(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.empty())
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);
871 for (unsigned int i=0;i<expression.size();++i)
873 CRegExp reg(true, CRegExp::autoUtf8);
874 if (!reg.RegComp(expression[i].regexp))
877 int regexppos, regexp2pos;
878 //CLog::Log(LOGDEBUG,"running expression %s on %s",expression[i].regexp.c_str(),strLabel.c_str());
879 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
883 episode.strPath = item->GetPath();
884 episode.iSeason = -1;
885 episode.iEpisode = -1;
886 episode.cDate.SetValid(false);
887 episode.isFolder = false;
889 bool byDate = expression[i].byDate ? true : false;
890 int defaultSeason = expression[i].defaultSeason;
894 if (!GetAirDateFromRegExp(reg, episode))
897 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match %s (%s) [%s]", strLabel.c_str(),
898 episode.cDate.GetAsLocalizedDate().c_str(), expression[i].regexp.c_str());
902 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
905 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match %s (s%ie%i) [%s]", strLabel.c_str(),
906 episode.iSeason, episode.iEpisode, expression[i].regexp.c_str());
909 // Grab the remainder from first regexp run
910 // as second run might modify or empty it.
911 std::string remainder(reg.GetMatch(3));
914 * Check if the files base path is a dedicated folder that contains
915 * only this single episode. If season and episode match with the
916 * actual media file, we set episode.isFolder to true.
918 CStdString strBasePath = item->GetBaseMoviePath(true);
919 URIUtils::RemoveSlashAtEnd(strBasePath);
920 strBasePath = URIUtils::GetFileName(strBasePath);
922 if (reg.RegFind(strBasePath.c_str()) > -1)
927 GetAirDateFromRegExp(reg, parent);
928 if (episode.cDate == parent.cDate)
929 episode.isFolder = true;
933 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
934 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
935 episode.isFolder = true;
939 // add what we found by now
940 episodeList.push_back(episode);
942 CRegExp reg2(true, CRegExp::autoUtf8);
943 // check the remainder of the string for any further episodes.
944 if (!byDate && reg2.RegComp(g_advancedSettings.m_tvshowMultiPartEnumRegExp))
948 // we want "long circuit" OR below so that both offsets are evaluated
949 while (((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) | ((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
951 if (((regexppos <= regexp2pos) && regexppos != -1) ||
952 (regexppos >= 0 && regexp2pos == -1))
954 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
956 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season %u, multipart episode %u [%s]",
957 episode.iSeason, episode.iEpisode,
958 g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
960 episodeList.push_back(episode);
961 remainder = reg.GetMatch(3);
964 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
965 (regexp2pos >= 0 && regexppos == -1))
967 episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
968 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode %u [%s]",
969 episode.iEpisode, g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
970 episodeList.push_back(episode);
971 offset += regexp2pos + reg2.GetFindLen();
980 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp ®, EPISODE &episodeInfo, int defaultSeason)
982 std::string season(reg.GetMatch(1));
983 std::string episode(reg.GetMatch(2));
985 if (!season.empty() || !episode.empty())
988 if (season.empty() && !episode.empty())
989 { // no season specified -> assume defaultSeason
990 episodeInfo.iSeason = defaultSeason;
991 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
992 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
994 else if (!season.empty() && episode.empty())
995 { // no episode specification -> assume defaultSeason
996 episodeInfo.iSeason = defaultSeason;
997 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
998 episodeInfo.iEpisode = atoi(season.c_str());
1001 { // season and episode specified
1002 episodeInfo.iSeason = atoi(season.c_str());
1003 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1007 if (isalpha(*endptr))
1008 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1009 else if (*endptr == '.')
1010 episodeInfo.iSubepisode = atoi(endptr+1);
1017 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp ®, EPISODE &episodeInfo)
1019 std::string param1(reg.GetMatch(1));
1020 std::string param2(reg.GetMatch(2));
1021 std::string param3(reg.GetMatch(3));
1023 if (!param1.empty() && !param2.empty() && !param3.empty())
1025 // regular expression by date
1026 int len1 = param1.size();
1027 int len2 = param2.size();
1028 int len3 = param3.size();
1030 if (len1==4 && len2==2 && len3==2)
1032 // yyyy mm dd format
1033 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1035 else if (len1==2 && len2==2 && len3==4)
1037 // mm dd yyyy format
1038 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1041 return episodeInfo.cDate.IsValid();
1044 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1046 // ensure our database is open (this can get called via other classes)
1047 if (!m_database.Open())
1051 GetArtwork(pItem, content, videoFolder, useLocal, showInfo ? showInfo->m_strPath : "");
1053 // ensure the art map isn't completely empty by specifying an empty thumb
1054 map<string, string> art = pItem->GetArt();
1058 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1059 if (movieDetails.m_basePath.empty())
1060 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1061 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1063 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1065 if (pItem->m_bIsFolder)
1066 movieDetails.m_strPath = pItem->GetPath();
1068 CStdString strTitle(movieDetails.m_strTitle);
1070 if (showInfo && content == CONTENT_TVSHOWS)
1072 strTitle = StringUtils::Format("%s - %ix%i - %s", showInfo->m_strTitle.c_str(), movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle.c_str());
1075 std::string redactPath = pItem->GetPath();
1076 CURL::Decode(redactPath);
1077 redactPath = CURL::GetRedacted(redactPath);
1079 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to %s:%s", TranslateContent(content).c_str(), redactPath.c_str());
1082 if (content == CONTENT_MOVIES)
1084 // find local trailer first
1085 CStdString strTrailer = pItem->FindTrailer();
1086 if (!strTrailer.empty())
1087 movieDetails.m_strTrailer = strTrailer;
1089 lResult = m_database.SetDetailsForMovie(pItem->GetPath(), movieDetails, art);
1090 movieDetails.m_iDbId = lResult;
1091 movieDetails.m_type = "movie";
1093 // setup links to shows if the linked shows are in the db
1094 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1096 CFileItemList items;
1097 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1099 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1101 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie %s to show %s", movieDetails.m_strTitle.c_str(), movieDetails.m_showLink[i].c_str());
1104 else if (content == CONTENT_TVSHOWS)
1106 if (pItem->m_bIsFolder)
1108 map<int, map<string, string> > seasonArt;
1110 { // get and cache season thumbs
1111 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes("season"), useLocal);
1112 for (map<int, map<string, string> >::iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
1113 for (map<string, string>::iterator j = i->second.begin(); j != i->second.end(); ++j)
1114 CTextureCache::Get().BackgroundCacheImage(j->second);
1116 lResult = m_database.SetDetailsForTvShow(pItem->GetPath(), movieDetails, art, seasonArt);
1117 movieDetails.m_iDbId = lResult;
1118 movieDetails.m_type = "tvshow";
1122 // we add episode then set details, as otherwise set details will delete the
1123 // episode then add, which breaks multi-episode files.
1124 int idShow = showInfo ? showInfo->m_iDbId : -1;
1125 int idEpisode = m_database.AddEpisode(idShow, pItem->GetPath());
1126 lResult = m_database.SetDetailsForEpisode(pItem->GetPath(), movieDetails, art, idShow, idEpisode);
1127 movieDetails.m_iDbId = lResult;
1128 movieDetails.m_type = "episode";
1129 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1130 if (movieDetails.m_fEpBookmark > 0)
1132 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1134 bookmark.timeInSeconds = movieDetails.m_fEpBookmark;
1135 bookmark.seasonNumber = movieDetails.m_iSeason;
1136 bookmark.episodeNumber = movieDetails.m_iEpisode;
1137 m_database.AddBookMarkForEpisode(movieDetails, bookmark);
1141 else if (content == CONTENT_MUSICVIDEOS)
1143 lResult = m_database.SetDetailsForMusicVideo(pItem->GetPath(), movieDetails, art);
1144 movieDetails.m_iDbId = lResult;
1145 movieDetails.m_type = "musicvideo";
1148 if (g_advancedSettings.m_bVideoLibraryImportWatchedState || libraryImport)
1149 m_database.SetPlayCount(*pItem, movieDetails.m_playCount, movieDetails.m_lastPlayed);
1151 if ((g_advancedSettings.m_bVideoLibraryImportResumePoint || libraryImport) &&
1152 movieDetails.m_resumePoint.IsSet())
1153 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.m_resumePoint, CBookmark::RESUME);
1157 CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
1158 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", itemCopy);
1162 string ContentToMediaType(CONTENT_TYPE content, bool folder)
1166 case CONTENT_MOVIES:
1168 case CONTENT_MUSICVIDEOS:
1169 return "musicvideo";
1170 case CONTENT_TVSHOWS:
1171 return folder ? "tvshow" : "episode";
1177 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1179 std::string type = "thumb";
1180 if (width*5 < height*4)
1182 else if (width*1 > height*4)
1187 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1189 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1190 movieDetails.m_fanart.Unpack();
1191 movieDetails.m_strPictureURL.Parse();
1193 CGUIListItem::ArtMap art = pItem->GetArt();
1195 // get and cache thumb images
1196 vector<string> artTypes = CVideoThumbLoader::GetArtTypes(ContentToMediaType(content, pItem->m_bIsFolder));
1197 vector<string>::iterator i = find(artTypes.begin(), artTypes.end(), "fanart");
1198 if (i != artTypes.end())
1199 artTypes.erase(i); // fanart is handled below
1200 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end() &&
1201 art.find("thumb") == art.end();
1205 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1207 if (art.find(*i) == art.end())
1209 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, *i, bApplyToDir);
1211 art.insert(make_pair(*i, image));
1214 // find and classify the local thumb (backcompat) if available
1217 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, "thumb", bApplyToDir);
1219 { // cache the image and determine sizing
1220 CTextureDetails details;
1221 if (CTextureCache::Get().CacheImage(image, details))
1223 std::string type = GetArtTypeFromSize(details.width, details.height);
1224 if (art.find(type) == art.end())
1225 art.insert(make_pair(type, image));
1232 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1234 if (art.find(*i) == art.end())
1236 std::string image = GetImage(pItem, false, bApplyToDir, *i);
1238 art.insert(make_pair(*i, image));
1242 // use the first piece of online art as the first art type if no thumb type is available yet
1243 if (art.empty() && lookForThumb)
1245 std::string image = GetImage(pItem, false, bApplyToDir, "thumb");
1247 art.insert(make_pair(artTypes.front(), image));
1250 // get & save fanart image (treated separately due to it being stored in m_fanart)
1251 bool isEpisode = (content == CONTENT_TVSHOWS && !pItem->m_bIsFolder);
1252 if (!isEpisode && art.find("fanart") == art.end())
1254 string fanart = GetFanart(pItem, useLocal);
1255 if (!fanart.empty())
1256 art.insert(make_pair("fanart", fanart));
1259 for (CGUIListItem::ArtMap::const_iterator i = art.begin(); i != art.end(); ++i)
1260 CTextureCache::Get().BackgroundCacheImage(i->second);
1264 // parent folder to apply the thumb to and to search for local actor thumbs
1265 CStdString parentDir = GetParentDir(*pItem);
1266 if (CSettings::Get().GetBool("videolibrary.actorthumbs"))
1267 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1269 ApplyThumbToFolder(parentDir, art["thumb"]);
1272 std::string CVideoInfoScanner::GetImage(CFileItem *pItem, bool useLocal, bool bApplyToDir, const std::string &type)
1276 thumb = CVideoThumbLoader::GetLocalArt(*pItem, type, bApplyToDir);
1280 thumb = CScraperUrl::GetThumbURL(pItem->GetVideoInfoTag()->m_strPictureURL.GetFirstThumb(type));
1283 if (thumb.find("http://") == string::npos &&
1284 thumb.find("/") == string::npos &&
1285 thumb.find("\\") == string::npos)
1287 CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
1288 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1295 std::string CVideoInfoScanner::GetFanart(CFileItem *pItem, bool useLocal)
1297 std::string fanart = pItem->GetArt("fanart");
1298 if (fanart.empty() && useLocal)
1299 fanart = pItem->FindLocalArt("fanart.jpg", true);
1301 fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1305 INFO_RET CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress /* = NULL */)
1309 pDlgProgress->SetLine(1, showInfo.m_strTitle);
1310 pDlgProgress->SetLine(2, 20361);
1311 pDlgProgress->SetPercentage(0);
1312 pDlgProgress->ShowProgressBar(true);
1313 pDlgProgress->Progress();
1316 EPISODELIST episodes;
1317 bool hasEpisodeGuide = false;
1319 int iMax = files.size();
1321 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1323 m_nfoReader.Close();
1326 pDlgProgress->SetLine(2, 20361);
1327 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1328 pDlgProgress->Progress();
1331 m_handle->SetPercentage(100.f*iCurr++/iMax);
1333 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1334 return INFO_CANCELLED;
1336 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1339 m_handle->SetText(g_localizeStrings.Get(20415));
1344 item.SetPath(file->strPath);
1346 // handle .nfo files
1347 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1349 ScraperPtr info(scraper);
1350 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1352 result = CheckForNFOFile(&item, false, info,scrUrl);
1353 if (result == CNfoFile::FULL_NFO)
1355 m_nfoReader.GetDetails(*item.GetVideoInfoTag());
1356 // override with episode and season number from file if available
1357 if (file->iEpisode > -1)
1359 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1360 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1362 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1367 if (!hasEpisodeGuide)
1369 // fetch episode guide
1370 if (!showInfo.m_strEpisodeGuide.empty())
1373 url.ParseEpisodeGuide(showInfo.m_strEpisodeGuide);
1377 pDlgProgress->SetLine(2, 20354);
1378 pDlgProgress->Progress();
1381 CVideoInfoDownloader imdb(scraper);
1382 if (!imdb.GetEpisodeList(url, episodes))
1383 return INFO_NOT_FOUND;
1385 hasEpisodeGuide = true;
1389 if (episodes.empty())
1391 CLog::Log(LOGERROR, "VideoInfoScanner: Asked to lookup episode %s"
1392 " online, but we have no episode guide. Check your tvshow.nfo and make"
1393 " sure the <episodeguide> tag is in place.", file->strPath.c_str());
1397 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
1398 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
1399 bool bFound = false;
1400 EPISODELIST::iterator guide = episodes.begin();;
1401 EPISODELIST matches;
1403 for (; guide != episodes.end(); ++guide )
1405 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
1412 else if ((file->iSubepisode!=0) && (backupkey==*guide))
1414 matches.push_back(*guide);
1418 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1420 matches.push_back(*guide);
1423 if (!guide->cScraperUrl.strTitle.empty() && StringUtils::EqualsNoCase(guide->cScraperUrl.strTitle, file->strTitle))
1433 * If there is only one match or there are matches but no title to compare with to help
1434 * identify the best match, then pick the first match as the best possible candidate.
1436 * Otherwise, use the title to further refine the best match.
1438 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
1440 guide = matches.begin();
1443 else if (!file->strTitle.empty())
1445 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
1447 EPISODELIST *candidates;
1448 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1450 minscore = 0.8; // 80% should ensure a good match.
1451 candidates = &episodes;
1453 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1454 candidates = &matches;
1456 CStdStringArray titles;
1457 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
1459 StringUtils::ToLower(guide->cScraperUrl.strTitle);
1460 titles.push_back(guide->cScraperUrl.strTitle);
1464 std::string loweredTitle(file->strTitle);
1465 StringUtils::ToLower(loweredTitle);
1466 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
1467 if (matchscore >= minscore)
1469 guide = candidates->begin() + index;
1471 CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1472 __FUNCTION__, showInfo.m_strTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1479 CVideoInfoDownloader imdb(scraper);
1481 item.SetPath(file->strPath);
1482 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1483 return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1485 // Only set season/epnum from filename when it is not already set by a scraper
1486 if (item.GetVideoInfoTag()->m_iSeason == -1)
1487 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
1488 if (item.GetVideoInfoTag()->m_iEpisode == -1)
1489 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
1491 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
1496 CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d.%d, airdate: '%s', title: '%s'",
1497 __FUNCTION__, showInfo.m_strTitle.c_str(), file->iSeason, file->iEpisode, file->iSubepisode,
1498 file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1504 CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1507 // Find a matching .nfo file
1508 if (!item->m_bIsFolder)
1510 if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1512 CFileItem item2(*item);
1513 CURL url(item->GetPath());
1514 CStdString strPath = URIUtils::GetDirectory(url.GetHostName());
1515 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1516 return GetnfoFile(&item2, bGrabAny);
1519 // grab the folder path
1520 CStdString strPath = URIUtils::GetDirectory(item->GetPath());
1522 if (bGrabAny && !item->IsStack())
1523 { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1524 nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1525 if (CFile::Exists(nfoFile))
1529 // try looking for .nfo file for a stacked item
1530 if (item->IsStack())
1532 // first try .nfo file matching first file in stack
1533 CStackDirectory dir;
1534 CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1536 item2.SetPath(firstFile);
1537 nfoFile = GetnfoFile(&item2, bGrabAny);
1538 // else try .nfo file matching stacked title
1539 if (nfoFile.empty())
1541 CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1542 item2.SetPath(stackedTitlePath);
1543 nfoFile = GetnfoFile(&item2, bGrabAny);
1548 // already an .nfo file?
1549 if (URIUtils::HasExtension(item->GetPath(), ".nfo"))
1550 nfoFile = item->GetPath();
1551 // no, create .nfo file
1553 nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1556 // test file existence
1557 if (!nfoFile.empty() && !CFile::Exists(nfoFile))
1560 if (nfoFile.empty()) // final attempt - strip off any cd1 folders
1562 URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1564 if (StringUtils::EndsWithNoCase(strPath, "cd1"))
1566 strPath.erase(strPath.size() - 3);
1567 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1568 return GetnfoFile(&item2, bGrabAny);
1572 if (nfoFile.empty() && item->IsOpticalMediaFile())
1574 CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1575 nfoFile = GetnfoFile(&parentDirectory, true);
1578 // folders (or stacked dvds) can take any nfo file if there's a unique one
1579 if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.empty()))
1581 // see if there is a unique nfo file in this folder, and if so, use that
1582 CFileItemList items;
1585 if (item->m_bIsFolder)
1586 strPath = item->GetPath();
1588 strPath = URIUtils::GetDirectory(item->GetPath());
1590 if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1593 for (int i = 0; i < items.Size(); i++)
1595 if (items[i]->IsNFO())
1607 return items[numNFO]->GetPath();
1614 bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1616 CVideoInfoTag movieDetails;
1618 if (m_handle && !url.strTitle.empty())
1619 m_handle->SetText(url.strTitle);
1621 CVideoInfoDownloader imdb(scraper);
1622 bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1627 nfoFile->GetDetails(movieDetails,NULL,true);
1629 if (m_handle && url.strTitle.empty())
1630 m_handle->SetText(movieDetails.m_strTitle);
1634 pDialog->SetLine(1, movieDetails.m_strTitle);
1635 pDialog->Progress();
1638 *pItem->GetVideoInfoTag() = movieDetails;
1641 return false; // no info found, or cancelled
1644 void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1646 // copy icon to folder also;
1647 if (!imdbThumb.empty())
1649 CFileItem folderItem(folder, true);
1650 CThumbLoader loader;
1651 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
1655 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1657 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1658 if (0 == items.Size()) return 0;
1659 XBMC::XBMC_MD5 md5state;
1661 for (int i = 0; i < items.Size(); ++i)
1663 const CFileItemPtr pItem = items[i];
1664 md5state.append(pItem->GetPath());
1665 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1666 FILETIME time = pItem->m_dateTime;
1667 md5state.append((unsigned char *)&time, sizeof(FILETIME));
1668 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1671 md5state.getDigest(hash);
1675 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1677 // TODO: Probably should account for excluded folders here (eg samples), though that then
1678 // introduces possible problems if the user then changes the exclude regexps and
1679 // expects excluded folders that are inside a fast-hashed folder to then be picked
1680 // up. The chances that the user has a folder which contains only excluded folders
1681 // where some of those folders should be scanned recursively is pretty small.
1682 return items.GetFolderCount() == 0;
1685 CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1687 struct __stat64 buffer;
1688 if (XFILE::CFile::Stat(directory, &buffer) == 0)
1690 int64_t time = buffer.st_mtime;
1692 time = buffer.st_ctime;
1694 return StringUtils::Format("fast%"PRId64, time);
1699 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, map<int, map<string, string> > &seasonArt, const vector<string> &artTypes, bool useLocal)
1701 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end();
1703 // find the maximum number of seasons we have thumbs for (local + remote)
1704 int maxSeasons = show.m_strPictureURL.GetMaxSeasonThumb();
1706 CFileItemList items;
1707 CDirectory::GetDirectory(show.m_strPath, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
1709 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z]+)?\\.(tbn|jpg|png)"))
1711 for (int i = 0; i < items.Size(); i++)
1713 CStdString name = URIUtils::GetFileName(items[i]->GetPath());
1714 if (reg.RegFind(name) > -1)
1716 int season = atoi(reg.GetMatch(1).c_str());
1717 if (season > maxSeasons)
1718 maxSeasons = season;
1722 for (int season = -1; season <= maxSeasons; season++)
1724 map<string, string> art;
1729 basePath = "season-all";
1730 else if (season == 0)
1731 basePath = "season-specials";
1733 basePath = StringUtils::Format("season%02i", season);
1734 CFileItem artItem(URIUtils::AddFileToFolder(show.m_strPath, basePath), false);
1736 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1738 std::string image = CVideoThumbLoader::GetLocalArt(artItem, *i, false);
1740 art.insert(make_pair(*i, image));
1742 // find and classify the local thumb (backcompat) if available
1745 std::string image = CVideoThumbLoader::GetLocalArt(artItem, "thumb", false);
1747 { // cache the image and determine sizing
1748 CTextureDetails details;
1749 if (CTextureCache::Get().CacheImage(image, details))
1751 std::string type = GetArtTypeFromSize(details.width, details.height);
1752 if (art.find(type) == art.end())
1753 art.insert(make_pair(type, image));
1760 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1762 if (art.find(*i) == art.end())
1764 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, *i));
1766 art.insert(make_pair(*i, image));
1769 // use the first piece of online art as the first art type if no thumb type is available yet
1770 if (art.empty() && lookForThumb)
1772 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, "thumb"));
1774 art.insert(make_pair(artTypes.front(), image));
1777 seasonArt.insert(make_pair(season, art));
1781 void CVideoInfoScanner::FetchActorThumbs(vector<SActorInfo>& actors, const CStdString& strPath)
1783 CFileItemList items;
1784 CStdString actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
1785 if (CDirectory::Exists(actorsDir))
1786 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
1787 DIR_FLAG_NO_FILE_INFO);
1788 for (vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
1790 if (i->thumb.empty())
1792 CStdString thumbFile = i->strName;
1793 StringUtils::Replace(thumbFile, ' ', '_');
1794 for (int j = 0; j < items.Size(); j++)
1796 CStdString compare = URIUtils::GetFileName(items[j]->GetPath());
1797 URIUtils::RemoveExtension(compare);
1798 if (!items[j]->m_bIsFolder && compare == thumbFile)
1800 i->thumb = items[j]->GetPath();
1804 if (i->thumb.empty() && !i->thumbUrl.GetFirstThumb().m_url.empty())
1805 i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1806 if (!i->thumb.empty())
1807 CTextureCache::Get().BackgroundCacheImage(i->thumb);
1812 CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1814 CStdString strNfoFile;
1815 if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1816 || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1817 strNfoFile = GetnfoFile(pItem, bGrabAny);
1818 if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1819 strNfoFile = URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo");
1821 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1822 if (!strNfoFile.empty() && CFile::Exists(strNfoFile))
1824 if (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder)
1825 result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1827 result = m_nfoReader.Create(strNfoFile,info);
1832 case CNfoFile::COMBINED_NFO:
1835 case CNfoFile::FULL_NFO:
1838 case CNfoFile::URL_NFO:
1841 case CNfoFile::NO_NFO:
1847 if (result != CNfoFile::NO_NFO)
1848 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), CURL::GetRedacted(strNfoFile).c_str());
1849 if (result == CNfoFile::FULL_NFO)
1851 if (info->Content() == CONTENT_TVSHOWS)
1852 info = m_nfoReader.GetScraperInfo();
1854 else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1856 scrUrl = m_nfoReader.ScraperUrl();
1857 info = m_nfoReader.GetScraperInfo();
1859 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1860 scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1862 if (result == CNfoFile::COMBINED_NFO)
1863 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1867 CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", CURL::GetRedacted(pItem->GetPath()).c_str());
1872 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1874 if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1879 CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1882 return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1885 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1889 progress->SetHeading(heading);
1890 progress->SetLine(0, line1);
1891 progress->SetLine(2, "");
1892 progress->Progress();
1893 return progress->IsCanceled();
1898 int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1900 MOVIELIST movielist;
1901 CVideoInfoDownloader imdb(scraper);
1902 int returncode = imdb.FindMovie(videoName, movielist, progress);
1903 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
1904 { // scraper reported an error, or we had an error and user wants to cancel the scan
1906 return -1; // cancelled
1908 if (returncode > 0 && movielist.size())
1911 return 1; // found a movie
1913 return 0; // didn't find anything
1916 CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1918 CStdString strCheck = item.GetPath();
1920 strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1922 CStdString strDirectory = URIUtils::GetDirectory(strCheck);
1923 if (URIUtils::IsInRAR(strCheck))
1925 CStdString strPath=strDirectory;
1926 URIUtils::GetParentPath(strPath, strDirectory);
1930 strCheck = strDirectory;
1931 URIUtils::RemoveSlashAtEnd(strCheck);
1932 if (URIUtils::GetFileName(strCheck).size() == 3 && StringUtils::StartsWithNoCase(URIUtils::GetFileName(strCheck), "cd"))
1933 strDirectory = URIUtils::GetDirectory(strCheck);
1935 return strDirectory;