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