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