Merge pull request #4314 from MartijnKaijser/beta1
[vuplus_xbmc] / xbmc / video / dialogs / GUIDialogSubtitles.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 #include "system.h"
22 #include "GUIUserMessages.h"
23 #include "Application.h"
24 #include "GUIDialogSubtitles.h"
25 #include "addons/AddonManager.h"
26 #include "cores/IPlayer.h"
27 #include "dialogs/GUIDialogKaiToast.h"
28 #include "filesystem/AddonsDirectory.h"
29 #include "filesystem/File.h"
30 #include "filesystem/PluginDirectory.h"
31 #include "filesystem/SpecialProtocol.h"
32 #include "filesystem/StackDirectory.h"
33 #include "guilib/GUIImage.h"
34 #include "guilib/GUIKeyboardFactory.h"
35 #include "guilib/Key.h"
36 #include "settings/MediaSettings.h"
37 #include "settings/Settings.h"
38 #include "settings/VideoSettings.h"
39 #include "settings/lib/Setting.h"
40 #include "utils/JobManager.h"
41 #include "utils/LangCodeExpander.h"
42 #include "utils/log.h"
43 #include "utils/StringUtils.h"
44 #include "utils/URIUtils.h"
45 #include "URL.h"
46 #include "Util.h"
47 #include "video/VideoDatabase.h"
48
49 using namespace ADDON;
50 using namespace XFILE;
51
52 #define CONTROL_NAMELABEL            100
53 #define CONTROL_NAMELOGO             110
54 #define CONTROL_SUBLIST              120
55 #define CONTROL_SUBSEXIST            130
56 #define CONTROL_SUBSTATUS            140
57 #define CONTROL_SERVICELIST          150
58 #define CONTROL_MANUALSEARCH         160
59
60 /*! \brief simple job to retrieve a directory and store a string (language)
61  */
62 class CSubtitlesJob: public CJob
63 {
64 public:
65   CSubtitlesJob(const CURL &url, const std::string &language) : m_url(url), m_language(language)
66   {
67     m_items = new CFileItemList;
68   }
69   virtual ~CSubtitlesJob()
70   {
71     delete m_items;
72   }
73   virtual bool DoWork()
74   {
75     CDirectory::GetDirectory(m_url.Get(), *m_items);
76     // Sort items by path so they properly order for eg. stacks
77     m_items->Sort(SortByPath, SortOrderAscending);
78     return true;
79   }
80   virtual bool operator==(const CJob *job) const
81   {
82     if (strcmp(job->GetType(),GetType()) == 0)
83     {
84       const CSubtitlesJob* rjob = dynamic_cast<const CSubtitlesJob*>(job);
85       if (rjob)
86       {
87         return m_url.Get() == rjob->m_url.Get() &&
88                m_language == rjob->m_language;
89       }
90     }
91     return false;
92   }
93   const CFileItemList *GetItems() const { return m_items; }
94   const CURL &GetURL() const { return m_url; }
95   const std::string &GetLanguage() const { return m_language; }
96 private:
97   CURL           m_url;
98   CFileItemList *m_items;
99   std::string    m_language;
100 };
101
102 CGUIDialogSubtitles::CGUIDialogSubtitles(void)
103     : CGUIDialog(WINDOW_DIALOG_SUBTITLES, "DialogSubtitles.xml")
104 {
105   m_loadType  = KEEP_IN_MEMORY;
106   m_subtitles = new CFileItemList;
107   m_serviceItems = new CFileItemList;
108   m_pausedOnRun = false;
109   m_updateSubsList = false;
110 }
111
112 CGUIDialogSubtitles::~CGUIDialogSubtitles(void)
113 {
114   CancelJobs();
115   delete m_subtitles;
116   delete m_serviceItems;
117 }
118
119 bool CGUIDialogSubtitles::OnMessage(CGUIMessage& message)
120 {
121   if (message.GetMessage() == GUI_MSG_CLICKED)
122   {
123     int iControl = message.GetSenderId();
124     bool selectAction = (message.GetParam1() == ACTION_SELECT_ITEM ||
125                          message.GetParam1() == ACTION_MOUSE_LEFT_CLICK);
126
127     if (selectAction && iControl == CONTROL_SUBLIST)
128     {
129       CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SUBLIST);
130       OnMessage(msg);
131
132       int item = msg.GetParam1();
133       if (item >= 0 && item < m_subtitles->Size())
134         Download(*m_subtitles->Get(item));
135       return true;
136     }
137     else if (selectAction && iControl == CONTROL_SERVICELIST)
138     {
139       CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SERVICELIST);
140       OnMessage(msg);
141
142       int item = msg.GetParam1();
143       if (item >= 0 && item < m_serviceItems->Size() &&
144           SetService(m_serviceItems->Get(item)->GetProperty("Addon.ID").asString()))
145         Search();
146
147       return true;
148     }
149     else if (iControl == CONTROL_MANUALSEARCH)
150     {
151       //manual search
152       if (CGUIKeyboardFactory::ShowAndGetInput(m_strManualSearch, g_localizeStrings.Get(24121), true))
153       {
154         Search(m_strManualSearch);
155         return true;
156       }
157     }
158   }
159   else if (message.GetMessage() == GUI_MSG_WINDOW_DEINIT)
160   {
161     // Resume the video if the user has requested it
162     if (g_application.m_pPlayer->IsPaused() && m_pausedOnRun)
163       g_application.m_pPlayer->Pause();
164
165     CGUIDialog::OnMessage(message);
166
167     ClearSubtitles();
168     ClearServices();
169     return true;
170   }
171   return CGUIDialog::OnMessage(message);
172 }
173
174 void CGUIDialogSubtitles::OnInitWindow()
175 {
176   // Pause the video if the user has requested it
177   m_pausedOnRun = false;
178   if (CSettings::Get().GetBool("subtitles.pauseonsearch") && !g_application.m_pPlayer->IsPaused())
179   {
180     g_application.m_pPlayer->Pause();
181     m_pausedOnRun = true;
182   }
183
184   FillServices();
185   CGUIWindow::OnInitWindow();
186   Search();
187 }
188
189 void CGUIDialogSubtitles::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
190 {
191   if (m_bInvalidated)
192   {
193     // take copies of our variables to ensure we don't hold the lock for long.
194     std::string status;
195     CFileItemList subs;
196     {
197       CSingleLock lock(m_critsection);
198       status = m_status;
199       subs.Assign(*m_subtitles);
200     }
201     SET_CONTROL_LABEL(CONTROL_SUBSTATUS, status);
202
203     if (m_updateSubsList)
204     {
205       CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_SUBLIST, 0, 0, &subs);
206       OnMessage(message);
207       m_updateSubsList = false;
208     }
209     
210     int control = GetFocusedControlID();
211     // nothing has focus
212     if (!control)
213     {
214       CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), m_subtitles->IsEmpty() ?
215                       CONTROL_SERVICELIST : CONTROL_SUBLIST);
216       OnMessage(msg);
217     }
218     // subs list is focused but we have no subs
219     else if (control == CONTROL_SUBLIST && m_subtitles->IsEmpty())
220     {
221       CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), CONTROL_SERVICELIST);
222       OnMessage(msg);
223     }
224   }
225   CGUIDialog::Process(currentTime, dirtyregions);
226 }
227
228 void CGUIDialogSubtitles::FillServices()
229 {
230   ClearServices();
231
232   VECADDONS addons;
233   ADDON::CAddonMgr::Get().GetAddons(ADDON_SUBTITLE_MODULE, addons, true);
234
235   if (addons.empty())
236   {
237     UpdateStatus(NO_SERVICES);
238     return;
239   }
240
241   std::string defaultService;
242   const CFileItem &item = g_application.CurrentUnstackedItem();
243   if (item.GetVideoContentType() == VIDEODB_CONTENT_TVSHOWS ||
244       item.GetVideoContentType() == VIDEODB_CONTENT_EPISODES)
245     // Set default service for tv shows
246     defaultService = CSettings::Get().GetString("subtitles.tv");
247   else
248     // Set default service for filemode and movies
249     defaultService = CSettings::Get().GetString("subtitles.movie");
250   
251   std::string service = addons.front()->ID();
252   for (VECADDONS::const_iterator addonIt = addons.begin(); addonIt != addons.end(); addonIt++)
253   {
254     CFileItemPtr item(CAddonsDirectory::FileItemFromAddon(*addonIt, "plugin://", false));
255     m_serviceItems->Add(item);
256     if ((*addonIt)->ID() == defaultService)
257       service = (*addonIt)->ID();
258   }
259
260   // Bind our services to the UI
261   CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_SERVICELIST, 0, 0, m_serviceItems);
262   OnMessage(msg);
263
264   SetService(service);
265 }
266
267 bool CGUIDialogSubtitles::SetService(const std::string &service)
268 {
269   if (service != m_currentService)
270   {
271     m_currentService = service;
272     CLog::Log(LOGDEBUG, "New Service [%s] ", m_currentService.c_str());
273
274     CFileItemPtr currentService = GetService();
275     // highlight this item in the skin
276     for (int i = 0; i < m_serviceItems->Size(); i++)
277     {
278       CFileItemPtr pItem = m_serviceItems->Get(i);
279       pItem->Select(pItem == currentService);
280     }
281
282     SET_CONTROL_LABEL(CONTROL_NAMELABEL, currentService->GetLabel());
283
284     CGUIImage* image = (CGUIImage*)GetControl(CONTROL_NAMELOGO);
285     if (image)
286     {
287       std::string icon = URIUtils::AddFileToFolder(currentService->GetProperty("Addon.Path").asString(), "logo.png");
288       image->SetFileName(icon);
289     }
290     if (g_application.m_pPlayer->GetSubtitleCount() == 0)
291       SET_CONTROL_HIDDEN(CONTROL_SUBSEXIST);
292     else
293       SET_CONTROL_VISIBLE(CONTROL_SUBSEXIST);
294
295     return true;
296   }
297   return false;
298 }
299
300 const CFileItemPtr CGUIDialogSubtitles::GetService() const
301 {
302   for (int i = 0; i < m_serviceItems->Size(); i++)
303   {
304     if (m_serviceItems->Get(i)->GetProperty("Addon.ID") == m_currentService)
305       return m_serviceItems->Get(i);
306   }
307   return CFileItemPtr();
308 }
309
310 void CGUIDialogSubtitles::Search(const std::string &search/*=""*/)
311 {
312   if (m_currentService.empty())
313     return; // no services available
314
315   UpdateStatus(SEARCHING);
316   ClearSubtitles();
317
318   CURL url("plugin://" + m_currentService + "/");
319   if (!search.empty())
320   {
321     url.SetOption("action", "manualsearch");
322     url.SetOption("searchstring", search);
323   }
324   else
325     url.SetOption("action", "search");
326
327   const CSetting *setting = CSettings::Get().GetSetting("subtitles.languages");
328   if (setting)
329     url.SetOption("languages", setting->ToString());
330
331   // Check for stacking
332   if (g_application.CurrentFileItem().IsStack())
333     url.SetOption("stack", "1");
334
335   AddJob(new CSubtitlesJob(url, ""));
336 }
337
338 void CGUIDialogSubtitles::OnJobComplete(unsigned int jobID, bool success, CJob *job)
339 {
340   const CURL &url             = ((CSubtitlesJob *)job)->GetURL();
341   const CFileItemList *items  = ((CSubtitlesJob *)job)->GetItems();
342   const std::string &language = ((CSubtitlesJob *)job)->GetLanguage();
343   if (url.GetOption("action") == "search" || url.GetOption("action") == "manualsearch")
344     OnSearchComplete(items);
345   else
346     OnDownloadComplete(items, language);
347   CJobQueue::OnJobComplete(jobID, success, job);
348 }
349
350 void CGUIDialogSubtitles::OnSearchComplete(const CFileItemList *items)
351 {
352   CSingleLock lock(m_critsection);
353   m_subtitles->Assign(*items);
354   UpdateStatus(SEARCH_COMPLETE);
355   m_updateSubsList = true;
356   SetInvalid();
357 }
358
359 void CGUIDialogSubtitles::UpdateStatus(STATUS status)
360 {
361   CSingleLock lock(m_critsection);
362   std::string label;
363   switch (status)
364   {
365     case NO_SERVICES:
366       label = g_localizeStrings.Get(24114);
367       break;
368     case SEARCHING:
369       label = g_localizeStrings.Get(24107);
370       break;
371     case SEARCH_COMPLETE:
372       if (!m_subtitles->IsEmpty())
373         label = StringUtils::Format(g_localizeStrings.Get(24108).c_str(), m_subtitles->Size());
374       else
375         label = g_localizeStrings.Get(24109);
376       break;
377     case DOWNLOADING:
378       label = g_localizeStrings.Get(24110);
379       break;
380     default:
381       break;
382   }
383   if (label != m_status)
384   {
385     m_status = label;
386     SetInvalid();
387   }
388 }
389
390 void CGUIDialogSubtitles::Download(const CFileItem &subtitle)
391 {
392   UpdateStatus(DOWNLOADING);
393
394   // subtitle URL should be of the form plugin://<addonid>/?param=foo&param=bar
395   // we just append (if not already present) the action=download parameter.
396   CURL url(subtitle.GetAsUrl());
397   if (url.GetOption("action").empty())
398     url.SetOption("action", "download");
399
400   AddJob(new CSubtitlesJob(url, subtitle.GetLabel()));
401 }
402
403 void CGUIDialogSubtitles::OnDownloadComplete(const CFileItemList *items, const std::string &language)
404 {
405   if (items->IsEmpty())
406   {
407     CFileItemPtr service = GetService();
408     if (service)
409       CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, service->GetLabel(), g_localizeStrings.Get(24113));
410     UpdateStatus(SEARCH_COMPLETE);
411     return;
412   }
413
414   SUBTITLE_STORAGEMODE storageMode = (SUBTITLE_STORAGEMODE) CSettings::Get().GetInt("subtitles.storagemode");
415
416   // Get (unstacked) path
417   const CStdString &strCurrentFile = g_application.CurrentUnstackedItem().GetPath();
418
419   CStdString strDownloadPath = "special://temp";
420   CStdString strDestPath;
421   std::vector<CStdString> vecFiles;
422
423   CStdString strCurrentFilePath = URIUtils::GetDirectory(strCurrentFile);
424   if (StringUtils::StartsWith(strCurrentFilePath, "http://"))
425   {
426     vecFiles.push_back("TemporarySubs");
427   }
428   else
429   {
430     CStdString subPath = CSpecialProtocol::TranslatePath("special://subtitles");
431     if (!subPath.empty())
432       strDownloadPath = subPath;
433
434     // Handle stacks
435     if (g_application.CurrentFileItem().IsStack() && items->Size() > 1)
436     {
437       CStackDirectory::GetPaths(g_application.CurrentFileItem().GetPath(), vecFiles);
438       // Make sure (stack) size is the same as the size of the items handed to us, else fallback to single item
439       if (items->Size() != (int) vecFiles.size())
440       {
441         vecFiles.clear();
442         vecFiles.push_back(strCurrentFile);
443       }
444     }
445     else
446     {
447       vecFiles.push_back(strCurrentFile);
448     }
449
450     if (storageMode == SUBTITLE_STORAGEMODE_MOVIEPATH &&
451         CUtil::SupportsWriteFileOperations(strCurrentFilePath))
452     {
453       strDestPath = strCurrentFilePath;
454     }
455   }
456
457   // Use fallback?
458   if (strDestPath.empty())
459     strDestPath = strDownloadPath;
460
461   // Extract the language and appropriate extension
462   CStdString strSubLang;
463   g_LangCodeExpander.ConvertToTwoCharCode(strSubLang, language);
464
465   // Iterate over all items to transfer
466   for (unsigned int i = 0; i < vecFiles.size() && i < (unsigned int) items->Size(); i++)
467   {
468     CStdString strUrl = items->Get(i)->GetPath();
469     CStdString strFileName = URIUtils::GetFileName(vecFiles[i]);
470     URIUtils::RemoveExtension(strFileName);
471
472     // construct subtitle path
473     CStdString strSubExt = URIUtils::GetExtension(strUrl);
474     CStdString strSubName = StringUtils::Format("%s.%s%s", strFileName.c_str(), strSubLang.c_str(), strSubExt.c_str());
475
476     // Handle URL decoding/slash correction:
477     CStdString strDownloadFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubName, strDownloadPath);
478     CStdString strDestFile = strDownloadFile;
479
480     if (!CFile::Cache(strUrl, strDownloadFile))
481     {
482       CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, strSubName, g_localizeStrings.Get(24113));
483     }
484     else
485     {
486       if (strDestPath != strDownloadPath)
487       {
488         CStdString strTryDestFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubName, strDestPath);
489
490         /* Copy the file from temp to our final destination, if that fails fallback to download path
491          * (ie. special://subtitles or use special://temp). Note that after the first item strDownloadPath equals strDestpath
492          * so that all remaining items (including the .idx below) are copied directly to their final destination and thus all
493          * items end up in the same folder
494          */
495         CLog::Log(LOGDEBUG, "%s - Saving subtitle %s to %s", __FUNCTION__, strDownloadFile.c_str(), strDestPath.c_str());
496         if (CFile::Cache(strDownloadFile, strTryDestFile))
497         {
498           CFile::Delete(strDownloadFile);
499           strDestFile = strTryDestFile;
500           strDownloadPath = strDestPath; // Update download path so all the other items get directly downloaded to our final destination
501         }
502         else
503         {
504           CLog::Log(LOGWARNING, "%s - Saving of subtitle %s to %s failed. Falling back to %s", __FUNCTION__, strDownloadFile.c_str(), strDestPath.c_str(), strDownloadPath.c_str());
505           strDestPath = strDownloadPath; // Copy failed, use fallback for the rest of the items
506         }
507       }
508
509       // for ".sub" subtitles we check if ".idx" counterpart exists and copy that as well
510       if (strSubExt.Equals(".sub"))
511       {
512         strUrl = URIUtils::ReplaceExtension(strUrl, ".idx");
513         if(CFile::Exists(strUrl))
514         {
515           CStdString strSubNameIdx = StringUtils::Format("%s.%s.idx", strFileName.c_str(), strSubLang.c_str());
516           // Handle URL decoding/slash correction:
517           strDestFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubNameIdx, strDestPath);
518           CFile::Cache(strUrl, strDestFile);
519         }
520       }
521
522       // Set sub for currently playing (stack) item
523       if (vecFiles[i] == strCurrentFile)
524         SetSubtitles(strDestFile);
525     }
526   }
527
528   // Close the window
529   Close();
530 }
531
532 void CGUIDialogSubtitles::ClearSubtitles()
533 {
534   CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_SUBLIST);
535   OnMessage(msg);
536   CSingleLock lock(m_critsection);
537   m_subtitles->Clear();
538 }
539
540 void CGUIDialogSubtitles::ClearServices()
541 {
542   CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_SERVICELIST);
543   OnMessage(msg);
544   m_serviceItems->Clear();
545   m_currentService.clear();
546 }
547
548 void CGUIDialogSubtitles::SetSubtitles(const std::string &subtitle)
549 {
550   if (g_application.m_pPlayer)
551   {
552     int nStream = g_application.m_pPlayer->AddSubtitle(subtitle);
553     if(nStream >= 0)
554     {
555       g_application.m_pPlayer->SetSubtitle(nStream);
556       g_application.m_pPlayer->SetSubtitleVisible(true);
557       CMediaSettings::Get().GetCurrentVideoSettings().m_SubtitleDelay = 0.0f;
558       g_application.m_pPlayer->SetSubTitleDelay(0);
559     }
560   }
561 }