1 /*
2  *  Copyright (C) 2012-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "EpgTagsContainer.h"
10 
11 #include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
12 #include "pvr/epg/EpgDatabase.h"
13 #include "pvr/epg/EpgInfoTag.h"
14 #include "pvr/epg/EpgTagsCache.h"
15 #include "utils/log.h"
16 
17 using namespace PVR;
18 
19 namespace
20 {
21 const CDateTimeSpan ONE_SECOND(0, 0, 0, 1);
22 }
23 
CPVREpgTagsContainer(int iEpgID,const std::shared_ptr<CPVREpgChannelData> & channelData,const std::shared_ptr<CPVREpgDatabase> & database)24 CPVREpgTagsContainer::CPVREpgTagsContainer(int iEpgID,
25                                            const std::shared_ptr<CPVREpgChannelData>& channelData,
26                                            const std::shared_ptr<CPVREpgDatabase>& database)
27   : m_iEpgID(iEpgID),
28     m_channelData(channelData),
29     m_database(database),
30     m_tagsCache(new CPVREpgTagsCache(iEpgID, channelData, database, m_changedTags))
31 {
32 }
33 
34 CPVREpgTagsContainer::~CPVREpgTagsContainer() = default;
35 
SetEpgID(int iEpgID)36 void CPVREpgTagsContainer::SetEpgID(int iEpgID)
37 {
38   m_iEpgID = iEpgID;
39   for (const auto& tag : m_changedTags)
40     tag.second->SetEpgID(iEpgID);
41 }
42 
SetChannelData(const std::shared_ptr<CPVREpgChannelData> & data)43 void CPVREpgTagsContainer::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
44 {
45   m_channelData = data;
46   m_tagsCache->SetChannelData(data);
47   for (const auto& tag : m_changedTags)
48     tag.second->SetChannelData(data);
49 }
50 
51 namespace
52 {
53 
ResolveConflictingTags(const std::shared_ptr<CPVREpgInfoTag> & changedTag,std::vector<std::shared_ptr<CPVREpgInfoTag>> & tags)54 void ResolveConflictingTags(const std::shared_ptr<CPVREpgInfoTag>& changedTag,
55                             std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags)
56 {
57   const CDateTime changedTagStart = changedTag->StartAsUTC();
58   const CDateTime changedTagEnd = changedTag->EndAsUTC();
59 
60   for (auto it = tags.begin(); it != tags.end();)
61   {
62     bool bInsert = false;
63 
64     if (changedTagEnd > (*it)->StartAsUTC() && changedTagStart < (*it)->EndAsUTC())
65     {
66       it = tags.erase(it);
67 
68       if (it == tags.end())
69       {
70         bInsert = true;
71       }
72     }
73     else if ((*it)->StartAsUTC() >= changedTagEnd)
74     {
75       bInsert = true;
76     }
77     else
78     {
79       ++it;
80     }
81 
82     if (bInsert)
83     {
84       tags.emplace(it, changedTag);
85       break;
86     }
87   }
88 }
89 
FixOverlap(const std::shared_ptr<CPVREpgInfoTag> & previousTag,const std::shared_ptr<CPVREpgInfoTag> & currentTag)90 bool FixOverlap(const std::shared_ptr<CPVREpgInfoTag>& previousTag,
91                 const std::shared_ptr<CPVREpgInfoTag>& currentTag)
92 {
93   if (!previousTag)
94     return true;
95 
96   if (previousTag->EndAsUTC() >= currentTag->EndAsUTC())
97   {
98     // delete the current tag. it's completely overlapped
99     CLog::LogF(LOGDEBUG,
100                "Erasing completely overlapped event from EPG timeline "
101                "({} - {} - {} - {}) "
102                "({} - {} - {} - {}).",
103                previousTag->UniqueBroadcastID(), previousTag->Title(),
104                previousTag->StartAsUTC().GetAsDBDateTime(),
105                previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
106                currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
107                currentTag->EndAsUTC().GetAsDBDateTime());
108 
109     return false;
110   }
111   else if (previousTag->EndAsUTC() > currentTag->StartAsUTC())
112   {
113     // fix the end time of the predecessor of the event
114     CLog::LogF(LOGDEBUG,
115                "Fixing partly overlapped event in EPG timeline "
116                "({} - {} - {} - {}) "
117                "({} - {} - {} - {}).",
118                previousTag->UniqueBroadcastID(), previousTag->Title(),
119                previousTag->StartAsUTC().GetAsDBDateTime(),
120                previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
121                currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
122                currentTag->EndAsUTC().GetAsDBDateTime());
123 
124     previousTag->SetEndFromUTC(currentTag->StartAsUTC());
125   }
126   return true;
127 }
128 
129 } // unnamed namespace
130 
UpdateEntries(const CPVREpgTagsContainer & tags)131 bool CPVREpgTagsContainer::UpdateEntries(const CPVREpgTagsContainer& tags)
132 {
133   if (tags.m_changedTags.empty())
134     return false;
135 
136   if (m_database)
137   {
138     const CDateTime minEventEnd = (*tags.m_changedTags.cbegin()).second->StartAsUTC() + ONE_SECOND;
139     const CDateTime maxEventStart = (*tags.m_changedTags.crbegin()).second->EndAsUTC();
140 
141     std::vector<std::shared_ptr<CPVREpgInfoTag>> existingTags =
142         m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
143 
144     if (!m_changedTags.empty())
145     {
146       // Fix data inconsistencies
147       for (const auto& changedTagsEntry : m_changedTags)
148       {
149         const auto& changedTag = changedTagsEntry.second;
150 
151         if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
152         {
153           // tag is in queried range, thus it could cause inconsistencies...
154           ResolveConflictingTags(changedTag, existingTags);
155         }
156       }
157     }
158 
159     bool bResetCache = false;
160     for (const auto& tagsEntry : tags.m_changedTags)
161     {
162       const auto& tag = tagsEntry.second;
163 
164       tag->SetChannelData(m_channelData);
165       tag->SetEpgID(m_iEpgID);
166 
167       std::shared_ptr<CPVREpgInfoTag> existingTag;
168       for (const auto& t : existingTags)
169       {
170         if (t->StartAsUTC() == tag->StartAsUTC())
171         {
172           existingTag = t;
173           break;
174         }
175       }
176 
177       if (existingTag)
178       {
179         existingTag->SetChannelData(m_channelData);
180         existingTag->SetEpgID(m_iEpgID);
181 
182         if (existingTag->Update(*tag, false))
183         {
184           // tag differs from existing tag and must be persisted
185           m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
186           bResetCache = true;
187         }
188       }
189       else
190       {
191         // new tags must always be persisted
192         m_changedTags.insert({tag->StartAsUTC(), tag});
193         bResetCache = true;
194       }
195     }
196 
197     if (bResetCache)
198       m_tagsCache->Reset();
199   }
200   else
201   {
202     for (const auto& tag : tags.m_changedTags)
203       UpdateEntry(tag.second);
204   }
205 
206   return true;
207 }
208 
FixOverlappingEvents(std::vector<std::shared_ptr<CPVREpgInfoTag>> & tags) const209 void CPVREpgTagsContainer::FixOverlappingEvents(
210     std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
211 {
212   std::shared_ptr<CPVREpgInfoTag> previousTag;
213   for (auto it = tags.begin(); it != tags.end();)
214   {
215     const std::shared_ptr<CPVREpgInfoTag> currentTag = *it;
216     if (FixOverlap(previousTag, currentTag))
217     {
218       previousTag = currentTag;
219       ++it;
220     }
221     else
222     {
223       it = tags.erase(it);
224       m_tagsCache->Reset();
225     }
226   }
227 }
228 
FixOverlappingEvents(std::map<CDateTime,std::shared_ptr<CPVREpgInfoTag>> & tags) const229 void CPVREpgTagsContainer::FixOverlappingEvents(
230     std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& tags) const
231 {
232   std::shared_ptr<CPVREpgInfoTag> previousTag;
233   for (auto it = tags.begin(); it != tags.end();)
234   {
235     const std::shared_ptr<CPVREpgInfoTag> currentTag = (*it).second;
236     if (FixOverlap(previousTag, currentTag))
237     {
238       previousTag = currentTag;
239       ++it;
240     }
241     else
242     {
243       it = tags.erase(it);
244       m_tagsCache->Reset();
245     }
246   }
247 }
248 
CreateEntry(const std::shared_ptr<CPVREpgInfoTag> & tag) const249 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateEntry(
250     const std::shared_ptr<CPVREpgInfoTag>& tag) const
251 {
252   if (tag)
253   {
254     tag->SetChannelData(m_channelData);
255   }
256   return tag;
257 }
258 
CreateEntries(const std::vector<std::shared_ptr<CPVREpgInfoTag>> & tags) const259 std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::CreateEntries(
260     const std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
261 {
262   for (auto& tag : tags)
263   {
264     tag->SetChannelData(m_channelData);
265   }
266   return tags;
267 }
268 
UpdateEntry(const std::shared_ptr<CPVREpgInfoTag> & tag)269 bool CPVREpgTagsContainer::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
270 {
271   tag->SetChannelData(m_channelData);
272   tag->SetEpgID(m_iEpgID);
273 
274   std::shared_ptr<CPVREpgInfoTag> existingTag = GetTag(tag->StartAsUTC());
275   if (existingTag)
276   {
277     if (existingTag->Update(*tag, false))
278     {
279       // tag differs from existing tag and must be persisted
280       m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
281       m_tagsCache->Reset();
282     }
283   }
284   else
285   {
286     // new tags must always be persisted
287     m_changedTags.insert({tag->StartAsUTC(), tag});
288     m_tagsCache->Reset();
289   }
290 
291   return true;
292 }
293 
DeleteEntry(const std::shared_ptr<CPVREpgInfoTag> & tag)294 bool CPVREpgTagsContainer::DeleteEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
295 {
296   m_changedTags.erase(tag->StartAsUTC());
297   m_deletedTags.insert({tag->StartAsUTC(), tag});
298   m_tagsCache->Reset();
299   return true;
300 }
301 
Cleanup(const CDateTime & time)302 void CPVREpgTagsContainer::Cleanup(const CDateTime& time)
303 {
304   for (auto it = m_changedTags.begin(); it != m_changedTags.end();)
305   {
306     if (it->second->EndAsUTC() < time)
307     {
308       m_tagsCache->Reset();
309 
310       const auto it1 = m_deletedTags.find(it->first);
311       if (it1 != m_deletedTags.end())
312         m_deletedTags.erase(it1);
313 
314       it = m_changedTags.erase(it);
315     }
316     else
317     {
318       ++it;
319     }
320   }
321 
322   if (m_database)
323     m_database->DeleteEpgTags(m_iEpgID, time);
324 }
325 
Clear()326 void CPVREpgTagsContainer::Clear()
327 {
328   m_changedTags.clear();
329 }
330 
IsEmpty() const331 bool CPVREpgTagsContainer::IsEmpty() const
332 {
333   if (!m_changedTags.empty())
334     return false;
335 
336   if (m_database)
337     return !m_database->GetFirstStartTime(m_iEpgID).IsValid();
338 
339   return true;
340 }
341 
GetTag(const CDateTime & startTime) const342 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(const CDateTime& startTime) const
343 {
344   const auto it = m_changedTags.find(startTime);
345   if (it != m_changedTags.cend())
346     return (*it).second;
347 
348   if (m_database)
349     return CreateEntry(m_database->GetEpgTagByStartTime(m_iEpgID, startTime));
350 
351   return {};
352 }
353 
GetTag(unsigned int iUniqueBroadcastID) const354 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(unsigned int iUniqueBroadcastID) const
355 {
356   if (iUniqueBroadcastID == EPG_TAG_INVALID_UID)
357     return {};
358 
359   for (const auto& tag : m_changedTags)
360   {
361     if (tag.second->UniqueBroadcastID() == iUniqueBroadcastID)
362       return tag.second;
363   }
364 
365   if (m_database)
366     return CreateEntry(m_database->GetEpgTagByUniqueBroadcastID(m_iEpgID, iUniqueBroadcastID));
367 
368   return {};
369 }
370 
GetTagByDatabaseID(int iDatabaseID) const371 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagByDatabaseID(int iDatabaseID) const
372 {
373   if (iDatabaseID <= 0)
374     return {};
375 
376   for (const auto& tag : m_changedTags)
377   {
378     if (tag.second->DatabaseID() == iDatabaseID)
379       return tag.second;
380   }
381 
382   if (m_database)
383     return CreateEntry(m_database->GetEpgTagByDatabaseID(m_iEpgID, iDatabaseID));
384 
385   return {};
386 }
387 
GetTagBetween(const CDateTime & start,const CDateTime & end) const388 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagBetween(const CDateTime& start,
389                                                                     const CDateTime& end) const
390 {
391   for (const auto& tag : m_changedTags)
392   {
393     if (tag.second->StartAsUTC() >= start)
394     {
395       if (tag.second->EndAsUTC() <= end)
396         return tag.second;
397       else
398         break;
399     }
400   }
401 
402   if (m_database)
403   {
404     const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags =
405         CreateEntries(m_database->GetEpgTagsByMinStartMaxEndTime(m_iEpgID, start, end));
406     if (!tags.empty())
407     {
408       if (tags.size() > 1)
409         CLog::LogF(LOGWARNING, "Got multiple tags. Picking up the first.");
410 
411       return tags.front();
412     }
413   }
414 
415   return {};
416 }
417 
GetActiveTag(bool bUpdateIfNeeded) const418 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetActiveTag(bool bUpdateIfNeeded) const
419 {
420   return m_tagsCache->GetNowActiveTag(bUpdateIfNeeded);
421 }
422 
GetLastEndedTag() const423 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetLastEndedTag() const
424 {
425   return m_tagsCache->GetLastEndedTag();
426 }
427 
GetNextStartingTag() const428 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetNextStartingTag() const
429 {
430   return m_tagsCache->GetNextStartingTag();
431 }
432 
CreateGapTag(const CDateTime & start,const CDateTime & end) const433 std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateGapTag(const CDateTime& start,
434                                                                    const CDateTime& end) const
435 {
436   return std::make_shared<CPVREpgInfoTag>(m_channelData, m_iEpgID, start, end, true);
437 }
438 
GetTimeline(const CDateTime & timelineStart,const CDateTime & timelineEnd,const CDateTime & minEventEnd,const CDateTime & maxEventStart) const439 std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetTimeline(
440     const CDateTime& timelineStart,
441     const CDateTime& timelineEnd,
442     const CDateTime& minEventEnd,
443     const CDateTime& maxEventStart) const
444 {
445   if (m_database)
446   {
447     std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
448 
449     if (!m_changedTags.empty() && !m_database->GetFirstStartTime(m_iEpgID).IsValid())
450     {
451       // nothing in the db yet. take what we have in memory.
452       for (const auto& tag : m_changedTags)
453       {
454         if (tag.second->EndAsUTC() > minEventEnd && tag.second->StartAsUTC() < maxEventStart)
455           tags.emplace_back(tag.second);
456       }
457 
458       if (!tags.empty())
459         FixOverlappingEvents(tags);
460     }
461     else
462     {
463       tags = m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
464 
465       if (!m_changedTags.empty())
466       {
467         // Fix data inconsistencies
468         for (const auto& changedTagsEntry : m_changedTags)
469         {
470           const auto& changedTag = changedTagsEntry.second;
471 
472           if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
473           {
474             // tag is in queried range, thus it could cause inconsistencies...
475             ResolveConflictingTags(changedTag, tags);
476           }
477         }
478       }
479     }
480 
481     tags = CreateEntries(tags);
482 
483     std::vector<std::shared_ptr<CPVREpgInfoTag>> result;
484 
485     for (const auto& epgTag : tags)
486     {
487       if (!result.empty())
488       {
489         const CDateTime currStart = epgTag->StartAsUTC();
490         const CDateTime prevEnd = result.back()->EndAsUTC();
491         if ((currStart - prevEnd) >= ONE_SECOND)
492         {
493           // insert gap tag before current tag
494           result.emplace_back(CreateGapTag(prevEnd, currStart));
495         }
496       }
497 
498       result.emplace_back(epgTag);
499     }
500 
501     if (result.empty())
502     {
503       // create single gap tag
504       CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
505       if (!maxEnd.IsValid() || maxEnd < timelineStart)
506         maxEnd = timelineStart;
507 
508       CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
509       if (!minStart.IsValid() || minStart > timelineEnd)
510         minStart = timelineEnd;
511 
512       result.emplace_back(CreateGapTag(maxEnd, minStart));
513     }
514     else
515     {
516       if (result.front()->StartAsUTC() > minEventEnd)
517       {
518         // prepend gap tag
519         CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
520         if (!maxEnd.IsValid() || maxEnd < timelineStart)
521           maxEnd = timelineStart;
522 
523         result.insert(result.begin(), CreateGapTag(maxEnd, result.front()->StartAsUTC()));
524       }
525 
526       if (result.back()->EndAsUTC() < maxEventStart)
527       {
528         // append gap tag
529         CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
530         if (!minStart.IsValid() || minStart > timelineEnd)
531           minStart = timelineEnd;
532 
533         result.emplace_back(CreateGapTag(result.back()->EndAsUTC(), minStart));
534       }
535     }
536 
537     return result;
538   }
539 
540   return {};
541 }
542 
GetAllTags() const543 std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetAllTags() const
544 {
545   if (m_database)
546   {
547     std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
548     if (!m_changedTags.empty() && !m_database->GetFirstStartTime(m_iEpgID).IsValid())
549     {
550       // nothing in the db yet. take what we have in memory.
551       for (const auto& tag : m_changedTags)
552         tags.emplace_back(tag.second);
553 
554       FixOverlappingEvents(tags);
555     }
556     else
557     {
558       tags = m_database->GetAllEpgTags(m_iEpgID);
559 
560       if (!m_changedTags.empty())
561       {
562         // Fix data inconsistencies
563         for (const auto& changedTagsEntry : m_changedTags)
564         {
565           ResolveConflictingTags(changedTagsEntry.second, tags);
566         }
567       }
568     }
569 
570     return CreateEntries(tags);
571   }
572 
573   return {};
574 }
575 
GetFirstStartTime() const576 CDateTime CPVREpgTagsContainer::GetFirstStartTime() const
577 {
578   CDateTime result;
579 
580   if (!m_changedTags.empty())
581     result = (*m_changedTags.cbegin()).second->StartAsUTC();
582 
583   if (m_database)
584   {
585     const CDateTime dbResult = m_database->GetFirstStartTime(m_iEpgID);
586     if (!result.IsValid() || (dbResult.IsValid() && dbResult < result))
587       result = dbResult;
588   }
589 
590   return result;
591 }
592 
GetLastEndTime() const593 CDateTime CPVREpgTagsContainer::GetLastEndTime() const
594 {
595   CDateTime result;
596 
597   if (!m_changedTags.empty())
598     result = (*m_changedTags.crbegin()).second->EndAsUTC();
599 
600   if (m_database)
601   {
602     const CDateTime dbResult = m_database->GetLastEndTime(m_iEpgID);
603     if (!result.IsValid() || (dbResult.IsValid() && dbResult > result))
604       result = dbResult;
605   }
606 
607   return result;
608 }
609 
NeedsSave() const610 bool CPVREpgTagsContainer::NeedsSave() const
611 {
612   return !m_changedTags.empty() || !m_deletedTags.empty();
613 }
614 
QueuePersistQuery()615 void CPVREpgTagsContainer::QueuePersistQuery()
616 {
617   if (m_database)
618   {
619     m_database->Lock();
620 
621     CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Tags Container: Updating {}, deleting {} events...",
622                 m_changedTags.size(), m_deletedTags.size());
623 
624     for (const auto& tag : m_deletedTags)
625       m_database->QueueDeleteTagQuery(*tag.second);
626 
627     m_deletedTags.clear();
628 
629     FixOverlappingEvents(m_changedTags);
630 
631     for (const auto& tag : m_changedTags)
632     {
633       // remove any conflicting events from database before persisting the new event
634       m_database->QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(
635           m_iEpgID, tag.second->StartAsUTC() + ONE_SECOND, tag.second->EndAsUTC() - ONE_SECOND);
636 
637       tag.second->QueuePersistQuery(m_database);
638     }
639 
640     m_changedTags.clear();
641 
642     m_database->Unlock();
643   }
644 }
645 
QueueDelete()646 void CPVREpgTagsContainer::QueueDelete()
647 {
648   if (m_database)
649     m_database->QueueDeleteEpgTags(m_iEpgID);
650 
651   Clear();
652 }
653