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.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;
1694 hash.Format("fast%"PRId64, time);
1701 void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, map<int, map<string, string> > &seasonArt, const vector<string> &artTypes, bool useLocal)
1703 bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end();
1705 // find the maximum number of seasons we have thumbs for (local + remote)
1706 int maxSeasons = show.m_strPictureURL.GetMaxSeasonThumb();
1708 CFileItemList items;
1709 CDirectory::GetDirectory(show.m_strPath, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
1711 if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z]+)?\\.(tbn|jpg|png)"))
1713 for (int i = 0; i < items.Size(); i++)
1715 CStdString name = URIUtils::GetFileName(items[i]->GetPath());
1716 if (reg.RegFind(name) > -1)
1718 int season = atoi(reg.GetMatch(1).c_str());
1719 if (season > maxSeasons)
1720 maxSeasons = season;
1724 for (int season = -1; season <= maxSeasons; season++)
1726 map<string, string> art;
1731 basePath = "season-all";
1732 else if (season == 0)
1733 basePath = "season-specials";
1735 basePath = StringUtils::Format("season%02i", season);
1736 CFileItem artItem(URIUtils::AddFileToFolder(show.m_strPath, basePath), false);
1738 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1740 std::string image = CVideoThumbLoader::GetLocalArt(artItem, *i, false);
1742 art.insert(make_pair(*i, image));
1744 // find and classify the local thumb (backcompat) if available
1747 std::string image = CVideoThumbLoader::GetLocalArt(artItem, "thumb", false);
1749 { // cache the image and determine sizing
1750 CTextureDetails details;
1751 if (CTextureCache::Get().CacheImage(image, details))
1753 std::string type = GetArtTypeFromSize(details.width, details.height);
1754 if (art.find(type) == art.end())
1755 art.insert(make_pair(type, image));
1762 for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1764 if (art.find(*i) == art.end())
1766 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, *i));
1768 art.insert(make_pair(*i, image));
1771 // use the first piece of online art as the first art type if no thumb type is available yet
1772 if (art.empty() && lookForThumb)
1774 string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, "thumb"));
1776 art.insert(make_pair(artTypes.front(), image));
1779 seasonArt.insert(make_pair(season, art));
1783 void CVideoInfoScanner::FetchActorThumbs(vector<SActorInfo>& actors, const CStdString& strPath)
1785 CFileItemList items;
1786 CStdString actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
1787 if (CDirectory::Exists(actorsDir))
1788 CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
1789 DIR_FLAG_NO_FILE_INFO);
1790 for (vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
1792 if (i->thumb.IsEmpty())
1794 CStdString thumbFile = i->strName;
1795 thumbFile.Replace(" ","_");
1796 for (int j = 0; j < items.Size(); j++)
1798 CStdString compare = URIUtils::GetFileName(items[j]->GetPath());
1799 URIUtils::RemoveExtension(compare);
1800 if (!items[j]->m_bIsFolder && compare == thumbFile)
1802 i->thumb = items[j]->GetPath();
1806 if (i->thumb.IsEmpty() && !i->thumbUrl.GetFirstThumb().m_url.IsEmpty())
1807 i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1808 if (!i->thumb.IsEmpty())
1809 CTextureCache::Get().BackgroundCacheImage(i->thumb);
1814 CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1816 CStdString strNfoFile;
1817 if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1818 || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1819 strNfoFile = GetnfoFile(pItem, bGrabAny);
1820 if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1821 strNfoFile = URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo");
1823 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1824 if (!strNfoFile.IsEmpty() && CFile::Exists(strNfoFile))
1826 if (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder)
1827 result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1829 result = m_nfoReader.Create(strNfoFile,info);
1834 case CNfoFile::COMBINED_NFO:
1837 case CNfoFile::FULL_NFO:
1840 case CNfoFile::URL_NFO:
1843 case CNfoFile::NO_NFO:
1849 if (result != CNfoFile::NO_NFO)
1850 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), CURL::GetRedacted(strNfoFile).c_str());
1851 if (result == CNfoFile::FULL_NFO)
1853 if (info->Content() == CONTENT_TVSHOWS)
1854 info = m_nfoReader.GetScraperInfo();
1856 else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1858 scrUrl = m_nfoReader.ScraperUrl();
1859 info = m_nfoReader.GetScraperInfo();
1861 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1862 scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1864 if (result == CNfoFile::COMBINED_NFO)
1865 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1869 CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", CURL::GetRedacted(pItem->GetPath()).c_str());
1874 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1876 if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1881 CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1884 return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1887 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1891 progress->SetHeading(heading);
1892 progress->SetLine(0, line1);
1893 progress->SetLine(2, "");
1894 progress->Progress();
1895 return progress->IsCanceled();
1900 int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1902 MOVIELIST movielist;
1903 CVideoInfoDownloader imdb(scraper);
1904 int returncode = imdb.FindMovie(videoName, movielist, progress);
1905 if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
1906 { // scraper reported an error, or we had an error and user wants to cancel the scan
1908 return -1; // cancelled
1910 if (returncode > 0 && movielist.size())
1913 return 1; // found a movie
1915 return 0; // didn't find anything
1918 CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1920 CStdString strCheck = item.GetPath();
1922 strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1924 CStdString strDirectory = URIUtils::GetDirectory(strCheck);
1925 if (URIUtils::IsInRAR(strCheck))
1927 CStdString strPath=strDirectory;
1928 URIUtils::GetParentPath(strPath, strDirectory);
1932 strCheck = strDirectory;
1933 URIUtils::RemoveSlashAtEnd(strCheck);
1934 if (URIUtils::GetFileName(strCheck).size() == 3 && StringUtils::StartsWithNoCase(URIUtils::GetFileName(strCheck), "cd"))
1935 strDirectory = URIUtils::GetDirectory(strCheck);
1937 return strDirectory;