Merge remote branch 'origin/trunk' into trac-8658
[xbmc:xbmc-antiquated.git] / xbmc / FileSystem / RSSDirectory.cpp
1 /*
2  *      Copyright (C) 2005-2008 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, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21
22 #include "RSSDirectory.h"
23 #include "FileItem.h"
24 #include "Settings.h"
25 #include "Util.h"
26 #include "tinyXML/tinyxml.h"
27 #include "HTMLUtil.h"
28 #include "StringUtils.h"
29 #include "VideoInfoTag.h"
30 #include "MusicInfoTag.h"
31 #include "utils/log.h"
32 #include "URL.h"
33
34 using namespace XFILE;
35 using namespace std;
36 using namespace MUSIC_INFO;
37
38 namespace {
39
40   struct SResource
41   {
42     SResource()
43       : width(0)
44       , height(0)
45       , bitrate(0)
46       , duration(0)
47       , size(0)
48     {}
49
50     CStdString tag;
51     CStdString path;
52     CStdString mime;
53     CStdString lang;
54     int        width;
55     int        height;
56     int        bitrate;
57     int        duration;
58     int64_t    size;
59   };
60
61   typedef std::vector<SResource> SResources;
62
63 }
64
65 CRSSDirectory::CRSSDirectory()
66 {
67   SetCacheDirectory(DIR_CACHE_ONCE);
68 }
69
70 CRSSDirectory::~CRSSDirectory()
71 {
72 }
73
74 bool CRSSDirectory::ContainsFiles(const CStdString& strPath)
75 {
76   CFileItemList items;
77   if(!GetDirectory(strPath, items))
78     return false;
79
80   return items.Size() > 0;
81 }
82
83 static bool IsPathToMedia(const CStdString& strPath )
84 {
85   CStdString extension;
86   CUtil::GetExtension(strPath, extension);
87
88   if (extension.IsEmpty())
89     return false;
90
91   extension.ToLower();
92
93   if (g_settings.m_videoExtensions.Find(extension) != -1)
94     return true;
95
96   if (g_settings.m_musicExtensions.Find(extension) != -1)
97     return true;
98
99   if (g_settings.m_pictureExtensions.Find(extension) != -1)
100     return true;
101
102   return false;
103 }
104
105 static bool IsPathToThumbnail(const CStdString& strPath )
106 {
107   // Currently just check if this is an image, maybe we will add some
108   // other checks later
109   CStdString extension;
110   CUtil::GetExtension(strPath, extension);
111
112   if (extension.IsEmpty())
113     return false;
114
115   extension.ToLower();
116
117   if (g_settings.m_pictureExtensions.Find(extension) != -1)
118     return true;
119
120   return false;
121 }
122
123 static time_t ParseDate(const CStdString & strDate)
124 {
125   struct tm pubDate = {0};
126   // TODO: Handle time zone
127   strptime(strDate.c_str(), "%a, %d %b %Y %H:%M:%S", &pubDate);
128   // Check the difference between the time of last check and time of the item
129   return mktime(&pubDate);
130 }
131 static void ParseItem(CFileItem* item, SResources& resources, TiXmlElement* root);
132
133 static void ParseItemMRSS(CFileItem* item, SResources& resources, TiXmlElement* item_child, const CStdString& name, const CStdString& xmlns)
134 {
135   CVideoInfoTag* vtag = item->GetVideoInfoTag();
136   CStdString text = item_child->GetText();
137
138   if(name == "content")
139   {
140     SResource res;
141     res.tag = "media:content";
142     res.mime    = item_child->Attribute("type");
143     res.path    = item_child->Attribute("url");
144     if(item_child->Attribute("width"))
145       res.width    = atoi(item_child->Attribute("width"));
146     if(item_child->Attribute("height"))
147       res.height   = atoi(item_child->Attribute("height"));
148     if(item_child->Attribute("bitrate"))
149       res.bitrate  = atoi(item_child->Attribute("bitrate"));
150     if(item_child->Attribute("duration"))
151       res.duration = atoi(item_child->Attribute("duration"));
152     if(item_child->Attribute("fileSize"))
153       res.size     = _atoi64(item_child->Attribute("fileSize"));
154
155     resources.push_back(res);
156     ParseItem(item, resources, item_child);
157   }
158   else if(name == "group")
159   {
160     ParseItem(item, resources, item_child);
161   }
162   else if(name == "thumbnail")
163   {
164     if(item_child->GetText() && IsPathToThumbnail(item_child->GetText()))
165       item->SetThumbnailImage(item_child->GetText());
166     else
167     {
168       const char * url = item_child->Attribute("url");
169       if(url && IsPathToThumbnail(url))
170         item->SetThumbnailImage(url);
171     }
172   }
173   else if (name == "title")
174   {
175     if(text.IsEmpty())
176       return;
177
178     if(text.length() > item->m_strTitle.length())
179       item->m_strTitle = text;
180   }
181   else if(name == "description")
182   {
183     if(text.IsEmpty())
184       return;
185
186     CStdString description = text;
187     if(CStdString(item_child->Attribute("type")) == "html")
188       HTML::CHTMLUtil::RemoveTags(description);
189     item->SetProperty("description", description);
190   }
191   else if(name == "category")
192   {
193     if(text.IsEmpty())
194       return;
195
196     CStdString scheme = item_child->Attribute("scheme");
197
198     /* okey this is silly, boxee what did you think?? */
199     if     (scheme == "urn:boxee:genre")
200       vtag->m_strGenre = text;
201     else if(scheme == "urn:boxee:title-type")
202     {
203       if     (text == "tv")
204         item->SetProperty("boxee:istvshow", true);
205       else if(text == "movie")
206         item->SetProperty("boxee:ismovie", true);
207     }
208     else if(scheme == "urn:boxee:episode")
209       vtag->m_iEpisode = atoi(text.c_str());
210     else if(scheme == "urn:boxee:season")
211       vtag->m_iSeason  = atoi(text.c_str());
212     else if(scheme == "urn:boxee:show-title")
213       vtag->m_strShowTitle = text.c_str();
214     else if(scheme == "urn:boxee:view-count")
215       vtag->m_playCount = atoi(text.c_str());
216     else if(scheme == "urn:boxee:source")
217       item->SetProperty("boxee:provider_source", text);
218     else
219       vtag->m_strGenre = text;
220   }
221   else if(name == "rating")
222   {
223     CStdString scheme = item_child->Attribute("scheme");
224     if(scheme == "urn:user")
225       vtag->m_fRating = (float)atof(text.c_str());
226     else
227       vtag->m_strMPAARating = text;
228   }
229   else if(name == "credit")
230   {
231     CStdString role = item_child->Attribute("role");
232     if     (role == "director")
233       vtag->m_strDirector += ", " + text;
234     else if(role == "author"
235          || role == "writer")
236       vtag->m_strWritingCredits += ", " + text;
237     else if(role == "actor")
238     {
239       SActorInfo actor;
240       actor.strName = text;
241       vtag->m_cast.push_back(actor);
242     }
243   }
244   else if(name == "copyright")
245     vtag->m_strStudio = text;
246   else if(name == "keywords")
247     item->SetProperty("keywords", text);
248
249 }
250
251 static void ParseItemItunes(CFileItem* item, SResources& resources, TiXmlElement* item_child, const CStdString& name, const CStdString& xmlns)
252 {
253   CVideoInfoTag* vtag = item->GetVideoInfoTag();
254   CStdString text = item_child->GetText();
255
256   if(name == "image")
257   {
258     const char * url = item_child->Attribute("href");
259     if(url)
260       item->SetThumbnailImage(url);
261     else
262       item->SetThumbnailImage(text);
263   }
264   else if(name == "summary")
265     vtag->m_strPlot = text;
266   else if(name == "subtitle")
267     vtag->m_strPlotOutline = text;
268   else if(name == "author")
269     vtag->m_strWritingCredits += ", " + text;
270   else if(name == "duration")
271     vtag->m_strRuntime = text;
272   else if(name == "keywords")
273     item->SetProperty("keywords", text);
274 }
275
276 static void ParseItemRSS(CFileItem* item, SResources& resources, TiXmlElement* item_child, const CStdString& name, const CStdString& xmlns)
277 {
278   CStdString text = item_child->GetText();
279   if (name == "title")
280   {
281     if(text.length() > item->m_strTitle.length())
282       item->m_strTitle = text;
283   }
284   else if (name == "pubDate")
285   {
286     CDateTime pubDate(ParseDate(text));
287     item->m_dateTime = pubDate;
288   }
289   else if (name == "link")
290   {
291     SResource res;
292     res.tag  = "rss:link";
293     res.path = text;
294     resources.push_back(res);
295   }
296   else if(name == "enclosure")
297   {
298     const char * url  = item_child->Attribute("url");
299     const char * type = item_child->Attribute("type");
300     const char * len  = item_child->Attribute("length");
301
302     SResource res;
303     res.tag = "rss:enclosure";
304     if(url)
305       res.path = url;
306     if(type)
307       res.mime = type;
308     if(len)
309       res.size = _atoi64(len);
310
311     resources.push_back(res);
312   }
313   else if(name == "description")
314   {
315     CStdString description = text;
316     HTML::CHTMLUtil::RemoveTags(description);
317     item->SetProperty("description", description);
318   }
319   else if(name == "guid")
320   {
321     if(IsPathToMedia(text))
322     {
323       SResource res;
324       res.tag  = "rss:guid";
325       res.path = text;
326       resources.push_back(res);
327     }
328   }
329 }
330
331 static void ParseItemVoddler(CFileItem* item, SResources& resources, TiXmlElement* element, const CStdString& name, const CStdString& xmlns)
332 {
333   CVideoInfoTag* vtag = item->GetVideoInfoTag();
334   CStdString text = element->GetText();
335
336   if(name == "trailer")
337   {
338     vtag->m_strTrailer = text;
339
340     SResource res;
341     res.tag  = "voddler:trailer";
342     res.mime = element->Attribute("type");
343     res.path = text;
344     resources.push_back(res);
345   }
346   else if(name == "year")
347     vtag->m_iYear = atoi(text);
348   else if(name == "rating")
349     vtag->m_fRating = (float)atof(text);
350   else if(name == "tagline")
351     vtag->m_strTagLine = text;
352   else if(name == "posterwall")
353   {
354     const char* url = element->Attribute("url");
355     if(url)
356       item->SetProperty("fanart_image", url);
357     else if(IsPathToThumbnail(text))
358       item->SetProperty("fanart_image", text);
359   }
360 }
361
362 static void ParseItemBoxee(CFileItem* item, SResources& resources, TiXmlElement* element, const CStdString& name, const CStdString& xmlns)
363 {
364   CVideoInfoTag* vtag = item->GetVideoInfoTag();
365   CStdString text = element->GetText();
366
367   if     (name == "image")
368     item->SetThumbnailImage(text);
369   else if(name == "user_agent")
370     item->SetProperty("boxee:user_agent", text);
371   else if(name == "content_type")
372     item->SetMimeType(text);
373   else if(name == "runtime")
374     vtag->m_strRuntime = text;
375   else if(name == "episode")
376     vtag->m_iEpisode = atoi(text);
377   else if(name == "season")
378     vtag->m_iSeason = atoi(text);
379   else if(name == "view-count")
380     vtag->m_playCount = atoi(text);
381   else if(name == "tv-show-title")
382     vtag->m_strShowTitle = text;
383   else if(name == "release-date")
384     item->SetProperty("boxee:releasedate", text);
385 }
386
387 static void ParseItemZink(CFileItem* item, SResources& resources, TiXmlElement* element, const CStdString& name, const CStdString& xmlns)
388 {
389   CVideoInfoTag* vtag = item->GetVideoInfoTag();
390   CStdString text = element->GetText();
391   if     (name == "episode")
392     vtag->m_iEpisode = atoi(text);
393   else if(name == "season")
394     vtag->m_iSeason = atoi(text);
395   else if(name == "views")
396     vtag->m_playCount = atoi(text);
397   else if(name == "airdate")
398     vtag->m_strFirstAired = text;
399   else if(name == "userrating")
400     vtag->m_fRating = (float)atof(text.c_str());
401   else if(name == "duration")
402     vtag->m_strRuntime = StringUtils::SecondsToTimeString(atoi(text));
403   else if(name == "durationstr")
404     vtag->m_strRuntime = text;
405 }
406
407 static void ParseItemSVT(CFileItem* item, SResources& resources, TiXmlElement* element, const CStdString& name, const CStdString& xmlns)
408 {
409   CStdString text = element->GetText();
410   if     (name == "xmllink")
411   {
412     SResource res;
413     res.tag  = "svtplay:xmllink";
414     res.path = text;
415     res.mime = "application/rss+xml";
416     resources.push_back(res);
417   }
418 }
419
420 static void ParseItem(CFileItem* item, SResources& resources, TiXmlElement* root)
421 {
422   for (TiXmlElement* child = root->FirstChildElement(); child; child = child->NextSiblingElement())
423   {
424     CStdString name = child->Value();
425     CStdString xmlns;
426     int pos = name.Find(':');
427     if(pos >= 0)
428     {
429       xmlns = name.Left(pos);
430       name.Delete(0, pos+1);
431     }
432
433     if      (xmlns == "media")
434       ParseItemMRSS   (item, resources, child, name, xmlns);
435     else if (xmlns == "itunes")
436       ParseItemItunes (item, resources, child, name, xmlns);
437     else if (xmlns == "voddler")
438       ParseItemVoddler(item, resources, child, name, xmlns);
439     else if (xmlns == "boxee")
440       ParseItemBoxee  (item, resources, child, name, xmlns);
441     else if (xmlns == "zn")
442       ParseItemZink   (item, resources, child, name, xmlns);
443     else if (xmlns == "svtplay")
444       ParseItemSVT    (item, resources, child, name, xmlns);
445     else
446       ParseItemRSS    (item, resources, child, name, xmlns);
447   }
448 }
449
450 static bool FindMime(SResources resources, CStdString mime)
451 {
452   for(SResources::iterator it = resources.begin(); it != resources.end(); it++)
453   {
454     if(it->mime.Left(mime.length()).Equals(mime))
455       return true;
456   }
457   return false;
458 }
459
460 static void ParseItem(CFileItem* item, TiXmlElement* root)
461 {
462   SResources resources;
463   ParseItem(item, resources, root);
464
465   const char* prio[] = { "media:content", "voddler:trailer", "rss:enclosure", "svtplay:xmllink", "rss:link", "rss:guid", NULL };
466
467   CStdString mime;
468   if     (FindMime(resources, "video/"))
469     mime = "video/";
470   else if(FindMime(resources, "audio/"))
471     mime = "audio/";
472   else if(FindMime(resources, "application/rss"))
473     mime = "application/rss";
474   else if(FindMime(resources, "image/"))
475     mime = "image/";
476
477   SResources::iterator best = resources.end();
478   for(const char** type = prio; *type && best == resources.end(); type++)
479   {
480     for(SResources::iterator it = resources.begin(); it != resources.end(); it++)
481     {
482       if(it->mime.Left(mime.length()) != mime)
483         continue;
484
485       if(it->tag == *type)
486       {
487         if(best == resources.end())
488           best = it;
489         else if(it->width && it->height || best->width && best->height)
490         {
491           if(it->width*it->height > best->width*best->height)
492             best = it;
493         }
494         else if(it->bitrate > best->bitrate)
495           best = it;
496       }
497     }
498   }
499
500   if(best != resources.end())
501   {
502     item->SetMimeType(best->mime);
503     item->m_strPath = best->path;
504     item->m_dwSize  = best->size;
505
506     /* handling of mimetypes fo directories are sub optimal at best */
507     if(best->mime == "application/rss+xml" && item->m_strPath.Left(7).Equals("http://"))
508       item->m_strPath.replace(0, 7, "rss://");
509
510     if(item->m_strPath.Left(6).Equals("rss://"))
511       item->m_bIsFolder = true;
512     else
513       item->m_bIsFolder = false;
514   }
515
516   if(!item->m_strTitle.IsEmpty())
517     item->SetLabel(item->m_strTitle);
518
519   if(item->HasVideoInfoTag())
520   {
521     CVideoInfoTag* vtag = item->GetVideoInfoTag();
522     // clean up ", " added during build
523     vtag->m_strDirector.Delete(0, 2);
524     vtag->m_strWritingCredits.Delete(0, 2);
525
526     if(item->HasProperty("duration")    && vtag->m_strRuntime.IsEmpty())
527       vtag->m_strRuntime = item->GetProperty("duration");
528
529     if(item->HasProperty("description") && vtag->m_strPlot.IsEmpty())
530       vtag->m_strPlot = item->GetProperty("description");
531
532     if(vtag->m_strPlotOutline.IsEmpty() && !vtag->m_strPlot.IsEmpty())
533     {
534       int pos = vtag->m_strPlot.Find('\n');
535       if(pos >= 0)
536         vtag->m_strPlotOutline = vtag->m_strPlot.Left(pos);
537       else
538         vtag->m_strPlotOutline = vtag->m_strPlot;
539     }
540
541     if(!vtag->m_strRuntime.IsEmpty())
542       item->SetLabel2(vtag->m_strRuntime);
543   }
544 }
545
546 bool CRSSDirectory::GetDirectory(const CStdString& path, CFileItemList &items)
547 {
548   CStdString strPath(path);
549   CUtil::RemoveSlashAtEnd(strPath);
550
551   /* check cache */
552   if(m_path == strPath)
553   {
554     items = m_items;
555     return true;
556   }
557
558   /* clear cache */
559   m_items.Clear();
560   m_path == "";
561
562   TiXmlDocument xmlDoc;
563   if (!xmlDoc.LoadFile(strPath))
564   {
565     CLog::Log(LOGERROR,"failed to load xml from <%s>. error: <%d>", strPath.c_str(), xmlDoc.ErrorId());
566     return false;
567   }
568   if (xmlDoc.Error())
569   {
570     CLog::Log(LOGERROR,"error parsing xml doc from <%s>. error: <%d>", strPath.c_str(), xmlDoc.ErrorId());
571     return false;
572   }
573
574   TiXmlElement* rssXmlNode = xmlDoc.RootElement();
575
576   if (!rssXmlNode)
577     return false;
578
579   TiXmlHandle docHandle( &xmlDoc );
580   TiXmlElement* channelXmlNode = docHandle.FirstChild( "rss" ).FirstChild( "channel" ).Element();
581   if (channelXmlNode)
582     ParseItem(&items, channelXmlNode);
583   else
584     return false;
585
586   TiXmlElement* child = NULL;
587   for (child = channelXmlNode->FirstChildElement("item"); child; child = child->NextSiblingElement())
588   {
589     // Create new item,
590     CFileItemPtr item(new CFileItem());
591     ParseItem(item.get(), child);
592
593     item->SetProperty("isrss", "1");
594
595     if (!item->m_strPath.IsEmpty())
596       items.Add(item);
597   }
598
599   m_items = items;
600   m_path  = strPath;
601
602   return true;
603 }