PageRenderTime 61ms CodeModel.GetById 11ms app.highlight 44ms RepoModel.GetById 1ms app.codeStats 0ms

/xbmc/cores/ExternalPlayer/ExternalPlayer.cpp

http://github.com/xbmc/xbmc
C++ | 683 lines | 559 code | 94 blank | 30 comment | 107 complexity | b58159e67910de3cedd7323d15d6aa54 MD5 | raw file
  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
 46using namespace XFILE;
 47
 48#if defined(TARGET_WINDOWS_DESKTOP)
 49extern HWND g_hWnd;
 50#endif
 51
 52CExternalPlayer::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
 78CExternalPlayer::~CExternalPlayer()
 79{
 80  CloseFile();
 81}
 82
 83bool 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
105bool 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
121bool CExternalPlayer::IsPlaying() const
122{
123  return m_bIsPlaying;
124}
125
126void 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)
396bool 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)
452bool 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)
467bool 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
482void CExternalPlayer::Pause()
483{
484}
485
486bool CExternalPlayer::HasVideo() const
487{
488  return true;
489}
490
491bool CExternalPlayer::HasAudio() const
492{
493  return false;
494}
495
496bool CExternalPlayer::CanSeek()
497{
498  return false;
499}
500
501void CExternalPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
502{
503}
504
505void CExternalPlayer::SeekPercentage(float iPercent)
506{
507}
508
509void CExternalPlayer::SetAVDelay(float fValue)
510{
511}
512
513float CExternalPlayer::GetAVDelay()
514{
515  return 0.0f;
516}
517
518void CExternalPlayer::SetSubTitleDelay(float fValue)
519{
520}
521
522float CExternalPlayer::GetSubTitleDelay()
523{
524  return 0.0;
525}
526
527void CExternalPlayer::SeekTime(int64_t iTime)
528{
529}
530
531void CExternalPlayer::SetSpeed(float speed)
532{
533  m_speed = speed;
534  CDataCacheCore::GetInstance().SetSpeed(1.0, speed);
535}
536
537std::string CExternalPlayer::GetPlayerState()
538{
539  return "";
540}
541
542bool CExternalPlayer::SetPlayerState(const std::string& state)
543{
544  return true;
545}
546
547bool 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
616void 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}