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