b75b485ccc31d543926a3e661ec739f44e3dae91
[vuplus_xbmc] / xbmc / filesystem / PluginDirectory.cpp
1 /*
2  *      Copyright (C) 2005-2012 Team XBMC
3  *      http://www.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
22 #include "threads/SystemClock.h"
23 #include "system.h"
24 #include "PluginDirectory.h"
25 #include "utils/URIUtils.h"
26 #include "addons/AddonManager.h"
27 #include "addons/AddonInstaller.h"
28 #include "addons/IAddon.h"
29 #ifdef HAS_PYTHON
30 #include "interfaces/python/XBPython.h"
31 #endif
32 #include "threads/SingleLock.h"
33 #include "guilib/GUIWindowManager.h"
34 #include "dialogs/GUIDialogProgress.h"
35 #include "settings/GUISettings.h"
36 #include "FileItem.h"
37 #include "video/VideoInfoTag.h"
38 #include "guilib/LocalizeStrings.h"
39 #include "utils/log.h"
40 #include "utils/TimeUtils.h"
41 #include "ApplicationMessenger.h"
42 #include "Application.h"
43 #include "URL.h"
44
45 using namespace XFILE;
46 using namespace std;
47 using namespace ADDON;
48
49 map<int, CPluginDirectory *> CPluginDirectory::globalHandles;
50 int CPluginDirectory::handleCounter = 0;
51 CCriticalSection CPluginDirectory::m_handleLock;
52
53 CPluginDirectory::CPluginDirectory()
54 {
55   m_listItems = new CFileItemList;
56   m_fileResult = new CFileItem;
57 }
58
59 CPluginDirectory::~CPluginDirectory(void)
60 {
61   delete m_listItems;
62   delete m_fileResult;
63 }
64
65 int CPluginDirectory::getNewHandle(CPluginDirectory *cp)
66 {
67   CSingleLock lock(m_handleLock);
68   int handle = ++handleCounter;
69   globalHandles[handle] = cp;
70   return handle;
71 }
72
73 void CPluginDirectory::removeHandle(int handle)
74 {
75   CSingleLock lock(m_handleLock);
76   if (!globalHandles.erase(handle))
77     CLog::Log(LOGWARNING, "Attempt to erase invalid handle %i", handle);
78 }
79
80 CPluginDirectory *CPluginDirectory::dirFromHandle(int handle)
81 {
82   CSingleLock lock(m_handleLock);
83   map<int, CPluginDirectory *>::iterator i = globalHandles.find(handle);
84   if (i != globalHandles.end())
85     return i->second;
86   CLog::Log(LOGWARNING, "Attempt to use invalid handle %i", handle);
87   return NULL;
88 }
89
90 bool CPluginDirectory::StartScript(const CStdString& strPath, bool retrievingDir)
91 {
92   CURL url(strPath);
93
94   if (!CAddonMgr::Get().GetAddon(url.GetHostName(), m_addon, ADDON_PLUGIN) && !CAddonInstaller::Get().PromptForInstall(url.GetHostName(), m_addon))
95   {
96     CLog::Log(LOGERROR, "Unable to find plugin %s", url.GetHostName().c_str());
97     return false;
98   }
99
100   // get options
101   CStdString options = url.GetOptions();
102   URIUtils::RemoveSlashAtEnd(options); // This MAY kill some scripts (eg though with a URL ending with a slash), but
103                                     // is needed for all others, as XBMC adds slashes to "folders"
104   url.SetOptions(""); // do this because we can then use the url to generate the basepath
105                       // which is passed to the plugin (and represents the share)
106
107   CStdString basePath(url.Get());
108   // reset our wait event, and grab a new handle
109   m_fetchComplete.Reset();
110   int handle = getNewHandle(this);
111
112   // clear out our status variables
113   m_fileResult->Reset();
114   m_listItems->Clear();
115   m_listItems->SetPath(strPath);
116   m_listItems->SetLabel(m_addon->Name());
117   m_cancelled = false;
118   m_success = false;
119   m_totalItems = 0;
120
121   // setup our parameters to send the script
122   CStdString strHandle;
123   strHandle.Format("%i", handle);
124   vector<CStdString> argv;
125   argv.push_back(basePath);
126   argv.push_back(strHandle);
127   argv.push_back(options);
128
129   // run the script
130   CLog::Log(LOGDEBUG, "%s - calling plugin %s('%s','%s','%s')", __FUNCTION__, m_addon->Name().c_str(), argv[0].c_str(), argv[1].c_str(), argv[2].c_str());
131   bool success = false;
132 #ifdef HAS_PYTHON
133   CStdString file = m_addon->LibPath();
134   int id = g_pythonParser.evalFile(file, argv,m_addon);
135   if (id >= 0)
136   { // wait for our script to finish
137     CStdString scriptName = m_addon->Name();
138     success = WaitOnScriptResult(file, id, scriptName, retrievingDir);
139   }
140   else
141 #endif
142     CLog::Log(LOGERROR, "Unable to run plugin %s", m_addon->Name().c_str());
143
144   // free our handle
145   removeHandle(handle);
146
147   return success;
148 }
149
150 bool CPluginDirectory::GetPluginResult(const CStdString& strPath, CFileItem &resultItem)
151 {
152   CURL url(strPath);
153   CPluginDirectory* newDir = new CPluginDirectory();
154
155   bool success = newDir->StartScript(strPath, false);
156
157   if (success)
158   { // update the play path and metadata, saving the old one as needed
159     if (!resultItem.HasProperty("original_listitem_url"))
160       resultItem.SetProperty("original_listitem_url", resultItem.GetPath());
161     resultItem.SetPath(newDir->m_fileResult->GetPath());
162     resultItem.SetMimeType(newDir->m_fileResult->GetMimeType(false));
163     resultItem.UpdateInfo(*newDir->m_fileResult);
164     if (newDir->m_fileResult->HasVideoInfoTag() && newDir->m_fileResult->GetVideoInfoTag()->m_resumePoint.IsSet())
165       resultItem.m_lStartOffset = STARTOFFSET_RESUME; // resume point set in the resume item, so force resume
166   }
167   delete newDir;
168
169   return success;
170 }
171
172 bool CPluginDirectory::AddItem(int handle, const CFileItem *item, int totalItems)
173 {
174   CSingleLock lock(m_handleLock);
175   CPluginDirectory *dir = dirFromHandle(handle);
176   if (!dir)
177     return false;
178
179   CFileItemPtr pItem(new CFileItem(*item));
180   dir->m_listItems->Add(pItem);
181   dir->m_totalItems = totalItems;
182
183   return !dir->m_cancelled;
184 }
185
186 bool CPluginDirectory::AddItems(int handle, const CFileItemList *items, int totalItems)
187 {
188   CSingleLock lock(m_handleLock);
189   CPluginDirectory *dir = dirFromHandle(handle);
190   if (!dir)
191     return false;
192
193   CFileItemList pItemList;
194   pItemList.Copy(*items);
195   dir->m_listItems->Append(pItemList);
196   dir->m_totalItems = totalItems;
197
198   return !dir->m_cancelled;
199 }
200
201 void CPluginDirectory::EndOfDirectory(int handle, bool success, bool replaceListing, bool cacheToDisc)
202 {
203   CSingleLock lock(m_handleLock);
204   CPluginDirectory *dir = dirFromHandle(handle);
205   if (!dir)
206     return;
207
208   // set cache to disc
209   dir->m_listItems->SetCacheToDisc(cacheToDisc ? CFileItemList::CACHE_IF_SLOW : CFileItemList::CACHE_NEVER);
210
211   dir->m_success = success;
212   dir->m_listItems->SetReplaceListing(replaceListing);
213
214   if (!dir->m_listItems->HasSortDetails())
215     dir->m_listItems->AddSortMethod(SORT_METHOD_NONE, 552, LABEL_MASKS("%L", "%D"));
216
217   // set the event to mark that we're done
218   dir->m_fetchComplete.Set();
219 }
220
221 void CPluginDirectory::AddSortMethod(int handle, SORT_METHOD sortMethod, const CStdString &label2Mask)
222 {
223   CSingleLock lock(m_handleLock);
224   CPluginDirectory *dir = dirFromHandle(handle);
225   if (!dir)
226     return;
227
228   // TODO: Add all sort methods and fix which labels go on the right or left
229   switch(sortMethod)
230   {
231     case SORT_METHOD_LABEL:
232     case SORT_METHOD_LABEL_IGNORE_THE:
233       {
234         if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
235           dir->m_listItems->AddSortMethod(SORT_METHOD_LABEL_IGNORE_THE, 551, LABEL_MASKS("%T", label2Mask));
236         else
237           dir->m_listItems->AddSortMethod(SORT_METHOD_LABEL, 551, LABEL_MASKS("%T", label2Mask));
238         break;
239       }
240     case SORT_METHOD_TITLE:
241     case SORT_METHOD_TITLE_IGNORE_THE:
242       {
243         if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
244           dir->m_listItems->AddSortMethod(SORT_METHOD_TITLE_IGNORE_THE, 556, LABEL_MASKS("%T", label2Mask));
245         else
246           dir->m_listItems->AddSortMethod(SORT_METHOD_TITLE, 556, LABEL_MASKS("%T", label2Mask));
247         break;
248       }
249     case SORT_METHOD_ARTIST:
250     case SORT_METHOD_ARTIST_IGNORE_THE:
251       {
252         if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
253           dir->m_listItems->AddSortMethod(SORT_METHOD_ARTIST_IGNORE_THE, 557, LABEL_MASKS("%T", "%A"));
254         else
255           dir->m_listItems->AddSortMethod(SORT_METHOD_ARTIST, 557, LABEL_MASKS("%T", "%A"));
256         break;
257       }
258     case SORT_METHOD_ALBUM:
259     case SORT_METHOD_ALBUM_IGNORE_THE:
260       {
261         if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
262           dir->m_listItems->AddSortMethod(SORT_METHOD_ALBUM_IGNORE_THE, 558, LABEL_MASKS("%T", "%B"));
263         else
264           dir->m_listItems->AddSortMethod(SORT_METHOD_ALBUM, 558, LABEL_MASKS("%T", "%B"));
265         break;
266       }
267     case SORT_METHOD_DATE:
268       {
269         dir->m_listItems->AddSortMethod(SORT_METHOD_DATE, 552, LABEL_MASKS("%T", "%J"));
270         break;
271       }
272     case SORT_METHOD_BITRATE:
273       {
274         dir->m_listItems->AddSortMethod(SORT_METHOD_BITRATE, 623, LABEL_MASKS("%T", "%X"));
275         break;
276       }             
277     case SORT_METHOD_SIZE:
278       {
279         dir->m_listItems->AddSortMethod(SORT_METHOD_SIZE, 553, LABEL_MASKS("%T", "%I"));
280         break;
281       }
282     case SORT_METHOD_FILE:
283       {
284         dir->m_listItems->AddSortMethod(SORT_METHOD_FILE, 561, LABEL_MASKS("%T", label2Mask));
285         break;
286       }
287     case SORT_METHOD_TRACKNUM:
288       {
289         dir->m_listItems->AddSortMethod(SORT_METHOD_TRACKNUM, 554, LABEL_MASKS("[%N. ]%T", label2Mask));
290         break;
291       }
292     case SORT_METHOD_DURATION:
293       {
294         dir->m_listItems->AddSortMethod(SORT_METHOD_DURATION, 180, LABEL_MASKS("%T", "%D"));
295         break;
296       }
297     case SORT_METHOD_VIDEO_RATING:
298       {
299         dir->m_listItems->AddSortMethod(SORT_METHOD_VIDEO_RATING, 563, LABEL_MASKS("%T", "%R"));
300         break;
301       }
302     case SORT_METHOD_YEAR:
303       {
304         dir->m_listItems->AddSortMethod(SORT_METHOD_YEAR, 562, LABEL_MASKS("%T", "%Y"));
305         break;
306       }
307     case SORT_METHOD_SONG_RATING:
308       {
309         dir->m_listItems->AddSortMethod(SORT_METHOD_SONG_RATING, 563, LABEL_MASKS("%T", "%R"));
310         break;
311       }
312     case SORT_METHOD_GENRE:
313       {
314         dir->m_listItems->AddSortMethod(SORT_METHOD_GENRE, 515, LABEL_MASKS("%T", "%G"));
315         break;
316       }
317     case SORT_METHOD_COUNTRY:
318       {
319         dir->m_listItems->AddSortMethod(SORT_METHOD_COUNTRY, 574, LABEL_MASKS("%T", "%G"));
320         break;
321       }
322     case SORT_METHOD_VIDEO_TITLE:
323       {
324         dir->m_listItems->AddSortMethod(SORT_METHOD_VIDEO_TITLE, 369, LABEL_MASKS("%T", label2Mask));
325         break;
326       }
327     case SORT_METHOD_VIDEO_SORT_TITLE:
328     case SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE:
329       {
330         if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
331           dir->m_listItems->AddSortMethod(SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE, 556, LABEL_MASKS("%T", label2Mask));
332         else
333           dir->m_listItems->AddSortMethod(SORT_METHOD_VIDEO_SORT_TITLE, 556, LABEL_MASKS("%T", label2Mask));
334         break;
335       }
336     case SORT_METHOD_MPAA_RATING:
337       {
338         dir->m_listItems->AddSortMethod(SORT_METHOD_MPAA_RATING, 20074, LABEL_MASKS("%T", "%O"));
339         break;
340       }
341     case SORT_METHOD_VIDEO_RUNTIME:
342       {
343         dir->m_listItems->AddSortMethod(SORT_METHOD_VIDEO_RUNTIME, 180, LABEL_MASKS("%T", "%D"));
344         break;
345       }
346     case SORT_METHOD_STUDIO:
347     case SORT_METHOD_STUDIO_IGNORE_THE:
348       {
349         if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
350           dir->m_listItems->AddSortMethod(SORT_METHOD_STUDIO_IGNORE_THE, 572, LABEL_MASKS("%T", "%U"));
351         else
352           dir->m_listItems->AddSortMethod(SORT_METHOD_STUDIO, 572, LABEL_MASKS("%T", "%U"));
353         break;
354       }
355     case SORT_METHOD_PROGRAM_COUNT:
356       {
357         dir->m_listItems->AddSortMethod(SORT_METHOD_PROGRAM_COUNT, 567, LABEL_MASKS("%T", "%C"));
358         break;
359       }
360     case SORT_METHOD_UNSORTED:
361       {
362         dir->m_listItems->AddSortMethod(SORT_METHOD_UNSORTED, 571, LABEL_MASKS("%T", label2Mask));
363         break;
364       }
365     case SORT_METHOD_NONE:
366       {
367         dir->m_listItems->AddSortMethod(SORT_METHOD_NONE, 552, LABEL_MASKS("%T", label2Mask));
368         break;
369       }
370     case SORT_METHOD_DRIVE_TYPE:
371       {
372         dir->m_listItems->AddSortMethod(SORT_METHOD_DRIVE_TYPE, 564, LABEL_MASKS()); // Preformatted
373         break;
374       }
375     case SORT_METHOD_PLAYLIST_ORDER:
376       {
377         CStdString strTrackLeft=g_guiSettings.GetString("musicfiles.trackformat");
378         CStdString strTrackRight=g_guiSettings.GetString("musicfiles.trackformatright");
379
380         dir->m_listItems->AddSortMethod(SORT_METHOD_PLAYLIST_ORDER, 559, LABEL_MASKS(strTrackLeft, strTrackRight));
381         break;
382       }
383     case SORT_METHOD_EPISODE:
384       {
385         dir->m_listItems->AddSortMethod(SORT_METHOD_EPISODE,20359,LABEL_MASKS("%E. %T","%R"));
386         break;
387       }
388     case SORT_METHOD_PRODUCTIONCODE:
389       {
390         //dir->m_listItems.AddSortMethod(SORT_METHOD_PRODUCTIONCODE,20368,LABEL_MASKS("%E. %T","%P", "%E. %T","%P"));
391         dir->m_listItems->AddSortMethod(SORT_METHOD_PRODUCTIONCODE,20368,LABEL_MASKS("%H. %T","%P", "%H. %T","%P"));
392         break;
393       }
394     case SORT_METHOD_LISTENERS:
395       {
396        dir->m_listItems->AddSortMethod(SORT_METHOD_LISTENERS,20455,LABEL_MASKS("%T","%W"));
397        break;
398       }
399    
400     default:
401       break;
402   }
403 }
404
405 bool CPluginDirectory::GetDirectory(const CStdString& strPath, CFileItemList& items)
406 {
407   CURL url(strPath);
408
409   bool success = StartScript(strPath, true);
410
411   // append the items to the list
412   items.Assign(*m_listItems, true); // true to keep the current items
413   m_listItems->Clear();
414   return success;
415 }
416
417 bool CPluginDirectory::RunScriptWithParams(const CStdString& strPath)
418 {
419   CURL url(strPath);
420   if (url.GetHostName().IsEmpty()) // called with no script - should never happen
421     return false;
422
423   AddonPtr addon;
424   if (!CAddonMgr::Get().GetAddon(url.GetHostName(), addon, ADDON_PLUGIN) && !CAddonInstaller::Get().PromptForInstall(url.GetHostName(), addon))
425   {
426     CLog::Log(LOGERROR, "Unable to find plugin %s", url.GetHostName().c_str());
427     return false;
428   }
429
430   // options
431   CStdString options = url.GetOptions();
432   URIUtils::RemoveSlashAtEnd(options); // This MAY kill some scripts (eg though with a URL ending with a slash), but
433                                     // is needed for all others, as XBMC adds slashes to "folders"
434   url.SetOptions(""); // do this because we can then use the url to generate the basepath
435                       // which is passed to the plugin (and represents the share)
436
437   CStdString basePath(url.Get());
438
439   // setup our parameters to send the script
440   CStdString strHandle;
441   strHandle.Format("%i", -1);
442   vector<CStdString> argv;
443   argv.push_back(basePath);
444   argv.push_back(strHandle);
445   argv.push_back(options);
446
447   // run the script
448 #ifdef HAS_PYTHON
449   CLog::Log(LOGDEBUG, "%s - calling plugin %s('%s','%s','%s')", __FUNCTION__, addon->Name().c_str(), argv[0].c_str(), argv[1].c_str(), argv[2].c_str());
450   if (g_pythonParser.evalFile(addon->LibPath(), argv,addon) >= 0)
451     return true;
452   else
453 #endif
454     CLog::Log(LOGERROR, "Unable to run plugin %s", addon->Name().c_str());
455
456   return false;
457 }
458
459 bool CPluginDirectory::WaitOnScriptResult(const CStdString &scriptPath, int scriptId, const CStdString &scriptName, bool retrievingDir)
460 {
461   const unsigned int timeBeforeProgressBar = 1500;
462   const unsigned int timeToKillScript = 1000;
463
464   unsigned int startTime = XbmcThreads::SystemClockMillis();
465   CGUIDialogProgress *progressBar = NULL;
466   bool cancelled = false;
467   bool inMainAppThread = g_application.IsCurrentThread();
468
469   CLog::Log(LOGDEBUG, "%s - waiting on the %s (id=%d) plugin...", __FUNCTION__, scriptName.c_str(), scriptId);
470   while (true)
471   {
472     {
473       CSingleExit ex(g_graphicsContext);
474       // check if the python script is finished
475       if (m_fetchComplete.WaitMSec(20))
476       { // python has returned
477         CLog::Log(LOGDEBUG, "%s- plugin returned %s", __FUNCTION__, m_success ? "successfully" : "failure");
478         break;
479       }
480     }
481     // check our script is still running
482 #ifdef HAS_PYTHON
483     if (!g_pythonParser.isRunning(scriptId))
484 #endif
485     { // check whether we exited normally
486       if (!m_fetchComplete.WaitMSec(0))
487       { // python didn't return correctly
488         CLog::Log(LOGDEBUG, " %s - plugin exited prematurely - terminating", __FUNCTION__);
489         m_success = false;
490       }
491       break;
492     }
493
494     // check whether we should pop up the progress dialog
495     if (!retrievingDir && !progressBar && XbmcThreads::SystemClockMillis() - startTime > timeBeforeProgressBar)
496     { // loading takes more then 1.5 secs, show a progress dialog
497       progressBar = (CGUIDialogProgress *)g_windowManager.GetWindow(WINDOW_DIALOG_PROGRESS);
498
499       // if script has shown progressbar don't override it
500       if (progressBar && progressBar->IsActive())
501       {
502         startTime = XbmcThreads::SystemClockMillis();
503         progressBar = NULL;
504       }
505
506       if (progressBar)
507       {
508         progressBar->SetHeading(scriptName);
509         progressBar->SetLine(0, retrievingDir ? 1040 : 10214);
510         progressBar->SetLine(1, "");
511         progressBar->SetLine(2, "");
512         progressBar->ShowProgressBar(retrievingDir);
513         progressBar->StartModal();
514       }
515     }
516
517     if (progressBar)
518     { // update the progress bar and check for user cancel
519       progressBar->Progress();
520       if (progressBar->IsCanceled())
521       { // user has cancelled our process - cancel our process
522         m_cancelled = true;
523       }
524     }
525     else // if the progressBar exists and we call StartModal or Progress we get the
526          //  ProcessRenderLoop call anyway.
527       if (inMainAppThread) 
528         g_windowManager.ProcessRenderLoop();
529
530     if (!cancelled && m_cancelled)
531     {
532       cancelled = true;
533       startTime = XbmcThreads::SystemClockMillis();
534     }
535     if (cancelled && XbmcThreads::SystemClockMillis() - startTime > timeToKillScript)
536     { // cancel our script
537 #ifdef HAS_PYTHON
538       if (scriptId != -1 && g_pythonParser.isRunning(scriptId))
539       {
540         CLog::Log(LOGDEBUG, "%s- cancelling plugin %s (id=%d)", __FUNCTION__, scriptName.c_str(), scriptId);
541         g_pythonParser.stopScript(scriptId);
542         break;
543       }
544 #endif
545     }
546   }
547
548   if (progressBar)
549     CApplicationMessenger::Get().Close(progressBar, false, false);
550
551   return !cancelled && m_success;
552 }
553
554 void CPluginDirectory::SetResolvedUrl(int handle, bool success, const CFileItem *resultItem)
555 {
556   CSingleLock lock(m_handleLock);
557   CPluginDirectory *dir = dirFromHandle(handle);
558   if (!dir)
559     return;
560
561   dir->m_success = success;
562   *dir->m_fileResult = *resultItem;
563
564   // set the event to mark that we're done
565   dir->m_fetchComplete.Set();
566 }
567
568 CStdString CPluginDirectory::GetSetting(int handle, const CStdString &strID)
569 {
570   CSingleLock lock(m_handleLock);
571   CPluginDirectory *dir = dirFromHandle(handle);
572   if(dir && dir->m_addon)
573     return dir->m_addon->GetSetting(strID);
574   else
575     return "";
576 }
577
578 void CPluginDirectory::SetSetting(int handle, const CStdString &strID, const CStdString &value)
579 {
580   CSingleLock lock(m_handleLock);
581   CPluginDirectory *dir = dirFromHandle(handle);
582   if(dir && dir->m_addon)
583     dir->m_addon->UpdateSetting(strID, value);
584 }
585
586 void CPluginDirectory::SetContent(int handle, const CStdString &strContent)
587 {
588   CSingleLock lock(m_handleLock);
589   CPluginDirectory *dir = dirFromHandle(handle);
590   if (dir)
591     dir->m_listItems->SetContent(strContent);
592 }
593
594 void CPluginDirectory::SetProperty(int handle, const CStdString &strProperty, const CStdString &strValue)
595 {
596   CSingleLock lock(m_handleLock);
597   CPluginDirectory *dir = dirFromHandle(handle);
598   if (!dir)
599     return;
600   if (strProperty == "fanart_image")
601     dir->m_listItems->SetArt("fanart", strValue);
602   else
603     dir->m_listItems->SetProperty(strProperty, strValue);
604 }
605
606 void CPluginDirectory::CancelDirectory()
607 {
608   m_cancelled = true;
609 }
610
611 float CPluginDirectory::GetProgress() const
612 {
613   if (m_totalItems > 0)
614     return (m_listItems->Size() * 100.0f) / m_totalItems;
615   return 0.0f;
616 }