[release] version bump to 13.0 beta1
[vuplus_xbmc] / xbmc / video / VideoInfoScanner.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://xbmc.org
4  *
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)
8  *  any later version.
9  *
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.
14  *
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/>.
18  *
19  */
20
21 #include "threads/SystemClock.h"
22 #include "FileItem.h"
23 #include "VideoInfoScanner.h"
24 #include "addons/AddonManager.h"
25 #include "filesystem/DirectoryCache.h"
26 #include "Util.h"
27 #include "NfoFile.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"
51 #include "URL.h"
52
53 using namespace std;
54 using namespace XFILE;
55 using namespace ADDON;
56
57 namespace VIDEO
58 {
59
60   CVideoInfoScanner::CVideoInfoScanner() : CThread("VideoInfoScanner")
61   {
62     m_bRunning = false;
63     m_handle = NULL;
64     m_showDialog = false;
65     m_bCanInterrupt = false;
66     m_currentItem = 0;
67     m_itemCount = 0;
68     m_bClean = false;
69     m_scanAll = false;
70   }
71
72   CVideoInfoScanner::~CVideoInfoScanner()
73   {
74   }
75
76   void CVideoInfoScanner::Process()
77   {
78     try
79     {
80       unsigned int tick = XbmcThreads::SystemClockMillis();
81
82       m_database.Open();
83
84       if (m_showDialog && !CSettings::Get().GetBool("videolibrary.backgroundupdate"))
85       {
86         CGUIDialogExtendedProgressBar* dialog =
87           (CGUIDialogExtendedProgressBar*)g_windowManager.GetWindow(WINDOW_DIALOG_EXT_PROGRESS);
88         if (dialog)
89            m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
90       }
91
92       m_bCanInterrupt = true;
93
94       CLog::Log(LOGNOTICE, "VideoInfoScanner: Starting scan ..");
95       ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanStarted");
96
97       // Reset progress vars
98       m_currentItem = 0;
99       m_itemCount = -1;
100
101       SetPriority(GetMinPriority());
102
103       // Database operations should not be canceled
104       // using Interupt() while scanning as it could
105       // result in unexpected behaviour.
106       m_bCanInterrupt = false;
107
108       bool bCancelled = false;
109       while (!bCancelled && m_pathsToScan.size())
110       {
111         /*
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
115          * occurs.
116          */
117         CStdString directory = *m_pathsToScan.begin();
118         if (!CDirectory::Exists(directory))
119         {
120           /*
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.
124            */
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());
127         }
128         else if (!DoScan(directory))
129           bCancelled = true;
130       }
131
132       if (!bCancelled)
133       {
134         if (m_bClean)
135           CleanDatabase(m_handle,&m_pathsToClean, false);
136         else
137         {
138           if (m_handle)
139             m_handle->SetTitle(g_localizeStrings.Get(331));
140           m_database.Compress(false);
141         }
142       }
143
144       m_database.Close();
145
146       tick = XbmcThreads::SystemClockMillis() - tick;
147       CLog::Log(LOGNOTICE, "VideoInfoScanner: Finished scan. Scanning for video info took %s", StringUtils::SecondsToTimeString(tick / 1000).c_str());
148     }
149     catch (...)
150     {
151       CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
152     }
153     
154     m_bRunning = false;
155     ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnScanFinished");
156
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);
161     
162     if (m_handle)
163       m_handle->MarkFinished();
164     m_handle = NULL;
165   }
166
167   void CVideoInfoScanner::Start(const CStdString& strDirectory, bool scanAll)
168   {
169     m_strStartDir = strDirectory;
170     m_scanAll = scanAll;
171     m_pathsToScan.clear();
172     m_pathsToClean.clear();
173
174     if (strDirectory.empty())
175     { // scan all paths in the database.  We do this by scanning all paths in the db, and crossing them off the list as
176       // we go.
177       m_database.Open();
178       m_database.GetPaths(m_pathsToScan);
179       m_database.Close();
180     }
181     else
182     {
183       m_pathsToScan.insert(strDirectory);
184     }
185     m_bClean = g_advancedSettings.m_bVideoLibraryCleanOnUpdate;
186
187     StopThread();
188     Create();
189     m_bRunning = true;
190   }
191
192   bool CVideoInfoScanner::IsScanning()
193   {
194     return m_bRunning;
195   }
196
197   void CVideoInfoScanner::Stop()
198   {
199     if (m_bCanInterrupt)
200       m_database.Interupt();
201
202     StopThread(false);
203   }
204
205   void CVideoInfoScanner::CleanDatabase(CGUIDialogProgressBarHandle* handle /*= NULL */, const set<int>* paths /*= NULL */, bool showProgress /*= true */)
206   {
207     m_bRunning = true;
208     m_database.Open();
209     m_database.CleanDatabase(handle, paths, showProgress);
210     m_database.Close();
211     m_bRunning = false;
212   }
213
214   static void OnDirectoryScanned(const CStdString& strDirectory)
215   {
216     CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
217     msg.SetStringParam(strDirectory);
218     g_windowManager.SendThreadMessage(msg);
219   }
220
221   bool CVideoInfoScanner::DoScan(const CStdString& strDirectory)
222   {
223     if (m_handle)
224     {
225       m_handle->SetText(g_localizeStrings.Get(20415));
226     }
227
228     /*
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
231      * in Process().
232      */
233     set<CStdString>::iterator it = m_pathsToScan.find(strDirectory);
234     if (it != m_pathsToScan.end())
235       m_pathsToScan.erase(it);
236
237     // load subfolder
238     CFileItemList items;
239     bool foundDirectly = false;
240     bool bSkip = false;
241
242     SScanSettings settings;
243     ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
244     CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
245
246     // exclude folders that match our exclude regexps
247     CStdStringArray regexps = content == CONTENT_TVSHOWS ? g_advancedSettings.m_tvshowExcludeFromScanRegExps
248                                                          : g_advancedSettings.m_moviesExcludeFromScanRegExps;
249
250     if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
251       return true;
252
253     bool ignoreFolder = !m_scanAll && settings.noupdate;
254     if (content == CONTENT_NONE || ignoreFolder)
255       return true;
256
257     CStdString hash, dbHash;
258     if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
259     {
260       if (m_handle)
261       {
262         int str = content == CONTENT_MOVIES ? 20317:20318;
263         m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name().c_str()));
264       }
265
266       CStdString fastHash = GetFastHash(strDirectory);
267       if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && fastHash == dbHash)
268       { // fast hashes match - no need to process anything
269         CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change (fasthash)", CURL::GetRedacted(strDirectory).c_str());
270         hash = fastHash;
271         bSkip = true;
272       }
273       if (!bSkip)
274       { // need to fetch the folder
275         CDirectory::GetDirectory(strDirectory, items, g_advancedSettings.m_videoExtensions);
276         items.Stack();
277         // compute hash
278         GetPathHash(items, hash);
279         if (hash != dbHash && !hash.empty())
280         {
281           if (dbHash.empty())
282             CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '%s' as not in the database", CURL::GetRedacted(strDirectory).c_str());
283           else
284             CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '%s' due to change (%s != %s)", CURL::GetRedacted(strDirectory).c_str(), dbHash.c_str(), hash.c_str());
285         }
286         else
287         { // they're the same or the hash is empty (dir empty/dir not retrievable)
288           if (hash.empty() && !dbHash.empty())
289           {
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));
292           }
293           else
294             CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '%s' due to no change", CURL::GetRedacted(strDirectory).c_str());
295           bSkip = true;
296           if (m_handle)
297             OnDirectoryScanned(strDirectory);
298         }
299         // update the hash to a fast hash if needed
300         if (CanFastHash(items) && !fastHash.empty())
301           hash = fastHash;
302       }
303     }
304     else if (content == CONTENT_TVSHOWS)
305     {
306       if (m_handle)
307         m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name().c_str()));
308
309       if (foundDirectly && !settings.parent_name_root)
310       {
311         CDirectory::GetDirectory(strDirectory, items, g_advancedSettings.m_videoExtensions);
312         items.SetPath(strDirectory);
313         GetPathHash(items, hash);
314         bSkip = true;
315         if (!m_database.GetPathHash(strDirectory, dbHash) || dbHash != hash)
316         {
317           m_database.SetPathHash(strDirectory, hash);
318           bSkip = false;
319         }
320         else
321           items.Clear();
322       }
323       else
324       {
325         CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
326         item->SetPath(strDirectory);
327         item->m_bIsFolder = true;
328         items.Add(item);
329         items.SetPath(URIUtils::GetParentPath(item->GetPath()));
330       }
331     }
332
333     if (!bSkip)
334     {
335       if (RetrieveVideoInfo(items, settings.parent_name_root, content))
336       {
337         if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
338         {
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());
342         }
343       }
344       else
345       {
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());
348       }
349     }
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);
353     }
354
355     if (m_handle)
356       OnDirectoryScanned(strDirectory);
357
358     for (int i = 0; i < items.Size(); ++i)
359     {
360       CFileItemPtr pItem = items[i];
361
362       if (m_bStop)
363         break;
364
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)
368       {
369         if (!DoScan(pItem->GetPath()))
370         {
371           m_bStop = true;
372         }
373       }
374     }
375     return !m_bStop;
376   }
377
378   bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
379   {
380     if (pDlgProgress)
381     {
382       if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
383       {
384         pDlgProgress->ShowProgressBar(true);
385         pDlgProgress->SetPercentage(0);
386       }
387       else
388         pDlgProgress->ShowProgressBar(false);
389
390       pDlgProgress->Progress();
391     }
392
393     m_database.Open();
394
395     bool FoundSomeInfo = false;
396     vector<int> seenPaths;
397     for (int i = 0; i < (int)items.Size(); ++i)
398     {
399       m_nfoReader.Close();
400       CFileItemPtr pItem = items[i];
401
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());
404       if (!info2) // skip
405         continue;
406
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))
410         continue;
411
412       if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
413       {
414         if (m_handle)
415           m_handle->SetPercentage(i*100.f/items.Size());
416       }
417
418       // clear our scraper cache
419       info2->ClearCache();
420
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);
428       else
429       {
430         CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type %d (%s)", info2->Content(), CURL::GetRedacted(pItem->GetPath()).c_str());
431         FoundSomeInfo = false;
432         break;
433       }
434       if (ret == INFO_CANCELLED || ret == INFO_ERROR)
435       {
436         FoundSomeInfo = false;
437         break;
438       }
439       if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
440         FoundSomeInfo = true;
441       else if (ret == INFO_NOT_FOUND)
442       {
443         CLog::Log(LOGWARNING, "No information found for item '%s', it won't be added to the library.", CURL::GetRedacted(pItem->GetPath()).c_str());
444       }
445
446       pURL = NULL;
447
448       // Keep track of directories we've seen
449       if (pItem->m_bIsFolder)
450         seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
451     }
452
453     if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
454     {
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)
458       {
459         if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
460           m_pathsToClean.insert(i->first);
461       }
462     }
463     if(pDlgProgress)
464       pDlgProgress->ShowProgressBar(false);
465
466     g_infoManager.ResetLibraryBools();
467     m_database.Close();
468     return FoundSomeInfo;
469   }
470
471   INFO_RET CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
472   {
473     long idTvShow = -1;
474     if (pItem->m_bIsFolder)
475       idTvShow = m_database.GetTvShowId(pItem->GetPath());
476     else
477     {
478       CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
479       idTvShow = m_database.GetTvShowId(strPath);
480     }
481     if (idTvShow > -1 && (fetchEpisodes || !pItem->m_bIsFolder))
482     {
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());
486       return ret;
487     }
488
489     if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361, pItem->GetLabel()))
490       return INFO_CANCELLED;
491
492     if (m_handle)
493       m_handle->SetText(pItem->GetMovieName(bDirNames));
494
495     CNfoFile::NFOResult result=CNfoFile::NO_NFO;
496     CScraperUrl scrUrl;
497     // handle .nfo files
498     if (useLocal)
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);
506     }
507     if (result == CNfoFile::FULL_NFO)
508     {
509       pItem->GetVideoInfoTag()->Reset();
510       m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
511
512       long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
513       if (lResult < 0)
514         return INFO_ERROR;
515       if (fetchEpisodes)
516       {
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());
520         return ret;
521       }
522       return INFO_ADDED;
523     }
524     if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
525       pURL = &scrUrl;
526
527     CScraperUrl url;
528     int retVal = 0;
529     if (pURL)
530       url = *pURL;
531     else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
532       return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
533
534     long lResult=-1;
535     if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
536     {
537       if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
538         return INFO_ERROR;
539     }
540     if (fetchEpisodes)
541     {
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());
545     }
546     return INFO_ADDED;
547   }
548
549   INFO_RET CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
550   {
551     if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
552        (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
553       return INFO_NOT_NEEDED;
554
555     if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
556       return INFO_CANCELLED;
557
558     if (m_database.HasMovieInfo(pItem->GetPath()))
559       return INFO_HAVE_ALREADY;
560
561     if (m_handle)
562       m_handle->SetText(pItem->GetMovieName(bDirNames));
563
564     CNfoFile::NFOResult result=CNfoFile::NO_NFO;
565     CScraperUrl scrUrl;
566     // handle .nfo files
567     if (useLocal)
568       result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
569     if (result == CNfoFile::FULL_NFO)
570     {
571       pItem->GetVideoInfoTag()->Reset();
572       m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
573
574       if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
575         return INFO_ERROR;
576       return INFO_ADDED;
577     }
578     if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
579       pURL = &scrUrl;
580
581     CScraperUrl url;
582     int retVal = 0;
583     if (pURL)
584       url = *pURL;
585     else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
586       return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
587
588     if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
589     {
590       if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
591         return INFO_ERROR;
592       return INFO_ADDED;
593     }
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;
596   }
597
598   INFO_RET CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem, bool bDirNames, ScraperPtr &info2, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress)
599   {
600     if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
601        (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
602       return INFO_NOT_NEEDED;
603
604     if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
605       return INFO_CANCELLED;
606
607     if (m_database.HasMusicVideoInfo(pItem->GetPath()))
608       return INFO_HAVE_ALREADY;
609
610     if (m_handle)
611       m_handle->SetText(pItem->GetMovieName(bDirNames));
612
613     CNfoFile::NFOResult result=CNfoFile::NO_NFO;
614     CScraperUrl scrUrl;
615     // handle .nfo files
616     if (useLocal)
617       result = CheckForNFOFile(pItem, bDirNames, info2, scrUrl);
618     if (result == CNfoFile::FULL_NFO)
619     {
620       pItem->GetVideoInfoTag()->Reset();
621       m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
622
623       if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
624         return INFO_ERROR;
625       return INFO_ADDED;
626     }
627     if (result == CNfoFile::URL_NFO || result == CNfoFile::COMBINED_NFO)
628       pURL = &scrUrl;
629
630     CScraperUrl url;
631     int retVal = 0;
632     if (pURL)
633       url = *pURL;
634     else if ((retVal = FindVideo(pItem->GetMovieName(bDirNames), info2, url, pDlgProgress)) <= 0)
635       return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
636
637     if (GetDetails(pItem, url, info2, result == CNfoFile::COMBINED_NFO ? &m_nfoReader : NULL, pDlgProgress))
638     {
639       if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
640         return INFO_ERROR;
641       return INFO_ADDED;
642     }
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;
645   }
646
647   INFO_RET CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item, long showID, const ADDON::ScraperPtr &scraper, bool useLocal, CGUIDialogProgress *progress)
648   {
649     // enumerate episodes
650     EPISODELIST files;
651     EnumerateSeriesFolder(item, files);
652     if (files.size() == 0) // no update or no files
653       return INFO_NOT_NEEDED;
654
655     if (m_bStop || (progress && progress->IsCanceled()))
656       return INFO_CANCELLED;
657
658     CVideoInfoTag showInfo;
659     m_database.GetTvShowInfo("", showInfo, showID);
660     return OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
661   }
662
663   void CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
664   {
665     CFileItemList items;
666
667     if (item->m_bIsFolder)
668     {
669       CUtil::GetRecursiveListing(item->GetPath(), items, g_advancedSettings.m_videoExtensions, true);
670       CStdString hash, dbHash;
671       int numFilesInFolder = GetPathHash(items, hash);
672
673       if (m_database.GetPathHash(item->GetPath(), dbHash) && dbHash == hash)
674       {
675         m_currentItem += numFilesInFolder;
676
677         // update our dialog with our progress
678         if (m_handle)
679         {
680           if (m_itemCount>0)
681             m_handle->SetPercentage(m_currentItem*100.f/m_itemCount);
682
683           OnDirectoryScanned(item->GetPath());
684         }
685         return;
686       }
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);
690     }
691     else
692     {
693       CFileItemPtr newItem(new CFileItem(*item));
694       items.Add(newItem);
695     }
696
697     /*
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
701
702     /foo/bar/video_ts.ifo
703     /foo/bar/vts_x_y.ifo
704     /foo/bar/vts_x_y.vob
705     */
706
707     // since we're doing this now anyway, should other items be stacked?
708     items.Sort(SortByPath, SortOrderAscending);
709     int x = 0;
710     while (x < items.Size())
711     {
712       if (items[x]->m_bIsFolder)
713         continue;
714
715
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());
719
720       int y = x + 1;
721       if (strFileX.Equals("VIDEO_TS.IFO"))
722       {
723         while (y < items.Size())
724         {
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());
728
729           if (strPathY.Equals(strPathX))
730             /*
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.
735             */
736             items.Remove(y);
737           else
738             break;
739         }
740       }
741       x = y;
742     }
743
744     // enumerate
745     CStdStringArray regexps = g_advancedSettings.m_tvshowExcludeFromScanRegExps;
746
747     for (int i=0;i<items.Size();++i)
748     {
749       if (items[i]->m_bIsFolder)
750         continue;
751       CStdString strPath = URIUtils::GetDirectory(items[i]->GetPath());
752       URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
753
754       if (URIUtils::GetFileName(strPath).Equals("sample"))
755         continue;
756
757       // Discard all exclude files defined by regExExcludes
758       if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
759         continue;
760
761       /*
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.
765        */
766       if (ProcessItemByVideoInfoTag(items[i].get(), episodeList))
767         continue;
768
769       if (!EnumerateEpisodeItem(items[i].get(), episodeList))
770       {
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());
774       }
775     }
776   }
777
778   bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
779   {
780     if (!item->HasVideoInfoTag())
781       return false;
782
783     const CVideoInfoTag* tag = item->GetVideoInfoTag();
784     /*
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.
787      */
788     if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
789     {
790       EPISODE episode;
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);
798       return true;
799     }
800
801     /*
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.
804      */
805     if (tag->m_firstAired.IsValid())
806     {
807       EPISODE episode;
808       episode.strPath = item->GetPath();
809       episode.strTitle = tag->m_strTitle;
810       episode.isFolder = false;
811       /*
812        * Set season and episode to -1 to indicate to use the aired date.
813        */
814       episode.iSeason = -1;
815       episode.iEpisode = -1;
816       /*
817        * The first aired date string must be parseable.
818        */
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());
824       return true;
825     }
826
827     /*
828      * Next preference is the episode title. If it exists use that for matching the TV Show
829      * information.
830      */
831     if (!tag->m_strTitle.empty())
832     {
833       EPISODE episode;
834       episode.strPath = item->GetPath();
835       episode.strTitle = tag->m_strTitle;
836       episode.isFolder = false;
837       /*
838        * Set season and episode to -1 to indicate to use the title.
839        */
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());
845       return true;
846     }
847
848     /*
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.
852      */
853     if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
854     {
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());
857       return true;
858     }
859
860     return false;
861   }
862
863   bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
864   {
865     SETTINGS_TVSHOWLIST expression = g_advancedSettings.m_tvshowEnumRegExps;
866
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
871     for (unsigned int i=0;i<expression.size();++i)
872     {
873       CRegExp reg(true, CRegExp::autoUtf8);
874       if (!reg.RegComp(expression[i].regexp))
875         continue;
876
877       int regexppos, regexp2pos;
878       //CLog::Log(LOGDEBUG,"running expression %s on %s",expression[i].regexp.c_str(),strLabel.c_str());
879       if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
880         continue;
881
882       EPISODE episode;
883       episode.strPath = item->GetPath();
884       episode.iSeason = -1;
885       episode.iEpisode = -1;
886       episode.cDate.SetValid(false);
887       episode.isFolder = false;
888
889       bool byDate = expression[i].byDate ? true : false;
890       int defaultSeason = expression[i].defaultSeason;
891
892       if (byDate)
893       {
894         if (!GetAirDateFromRegExp(reg, episode))
895           continue;
896
897         CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match %s (%s) [%s]", strLabel.c_str(),
898                   episode.cDate.GetAsLocalizedDate().c_str(), expression[i].regexp.c_str());
899       }
900       else
901       {
902         if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
903           continue;
904
905         CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match %s (s%ie%i) [%s]", strLabel.c_str(),
906                   episode.iSeason, episode.iEpisode, expression[i].regexp.c_str());
907       }
908
909       // Grab the remainder from first regexp run
910       // as second run might modify or empty it.
911       std::string remainder(reg.GetMatch(3));
912
913       /*
914        * Check if the files base path is a dedicated folder that contains
915        * only this single episode. If season and episode match with the
916        * actual media file, we set episode.isFolder to true.
917        */
918       CStdString strBasePath = item->GetBaseMoviePath(true);
919       URIUtils::RemoveSlashAtEnd(strBasePath);
920       strBasePath = URIUtils::GetFileName(strBasePath);
921
922       if (reg.RegFind(strBasePath.c_str()) > -1)
923       {
924         EPISODE parent;
925         if (byDate)
926         {
927           GetAirDateFromRegExp(reg, parent);
928           if (episode.cDate == parent.cDate)
929             episode.isFolder = true;
930         }
931         else
932         {
933           GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
934           if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
935             episode.isFolder = true;
936         }
937       }
938
939       // add what we found by now
940       episodeList.push_back(episode);
941
942       CRegExp reg2(true, CRegExp::autoUtf8);
943       // check the remainder of the string for any further episodes.
944       if (!byDate && reg2.RegComp(g_advancedSettings.m_tvshowMultiPartEnumRegExp))
945       {
946         int offset = 0;
947
948         // we want "long circuit" OR below so that both offsets are evaluated
949         while (((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) | ((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
950         {
951           if (((regexppos <= regexp2pos) && regexppos != -1) ||
952              (regexppos >= 0 && regexp2pos == -1))
953           {
954             GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
955
956             CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season %u, multipart episode %u [%s]",
957                       episode.iSeason, episode.iEpisode,
958                       g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
959
960             episodeList.push_back(episode);
961             remainder = reg.GetMatch(3);
962             offset = 0;
963           }
964           else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
965                    (regexp2pos >= 0 && regexppos == -1))
966           {
967             episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
968             CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode %u [%s]",
969                       episode.iEpisode, g_advancedSettings.m_tvshowMultiPartEnumRegExp.c_str());
970             episodeList.push_back(episode);
971             offset += regexp2pos + reg2.GetFindLen();
972           }
973         }
974       }
975       return true;
976     }
977     return false;
978   }
979
980   bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp &reg, EPISODE &episodeInfo, int defaultSeason)
981   {
982     std::string season(reg.GetMatch(1));
983     std::string episode(reg.GetMatch(2));
984
985     if (!season.empty() || !episode.empty())
986     {
987       char* endptr = NULL;
988       if (season.empty() && !episode.empty())
989       { // no season specified -> assume defaultSeason
990         episodeInfo.iSeason = defaultSeason;
991         if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
992           episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
993       }
994       else if (!season.empty() && episode.empty())
995       { // no episode specification -> assume defaultSeason
996         episodeInfo.iSeason = defaultSeason;
997         if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
998           episodeInfo.iEpisode = atoi(season.c_str());
999       }
1000       else
1001       { // season and episode specified
1002         episodeInfo.iSeason = atoi(season.c_str());
1003         episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
1004       }
1005       if (endptr)
1006       {
1007         if (isalpha(*endptr))
1008           episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
1009         else if (*endptr == '.')
1010           episodeInfo.iSubepisode = atoi(endptr+1);
1011       }
1012       return true;
1013     }
1014     return false;
1015   }
1016
1017   bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp &reg, EPISODE &episodeInfo)
1018   {
1019     std::string param1(reg.GetMatch(1));
1020     std::string param2(reg.GetMatch(2));
1021     std::string param3(reg.GetMatch(3));
1022
1023     if (!param1.empty() && !param2.empty() && !param3.empty())
1024     {
1025       // regular expression by date
1026       int len1 = param1.size();
1027       int len2 = param2.size();
1028       int len3 = param3.size();
1029
1030       if (len1==4 && len2==2 && len3==2)
1031       {
1032         // yyyy mm dd format
1033         episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
1034       }
1035       else if (len1==2 && len2==2 && len3==4)
1036       {
1037         // mm dd yyyy format
1038         episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
1039       }
1040     }
1041     return episodeInfo.cDate.IsValid();
1042   }
1043
1044   long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
1045   {
1046     // ensure our database is open (this can get called via other classes)
1047     if (!m_database.Open())
1048       return -1;
1049
1050     if (!libraryImport)
1051       GetArtwork(pItem, content, videoFolder, useLocal, showInfo ? showInfo->m_strPath : "");
1052
1053     // ensure the art map isn't completely empty by specifying an empty thumb
1054     map<string, string> art = pItem->GetArt();
1055     if (art.empty())
1056       art["thumb"] = "";
1057
1058     CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1059     if (movieDetails.m_basePath.empty())
1060       movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
1061     movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
1062
1063     movieDetails.m_strFileNameAndPath = pItem->GetPath();
1064
1065     if (pItem->m_bIsFolder)
1066       movieDetails.m_strPath = pItem->GetPath();
1067
1068     CStdString strTitle(movieDetails.m_strTitle);
1069
1070     if (showInfo && content == CONTENT_TVSHOWS)
1071     {
1072       strTitle = StringUtils::Format("%s - %ix%i - %s", showInfo->m_strTitle.c_str(), movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle.c_str());
1073     }
1074
1075     std::string redactPath = pItem->GetPath();
1076     CURL::Decode(redactPath);
1077     redactPath = CURL::GetRedacted(redactPath);
1078
1079     CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to %s:%s", TranslateContent(content).c_str(), redactPath.c_str());
1080     long lResult = -1;
1081
1082     if (content == CONTENT_MOVIES)
1083     {
1084       // find local trailer first
1085       CStdString strTrailer = pItem->FindTrailer();
1086       if (!strTrailer.empty())
1087         movieDetails.m_strTrailer = strTrailer;
1088
1089       lResult = m_database.SetDetailsForMovie(pItem->GetPath(), movieDetails, art);
1090       movieDetails.m_iDbId = lResult;
1091       movieDetails.m_type = "movie";
1092
1093       // setup links to shows if the linked shows are in the db
1094       for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
1095       {
1096         CFileItemList items;
1097         m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
1098         if (items.Size())
1099           m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
1100         else
1101           CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie %s to show %s", movieDetails.m_strTitle.c_str(), movieDetails.m_showLink[i].c_str());
1102       }
1103     }
1104     else if (content == CONTENT_TVSHOWS)
1105     {
1106       if (pItem->m_bIsFolder)
1107       {
1108         map<int, map<string, string> > seasonArt;
1109         if (!libraryImport)
1110         { // get and cache season thumbs
1111           GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes("season"), useLocal);
1112           for (map<int, map<string, string> >::iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
1113             for (map<string, string>::iterator j = i->second.begin(); j != i->second.end(); ++j)
1114               CTextureCache::Get().BackgroundCacheImage(j->second);
1115         }
1116         lResult = m_database.SetDetailsForTvShow(pItem->GetPath(), movieDetails, art, seasonArt);
1117         movieDetails.m_iDbId = lResult;
1118         movieDetails.m_type = "tvshow";
1119       }
1120       else
1121       {
1122         // we add episode then set details, as otherwise set details will delete the
1123         // episode then add, which breaks multi-episode files.
1124         int idShow = showInfo ? showInfo->m_iDbId : -1;
1125         int idEpisode = m_database.AddEpisode(idShow, pItem->GetPath());
1126         lResult = m_database.SetDetailsForEpisode(pItem->GetPath(), movieDetails, art, idShow, idEpisode);
1127         movieDetails.m_iDbId = lResult;
1128         movieDetails.m_type = "episode";
1129         movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
1130         if (movieDetails.m_fEpBookmark > 0)
1131         {
1132           movieDetails.m_strFileNameAndPath = pItem->GetPath();
1133           CBookmark bookmark;
1134           bookmark.timeInSeconds = movieDetails.m_fEpBookmark;
1135           bookmark.seasonNumber = movieDetails.m_iSeason;
1136           bookmark.episodeNumber = movieDetails.m_iEpisode;
1137           m_database.AddBookMarkForEpisode(movieDetails, bookmark);
1138         }
1139       }
1140     }
1141     else if (content == CONTENT_MUSICVIDEOS)
1142     {
1143       lResult = m_database.SetDetailsForMusicVideo(pItem->GetPath(), movieDetails, art);
1144       movieDetails.m_iDbId = lResult;
1145       movieDetails.m_type = "musicvideo";
1146     }
1147
1148     if (g_advancedSettings.m_bVideoLibraryImportWatchedState || libraryImport)
1149       m_database.SetPlayCount(*pItem, movieDetails.m_playCount, movieDetails.m_lastPlayed);
1150
1151     if ((g_advancedSettings.m_bVideoLibraryImportResumePoint || libraryImport) &&
1152         movieDetails.m_resumePoint.IsSet())
1153       m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.m_resumePoint, CBookmark::RESUME);
1154
1155     m_database.Close();
1156
1157     CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
1158     ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", itemCopy);
1159     return lResult;
1160   }
1161
1162   string ContentToMediaType(CONTENT_TYPE content, bool folder)
1163   {
1164     switch (content)
1165     {
1166       case CONTENT_MOVIES:
1167         return "movie";
1168       case CONTENT_MUSICVIDEOS:
1169         return "musicvideo";
1170       case CONTENT_TVSHOWS:
1171         return folder ? "tvshow" : "episode";
1172       default:
1173         return "";
1174     }
1175   }
1176
1177   std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
1178   {
1179     std::string type = "thumb";
1180     if (width*5 < height*4)
1181       type = "poster";
1182     else if (width*1 > height*4)
1183       type = "banner";
1184     return type;
1185   }
1186
1187   void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
1188   {
1189     CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
1190     movieDetails.m_fanart.Unpack();
1191     movieDetails.m_strPictureURL.Parse();
1192
1193     CGUIListItem::ArtMap art = pItem->GetArt();
1194
1195     // get and cache thumb images
1196     vector<string> artTypes = CVideoThumbLoader::GetArtTypes(ContentToMediaType(content, pItem->m_bIsFolder));
1197     vector<string>::iterator i = find(artTypes.begin(), artTypes.end(), "fanart");
1198     if (i != artTypes.end())
1199       artTypes.erase(i); // fanart is handled below
1200     bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end() &&
1201                         art.find("thumb") == art.end();
1202     // find local art
1203     if (useLocal)
1204     {
1205       for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1206       {
1207         if (art.find(*i) == art.end())
1208         {
1209           std::string image = CVideoThumbLoader::GetLocalArt(*pItem, *i, bApplyToDir);
1210           if (!image.empty())
1211             art.insert(make_pair(*i, image));
1212         }
1213       }
1214       // find and classify the local thumb (backcompat) if available
1215       if (lookForThumb)
1216       {
1217         std::string image = CVideoThumbLoader::GetLocalArt(*pItem, "thumb", bApplyToDir);
1218         if (!image.empty())
1219         { // cache the image and determine sizing
1220           CTextureDetails details;
1221           if (CTextureCache::Get().CacheImage(image, details))
1222           {
1223             std::string type = GetArtTypeFromSize(details.width, details.height);
1224             if (art.find(type) == art.end())
1225               art.insert(make_pair(type, image));
1226           }
1227         }
1228       }
1229     }
1230
1231     // find online art
1232     for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1233     {
1234       if (art.find(*i) == art.end())
1235       {
1236         std::string image = GetImage(pItem, false, bApplyToDir, *i);
1237         if (!image.empty())
1238           art.insert(make_pair(*i, image));
1239       }
1240     }
1241
1242     // use the first piece of online art as the first art type if no thumb type is available yet
1243     if (art.empty() && lookForThumb)
1244     {
1245       std::string image = GetImage(pItem, false, bApplyToDir, "thumb");
1246       if (!image.empty())
1247         art.insert(make_pair(artTypes.front(), image));
1248     }
1249
1250     // get & save fanart image (treated separately due to it being stored in m_fanart)
1251     bool isEpisode = (content == CONTENT_TVSHOWS && !pItem->m_bIsFolder);
1252     if (!isEpisode && art.find("fanart") == art.end())
1253     {
1254       string fanart = GetFanart(pItem, useLocal);
1255       if (!fanart.empty())
1256         art.insert(make_pair("fanart", fanart));
1257     }
1258
1259     for (CGUIListItem::ArtMap::const_iterator i = art.begin(); i != art.end(); ++i)
1260       CTextureCache::Get().BackgroundCacheImage(i->second);
1261
1262     pItem->SetArt(art);
1263
1264     // parent folder to apply the thumb to and to search for local actor thumbs
1265     CStdString parentDir = GetParentDir(*pItem);
1266     if (CSettings::Get().GetBool("videolibrary.actorthumbs"))
1267       FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
1268     if (bApplyToDir)
1269       ApplyThumbToFolder(parentDir, art["thumb"]);
1270   }
1271
1272   std::string CVideoInfoScanner::GetImage(CFileItem *pItem, bool useLocal, bool bApplyToDir, const std::string &type)
1273   {
1274     std::string thumb;
1275     if (useLocal)
1276       thumb = CVideoThumbLoader::GetLocalArt(*pItem, type, bApplyToDir);
1277
1278     if (thumb.empty())
1279     {
1280       thumb = CScraperUrl::GetThumbURL(pItem->GetVideoInfoTag()->m_strPictureURL.GetFirstThumb(type));
1281       if (!thumb.empty())
1282       {
1283         if (thumb.find("http://") == string::npos &&
1284             thumb.find("/") == string::npos &&
1285             thumb.find("\\") == string::npos)
1286         {
1287           CStdString strPath = URIUtils::GetDirectory(pItem->GetPath());
1288           thumb = URIUtils::AddFileToFolder(strPath, thumb);
1289         }
1290       }
1291     }
1292     return thumb;
1293   }
1294
1295   std::string CVideoInfoScanner::GetFanart(CFileItem *pItem, bool useLocal)
1296   {
1297     std::string fanart = pItem->GetArt("fanart");
1298     if (fanart.empty() && useLocal)
1299       fanart = pItem->FindLocalArt("fanart.jpg", true);
1300     if (fanart.empty())
1301       fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
1302     return fanart;
1303   }
1304
1305   INFO_RET CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress /* = NULL */)
1306   {
1307     if (pDlgProgress)
1308     {
1309       pDlgProgress->SetLine(1, showInfo.m_strTitle);
1310       pDlgProgress->SetLine(2, 20361);
1311       pDlgProgress->SetPercentage(0);
1312       pDlgProgress->ShowProgressBar(true);
1313       pDlgProgress->Progress();
1314     }
1315
1316     EPISODELIST episodes;
1317     bool hasEpisodeGuide = false;
1318
1319     int iMax = files.size();
1320     int iCurr = 1;
1321     for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
1322     {
1323       m_nfoReader.Close();
1324       if (pDlgProgress)
1325       {
1326         pDlgProgress->SetLine(2, 20361);
1327         pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
1328         pDlgProgress->Progress();
1329       }
1330       if (m_handle)
1331         m_handle->SetPercentage(100.f*iCurr++/iMax);
1332
1333       if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
1334         return INFO_CANCELLED;
1335
1336       if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
1337       {
1338         if (m_handle)
1339           m_handle->SetText(g_localizeStrings.Get(20415));
1340         continue;
1341       }
1342
1343       CFileItem item;
1344       item.SetPath(file->strPath);
1345
1346       // handle .nfo files
1347       CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1348       CScraperUrl scrUrl;
1349       ScraperPtr info(scraper);
1350       item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1351       if (useLocal)
1352         result = CheckForNFOFile(&item, false, info,scrUrl);
1353       if (result == CNfoFile::FULL_NFO)
1354       {
1355         m_nfoReader.GetDetails(*item.GetVideoInfoTag());
1356         // override with episode and season number from file if available
1357         if (file->iEpisode > -1)
1358         {
1359           item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
1360           item.GetVideoInfoTag()->m_iSeason = file->iSeason;
1361         }
1362         if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
1363           return INFO_ERROR;
1364         continue;
1365       }
1366
1367       if (!hasEpisodeGuide)
1368       {
1369         // fetch episode guide
1370         if (!showInfo.m_strEpisodeGuide.empty())
1371         {
1372           CScraperUrl url;
1373           url.ParseEpisodeGuide(showInfo.m_strEpisodeGuide);
1374
1375           if (pDlgProgress)
1376           {
1377             pDlgProgress->SetLine(2, 20354);
1378             pDlgProgress->Progress();
1379           }
1380
1381           CVideoInfoDownloader imdb(scraper);
1382           if (!imdb.GetEpisodeList(url, episodes))
1383             return INFO_NOT_FOUND;
1384
1385           hasEpisodeGuide = true;
1386         }
1387       }
1388
1389       if (episodes.empty())
1390       {
1391         CLog::Log(LOGERROR, "VideoInfoScanner: Asked to lookup episode %s"
1392                             " online, but we have no episode guide. Check your tvshow.nfo and make"
1393                             " sure the <episodeguide> tag is in place.", file->strPath.c_str());
1394         continue;
1395       }
1396
1397       EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
1398       EPISODE backupkey(file->iSeason, file->iEpisode, 0);
1399       bool bFound = false;
1400       EPISODELIST::iterator guide = episodes.begin();;
1401       EPISODELIST matches;
1402
1403       for (; guide != episodes.end(); ++guide )
1404       {
1405         if ((file->iEpisode!=-1) && (file->iSeason!=-1))
1406         {
1407           if (key==*guide)
1408           {
1409             bFound = true;
1410             break;
1411           }
1412           else if ((file->iSubepisode!=0) && (backupkey==*guide))
1413           {
1414             matches.push_back(*guide);
1415             continue;
1416           }
1417         }
1418         if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
1419         {
1420           matches.push_back(*guide);
1421           continue;
1422         }
1423         if (!guide->cScraperUrl.strTitle.empty() && StringUtils::EqualsNoCase(guide->cScraperUrl.strTitle, file->strTitle))
1424         {
1425           bFound = true;
1426           break;
1427         }
1428       }
1429
1430       if (!bFound)
1431       {
1432         /*
1433          * If there is only one match or there are matches but no title to compare with to help
1434          * identify the best match, then pick the first match as the best possible candidate.
1435          *
1436          * Otherwise, use the title to further refine the best match.
1437          */
1438         if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
1439         {
1440           guide = matches.begin();
1441           bFound = true;
1442         }
1443         else if (!file->strTitle.empty())
1444         {
1445           double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
1446
1447           EPISODELIST *candidates;
1448           if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
1449           {
1450             minscore = 0.8; // 80% should ensure a good match.
1451             candidates = &episodes;
1452           }
1453           else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
1454             candidates = &matches;
1455
1456           CStdStringArray titles;
1457           for (guide = candidates->begin(); guide != candidates->end(); ++guide)
1458           {
1459             StringUtils::ToLower(guide->cScraperUrl.strTitle);
1460             titles.push_back(guide->cScraperUrl.strTitle);
1461           }
1462
1463           double matchscore;
1464           std::string loweredTitle(file->strTitle);
1465           StringUtils::ToLower(loweredTitle);
1466           int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
1467           if (matchscore >= minscore)
1468           {
1469             guide = candidates->begin() + index;
1470             bFound = true;
1471             CLog::Log(LOGDEBUG,"%s fuzzy title match for show: '%s', title: '%s', match: '%s', score: %f >= %f",
1472                       __FUNCTION__, showInfo.m_strTitle.c_str(), file->strTitle.c_str(), titles[index].c_str(), matchscore, minscore);
1473           }
1474         }
1475       }
1476
1477       if (bFound)
1478       {
1479         CVideoInfoDownloader imdb(scraper);
1480         CFileItem item;
1481         item.SetPath(file->strPath);
1482         if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
1483           return INFO_NOT_FOUND; // TODO: should we just skip to the next episode?
1484           
1485         // Only set season/epnum from filename when it is not already set by a scraper
1486         if (item.GetVideoInfoTag()->m_iSeason == -1)
1487           item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
1488         if (item.GetVideoInfoTag()->m_iEpisode == -1)
1489           item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
1490           
1491         if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
1492           return INFO_ERROR;
1493       }
1494       else
1495       {
1496         CLog::Log(LOGDEBUG,"%s - no match for show: '%s', season: %d, episode: %d.%d, airdate: '%s', title: '%s'",
1497                   __FUNCTION__, showInfo.m_strTitle.c_str(), file->iSeason, file->iEpisode, file->iSubepisode,
1498                   file->cDate.GetAsLocalizedDate().c_str(), file->strTitle.c_str());
1499       }
1500     }
1501     return INFO_ADDED;
1502   }
1503
1504   CStdString CVideoInfoScanner::GetnfoFile(CFileItem *item, bool bGrabAny) const
1505   {
1506     CStdString nfoFile;
1507     // Find a matching .nfo file
1508     if (!item->m_bIsFolder)
1509     {
1510       if (URIUtils::IsInRAR(item->GetPath())) // we have a rarred item - we want to check outside the rars
1511       {
1512         CFileItem item2(*item);
1513         CURL url(item->GetPath());
1514         CStdString strPath = URIUtils::GetDirectory(url.GetHostName());
1515         item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1516         return GetnfoFile(&item2, bGrabAny);
1517       }
1518
1519       // grab the folder path
1520       CStdString strPath = URIUtils::GetDirectory(item->GetPath());
1521
1522       if (bGrabAny && !item->IsStack())
1523       { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
1524         nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
1525         if (CFile::Exists(nfoFile))
1526           return nfoFile;
1527       }
1528
1529       // try looking for .nfo file for a stacked item
1530       if (item->IsStack())
1531       {
1532         // first try .nfo file matching first file in stack
1533         CStackDirectory dir;
1534         CStdString firstFile = dir.GetFirstStackedFile(item->GetPath());
1535         CFileItem item2;
1536         item2.SetPath(firstFile);
1537         nfoFile = GetnfoFile(&item2, bGrabAny);
1538         // else try .nfo file matching stacked title
1539         if (nfoFile.empty())
1540         {
1541           CStdString stackedTitlePath = dir.GetStackedTitlePath(item->GetPath());
1542           item2.SetPath(stackedTitlePath);
1543           nfoFile = GetnfoFile(&item2, bGrabAny);
1544         }
1545       }
1546       else
1547       {
1548         // already an .nfo file?
1549         if (URIUtils::HasExtension(item->GetPath(), ".nfo"))
1550           nfoFile = item->GetPath();
1551         // no, create .nfo file
1552         else
1553           nfoFile = URIUtils::ReplaceExtension(item->GetPath(), ".nfo");
1554       }
1555
1556       // test file existence
1557       if (!nfoFile.empty() && !CFile::Exists(nfoFile))
1558         nfoFile.clear();
1559
1560       if (nfoFile.empty()) // final attempt - strip off any cd1 folders
1561       {
1562         URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
1563         CFileItem item2;
1564         if (StringUtils::EndsWithNoCase(strPath, "cd1"))
1565         {
1566           strPath.erase(strPath.size() - 3);
1567           item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item->GetPath())));
1568           return GetnfoFile(&item2, bGrabAny);
1569         }
1570       }
1571
1572       if (nfoFile.empty() && item->IsOpticalMediaFile())
1573       {
1574         CFileItem parentDirectory(item->GetLocalMetadataPath(), true);
1575         nfoFile = GetnfoFile(&parentDirectory, true);
1576       }
1577     }
1578     // folders (or stacked dvds) can take any nfo file if there's a unique one
1579     if (item->m_bIsFolder || item->IsOpticalMediaFile() || (bGrabAny && nfoFile.empty()))
1580     {
1581       // see if there is a unique nfo file in this folder, and if so, use that
1582       CFileItemList items;
1583       CDirectory dir;
1584       CStdString strPath;
1585       if (item->m_bIsFolder)
1586         strPath = item->GetPath();
1587       else
1588         strPath = URIUtils::GetDirectory(item->GetPath());
1589
1590       if (dir.GetDirectory(strPath, items, ".nfo") && items.Size())
1591       {
1592         int numNFO = -1;
1593         for (int i = 0; i < items.Size(); i++)
1594         {
1595           if (items[i]->IsNFO())
1596           {
1597             if (numNFO == -1)
1598               numNFO = i;
1599             else
1600             {
1601               numNFO = -1;
1602               break;
1603             }
1604           }
1605         }
1606         if (numNFO > -1)
1607           return items[numNFO]->GetPath();
1608       }
1609     }
1610
1611     return nfoFile;
1612   }
1613
1614   bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url, const ScraperPtr& scraper, CNfoFile *nfoFile, CGUIDialogProgress* pDialog /* = NULL */)
1615   {
1616     CVideoInfoTag movieDetails;
1617
1618     if (m_handle && !url.strTitle.empty())
1619       m_handle->SetText(url.strTitle);
1620
1621     CVideoInfoDownloader imdb(scraper);
1622     bool ret = imdb.GetDetails(url, movieDetails, pDialog);
1623
1624     if (ret)
1625     {
1626       if (nfoFile)
1627         nfoFile->GetDetails(movieDetails,NULL,true);
1628
1629       if (m_handle && url.strTitle.empty())
1630         m_handle->SetText(movieDetails.m_strTitle);
1631
1632       if (pDialog)
1633       {
1634         pDialog->SetLine(1, movieDetails.m_strTitle);
1635         pDialog->Progress();
1636       }
1637
1638       *pItem->GetVideoInfoTag() = movieDetails;
1639       return true;
1640     }
1641     return false; // no info found, or cancelled
1642   }
1643
1644   void CVideoInfoScanner::ApplyThumbToFolder(const CStdString &folder, const CStdString &imdbThumb)
1645   {
1646     // copy icon to folder also;
1647     if (!imdbThumb.empty())
1648     {
1649       CFileItem folderItem(folder, true);
1650       CThumbLoader loader;
1651       loader.SetCachedImage(folderItem, "thumb", imdbThumb);
1652     }
1653   }
1654
1655   int CVideoInfoScanner::GetPathHash(const CFileItemList &items, CStdString &hash)
1656   {
1657     // Create a hash based on the filenames, filesize and filedate.  Also count the number of files
1658     if (0 == items.Size()) return 0;
1659     XBMC::XBMC_MD5 md5state;
1660     int count = 0;
1661     for (int i = 0; i < items.Size(); ++i)
1662     {
1663       const CFileItemPtr pItem = items[i];
1664       md5state.append(pItem->GetPath());
1665       md5state.append((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
1666       FILETIME time = pItem->m_dateTime;
1667       md5state.append((unsigned char *)&time, sizeof(FILETIME));
1668       if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
1669         count++;
1670     }
1671     md5state.getDigest(hash);
1672     return count;
1673   }
1674
1675   bool CVideoInfoScanner::CanFastHash(const CFileItemList &items) const
1676   {
1677     // TODO: Probably should account for excluded folders here (eg samples), though that then
1678     //       introduces possible problems if the user then changes the exclude regexps and
1679     //       expects excluded folders that are inside a fast-hashed folder to then be picked
1680     //       up. The chances that the user has a folder which contains only excluded folders
1681     //       where some of those folders should be scanned recursively is pretty small.
1682     return items.GetFolderCount() == 0;
1683   }
1684
1685   CStdString CVideoInfoScanner::GetFastHash(const CStdString &directory) const
1686   {
1687     struct __stat64 buffer;
1688     if (XFILE::CFile::Stat(directory, &buffer) == 0)
1689     {
1690       int64_t time = buffer.st_mtime;
1691       if (!time)
1692         time = buffer.st_ctime;
1693       if (time)
1694         return StringUtils::Format("fast%"PRId64, time);
1695     }
1696     return "";
1697   }
1698
1699   void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show, map<int, map<string, string> > &seasonArt, const vector<string> &artTypes, bool useLocal)
1700   {
1701     bool lookForThumb = find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end();
1702
1703     // find the maximum number of seasons we have thumbs for (local + remote)
1704     int maxSeasons = show.m_strPictureURL.GetMaxSeasonThumb();
1705
1706     CFileItemList items;
1707     CDirectory::GetDirectory(show.m_strPath, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
1708     CRegExp reg;
1709     if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z]+)?\\.(tbn|jpg|png)"))
1710     {
1711       for (int i = 0; i < items.Size(); i++)
1712       {
1713         CStdString name = URIUtils::GetFileName(items[i]->GetPath());
1714         if (reg.RegFind(name) > -1)
1715         {
1716           int season = atoi(reg.GetMatch(1).c_str());
1717           if (season > maxSeasons)
1718             maxSeasons = season;
1719         }
1720       }
1721     }
1722     for (int season = -1; season <= maxSeasons; season++)
1723     {
1724       map<string, string> art;
1725       if (useLocal)
1726       {
1727         string basePath;
1728         if (season == -1)
1729           basePath = "season-all";
1730         else if (season == 0)
1731           basePath = "season-specials";
1732         else
1733           basePath = StringUtils::Format("season%02i", season);
1734         CFileItem artItem(URIUtils::AddFileToFolder(show.m_strPath, basePath), false);
1735
1736         for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1737         {
1738           std::string image = CVideoThumbLoader::GetLocalArt(artItem, *i, false);
1739           if (!image.empty())
1740             art.insert(make_pair(*i, image));
1741         }
1742         // find and classify the local thumb (backcompat) if available
1743         if (lookForThumb)
1744         {
1745           std::string image = CVideoThumbLoader::GetLocalArt(artItem, "thumb", false);
1746           if (!image.empty())
1747           { // cache the image and determine sizing
1748             CTextureDetails details;
1749             if (CTextureCache::Get().CacheImage(image, details))
1750             {
1751               std::string type = GetArtTypeFromSize(details.width, details.height);
1752               if (art.find(type) == art.end())
1753                 art.insert(make_pair(type, image));
1754             }
1755           }
1756         }
1757       }
1758
1759       // find online art
1760       for (vector<string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
1761       {
1762         if (art.find(*i) == art.end())
1763         {
1764           string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, *i));
1765           if (!image.empty())
1766             art.insert(make_pair(*i, image));
1767         }
1768       }
1769       // use the first piece of online art as the first art type if no thumb type is available yet
1770       if (art.empty() && lookForThumb)
1771       {
1772         string image = CScraperUrl::GetThumbURL(show.m_strPictureURL.GetSeasonThumb(season, "thumb"));
1773         if (!image.empty())
1774           art.insert(make_pair(artTypes.front(), image));
1775       }
1776
1777       seasonArt.insert(make_pair(season, art));
1778     }
1779   }
1780
1781   void CVideoInfoScanner::FetchActorThumbs(vector<SActorInfo>& actors, const CStdString& strPath)
1782   {
1783     CFileItemList items;
1784     CStdString actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
1785     if (CDirectory::Exists(actorsDir))
1786       CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
1787                                DIR_FLAG_NO_FILE_INFO);
1788     for (vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
1789     {
1790       if (i->thumb.empty())
1791       {
1792         CStdString thumbFile = i->strName;
1793         StringUtils::Replace(thumbFile, ' ', '_');
1794         for (int j = 0; j < items.Size(); j++)
1795         {
1796           CStdString compare = URIUtils::GetFileName(items[j]->GetPath());
1797           URIUtils::RemoveExtension(compare);
1798           if (!items[j]->m_bIsFolder && compare == thumbFile)
1799           {
1800             i->thumb = items[j]->GetPath();
1801             break;
1802           }
1803         }
1804         if (i->thumb.empty() && !i->thumbUrl.GetFirstThumb().m_url.empty())
1805           i->thumb = CScraperUrl::GetThumbURL(i->thumbUrl.GetFirstThumb());
1806         if (!i->thumb.empty())
1807           CTextureCache::Get().BackgroundCacheImage(i->thumb);
1808       }
1809     }
1810   }
1811
1812   CNfoFile::NFOResult CVideoInfoScanner::CheckForNFOFile(CFileItem* pItem, bool bGrabAny, ScraperPtr& info, CScraperUrl& scrUrl)
1813   {
1814     CStdString strNfoFile;
1815     if (info->Content() == CONTENT_MOVIES || info->Content() == CONTENT_MUSICVIDEOS
1816         || (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder))
1817       strNfoFile = GetnfoFile(pItem, bGrabAny);
1818     if (info->Content() == CONTENT_TVSHOWS && pItem->m_bIsFolder)
1819       strNfoFile = URIUtils::AddFileToFolder(pItem->GetPath(), "tvshow.nfo");
1820
1821     CNfoFile::NFOResult result=CNfoFile::NO_NFO;
1822     if (!strNfoFile.empty() && CFile::Exists(strNfoFile))
1823     {
1824       if (info->Content() == CONTENT_TVSHOWS && !pItem->m_bIsFolder)
1825         result = m_nfoReader.Create(strNfoFile,info,pItem->GetVideoInfoTag()->m_iEpisode);
1826       else
1827         result = m_nfoReader.Create(strNfoFile,info);
1828
1829       CStdString type;
1830       switch(result)
1831       {
1832         case CNfoFile::COMBINED_NFO:
1833           type = "Mixed";
1834           break;
1835         case CNfoFile::FULL_NFO:
1836           type = "Full";
1837           break;
1838         case CNfoFile::URL_NFO:
1839           type = "URL";
1840           break;
1841         case CNfoFile::NO_NFO:
1842           type = "";
1843           break;
1844         default:
1845           type = "malformed";
1846       }
1847       if (result != CNfoFile::NO_NFO)
1848         CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching %s NFO file: %s", type.c_str(), CURL::GetRedacted(strNfoFile).c_str());
1849       if (result == CNfoFile::FULL_NFO)
1850       {
1851         if (info->Content() == CONTENT_TVSHOWS)
1852           info = m_nfoReader.GetScraperInfo();
1853       }
1854       else if (result != CNfoFile::NO_NFO && result != CNfoFile::ERROR_NFO)
1855       {
1856         scrUrl = m_nfoReader.ScraperUrl();
1857         info = m_nfoReader.GetScraperInfo();
1858
1859         CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '%s' using %s scraper (content: '%s')",
1860           scrUrl.m_url[0].m_url.c_str(), info->Name().c_str(), TranslateContent(info->Content()).c_str());
1861
1862         if (result == CNfoFile::COMBINED_NFO)
1863           m_nfoReader.GetDetails(*pItem->GetVideoInfoTag());
1864       }
1865     }
1866     else
1867       CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '%s'", CURL::GetRedacted(pItem->GetPath()).c_str());
1868
1869     return result;
1870   }
1871
1872   bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
1873   {
1874     if (g_advancedSettings.m_bVideoScannerIgnoreErrors)
1875       return true;
1876
1877     if (pDialog)
1878     {
1879       CGUIDialogOK::ShowAndGetInput(20448,20449,20022,20022);
1880       return false;
1881     }
1882     return CGUIDialogYesNo::ShowAndGetInput(20448,20449,20450,20022);
1883   }
1884
1885   bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const CStdString &line1)
1886   {
1887     if (progress)
1888     {
1889       progress->SetHeading(heading);
1890       progress->SetLine(0, line1);
1891       progress->SetLine(2, "");
1892       progress->Progress();
1893       return progress->IsCanceled();
1894     }
1895     return m_bStop;
1896   }
1897
1898   int CVideoInfoScanner::FindVideo(const CStdString &videoName, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
1899   {
1900     MOVIELIST movielist;
1901     CVideoInfoDownloader imdb(scraper);
1902     int returncode = imdb.FindMovie(videoName, movielist, progress);
1903     if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
1904     { // scraper reported an error, or we had an error and user wants to cancel the scan
1905       m_bStop = true;
1906       return -1; // cancelled
1907     }
1908     if (returncode > 0 && movielist.size())
1909     {
1910       url = movielist[0];
1911       return 1;  // found a movie
1912     }
1913     return 0;    // didn't find anything
1914   }
1915
1916   CStdString CVideoInfoScanner::GetParentDir(const CFileItem &item) const
1917   {
1918     CStdString strCheck = item.GetPath();
1919     if (item.IsStack())
1920       strCheck = CStackDirectory::GetFirstStackedFile(item.GetPath());
1921
1922     CStdString strDirectory = URIUtils::GetDirectory(strCheck);
1923     if (URIUtils::IsInRAR(strCheck))
1924     {
1925       CStdString strPath=strDirectory;
1926       URIUtils::GetParentPath(strPath, strDirectory);
1927     }
1928     if (item.IsStack())
1929     {
1930       strCheck = strDirectory;
1931       URIUtils::RemoveSlashAtEnd(strCheck);
1932       if (URIUtils::GetFileName(strCheck).size() == 3 && StringUtils::StartsWithNoCase(URIUtils::GetFileName(strCheck), "cd"))
1933         strDirectory = URIUtils::GetDirectory(strCheck);
1934     }
1935     return strDirectory;
1936   }
1937
1938 }