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))
770 CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file %s", CURL::GetRedacted(CURL::Decode(items[i]->GetPath())).c_str());
774 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
776 if (!item->HasVideoInfoTag())
779 const CVideoInfoTag* tag = item->GetVideoInfoTag();
781 * First check the season and episode number. This takes precedence over the original air
782 * date and episode title. Must be a valid season and episode number combination.
784 if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
787 episode.strPath = item->GetPath();
788 episode.iSeason = tag->m_iSeason;
789 episode.iEpisode = tag->m_iEpisode;
790 episode.isFolder = false;
791 episodeList.push_back(episode);
792 CLog::Log(LOGDEBUG, "%s - found match for: %s. Season %d, Episode %d", __FUNCTION__,
793 episode.strPath.c_str(), episode.iSeason, episode.iEpisode);
798 * Next preference is the first aired date. If it exists use that for matching the TV Show
799 * information. Also set the title in case there are multiple matches for the first aired date.
801 if (tag->m_firstAired.IsValid())
804 episode.strPath = item->GetPath();
805 episode.strTitle = tag->m_strTitle;
806 episode.isFolder = false;
808 * Set season and episode to -1 to indicate to use the aired date.
810 episode.iSeason = -1;
811 episode.iEpisode = -1;
813 * The first aired date string must be parseable.
815 episode.cDate = item->GetVideoInfoTag()->m_firstAired;
816 episodeList.push_back(episode);
817 CLog::Log(LOGDEBUG, "%s - found match for: '%s', firstAired: '%s' = '%s', title: '%s'",
818 __FUNCTION__, episode.strPath.c_str(), tag->m_firstAired.GetAsDBDateTime().c_str(),
819 episode.cDate.GetAsLocalizedDate().c_str(), episode.strTitle.c_str());
824 * Next preference is the episode title. If it exists use that for matching the TV Show
827 if (!tag->m_strTitle.empty())
830 episode.strPath = item->GetPath();
831 episode.strTitle = tag->m_strTitle;
832 episode.isFolder = false;
834 * Set season and episode to -1 to indicate to use the title.
836 episode.iSeason = -1;
837 episode.iEpisode = -1;
838 episodeList.push_back(episode);
839 CLog::Log(LOGDEBUG,"%s - found match for: '%s', title: '%s'", __FUNCTION__,
840 episode.strPath.c_str(), episode.strTitle.c_str());
845 * There is no further episode information available if both the season and episode number have
846 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
847 * to the episode list.
849 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
851 CLog::Log(LOGDEBUG,"%s - found exclusion match for: %s. Both Season and Episode are 0. Item will be ignored for scanning.",
852 __FUNCTION__, item->GetPath().c_str());
859 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
861 SETTINGS_TVSHOWLIST expression = g_advancedSettings.m_tvshowEnumRegExps;
863 CStdString strLabel=item->GetPath();
864 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
865 strLabel = CURL::Decode(strLabel);
867 for (unsigned int i=0;i<expression.size();++i)
869 CRegExp reg(true, CRegExp::autoUtf8);
870 if (!reg.RegComp(expression[i].regexp))
873 int regexppos, regexp2pos;
874 //CLog::Log(LOGDEBUG,"running expression %s on %s",expression[i].regexp.c_str(),strLabel.c_str());
875 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
879 episode.strPath = item->GetPath();
880 episode.iSeason = -1;
881 episode.iEpisode = -1;
882 episode.cDate.SetValid(false);
883 episode.isFolder = false;
885 bool byDate = expression[i].byDate ? true : false;
886 int defaultSeason = expression[i].defaultSeason;
890 if (!GetAirDateFromRegExp(reg, episode))
893 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match %s (%s) [%s]", strLabel.c_str(),
894 episode.cDate.GetAsLocalizedDate().c_str(), expression[i].regexp.c_str());
898 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
901 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match %s (s%ie%i) [%s]", strLabel.c_str(),
902 episode.iSeason, episode.iEpisode, expression[i].regexp.c_str());
905 // Grab the remainder from first regexp run
906 // as second run might modify or empty it.
907 std::string remainder(reg.GetMatch(3));
910 * Check if the files base path is a dedicated folder that contains
911 * only this single episode. If season and episode match with the
912 * actual media file, we set episode.isFolder to true.
914 CStdString strBasePath = item->GetBaseMoviePath(true);
915 URIUtils::RemoveSlashAtEnd(strBasePath);
916 strBasePath = URIUtils::GetFileName(strBasePath);
918 if (reg.RegFind(strBasePath.c_str()) > -1)
923 GetAirDateFromRegExp(reg, parent);
924 if (episode.cDate == parent.cDate)
925 episode.isFolder = true;
929 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
930 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
931 episode.isFolder = true;
935 // add what we found by now
936 episodeList.push_back(episode);
938 CRegExp reg2(true, CRegExp::autoUtf8);
939 // check the remainder of the string for any further episodes.
940 if (!byDate && reg2.RegComp(g_advancedSettings.m_tvshowMultiPartEnumRegExp))
944 // we want "long circuit" OR below so that both offsets are evaluated
945 while (((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) | ((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
947 if (((regexppos <= regexp2pos) && regexppos != -1) ||
948 (regexppos >= 0 && regexp2pos == -1))
950 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
952 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season %u, multipart episode %u [%s]",
953 episode.iSeason, episode.iEpisode,
954 g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
956 episodeList.push_back(episode);
957 remainder = reg.GetMatch(3);
960 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
961 (regexp2pos >= 0 && regexppos == -1))
963 episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
964 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode %u [%s]",
965 episode.iEpisode, g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
966 episodeList.push_back(episode);
967 offset += regexp2pos + reg2.GetFindLen();
976 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp ®, EPISODE &episodeInfo, int defaultSeason)
978 std::string season(reg.GetMatch(1));
979 std::string episode(reg.GetMatch(2));
981 if (!season.empty() || !episode.empty())
984 if (season.empty() && !episode.empty())
985 { // no season specified -> assume defaultSeason
986 episodeInfo.iSeason = defaultSeason;
987 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
988 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
990 else if (!season.empty() && episode.empty())
991 { // no episode specification -> assume defaultSeason
992 episodeInfo.iSeason = defaultSeason;
993 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
994 episodeInfo.iEpisode = atoi(season.c_str());
997 { // season and episode specified
998 episodeInfo.iSeason = atoi(season.c_str());
999 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1003 if (isalpha(*endptr))
1004 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1005 else if (*endptr == '.')
1006 episodeInfo.iSubepisode = atoi(endptr+1);
1013 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp ®, EPISODE &episodeInfo)
1015 std::string param1(reg.GetMatch(1));
1016 std::string param2(reg.GetMatch(2));
1017 std::string param3(reg.GetMatch(3));
1019 if (!param1.empty() && !param2.empty() && !param3.empty())
1021 // regular expression by date
1022 int len1 = param1.size();
1023 int len2 = param2.size();
1024 int len3 = param3.size();
1026 if (len1==4 && len2==2 && len3==2)
1028 // yyyy mm dd format
1029 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1031 else if (len1==2 && len2==2 && len3==4)
1033 // mm dd yyyy format
1034 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1037 return episodeInfo.cDate.IsValid();
1040 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1042 // ensure our database is open (this can get called via other classes)
1043 if (!m_database.Open())
1047 GetArtwork(pItem, content, videoFolder, useLocal, showInfo ? showInfo->m_strPath : "");
1049 // ensure the art map isn't completely empty by specifying an empty thumb
1050 map<string, string> art = pItem->GetArt();
1054 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1055 if (movieDetails.m_basePath.empty())
1056 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1057 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1059 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1061 if (pItem->m_bIsFolder)
1062 movieDetails.m_strPath = pItem->GetPath();
1064 CStdString strTitle(movieDetails.m_strTitle);
1066 if (showInfo && content == CONTENT_TVSHOWS)
1068 strTitle = StringUtils::Format("%s - %ix%i - %s", showInfo->m_strTitle.c_str(), movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle.c_str());
1071 std::string redactPath(CURL::GetRedacted(CURL::Decode(pItem->GetPath())));
1073 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to %s:%s", TranslateContent(content).c_str(), redactPath.c_str());
1076 if (content == CONTENT_MOVIES)
1078 // find local trailer first
1079 CStdString strTrailer = pItem->FindTrailer();
1080 if (!strTrailer.empty())
1081 movieDetails.m_strTrailer = strTrailer;
1083 lResult = m_database.SetDetailsForMovie(pItem->GetPath(), movieDetails, art);
1084 movieDetails.m_iDbId = lResult;
1085 movieDetails.m_type = "movie";
1087 // setup links to shows if the linked shows are in the db
1088 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1090 CFileItemList items;
1091 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1093 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1095 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie %s to show %s", movieDetails.m_strTitle.c_str(), movieDetails.m_showLink[i].c_str());
1098 else if (content == CONTENT_TVSHOWS)
1100 if (pItem->m_bIsFolder)
1102 map<int, map<string, string> > seasonArt;
1104 { // get and cache season thumbs
1105 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes("season"), useLocal);
1106 for (map<int, map<string, string> >::iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
1107 for (map<string, string>::iterator j = i->second.begin(); j != i->second.end(); ++j)
1108 CTextureCache::Get().BackgroundCacheImage(j->second);
1110 lResult = m_database.SetDetailsForTvShow(pItem->GetPath(), movieDetails, art, seasonArt);
1111 movieDetails.m_iDbId = lResult;
1112 movieDetails.m_type = "tvshow";
1116 // we add episode then set details, as otherwise set details will delete the
1117 // episode then add, which breaks multi-episode files.
1118 int idShow = showInfo ? showInfo->m_iDbId : -1;
1119 int idEpisode = m_database.AddEpisode(idShow, pItem->GetPath());
1120 lResult = m_database.SetDetailsForEpisode(pItem->GetPath(), movieDetails, art, idShow, idEpisode);
1121 movieDetails.m_iDbId = lResult;
1122 movieDetails.m_type = "episode";
1123 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1124 if (movieDetails.m_fEpBookmark > 0)
1126 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1128 bookmark.timeInSeconds = movieDetails.m_fEpBookmark;
1129 bookmark.seasonNumber = movieDetails.m_iSeason;
1130 bookmark.episodeNumber = movieDetails.m_iEpisode;
1131 m_database.AddBookMarkForEpisode(movieDetails, bookmark);
1135 else if (content == CONTENT_MUSICVIDEOS)
1137 lResult = m_database.SetDetailsForMusicVideo(pItem->GetPath(), movieDetails, art);
1138 movieDetails.m_iDbId = lResult;
1139 movieDetails.m_type = "musicvideo";
1142 if (g_advancedSettings.m_bVideoLibraryImportWatchedState || libraryImport)
1143 m_database.SetPlayCount(*pItem, movieDetails.m_playCount, movieDetails.m_lastPlayed);
1145 if ((g_advancedSettings.m_bVideoLibraryImportResumePoint || libraryImport) &&
1146 movieDetails.m_resumePoint.IsSet())
1147 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.m_resumePoint, CBookmark::RESUME);
1151 CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
1152 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", itemCopy);
1156 string ContentToMediaType(CONTENT_TYPE content, bool folder)
1160 case CONTENT_MOVIES:
1162 case CONTENT_MUSICVIDEOS:
1163 return "musicvideo";
1164 case CONTENT_TVSHOWS:
1165 return folder ? "tvshow" : "episode";
1171 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1173 std::string type = "thumb";
1174 if (width*5 < height*4)
1176 else if (width*1 > height*4)
1181 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1183 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1184 movieDetails.m_fanart.Unpack();
1185 movieDetails.m_strPictureURL.Parse();
1187 CGUIListItem::ArtMap art = pItem->GetArt();
1189 // get and cache thumb images
1190 vector<string> artTypes = CVideoThumbLoader::GetArtTypes(ContentToMediaType(content, pItem->m_bIsFolder));
1191 vector<string>::iterator i = find(artTypes.begin(), artTypes.end(), "fanart");
1192 if (i != artTypes.end())
1193 artTypes.erase(i); // fanart is handled below
1194 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end() &&
1195 art.find("thumb") == art.end();
1199 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1201 if (art.find(*i) == art.end())
1203 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, *i, bApplyToDir);
1205 art.insert(make_pair(*i, image));
1208 // find and classify the local thumb (backcompat) if available
1211 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, "thumb", bApplyToDir);
1213 { // cache the image and determine sizing
1214 CTextureDetails details;
1215 if (CTextureCache::Get().CacheImage(image, details))
1217 std::string type = GetArtTypeFromSize(details.width, details.height);
1218 if (art.find(type) == art.end())
1219 art.insert(make_pair(type, image));
1226 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1228 if (art.find(*i) == art.end())
1230 std::string image = GetImage(pItem, false, bApplyToDir, *i);
1232 art.insert(make_pair(*i, image));
1236 // use the first piece of online art as the first art type if no thumb type is available yet
1237 if (art.empty() && lookForThumb)
1239 std::string image = GetImage(pItem, false, bApplyToDir, "thumb");
1241 art.insert(make_pair(artTypes.front(), image));
1244 // get & save fanart image (treated separately due to it being stored in m_fanart)
1245 bool isEpisode = (content == CONTENT_TVSHOWS && !pItem->m_bIsFolder);
1246 if (!isEpisode && art.find("fanart") == art.end())
1248 string fanart = GetFanart(pItem, useLocal);
1249 if (!fanart.empty())
1250 art.insert(make_pair("fanart", fanart));
1253 for (CGUIListItem::ArtMap::const_iterator i = art.begin(); i != art.end(); ++i)
1254 CTextureCache::Get().BackgroundCacheImage(i->second);
1258 // parent folder to apply the thumb to and to search for local actor thumbs
1259 CStdString parentDir = GetParentDir(*pItem);
1260 if (CSettings::Get().GetBool("videolibrary.actorthumbs"))
1261 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1263 ApplyThumbToFolder(parentDir, art["thumb"]);
1266 std::string CVideoInfoScanner::GetImage(CFileItem *pItem, bool useLocal, bool bApplyToDir, const std::string &type)
1270 thumb = CVideoThumbLoader::GetLocalArt(*pItem, type, bApplyToDir);
1274 thumb = CScraperUrl::GetThumbURL(pItem->GetVideoInfoTag()->m_strPictureURL.GetFirstThumb(type));
1277 if (thumb.find("http://") == string::npos &&
1278 thumb.find("/") == string::npos &&
1279 thumb.find("\\") == string::npos)
1281 CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
1282 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1289 std::string CVideoInfoScanner::GetFanart(CFileItem *pItem, bool useLocal)
1293 std::string fanart = pItem->GetArt("fanart");
1294 if (fanart.empty() && useLocal)
1295 fanart = pItem->FindLocalArt("fanart.jpg", true);
1297 fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1301 INFO_RET CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress /* = NULL */)
1305 pDlgProgress->SetLine(1, showInfo.m_strTitle);
1306 pDlgProgress->SetLine(2, 20361);
1307 pDlgProgress->SetPercentage(0);
1308 pDlgProgress->ShowProgressBar(true);
1309 pDlgProgress->Progress();
1312 EPISODELIST episodes;
1313 bool hasEpisodeGuide = false;
1315 int iMax = files.size();
1317 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1319 m_nfoReader.Close();
1322 pDlgProgress->SetLine(2, 20361);
1323 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1324 pDlgProgress->Progress();
1327 m_handle->SetPercentage(100.f*iCurr++/iMax);
1329 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1330 return INFO_CANCELLED;
1332 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1335 m_handle->SetText(g_localizeStrings.Get(20415));
1340 item.SetPath(file->strPath);
1342 // handle .nfo files
1343 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1345 ScraperPtr info(scraper);
1346 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1348 result = CheckForNFOFile(&item, false, info,scrUrl);
1349 if (result == CNfoFile::FULL_NFO)
1351 m_nfoReader.GetDetails(*item.GetVideoInfoTag());
1352 // override with episode and season number from file if available
1353 if (file->iEpisode > -1)
1355 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1356 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1358 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1363 if (!hasEpisodeGuide)
1365 // fetch episode guide
1366 if (!showInfo.m_strEpisodeGuide.empty())
1369 url.ParseEpisodeGuide(showInfo.m_strEpisodeGuide);
1373 pDlgProgress->SetLine(2, 20354);
1374 pDlgProgress->Progress();
1377 CVideoInfoDownloader imdb(scraper);
1378 if (!imdb.GetEpisodeList(url, episodes))
1379 return INFO_NOT_FOUND;
1381 hasEpisodeGuide = true;
1385 if (episodes.empty())
1387 CLog::Log(LOGERROR, "VideoInfoScanner: Asked to lookup episode %s"
1388 " online, but we have no episode guide. Check your tvshow.nfo and make"
1389 " sure the <episodeguide> tag is in place.", file->strPath.c_str());
1393 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
1394 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
1395 bool bFound = false;
1396 EPISODELIST::iterator guide = episodes.begin();;
1397 EPISODELIST matches;
1399 for (; guide != episodes.end(); ++guide )
1401 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
1408 else if ((file->iSubepisode!=0) && (backupkey==*guide))
1410 matches.push_back(*guide);
1414 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1416 matches.push_back(*guide);
1419 if (!guide->cScraperUrl.strTitle.empty() && StringUtils::EqualsNoCase(guide->cScraperUrl.strTitle, file->strTitle))
1429 * If there is only one match or there are matches but no title to compare with to help
1430 * identify the best match, then pick the first match as the best possible candidate.
1432 * Otherwise, use the title to further refine the best match.
1434 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
1436 guide = matches.begin();
1439 else if (!file->strTitle.empty())
1441 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
1443 EPISODELIST *candidates;
1444 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1446 minscore = 0.8; // 80% should ensure a good match.
1447 candidates = &episodes;
1449 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1450 candidates = &matches;
1452 CStdStringArray titles;
1453 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
1455 StringUtils::ToLower(guide->cScraperUrl.strTitle);
1456 titles.push_back(guide->cScraperUrl.strTitle);
1460 std::string loweredTitle(file->strTitle);
1461 StringUtils::ToLower(loweredTitle);
1462 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
1463 if (matchscore >= minscore)
1465 guide = candidates->begin() + index;
1467 CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1468 __FUNCTION__, showInfo.m_strTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1475 CVideoInfoDownloader imdb(scraper);
1477 item.SetPath(file->strPath);
1478 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1479 return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1481 // Only set season/epnum from filename when it is not already set by a scraper
1482 if (item.GetVideoInfoTag()->m_iSeason == -1)
1483 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
1484 if (item.GetVideoInfoTag()->m_iEpisode == -1)
1485 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
1487 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
1492 CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d.%d, airdate: '%s', title: '%s'",
1493 __FUNCTION__, showInfo.m_strTitle.c_str(), file->iSeason, file->iEpisode, file->iSubepisode,
1494 file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1500 CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1503 // Find a matching .nfo file
1504 if (!item->m_bIsFolder)
1506 if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1508 CFileItem item2(*item);
1509 CURL url(item->GetPath());
1510 CStdString strPath = URIUtils::GetDirectory(url.GetHostName());
1511 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1512 return GetnfoFile(&item2, bGrabAny);
1515 // grab the folder path
1516 CStdString strPath = URIUtils::GetDirectory(item->GetPath());
1518 if (bGrabAny && !item->IsStack())
1519 { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1520 nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1521 if (CFile::Exists(nfoFile))
1525 // try looking for .nfo file for a stacked item
1526 if (item->IsStack())
1528 // first try .nfo file matching first file in stack
1529 CStackDirectory dir;
1530 CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1532 item2.SetPath(firstFile);
1533 nfoFile = GetnfoFile(&item2, bGrabAny);
1534 // else try .nfo file matching stacked title
1535 if (nfoFile.empty())
1537 CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1538 item2.SetPath(stackedTitlePath);
1539 nfoFile = GetnfoFile(&item2, bGrabAny);
1544 // already an .nfo file?
1545 if (URIUtils::HasExtension(item->GetPath(), ".nfo"))
1546 nfoFile = item->GetPath();
1547 // no, create .nfo file
1549 nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1552 // test file existence
1553 if (!nfoFile.empty() && !CFile::Exists(nfoFile))
1556 if (nfoFile.empty()) // final attempt - strip off any cd1 folders
1558 URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1560 if (StringUtils::EndsWithNoCase(strPath, "cd1"))
1562 strPath.erase(strPath.size() - 3);
1563 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1564 return GetnfoFile(&item2, bGrabAny);
1568 if (nfoFile.empty() && item->IsOpticalMediaFile())
1570 CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1571 nfoFile = GetnfoFile(&parentDirectory, true);
1574 // folders (or stacked dvds) can take any nfo file if there's a unique one
1575 if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.empty()))
1577 // see if there is a unique nfo file in this folder, and if so, use that
1578 CFileItemList items;
1581 if (item->m_bIsFolder)
1582 strPath = item->GetPath();
1584 strPath = URIUtils::GetDirectory(item->GetPath());
1586 if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1589 for (int i = 0; i < items.Size(); i++)
1591 if (items[i]->IsNFO())
1603 return items[numNFO]->GetPath();
1610 bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1612 CVideoInfoTag movieDetails;
1614 if (m_handle && !url.strTitle.empty())
1615 m_handle->SetText(url.strTitle);
1617 CVideoInfoDownloader imdb(scraper);
1618 bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1623 nfoFile->GetDetails(movieDetails,NULL,true);
1625 if (m_handle && url.strTitle.empty())
1626 m_handle->SetText(movieDetails.m_strTitle);
1630 pDialog->SetLine(1, movieDetails.m_strTitle);
1631 pDialog->Progress();
1634 *pItem->GetVideoInfoTag() = movieDetails;
1637 return false; // no info found, or cancelled
1640 void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1642 // copy icon to folder also;
1643 if (!imdbThumb.empty())
1645 CFileItem folderItem(folder, true);
1646 CThumbLoader loader;
1647 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
1651 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1653 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1654 if (0 == items.Size()) return 0;
1655 XBMC::XBMC_MD5 md5state;
1657 for (int i = 0; i < items.Size(); ++i)
1659 const CFileItemPtr pItem = items[i];
1660 md5state.append(pItem->GetPath());
1661 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1662 FILETIME time = pItem->m_dateTime;
1663 md5state.append((unsigned char *)&time, sizeof(FILETIME));
1664 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1667 md5state.getDigest(hash);
1671 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1673 // TODO: Probably should account for excluded folders here (eg samples), though that then
1674 // introduces possible problems if the user then changes the exclude regexps and
1675 // expects excluded folders that are inside a fast-hashed folder to then be picked
1676 // up. The chances that the user has a folder which contains only excluded folders
1677 // where some of those folders should be scanned recursively is pretty small.
1678 return items.GetFolderCount() == 0;
1681 CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1683 struct __stat64 buffer;
1684 if (XFILE::CFile::Stat(directory, &buffer) == 0)
1686 int64_t time = buffer.st_mtime;
1688 time = buffer.st_ctime;
1690 return StringUtils::Format("fast%"PRId64, time);
1695 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, map<int, map<string, string> > &seasonArt, const vector<string> &artTypes, bool useLocal)
1697 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end();
1699 // find the maximum number of seasons we have thumbs for (local + remote)
1700 int maxSeasons = show.m_strPictureURL.GetMaxSeasonThumb();
1702 CFileItemList items;
1703 CDirectory::GetDirectory(show.m_strPath, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
1705 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z]+)?\\.(tbn|jpg|png)"))
1707 for (int i = 0; i < items.Size(); i++)
1709 CStdString name = URIUtils::GetFileName(items[i]->GetPath());
1710 if (reg.RegFind(name) > -1)
1712 int season = atoi(reg.GetMatch(1).c_str());
1713 if (season > maxSeasons)
1714 maxSeasons = season;
1718 for (int season = -1; season <= maxSeasons; season++)
1720 map<string, string> art;
1725 basePath = "season-all";
1726 else if (season == 0)
1727 basePath = "season-specials";
1729 basePath = StringUtils::Format("season%02i", season);
1730 CFileItem artItem(URIUtils::AddFileToFolder(show.m_strPath, basePath), false);
1732 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1734 std::string image = CVideoThumbLoader::GetLocalArt(artItem, *i, false);
1736 art.insert(make_pair(*i, image));
1738 // find and classify the local thumb (backcompat) if available
1741 std::string image = CVideoThumbLoader::GetLocalArt(artItem, "thumb", false);
1743 { // cache the image and determine sizing
1744 CTextureDetails details;
1745 if (CTextureCache::Get().CacheImage(image, details))
1747 std::string type = GetArtTypeFromSize(details.width, details.height);
1748 if (art.find(type) == art.end())
1749 art.insert(make_pair(type, image));
1756 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1758 if (art.find(*i) == art.end())
1760 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, *i));
1762 art.insert(make_pair(*i, image));
1765 // use the first piece of online art as the first art type if no thumb type is available yet
1766 if (art.empty() && lookForThumb)
1768 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, "thumb"));
1770 art.insert(make_pair(artTypes.front(), image));
1773 seasonArt.insert(make_pair(season, art));
1777 void CVideoInfoScanner::FetchActorThumbs(vector<SActorInfo>& actors, const CStdString& strPath)
1779 CFileItemList items;
1780 CStdString actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
1781 if (CDirectory::Exists(actorsDir))
1782 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
1783 DIR_FLAG_NO_FILE_INFO);
1784 for (vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
1786 if (i->thumb.empty())
1788 CStdString thumbFile = i->strName;
1789 StringUtils::Replace(thumbFile, ' ', '_');
1790 for (int j = 0; j < items.Size(); j++)
1792 CStdString compare = URIUtils::GetFileName(items[j]->GetPath());
1793 URIUtils::RemoveExtension(compare);
1794 if (!items[j]->m_bIsFolder && compare == thumbFile)
1796 i->thumb = items[j]->GetPath();
1800 if (i->thumb.empty() && !i->thumbUrl.GetFirstThumb().m_url.empty())
1801 i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1802 if (!i->thumb.empty())
1803 CTextureCache::Get().BackgroundCacheImage(i->thumb);
1808 CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1810 CStdString strNfoFile;
1811 if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1812 || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1813 strNfoFile = GetnfoFile(pItem, bGrabAny);
1814 if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1815 strNfoFile = URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo");
1817 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1818 if (!strNfoFile.empty() && CFile::Exists(strNfoFile))
1820 if (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder)
1821 result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1823 result = m_nfoReader.Create(strNfoFile,info);
1828 case CNfoFile::COMBINED_NFO:
1831 case CNfoFile::FULL_NFO:
1834 case CNfoFile::URL_NFO:
1837 case CNfoFile::NO_NFO:
1843 if (result != CNfoFile::NO_NFO)
1844 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), CURL::GetRedacted(strNfoFile).c_str());
1845 if (result == CNfoFile::FULL_NFO)
1847 if (info->Content() == CONTENT_TVSHOWS)
1848 info = m_nfoReader.GetScraperInfo();
1850 else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1852 scrUrl = m_nfoReader.ScraperUrl();
1853 info = m_nfoReader.GetScraperInfo();
1855 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1856 scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1858 if (result == CNfoFile::COMBINED_NFO)
1859 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1863 CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", CURL::GetRedacted(pItem->GetPath()).c_str());
1868 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1870 if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1875 CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1878 return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1881 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1885 progress->SetHeading(heading);
1886 progress->SetLine(0, line1);
1887 progress->SetLine(2, "");
1888 progress->Progress();
1889 return progress->IsCanceled();
1894 int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1896 MOVIELIST movielist;
1897 CVideoInfoDownloader imdb(scraper);
1898 int returncode = imdb.FindMovie(videoName, movielist, progress);
1899 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
1900 { // scraper reported an error, or we had an error and user wants to cancel the scan
1902 return -1; // cancelled
1904 if (returncode > 0 && movielist.size())
1907 return 1; // found a movie
1909 return 0; // didn't find anything
1912 CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1914 CStdString strCheck = item.GetPath();
1916 strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1918 CStdString strDirectory = URIUtils::GetDirectory(strCheck);
1919 if (URIUtils::IsInRAR(strCheck))
1921 CStdString strPath=strDirectory;
1922 URIUtils::GetParentPath(strPath, strDirectory);
1926 strCheck = strDirectory;
1927 URIUtils::RemoveSlashAtEnd(strCheck);
1928 if (URIUtils::GetFileName(strCheck).size() == 3 && StringUtils::StartsWithNoCase(URIUtils::GetFileName(strCheck), "cd"))
1929 strDirectory = URIUtils::GetDirectory(strCheck);
1931 return strDirectory;