Merge pull request #4401 from Jalle19/fix-recording-thumbnail
[vuplus_xbmc] / xbmc / pvr / recordings / PVRRecordings.cpp
1 /*
2  *      Copyright (C) 2012-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 "FileItem.h"
22 #include "dialogs/GUIDialogOK.h"
23 #include "guilib/GUIWindowManager.h"
24 #include "guilib/LocalizeStrings.h"
25 #include "Util.h"
26 #include "URL.h"
27 #include "utils/log.h"
28 #include "threads/SingleLock.h"
29 #include "video/VideoDatabase.h"
30 #include "settings/Settings.h"
31
32 #include "utils/URIUtils.h"
33 #include "utils/StringUtils.h"
34
35 #include "pvr/PVRManager.h"
36 #include "pvr/addons/PVRClients.h"
37 #include "PVRRecordings.h"
38
39 using namespace PVR;
40
41 CPVRRecordings::CPVRRecordings(void) :
42     m_bIsUpdating(false),
43     m_iLastId(0)
44 {
45
46 }
47
48 void CPVRRecordings::UpdateFromClients(void)
49 {
50   CSingleLock lock(m_critSection);
51   Clear();
52   g_PVRClients->GetRecordings(this);
53 }
54
55 CStdString CPVRRecordings::TrimSlashes(const CStdString &strOrig) const
56 {
57   CStdString strReturn(strOrig);
58   while (strReturn[0] == '/')
59     strReturn.erase(0, 1);
60
61   URIUtils::RemoveSlashAtEnd(strReturn);
62
63   return strReturn;
64 }
65
66 const CStdString CPVRRecordings::GetDirectoryFromPath(const CStdString &strPath, const CStdString &strBase) const
67 {
68   CStdString strReturn;
69   CStdString strUsePath = TrimSlashes(strPath);
70   CStdString strUseBase = TrimSlashes(strBase);
71
72   /* strip the base or return an empty value if it doesn't fit or match */
73   if (!strUseBase.empty())
74   {
75     /* adding "/" to make sure that base matches the complete folder name and not only parts of it */
76     if (strUsePath.size() <= strUseBase.size() || !StringUtils::StartsWith(strUsePath, strUseBase + "/"))
77       return strReturn;
78     strUsePath.erase(0, strUseBase.size());
79   }
80
81   /* check for more occurences */
82   size_t iDelimiter = strUsePath.find('/');
83   if (iDelimiter != std::string::npos && iDelimiter > 0)
84     strReturn = strUsePath.substr(0, iDelimiter);
85   else
86     strReturn = strUsePath;
87
88   return TrimSlashes(strReturn);
89 }
90
91 bool CPVRRecordings::IsDirectoryMember(const CStdString &strDirectory, const CStdString &strEntryDirectory, bool bDirectMember /* = true */) const
92 {
93   CStdString strUseDirectory = TrimSlashes(strDirectory);
94   CStdString strUseEntryDirectory = TrimSlashes(strEntryDirectory);
95
96   /* Case-insensitive comparison since sub folders are created with case-insensitive matching (GetSubDirectories) */
97   return StringUtils::StartsWithNoCase(strUseEntryDirectory, strUseDirectory) &&
98       (!bDirectMember || strUseEntryDirectory.Equals(strUseDirectory));
99 }
100
101 void CPVRRecordings::GetContents(const CStdString &strDirectory, CFileItemList *results)
102 {
103   for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
104   {
105     CPVRRecording *current = m_recordings.at(iRecordingPtr);
106     bool directMember = !HasAllRecordingsPathExtension(strDirectory);
107     if (!IsDirectoryMember(RemoveAllRecordingsPathExtension(strDirectory), current->m_strDirectory, directMember))
108       continue;
109
110     current->UpdateMetadata();
111     CFileItemPtr pFileItem(new CFileItem(*current));
112     pFileItem->SetLabel2(current->RecordingTimeAsLocalTime().GetAsLocalizedDateTime(true, false));
113     pFileItem->m_dateTime = current->RecordingTimeAsLocalTime();
114     pFileItem->SetPath(current->m_strFileNameAndPath);
115
116     // Set art
117     if (!current->m_strIconPath.empty())
118     {
119       pFileItem->SetIconImage(current->m_strIconPath);
120       pFileItem->SetArt("icon", current->m_strIconPath);
121     }
122
123     if (!current->m_strThumbnailPath.empty())
124       pFileItem->SetArt("thumb", current->m_strThumbnailPath);
125
126     if (!current->m_strFanartPath.empty())
127       pFileItem->SetArt("fanart", current->m_strFanartPath);
128     
129     // Use the channel icon as a fallback when a thumbnail is not available
130     pFileItem->SetArtFallback("thumb", "icon");
131
132     pFileItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, pFileItem->GetPVRRecordingInfoTag()->m_playCount > 0);
133
134     results->Add(pFileItem);
135   }
136 }
137
138 void CPVRRecordings::GetSubDirectories(const CStdString &strBase, CFileItemList *results)
139 {
140   CStdString strUseBase = TrimSlashes(strBase);
141
142   std::set<CStdString> unwatchedFolders;
143
144   for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
145   {
146     CPVRRecording *current = m_recordings.at(iRecordingPtr);
147     const CStdString strCurrent = GetDirectoryFromPath(current->m_strDirectory, strUseBase);
148     if (strCurrent.empty())
149       continue;
150
151     CStdString strFilePath;
152     if(strUseBase.empty())
153       strFilePath = StringUtils::Format("pvr://recordings/%s/", strCurrent.c_str());
154     else
155       strFilePath = StringUtils::Format("pvr://recordings/%s/%s/", strUseBase.c_str(), strCurrent.c_str());
156
157     if (!results->Contains(strFilePath))
158     {
159       current->UpdateMetadata();
160       CFileItemPtr pFileItem;
161       pFileItem.reset(new CFileItem(strCurrent, true));
162       pFileItem->SetPath(strFilePath);
163       pFileItem->SetLabel(strCurrent);
164       pFileItem->SetLabelPreformated(true);
165       pFileItem->m_dateTime = current->RecordingTimeAsLocalTime();
166
167       // Initialize folder overlay from play count (either directly from client or from video database)
168       if (current->m_playCount > 0)
169         pFileItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_WATCHED, false);
170       else
171         unwatchedFolders.insert(strFilePath);
172
173       results->Add(pFileItem);
174     }
175     else
176     {
177       CFileItemPtr pFileItem;
178       pFileItem=results->Get(strFilePath);
179       if (pFileItem->m_dateTime<current->RecordingTimeAsLocalTime())
180         pFileItem->m_dateTime  = current->RecordingTimeAsLocalTime();
181
182       // Unset folder overlay if recording is unwatched
183       if (unwatchedFolders.find(strFilePath) == unwatchedFolders.end())
184       {
185         if (current->m_playCount == 0)
186         {
187           pFileItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, false);
188           unwatchedFolders.insert(strFilePath);
189         }
190       }
191     }
192   }
193
194   int subDirectories = results->Size();
195   CFileItemList files;
196   GetContents(strBase, &files);
197
198   if (results->Size() == 1 && files.Size() == 0)
199   {
200     CStdString strGetPath = StringUtils::Format("%s/%s/", strUseBase.c_str(), results->Get(0)->GetLabel().c_str());
201
202     results->Clear();
203
204     CLog::Log(LOGDEBUG, "CPVRRecordings - %s - '%s' only has 1 subdirectory, selecting that directory ('%s')",
205         __FUNCTION__, strUseBase.c_str(), strGetPath.c_str());
206     GetSubDirectories(strGetPath, results);
207     return;
208   }
209
210   results->Append(files);
211
212   // Add 'All Recordings' item (if we have at least one subdirectory in the list)
213   if (subDirectories > 0)
214   {
215     CStdString strLabel(g_localizeStrings.Get(19270)); // "* All recordings"
216     CFileItemPtr pItem(new CFileItem(strLabel));
217     CStdString strAllPath;
218     if(strUseBase.empty())
219       strAllPath = "pvr://recordings";
220     else
221       strAllPath = StringUtils::Format("pvr://recordings/%s", strUseBase.c_str());
222     pItem->SetPath(AddAllRecordingsPathExtension(strAllPath));
223     pItem->SetSpecialSort(SortSpecialOnTop);
224     pItem->SetLabelPreformated(true);
225     pItem->m_bIsFolder = true;
226     pItem->m_bIsShareOrDrive = false;
227     for(int i=0; i<results->Size(); ++i)
228     {
229       if(pItem->m_dateTime < results->Get(i)->m_dateTime)
230         pItem->m_dateTime = results->Get(i)->m_dateTime;
231     }
232     results->AddFront(pItem, 0);
233   }
234 }
235
236 bool CPVRRecordings::HasAllRecordingsPathExtension(const CStdString &strDirectory)
237 {
238   CStdString strUseDir = TrimSlashes(strDirectory);
239   CStdString strAllRecordingsPathExtension(PVR_ALL_RECORDINGS_PATH_EXTENSION);
240
241   if (strUseDir.size() < strAllRecordingsPathExtension.size())
242     return false;
243
244   if (strUseDir.size() == strAllRecordingsPathExtension.size())
245     return strUseDir.Equals(strAllRecordingsPathExtension);
246
247   return StringUtils::EndsWith(strUseDir, "/" + strAllRecordingsPathExtension);
248 }
249
250 CStdString CPVRRecordings::AddAllRecordingsPathExtension(const CStdString &strDirectory)
251 {
252   if (HasAllRecordingsPathExtension(strDirectory))
253     return strDirectory;
254
255   CStdString strResult = strDirectory;
256   if (!StringUtils::EndsWith(strDirectory, "/"))
257     strResult = strResult + "/";
258
259   return strResult + PVR_ALL_RECORDINGS_PATH_EXTENSION + "/";
260 }
261
262 CStdString CPVRRecordings::RemoveAllRecordingsPathExtension(const CStdString &strDirectory)
263 {
264   if (!HasAllRecordingsPathExtension(strDirectory))
265     return strDirectory;
266
267   return strDirectory.substr(0, strDirectory.size() - strlen(PVR_ALL_RECORDINGS_PATH_EXTENSION) - 1);
268 }
269
270 int CPVRRecordings::Load(void)
271 {
272   Update();
273
274   return m_recordings.size();
275 }
276
277 void CPVRRecordings::Unload()
278 {
279   Clear();
280 }
281
282 void CPVRRecordings::Update(void)
283 {
284   CSingleLock lock(m_critSection);
285   if (m_bIsUpdating)
286     return;
287   m_bIsUpdating = true;
288   lock.Leave();
289
290   CLog::Log(LOGDEBUG, "CPVRRecordings - %s - updating recordings", __FUNCTION__);
291   UpdateFromClients();
292
293   lock.Enter();
294   m_bIsUpdating = false;
295   SetChanged();
296   lock.Leave();
297
298   NotifyObservers(ObservableMessageRecordings);
299 }
300
301 int CPVRRecordings::GetNumRecordings()
302 {
303   CSingleLock lock(m_critSection);
304   return m_recordings.size();
305 }
306
307 int CPVRRecordings::GetRecordings(CFileItemList* results)
308 {
309   CSingleLock lock(m_critSection);
310
311   for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
312   {
313     CFileItemPtr pFileItem(new CFileItem(*m_recordings.at(iRecordingPtr)));
314     results->Add(pFileItem);
315   }
316
317   return m_recordings.size();
318 }
319
320 bool CPVRRecordings::DeleteRecording(const CFileItem &item)
321 {
322   if (!item.IsPVRRecording())
323   {
324     CLog::Log(LOGERROR, "CPVRRecordings - %s - cannot delete file: no valid recording tag", __FUNCTION__);
325     return false;
326   }
327
328   CPVRRecording *tag = (CPVRRecording *)item.GetPVRRecordingInfoTag();
329   return tag->Delete();
330 }
331
332 bool CPVRRecordings::RenameRecording(CFileItem &item, CStdString &strNewName)
333 {
334   bool bReturn = false;
335
336   if (!item.IsPVRRecording())
337   {
338     CLog::Log(LOGERROR, "CPVRRecordings - %s - cannot rename file: no valid recording tag", __FUNCTION__);
339     return bReturn;
340   }
341
342   CPVRRecording* tag = item.GetPVRRecordingInfoTag();
343   return tag->Rename(strNewName);
344 }
345
346 bool CPVRRecordings::SetRecordingsPlayCount(const CFileItemPtr &item, int count)
347 {
348   bool bResult = false;
349
350   CVideoDatabase database;
351   if (database.Open())
352   {
353     bResult = true;
354
355     CLog::Log(LOGDEBUG, "CPVRRecordings - %s - item path %s", __FUNCTION__, item->GetPath().c_str());
356     CFileItemList items;
357     if (item->m_bIsFolder)
358     {
359       CStdString strPath = item->GetPath();
360       CDirectory::GetDirectory(strPath, items);
361     }
362     else
363       items.Add(item);
364
365     CLog::Log(LOGDEBUG, "CPVRRecordings - %s - will set watched for %d items", __FUNCTION__, items.Size());
366     for (int i=0;i<items.Size();++i)
367     {
368       CLog::Log(LOGDEBUG, "CPVRRecordings - %s - setting watched for item %d", __FUNCTION__, i);
369
370       CFileItemPtr pItem=items[i];
371       if (pItem->m_bIsFolder)
372       {
373         CLog::Log(LOGDEBUG, "CPVRRecordings - %s - path %s is a folder, will call recursively", __FUNCTION__, pItem->GetPath().c_str());
374         if (pItem->GetLabel() != "..")
375         {
376           SetRecordingsPlayCount(pItem, count);
377         }
378         continue;
379       }
380
381       pItem->GetPVRRecordingInfoTag()->SetPlayCount(count);
382
383       // Clear resume bookmark
384       if (count > 0)
385       {
386         database.ClearBookMarksOfFile(pItem->GetPath(), CBookmark::RESUME);
387         pItem->GetPVRRecordingInfoTag()->SetLastPlayedPosition(0);
388       }
389
390       database.SetPlayCount(*pItem, count);
391     }
392
393     database.Close();
394   }
395
396   return bResult;
397 }
398
399 bool CPVRRecordings::GetDirectory(const CStdString& strPath, CFileItemList &items)
400 {
401   bool bSuccess(false);
402
403   {
404     CSingleLock lock(m_critSection);
405
406     CURL url(strPath);
407     CStdString strFileName = url.GetFileName();
408     URIUtils::RemoveSlashAtEnd(strFileName);
409
410     if (StringUtils::StartsWith(strFileName, "recordings"))
411     {
412       strFileName.erase(0, 10);
413       GetSubDirectories(strFileName, &items);
414       bSuccess = true;
415     }
416   }
417
418   return bSuccess;
419 }
420
421 void CPVRRecordings::SetPlayCount(const CFileItem &item, int iPlayCount)
422 {
423   if (!item.HasPVRRecordingInfoTag())
424     return;
425
426   const CPVRRecording *recording = item.GetPVRRecordingInfoTag();
427   CSingleLock lock(m_critSection);
428   for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
429   {
430     CPVRRecording *current = m_recordings.at(iRecordingPtr);
431     if (current->m_iClientId == recording->m_iClientId && current->m_strRecordingId.Equals(recording->m_strRecordingId))
432     {
433       current->SetPlayCount(iPlayCount);
434       break;
435     }
436   }
437 }
438
439 void CPVRRecordings::GetAll(CFileItemList &items)
440 {
441   CSingleLock lock(m_critSection);
442   for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
443   {
444     CPVRRecording *current = m_recordings.at(iRecordingPtr);
445     current->UpdateMetadata();
446
447     CFileItemPtr pFileItem(new CFileItem(*current));
448     pFileItem->SetLabel2(current->RecordingTimeAsLocalTime().GetAsLocalizedDateTime(true, false));
449     pFileItem->m_dateTime = current->RecordingTimeAsLocalTime();
450     pFileItem->SetPath(current->m_strFileNameAndPath);
451     pFileItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, pFileItem->GetPVRRecordingInfoTag()->m_playCount > 0);
452
453     items.Add(pFileItem);
454   }
455 }
456
457 CFileItemPtr CPVRRecordings::GetById(unsigned int iId) const
458 {
459   CFileItemPtr item;
460   CSingleLock lock(m_critSection);
461   for (std::vector<CPVRRecording *>::const_iterator it = m_recordings.begin(); !item && it != m_recordings.end(); ++it)
462   {
463     if (iId == (*it)->m_iRecordingId)
464       item = CFileItemPtr(new CFileItem(**it));
465   }
466
467   return item;
468 }
469
470 CFileItemPtr CPVRRecordings::GetByPath(const CStdString &path)
471 {
472   CURL url(path);
473   CStdString fileName = url.GetFileName();
474   URIUtils::RemoveSlashAtEnd(fileName);
475
476   CSingleLock lock(m_critSection);
477
478   if (StringUtils::StartsWith(fileName, "recordings/"))
479   {
480     for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
481     {
482       if(path.Equals(m_recordings.at(iRecordingPtr)->m_strFileNameAndPath))
483       {
484         CFileItemPtr fileItem(new CFileItem(*m_recordings.at(iRecordingPtr)));
485         return fileItem;
486       }
487     }
488   }
489
490   CFileItemPtr fileItem(new CFileItem);
491   return fileItem;
492 }
493
494 void CPVRRecordings::Clear()
495 {
496   CSingleLock lock(m_critSection);
497
498   for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
499     delete m_recordings.at(iRecordingPtr);
500   m_recordings.erase(m_recordings.begin(), m_recordings.end());
501 }
502
503 void CPVRRecordings::UpdateEntry(const CPVRRecording &tag)
504 {
505   bool bFound = false;
506   CSingleLock lock(m_critSection);
507
508   for (unsigned int iRecordingPtr = 0; iRecordingPtr < m_recordings.size(); iRecordingPtr++)
509   {
510     CPVRRecording *currentTag = m_recordings.at(iRecordingPtr);
511     if (currentTag->m_iClientId == tag.m_iClientId &&
512         currentTag->m_strRecordingId.Equals(tag.m_strRecordingId))
513     {
514       currentTag->Update(tag);
515       bFound = true;
516       break;
517     }
518   }
519
520   if (!bFound)
521   {
522     CPVRRecording *newTag = new CPVRRecording();
523     newTag->Update(tag);
524     newTag->m_iRecordingId = ++m_iLastId;
525     m_recordings.push_back(newTag);
526   }
527 }