1 /*
2 * Copyright (C) 2005-2021 Team Kodi (https://kodi.tv)
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 * See LICENSE.md for more information.
6 */
7
8 #include "Channel.h"
9
10 #include "../Settings.h"
11 #include "../utilities/FileUtils.h"
12 #include "../utilities/Logger.h"
13 #include "../utilities/StreamUtils.h"
14 #include "../utilities/WebUtils.h"
15
16 #include <regex>
17
18 #include <kodi/General.h>
19 #include <kodi/Filesystem.h>
20 #include <kodi/tools/StringUtils.h>
21
22 using namespace kodi::tools;
23 using namespace iptvsimple;
24 using namespace iptvsimple::data;
25 using namespace iptvsimple::utilities;
26
GetCatchupModeText(const CatchupMode & catchupMode)27 const std::string Channel::GetCatchupModeText(const CatchupMode& catchupMode)
28 {
29 switch (catchupMode)
30 {
31 case CatchupMode::DISABLED:
32 return "Disabled";
33 case CatchupMode::DEFAULT:
34 return "Default";
35 case CatchupMode::APPEND:
36 return "Append";
37 case CatchupMode::TIMESHIFT:
38 case CatchupMode::SHIFT:
39 return "Shift (SIPTV)";
40 case CatchupMode::FLUSSONIC:
41 return "Flussonic";
42 case CatchupMode::XTREAM_CODES:
43 return "Xtream codes";
44 case CatchupMode::VOD:
45 return "VOD";
46 default:
47 return "";
48 }
49 }
50
UpdateTo(Channel & left) const51 void Channel::UpdateTo(Channel& left) const
52 {
53 left.m_uniqueId = m_uniqueId;
54 left.m_radio = m_radio;
55 left.m_channelNumber = m_channelNumber;
56 left.m_subChannelNumber = m_subChannelNumber;
57 left.m_encryptionSystem = m_encryptionSystem;
58 left.m_tvgShift = m_tvgShift;
59 left.m_channelName = m_channelName;
60 left.m_iconPath = m_iconPath;
61 left.m_streamURL = m_streamURL;
62 left.m_hasCatchup = m_hasCatchup;
63 left.m_catchupMode = m_catchupMode;
64 left.m_catchupDays = m_catchupDays;
65 left.m_catchupSource = m_catchupSource;
66 left.m_isCatchupTSStream = m_isCatchupTSStream;
67 left.m_catchupSupportsTimeshifting = m_catchupSupportsTimeshifting;
68 left.m_catchupSourceTerminates = m_catchupSourceTerminates;
69 left.m_catchupGranularitySeconds = m_catchupGranularitySeconds;
70 left.m_catchupCorrectionSecs = m_catchupCorrectionSecs;
71 left.m_tvgId = m_tvgId;
72 left.m_tvgName = m_tvgName;
73 left.m_properties = m_properties;
74 left.m_inputStreamName = m_inputStreamName;
75 }
76
UpdateTo(kodi::addon::PVRChannel & left) const77 void Channel::UpdateTo(kodi::addon::PVRChannel& left) const
78 {
79 left.SetUniqueId(m_uniqueId);
80 left.SetIsRadio(m_radio);
81 left.SetChannelNumber(m_channelNumber);
82 left.SetSubChannelNumber(m_subChannelNumber);
83 left.SetChannelName(m_channelName);
84 left.SetEncryptionSystem(m_encryptionSystem);
85 left.SetIconPath(m_iconPath);
86 left.SetIsHidden(false);
87 left.SetHasArchive(IsCatchupSupported());
88 }
89
Reset()90 void Channel::Reset()
91 {
92 m_uniqueId = 0;
93 m_radio = false;
94 m_channelNumber = 0;
95 m_subChannelNumber = 0;
96 m_encryptionSystem = 0;
97 m_tvgShift = 0;
98 m_channelName.clear();
99 m_iconPath.clear();
100 m_streamURL.clear();
101 m_hasCatchup = false;
102 m_catchupMode = CatchupMode::DISABLED;
103 m_catchupDays = 0;
104 m_catchupSource.clear();
105 m_catchupSupportsTimeshifting = false;
106 m_catchupSourceTerminates = false;
107 m_catchupGranularitySeconds = 1;
108 m_isCatchupTSStream = false;
109 m_catchupCorrectionSecs = 0;
110 m_tvgId.clear();
111 m_tvgName.clear();
112 m_properties.clear();
113 m_inputStreamName.clear();
114 }
115
SetIconPathFromTvgLogo(const std::string & tvgLogo,std::string & channelName)116 void Channel::SetIconPathFromTvgLogo(const std::string& tvgLogo, std::string& channelName)
117 {
118 m_iconPath = tvgLogo;
119
120 bool logoSetFromChannelName = false;
121 if (m_iconPath.empty())
122 {
123 m_iconPath = m_channelName;
124 logoSetFromChannelName = true;
125 }
126
127 kodi::UnknownToUTF8(m_iconPath, m_iconPath);
128
129 // urlencode channel logo when set from channel name and source is Remote Path
130 // append extension as channel name wouldn't have it
131 if (logoSetFromChannelName && Settings::GetInstance().GetLogoPathType() == PathType::REMOTE_PATH)
132 m_iconPath = utilities::WebUtils::UrlEncode(m_iconPath);
133
134 if (m_iconPath.find("://") == std::string::npos)
135 {
136 const std::string& logoLocation = Settings::GetInstance().GetLogoLocation();
137 // If the file does not exist it must be relative
138 if (!logoLocation.empty() && !kodi::vfs::FileExists(m_iconPath))
139 {
140 // not absolute path, only append .png in this case.
141 m_iconPath = utilities::FileUtils::PathCombine(logoLocation, m_iconPath);
142
143 if (!StringUtils::EndsWithNoCase(m_iconPath, ".png") && !StringUtils::EndsWithNoCase(m_iconPath, ".jpg"))
144 m_iconPath += CHANNEL_LOGO_EXTENSION;
145 }
146 }
147 }
148
SetStreamURL(const std::string & url)149 void Channel::SetStreamURL(const std::string& url)
150 {
151 m_streamURL = url;
152
153 if (StringUtils::StartsWith(url, HTTP_PREFIX) || StringUtils::StartsWith(url, HTTPS_PREFIX))
154 {
155 if (!Settings::GetInstance().GetDefaultUserAgent().empty() && GetProperty("http-user-agent").empty())
156 AddProperty("http-user-agent", Settings::GetInstance().GetDefaultUserAgent());
157
158 TryToAddPropertyAsHeader("http-user-agent", "user-agent");
159 TryToAddPropertyAsHeader("http-referrer", "referer"); // spelling differences are correct
160 }
161
162 if (Settings::GetInstance().TransformMulticastStreamUrls() &&
163 (StringUtils::StartsWith(url, UDP_MULTICAST_PREFIX) || StringUtils::StartsWith(url, RTP_MULTICAST_PREFIX)))
164 {
165 const std::string typePath = StringUtils::StartsWith(url, "rtp") ? "/rtp/" : "/udp/";
166
167 m_streamURL = "http://" + Settings::GetInstance().GetUdpxyHost() + ":" + std::to_string(Settings::GetInstance().GetUdpxyPort()) + typePath + url.substr(UDP_MULTICAST_PREFIX.length());
168 Logger::Log(LEVEL_DEBUG, "%s - Transformed multicast stream URL to local relay url: %s", __FUNCTION__, m_streamURL.c_str());
169 }
170
171 if (!Settings::GetInstance().GetDefaultInputstream().empty() && GetProperty(PVR_STREAM_PROPERTY_INPUTSTREAM).empty())
172 AddProperty(PVR_STREAM_PROPERTY_INPUTSTREAM, Settings::GetInstance().GetDefaultInputstream());
173
174 if (!Settings::GetInstance().GetDefaultMimeType().empty() && GetProperty(PVR_STREAM_PROPERTY_MIMETYPE).empty())
175 AddProperty(PVR_STREAM_PROPERTY_MIMETYPE, Settings::GetInstance().GetDefaultMimeType());
176
177 m_inputStreamName = GetProperty(PVR_STREAM_PROPERTY_INPUTSTREAM);
178 }
179
GetProperty(const std::string & propName) const180 std::string Channel::GetProperty(const std::string& propName) const
181 {
182 auto propPair = m_properties.find(propName);
183 if (propPair != m_properties.end())
184 return propPair->second;
185
186 return {};
187 }
188
RemoveProperty(const std::string & propName)189 void Channel::RemoveProperty(const std::string& propName)
190 {
191 m_properties.erase(propName);
192 }
193
TryToAddPropertyAsHeader(const std::string & propertyName,const std::string & headerName)194 void Channel::TryToAddPropertyAsHeader(const std::string& propertyName, const std::string& headerName)
195 {
196 const std::string value = GetProperty(propertyName);
197
198 if (!value.empty())
199 {
200 m_streamURL = StreamUtils::AddHeaderToStreamUrl(m_streamURL, headerName, value);
201
202 RemoveProperty(propertyName);
203 }
204 }
205
ChannelTypeAllowsGroupsOnly() const206 bool Channel::ChannelTypeAllowsGroupsOnly() const
207 {
208 return ((m_radio && Settings::GetInstance().AllowRadioChannelGroupsOnly()) ||
209 (!m_radio && Settings::GetInstance().AllowTVChannelGroupsOnly()));
210 }
211
SetCatchupDays(int catchupDays)212 void Channel::SetCatchupDays(int catchupDays)
213 {
214 if (catchupDays > 0 || catchupDays == IGNORE_CATCHUP_DAYS)
215 m_catchupDays = catchupDays;
216 else
217 m_catchupDays = Settings::GetInstance().GetCatchupDays();
218 }
219
IsCatchupSupported() const220 bool Channel::IsCatchupSupported() const
221 {
222 return Settings::GetInstance().IsCatchupEnabled() && m_hasCatchup && !m_catchupSource.empty();
223 }
224
SupportsLiveStreamTimeshifting() const225 bool Channel::SupportsLiveStreamTimeshifting() const
226 {
227 return Settings::GetInstance().IsTimeshiftEnabled() && GetProperty(PVR_STREAM_PROPERTY_ISREALTIMESTREAM) == "true" &&
228 (Settings::GetInstance().IsTimeshiftEnabledAll() ||
229 (Settings::GetInstance().IsTimeshiftEnabledHttp() && StringUtils::StartsWith(m_streamURL, "http")) ||
230 (Settings::GetInstance().IsTimeshiftEnabledUdp() && StringUtils::StartsWith(m_streamURL, "udp"))
231 );
232 }
233
234 namespace
235 {
IsValidTimeshiftingCatchupSource(const std::string & formatString,const CatchupMode & catchupMode)236 bool IsValidTimeshiftingCatchupSource(const std::string& formatString, const CatchupMode& catchupMode)
237 {
238 // match any specifier, i.e. anything inside curly braces
239 std::regex const specifierRegex("\\{[^{]+\\}");
240
241 std::ptrdiff_t const numSpecifiers(std::distance(
242 std::sregex_iterator(formatString.begin(), formatString.end(), specifierRegex),
243 std::sregex_iterator()));
244
245 if (numSpecifiers > 0)
246 {
247 // If we only have a catchup-id specifier and nothing else then we can't timeshift
248 if ((formatString.find("{catchup-id}") != std::string::npos && numSpecifiers == 1) ||
249 catchupMode == CatchupMode::VOD)
250 return false;
251
252 return true;
253 }
254
255 return false;
256 }
257
IsTerminatingCatchupSource(const std::string & formatString)258 bool IsTerminatingCatchupSource(const std::string& formatString)
259 {
260 // A catchup stream terminates if it has an end time specifier
261 if (formatString.find("{duration}") != std::string::npos ||
262 formatString.find("{duration:") != std::string::npos ||
263 formatString.find("{lutc}") != std::string::npos ||
264 formatString.find("{lutc:") != std::string::npos ||
265 formatString.find("${timestamp}") != std::string::npos ||
266 formatString.find("${timestamp:") != std::string::npos ||
267 formatString.find("{utcend}") != std::string::npos ||
268 formatString.find("{utcend:") != std::string::npos ||
269 formatString.find("${end}") != std::string::npos ||
270 formatString.find("${end:") != std::string::npos)
271 return true;
272
273 return false;
274 }
275
FindCatchupSourceGranularitySeconds(const std::string & formatString)276 int FindCatchupSourceGranularitySeconds(const std::string& formatString)
277 {
278 // A catchup stream has one second granularity if it supports these specifiers
279 if (formatString.find("{utc}") != std::string::npos ||
280 formatString.find("{utc:") != std::string::npos ||
281 formatString.find("${start}") != std::string::npos ||
282 formatString.find("${start:") != std::string::npos ||
283 formatString.find("{S}") != std::string::npos ||
284 formatString.find("{offset:1}") != std::string::npos)
285 return 1;
286
287 return 60;
288 }
289
290 } // unnamed namespace
291
ConfigureCatchupMode()292 void Channel::ConfigureCatchupMode()
293 {
294 bool invalidCatchupSource = false;
295 bool appendProtocolOptions = true;
296
297 // preserve any kodi protocol options after "|"
298 std::string url = m_streamURL;
299 std::string protocolOptions;
300 size_t found = m_streamURL.find_first_of('|');
301 if (found != std::string::npos)
302 {
303 url = m_streamURL.substr(0, found);
304 protocolOptions = m_streamURL.substr(found, m_streamURL.length());
305 }
306
307 if (Settings::GetInstance().GetAllChannelsCatchupMode() != CatchupMode::DISABLED)
308 {
309 bool overrideCatchupMode = false;
310
311 if (Settings::GetInstance().GetCatchupOverrideMode() == CatchupOverrideMode::WITHOUT_TAGS &&
312 (m_catchupMode == CatchupMode::DISABLED || m_catchupMode == CatchupMode::TIMESHIFT))
313 {
314 // As CatchupMode::TIMESHIFT is obsolete and some providers use it
315 // incorrectly we allow this setting to override it
316 overrideCatchupMode = true;
317 }
318 else if (Settings::GetInstance().GetCatchupOverrideMode() == CatchupOverrideMode::WITH_TAGS &&
319 m_catchupMode != CatchupMode::DISABLED)
320 {
321 overrideCatchupMode = true;
322 }
323 else if (Settings::GetInstance().GetCatchupOverrideMode() == CatchupOverrideMode::ALL_CHANNELS)
324 {
325 overrideCatchupMode = true;
326 }
327
328 if (overrideCatchupMode)
329 {
330 m_catchupMode = Settings::GetInstance().GetAllChannelsCatchupMode();
331 m_hasCatchup = true;
332 }
333 }
334
335 switch (m_catchupMode)
336 {
337 case CatchupMode::DISABLED:
338 invalidCatchupSource = true;
339 break;
340 case CatchupMode::DEFAULT:
341 if (!m_catchupSource.empty())
342 {
343 if (m_catchupSource.find_first_of('|') != std::string::npos)
344 appendProtocolOptions = false;
345 break;
346 }
347 if (!GenerateAppendCatchupSource(url))
348 invalidCatchupSource = true;
349 break;
350 case CatchupMode::APPEND:
351 if (!GenerateAppendCatchupSource(url))
352 invalidCatchupSource = true;
353 break;
354 case CatchupMode::TIMESHIFT:
355 case CatchupMode::SHIFT:
356 GenerateShiftCatchupSource(url);
357 break;
358 case CatchupMode::FLUSSONIC:
359 if (!GenerateFlussonicCatchupSource(url))
360 invalidCatchupSource = true;
361 break;
362 case CatchupMode::XTREAM_CODES:
363 if (!GenerateXtreamCodesCatchupSource(url))
364 invalidCatchupSource = true;
365 break;
366 case CatchupMode::VOD:
367 if (!m_catchupSource.empty())
368 {
369 if (m_catchupSource.find_first_of('|') != std::string::npos)
370 appendProtocolOptions = false;
371 break;
372 }
373 m_catchupSource = "{catchup-id}";
374 break;
375 }
376
377 if (invalidCatchupSource)
378 {
379 m_catchupMode = CatchupMode::DISABLED;
380 m_hasCatchup = false;
381 m_catchupSource.clear();
382 }
383 else
384 {
385 if (!protocolOptions.empty() && appendProtocolOptions)
386 m_catchupSource += protocolOptions;
387
388 m_catchupSupportsTimeshifting = IsValidTimeshiftingCatchupSource(m_catchupSource, m_catchupMode);
389 m_catchupSourceTerminates = IsTerminatingCatchupSource(m_catchupSource);
390 m_catchupGranularitySeconds = FindCatchupSourceGranularitySeconds(m_catchupSource);
391 Logger::Log(LEVEL_DEBUG, "Channel Catchup Format string properties: %s, valid timeshifting source: %s, terminating source: %s, granularity secs: %d",
392 m_channelName.c_str(), m_catchupSupportsTimeshifting ? "true" : "false", m_catchupSourceTerminates ? "true" : "false", m_catchupGranularitySeconds);
393 }
394
395 if (m_catchupMode != CatchupMode::DISABLED)
396 Logger::Log(LEVEL_DEBUG, "%s - %s - %s: %s", __FUNCTION__, GetCatchupModeText(m_catchupMode).c_str(), m_channelName.c_str(), WebUtils::RedactUrl(m_catchupSource).c_str());
397 }
398
GenerateAppendCatchupSource(const std::string & url)399 bool Channel::GenerateAppendCatchupSource(const std::string& url)
400 {
401 if (!m_catchupSource.empty())
402 {
403 m_catchupSource = url + m_catchupSource;
404 return true;
405 }
406 else
407 {
408 if (!Settings::GetInstance().GetCatchupQueryFormat().empty())
409 {
410 m_catchupSource = url + Settings::GetInstance().GetCatchupQueryFormat();
411 return true;
412 }
413 }
414
415 return false;
416 }
417
GenerateShiftCatchupSource(const std::string & url)418 void Channel::GenerateShiftCatchupSource(const std::string& url)
419 {
420 if (url.find('?') != std::string::npos)
421 m_catchupSource = url + "&utc={utc}&lutc={lutc}";
422 else
423 m_catchupSource = url + "?utc={utc}&lutc={lutc}";
424 }
425
GenerateFlussonicCatchupSource(const std::string & url)426 bool Channel::GenerateFlussonicCatchupSource(const std::string& url)
427 {
428 // Example stream and catchup URLs
429 // stream: http://ch01.spr24.net/151/mpegts?token=my_token
430 // catchup: http://ch01.spr24.net/151/timeshift_abs-{utc}.ts?token=my_token
431 // stream: http://list.tv:8888/325/index.m3u8?token=secret
432 // catchup: http://list.tv:8888/325/timeshift_rel-{offset:1}.m3u8?token=secret
433 // stream: http://list.tv:8888/325/mono.m3u8?token=secret
434 // catchup: http://list.tv:8888/325/mono-timeshift_rel-{offset:1}.m3u8?token=secret
435
436 static std::regex fsRegex("^(http[s]?://[^/]+)/(.*)/([^/]*)(mpegts|\\.m3u8)(\\?.+=.+)?$");
437 std::smatch matches;
438
439 if (std::regex_match(url, matches, fsRegex))
440 {
441 if (matches.size() == 6)
442 {
443 const std::string fsHost = matches[1].str();
444 const std::string fsChannelId = matches[2].str();
445 const std::string fsListType = matches[3].str();
446 const std::string fsStreamType = matches[4].str();
447 const std::string fsUrlAppend = matches[5].str();
448
449 m_isCatchupTSStream = fsStreamType == "mpegts";
450 if (m_isCatchupTSStream)
451 {
452 m_catchupSource = fsHost + "/" + fsChannelId + "/timeshift_abs-${start}.ts" + fsUrlAppend;
453 }
454 else
455 {
456 if (fsListType == "index")
457 m_catchupSource = fsHost + "/" + fsChannelId + "/timeshift_rel-{offset:1}.m3u8" + fsUrlAppend;
458 else
459 m_catchupSource = fsHost + "/" + fsChannelId + "/" + fsListType + "-timeshift_rel-{offset:1}.m3u8" + fsUrlAppend;
460 }
461
462 return true;
463 }
464 }
465
466 return false;
467 }
468
GenerateXtreamCodesCatchupSource(const std::string & url)469 bool Channel::GenerateXtreamCodesCatchupSource(const std::string& url)
470 {
471 // Example stream and catchup URLs
472 // stream: http://list.tv:8080/my@account.xc/my_password/1477
473 // catchup: http://list.tv:8080/timeshift/my@account.xc/my_password/{duration}/{Y}-{m}-{d}:{H}-{M}/1477.ts
474 // stream: http://list.tv:8080/live/my@account.xc/my_password/1477.m3u8
475 // catchup: http://list.tv:8080/timeshift/my@account.xc/my_password/{duration}/{Y}-{m}-{d}:{H}-{M}/1477.m3u8
476
477 static std::regex xcRegex("^(http[s]?://[^/]+)/(?:live/)?([^/]+)/([^/]+)/([^/\\.]+)(\\.m3u[8]?)?$");
478 std::smatch matches;
479
480 if (std::regex_match(url, matches, xcRegex))
481 {
482 if (matches.size() == 6)
483 {
484 const std::string xcHost = matches[1].str();
485 const std::string xcUsername = matches[2].str();
486 const std::string xcPasssword = matches[3].str();
487 const std::string xcChannelId = matches[4].str();
488 std::string xcExtension;
489 if (matches[5].matched)
490 xcExtension = matches[5].str();
491
492 if (xcExtension.empty())
493 {
494 m_isCatchupTSStream = true;
495 xcExtension = ".ts";
496 }
497
498 m_catchupSource = xcHost + "/timeshift/" + xcUsername + "/" + xcPasssword +
499 "/{duration:60}/{Y}-{m}-{d}:{H}-{M}/" + xcChannelId + xcExtension;
500
501 return true;
502 }
503 }
504
505 return false;
506 }
507