merged: Linuxport revisions through to r15292.
[xbmc:xbmc-antiquated.git] / xbmc / utils / RssReader.cpp
1 /*
2  *      Copyright (C) 2005-2008 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, 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 // RssReader.cpp: implementation of the CRssReader class.
23 //
24 //////////////////////////////////////////////////////////////////////
25
26 #include "stdafx.h"
27 #include "RssReader.h"
28 #include "HTTP.h"
29 #include "utils/HTMLUtil.h"
30 #include "xbox/network.h"
31 #include "GUISettings.h"
32 #include "URL.h"
33 #include "FileSystem/File.h"
34 #ifdef __APPLE__
35 #include "CocoaUtils.h"
36 #endif
37
38 using namespace std;
39 using namespace XFILE;
40
41 //////////////////////////////////////////////////////////////////////
42 // Construction/Destruction
43 //////////////////////////////////////////////////////////////////////
44
45 CRssReader::CRssReader() : CThread()
46 {
47   m_pObserver = NULL;
48   m_spacesBetweenFeeds = 0;
49   m_bIsRunning = false;
50 }
51
52 CRssReader::~CRssReader()
53 {
54   if (m_pObserver)
55     m_pObserver->OnFeedRelease();
56   StopThread();
57   for (unsigned int i = 0; i < m_vecTimeStamps.size(); i++)
58     delete m_vecTimeStamps[i];
59 }
60
61 void CRssReader::Create(IRssObserver* aObserver, const vector<string>& aUrls, const vector<int> &times, int spacesBetweenFeeds)
62 {
63   CSingleLock lock(*this);
64   
65   m_pObserver = aObserver;
66   m_spacesBetweenFeeds = spacesBetweenFeeds; 
67   m_vecUrls = aUrls;
68   m_strFeed.resize(aUrls.size());
69   m_strColors.resize(aUrls.size());
70   // set update times
71   m_vecUpdateTimes = times;
72   m_rtlText = g_guiSettings.GetBool("lookandfeel.rssfeedsrtl");
73   m_requestRefresh = false;
74
75   // update each feed on creation
76   for (unsigned int i=0;i<m_vecUpdateTimes.size();++i )
77   {
78     AddToQueue(i);
79     SYSTEMTIME* time = new SYSTEMTIME;
80     GetLocalTime(time);
81     m_vecTimeStamps.push_back(time);
82   }
83 }
84
85 void CRssReader::requestRefresh()
86 {
87   m_requestRefresh = true;
88 }
89
90 void CRssReader::AddToQueue(int iAdd)
91 {  
92   CSingleLock lock(*this);
93   if (iAdd < (int)m_vecUrls.size())
94     m_vecQueue.push_back(iAdd);
95   if (!m_bIsRunning)
96   {
97     StopThread();
98     m_bIsRunning = true;
99     CThread::Create(false, THREAD_MINSTACKSIZE);
100   }
101 }
102
103 void CRssReader::OnExit()
104 {
105   m_bIsRunning = false;
106 }
107
108 int CRssReader::GetQueueSize()
109 {
110   CSingleLock lock(*this);
111   return m_vecQueue.size(); 
112 }
113
114 void CRssReader::Process()
115 {
116   while (GetQueueSize())
117   {
118     EnterCriticalSection(*this);
119     
120     int iFeed = m_vecQueue.front();
121     m_vecQueue.erase(m_vecQueue.begin());
122
123     m_strFeed[iFeed] = "";
124     m_strColors[iFeed] = "";
125
126     CHTTP http;
127     http.SetUserAgent("XBMC/pre-2.1 (http://www.xbmc.org)");
128     CStdString strXML;
129     CStdString strUrl = m_vecUrls[iFeed];
130
131     LeaveCriticalSection(*this);
132     
133     int nRetries = 3;
134     CURL url(strUrl);
135
136     // we wait for the network to come up
137     if ((url.GetProtocol() == "http" || url.GetProtocol() == "https") && (!g_guiSettings.GetBool("network.enableinternet") || !g_network.IsAvailable(true)))
138       strXML = "<rss><item><title>"+g_localizeStrings.Get(15301)+"</title></item></rss>";
139     else
140     {
141       while ( (!m_bStop) && (nRetries > 0) )
142       {
143         nRetries--;
144
145         if (url.GetProtocol() != "http" && url.GetProtocol() != "https")
146         {
147           CFile file;
148           if (file.Open(strUrl))
149           {
150             char *yo = new char[(int)file.GetLength()+1];
151             file.Read(yo,file.GetLength());
152             yo[file.GetLength()] = '\0';
153             strXML = yo;
154             delete[] yo;
155             break;
156           }
157         }
158         else
159           if (http.Get(strUrl, strXML))
160           {
161             CLog::Log(LOGDEBUG, "Got rss feed: %s", strUrl.c_str());
162             break;
163           }
164       }
165     }
166     if ((!strXML.IsEmpty()) && m_pObserver)
167     {
168       // erase any <content:encoded> tags (also unsupported by tinyxml)
169       int iStart = strXML.Find("<content:encoded>");
170       int iEnd = 0;
171       while (iStart > 0)
172       {
173         // get <content:encoded> end position
174         iEnd = strXML.Find("</content:encoded>", iStart) + 18;
175
176         // erase the section
177         strXML = strXML.erase(iStart, iEnd - iStart);
178
179         iStart = strXML.Find("<content:encoded>");
180       }
181
182       if (Parse((LPSTR)strXML.c_str(),iFeed))
183       {
184         CLog::Log(LOGDEBUG, "Parsed rss feed: %s", strUrl.c_str());
185       }
186     }
187   }
188   UpdateObserver();
189 }
190
191 void CRssReader::getFeed(vector<DWORD> &text)
192 {
193   text.clear();
194   // double the spaces at the start of the set
195   for (int j = 0; j < m_spacesBetweenFeeds; j++)
196     text.push_back(L' ');
197   for (unsigned int i = 0; i < m_strFeed.size(); i++)
198   {
199     for (int j = 0; j < m_spacesBetweenFeeds; j++)
200       text.push_back(L' ');
201     for (unsigned int j = 0; j < m_strFeed[i].size(); j++)
202     {
203       DWORD letter = m_strFeed[i][j] | ((m_strColors[i][j] - 48) << 16);
204       text.push_back(letter);
205     }
206   }
207 }
208
209 void CRssReader::AddTag(const CStdString aString)
210 {
211   m_tagSet.push_back(aString);
212 }
213
214 void CRssReader::AddString(CStdStringW aString, int aColour, int iFeed)
215 {
216   if (m_rtlText)
217     m_strFeed[iFeed] = aString + m_strFeed[iFeed];
218   else
219     m_strFeed[iFeed] += aString;
220
221   int nStringLength = aString.GetLength();
222
223   for (int i = 0;i < nStringLength;i++)
224   {
225     aString[i] = (CHAR) (48 + aColour);
226   }
227
228   if (m_rtlText)
229     m_strColors[iFeed] = aString + m_strColors[iFeed];
230   else
231     m_strColors[iFeed] += aString;
232 }
233
234 void CRssReader::GetNewsItems(TiXmlElement* channelXmlNode, int iFeed)
235 {
236   HTML::CHTMLUtil html;
237
238   TiXmlElement * itemNode = channelXmlNode->FirstChildElement("item");
239   map <CStdString, CStdStringW> mTagElements;
240   typedef pair <CStdString, CStdStringW> StrPair;
241   list <CStdString>::iterator i;
242
243   bool bEmpty=true;
244
245   // Add the title tag in if we didn't pass any tags in at all
246   // Represents default behaviour before configurability
247
248   if (m_tagSet.empty())
249     AddTag("title");
250
251   while (itemNode > 0)
252   {
253     bEmpty = false;
254     TiXmlNode* childNode = itemNode->FirstChild();
255     mTagElements.clear();
256     while (childNode > 0)
257     {
258       CStdString strName = childNode->Value();
259
260       for (i = m_tagSet.begin(); i != m_tagSet.end(); i++)
261       {
262         if (!childNode->NoChildren() && i->Equals(strName))
263         {
264           CStdString htmlText = childNode->FirstChild()->Value();
265
266           // This usually happens in right-to-left languages where they want to
267           // specify in the RSS body that the text should be RTL.
268           // <title>
269           //            <div dir="RTL">òìå áøùú: ùîøå òì òöîëí</div> 
270           // </title>
271           if (htmlText.Equals("div") || htmlText.Equals("span"))
272           {
273             htmlText = childNode->FirstChild()->FirstChild()->Value();
274           }
275
276           CStdString text;
277           CStdStringW unicodeText;
278
279           html.ConvertHTMLToAnsi(htmlText, text);
280           fromRSSToUTF16(text, unicodeText);
281
282           mTagElements.insert(StrPair(*i, unicodeText));
283         }
284       }
285       childNode = childNode->NextSibling();
286     }
287
288     int rsscolour = RSS_COLOR_HEADLINE;
289     for (i = m_tagSet.begin();i != m_tagSet.end();i++)
290     {
291       map <CStdString, CStdStringW>::iterator j = mTagElements.find(*i);
292
293       if (j == mTagElements.end())
294         continue;
295
296       CStdStringW& text = j->second;
297       AddString(text, rsscolour, iFeed);
298       rsscolour = RSS_COLOR_BODY;
299       text = " - ";
300       AddString(text, rsscolour, iFeed);
301     }
302     itemNode = itemNode->NextSiblingElement("item");
303   }
304 }
305
306 void CRssReader::fromRSSToUTF16(const CStdStringA& strSource, CStdStringW& strDest)
307 {
308         CStdString flippedStrSource;
309   CStdString strSourceUtf8;
310
311   g_charsetConverter.stringCharsetToUtf8(m_encoding, strSource, strSourceUtf8);
312   if (m_rtlText)
313     g_charsetConverter.logicalToVisualBiDi(strSourceUtf8, flippedStrSource, FRIBIDI_CHAR_SET_UTF8, FRIBIDI_TYPE_RTL);
314   else
315     flippedStrSource = strSourceUtf8;
316   g_charsetConverter.utf8ToW(flippedStrSource, strDest, false);
317 }
318
319 bool CRssReader::Parse(LPSTR szBuffer, int iFeed)
320 {
321   m_xml.Clear();
322   m_xml.Parse((LPCSTR)szBuffer, 0, TIXML_ENCODING_LEGACY);
323
324   m_encoding = "UTF-8";
325   if (m_xml.RootElement())
326   {
327         TiXmlDeclaration *tiXmlDeclaration = m_xml.RootElement()->Parent()->FirstChild()->ToDeclaration();
328         if (tiXmlDeclaration != NULL && strlen(tiXmlDeclaration->Encoding()) > 0)
329         {
330                 m_encoding = tiXmlDeclaration->Encoding();
331         }
332   }
333
334   CLog::Log(LOGDEBUG, "RSS feed encoding: %s", m_encoding.c_str());
335
336   return Parse(iFeed);
337 }
338
339 bool CRssReader::Parse(int iFeed)
340 {
341   TiXmlElement* rootXmlNode = m_xml.RootElement();
342
343   if (!rootXmlNode)
344     return false;
345
346   TiXmlElement* rssXmlNode = NULL;
347
348   CStdString strValue = rootXmlNode->Value();
349   if (( strValue.Find("rss") >= 0 ) || ( strValue.Find("rdf") >= 0 ))
350   {
351     rssXmlNode = rootXmlNode;
352   }
353   else
354   {
355     // Unable to find root <rss> or <rdf> node
356     return false;
357   }
358
359   TiXmlElement* channelXmlNode = rssXmlNode->FirstChildElement("channel");
360   if (channelXmlNode)
361   {
362     TiXmlElement* titleNode = channelXmlNode->FirstChildElement("title");
363     if (titleNode && !titleNode->NoChildren())
364     {
365       CStdString strChannel = titleNode->FirstChild()->Value();
366       CStdStringW strChannelUnicode;
367       fromRSSToUTF16(strChannel, strChannelUnicode);
368       AddString(strChannelUnicode, RSS_COLOR_CHANNEL, iFeed);
369
370       AddString(":", RSS_COLOR_CHANNEL, iFeed);
371       AddString(" ", RSS_COLOR_CHANNEL, iFeed);
372     }
373
374     GetNewsItems(channelXmlNode,iFeed);
375   }
376
377   GetNewsItems(rssXmlNode,iFeed);
378
379   // avoid trailing ' - '
380   if (m_strFeed[iFeed].size() > 3 && m_strFeed[iFeed].Mid(m_strFeed[iFeed].size()-3) == L" - " && !m_rtlText)
381   {
382     m_strFeed[iFeed].erase(m_strFeed[iFeed].length()-3);
383     m_strColors[iFeed].erase(m_strColors[iFeed].length()-3);
384   }
385   else if (m_strFeed[iFeed].size() > 3 && m_strFeed[iFeed].Mid(m_strFeed[iFeed].size()-3) == L" - " && m_rtlText)
386   {
387     m_strFeed[iFeed].erase(0, 3);
388     m_strColors[iFeed].erase(0, 3);
389   }
390   return true;
391 }
392
393 void CRssReader::SetObserver(IRssObserver *observer)
394 {
395   m_pObserver = observer;
396 }
397
398 void CRssReader::UpdateObserver()
399 {
400   if (!m_pObserver) return;
401   vector<DWORD> feed;
402   getFeed(feed);
403   if (feed.size() > 0)
404   {
405     g_graphicsContext.Lock();
406     if (m_pObserver) // need to check again when locked to make sure observer wasnt removed
407       m_pObserver->OnFeedUpdate(feed);
408     g_graphicsContext.Unlock();
409   }
410 }
411
412 void CRssReader::CheckForUpdates()
413 {
414   SYSTEMTIME time;
415   GetLocalTime(&time);
416
417   for (unsigned int i = 0;i < m_vecUpdateTimes.size(); ++i )
418   {
419     if (m_requestRefresh || ((time.wDay * 24 * 60) + (time.wHour * 60) + time.wMinute) - ((m_vecTimeStamps[i]->wDay * 24 * 60) + (m_vecTimeStamps[i]->wHour * 60) + m_vecTimeStamps[i]->wMinute) > m_vecUpdateTimes[i] )
420     {
421       CLog::Log(LOGDEBUG, "Updating RSS");
422       GetLocalTime(m_vecTimeStamps[i]);
423       AddToQueue(i);
424     }
425   }
426
427   m_requestRefresh = false;
428 }
429
430 CRssManager g_rssManager;
431
432 CRssManager::CRssManager()
433 {
434   m_bActive = false;
435 }
436
437 CRssManager::~CRssManager()
438 {
439   Stop();
440 }
441
442 void CRssManager::Start()
443  { 
444    m_bActive = true;
445 }
446
447 void CRssManager::Stop()
448 {
449   m_bActive = false;
450   for (unsigned int i = 0; i < m_readers.size(); i++)
451   {
452     if (m_readers[i].reader)
453     {
454       delete m_readers[i].reader;
455     }
456   }
457   m_readers.clear();
458 }
459
460 // returns true if the reader doesn't need creating, false otherwise
461 bool CRssManager::GetReader(DWORD controlID, DWORD windowID, IRssObserver* observer, CRssReader *&reader)
462 {
463   // check to see if we've already created this reader
464   for (unsigned int i = 0; i < m_readers.size(); i++)
465   {
466     if (m_readers[i].controlID == controlID && m_readers[i].windowID == windowID)
467     {
468       reader = m_readers[i].reader;
469       reader->SetObserver(observer);
470       reader->UpdateObserver();
471       return true;
472     }
473   }
474   // need to create a new one
475   READERCONTROL readerControl;
476   readerControl.controlID = controlID;
477   readerControl.windowID = windowID;
478   reader = readerControl.reader = new CRssReader;
479   m_readers.push_back(readerControl);
480   return false;
481 }