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 "threads/SystemClock.h"
10 #include "CompileInfo.h"
11 #include "ExternalPlayer.h"
12 #include "windowing/WinSystem.h"
13 #include "dialogs/GUIDialogOK.h"
14 #include "guilib/GUIComponent.h"
15 #include "guilib/GUIWindowManager.h"
16 #include "Application.h"
17 #include "filesystem/MusicDatabaseFile.h"
18 #include "FileItem.h"
19 #include "utils/RegExp.h"
20 #include "utils/StringUtils.h"
21 #include "utils/URIUtils.h"
22 #include "URL.h"
23 #include "utils/XMLUtils.h"
24 #include "utils/log.h"
25 #include "utils/Variant.h"
26 #include "video/Bookmark.h"
27 #include "ServiceBroker.h"
28 #include "cores/AudioEngine/Interfaces/AE.h"
29 #include "cores/DataCacheCore.h"
30 #if defined(TARGET_WINDOWS)
31   #include "utils/CharsetConverter.h"
32   #include <Windows.h>
33 #endif
34 #if defined(TARGET_ANDROID)
35   #include "platform/android/activity/XBMCApp.h"
36 #endif
37 
38 // If the process ends in less than this time (ms), we assume it's a launcher
39 // and wait for manual intervention before continuing
40 #define LAUNCHER_PROCESS_TIME 2000
41 // Time (ms) we give a process we sent a WM_QUIT to close before terminating
42 #define PROCESS_GRACE_TIME 3000
43 // Default time after which the item's playcount is incremented
44 #define DEFAULT_PLAYCOUNT_MIN_TIME 10
45 
46 using namespace XFILE;
47 
48 #if defined(TARGET_WINDOWS_DESKTOP)
49 extern HWND g_hWnd;
50 #endif
51 
CExternalPlayer(IPlayerCallback & callback)52 CExternalPlayer::CExternalPlayer(IPlayerCallback& callback)
53     : IPlayer(callback),
54       CThread("ExternalPlayer")
55 {
56   m_bAbortRequest = false;
57   m_bIsPlaying = false;
58   m_playbackStartTime = 0;
59   m_speed = 1;
60   m_time = 0;
61 
62   m_hideconsole = false;
63   m_warpcursor = WARP_NONE;
64   m_hidexbmc = false;
65   m_islauncher = false;
66   m_playCountMinTime = DEFAULT_PLAYCOUNT_MIN_TIME;
67   m_playOneStackItem = false;
68 
69   m_dialog = NULL;
70 #if defined(TARGET_WINDOWS_DESKTOP)
71   m_xPos = 0;
72   m_yPos = 0;
73 
74   memset(&m_processInfo, 0, sizeof(m_processInfo));
75 #endif
76 }
77 
~CExternalPlayer()78 CExternalPlayer::~CExternalPlayer()
79 {
80   CloseFile();
81 }
82 
OpenFile(const CFileItem & file,const CPlayerOptions & options)83 bool CExternalPlayer::OpenFile(const CFileItem& file, const CPlayerOptions &options)
84 {
85   try
86   {
87     m_file = file;
88     m_bIsPlaying = true;
89     m_time = 0;
90     m_playbackStartTime = XbmcThreads::SystemClockMillis();
91     m_launchFilename = file.GetDynPath();
92     CLog::Log(LOGINFO, "%s: %s", __FUNCTION__, m_launchFilename.c_str());
93     Create();
94 
95     return true;
96   }
97   catch(...)
98   {
99     m_bIsPlaying = false;
100     CLog::Log(LOGERROR,"%s - Exception thrown", __FUNCTION__);
101     return false;
102   }
103 }
104 
CloseFile(bool reopen)105 bool CExternalPlayer::CloseFile(bool reopen)
106 {
107   m_bAbortRequest = true;
108 
109   if (m_dialog && m_dialog->IsActive()) m_dialog->Close();
110 
111 #if defined(TARGET_WINDOWS_DESKTOP)
112   if (m_bIsPlaying && m_processInfo.hProcess)
113   {
114     TerminateProcess(m_processInfo.hProcess, 1);
115   }
116 #endif
117 
118   return true;
119 }
120 
IsPlaying() const121 bool CExternalPlayer::IsPlaying() const
122 {
123   return m_bIsPlaying;
124 }
125 
Process()126 void CExternalPlayer::Process()
127 {
128   std::string mainFile = m_launchFilename;
129   std::string archiveContent;
130 
131   if (m_args.find("{0}") == std::string::npos)
132   {
133     // Unwind archive names
134     CURL url(m_launchFilename);
135     if (url.IsProtocol("zip") || url.IsProtocol("rar") /* || url.IsProtocol("iso9660") ??*/ || url.IsProtocol("udf"))
136     {
137       mainFile = url.GetHostName();
138       archiveContent = url.GetFileName();
139     }
140     if (url.IsProtocol("musicdb"))
141       mainFile = CMusicDatabaseFile::TranslateUrl(url);
142     if (url.IsProtocol("bluray"))
143     {
144       CURL base(url.GetHostName());
145       if (base.IsProtocol("udf"))
146       {
147         mainFile = base.GetHostName(); /* image file */
148         archiveContent = base.GetFileName();
149       }
150       else
151         mainFile = URIUtils::AddFileToFolder(base.Get(), url.GetFileName());
152     }
153   }
154 
155   if (!m_filenameReplacers.empty())
156   {
157     for (unsigned int i = 0; i < m_filenameReplacers.size(); i++)
158     {
159       std::vector<std::string> vecSplit = StringUtils::Split(m_filenameReplacers[i], " , ");
160 
161       // something is wrong, go to next substitution
162       if (vecSplit.size() != 4)
163         continue;
164 
165       std::string strMatch = vecSplit[0];
166       StringUtils::Replace(strMatch, ",,",",");
167       bool bCaseless = vecSplit[3].find('i') != std::string::npos;
168       CRegExp regExp(bCaseless, CRegExp::autoUtf8);
169 
170       if (!regExp.RegComp(strMatch.c_str()))
171       { // invalid regexp - complain in logs
172         CLog::Log(LOGERROR, "%s: Invalid RegExp:'%s'", __FUNCTION__, strMatch.c_str());
173         continue;
174       }
175 
176       if (regExp.RegFind(mainFile) > -1)
177       {
178         std::string strPat = vecSplit[1];
179         StringUtils::Replace(strPat, ",,",",");
180 
181         if (!regExp.RegComp(strPat.c_str()))
182         { // invalid regexp - complain in logs
183           CLog::Log(LOGERROR, "%s: Invalid RegExp:'%s'", __FUNCTION__, strPat.c_str());
184           continue;
185         }
186 
187         std::string strRep = vecSplit[2];
188         StringUtils::Replace(strRep, ",,",",");
189         bool bGlobal = vecSplit[3].find('g') != std::string::npos;
190         bool bStop = vecSplit[3].find('s') != std::string::npos;
191         int iStart = 0;
192         while ((iStart = regExp.RegFind(mainFile, iStart)) > -1)
193         {
194           int iLength = regExp.GetFindLen();
195           mainFile = mainFile.substr(0, iStart) + regExp.GetReplaceString(strRep) + mainFile.substr(iStart + iLength);
196           if (!bGlobal)
197             break;
198         }
199         CLog::Log(LOGINFO, "%s: File matched:'%s' (RE='%s',Rep='%s') new filename:'%s'.", __FUNCTION__, strMatch.c_str(), strPat.c_str(), strRep.c_str(), mainFile.c_str());
200         if (bStop) break;
201       }
202     }
203   }
204 
205   CLog::Log(LOGINFO, "%s: Player : %s", __FUNCTION__, m_filename.c_str());
206   CLog::Log(LOGINFO, "%s: File   : %s", __FUNCTION__, mainFile.c_str());
207   CLog::Log(LOGINFO, "%s: Content: %s", __FUNCTION__, archiveContent.c_str());
208   CLog::Log(LOGINFO, "%s: Args   : %s", __FUNCTION__, m_args.c_str());
209   CLog::Log(LOGINFO, "%s: Start", __FUNCTION__);
210 
211   // make sure we surround the arguments with quotes where necessary
212   std::string strFName;
213   std::string strFArgs;
214 #if defined(TARGET_WINDOWS_DESKTOP)
215   // W32 batch-file handline
216   if (StringUtils::EndsWith(m_filename, ".bat") || StringUtils::EndsWith(m_filename, ".cmd"))
217   {
218     // MSDN says you just need to do this, but cmd's handing of spaces and
219     // quotes is soo broken it seems to work much better if you just omit
220     // lpApplicationName and enclose the module in lpCommandLine in quotes
221     //strFName = "cmd.exe";
222     //strFArgs = "/c ";
223   }
224   else
225 #endif
226     strFName = m_filename;
227 
228   strFArgs.append("\"");
229   strFArgs.append(m_filename);
230   strFArgs.append("\" ");
231   strFArgs.append(m_args);
232 
233   int nReplaced = StringUtils::Replace(strFArgs, "{0}", mainFile);
234 
235   if (!nReplaced)
236     nReplaced = StringUtils::Replace(strFArgs, "{1}", mainFile) + StringUtils::Replace(strFArgs, "{2}", archiveContent);
237 
238   if (!nReplaced)
239   {
240     strFArgs.append(" \"");
241     strFArgs.append(mainFile);
242     strFArgs.append("\"");
243   }
244 
245 #if defined(TARGET_WINDOWS_DESKTOP)
246   if (m_warpcursor)
247   {
248     GetCursorPos(&m_ptCursorpos);
249     int x = 0;
250     int y = 0;
251     switch (m_warpcursor)
252     {
253       case WARP_BOTTOM_RIGHT:
254         x = GetSystemMetrics(SM_CXSCREEN);
255       case WARP_BOTTOM_LEFT:
256         y = GetSystemMetrics(SM_CYSCREEN);
257         break;
258       case WARP_TOP_RIGHT:
259         x = GetSystemMetrics(SM_CXSCREEN);
260         break;
261       case WARP_CENTER:
262         x = GetSystemMetrics(SM_CXSCREEN) / 2;
263         y = GetSystemMetrics(SM_CYSCREEN) / 2;
264         break;
265     }
266     CLog::Log(LOGINFO, "%s: Warping cursor to (%d,%d)", __FUNCTION__, x, y);
267     SetCursorPos(x,y);
268   }
269 
270   LONG currentStyle = GetWindowLong(g_hWnd, GWL_EXSTYLE);
271 #endif
272 
273   if (m_hidexbmc && !m_islauncher)
274   {
275     CLog::Log(LOGINFO, "%s: Hiding %s window", __FUNCTION__, CCompileInfo::GetAppName());
276     CServiceBroker::GetWinSystem()->Hide();
277   }
278 #if defined(TARGET_WINDOWS_DESKTOP)
279   else if (currentStyle & WS_EX_TOPMOST)
280   {
281     CLog::Log(LOGINFO, "%s: Lowering %s window", __FUNCTION__, CCompileInfo::GetAppName());
282     SetWindowPos(g_hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOREDRAW | SWP_ASYNCWINDOWPOS);
283   }
284 
285   CLog::Log(LOGDEBUG, "%s: Unlocking foreground window", __FUNCTION__);
286   LockSetForegroundWindow(LSFW_UNLOCK);
287 #endif
288 
289   m_playbackStartTime = XbmcThreads::SystemClockMillis();
290 
291   /* Suspend AE temporarily so exclusive or hog-mode sinks */
292   /* don't block external player's access to audio device  */
293   CServiceBroker::GetActiveAE()->Suspend();
294   // wait for AE has completed suspended
295   XbmcThreads::EndTime timer(2000);
296   while (!timer.IsTimePast() && !CServiceBroker::GetActiveAE()->IsSuspended())
297   {
298     CThread::Sleep(50);
299   }
300   if (timer.IsTimePast())
301   {
302     CLog::Log(LOGERROR,"%s: AudioEngine did not suspend before launching external player", __FUNCTION__);
303   }
304 
305   m_callback.OnPlayBackStarted(m_file);
306   m_callback.OnAVStarted(m_file);
307 
308   bool ret = true;
309 #if defined(TARGET_WINDOWS_DESKTOP)
310   ret = ExecuteAppW32(strFName.c_str(),strFArgs.c_str());
311 #elif defined(TARGET_ANDROID)
312   ret = ExecuteAppAndroid(m_filename.c_str(), mainFile.c_str());
313 #elif defined(TARGET_POSIX) && !defined(TARGET_DARWIN_EMBEDDED)
314   ret = ExecuteAppLinux(strFArgs.c_str());
315 #endif
316   int64_t elapsedMillis = XbmcThreads::SystemClockMillis() - m_playbackStartTime;
317 
318   if (ret && (m_islauncher || elapsedMillis < LAUNCHER_PROCESS_TIME))
319   {
320     if (m_hidexbmc)
321     {
322       CLog::Log(LOGINFO, "%s: %s cannot stay hidden for a launcher process", __FUNCTION__,
323                 CCompileInfo::GetAppName());
324       CServiceBroker::GetWinSystem()->Show(false);
325     }
326 
327     {
328       m_dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogOK>(WINDOW_DIALOG_OK);
329       m_dialog->SetHeading(CVariant{23100});
330       m_dialog->SetLine(1, CVariant{23104});
331       m_dialog->SetLine(2, CVariant{23105});
332       m_dialog->SetLine(3, CVariant{23106});
333     }
334 
335     if (!m_bAbortRequest)
336       m_dialog->Open();
337   }
338 
339   m_bIsPlaying = false;
340   CLog::Log(LOGINFO, "%s: Stop", __FUNCTION__);
341 
342 #if defined(TARGET_WINDOWS_DESKTOP)
343   CServiceBroker::GetWinSystem()->Restore();
344 
345   if (currentStyle & WS_EX_TOPMOST)
346   {
347     CLog::Log(LOGINFO, "%s: Showing %s window TOPMOST", __FUNCTION__, CCompileInfo::GetAppName());
348     SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS);
349     SetForegroundWindow(g_hWnd);
350   }
351   else
352 #endif
353   {
354     CLog::Log(LOGINFO, "%s: Showing %s window", __FUNCTION__, CCompileInfo::GetAppName());
355     CServiceBroker::GetWinSystem()->Show();
356   }
357 
358 #if defined(TARGET_WINDOWS_DESKTOP)
359   if (m_warpcursor)
360   {
361     m_xPos = 0;
362     m_yPos = 0;
363     if (&m_ptCursorpos != 0)
364     {
365       m_xPos = (m_ptCursorpos.x);
366       m_yPos = (m_ptCursorpos.y);
367     }
368     CLog::Log(LOGINFO, "%s: Restoring cursor to (%d,%d)", __FUNCTION__, m_xPos, m_yPos);
369     SetCursorPos(m_xPos,m_yPos);
370   }
371 #endif
372 
373   CBookmark bookmark;
374   bookmark.totalTimeInSeconds = 1;
375   bookmark.timeInSeconds = (elapsedMillis / 1000 >= m_playCountMinTime) ? 1 : 0;
376   bookmark.player = m_name;
377   m_callback.OnPlayerCloseFile(m_file, bookmark);
378 
379   /* Resume AE processing of XBMC native audio */
380   if (!CServiceBroker::GetActiveAE()->Resume())
381   {
382     CLog::Log(LOGFATAL, "%s: Failed to restart AudioEngine after return from external player",__FUNCTION__);
383   }
384 
385   // We don't want to come back to an active screensaver
386   g_application.ResetScreenSaver();
387   g_application.WakeUpScreenSaverAndDPMS();
388 
389   if (!ret || (m_playOneStackItem && g_application.CurrentFileItem().IsStack()))
390     m_callback.OnPlayBackStopped();
391   else
392     m_callback.OnPlayBackEnded();
393 }
394 
395 #if defined(TARGET_WINDOWS_DESKTOP)
ExecuteAppW32(const char * strPath,const char * strSwitches)396 bool CExternalPlayer::ExecuteAppW32(const char* strPath, const char* strSwitches)
397 {
398   CLog::Log(LOGINFO, "%s: %s %s", __FUNCTION__, strPath, strSwitches);
399 
400   STARTUPINFOW si;
401   memset(&si, 0, sizeof(si));
402   si.cb = sizeof(si);
403   si.dwFlags = STARTF_USESHOWWINDOW;
404   si.wShowWindow = m_hideconsole ? SW_HIDE : SW_SHOW;
405 
406   std::wstring WstrPath, WstrSwitches;
407   g_charsetConverter.utf8ToW(strPath, WstrPath, false);
408   g_charsetConverter.utf8ToW(strSwitches, WstrSwitches, false);
409 
410   if (m_bAbortRequest) return false;
411 
412   BOOL ret = CreateProcessW(WstrPath.empty() ? NULL : WstrPath.c_str(),
413                             (LPWSTR) WstrSwitches.c_str(), NULL, NULL, FALSE, NULL,
414                             NULL, NULL, &si, &m_processInfo);
415 
416   if (ret == FALSE)
417   {
418     DWORD lastError = GetLastError();
419     CLog::Log(LOGINFO, "%s - Failure: %d", __FUNCTION__, lastError);
420   }
421   else
422   {
423     int res = WaitForSingleObject(m_processInfo.hProcess, INFINITE);
424 
425     switch (res)
426     {
427       case WAIT_OBJECT_0:
428         CLog::Log(LOGINFO, "%s: WAIT_OBJECT_0", __FUNCTION__);
429         break;
430       case WAIT_ABANDONED:
431         CLog::Log(LOGINFO, "%s: WAIT_ABANDONED", __FUNCTION__);
432         break;
433       case WAIT_TIMEOUT:
434         CLog::Log(LOGINFO, "%s: WAIT_TIMEOUT", __FUNCTION__);
435         break;
436       case WAIT_FAILED:
437         CLog::Log(LOGINFO, "%s: WAIT_FAILED (%d)", __FUNCTION__, GetLastError());
438         ret = FALSE;
439         break;
440     }
441 
442     CloseHandle(m_processInfo.hThread);
443     m_processInfo.hThread = 0;
444     CloseHandle(m_processInfo.hProcess);
445     m_processInfo.hProcess = 0;
446   }
447   return (ret == TRUE);
448 }
449 #endif
450 
451 #if !defined(TARGET_ANDROID) && !defined(TARGET_DARWIN_EMBEDDED) && defined(TARGET_POSIX)
ExecuteAppLinux(const char * strSwitches)452 bool CExternalPlayer::ExecuteAppLinux(const char* strSwitches)
453 {
454   CLog::Log(LOGINFO, "%s: %s", __FUNCTION__, strSwitches);
455 
456   int ret = system(strSwitches);
457   if (ret != 0)
458   {
459     CLog::Log(LOGINFO, "%s: Failure: %d", __FUNCTION__, ret);
460   }
461 
462   return (ret == 0);
463 }
464 #endif
465 
466 #if defined(TARGET_ANDROID)
ExecuteAppAndroid(const char * strSwitches,const char * strPath)467 bool CExternalPlayer::ExecuteAppAndroid(const char* strSwitches,const char* strPath)
468 {
469   CLog::Log(LOGINFO, "%s: %s", __FUNCTION__, strSwitches);
470 
471   bool ret = CXBMCApp::StartActivity(strSwitches, "android.intent.action.VIEW", "video/*", strPath);
472 
473   if (!ret)
474   {
475     CLog::Log(LOGINFO, "%s: Failure", __FUNCTION__);
476   }
477 
478   return (ret == 0);
479 }
480 #endif
481 
Pause()482 void CExternalPlayer::Pause()
483 {
484 }
485 
HasVideo() const486 bool CExternalPlayer::HasVideo() const
487 {
488   return true;
489 }
490 
HasAudio() const491 bool CExternalPlayer::HasAudio() const
492 {
493   return false;
494 }
495 
CanSeek()496 bool CExternalPlayer::CanSeek()
497 {
498   return false;
499 }
500 
Seek(bool bPlus,bool bLargeStep,bool bChapterOverride)501 void CExternalPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
502 {
503 }
504 
SeekPercentage(float iPercent)505 void CExternalPlayer::SeekPercentage(float iPercent)
506 {
507 }
508 
SetAVDelay(float fValue)509 void CExternalPlayer::SetAVDelay(float fValue)
510 {
511 }
512 
GetAVDelay()513 float CExternalPlayer::GetAVDelay()
514 {
515   return 0.0f;
516 }
517 
SetSubTitleDelay(float fValue)518 void CExternalPlayer::SetSubTitleDelay(float fValue)
519 {
520 }
521 
GetSubTitleDelay()522 float CExternalPlayer::GetSubTitleDelay()
523 {
524   return 0.0;
525 }
526 
SeekTime(int64_t iTime)527 void CExternalPlayer::SeekTime(int64_t iTime)
528 {
529 }
530 
SetSpeed(float speed)531 void CExternalPlayer::SetSpeed(float speed)
532 {
533   m_speed = speed;
534   CDataCacheCore::GetInstance().SetSpeed(1.0, speed);
535 }
536 
GetPlayerState()537 std::string CExternalPlayer::GetPlayerState()
538 {
539   return "";
540 }
541 
SetPlayerState(const std::string & state)542 bool CExternalPlayer::SetPlayerState(const std::string& state)
543 {
544   return true;
545 }
546 
Initialize(TiXmlElement * pConfig)547 bool CExternalPlayer::Initialize(TiXmlElement* pConfig)
548 {
549   XMLUtils::GetString(pConfig, "filename", m_filename);
550   if (m_filename.length() > 0)
551   {
552     CLog::Log(LOGINFO, "ExternalPlayer Filename: %s", m_filename.c_str());
553   }
554   else
555   {
556     std::string xml;
557     xml<<*pConfig;
558     CLog::Log(LOGERROR, "ExternalPlayer Error: filename element missing from: %s", xml.c_str());
559     return false;
560   }
561 
562   XMLUtils::GetString(pConfig, "args", m_args);
563   XMLUtils::GetBoolean(pConfig, "playonestackitem", m_playOneStackItem);
564   XMLUtils::GetBoolean(pConfig, "islauncher", m_islauncher);
565   XMLUtils::GetBoolean(pConfig, "hidexbmc", m_hidexbmc);
566   if (!XMLUtils::GetBoolean(pConfig, "hideconsole", m_hideconsole))
567   {
568 #ifdef TARGET_WINDOWS_DESKTOP
569     // Default depends on whether player is a batch file
570     m_hideconsole = StringUtils::EndsWith(m_filename, ".bat");
571 #endif
572   }
573 
574   bool bHideCursor;
575   if (XMLUtils::GetBoolean(pConfig, "hidecursor", bHideCursor) && bHideCursor)
576     m_warpcursor = WARP_BOTTOM_RIGHT;
577 
578   std::string warpCursor;
579   if (XMLUtils::GetString(pConfig, "warpcursor", warpCursor) && !warpCursor.empty())
580   {
581     if (warpCursor == "bottomright") m_warpcursor = WARP_BOTTOM_RIGHT;
582     else if (warpCursor == "bottomleft") m_warpcursor = WARP_BOTTOM_LEFT;
583     else if (warpCursor == "topleft") m_warpcursor = WARP_TOP_LEFT;
584     else if (warpCursor == "topright") m_warpcursor = WARP_TOP_RIGHT;
585     else if (warpCursor == "center") m_warpcursor = WARP_CENTER;
586     else
587     {
588       warpCursor = "none";
589       CLog::Log(LOGWARNING, "ExternalPlayer: invalid value for warpcursor: %s", warpCursor.c_str());
590     }
591   }
592 
593   XMLUtils::GetInt(pConfig, "playcountminimumtime", m_playCountMinTime, 1, INT_MAX);
594 
595   CLog::Log(
596       LOGINFO,
597       "ExternalPlayer Tweaks: hideconsole (%s), hidexbmc (%s), islauncher (%s), warpcursor (%s)",
598       m_hideconsole ? "true" : "false", m_hidexbmc ? "true" : "false",
599       m_islauncher ? "true" : "false", warpCursor.c_str());
600 
601 #ifdef TARGET_WINDOWS_DESKTOP
602   m_filenameReplacers.push_back("^smb:// , / , \\\\ , g");
603   m_filenameReplacers.push_back("^smb:\\\\\\\\ , smb:(\\\\\\\\[^\\\\]*\\\\) , \\1 , ");
604 #endif
605 
606   TiXmlElement* pReplacers = pConfig->FirstChildElement("replacers");
607   while (pReplacers)
608   {
609     GetCustomRegexpReplacers(pReplacers, m_filenameReplacers);
610     pReplacers = pReplacers->NextSiblingElement("replacers");
611   }
612 
613   return true;
614 }
615 
GetCustomRegexpReplacers(TiXmlElement * pRootElement,std::vector<std::string> & settings)616 void CExternalPlayer::GetCustomRegexpReplacers(TiXmlElement *pRootElement,
617                                                std::vector<std::string>& settings)
618 {
619   int iAction = 0; // overwrite
620   // for backward compatibility
621   const char* szAppend = pRootElement->Attribute("append");
622   if ((szAppend && StringUtils::CompareNoCase(szAppend, "yes") == 0))
623     iAction = 1;
624   // action takes precedence if both attributes exist
625   const char* szAction = pRootElement->Attribute("action");
626   if (szAction)
627   {
628     iAction = 0; // overwrite
629     if (StringUtils::CompareNoCase(szAction, "append") == 0)
630       iAction = 1; // append
631     else if (StringUtils::CompareNoCase(szAction, "prepend") == 0)
632       iAction = 2; // prepend
633   }
634   if (iAction == 0)
635     settings.clear();
636 
637   TiXmlElement* pReplacer = pRootElement->FirstChildElement("replacer");
638   int i = 0;
639   while (pReplacer)
640   {
641     if (pReplacer->FirstChild())
642     {
643       const char* szGlobal = pReplacer->Attribute("global");
644       const char* szStop = pReplacer->Attribute("stop");
645       bool bGlobal = szGlobal && StringUtils::CompareNoCase(szGlobal, "true") == 0;
646       bool bStop = szStop && StringUtils::CompareNoCase(szStop, "true") == 0;
647 
648       std::string strMatch;
649       std::string strPat;
650       std::string strRep;
651       XMLUtils::GetString(pReplacer,"match",strMatch);
652       XMLUtils::GetString(pReplacer,"pat",strPat);
653       XMLUtils::GetString(pReplacer,"rep",strRep);
654 
655       if (!strPat.empty() && !strRep.empty())
656       {
657         CLog::Log(LOGDEBUG,"  Registering replacer:");
658         CLog::Log(LOGDEBUG,"    Match:[%s] Pattern:[%s] Replacement:[%s]", strMatch.c_str(), strPat.c_str(), strRep.c_str());
659         CLog::Log(LOGDEBUG,"    Global:[%s] Stop:[%s]", bGlobal?"true":"false", bStop?"true":"false");
660         // keep literal commas since we use comma as a separator
661         StringUtils::Replace(strMatch, ",",",,");
662         StringUtils::Replace(strPat, ",",",,");
663         StringUtils::Replace(strRep, ",",",,");
664 
665         std::string strReplacer = strMatch + " , " + strPat + " , " + strRep + " , " + (bGlobal ? "g" : "") + (bStop ? "s" : "");
666         if (iAction == 2)
667           settings.insert(settings.begin() + i++, 1, strReplacer);
668         else
669           settings.push_back(strReplacer);
670       }
671       else
672       {
673         // error message about missing tag
674         if (strPat.empty())
675           CLog::Log(LOGERROR,"  Missing <Pat> tag");
676         else
677           CLog::Log(LOGERROR,"  Missing <Rep> tag");
678       }
679     }
680 
681     pReplacer = pReplacer->NextSiblingElement("replacer");
682   }
683 }
684