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 "PlaylistLoader.h"
9 
10 #include "Settings.h"
11 #include "utilities/FileUtils.h"
12 #include "utilities/Logger.h"
13 #include "utilities/WebUtils.h"
14 
15 #include <chrono>
16 #include <cstdlib>
17 #include <map>
18 #include <regex>
19 #include <sstream>
20 #include <vector>
21 
22 #include <kodi/General.h>
23 #include <kodi/tools/StringUtils.h>
24 
25 using namespace kodi::tools;
26 using namespace iptvsimple;
27 using namespace iptvsimple::data;
28 using namespace iptvsimple::utilities;
29 
PlaylistLoader(kodi::addon::CInstancePVRClient * client,Channels & channels,ChannelGroups & channelGroups)30 PlaylistLoader::PlaylistLoader(kodi::addon::CInstancePVRClient* client, Channels& channels, ChannelGroups& channelGroups)
31   : m_channelGroups(channelGroups), m_channels(channels), m_client(client) { }
32 
Init()33 bool PlaylistLoader::Init()
34 {
35   m_m3uLocation = Settings::GetInstance().GetM3ULocation();
36   m_logoLocation = Settings::GetInstance().GetLogoLocation();
37   return true;
38 }
39 
LoadPlayList()40 bool PlaylistLoader::LoadPlayList()
41 {
42   auto started = std::chrono::high_resolution_clock::now();
43   Logger::Log(LEVEL_DEBUG, "%s - Playlist Load Start", __FUNCTION__);
44 
45   if (m_m3uLocation.empty())
46   {
47     Logger::Log(LEVEL_ERROR, "%s - Playlist file path is not configured. Channels not loaded.", __FUNCTION__);
48     return false;
49   }
50 
51   // Cache is only allowed if refresh mode is disabled
52   bool useM3UCache = Settings::GetInstance().GetM3URefreshMode() != RefreshMode::DISABLED ? false : Settings::GetInstance().UseM3UCache();
53 
54   std::string playlistContent;
55   if (!FileUtils::GetCachedFileContents(M3U_CACHE_FILENAME, m_m3uLocation, playlistContent, useM3UCache))
56   {
57     Logger::Log(LEVEL_ERROR, "%s - Unable to load playlist cache file '%s':  file is missing or empty.", __FUNCTION__, m_m3uLocation.c_str());
58     return false;
59   }
60 
61   std::stringstream stream(playlistContent);
62 
63   /* load channels */
64   bool isFirstLine = true;
65   bool isRealTime = true;
66   int epgTimeShift = 0;
67   int catchupCorrectionSecs = Settings::GetInstance().GetCatchupCorrectionSecs();
68   std::vector<int> currentChannelGroupIdList;
69   bool channelHadGroups = false;
70   bool xeevCatchup = false;
71 
72   Channel tmpChannel;
73 
74   std::string line;
75   while (std::getline(stream, line))
76   {
77     line = StringUtils::TrimRight(line, " \t\r\n");
78     line = StringUtils::TrimLeft(line, " \t");
79 
80     Logger::Log(LEVEL_DEBUG, "%s - M3U line read: '%s'", __FUNCTION__, line.c_str());
81 
82     if (line.empty())
83       continue;
84 
85     if (isFirstLine)
86     {
87       isFirstLine = false;
88 
89       if (StringUtils::Left(line, 3) == "\xEF\xBB\xBF")
90         line.erase(0, 3);
91 
92       if (StringUtils::StartsWith(line, M3U_START_MARKER)) //#EXTM3U
93       {
94         double tvgShiftDecimal = std::atof(ReadMarkerValue(line, TVG_INFO_SHIFT_MARKER).c_str());
95         epgTimeShift = static_cast<int>(tvgShiftDecimal * 3600.0);
96 
97         std::string strCatchupCorrection = ReadMarkerValue(line, CATCHUP_CORRECTION);
98         if (!strCatchupCorrection.empty())
99         {
100           double catchupCorrectionDecimal = std::atof(strCatchupCorrection.c_str());
101           catchupCorrectionSecs = static_cast<int>(catchupCorrectionDecimal * 3600.0);
102         }
103 
104         std::string strXeevCatchup = ReadMarkerValue(line, CATCHUP);
105         if (strXeevCatchup == "xc")
106           xeevCatchup = true;
107 
108         std::string tvgUrl = ReadMarkerValue(line, TVG_URL_MARKER);
109         if (tvgUrl.empty())
110           tvgUrl = ReadMarkerValue(line, TVG_URL_OTHER_MARKER);
111         Settings::GetInstance().SetTvgUrl(tvgUrl);
112 
113         continue;
114       }
115       else
116       {
117         Logger::Log(LEVEL_ERROR, "%s - URL '%s' missing %s descriptor on line 1, attempting to parse it anyway.",
118                     __FUNCTION__, m_m3uLocation.c_str(), M3U_START_MARKER.c_str());
119       }
120     }
121 
122     if (StringUtils::StartsWith(line, M3U_INFO_MARKER)) //#EXTINF
123     {
124       tmpChannel.SetChannelNumber(m_channels.GetCurrentChannelNumber());
125       currentChannelGroupIdList.clear();
126 
127       const std::string groupNamesListString = ParseIntoChannel(line, tmpChannel, currentChannelGroupIdList, epgTimeShift, catchupCorrectionSecs, xeevCatchup);
128 
129       if (!groupNamesListString.empty())
130       {
131         ParseAndAddChannelGroups(groupNamesListString, currentChannelGroupIdList, tmpChannel.IsRadio());
132         channelHadGroups = true;
133       }
134     }
135     else if (StringUtils::StartsWith(line, KODIPROP_MARKER)) //#KODIPROP:
136     {
137       ParseSinglePropertyIntoChannel(line, tmpChannel, KODIPROP_MARKER);
138     }
139     else if (StringUtils::StartsWith(line, EXTVLCOPT_MARKER)) //#EXTVLCOPT:
140     {
141       ParseSinglePropertyIntoChannel(line, tmpChannel, EXTVLCOPT_MARKER);
142     }
143     else if (StringUtils::StartsWith(line, EXTVLCOPT_DASH_MARKER)) //#EXTVLCOPT--
144     {
145       ParseSinglePropertyIntoChannel(line, tmpChannel, EXTVLCOPT_DASH_MARKER);
146     }
147     else if (StringUtils::StartsWith(line, M3U_GROUP_MARKER)) //#EXTGRP:
148     {
149       const std::string groupNamesListString = ReadMarkerValue(line, M3U_GROUP_MARKER);
150       if (!groupNamesListString.empty())
151       {
152         ParseAndAddChannelGroups(groupNamesListString, currentChannelGroupIdList, tmpChannel.IsRadio());
153         channelHadGroups = true;
154       }
155     }
156     else if (StringUtils::StartsWith(line, PLAYLIST_TYPE_MARKER)) //#EXT-X-PLAYLIST-TYPE:
157     {
158       if (ReadMarkerValue(line, PLAYLIST_TYPE_MARKER) == "VOD")
159         isRealTime = false;
160     }
161     else if (line[0] != '#')
162     {
163       Logger::Log(LEVEL_DEBUG, "%s - Adding channel '%s' with URL: '%s'", __FUNCTION__, tmpChannel.GetChannelName().c_str(), line.c_str());
164 
165       if (isRealTime)
166         tmpChannel.AddProperty(PVR_STREAM_PROPERTY_ISREALTIMESTREAM, "true");
167 
168       Channel channel(tmpChannel);
169       channel.SetStreamURL(line);
170       channel.ConfigureCatchupMode();
171 
172       if (!m_channels.AddChannel(channel, currentChannelGroupIdList, m_channelGroups, channelHadGroups))
173         Logger::Log(LEVEL_DEBUG, "%s - Not adding channel '%s' as only channels with groups are supported for %s channels per add-on settings", __func__, tmpChannel.GetChannelName().c_str(), channel.IsRadio() ? "radio" : "tv");
174 
175       tmpChannel.Reset();
176       isRealTime = true;
177       channelHadGroups = false;
178     }
179   }
180 
181   stream.clear();
182 
183   int milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(
184                       std::chrono::high_resolution_clock::now() - started).count();
185 
186   Logger::Log(LEVEL_INFO, "%s Playlist Loaded - %d (ms)", __FUNCTION__, milliseconds);
187 
188   if (m_channels.GetChannelsAmount() == 0)
189   {
190     Logger::Log(LEVEL_ERROR, "%s - Unable to load channels from file '%s'", __FUNCTION__, m_m3uLocation.c_str());
191     // We no longer return false as this is just an empty M3U and a missing file error.
192     //return false;
193   }
194 
195   Logger::Log(LEVEL_INFO, "%s - Loaded %d channels.", __FUNCTION__, m_channels.GetChannelsAmount());
196   return true;
197 }
198 
ParseIntoChannel(const std::string & line,Channel & channel,std::vector<int> & groupIdList,int epgTimeShift,int catchupCorrectionSecs,bool xeevCatchup)199 std::string PlaylistLoader::ParseIntoChannel(const std::string& line, Channel& channel, std::vector<int>& groupIdList, int epgTimeShift, int catchupCorrectionSecs, bool xeevCatchup)
200 {
201   size_t colonIndex = line.find(':');
202   size_t commaIndex = line.rfind(','); //default to last comma on line in case we don't find a better match
203 
204   size_t lastQuoteIndex = line.rfind('"');
205   if (lastQuoteIndex != std::string::npos)
206   {
207     // This is a better way to find the correct comma in
208     // case there is a comma embedded in the channel name
209     std::string possibleName = line.substr(lastQuoteIndex + 1);
210     std::string commaName = possibleName;
211     StringUtils::Trim(commaName);
212 
213     if (StringUtils::StartsWith(commaName, ","))
214     {
215       commaIndex = lastQuoteIndex + possibleName.find(',') + 1;
216       std::string temp = line.substr(commaIndex + 1);
217     }
218   }
219 
220   if (colonIndex != std::string::npos && commaIndex != std::string::npos && commaIndex > colonIndex)
221   {
222     // parse name
223     std::string channelName = line.substr(commaIndex + 1);
224     channelName = StringUtils::Trim(channelName);
225     kodi::UnknownToUTF8(channelName, channelName);
226     channel.SetChannelName(channelName);
227 
228     // parse info line containng the attributes for a channel
229     const std::string infoLine = line.substr(colonIndex + 1, commaIndex - colonIndex - 1);
230 
231     std::string strTvgId      = ReadMarkerValue(infoLine, TVG_INFO_ID_MARKER);
232     std::string strTvgName    = ReadMarkerValue(infoLine, TVG_INFO_NAME_MARKER);
233     std::string strTvgLogo    = ReadMarkerValue(infoLine, TVG_INFO_LOGO_MARKER);
234     std::string strChnlNo     = ReadMarkerValue(infoLine, TVG_INFO_CHNO_MARKER);
235     std::string strRadio      = ReadMarkerValue(infoLine, RADIO_MARKER);
236     std::string strTvgShift   = ReadMarkerValue(infoLine, TVG_INFO_SHIFT_MARKER);
237     std::string strCatchup       = ReadMarkerValue(infoLine, CATCHUP);
238     std::string strCatchupDays   = ReadMarkerValue(infoLine, CATCHUP_DAYS);
239     std::string strTvgRec        = ReadMarkerValue(infoLine, TVG_INFO_REC);
240     std::string strCatchupSource = ReadMarkerValue(infoLine, CATCHUP_SOURCE);
241     std::string strCatchupSiptv = ReadMarkerValue(infoLine, CATCHUP_SIPTV);
242     std::string strCatchupCorrection = ReadMarkerValue(infoLine, CATCHUP_CORRECTION);
243 
244     kodi::UnknownToUTF8(strTvgName, strTvgName);
245     kodi::UnknownToUTF8(strCatchupSource, strCatchupSource);
246 
247     // Some providers use a 'catchup-type' tag instead of 'catchup'
248     if (strCatchup.empty())
249       strCatchup = ReadMarkerValue(infoLine, CATCHUP_TYPE);
250 
251     if (strTvgId.empty())
252       strTvgId = ReadMarkerValue(infoLine, TVG_INFO_ID_MARKER_UC);
253 
254     if (strTvgId.empty())
255     {
256       char buff[255];
257       sprintf(buff, "%d", std::atoi(infoLine.c_str()));
258       strTvgId.append(buff);
259     }
260 
261     if (!strChnlNo.empty() && !Settings::GetInstance().NumberChannelsByM3uOrderOnly())
262     {
263       size_t found = strChnlNo.find('.');
264       if (found != std::string::npos)
265       {
266         channel.SetChannelNumber(std::atoi(strChnlNo.substr(0, found).c_str()));
267         channel.SetSubChannelNumber(std::atoi(strChnlNo.substr(found + 1).c_str()));
268       }
269       else
270       {
271         channel.SetChannelNumber(std::atoi(strChnlNo.c_str()));
272       }
273     }
274 
275     double tvgShiftDecimal = std::atof(strTvgShift.c_str());
276 
277     bool isRadio = StringUtils::EqualsNoCase(strRadio, "true");
278     channel.SetTvgId(strTvgId);
279     channel.SetTvgName(strTvgName);
280     channel.SetCatchupSource(strCatchupSource);
281     channel.SetTvgShift(static_cast<int>(tvgShiftDecimal * 3600.0));
282     channel.SetRadio(isRadio);
283     if (Settings::GetInstance().GetLogoPathType() == PathType::LOCAL_PATH && Settings::GetInstance().UseLocalLogosOnlyIgnoreM3U())
284       channel.SetIconPathFromTvgLogo("", channelName);
285     else
286       channel.SetIconPathFromTvgLogo(strTvgLogo, channelName);
287     if (strTvgShift.empty())
288       channel.SetTvgShift(epgTimeShift);
289 
290     double catchupCorrectionDecimal = std::atof(strCatchupCorrection.c_str());
291     channel.SetCatchupCorrectionSecs(static_cast<int>(catchupCorrectionDecimal * 3600.0));
292     if (strCatchupCorrection.empty())
293       channel.SetCatchupCorrectionSecs(catchupCorrectionSecs);
294 
295     if (StringUtils::EqualsNoCase(strCatchup, "default") || StringUtils::EqualsNoCase(strCatchup, "append") ||
296         StringUtils::EqualsNoCase(strCatchup, "shift") || StringUtils::EqualsNoCase(strCatchup, "flussonic") ||
297         StringUtils::EqualsNoCase(strCatchup, "flussonic-ts") || StringUtils::EqualsNoCase(strCatchup, "fs") ||
298         StringUtils::EqualsNoCase(strCatchup, "xc") || StringUtils::EqualsNoCase(strCatchup, "vod"))
299       channel.SetHasCatchup(true);
300 
301     if (StringUtils::EqualsNoCase(strCatchup, "default"))
302       channel.SetCatchupMode(CatchupMode::DEFAULT);
303     else if (StringUtils::EqualsNoCase(strCatchup, "append"))
304       channel.SetCatchupMode(CatchupMode::APPEND);
305     else if (StringUtils::EqualsNoCase(strCatchup, "shift"))
306       channel.SetCatchupMode(CatchupMode::SHIFT);
307     else if (StringUtils::EqualsNoCase(strCatchup, "flussonic") || StringUtils::EqualsNoCase(strCatchup, "flussonic-ts") || StringUtils::EqualsNoCase(strCatchup, "fs"))
308       channel.SetCatchupMode(CatchupMode::FLUSSONIC);
309     else if (StringUtils::EqualsNoCase(strCatchup, "xc"))
310       channel.SetCatchupMode(CatchupMode::XTREAM_CODES);
311     else if (StringUtils::EqualsNoCase(strCatchup, "vod"))
312       channel.SetCatchupMode(CatchupMode::VOD);
313 
314     if (!channel.HasCatchup() && xeevCatchup && (StringUtils::StartsWith(channelName, "* ") || StringUtils::StartsWith(channelName, "[+] ")))
315     {
316       channel.SetHasCatchup(true);
317       channel.SetCatchupMode(CatchupMode::XTREAM_CODES);
318     }
319 
320     int siptvTimeshiftDays = 0;
321     if (!strCatchupSiptv.empty())
322       siptvTimeshiftDays = atoi(strCatchupSiptv.c_str());
323     // treat tvg-rec tag like siptv if siptv has not been used
324     if (!strTvgRec.empty() && siptvTimeshiftDays == 0)
325       siptvTimeshiftDays = atoi(strTvgRec.c_str());
326 
327     if (!strCatchupDays.empty())
328       channel.SetCatchupDays(atoi(strCatchupDays.c_str()));
329     else if (channel.GetCatchupMode() == CatchupMode::VOD)
330       channel.SetCatchupDays(IGNORE_CATCHUP_DAYS);
331     else if (siptvTimeshiftDays > 0)
332       channel.SetCatchupDays(siptvTimeshiftDays);
333     else
334       channel.SetCatchupDays(Settings::GetInstance().GetCatchupDays());
335 
336     // We also need to support the timeshift="days" tag from siptv
337     // this was used before the catchup tags were introduced
338     // it is the same as catchup="shift" except it also includes days
339     if (!channel.HasCatchup() && siptvTimeshiftDays > 0)
340     {
341       channel.SetCatchupMode(CatchupMode::TIMESHIFT);
342       channel.SetHasCatchup(true);
343     }
344 
345     return ReadMarkerValue(infoLine, GROUP_NAME_MARKER);
346   }
347 
348   return "";
349 }
350 
ParseAndAddChannelGroups(const std::string & groupNamesListString,std::vector<int> & groupIdList,bool isRadio)351 void PlaylistLoader::ParseAndAddChannelGroups(const std::string& groupNamesListString, std::vector<int>& groupIdList, bool isRadio)
352 {
353   // groupNamesListString may have a single or multiple group names seapareted by ';'
354 
355   std::stringstream streamGroups(groupNamesListString);
356   std::string groupName;
357 
358   while (std::getline(streamGroups, groupName, ';'))
359   {
360     kodi::UnknownToUTF8(groupName, groupName);
361 
362     ChannelGroup group;
363     group.SetGroupName(groupName);
364     group.SetRadio(isRadio);
365 
366     if (m_channelGroups.CheckChannelGroupAllowed(group))
367     {
368       int uniqueGroupId = m_channelGroups.AddChannelGroup(group);
369       groupIdList.emplace_back(uniqueGroupId);
370     }
371   }
372 }
373 
ParseSinglePropertyIntoChannel(const std::string & line,Channel & channel,const std::string & markerName)374 void PlaylistLoader::ParseSinglePropertyIntoChannel(const std::string& line, Channel& channel, const std::string& markerName)
375 {
376   const std::string value = ReadMarkerValue(line, markerName);
377   auto pos = value.find('=');
378   if (pos != std::string::npos)
379   {
380     std::string prop = value.substr(0, pos);
381     StringUtils::ToLower(prop);
382     const std::string propValue = value.substr(pos + 1);
383 
384     bool addProperty = true;
385     if (markerName == EXTVLCOPT_DASH_MARKER)
386     {
387       addProperty &= prop == "http-reconnect";
388     }
389     else if (markerName == EXTVLCOPT_MARKER)
390     {
391       addProperty &= prop == "http-user-agent" || prop == "http-referrer" || prop == "program";
392     }
393     else if (markerName == KODIPROP_MARKER && (prop == "inputstreamaddon" || prop == "inputstreamclass"))
394     {
395       prop = PVR_STREAM_PROPERTY_INPUTSTREAM;
396     }
397 
398     if (addProperty)
399       channel.AddProperty(prop, propValue);
400 
401     Logger::Log(LEVEL_DEBUG, "%s - Found %s property: '%s' value: '%s' added: %s", __FUNCTION__, markerName.c_str(), prop.c_str(), propValue.c_str(), addProperty ? "true" : "false");
402   }
403 }
404 
ReloadPlayList()405 void PlaylistLoader::ReloadPlayList()
406 {
407   m_m3uLocation = Settings::GetInstance().GetM3ULocation();
408 
409   m_channels.Clear();
410   m_channelGroups.Clear();
411 
412   if (LoadPlayList())
413   {
414     m_client->TriggerChannelUpdate();
415     m_client->TriggerChannelGroupsUpdate();
416   }
417   else
418   {
419     m_channels.ChannelsLoadFailed();
420     m_channelGroups.ChannelGroupsLoadFailed();
421   }
422 }
423 
ReadMarkerValue(const std::string & line,const std::string & markerName)424 std::string PlaylistLoader::ReadMarkerValue(const std::string& line, const std::string& markerName)
425 {
426   size_t markerStart = line.find(markerName);
427   if (markerStart != std::string::npos)
428   {
429     const std::string marker = markerName;
430     markerStart += marker.length();
431     if (markerStart < line.length())
432     {
433       char find = ' ';
434       if (line[markerStart] == '"')
435       {
436         find = '"';
437         markerStart++;
438       }
439       size_t markerEnd = line.find(find, markerStart);
440       if (markerEnd == std::string::npos)
441       {
442         markerEnd = line.length();
443       }
444       return line.substr(markerStart, markerEnd - markerStart);
445     }
446   }
447 
448   return "";
449 }
450