2 * Copyright (C) 2005-2013 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
21 #include "threads/SystemClock.h"
23 #include "VideoInfoScanner.h"
24 #include "addons/AddonManager.h"
25 #include "filesystem/DirectoryCache.h"
28 #include "utils/RegExp.h"
29 #include "utils/md5.h"
30 #include "filesystem/StackDirectory.h"
31 #include "VideoInfoDownloader.h"
32 #include "GUIInfoManager.h"
33 #include "filesystem/File.h"
34 #include "dialogs/GUIDialogExtendedProgressBar.h"
35 #include "dialogs/GUIDialogProgress.h"
36 #include "dialogs/GUIDialogYesNo.h"
37 #include "dialogs/GUIDialogOK.h"
38 #include "interfaces/AnnouncementManager.h"
39 #include "settings/AdvancedSettings.h"
40 #include "settings/Settings.h"
41 #include "utils/StringUtils.h"
42 #include "guilib/LocalizeStrings.h"
43 #include "guilib/GUIWindowManager.h"
44 #include "utils/TimeUtils.h"
45 #include "utils/log.h"
46 #include "utils/URIUtils.h"
47 #include "utils/Variant.h"
48 #include "video/VideoThumbLoader.h"
49 #include "TextureCache.h"
50 #include "GUIUserMessages.h"
54 using namespace XFILE;
55 using namespace ADDON;
60 CVideoInfoScanner::CVideoInfoScanner() : CThread("VideoInfoScanner")
65 m_bCanInterrupt = false;
72 CVideoInfoScanner::~CVideoInfoScanner()
76 void CVideoInfoScanner::Process()
80 unsigned int tick = XbmcThreads::SystemClockMillis();
84 if (m_showDialog && !CSettings::Get().GetBool("videolibrary.backgroundupdate"))
86 CGUIDialogExtendedProgressBar* dialog =
87 (CGUIDialogExtendedProgressBar*)g_windowManager.GetWindow(WINDOW_DIALOG_EXT_PROGRESS);
89 m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
92 m_bCanInterrupt = true;
94 CLog::Log(LOGNOTICE, "VideoInfoScanner: Starting scan ..");
95 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanStarted");
97 // Reset progress vars
101 SetPriority(GetMinPriority());
103 // Database operations should not be canceled
104 // using Interupt() while scanning as it could
105 // result in unexpected behaviour.
106 m_bCanInterrupt = false;
108 bool bCancelled = false;
109 while (!bCancelled && m_pathsToScan.size())
112 * A copy of the directory path is used because the path supplied is
113 * immediately removed from the m_pathsToScan set in DoScan(). If the
114 * reference points to the entry in the set a null reference error
117 CStdString directory = *m_pathsToScan.begin();
118 if (!CDirectory::Exists(directory))
121 * Note that this will skip clean (if m_bClean is enabled) if the directory really
122 * doesn't exist rather than a NAS being switched off. A manual clean from settings
123 * will still pick up and remove it though.
125 CLog::Log(LOGWARNING, "%s directory '%s' does not exist - skipping scan%s.", __FUNCTION__, directory.c_str(), m_bClean ? " and clean" : "");
126 m_pathsToScan.erase(m_pathsToScan.begin());
128 else if (!DoScan(directory))
135 CleanDatabase(m_handle,&m_pathsToClean, false);
139 m_handle->SetTitle(g_localizeStrings.Get(331));
140 m_database.Compress(false);
146 tick = XbmcThreads::SystemClockMillis() - tick;
147 CLog::Log(LOGNOTICE, "VideoInfoScanner: Finished scan. Scanning for video info took %s", StringUtils::SecondsToTimeString(tick / 1000).c_str());
151 CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
155 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanFinished");
157 // we need to clear the videodb cache and update any active lists
158 CUtil::DeleteVideoDatabaseDirectoryCache();
159 CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0);
160 g_windowManager.SendThreadMessage(msg);
163 m_handle->MarkFinished();
167 void CVideoInfoScanner::Start(const CStdString& strDirectory, bool scanAll)
169 m_strStartDir = strDirectory;
171 m_pathsToScan.clear();
172 m_pathsToClean.clear();
174 if (strDirectory.IsEmpty())
175 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
178 m_database.GetPaths(m_pathsToScan);
183 m_pathsToScan.insert(strDirectory);
185 m_bClean = g_advancedSettings.m_bVideoLibraryCleanOnUpdate;
192 bool CVideoInfoScanner::IsScanning()
197 void CVideoInfoScanner::Stop()
200 m_database.Interupt();
205 void CVideoInfoScanner::CleanDatabase(CGUIDialogProgressBarHandle* handle /*= NULL */, const set<int>* paths /*= NULL */, bool showProgress /*= true */)
209 m_database.CleanDatabase(handle, paths, showProgress);
214 static void OnDirectoryScanned(const CStdString& strDirectory)
216 CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
217 msg.SetStringParam(strDirectory);
218 g_windowManager.SendThreadMessage(msg);
221 bool CVideoInfoScanner::DoScan(const CStdString& strDirectory)
225 m_handle->SetText(g_localizeStrings.Get(20415));
229 * Remove this path from the list we're processing. This must be done prior to
230 * the check for file or folder exclusion to prevent an infinite while loop
233 set<CStdString>::iterator it = m_pathsToScan.find(strDirectory);
234 if (it != m_pathsToScan.end())
235 m_pathsToScan.erase(it);
239 bool foundDirectly = false;
242 SScanSettings settings;
243 ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
244 CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
246 // exclude folders that match our exclude regexps
247 CStdStringArray regexps = content == CONTENT_TVSHOWS ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
248 : g_advancedSettings.m_moviesExcludeFromScanRegExps;
250 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
253 bool ignoreFolder = !m_scanAll && settings.noupdate;
254 if (content == CONTENT_NONE || ignoreFolder)
257 CStdString hash, dbHash;
258 if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
262 int str = content == CONTENT_MOVIES ? 20317:20318;
263 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name().c_str()));
266 CStdString fastHash = GetFastHash(strDirectory);
267 if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.IsEmpty() && fastHash == dbHash)
268 { // fast hashes match - no need to process anything
269 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change (fasthash)", 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.IsEmpty())
281 if (dbHash.IsEmpty())
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.IsEmpty() && !dbHash.IsEmpty())
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.IsEmpty())
304 else if (content == CONTENT_TVSHOWS)
307 m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name().c_str()));
309 if (foundDirectly && !settings.parent_name_root)
311 CDirectory::GetDirectory(strDirectory, items, g_advancedSettings.m_videoExtensions);
312 items.SetPath(strDirectory);
313 GetPathHash(items, hash);
315 if (!m_database.GetPathHash(strDirectory, dbHash) || dbHash != hash)
317 m_database.SetPathHash(strDirectory, hash);
325 CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
326 item->SetPath(strDirectory);
327 item->m_bIsFolder = true;
329 items.SetPath(URIUtils::GetParentPath(item->GetPath()));
335 if (RetrieveVideoInfo(items, settings.parent_name_root, content))
337 if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
339 m_database.SetPathHash(strDirectory, hash);
340 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
341 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir %s", 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.IsEmpty())
834 episode.strPath = item->GetPath();
835 episode.strTitle = tag->m_strTitle;
836 episode.isFolder = false;
838 * Set season and episode to -1 to indicate to use the title.
840 episode.iSeason = -1;
841 episode.iEpisode = -1;
842 episodeList.push_back(episode);
843 CLog::Log(LOGDEBUG,"%s - found match for: '%s', title: '%s'", __FUNCTION__,
844 episode.strPath.c_str(), episode.strTitle.c_str());
849 * There is no further episode information available if both the season and episode number have
850 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
851 * to the episode list.
853 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
855 CLog::Log(LOGDEBUG,"%s - found exclusion match for: %s. Both Season and Episode are 0. Item will be ignored for scanning.",
856 __FUNCTION__, item->GetPath().c_str());
863 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
865 SETTINGS_TVSHOWLIST expression = g_advancedSettings.m_tvshowEnumRegExps;
867 CStdString strLabel=item->GetPath();
868 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
869 CURL::Decode(strLabel);
870 strLabel.MakeLower();
872 for (unsigned int i=0;i<expression.size();++i)
874 CRegExp reg(true, true);
875 if (!reg.RegComp(expression[i].regexp))
878 int regexppos, regexp2pos;
879 //CLog::Log(LOGDEBUG,"running expression %s on %s",expression[i].regexp.c_str(),strLabel.c_str());
880 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
884 episode.strPath = item->GetPath();
885 episode.iSeason = -1;
886 episode.iEpisode = -1;
887 episode.cDate.SetValid(false);
888 episode.isFolder = false;
890 bool byDate = expression[i].byDate ? true : false;
891 int defaultSeason = expression[i].defaultSeason;
895 if (!GetAirDateFromRegExp(reg, episode))
898 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match %s (%s) [%s]", strLabel.c_str(),
899 episode.cDate.GetAsLocalizedDate().c_str(), expression[i].regexp.c_str());
903 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
906 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match %s (s%ie%i) [%s]", strLabel.c_str(),
907 episode.iSeason, episode.iEpisode, expression[i].regexp.c_str());
910 // Grab the remainder from first regexp run
911 // as second run might modify or empty it.
912 std::string remainder(reg.GetMatch(3));
915 * Check if the files base path is a dedicated folder that contains
916 * only this single episode. If season and episode match with the
917 * actual media file, we set episode.isFolder to true.
919 CStdString strBasePath = item->GetBaseMoviePath(true);
920 URIUtils::RemoveSlashAtEnd(strBasePath);
921 strBasePath = URIUtils::GetFileName(strBasePath);
923 if (reg.RegFind(strBasePath.c_str()) > -1)
928 GetAirDateFromRegExp(reg, parent);
929 if (episode.cDate == parent.cDate)
930 episode.isFolder = true;
934 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
935 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
936 episode.isFolder = true;
940 // add what we found by now
941 episodeList.push_back(episode);
943 CRegExp reg2(true, true);
944 // check the remainder of the string for any further episodes.
945 if (!byDate && reg2.RegComp(g_advancedSettings.m_tvshowMultiPartEnumRegExp))
949 // we want "long circuit" OR below so that both offsets are evaluated
950 while (((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) | ((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
952 if (((regexppos <= regexp2pos) && regexppos != -1) ||
953 (regexppos >= 0 && regexp2pos == -1))
955 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
957 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season %u, multipart episode %u [%s]",
958 episode.iSeason, episode.iEpisode,
959 g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
961 episodeList.push_back(episode);
962 remainder = reg.GetMatch(3);
965 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
966 (regexp2pos >= 0 && regexppos == -1))
968 episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
969 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode %u [%s]",
970 episode.iEpisode, g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
971 episodeList.push_back(episode);
972 offset += regexp2pos + reg2.GetFindLen();
981 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp ®, EPISODE &episodeInfo, int defaultSeason)
983 std::string season(reg.GetMatch(1));
984 std::string episode(reg.GetMatch(2));
986 if (!season.empty() || !episode.empty())
989 if (season.empty() && !episode.empty())
990 { // no season specified -> assume defaultSeason
991 episodeInfo.iSeason = defaultSeason;
992 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
993 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
995 else if (!season.empty() && episode.empty())
996 { // no episode specification -> assume defaultSeason
997 episodeInfo.iSeason = defaultSeason;
998 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
999 episodeInfo.iEpisode = atoi(season.c_str());
1002 { // season and episode specified
1003 episodeInfo.iSeason = atoi(season.c_str());
1004 episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1008 if (isalpha(*endptr))
1009 episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1010 else if (*endptr == '.')
1011 episodeInfo.iSubepisode = atoi(endptr+1);
1018 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp ®, EPISODE &episodeInfo)
1020 std::string param1(reg.GetMatch(1));
1021 std::string param2(reg.GetMatch(2));
1022 std::string param3(reg.GetMatch(3));
1024 if (!param1.empty() && !param2.empty() && !param3.empty())
1026 // regular expression by date
1027 int len1 = param1.size();
1028 int len2 = param2.size();
1029 int len3 = param3.size();
1031 if (len1==4 && len2==2 && len3==2)
1033 // yyyy mm dd format
1034 episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1036 else if (len1==2 && len2==2 && len3==4)
1038 // mm dd yyyy format
1039 episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1042 return episodeInfo.cDate.IsValid();
1045 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1047 // ensure our database is open (this can get called via other classes)
1048 if (!m_database.Open())
1052 GetArtwork(pItem, content, videoFolder, useLocal, showInfo ? showInfo->m_strPath : "");
1054 // ensure the art map isn't completely empty by specifying an empty thumb
1055 map<string, string> art = pItem->GetArt();
1059 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1060 if (movieDetails.m_basePath.IsEmpty())
1061 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1062 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1064 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1066 if (pItem->m_bIsFolder)
1067 movieDetails.m_strPath = pItem->GetPath();
1069 CStdString strTitle(movieDetails.m_strTitle);
1071 if (showInfo && content == CONTENT_TVSHOWS)
1073 strTitle = StringUtils::Format("%s - %ix%i - %s", showInfo->m_strTitle.c_str(), movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle.c_str());
1076 std::string redactPath = pItem->GetPath();
1077 CURL::Decode(redactPath);
1078 redactPath = CURL::GetRedacted(redactPath);
1080 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to %s:%s", TranslateContent(content).c_str(), redactPath.c_str());
1083 if (content == CONTENT_MOVIES)
1085 // find local trailer first
1086 CStdString strTrailer = pItem->FindTrailer();
1087 if (!strTrailer.IsEmpty())
1088 movieDetails.m_strTrailer = strTrailer;
1090 lResult = m_database.SetDetailsForMovie(pItem->GetPath(), movieDetails, art);
1091 movieDetails.m_iDbId = lResult;
1092 movieDetails.m_type = "movie";
1094 // setup links to shows if the linked shows are in the db
1095 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1097 CFileItemList items;
1098 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1100 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1102 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie %s to show %s", movieDetails.m_strTitle.c_str(), movieDetails.m_showLink[i].c_str());
1105 else if (content == CONTENT_TVSHOWS)
1107 if (pItem->m_bIsFolder)
1109 map<int, map<string, string> > seasonArt;
1111 { // get and cache season thumbs
1112 GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes("season"), useLocal);
1113 for (map<int, map<string, string> >::iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
1114 for (map<string, string>::iterator j = i->second.begin(); j != i->second.end(); ++j)
1115 CTextureCache::Get().BackgroundCacheImage(j->second);
1117 lResult = m_database.SetDetailsForTvShow(pItem->GetPath(), movieDetails, art, seasonArt);
1118 movieDetails.m_iDbId = lResult;
1119 movieDetails.m_type = "tvshow";
1123 // we add episode then set details, as otherwise set details will delete the
1124 // episode then add, which breaks multi-episode files.
1125 int idShow = showInfo ? showInfo->m_iDbId : -1;
1126 int idEpisode = m_database.AddEpisode(idShow, pItem->GetPath());
1127 lResult = m_database.SetDetailsForEpisode(pItem->GetPath(), movieDetails, art, idShow, idEpisode);
1128 movieDetails.m_iDbId = lResult;
1129 movieDetails.m_type = "episode";
1130 movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1131 if (movieDetails.m_fEpBookmark > 0)
1133 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1135 bookmark.timeInSeconds = movieDetails.m_fEpBookmark;
1136 bookmark.seasonNumber = movieDetails.m_iSeason;
1137 bookmark.episodeNumber = movieDetails.m_iEpisode;
1138 m_database.AddBookMarkForEpisode(movieDetails, bookmark);
1142 else if (content == CONTENT_MUSICVIDEOS)
1144 lResult = m_database.SetDetailsForMusicVideo(pItem->GetPath(), movieDetails, art);
1145 movieDetails.m_iDbId = lResult;
1146 movieDetails.m_type = "musicvideo";
1149 if (g_advancedSettings.m_bVideoLibraryImportWatchedState || libraryImport)
1150 m_database.SetPlayCount(*pItem, movieDetails.m_playCount, movieDetails.m_lastPlayed);
1152 if ((g_advancedSettings.m_bVideoLibraryImportResumePoint || libraryImport) &&
1153 movieDetails.m_resumePoint.IsSet())
1154 m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.m_resumePoint, CBookmark::RESUME);
1158 CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
1159 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", itemCopy);
1163 string ContentToMediaType(CONTENT_TYPE content, bool folder)
1167 case CONTENT_MOVIES:
1169 case CONTENT_MUSICVIDEOS:
1170 return "musicvideo";
1171 case CONTENT_TVSHOWS:
1172 return folder ? "tvshow" : "episode";
1178 std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1180 std::string type = "thumb";
1181 if (width*5 < height*4)
1183 else if (width*1 > height*4)
1188 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1190 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1191 movieDetails.m_fanart.Unpack();
1192 movieDetails.m_strPictureURL.Parse();
1194 CGUIListItem::ArtMap art = pItem->GetArt();
1196 // get and cache thumb images
1197 vector<string> artTypes = CVideoThumbLoader::GetArtTypes(ContentToMediaType(content, pItem->m_bIsFolder));
1198 vector<string>::iterator i = find(artTypes.begin(), artTypes.end(), "fanart");
1199 if (i != artTypes.end())
1200 artTypes.erase(i); // fanart is handled below
1201 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end() &&
1202 art.find("thumb") == art.end();
1206 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1208 if (art.find(*i) == art.end())
1210 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, *i, bApplyToDir);
1212 art.insert(make_pair(*i, image));
1215 // find and classify the local thumb (backcompat) if available
1218 std::string image = CVideoThumbLoader::GetLocalArt(*pItem, "thumb", bApplyToDir);
1220 { // cache the image and determine sizing
1221 CTextureDetails details;
1222 if (CTextureCache::Get().CacheImage(image, details))
1224 std::string type = GetArtTypeFromSize(details.width, details.height);
1225 if (art.find(type) == art.end())
1226 art.insert(make_pair(type, image));
1233 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1235 if (art.find(*i) == art.end())
1237 std::string image = GetImage(pItem, false, bApplyToDir, *i);
1239 art.insert(make_pair(*i, image));
1243 // use the first piece of online art as the first art type if no thumb type is available yet
1244 if (art.empty() && lookForThumb)
1246 std::string image = GetImage(pItem, false, bApplyToDir, "thumb");
1248 art.insert(make_pair(artTypes.front(), image));
1251 // get & save fanart image (treated separately due to it being stored in m_fanart)
1252 bool isEpisode = (content == CONTENT_TVSHOWS && !pItem->m_bIsFolder);
1253 if (!isEpisode && art.find("fanart") == art.end())
1255 string fanart = GetFanart(pItem, useLocal);
1256 if (!fanart.empty())
1257 art.insert(make_pair("fanart", fanart));
1260 for (CGUIListItem::ArtMap::const_iterator i = art.begin(); i != art.end(); ++i)
1261 CTextureCache::Get().BackgroundCacheImage(i->second);
1265 // parent folder to apply the thumb to and to search for local actor thumbs
1266 CStdString parentDir = GetParentDir(*pItem);
1267 if (CSettings::Get().GetBool("videolibrary.actorthumbs"))
1268 FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1270 ApplyThumbToFolder(parentDir, art["thumb"]);
1273 std::string CVideoInfoScanner::GetImage(CFileItem *pItem, bool useLocal, bool bApplyToDir, const std::string &type)
1277 thumb = CVideoThumbLoader::GetLocalArt(*pItem, type, bApplyToDir);
1281 thumb = CScraperUrl::GetThumbURL(pItem->GetVideoInfoTag()->m_strPictureURL.GetFirstThumb(type));
1284 if (thumb.find("http://") == string::npos &&
1285 thumb.find("/") == string::npos &&
1286 thumb.find("\\") == string::npos)
1288 CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
1289 thumb = URIUtils::AddFileToFolder(strPath, thumb);
1296 std::string CVideoInfoScanner::GetFanart(CFileItem *pItem, bool useLocal)
1298 std::string fanart = pItem->GetArt("fanart");
1299 if (fanart.empty() && useLocal)
1300 fanart = pItem->FindLocalArt("fanart.jpg", true);
1302 fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1306 INFO_RET CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress /* = NULL */)
1310 pDlgProgress->SetLine(1, showInfo.m_strTitle);
1311 pDlgProgress->SetLine(2, 20361);
1312 pDlgProgress->SetPercentage(0);
1313 pDlgProgress->ShowProgressBar(true);
1314 pDlgProgress->Progress();
1317 EPISODELIST episodes;
1318 bool hasEpisodeGuide = false;
1320 int iMax = files.size();
1322 for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1324 m_nfoReader.Close();
1327 pDlgProgress->SetLine(2, 20361);
1328 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1329 pDlgProgress->Progress();
1332 m_handle->SetPercentage(100.f*iCurr++/iMax);
1334 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1335 return INFO_CANCELLED;
1337 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1340 m_handle->SetText(g_localizeStrings.Get(20415));
1345 item.SetPath(file->strPath);
1347 // handle .nfo files
1348 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1350 ScraperPtr info(scraper);
1351 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1353 result = CheckForNFOFile(&item, false, info,scrUrl);
1354 if (result == CNfoFile::FULL_NFO)
1356 m_nfoReader.GetDetails(*item.GetVideoInfoTag());
1357 // override with episode and season number from file if available
1358 if (file->iEpisode > -1)
1360 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1361 item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1363 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1368 if (!hasEpisodeGuide)
1370 // fetch episode guide
1371 if (!showInfo.m_strEpisodeGuide.IsEmpty())
1374 url.ParseEpisodeGuide(showInfo.m_strEpisodeGuide);
1378 pDlgProgress->SetLine(2, 20354);
1379 pDlgProgress->Progress();
1382 CVideoInfoDownloader imdb(scraper);
1383 if (!imdb.GetEpisodeList(url, episodes))
1384 return INFO_NOT_FOUND;
1386 hasEpisodeGuide = true;
1390 if (episodes.empty())
1392 CLog::Log(LOGERROR, "VideoInfoScanner: Asked to lookup episode %s"
1393 " online, but we have no episode guide. Check your tvshow.nfo and make"
1394 " sure the <episodeguide> tag is in place.", file->strPath.c_str());
1398 EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
1399 EPISODE backupkey(file->iSeason, file->iEpisode, 0);
1400 bool bFound = false;
1401 EPISODELIST::iterator guide = episodes.begin();;
1402 EPISODELIST matches;
1404 for (; guide != episodes.end(); ++guide )
1406 if ((file->iEpisode!=-1) && (file->iSeason!=-1))
1413 else if ((file->iSubepisode!=0) && (backupkey==*guide))
1415 matches.push_back(*guide);
1419 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1421 matches.push_back(*guide);
1424 if (!guide->cScraperUrl.strTitle.IsEmpty() && guide->cScraperUrl.strTitle.CompareNoCase(file->strTitle.c_str()) == 0)
1434 * If there is only one match or there are matches but no title to compare with to help
1435 * identify the best match, then pick the first match as the best possible candidate.
1437 * Otherwise, use the title to further refine the best match.
1439 if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
1441 guide = matches.begin();
1444 else if (!file->strTitle.empty())
1446 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
1448 EPISODELIST *candidates;
1449 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1451 minscore = 0.8; // 80% should ensure a good match.
1452 candidates = &episodes;
1454 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1455 candidates = &matches;
1457 CStdStringArray titles;
1458 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
1459 titles.push_back(guide->cScraperUrl.strTitle.ToLower());
1462 std::string loweredTitle(file->strTitle);
1463 StringUtils::ToLower(loweredTitle);
1464 int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
1465 if (matchscore >= minscore)
1467 guide = candidates->begin() + index;
1469 CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1470 __FUNCTION__, showInfo.m_strTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1477 CVideoInfoDownloader imdb(scraper);
1479 item.SetPath(file->strPath);
1480 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1481 return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1483 // Only set season/epnum from filename when it is not already set by a scraper
1484 if (item.GetVideoInfoTag()->m_iSeason == -1)
1485 item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
1486 if (item.GetVideoInfoTag()->m_iEpisode == -1)
1487 item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
1489 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
1494 CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d.%d, airdate: '%s', title: '%s'",
1495 __FUNCTION__, showInfo.m_strTitle.c_str(), file->iSeason, file->iEpisode, file->iSubepisode,
1496 file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1502 CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1505 // Find a matching .nfo file
1506 if (!item->m_bIsFolder)
1508 if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1510 CFileItem item2(*item);
1511 CURL url(item->GetPath());
1512 CStdString strPath = URIUtils::GetDirectory(url.GetHostName());
1513 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1514 return GetnfoFile(&item2, bGrabAny);
1517 // grab the folder path
1518 CStdString strPath = URIUtils::GetDirectory(item->GetPath());
1520 if (bGrabAny && !item->IsStack())
1521 { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1522 nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1523 if (CFile::Exists(nfoFile))
1527 // try looking for .nfo file for a stacked item
1528 if (item->IsStack())
1530 // first try .nfo file matching first file in stack
1531 CStackDirectory dir;
1532 CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1534 item2.SetPath(firstFile);
1535 nfoFile = GetnfoFile(&item2, bGrabAny);
1536 // else try .nfo file matching stacked title
1537 if (nfoFile.IsEmpty())
1539 CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1540 item2.SetPath(stackedTitlePath);
1541 nfoFile = GetnfoFile(&item2, bGrabAny);
1546 // already an .nfo file?
1547 if (URIUtils::HasExtension(item->GetPath(), ".nfo"))
1548 nfoFile = item->GetPath();
1549 // no, create .nfo file
1551 nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1554 // test file existence
1555 if (!nfoFile.IsEmpty() && !CFile::Exists(nfoFile))
1558 if (nfoFile.IsEmpty()) // final attempt - strip off any cd1 folders
1560 URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1562 if (strPath.Mid(strPath.size()-3).Equals("cd1"))
1564 strPath = strPath.Mid(0,strPath.size()-3);
1565 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1566 return GetnfoFile(&item2, bGrabAny);
1570 if (nfoFile.IsEmpty() && item->IsOpticalMediaFile())
1572 CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1573 nfoFile = GetnfoFile(&parentDirectory, true);
1576 // folders (or stacked dvds) can take any nfo file if there's a unique one
1577 if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.IsEmpty()))
1579 // see if there is a unique nfo file in this folder, and if so, use that
1580 CFileItemList items;
1583 if (item->m_bIsFolder)
1584 strPath = item->GetPath();
1586 strPath = URIUtils::GetDirectory(item->GetPath());
1588 if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1591 for (int i = 0; i < items.Size(); i++)
1593 if (items[i]->IsNFO())
1605 return items[numNFO]->GetPath();
1612 bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1614 CVideoInfoTag movieDetails;
1616 if (m_handle && !url.strTitle.IsEmpty())
1617 m_handle->SetText(url.strTitle);
1619 CVideoInfoDownloader imdb(scraper);
1620 bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1625 nfoFile->GetDetails(movieDetails,NULL,true);
1627 if (m_handle && url.strTitle.IsEmpty())
1628 m_handle->SetText(movieDetails.m_strTitle);
1632 pDialog->SetLine(1, movieDetails.m_strTitle);
1633 pDialog->Progress();
1636 *pItem->GetVideoInfoTag() = movieDetails;
1639 return false; // no info found, or cancelled
1642 void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1644 // copy icon to folder also;
1645 if (!imdbThumb.IsEmpty())
1647 CFileItem folderItem(folder, true);
1648 CThumbLoader loader;
1649 loader.SetCachedImage(folderItem, "thumb", imdbThumb);
1653 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1655 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1656 if (0 == items.Size()) return 0;
1657 XBMC::XBMC_MD5 md5state;
1659 for (int i = 0; i < items.Size(); ++i)
1661 const CFileItemPtr pItem = items[i];
1662 md5state.append(pItem->GetPath());
1663 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1664 FILETIME time = pItem->m_dateTime;
1665 md5state.append((unsigned char *)&time, sizeof(FILETIME));
1666 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1669 md5state.getDigest(hash);
1673 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1675 // TODO: Probably should account for excluded folders here (eg samples), though that then
1676 // introduces possible problems if the user then changes the exclude regexps and
1677 // expects excluded folders that are inside a fast-hashed folder to then be picked
1678 // up. The chances that the user has a folder which contains only excluded folders
1679 // where some of those folders should be scanned recursively is pretty small.
1680 return items.GetFolderCount() == 0;
1683 CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1685 struct __stat64 buffer;
1686 if (XFILE::CFile::Stat(directory, &buffer) == 0)
1688 int64_t time = buffer.st_mtime;
1690 time = buffer.st_ctime;
1692 return StringUtils::Format("fast%"PRId64, time);
1697 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, map<int, map<string, string> > &seasonArt, const vector<string> &artTypes, bool useLocal)
1699 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end();
1701 // find the maximum number of seasons we have thumbs for (local + remote)
1702 int maxSeasons = show.m_strPictureURL.GetMaxSeasonThumb();
1704 CFileItemList items;
1705 CDirectory::GetDirectory(show.m_strPath, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
1707 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z]+)?\\.(tbn|jpg|png)"))
1709 for (int i = 0; i < items.Size(); i++)
1711 CStdString name = URIUtils::GetFileName(items[i]->GetPath());
1712 if (reg.RegFind(name) > -1)
1714 int season = atoi(reg.GetMatch(1).c_str());
1715 if (season > maxSeasons)
1716 maxSeasons = season;
1720 for (int season = -1; season <= maxSeasons; season++)
1722 map<string, string> art;
1727 basePath = "season-all";
1728 else if (season == 0)
1729 basePath = "season-specials";
1731 basePath = StringUtils::Format("season%02i", season);
1732 CFileItem artItem(URIUtils::AddFileToFolder(show.m_strPath, basePath), false);
1734 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1736 std::string image = CVideoThumbLoader::GetLocalArt(artItem, *i, false);
1738 art.insert(make_pair(*i, image));
1740 // find and classify the local thumb (backcompat) if available
1743 std::string image = CVideoThumbLoader::GetLocalArt(artItem, "thumb", false);
1745 { // cache the image and determine sizing
1746 CTextureDetails details;
1747 if (CTextureCache::Get().CacheImage(image, details))
1749 std::string type = GetArtTypeFromSize(details.width, details.height);
1750 if (art.find(type) == art.end())
1751 art.insert(make_pair(type, image));
1758 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1760 if (art.find(*i) == art.end())
1762 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, *i));
1764 art.insert(make_pair(*i, image));
1767 // use the first piece of online art as the first art type if no thumb type is available yet
1768 if (art.empty() && lookForThumb)
1770 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, "thumb"));
1772 art.insert(make_pair(artTypes.front(), image));
1775 seasonArt.insert(make_pair(season, art));
1779 void CVideoInfoScanner::FetchActorThumbs(vector<SActorInfo>& actors, const CStdString& strPath)
1781 CFileItemList items;
1782 CStdString actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
1783 if (CDirectory::Exists(actorsDir))
1784 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
1785 DIR_FLAG_NO_FILE_INFO);
1786 for (vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
1788 if (i->thumb.IsEmpty())
1790 CStdString thumbFile = i->strName;
1791 thumbFile.Replace(" ","_");
1792 for (int j = 0; j < items.Size(); j++)
1794 CStdString compare = URIUtils::GetFileName(items[j]->GetPath());
1795 URIUtils::RemoveExtension(compare);
1796 if (!items[j]->m_bIsFolder && compare == thumbFile)
1798 i->thumb = items[j]->GetPath();
1802 if (i->thumb.IsEmpty() && !i->thumbUrl.GetFirstThumb().m_url.IsEmpty())
1803 i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1804 if (!i->thumb.IsEmpty())
1805 CTextureCache::Get().BackgroundCacheImage(i->thumb);
1810 CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1812 CStdString strNfoFile;
1813 if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1814 || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1815 strNfoFile = GetnfoFile(pItem, bGrabAny);
1816 if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1817 strNfoFile = URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo");
1819 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1820 if (!strNfoFile.IsEmpty() && CFile::Exists(strNfoFile))
1822 if (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder)
1823 result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1825 result = m_nfoReader.Create(strNfoFile,info);
1830 case CNfoFile::COMBINED_NFO:
1833 case CNfoFile::FULL_NFO:
1836 case CNfoFile::URL_NFO:
1839 case CNfoFile::NO_NFO:
1845 if (result != CNfoFile::NO_NFO)
1846 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), CURL::GetRedacted(strNfoFile).c_str());
1847 if (result == CNfoFile::FULL_NFO)
1849 if (info->Content() == CONTENT_TVSHOWS)
1850 info = m_nfoReader.GetScraperInfo();
1852 else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1854 scrUrl = m_nfoReader.ScraperUrl();
1855 info = m_nfoReader.GetScraperInfo();
1857 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1858 scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1860 if (result == CNfoFile::COMBINED_NFO)
1861 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1865 CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", CURL::GetRedacted(pItem->GetPath()).c_str());
1870 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1872 if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1877 CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1880 return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1883 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1887 progress->SetHeading(heading);
1888 progress->SetLine(0, line1);
1889 progress->SetLine(2, "");
1890 progress->Progress();
1891 return progress->IsCanceled();
1896 int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1898 MOVIELIST movielist;
1899 CVideoInfoDownloader imdb(scraper);
1900 int returncode = imdb.FindMovie(videoName, movielist, progress);
1901 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
1902 { // scraper reported an error, or we had an error and user wants to cancel the scan
1904 return -1; // cancelled
1906 if (returncode > 0 && movielist.size())
1909 return 1; // found a movie
1911 return 0; // didn't find anything
1914 CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1916 CStdString strCheck = item.GetPath();
1918 strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1920 CStdString strDirectory = URIUtils::GetDirectory(strCheck);
1921 if (URIUtils::IsInRAR(strCheck))
1923 CStdString strPath=strDirectory;
1924 URIUtils::GetParentPath(strPath, strDirectory);
1928 strCheck = strDirectory;
1929 URIUtils::RemoveSlashAtEnd(strCheck);
1930 if (URIUtils::GetFileName(strCheck).size() == 3 && StringUtils::StartsWithNoCase(URIUtils::GetFileName(strCheck), "cd"))
1931 strDirectory = URIUtils::GetDirectory(strCheck);
1933 return strDirectory;