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¶m=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