2 * Copyright (C) 2005-2008 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, write to
17 * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18 * http://www.gnu.org/copyleft/gpl.html
22 #include "threads/SystemClock.h"
24 #include "VideoInfoScanner.h"
25 #include "addons/AddonManager.h"
26 #include "filesystem/DirectoryCache.h"
29 #include "utils/RegExp.h"
30 #include "utils/md5.h"
31 #include "pictures/Picture.h"
32 #include "filesystem/StackDirectory.h"
33 #include "VideoInfoDownloader.h"
34 #include "GUIInfoManager.h"
35 #include "filesystem/File.h"
36 #include "dialogs/GUIDialogProgress.h"
37 #include "dialogs/GUIDialogYesNo.h"
38 #include "dialogs/GUIDialogOK.h"
39 #include "interfaces/AnnouncementManager.h"
40 #include "settings/AdvancedSettings.h"
41 #include "settings/GUISettings.h"
42 #include "settings/Settings.h"
43 #include "utils/StringUtils.h"
44 #include "guilib/LocalizeStrings.h"
45 #include "utils/TimeUtils.h"
46 #include "utils/log.h"
47 #include "utils/URIUtils.h"
48 #include "utils/Variant.h"
51 using namespace XFILE;
52 using namespace ADDON;
57 CVideoInfoScanner::CVideoInfoScanner()
61 m_bCanInterrupt = false;
68 CVideoInfoScanner::~CVideoInfoScanner()
72 void CVideoInfoScanner::Process()
76 unsigned int tick = XbmcThreads::SystemClockMillis();
81 m_pObserver->OnStateChanged(PREPARING);
83 m_bCanInterrupt = true;
85 CLog::Log(LOGNOTICE, "VideoInfoScanner: Starting scan ..");
87 // Reset progress vars
91 SetPriority(GetMinPriority());
93 // Database operations should not be canceled
94 // using Interupt() while scanning as it could
95 // result in unexpected behaviour.
96 m_bCanInterrupt = false;
98 bool bCancelled = false;
99 while (!bCancelled && m_pathsToScan.size())
102 * A copy of the directory path is used because the path supplied is
103 * immediately removed from the m_pathsToScan set in DoScan(). If the
104 * reference points to the entry in the set a null reference error
107 CStdString directory = *m_pathsToScan.begin();
108 if (!DoScan(directory))
115 m_database.CleanDatabase(m_pObserver,&m_pathsToClean);
119 m_pObserver->OnStateChanged(COMPRESSING_DATABASE);
120 m_database.Compress(false);
126 tick = XbmcThreads::SystemClockMillis() - tick;
127 CLog::Log(LOGNOTICE, "VideoInfoScanner: Finished scan. Scanning for video info took %s", StringUtils::SecondsToTimeString(tick / 1000).c_str());
128 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanFinished");
132 m_pObserver->OnFinished();
136 CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
140 void CVideoInfoScanner::Start(const CStdString& strDirectory, bool scanAll)
142 m_strStartDir = strDirectory;
144 m_pathsToScan.clear();
145 m_pathsToClean.clear();
147 if (strDirectory.IsEmpty())
148 { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
151 m_database.GetPaths(m_pathsToScan);
156 m_pathsToScan.insert(strDirectory);
158 m_bClean = g_advancedSettings.m_bVideoLibraryCleanOnUpdate;
165 bool CVideoInfoScanner::IsScanning()
170 void CVideoInfoScanner::Stop()
173 m_database.Interupt();
178 void CVideoInfoScanner::SetObserver(IVideoInfoScannerObserver* pObserver)
180 m_pObserver = pObserver;
183 bool CVideoInfoScanner::DoScan(const CStdString& strDirectory)
187 m_pObserver->OnDirectoryChanged(strDirectory);
188 m_pObserver->OnSetTitle(g_localizeStrings.Get(20415));
192 * Remove this path from the list we're processing. This must be done prior to
193 * the check for file or folder exclusion to prevent an infinite while loop
196 set<CStdString>::iterator it = m_pathsToScan.find(strDirectory);
197 if (it != m_pathsToScan.end())
198 m_pathsToScan.erase(it);
202 bool foundDirectly = false;
205 SScanSettings settings;
206 ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
207 CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
209 // exclude folders that match our exclude regexps
210 CStdStringArray regexps = content == CONTENT_TVSHOWS ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
211 : g_advancedSettings.m_moviesExcludeFromScanRegExps;
213 if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
216 bool ignoreFolder = m_scanAll && settings.noupdate;
217 if (content == CONTENT_NONE || ignoreFolder)
220 CStdString hash, dbHash;
221 if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
224 m_pObserver->OnStateChanged(content == CONTENT_MOVIES ? FETCHING_MOVIE_INFO : FETCHING_MUSICVIDEO_INFO);
226 CStdString fastHash = GetFastHash(strDirectory);
227 if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.IsEmpty() && fastHash == dbHash)
228 { // fast hashes match - no need to process anything
229 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change (fasthash)", strDirectory.c_str());
234 { // need to fetch the folder
235 CDirectory::GetDirectory(strDirectory, items, g_settings.m_videoExtensions);
238 GetPathHash(items, hash);
239 if (hash != dbHash && !hash.IsEmpty())
241 if (dbHash.IsEmpty())
242 CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '%s' as not in the database", strDirectory.c_str());
244 CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '%s' due to change (%s != %s)", strDirectory.c_str(), dbHash.c_str(), hash.c_str());
247 { // they're the same or the hash is empty (dir empty/dir not retrievable)
248 if (hash.IsEmpty() && !dbHash.IsEmpty())
250 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' as it's empty or doesn't exist - adding to clean list", strDirectory.c_str());
251 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
254 CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change", strDirectory.c_str());
257 m_pObserver->OnDirectoryScanned(strDirectory);
259 // update the hash to a fast hash if needed
260 if (CanFastHash(items) && !fastHash.IsEmpty())
264 else if (content == CONTENT_TVSHOWS)
267 m_pObserver->OnStateChanged(FETCHING_TVSHOW_INFO);
269 if (foundDirectly && !settings.parent_name_root)
271 CDirectory::GetDirectory(strDirectory, items, g_settings.m_videoExtensions);
272 items.SetPath(strDirectory);
273 GetPathHash(items, hash);
275 if (!m_database.GetPathHash(strDirectory, dbHash) || dbHash != hash)
277 m_database.SetPathHash(strDirectory, hash);
285 CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
286 item->SetPath(strDirectory);
287 item->m_bIsFolder = true;
289 items.SetPath(URIUtils::GetParentPath(item->GetPath()));
295 if (RetrieveVideoInfo(items, settings.parent_name_root, content))
297 if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
299 m_database.SetPathHash(strDirectory, hash);
300 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
301 CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir %s", strDirectory.c_str());
306 m_pathsToClean.insert(m_database.GetPathId(strDirectory));
307 CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir %s", strDirectory.c_str());
310 else if (hash != dbHash && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
311 { // update the hash either way - we may have changed the hash to a fast version
312 m_database.SetPathHash(strDirectory, hash);
316 m_pObserver->OnDirectoryScanned(strDirectory);
318 for (int i = 0; i < items.Size(); ++i)
320 CFileItemPtr pItem = items[i];
325 // if we have a directory item (non-playlist) we then recurse into that folder
326 // do not recurse for tv shows - we have already looked recursively for episodes
327 if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList() && settings.recurse > 0 && content != CONTENT_TVSHOWS)
329 if (!DoScan(pItem->GetPath()))
338 bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
342 if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
344 pDlgProgress->ShowProgressBar(true);
345 pDlgProgress->SetPercentage(0);
348 pDlgProgress->ShowProgressBar(false);
350 pDlgProgress->Progress();
355 bool FoundSomeInfo = false;
356 vector<int> seenPaths;
357 for (int i = 0; i < (int)items.Size(); ++i)
360 CFileItemPtr pItem = items[i];
362 // we do this since we may have a override per dir
363 ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath());
367 // Discard all exclude files defined by regExExclude
368 if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
369 : g_advancedSettings.m_moviesExcludeFromScanRegExps))
372 if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
376 m_pObserver->OnSetCurrentProgress(i, items.Size());
377 if (!pItem->m_bIsFolder && m_itemCount)
378 m_pObserver->OnSetProgress(m_currentItem++, m_itemCount);
383 // clear our scraper cache
386 INFO_RET ret = INFO_CANCELLED;
387 if (info2->Content() == CONTENT_TVSHOWS)
388 ret = RetrieveInfoForTvShow(pItem, bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress);
389 else if (info2->Content() == CONTENT_MOVIES)
390 ret = RetrieveInfoForMovie(pItem, bDirNames, info2, useLocal, pURL, pDlgProgress);
391 else if (info2->Content() == CONTENT_MUSICVIDEOS)
392 ret = RetrieveInfoForMusicVideo(pItem, bDirNames, info2, useLocal, pURL, pDlgProgress);
395 CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type %d (%s)", info2->Content(), pItem->GetPath().c_str());
396 FoundSomeInfo = false;
399 if (ret == INFO_CANCELLED || ret == INFO_ERROR)
401 FoundSomeInfo = false;
404 if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
405 FoundSomeInfo = true;
409 // Keep track of directories we've seen
410 if (pItem->m_bIsFolder)
411 seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
414 if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
416 vector< pair<int,string> > libPaths;
417 m_database.GetSubPaths(items.GetPath(), libPaths);
418 for (vector< pair<int,string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
420 if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
421 m_pathsToClean.insert(i->first);
425 pDlgProgress->ShowProgressBar(false);
427 g_infoManager.ResetLibraryBools();
429 return FoundSomeInfo;
432 INFO_RET CVideoInfoScanner::RetrieveInfoForTvShow(CFileItemPtr pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
435 if (pItem->m_bIsFolder)
436 idTvShow = m_database.GetTvShowId(pItem->GetPath());
440 URIUtils::GetDirectory(pItem->GetPath(),strPath);
441 idTvShow = m_database.GetTvShowId(strPath);
443 if (idTvShow > -1 && (fetchEpisodes || !pItem->m_bIsFolder))
445 INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
446 if (ret == INFO_ADDED)
447 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
451 if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361, pItem->GetLabel()))
452 return INFO_CANCELLED;
454 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
458 result = CheckForNFOFile(pItem.get(), bDirNames, info2, scrUrl);
459 if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
460 { // check for preconfigured scraper; if found, overwrite with interpreted scraper (from Nfofile)
461 // but keep current scan settings
462 SScanSettings settings;
463 if (m_database.GetScraperForPath(pItem->GetPath(), settings))
464 m_database.SetScraperForPath(pItem->GetPath(), info2, settings);
466 if (result == CNfoFile::FULL_NFO)
468 pItem->GetVideoInfoTag()->Reset();
469 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
471 long lResult = AddVideo(pItem.get(), info2->Content(), bDirNames);
474 GetArtwork(pItem.get(), info2->Content(), bDirNames, useLocal, pDlgProgress);
475 if (!fetchEpisodes && g_guiSettings.GetBool("videolibrary.seasonthumbs"))
476 FetchSeasonThumbs(lResult);
479 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
480 if (ret == INFO_ADDED)
481 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
486 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
493 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
494 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
497 if (GetDetails(pItem.get(), url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
499 if ((lResult = AddVideo(pItem.get(), info2->Content(), bDirNames)) < 0)
501 GetArtwork(pItem.get(), info2->Content(), false, useLocal);
505 INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
506 if (ret == INFO_ADDED)
507 m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
510 if (g_guiSettings.GetBool("videolibrary.seasonthumbs"))
511 FetchSeasonThumbs(lResult);
515 INFO_RET CVideoInfoScanner::RetrieveInfoForMovie(CFileItemPtr pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
517 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
518 (pItem->IsPlayList() && !URIUtils::GetExtension(pItem->GetPath()).Equals(".strm")))
519 return INFO_NOT_NEEDED;
521 if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
522 return INFO_CANCELLED;
524 if (m_database.HasMovieInfo(pItem->GetPath()))
525 return INFO_HAVE_ALREADY;
527 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
531 result = CheckForNFOFile(pItem.get(), bDirNames, info2, scrUrl);
532 if (result == CNfoFile::FULL_NFO)
534 pItem->GetVideoInfoTag()->Reset();
535 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
537 if (AddVideo(pItem.get(), info2->Content(), bDirNames) < 0)
539 GetArtwork(pItem.get(), info2->Content(), bDirNames, true, pDlgProgress);
542 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
549 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
550 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
552 if (GetDetails(pItem.get(), url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
554 if (AddVideo(pItem.get(), info2->Content(), bDirNames) < 0)
556 GetArtwork(pItem.get(), info2->Content(), bDirNames, useLocal);
559 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
560 return INFO_NOT_FOUND;
563 INFO_RET CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItemPtr pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
565 if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
566 (pItem->IsPlayList() && !URIUtils::GetExtension(pItem->GetPath()).Equals(".strm")))
567 return INFO_NOT_NEEDED;
569 if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
570 return INFO_CANCELLED;
572 if (m_database.HasMusicVideoInfo(pItem->GetPath()))
573 return INFO_HAVE_ALREADY;
575 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
579 result = CheckForNFOFile(pItem.get(), bDirNames, info2, scrUrl);
580 if (result == CNfoFile::FULL_NFO)
582 pItem->GetVideoInfoTag()->Reset();
583 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
585 if (AddVideo(pItem.get(), info2->Content(), bDirNames) < 0)
587 GetArtwork(pItem.get(), info2->Content(), bDirNames, true, pDlgProgress);
590 if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
597 else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
598 return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
600 if (GetDetails(pItem.get(), url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
602 if (AddVideo(pItem.get(), info2->Content(), bDirNames) < 0)
604 GetArtwork(pItem.get(), info2->Content(), bDirNames, useLocal);
607 // TODO: This is not strictly correct as we could fail to download information here or error, or be cancelled
608 return INFO_NOT_FOUND;
611 INFO_RET CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItemPtr item, long showID, const ADDON::ScraperPtr &scraper, bool useLocal, CGUIDialogProgress *progress)
613 // enumerate episodes
615 EnumerateSeriesFolder(item.get(), files);
616 if (files.size() == 0) // no update or no files
617 return INFO_NOT_NEEDED;
619 if (m_bStop || (progress && progress->IsCanceled()))
620 return INFO_CANCELLED;
623 m_pObserver->OnDirectoryChanged(item->GetPath());
625 CStdString showTitle = m_database.GetTvShowTitleById(showID);
626 return OnProcessSeriesFolder(files, scraper, useLocal, showID, showTitle, progress);
629 void CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODES& episodeList)
633 if (item->m_bIsFolder)
635 CUtil::GetRecursiveListing(item->GetPath(), items, g_settings.m_videoExtensions, true);
636 CStdString hash, dbHash;
637 int numFilesInFolder = GetPathHash(items, hash);
639 if (m_database.GetPathHash(item->GetPath(), dbHash) && dbHash == hash)
641 m_currentItem += numFilesInFolder;
643 // notify our observer of our progress
648 m_pObserver->OnSetProgress(m_currentItem, m_itemCount);
649 m_pObserver->OnSetCurrentProgress(numFilesInFolder, numFilesInFolder);
651 m_pObserver->OnDirectoryScanned(item->GetPath());
655 m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
656 m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
657 item->SetProperty("hash", hash);
661 CFileItemPtr newItem(new CFileItem(*item));
666 stack down any dvd folders
667 need to sort using the full path since this is a collapsed recursive listing of all subdirs
668 video_ts.ifo files should sort at the top of a dvd folder in ascending order
670 /foo/bar/video_ts.ifo
675 // since we're doing this now anyway, should other items be stacked?
676 items.Sort(SORT_METHOD_FULLPATH, SORT_ORDER_ASC);
678 while (x < items.Size())
680 if (items[x]->m_bIsFolder)
684 CStdString strPathX, strFileX;
685 URIUtils::Split(items[x]->GetPath(), strPathX, strFileX);
686 //CLog::Log(LOGDEBUG,"%i:%s:%s", x, strPathX.c_str(), strFileX.c_str());
689 if (strFileX.Equals("VIDEO_TS.IFO"))
691 while (y < items.Size())
693 CStdString strPathY, strFileY;
694 URIUtils::Split(items[y]->GetPath(), strPathY, strFileY);
695 //CLog::Log(LOGDEBUG," %i:%s:%s", y, strPathY.c_str(), strFileY.c_str());
697 if (strPathY.Equals(strPathX))
699 remove everything sorted below the video_ts.ifo file in the same path.
700 understandbly this wont stack correctly if there are other files in the the dvd folder.
701 this should be unlikely and thus is being ignored for now but we can monitor the
702 where the path changes and potentially remove the items above the video_ts.ifo file.
713 CStdStringArray regexps = g_advancedSettings.m_tvshowExcludeFromScanRegExps;
715 for (int i=0;i<items.Size();++i)
717 if (items[i]->m_bIsFolder)
720 URIUtils::GetDirectory(items[i]->GetPath(), strPath);
721 URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
723 if (URIUtils::GetFileName(strPath).Equals("sample"))
726 // Discard all exclude files defined by regExExcludes
727 if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
731 * Check if the media source has already set the season and episode or original air date in
732 * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
733 * any false positive matches.
735 if (ProcessItemByVideoInfoTag(items[i], episodeList))
738 if (!EnumerateEpisodeItem(items[i], episodeList))
740 CStdString decode(items[i]->GetPath());
741 CURL::Decode(decode);
742 CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file %s", decode.c_str());
747 bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItemPtr item, EPISODES &episodeList)
749 if (!item->HasVideoInfoTag())
752 CVideoInfoTag* tag = item->GetVideoInfoTag();
754 * First check the season and episode number. This takes precedence over the original air
755 * date and episode title. Must be a valid season and episode number combination.
757 if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
760 episode.strPath = item->GetPath();
761 episode.iSeason = tag->m_iSeason;
762 episode.iEpisode = tag->m_iEpisode;
763 episode.isFolder = false;
764 episodeList.push_back(episode);
765 CLog::Log(LOGDEBUG, "%s - found match for: %s. Season %d, Episode %d", __FUNCTION__,
766 episode.strPath.c_str(), episode.iSeason, episode.iEpisode);
771 * Next preference is the first aired date. If it exists use that for matching the TV Show
772 * information. Also set the title in case there are multiple matches for the first aired date.
774 if (tag->m_firstAired.IsValid())
777 episode.strPath = item->GetPath();
778 episode.strTitle = tag->m_strTitle;
779 episode.isFolder = false;
781 * Set season and episode to -1 to indicate to use the aired date.
783 episode.iSeason = -1;
784 episode.iEpisode = -1;
786 * The first aired date string must be parseable.
788 episode.cDate = item->GetVideoInfoTag()->m_firstAired;
789 episodeList.push_back(episode);
790 CLog::Log(LOGDEBUG, "%s - found match for: '%s', firstAired: '%s' = '%s', title: '%s'",
791 __FUNCTION__, episode.strPath.c_str(), tag->m_firstAired.GetAsDBDateTime().c_str(),
792 episode.cDate.GetAsLocalizedDate().c_str(), episode.strTitle.c_str());
797 * Next preference is the episode title. If it exists use that for matching the TV Show
800 if (!tag->m_strTitle.IsEmpty())
803 episode.strPath = item->GetPath();
804 episode.strTitle = tag->m_strTitle;
805 episode.isFolder = false;
807 * Set season and episode to -1 to indicate to use the title.
809 episode.iSeason = -1;
810 episode.iEpisode = -1;
811 episodeList.push_back(episode);
812 CLog::Log(LOGDEBUG,"%s - found match for: '%s', title: '%s'", __FUNCTION__,
813 episode.strPath.c_str(), episode.strTitle.c_str());
818 * There is no further episode information available if both the season and episode number have
819 * been set to 0. Return the match as true so no further matching is attempted, but don't add it
820 * to the episode list.
822 if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
824 CLog::Log(LOGDEBUG,"%s - found exclusion match for: %s. Both Season and Episode are 0. Item will be ignored for scanning.",
825 __FUNCTION__, item->GetPath().c_str());
832 bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItemPtr item, EPISODES& episodeList)
834 SETTINGS_TVSHOWLIST expression = g_advancedSettings.m_tvshowEnumRegExps;
836 CStdString strLabel=item->GetPath();
837 // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
838 CURL::Decode(strLabel);
839 strLabel.MakeLower();
841 for (unsigned int i=0;i<expression.size();++i)
844 if (!reg.RegComp(expression[i].regexp))
847 int regexppos, regexp2pos;
848 //CLog::Log(LOGDEBUG,"running expression %s on %s",expression[i].regexp.c_str(),strLabel.c_str());
849 if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
853 episode.strPath = item->GetPath();
854 episode.iSeason = -1;
855 episode.iEpisode = -1;
856 episode.cDate.SetValid(false);
857 episode.isFolder = false;
859 bool byDate = expression[i].byDate ? true : false;
860 int defaultSeason = expression[i].defaultSeason;
864 if (!GetAirDateFromRegExp(reg, episode))
867 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match %s (%s) [%s]", strLabel.c_str(),
868 episode.cDate.GetAsLocalizedDate().c_str(), expression[i].regexp.c_str());
872 if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
875 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match %s (s%ie%i) [%s]", strLabel.c_str(),
876 episode.iSeason, episode.iEpisode, expression[i].regexp.c_str());
879 // Grab the remainder from first regexp run
880 // as second run might modify or empty it.
881 char *remainder = reg.GetReplaceString("\\3");
884 * Check if the files base path is a dedicated folder that contains
885 * only this single episode. If season and episode match with the
886 * actual media file, we set episode.isFolder to true.
888 CStdString strBasePath = item->GetBaseMoviePath(true);
889 URIUtils::RemoveSlashAtEnd(strBasePath);
890 strBasePath = URIUtils::GetFileName(strBasePath);
892 if (reg.RegFind(strBasePath.c_str()) > -1)
897 GetAirDateFromRegExp(reg, parent);
898 if (episode.cDate == parent.cDate)
899 episode.isFolder = true;
903 GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
904 if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
905 episode.isFolder = true;
909 // add what we found by now
910 episodeList.push_back(episode);
913 // check the remainder of the string for any further episodes.
914 if (!byDate && reg2.RegComp(g_advancedSettings.m_tvshowMultiPartEnumRegExp))
918 // we want "long circuit" OR below so that both offsets are evaluated
919 while (((regexp2pos = reg2.RegFind(remainder + offset)) > -1) | ((regexppos = reg.RegFind(remainder + offset)) > -1))
921 if (((regexppos <= regexp2pos) && regexppos != -1) ||
922 (regexppos >= 0 && regexp2pos == -1))
924 GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
926 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season %u, multipart episode %u [%s]",
927 episode.iSeason, episode.iEpisode,
928 g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
930 episodeList.push_back(episode);
932 remainder = reg.GetReplaceString("\\3");
935 else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
936 (regexp2pos >= 0 && regexppos == -1))
938 char *ep = reg2.GetReplaceString("\\1");
939 episode.iEpisode = atoi(ep);
941 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode %u [%s]",
942 episode.iEpisode, g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
943 episodeList.push_back(episode);
944 offset += regexp2pos + reg2.GetFindLen();
954 bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp ®, SEpisode &episodeInfo, int defaultSeason)
956 char* season = reg.GetReplaceString("\\1");
957 char* episode = reg.GetReplaceString("\\2");
959 if (season && episode)
961 if (strlen(season) == 0 && strlen(episode) > 0)
962 { // no season specified -> assume defaultSeason
963 episodeInfo.iSeason = defaultSeason;
964 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode)) == -1)
965 episodeInfo.iEpisode = atoi(episode);
967 else if (strlen(season) > 0 && strlen(episode) == 0)
968 { // no episode specification -> assume defaultSeason
969 episodeInfo.iSeason = defaultSeason;
970 if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season)) == -1)
971 episodeInfo.iEpisode = atoi(season);
974 { // season and episode specified
975 episodeInfo.iSeason = atoi(season);
976 episodeInfo.iEpisode = atoi(episode);
981 return (season && episode);
984 bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp ®, SEpisode &episodeInfo)
986 char* param1 = reg.GetReplaceString("\\1");
987 char* param2 = reg.GetReplaceString("\\2");
988 char* param3 = reg.GetReplaceString("\\3");
990 if (param1 && param2 && param3)
992 // regular expression by date
993 int len1 = strlen( param1 );
994 int len2 = strlen( param2 );
995 int len3 = strlen( param3 );
997 if (len1==4 && len2==2 && len3==2)
1000 episodeInfo.cDate.SetDate(atoi(param1), atoi(param2), atoi(param3));
1002 else if (len1==2 && len2==2 && len3==4)
1004 // mm dd yyyy format
1005 episodeInfo.cDate.SetDate(atoi(param3), atoi(param1), atoi(param2));
1011 return episodeInfo.cDate.IsValid();
1014 long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder, int idShow)
1016 // ensure our database is open (this can get called via other classes)
1017 if (!m_database.Open())
1020 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1021 if (movieDetails.m_basePath.IsEmpty())
1022 movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1023 movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1025 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1027 if (pItem->m_bIsFolder)
1028 movieDetails.m_strPath = pItem->GetPath();
1030 CStdString strTitle(movieDetails.m_strTitle);
1032 if (idShow > -1 && content == CONTENT_TVSHOWS)
1034 CStdString strShowTitle = m_database.GetTvShowTitleById(idShow);
1035 strTitle.Format("%s - %ix%i - %s", strShowTitle.c_str(), movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle.c_str());
1039 m_pObserver->OnSetTitle(strTitle);
1041 CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to %s:%s", TranslateContent(content).c_str(), pItem->GetPath().c_str());
1044 if (content == CONTENT_MOVIES)
1046 // find local trailer first
1047 CStdString strTrailer = pItem->FindTrailer();
1048 if (!strTrailer.IsEmpty())
1049 movieDetails.m_strTrailer = strTrailer;
1051 lResult = m_database.SetDetailsForMovie(pItem->GetPath(), movieDetails);
1052 movieDetails.m_iDbId = lResult;
1054 // setup links to shows if the linked shows are in the db
1055 for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1057 CFileItemList items;
1058 m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1060 m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1062 CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie %s to show %s", movieDetails.m_strTitle.c_str(), movieDetails.m_showLink[i].c_str());
1065 else if (content == CONTENT_TVSHOWS)
1067 if (pItem->m_bIsFolder)
1069 lResult = m_database.SetDetailsForTvShow(pItem->GetPath(), movieDetails);
1070 movieDetails.m_iDbId = lResult;
1074 // we add episode then set details, as otherwise set details will delete the
1075 // episode then add, which breaks multi-episode files.
1076 int idEpisode = m_database.AddEpisode(idShow, pItem->GetPath());
1077 lResult = m_database.SetDetailsForEpisode(pItem->GetPath(), movieDetails, idShow, idEpisode);
1078 movieDetails.m_iDbId = lResult;
1079 if (movieDetails.m_fEpBookmark > 0)
1081 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1083 bookmark.timeInSeconds = movieDetails.m_fEpBookmark;
1084 bookmark.seasonNumber = movieDetails.m_iSeason;
1085 bookmark.episodeNumber = movieDetails.m_iEpisode;
1086 m_database.AddBookMarkForEpisode(movieDetails, bookmark);
1090 else if (content == CONTENT_MUSICVIDEOS)
1092 lResult = m_database.SetDetailsForMusicVideo(pItem->GetPath(), movieDetails);
1093 movieDetails.m_iDbId = lResult;
1096 if (g_advancedSettings.m_bVideoLibraryImportWatchedState)
1097 m_database.SetPlayCount(*pItem, movieDetails.m_playCount, movieDetails.m_lastPlayed);
1103 void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, CGUIDialogProgress* pDialog /* == NULL */)
1105 CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1106 // get & save fanart image
1107 bool isEpisode = (content == CONTENT_TVSHOWS && !pItem->m_bIsFolder);
1108 if (!isEpisode && (!useLocal || !pItem->CacheLocalFanart()))
1110 if (movieDetails.m_fanart.GetNumFanarts())
1111 DownloadImage(movieDetails.m_fanart.GetImageURL(), pItem->GetCachedFanart(), false, pDialog);
1114 // get & save thumb image
1115 CStdString cachedThumb = pItem->GetCachedVideoThumb();
1116 if (isEpisode && CFile::Exists(cachedThumb))
1117 { // have an episode (??? and also a normal "cached" thumb that we're going to override now???)
1118 movieDetails.m_strFileNameAndPath = pItem->GetPath();
1119 CFileItem item(movieDetails);
1120 cachedThumb = item.GetCachedEpisodeThumb();
1123 CStdString localThumb;
1126 localThumb = pItem->GetUserVideoThumb();
1127 if (bApplyToDir && localThumb.IsEmpty())
1129 CStdString strParent;
1130 URIUtils::GetParentPath(pItem->GetPath(), strParent);
1131 CFileItem item(*pItem);
1132 item.SetPath(strParent);
1133 item.m_bIsFolder = true;
1134 localThumb = item.GetUserVideoThumb();
1138 // parent folder to apply the thumb to and to search for local actor thumbs
1139 CStdString parentDir = GetParentDir(*pItem);
1141 if (!localThumb.IsEmpty())
1142 CPicture::CacheThumb(localThumb, cachedThumb);
1144 { // see if we have an online image to use
1145 CStdString onlineThumb = CScraperUrl::GetThumbURL(movieDetails.m_strPictureURL.GetFirstThumb());
1146 if (!onlineThumb.IsEmpty())
1148 if (onlineThumb.Find("http://") < 0 &&
1149 onlineThumb.Find("/") < 0 &&
1150 onlineThumb.Find("\\") < 0)
1153 URIUtils::GetDirectory(pItem->GetPath(), strPath);
1154 onlineThumb = URIUtils::AddFileToFolder(strPath, onlineThumb);
1156 DownloadImage(onlineThumb, cachedThumb, true, pDialog);
1159 if (g_guiSettings.GetBool("videolibrary.actorthumbs"))
1160 FetchActorThumbs(movieDetails.m_cast, parentDir);
1162 ApplyThumbToFolder(parentDir, cachedThumb);
1164 CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
1165 // Hack to make sure CVideoInfoTag::m_strShowTitle is set for tvshows
1166 // to make sure CAnnouncementManager provides the correct type for the item
1167 if (content == CONTENT_TVSHOWS && !isEpisode && itemCopy->HasVideoInfoTag())
1168 itemCopy->GetVideoInfoTag()->m_strShowTitle = itemCopy->GetVideoInfoTag()->m_strTitle;
1169 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", itemCopy);
1172 void CVideoInfoScanner::DownloadImage(const CStdString &url, const CStdString &destination, bool asThumb /*= true */, CGUIDialogProgress *progress /*= NULL */)
1176 progress->SetLine(2, 415);
1177 progress->Progress();
1179 bool result = false;
1181 result = CPicture::CreateThumbnail(url, destination);
1183 result = CPicture::CacheFanart(url, destination);
1186 CFile::Delete(destination);
1191 INFO_RET CVideoInfoScanner::OnProcessSeriesFolder(EPISODES& files, const ADDON::ScraperPtr &scraper, bool useLocal, int idShow, const CStdString& strShowTitle, CGUIDialogProgress* pDlgProgress /* = NULL */)
1195 pDlgProgress->SetLine(1, strShowTitle);
1196 pDlgProgress->SetLine(2, 20361);
1197 pDlgProgress->SetPercentage(0);
1198 pDlgProgress->ShowProgressBar(true);
1199 pDlgProgress->Progress();
1202 EPISODELIST episodes;
1203 bool hasEpisodeGuide = false;
1205 int iMax = files.size();
1207 for (EPISODES::iterator file = files.begin(); file != files.end(); ++file)
1209 m_nfoReader.Close();
1212 pDlgProgress->SetLine(2, 20361);
1213 pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1214 pDlgProgress->Progress();
1218 if (m_itemCount > 0)
1219 m_pObserver->OnSetProgress(m_currentItem++, m_itemCount);
1220 m_pObserver->OnSetCurrentProgress(iCurr++, iMax);
1222 if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1223 return INFO_CANCELLED;
1225 if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1228 m_pObserver->OnSetTitle(g_localizeStrings.Get(20415));
1233 item.SetPath(file->strPath);
1235 // handle .nfo files
1236 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1238 ScraperPtr info(scraper);
1239 item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1241 result = CheckForNFOFile(&item, false, info,scrUrl);
1242 if (result == CNfoFile::FULL_NFO)
1244 m_nfoReader.GetDetails(*item.GetVideoInfoTag());
1245 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, idShow) < 0)
1247 GetArtwork(&item, CONTENT_TVSHOWS);
1251 if (!hasEpisodeGuide)
1253 // fetch episode guide
1254 CVideoInfoTag details;
1255 m_database.GetTvShowInfo(item.GetPath(), details, idShow);
1256 if (!details.m_strEpisodeGuide.IsEmpty())
1259 url.ParseEpisodeGuide(details.m_strEpisodeGuide);
1263 pDlgProgress->SetLine(2, 20354);
1264 pDlgProgress->Progress();
1267 CVideoInfoDownloader imdb(scraper);
1268 if (!imdb.GetEpisodeList(url, episodes))
1269 return INFO_NOT_FOUND;
1271 hasEpisodeGuide = true;
1275 if (episodes.empty())
1277 CLog::Log(LOGERROR, "VideoInfoScanner: Asked to lookup episode %s"
1278 " online, but we have no episode guide. Check your tvshow.nfo and make"
1279 " sure the <episodeguide> tag is in place.", file->strPath.c_str());
1283 std::pair<int,int> key;
1284 key.first = file->iSeason;
1285 key.second = file->iEpisode;
1286 bool bFound = false;
1287 EPISODELIST::iterator guide = episodes.begin();;
1288 EPISODELIST matches;
1290 for (; guide != episodes.end(); ++guide )
1292 if ((file->iEpisode!=-1) && (file->iSeason!=-1) && (key==guide->key))
1297 if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1299 matches.push_back(*guide);
1302 if (!guide->cScraperUrl.strTitle.IsEmpty() && guide->cScraperUrl.strTitle.CompareNoCase(file->strTitle) == 0)
1312 * If there is only one match or there are matches but no title to compare with to help
1313 * identify the best match, then pick the first match as the best possible candidate.
1315 * Otherwise, use the title to further refine the best match.
1317 if (matches.size() == 1 || (file->strTitle.IsEmpty() && matches.size() > 1))
1319 guide = matches.begin();
1322 else if (!file->strTitle.IsEmpty())
1324 double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
1326 EPISODELIST *candidates;
1327 if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1329 minscore = 0.8; // 80% should ensure a good match.
1330 candidates = &episodes;
1332 else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1333 candidates = &matches;
1335 CStdStringArray titles;
1336 for (guide = candidates->begin(); guide != candidates->end(); ++guide)
1337 titles.push_back(guide->cScraperUrl.strTitle.ToLower());
1340 int index = StringUtils::FindBestMatch(file->strTitle.ToLower(), titles, matchscore);
1341 if (matchscore >= minscore)
1343 guide = candidates->begin() + index;
1345 CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1346 __FUNCTION__, strShowTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1353 CVideoInfoDownloader imdb(scraper);
1355 item.SetPath(file->strPath);
1356 if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1357 return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1358 item.GetVideoInfoTag()->m_iSeason = guide->key.first;
1359 item.GetVideoInfoTag()->m_iEpisode = guide->key.second;
1360 if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, idShow) < 0)
1362 GetArtwork(&item, CONTENT_TVSHOWS);
1366 CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d, airdate: '%s', title: '%s'",
1367 __FUNCTION__, strShowTitle.c_str(), file->iSeason, file->iEpisode,
1368 file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1371 if (g_guiSettings.GetBool("videolibrary.seasonthumbs"))
1372 FetchSeasonThumbs(idShow);
1376 CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1379 // Find a matching .nfo file
1380 if (!item->m_bIsFolder)
1383 CStdString strExtension;
1384 URIUtils::GetExtension(item->GetPath(), strExtension);
1386 if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1388 CFileItem item2(*item);
1389 CURL url(item->GetPath());
1391 URIUtils::GetDirectory(url.GetHostName(), strPath);
1392 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1393 return GetnfoFile(&item2, bGrabAny);
1396 // grab the folder path
1398 URIUtils::GetDirectory(item->GetPath(), strPath);
1400 if (bGrabAny && !item->IsStack())
1401 { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1402 nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1403 if (CFile::Exists(nfoFile))
1407 // try looking for .nfo file for a stacked item
1408 if (item->IsStack())
1410 // first try .nfo file matching first file in stack
1411 CStackDirectory dir;
1412 CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1414 item2.SetPath(firstFile);
1415 nfoFile = GetnfoFile(&item2, bGrabAny);
1416 // else try .nfo file matching stacked title
1417 if (nfoFile.IsEmpty())
1419 CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1420 item2.SetPath(stackedTitlePath);
1421 nfoFile = GetnfoFile(&item2, bGrabAny);
1426 // already an .nfo file?
1427 if ( strcmpi(strExtension.c_str(), ".nfo") == 0 )
1428 nfoFile = item->GetPath();
1429 // no, create .nfo file
1431 nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1434 // test file existence
1435 if (!nfoFile.IsEmpty() && !CFile::Exists(nfoFile))
1438 if (nfoFile.IsEmpty()) // final attempt - strip off any cd1 folders
1440 URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1442 if (strPath.Mid(strPath.size()-3).Equals("cd1"))
1444 strPath = strPath.Mid(0,strPath.size()-3);
1445 item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1446 return GetnfoFile(&item2, bGrabAny);
1450 if (nfoFile.IsEmpty() && item->IsOpticalMediaFile())
1452 CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1453 nfoFile = GetnfoFile(&parentDirectory, true);
1456 // folders (or stacked dvds) can take any nfo file if there's a unique one
1457 if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.IsEmpty()))
1459 // see if there is a unique nfo file in this folder, and if so, use that
1460 CFileItemList items;
1462 CStdString strPath = item->GetPath();
1463 if (!item->m_bIsFolder)
1464 URIUtils::GetDirectory(item->GetPath(), strPath);
1465 if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1468 for (int i = 0; i < items.Size(); i++)
1470 if (items[i]->IsNFO())
1482 return items[numNFO]->GetPath();
1489 bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1491 CVideoInfoTag movieDetails;
1493 CVideoInfoDownloader imdb(scraper);
1494 bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1499 nfoFile->GetDetails(movieDetails,NULL,true);
1501 if (m_pObserver && url.strTitle.IsEmpty())
1502 m_pObserver->OnSetTitle(movieDetails.m_strTitle);
1506 pDialog->SetLine(1, movieDetails.m_strTitle);
1507 pDialog->Progress();
1510 *pItem->GetVideoInfoTag() = movieDetails;
1513 return false; // no info found, or cancelled
1516 void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1518 // copy icon to folder also;
1519 if (CFile::Exists(imdbThumb))
1521 CFileItem folderItem(folder, true);
1522 CStdString strThumb(folderItem.GetCachedVideoThumb());
1523 CFile::Cache(imdbThumb.c_str(), strThumb.c_str(), NULL, NULL);
1527 int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1529 // Create a hash based on the filenames, filesize and filedate. Also count the number of files
1530 if (0 == items.Size()) return 0;
1531 XBMC::XBMC_MD5 md5state;
1533 for (int i = 0; i < items.Size(); ++i)
1535 const CFileItemPtr pItem = items[i];
1536 md5state.append(pItem->GetPath());
1537 md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1538 FILETIME time = pItem->m_dateTime;
1539 md5state.append((unsigned char *)&time, sizeof(FILETIME));
1540 if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1543 md5state.getDigest(hash);
1547 bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1549 // TODO: Probably should account for excluded folders here (eg samples), though that then
1550 // introduces possible problems if the user then changes the exclude regexps and
1551 // expects excluded folders that are inside a fast-hashed folder to then be picked
1552 // up. The chances that the user has a folder which contains only excluded folders
1553 // where some of those folders should be scanned recursively is pretty small.
1554 return items.GetFolderCount() == 0;
1557 CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1559 struct __stat64 buffer;
1560 if (XFILE::CFile::Stat(directory, &buffer) == 0)
1562 int64_t time = buffer.st_mtime;
1564 time = buffer.st_ctime;
1568 hash.Format("fast%"PRId64, time);
1575 void CVideoInfoScanner::FetchSeasonThumbs(int idTvShow, const CStdString &folderToCheck, bool download, bool overwrite)
1577 // ensure our database is open (this can get called via other classes)
1578 if (!m_database.Open())
1581 CVideoInfoTag movie;
1582 m_database.GetTvShowInfo("", movie, idTvShow);
1583 CStdString showDir(folderToCheck.IsEmpty() ? movie.m_strPath : folderToCheck);
1584 CFileItemList items;
1586 strPath.Format("videodb://2/2/%i/", idTvShow);
1587 m_database.GetSeasonsNav(strPath, items, -1, -1, -1, -1, idTvShow);
1589 pItem.reset(new CFileItem(g_localizeStrings.Get(20366))); // "All Seasons"
1590 CStdString path; path.Format("%s/-1/", strPath.c_str());
1591 pItem->SetPath(path);
1592 pItem->GetVideoInfoTag()->m_iSeason = -1;
1593 pItem->GetVideoInfoTag()->m_strPath = movie.m_strPath;
1594 if (overwrite || !XFILE::CFile::Exists(pItem->GetCachedSeasonThumb()))
1597 // used for checking for a season[ ._-](number).tbn
1598 CFileItemList tbnItems;
1599 CDirectory::GetDirectory(showDir, tbnItems, ".tbn");
1600 for (int i=0;i<items.Size();++i)
1602 if (overwrite || !items[i]->HasThumbnail())
1604 CStdString strExpression;
1605 int iSeason = items[i]->GetVideoInfoTag()->m_iSeason;
1607 strExpression = "season-all.tbn";
1608 else if (iSeason == 0)
1609 strExpression = "season-specials.tbn";
1611 strExpression.Format("season[ ._-]?(0?%i)\\.tbn", items[i]->GetVideoInfoTag()->m_iSeason);
1612 bool bDownload = download;
1614 if (reg.RegComp(strExpression.c_str()))
1616 for (int j=0;j<tbnItems.Size();++j)
1618 CStdString strCheck = URIUtils::GetFileName(tbnItems[j]->GetPath());
1620 if (reg.RegFind(strCheck.c_str()) > -1)
1622 CPicture::CreateThumbnail(tbnItems[j]->GetPath(), items[i]->GetCachedSeasonThumb());
1629 DownloadImage(CScraperUrl::GetThumbURL(movie.m_strPictureURL.GetSeasonThumb(items[i]->GetVideoInfoTag()->m_iSeason)), items[i]->GetCachedSeasonThumb());
1635 void CVideoInfoScanner::FetchActorThumbs(const vector<SActorInfo>& actors, const CStdString& strPath)
1637 for (unsigned int i=0;i<actors.size();++i)
1640 item.SetLabel(actors[i].strName);
1641 CStdString strThumb = item.GetCachedActorThumb();
1642 if (!CFile::Exists(strThumb))
1644 CStdString thumbFile = actors[i].strName;
1645 thumbFile.Replace(" ","_");
1646 thumbFile += ".tbn";
1647 CStdString strLocal = URIUtils::AddFileToFolder(URIUtils::AddFileToFolder(strPath, ".actors"), thumbFile);
1648 if (CFile::Exists(strLocal))
1649 CPicture::CreateThumbnail(strLocal, strThumb);
1650 else if (!actors[i].thumbUrl.GetFirstThumb().m_url.IsEmpty())
1651 DownloadImage(CScraperUrl::GetThumbURL(actors[i].thumbUrl.GetFirstThumb()), strThumb);
1656 CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1658 CStdString strNfoFile;
1659 if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1660 || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1661 strNfoFile = GetnfoFile(pItem, bGrabAny);
1662 if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1663 URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo", strNfoFile);
1665 CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1666 if (!strNfoFile.IsEmpty() && CFile::Exists(strNfoFile))
1668 result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1673 case CNfoFile::COMBINED_NFO:
1676 case CNfoFile::FULL_NFO:
1679 case CNfoFile::URL_NFO:
1682 case CNfoFile::NO_NFO:
1688 if (result != CNfoFile::NO_NFO)
1689 CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), strNfoFile.c_str());
1690 if (result == CNfoFile::FULL_NFO)
1692 if (info->Content() == CONTENT_TVSHOWS)
1693 info = m_nfoReader.GetScraperInfo();
1695 else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1697 scrUrl = m_nfoReader.ScraperUrl();
1698 info = m_nfoReader.GetScraperInfo();
1700 CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1701 scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1703 if (result == CNfoFile::COMBINED_NFO)
1704 m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1708 CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", pItem->GetPath().c_str());
1713 bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1715 if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1720 CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1723 return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1726 bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1730 progress->SetHeading(heading);
1731 progress->SetLine(0, line1);
1732 progress->SetLine(2, "");
1733 progress->Progress();
1734 return progress->IsCanceled();
1739 int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1741 MOVIELIST movielist;
1742 CVideoInfoDownloader imdb(scraper);
1743 int returncode = imdb.FindMovie(videoName, movielist, progress);
1744 if (returncode < 0 || (returncode == 0 && !DownloadFailed(progress)))
1745 { // scraper reported an error, or we had an error and user wants to cancel the scan
1747 return -1; // cancelled
1749 if (returncode > 0 && movielist.size())
1752 return 1; // found a movie
1754 return 0; // didn't find anything
1757 CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1759 CStdString strCheck = item.GetPath();
1761 strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1763 CStdString strDirectory;
1764 URIUtils::GetDirectory(strCheck, strDirectory);
1765 if (URIUtils::IsInRAR(strCheck))
1767 CStdString strPath=strDirectory;
1768 URIUtils::GetParentPath(strPath, strDirectory);
1772 strCheck = strDirectory;
1773 URIUtils::RemoveSlashAtEnd(strCheck);
1774 if (URIUtils::GetFileName(strCheck).size() == 3 && URIUtils::GetFileName(strCheck).Left(2).Equals("cd"))
1775 URIUtils::GetDirectory(strCheck, strDirectory);
1777 return strDirectory;