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