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