2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "GUIDialogSubtitles.h"
12 #include "FileItemList.h"
14 #include "ServiceBroker.h"
17 #include "addons/AddonManager.h"
18 #include "addons/addoninfo/AddonInfo.h"
19 #include "addons/addoninfo/AddonType.h"
20 #include "addons/gui/GUIDialogAddonSettings.h"
21 #include "application/Application.h"
22 #include "application/ApplicationComponents.h"
23 #include "application/ApplicationPlayer.h"
24 #include "cores/IPlayer.h"
25 #include "dialogs/GUIDialogContextMenu.h"
26 #include "dialogs/GUIDialogKaiToast.h"
27 #include "filesystem/AddonsDirectory.h"
28 #include "filesystem/Directory.h"
29 #include "filesystem/File.h"
30 #include "filesystem/SpecialProtocol.h"
31 #include "filesystem/StackDirectory.h"
32 #include "guilib/GUIComponent.h"
33 #include "guilib/GUIKeyboardFactory.h"
34 #include "guilib/GUIWindowManager.h"
35 #include "guilib/LocalizeStrings.h"
36 #include "input/actions/ActionIDs.h"
37 #include "settings/Settings.h"
38 #include "settings/SettingsComponent.h"
39 #include "settings/lib/Setting.h"
40 #include "utils/JobManager.h"
41 #include "utils/LangCodeExpander.h"
42 #include "utils/StringUtils.h"
43 #include "utils/URIUtils.h"
44 #include "utils/Variant.h"
45 #include "utils/log.h"
46 #include "video/VideoDatabase.h"
50 using namespace ADDON
;
51 using namespace XFILE
;
55 constexpr int CONTROL_NAMELABEL
= 100;
56 constexpr int CONTROL_NAMELOGO
= 110;
57 constexpr int CONTROL_SUBLIST
= 120;
58 constexpr int CONTROL_SUBSEXIST
= 130;
59 constexpr int CONTROL_SUBSTATUS
= 140;
60 constexpr int CONTROL_SERVICELIST
= 150;
61 constexpr int CONTROL_MANUALSEARCH
= 160;
63 enum class SUBTITLE_SERVICE_CONTEXT_BUTTONS
70 /*! \brief simple job to retrieve a directory and store a string (language)
72 class CSubtitlesJob
: public CJob
75 CSubtitlesJob(const CURL
&url
, const std::string
&language
) : m_url(url
), m_language(language
)
77 m_items
= new CFileItemList
;
79 ~CSubtitlesJob() override
83 bool DoWork() override
85 CDirectory::GetDirectory(m_url
.Get(), *m_items
, "", DIR_FLAG_DEFAULTS
);
88 bool operator==(const CJob
*job
) const override
90 if (strcmp(job
->GetType(),GetType()) == 0)
92 const CSubtitlesJob
* rjob
= dynamic_cast<const CSubtitlesJob
*>(job
);
95 return m_url
.Get() == rjob
->m_url
.Get() &&
96 m_language
== rjob
->m_language
;
101 const CFileItemList
*GetItems() const { return m_items
; }
102 const CURL
&GetURL() const { return m_url
; }
103 const std::string
&GetLanguage() const { return m_language
; }
106 CFileItemList
*m_items
;
107 std::string m_language
;
110 CGUIDialogSubtitles::CGUIDialogSubtitles(void)
111 : CGUIDialog(WINDOW_DIALOG_SUBTITLES
, "DialogSubtitles.xml")
112 , m_subtitles(new CFileItemList
)
113 , m_serviceItems(new CFileItemList
)
115 m_loadType
= KEEP_IN_MEMORY
;
118 CGUIDialogSubtitles::~CGUIDialogSubtitles(void)
122 delete m_serviceItems
;
125 bool CGUIDialogSubtitles::OnMessage(CGUIMessage
& message
)
127 if (message
.GetMessage() == GUI_MSG_CLICKED
)
129 int iControl
= message
.GetSenderId();
130 bool selectAction
= (message
.GetParam1() == ACTION_SELECT_ITEM
||
131 message
.GetParam1() == ACTION_MOUSE_LEFT_CLICK
);
133 bool contextMenuAction
= (message
.GetParam1() == ACTION_CONTEXT_MENU
||
134 message
.GetParam1() == ACTION_MOUSE_RIGHT_CLICK
);
136 if (selectAction
&& iControl
== CONTROL_SUBLIST
)
138 CGUIMessage
msg(GUI_MSG_ITEM_SELECTED
, GetID(), CONTROL_SUBLIST
);
141 int item
= msg
.GetParam1();
142 if (item
>= 0 && item
< m_subtitles
->Size())
143 Download(*m_subtitles
->Get(item
));
146 else if (selectAction
&& iControl
== CONTROL_SERVICELIST
)
148 CGUIMessage
msg(GUI_MSG_ITEM_SELECTED
, GetID(), CONTROL_SERVICELIST
);
151 int item
= msg
.GetParam1();
152 if (item
>= 0 && item
< m_serviceItems
->Size())
154 SetService(m_serviceItems
->Get(item
)->GetProperty("Addon.ID").asString());
159 else if (contextMenuAction
&& iControl
== CONTROL_SERVICELIST
)
161 CGUIMessage
msg(GUI_MSG_ITEM_SELECTED
, GetID(), CONTROL_SERVICELIST
);
164 const int itemIdx
= msg
.GetParam1();
165 if (itemIdx
>= 0 && itemIdx
< m_serviceItems
->Size())
167 OnSubtitleServiceContextMenu(itemIdx
);
170 else if (iControl
== CONTROL_MANUALSEARCH
)
173 if (CGUIKeyboardFactory::ShowAndGetInput(m_strManualSearch
, CVariant
{g_localizeStrings
.Get(24121)}, true))
175 Search(m_strManualSearch
);
180 else if (message
.GetMessage() == GUI_MSG_WINDOW_DEINIT
)
182 auto& components
= CServiceBroker::GetAppComponents();
183 const auto appPlayer
= components
.GetComponent
<CApplicationPlayer
>();
184 // Resume the video if the user has requested it
185 if (appPlayer
->IsPaused() && m_pausedOnRun
)
188 CGUIDialog::OnMessage(message
);
194 return CGUIDialog::OnMessage(message
);
197 void CGUIDialogSubtitles::OnInitWindow()
199 // Pause the video if the user has requested it
200 m_pausedOnRun
= false;
201 auto& components
= CServiceBroker::GetAppComponents();
202 const auto appPlayer
= components
.GetComponent
<CApplicationPlayer
>();
203 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
204 CSettings::SETTING_SUBTITLES_PAUSEONSEARCH
) &&
205 !appPlayer
->IsPaused())
208 m_pausedOnRun
= true;
212 CGUIWindow::OnInitWindow();
216 void CGUIDialogSubtitles::Process(unsigned int currentTime
, CDirtyRegionList
&dirtyregions
)
220 // take copies of our variables to ensure we don't hold the lock for long.
224 std::unique_lock
<CCriticalSection
> lock(m_critsection
);
226 subs
.Assign(*m_subtitles
);
228 SET_CONTROL_LABEL(CONTROL_SUBSTATUS
, status
);
230 if (m_updateSubsList
)
232 CGUIMessage
message(GUI_MSG_LABEL_BIND
, GetID(), CONTROL_SUBLIST
, 0, 0, &subs
);
236 // focus subtitles list
237 CGUIMessage
msg(GUI_MSG_SETFOCUS
, GetID(), CONTROL_SUBLIST
);
240 m_updateSubsList
= false;
243 int control
= GetFocusedControlID();
247 CGUIMessage
msg(GUI_MSG_SETFOCUS
, GetID(), m_subtitles
->IsEmpty() ?
248 CONTROL_SERVICELIST
: CONTROL_SUBLIST
);
251 // subs list is focused but we have no subs
252 else if (control
== CONTROL_SUBLIST
&& m_subtitles
->IsEmpty())
254 CGUIMessage
msg(GUI_MSG_SETFOCUS
, GetID(), CONTROL_SERVICELIST
);
258 CGUIDialog::Process(currentTime
, dirtyregions
);
261 void CGUIDialogSubtitles::FillServices()
266 CServiceBroker::GetAddonMgr().GetAddons(addons
, AddonType::SUBTITLE_MODULE
);
270 UpdateStatus(NO_SERVICES
);
274 std::string defaultService
;
275 const CFileItem
&item
= g_application
.CurrentUnstackedItem();
276 if (item
.GetVideoContentType() == VideoDbContentType::TVSHOWS
||
277 item
.GetVideoContentType() == VideoDbContentType::EPISODES
)
278 // Set default service for tv shows
279 defaultService
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_TV
);
281 // Set default service for filemode and movies
282 defaultService
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_MOVIE
);
284 std::string service
= addons
.front()->ID();
285 for (VECADDONS::const_iterator addonIt
= addons
.begin(); addonIt
!= addons
.end(); ++addonIt
)
287 CFileItemPtr
item(CAddonsDirectory::FileItemFromAddon(*addonIt
, "plugin://" + (*addonIt
)->ID(), false));
288 m_serviceItems
->Add(item
);
289 if ((*addonIt
)->ID() == defaultService
)
290 service
= (*addonIt
)->ID();
293 // Bind our services to the UI
294 CGUIMessage
msg(GUI_MSG_LABEL_BIND
, GetID(), CONTROL_SERVICELIST
, 0, 0, m_serviceItems
);
300 bool CGUIDialogSubtitles::SetService(const std::string
&service
)
302 if (service
!= m_currentService
)
304 m_currentService
= service
;
305 CLog::Log(LOGDEBUG
, "New Service [{}] ", m_currentService
);
307 CFileItemPtr currentService
= GetService();
308 // highlight this item in the skin
309 for (int i
= 0; i
< m_serviceItems
->Size(); i
++)
311 CFileItemPtr pItem
= m_serviceItems
->Get(i
);
312 pItem
->Select(pItem
== currentService
);
315 SET_CONTROL_LABEL(CONTROL_NAMELABEL
, currentService
->GetLabel());
317 if (currentService
->HasAddonInfo())
319 std::string icon
= URIUtils::AddFileToFolder(currentService
->GetAddonInfo()->Path(), "logo.png");
320 SET_CONTROL_FILENAME(CONTROL_NAMELOGO
, icon
);
323 const auto& components
= CServiceBroker::GetAppComponents();
324 const auto appPlayer
= components
.GetComponent
<CApplicationPlayer
>();
325 if (appPlayer
->GetSubtitleCount() == 0)
326 SET_CONTROL_HIDDEN(CONTROL_SUBSEXIST
);
328 SET_CONTROL_VISIBLE(CONTROL_SUBSEXIST
);
335 const CFileItemPtr
CGUIDialogSubtitles::GetService() const
337 for (int i
= 0; i
< m_serviceItems
->Size(); i
++)
339 if (m_serviceItems
->Get(i
)->GetProperty("Addon.ID") == m_currentService
)
340 return m_serviceItems
->Get(i
);
342 return CFileItemPtr();
345 void CGUIDialogSubtitles::Search(const std::string
&search
/*=""*/)
347 if (m_currentService
.empty())
348 return; // no services available
350 UpdateStatus(SEARCHING
);
353 CURL
url("plugin://" + m_currentService
+ "/");
356 url
.SetOption("action", "manualsearch");
357 url
.SetOption("searchstring", search
);
360 url
.SetOption("action", "search");
362 const std::shared_ptr
<CSettings
> settings
= CServiceBroker::GetSettingsComponent()->GetSettings();
363 SettingConstPtr setting
= settings
->GetSetting(CSettings::SETTING_SUBTITLES_LANGUAGES
);
365 url
.SetOption("languages", setting
->ToString());
367 // Check for stacking
368 if (g_application
.CurrentFileItem().IsStack())
369 url
.SetOption("stack", "1");
371 std::string preferredLanguage
= settings
->GetString(CSettings::SETTING_LOCALE_SUBTITLELANGUAGE
);
373 if (StringUtils::EqualsNoCase(preferredLanguage
, "original"))
375 AudioStreamInfo info
;
376 std::string strLanguage
;
378 const auto& components
= CServiceBroker::GetAppComponents();
379 const auto appPlayer
= components
.GetComponent
<CApplicationPlayer
>();
380 appPlayer
->GetAudioStreamInfo(CURRENT_STREAM
, info
);
382 if (!g_LangCodeExpander
.Lookup(info
.language
, strLanguage
))
383 strLanguage
= "Unknown";
385 preferredLanguage
= strLanguage
;
387 else if (StringUtils::EqualsNoCase(preferredLanguage
, "default"))
388 preferredLanguage
= g_langInfo
.GetEnglishLanguageName();
390 url
.SetOption("preferredlanguage", preferredLanguage
);
392 AddJob(new CSubtitlesJob(url
, ""));
395 void CGUIDialogSubtitles::OnJobComplete(unsigned int jobID
, bool success
, CJob
*job
)
397 const CURL
&url
= static_cast<CSubtitlesJob
*>(job
)->GetURL();
398 const CFileItemList
*items
= static_cast<CSubtitlesJob
*>(job
)->GetItems();
399 const std::string
&language
= static_cast<CSubtitlesJob
*>(job
)->GetLanguage();
400 if (url
.GetOption("action") == "search" || url
.GetOption("action") == "manualsearch")
401 OnSearchComplete(items
);
403 OnDownloadComplete(items
, language
);
404 CJobQueue::OnJobComplete(jobID
, success
, job
);
407 void CGUIDialogSubtitles::OnSearchComplete(const CFileItemList
*items
)
409 std::unique_lock
<CCriticalSection
> lock(m_critsection
);
410 m_subtitles
->Assign(*items
);
411 UpdateStatus(SEARCH_COMPLETE
);
412 m_updateSubsList
= true;
415 const auto& components
= CServiceBroker::GetAppComponents();
416 const auto appPlayer
= components
.GetComponent
<CApplicationPlayer
>();
417 if (!items
->IsEmpty() && appPlayer
->GetSubtitleCount() == 0 &&
418 m_LastAutoDownloaded
!= g_application
.CurrentFile() &&
419 CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
420 CSettings::SETTING_SUBTITLES_DOWNLOADFIRST
))
422 CFileItemPtr item
= items
->Get(0);
423 CLog::Log(LOGDEBUG
, "{} - Automatically download first subtitle: {}", __FUNCTION__
,
425 m_LastAutoDownloaded
= g_application
.CurrentFile();
432 void CGUIDialogSubtitles::OnSubtitleServiceContextMenu(int itemIdx
)
434 const auto service
= m_serviceItems
->Get(itemIdx
);
436 CContextButtons buttons
;
437 // Subtitle addon settings
438 buttons
.Add(static_cast<int>(SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_SETTINGS
),
439 g_localizeStrings
.Get(21417));
441 buttons
.Add(static_cast<int>(SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_DISABLE
),
442 g_localizeStrings
.Get(24021));
444 auto idx
= static_cast<SUBTITLE_SERVICE_CONTEXT_BUTTONS
>(CGUIDialogContextMenu::Show(buttons
));
447 case SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_SETTINGS
:
450 if (CServiceBroker::GetAddonMgr().GetAddon(service
->GetProperty("Addon.ID").asString(), addon
,
451 AddonType::SUBTITLE_MODULE
,
452 OnlyEnabled::CHOICE_YES
))
454 CGUIDialogAddonSettings::ShowForAddon(addon
);
458 CLog::Log(LOGERROR
, "{} - Could not open settings for addon: {}", __FUNCTION__
,
459 service
->GetProperty("Addon.ID").asString());
463 case SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_DISABLE
:
465 CServiceBroker::GetAddonMgr().DisableAddon(service
->GetProperty("Addon.ID").asString(),
466 AddonDisabledReason::USER
);
467 const bool currentActiveServiceWasDisabled
=
468 m_currentService
== service
->GetProperty("Addon.ID").asString();
470 // restart search if the current active service was disabled
471 if (currentActiveServiceWasDisabled
&& !m_serviceItems
->IsEmpty())
475 // if no more services are available make sure the subtitle list is cleaned up
476 else if (m_serviceItems
->IsEmpty())
487 void CGUIDialogSubtitles::UpdateStatus(STATUS status
)
489 std::unique_lock
<CCriticalSection
> lock(m_critsection
);
494 label
= g_localizeStrings
.Get(24114);
497 label
= g_localizeStrings
.Get(24107);
499 case SEARCH_COMPLETE
:
500 if (!m_subtitles
->IsEmpty())
501 label
= StringUtils::Format(g_localizeStrings
.Get(24108), m_subtitles
->Size());
503 label
= g_localizeStrings
.Get(24109);
506 label
= g_localizeStrings
.Get(24110);
511 if (label
!= m_status
)
518 void CGUIDialogSubtitles::Download(const CFileItem
&subtitle
)
520 UpdateStatus(DOWNLOADING
);
522 // subtitle URL should be of the form plugin://<addonid>/?param=foo¶m=bar
523 // we just append (if not already present) the action=download parameter.
524 CURL
url(subtitle
.GetURL());
525 if (url
.GetOption("action").empty())
526 url
.SetOption("action", "download");
528 AddJob(new CSubtitlesJob(url
, subtitle
.GetLabel()));
531 void CGUIDialogSubtitles::OnDownloadComplete(const CFileItemList
*items
, const std::string
&language
)
533 if (items
->IsEmpty())
535 CFileItemPtr service
= GetService();
537 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error
, service
->GetLabel(), g_localizeStrings
.Get(24113));
538 UpdateStatus(SEARCH_COMPLETE
);
542 SUBTITLE_STORAGEMODE storageMode
= (SUBTITLE_STORAGEMODE
) CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SUBTITLES_STORAGEMODE
);
544 // Get (unstacked) path
545 std::string strCurrentFile
= g_application
.CurrentUnstackedItem().GetDynPath();
547 std::string strDownloadPath
= "special://temp";
548 std::string strDestPath
;
549 std::vector
<std::string
> vecFiles
;
551 std::string strCurrentFilePath
;
552 const std::string subPath
= CSpecialProtocol::TranslatePath("special://subtitles");
554 if (subPath
.empty() && URIUtils::IsHTTP(strCurrentFile
))
556 strCurrentFile
= "TempSubtitle";
557 vecFiles
.push_back(strCurrentFile
);
561 if (!subPath
.empty())
562 strDownloadPath
= subPath
;
564 /** Get item's folder for sub storage, special case for RAR/ZIP items
565 * @todo We need some way to avoid special casing this all over the place
566 * for rar/zip (perhaps modify GetDirectory?)
568 if (URIUtils::IsInRAR(strCurrentFile
) || URIUtils::IsInZIP(strCurrentFile
))
569 strCurrentFilePath
= URIUtils::GetDirectory(CURL(strCurrentFile
).GetHostName());
571 strCurrentFilePath
= URIUtils::GetDirectory(strCurrentFile
);
574 if (g_application
.CurrentFileItem().IsStack() && items
->Size() > 1)
576 CStackDirectory::GetPaths(g_application
.CurrentFileItem().GetPath(), vecFiles
);
577 // Make sure (stack) size is the same as the size of the items handed to us, else fallback to single item
578 if (items
->Size() != (int) vecFiles
.size())
581 vecFiles
.push_back(strCurrentFile
);
586 vecFiles
.push_back(strCurrentFile
);
589 if (storageMode
== SUBTITLE_STORAGEMODE_MOVIEPATH
&&
590 CUtil::SupportsWriteFileOperations(strCurrentFilePath
))
592 strDestPath
= strCurrentFilePath
;
597 if (strDestPath
.empty())
598 strDestPath
= strDownloadPath
;
600 // Extract the language and appropriate extension
601 std::string strSubLang
;
602 g_LangCodeExpander
.ConvertToISO6391(language
, strSubLang
);
604 // Iterate over all items to transfer
605 for (unsigned int i
= 0; i
< vecFiles
.size() && i
< (unsigned int) items
->Size(); i
++)
607 std::string strUrl
= items
->Get(i
)->GetPath();
608 std::string strFileName
= URIUtils::GetFileName(vecFiles
[i
]);
609 URIUtils::RemoveExtension(strFileName
);
611 // construct subtitle path
612 std::string strSubExt
= URIUtils::GetExtension(strUrl
);
613 std::string strSubName
= StringUtils::Format("{}.{}{}", strFileName
, strSubLang
, strSubExt
);
615 // Handle URL encoding:
616 std::string strDownloadFile
= URIUtils::ChangeBasePath(strCurrentFilePath
, strSubName
, strDownloadPath
);
617 std::string strDestFile
= strDownloadFile
;
619 if (!CFile::Copy(strUrl
, strDownloadFile
))
621 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error
, strSubName
, g_localizeStrings
.Get(24113));
622 CLog::Log(LOGERROR
, "{} - Saving of subtitle {} to {} failed", __FUNCTION__
, strUrl
,
627 if (strDestPath
!= strDownloadPath
)
629 // Handle URL encoding:
630 std::string strTryDestFile
= URIUtils::ChangeBasePath(strCurrentFilePath
, strSubName
, strDestPath
);
632 /* Copy the file from temp to our final destination, if that fails fallback to download path
633 * (ie. special://subtitles or use special://temp). Note that after the first item strDownloadPath equals strDestpath
634 * so that all remaining items (including the .idx below) are copied directly to their final destination and thus all
635 * items end up in the same folder
637 CLog::Log(LOGDEBUG
, "{} - Saving subtitle {} to {}", __FUNCTION__
, strDownloadFile
,
639 if (CFile::Copy(strDownloadFile
, strTryDestFile
))
641 CFile::Delete(strDownloadFile
);
642 strDestFile
= strTryDestFile
;
643 strDownloadPath
= strDestPath
; // Update download path so all the other items get directly downloaded to our final destination
647 CLog::Log(LOGWARNING
, "{} - Saving of subtitle {} to {} failed. Falling back to {}",
648 __FUNCTION__
, strDownloadFile
, strTryDestFile
, strDownloadPath
);
649 strDestPath
= strDownloadPath
; // Copy failed, use fallback for the rest of the items
654 CLog::Log(LOGDEBUG
, "{} - Saved subtitle {} to {}", __FUNCTION__
, strUrl
, strDownloadFile
);
657 // for ".sub" subtitles we check if ".idx" counterpart exists and copy that as well
658 if (StringUtils::EqualsNoCase(strSubExt
, ".sub"))
660 strUrl
= URIUtils::ReplaceExtension(strUrl
, ".idx");
661 if(CFile::Exists(strUrl
))
663 std::string strSubNameIdx
= StringUtils::Format("{}.{}.idx", strFileName
, strSubLang
);
664 // Handle URL encoding:
665 strDestFile
= URIUtils::ChangeBasePath(strCurrentFilePath
, strSubNameIdx
, strDestPath
);
666 CFile::Copy(strUrl
, strDestFile
);
670 // Set sub for currently playing (stack) item
671 if (vecFiles
[i
] == strCurrentFile
)
672 SetSubtitles(strDestFile
);
676 // Notify window manager that a subtitle was downloaded
677 CGUIMessage
msg(GUI_MSG_SUBTITLE_DOWNLOADED
, 0, 0);
678 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg
);
684 void CGUIDialogSubtitles::ClearSubtitles()
686 CGUIMessage
msg(GUI_MSG_LABEL_RESET
, GetID(), CONTROL_SUBLIST
);
688 std::unique_lock
<CCriticalSection
> lock(m_critsection
);
689 m_subtitles
->Clear();
692 void CGUIDialogSubtitles::ClearServices()
694 CGUIMessage
msg(GUI_MSG_LABEL_RESET
, GetID(), CONTROL_SERVICELIST
);
696 m_serviceItems
->Clear();
697 m_currentService
.clear();
700 void CGUIDialogSubtitles::SetSubtitles(const std::string
&subtitle
)
702 auto& components
= CServiceBroker::GetAppComponents();
703 const auto appPlayer
= components
.GetComponent
<CApplicationPlayer
>();
704 appPlayer
->AddSubtitle(subtitle
);