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