1 /*
2 * dvbepg.cpp
3 *
4 * Copyright (C) 2009-2011 Christoph Pfister <christophpfister@gmail.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21 #include "../log.h"
22
23 #include <QDataStream>
24 #include <QFile>
25 #include <QLoggingCategory>
26 #include <QStandardPaths>
27
28 #include "../ensurenopendingoperation.h"
29 #include "../iso-codes.h"
30 #include "dvbdevice.h"
31 #include "dvbepg.h"
32 #include "dvbepg_p.h"
33 #include "dvbmanager.h"
34 #include "dvbsi.h"
35
validate() const36 bool DvbEpgEntry::validate() const
37 {
38 if (channel.isValid() && begin.isValid() && (begin.timeSpec() == Qt::UTC) &&
39 duration.isValid()) {
40 return true;
41 }
42
43 return false;
44 }
45
operator <(const DvbEpgEntryId & other) const46 bool DvbEpgEntryId::operator<(const DvbEpgEntryId &other) const
47 {
48 if (entry->channel != other.entry->channel) {
49 return (entry->channel < other.entry->channel);
50 }
51
52 if (entry->begin != other.entry->begin) {
53 return (entry->begin < other.entry->begin);
54 }
55 return false;
56 }
57
DvbEpgModel(DvbManager * manager_,QObject * parent)58 DvbEpgModel::DvbEpgModel(DvbManager *manager_, QObject *parent) : QObject(parent),
59 manager(manager_), hasPendingOperation(false)
60 {
61 currentDateTimeUtc = QDateTime::currentDateTime().toUTC();
62 startTimer(54000);
63
64 DvbChannelModel *channelModel = manager->getChannelModel();
65 connect(channelModel, SIGNAL(channelAboutToBeUpdated(DvbSharedChannel)),
66 this, SLOT(channelAboutToBeUpdated(DvbSharedChannel)));
67 connect(channelModel, SIGNAL(channelUpdated(DvbSharedChannel)),
68 this, SLOT(channelUpdated(DvbSharedChannel)));
69 connect(channelModel, SIGNAL(channelRemoved(DvbSharedChannel)),
70 this, SLOT(channelRemoved(DvbSharedChannel)));
71 connect(manager->getRecordingModel(), SIGNAL(recordingRemoved(DvbSharedRecording)),
72 this, SLOT(recordingRemoved(DvbSharedRecording)));
73
74 // TODO use SQL to store epg data
75
76 QFile file(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/epgdata.dvb"));
77
78 if (!file.open(QIODevice::ReadOnly)) {
79 qCWarning(logEpg, "Cannot open %s", qPrintable(file.fileName()));
80 return;
81 }
82
83 QDataStream stream(&file);
84 stream.setVersion(QDataStream::Qt_4_4);
85 DvbRecordingModel *recordingModel = manager->getRecordingModel();
86 bool hasRecordingKey = true, hasParental = true, hasMultilang = true;
87 int version;
88 stream >> version;
89
90 if (version == 0x1ce0eca7) {
91 hasRecordingKey = false;
92 } else if (version == 0x79cffd36) {
93 hasParental = false;
94 } else if (version == 0x140c37b5) {
95 hasMultilang = false;
96 } else if (version != 0x20171112) {
97 qCWarning(logEpg, "Wrong DB version for: %s", qPrintable(file.fileName()));
98 return;
99 }
100
101 while (!stream.atEnd()) {
102 DvbEpgEntry entry;
103 QString channelName;
104 stream >> channelName;
105 entry.channel = channelModel->findChannelByName(channelName);
106 stream >> entry.begin;
107 entry.begin = entry.begin.toUTC();
108 stream >> entry.duration;
109
110 if (hasMultilang) {
111 int i, count;
112
113 stream >> count;
114
115 for (i = 0; i < count; i++) {
116 QString code;
117
118 DvbEpgLangEntry langEntry;
119 stream >> code;
120 stream >> langEntry.title;
121 stream >> langEntry.subheading;
122 stream >> langEntry.details;
123
124 entry.langEntry[code] = langEntry;
125
126 if (!langEntry.title.isEmpty() && !manager->languageCodes.contains(code))
127 manager->languageCodes[code] = true;
128 }
129
130
131 } else {
132 DvbEpgLangEntry langEntry;
133
134 stream >> langEntry.title;
135 stream >> langEntry.subheading;
136 stream >> langEntry.details;
137
138 entry.langEntry[FIRST_LANG] = langEntry;
139 }
140
141 if (hasRecordingKey) {
142 SqlKey recordingKey;
143 stream >> recordingKey.sqlKey;
144
145 if (recordingKey.isSqlKeyValid()) {
146 entry.recording = recordingModel->findRecordingByKey(recordingKey);
147 }
148 }
149
150 if (hasParental) {
151 unsigned type;
152
153 stream >> type;
154 stream >> entry.content;
155 stream >> entry.parental;
156
157 if (type <= DvbEpgEntry::EitLast)
158 entry.type = DvbEpgEntry::EitType(type);
159 else
160 entry.type = DvbEpgEntry::EitActualTsSchedule;
161 }
162
163 if (stream.status() != QDataStream::Ok) {
164 qCWarning(logEpg, "Corrupt data %s", qPrintable(file.fileName()));
165 break;
166 }
167
168 addEntry(entry);
169 }
170 }
171
~DvbEpgModel()172 DvbEpgModel::~DvbEpgModel()
173 {
174 if (hasPendingOperation) {
175 qCWarning(logEpg, "Illegal recursive call");
176 }
177
178 if (!dvbEpgFilters.isEmpty() || !atscEpgFilters.isEmpty()) {
179 qCWarning(logEpg, "filter list not empty");
180 }
181
182 QFile file(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/epgdata.dvb"));
183
184 if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
185 qCWarning(logEpg, "Cannot open %s", qPrintable(file.fileName()));
186 return;
187 }
188
189 QDataStream stream(&file);
190 stream.setVersion(QDataStream::Qt_4_4);
191 int version = 0x20171112;
192 stream << version;
193
194 foreach (const DvbSharedEpgEntry &entry, entries) {
195 SqlKey recordingKey;
196
197 if (entry->recording.isValid()) {
198 recordingKey = *entry->recording;
199 }
200
201 stream << entry->channel->name;
202 stream << entry->begin;
203 stream << entry->duration;
204
205 stream << entry->langEntry.size();
206
207 QHashIterator<QString, DvbEpgLangEntry> i(entry->langEntry);
208
209 while (i.hasNext()) {
210 i.next();
211
212 stream << i.key();
213
214 DvbEpgLangEntry langEntry = i.value();
215
216 stream << langEntry.title;
217 stream << langEntry.subheading;
218 stream << langEntry.details;
219 }
220
221 stream << recordingKey.sqlKey;
222 stream << int(entry->type);
223 stream << entry->content;
224 stream << entry->parental;
225 }
226 }
227
getRecordings() const228 QMap<DvbSharedRecording, DvbSharedEpgEntry> DvbEpgModel::getRecordings() const
229 {
230 return recordings;
231 }
232
setRecordings(const QMap<DvbSharedRecording,DvbSharedEpgEntry> map)233 void DvbEpgModel::setRecordings(const QMap<DvbSharedRecording, DvbSharedEpgEntry> map)
234 {
235 recordings = map;
236 }
237
getEntries() const238 QMap<DvbEpgEntryId, DvbSharedEpgEntry> DvbEpgModel::getEntries() const
239 {
240 return entries;
241 }
242
getEpgChannels() const243 QHash<DvbSharedChannel, int> DvbEpgModel::getEpgChannels() const
244 {
245 return epgChannels;
246 }
247
getCurrentNext(const DvbSharedChannel & channel) const248 QList<DvbSharedEpgEntry> DvbEpgModel::getCurrentNext(const DvbSharedChannel &channel) const
249 {
250 QList<DvbSharedEpgEntry> result;
251 DvbEpgEntry fakeEntry(channel);
252
253 for (ConstIterator it = entries.lowerBound(DvbEpgEntryId(&fakeEntry));
254 it != entries.constEnd(); ++it) {
255 const DvbSharedEpgEntry &entry = *it;
256
257 if (entry->channel != channel) {
258 break;
259 }
260
261 result.append(entry);
262
263 if (result.size() == 2) {
264 break;
265 }
266 }
267
268 return result;
269 }
270
Debug(QString text,const DvbSharedEpgEntry & entry)271 void DvbEpgModel::Debug(QString text, const DvbSharedEpgEntry &entry)
272 {
273 if (!QLoggingCategory::defaultCategory()->isEnabled(QtDebugMsg))
274 return;
275
276 QDateTime begin = entry->begin.toLocalTime();
277 QTime end = entry->begin.addSecs(QTime(0, 0, 0).secsTo(entry->duration)).toLocalTime().time();
278
279 qCDebug(logEpg, "event %s: type %d, from %s to %s: %s: %s: %s : %s",
280 qPrintable(text), entry->type, qPrintable(QLocale().toString(begin, QLocale::ShortFormat)), qPrintable(QLocale().toString(end)),
281 qPrintable(entry->title()), qPrintable(entry->subheading()), qPrintable(entry->details()), qPrintable(entry->content));
282 }
283
addEntry(const DvbEpgEntry & entry)284 DvbSharedEpgEntry DvbEpgModel::addEntry(const DvbEpgEntry &entry)
285 {
286 if (!entry.validate()) {
287 qCWarning(logEpg, "Invalid entry: channel is %s, begin is %s, duration is %s", entry.channel.isValid() ? "valid" : "invalid", entry.begin.isValid() ? "valid" : "invalid", entry.duration.isValid() ? "valid" : "invalid");
288 return DvbSharedEpgEntry();
289 }
290
291 if (hasPendingOperation) {
292 qCWarning(logEpg, "Illegal recursive call");
293 return DvbSharedEpgEntry();
294 }
295
296 EnsureNoPendingOperation ensureNoPendingOperation(hasPendingOperation);
297
298 // Check if the event was already recorded
299 const QDateTime end = entry.begin.addSecs(QTime(0, 0, 0).secsTo(entry.duration));
300
301 // Optimize duplicated register logic by using find, with is O(log n)
302 Iterator it = entries.find(DvbEpgEntryId(&entry));
303 while (it != entries.end()) {
304 const DvbSharedEpgEntry &existingEntry = *it;
305
306 // Don't do anything if the event already exists
307 if (*existingEntry == entry)
308 return DvbSharedEpgEntry();
309
310 const QDateTime enEnd = existingEntry->begin.addSecs(QTime(0, 0, 0).secsTo(existingEntry->duration));
311
312 // The logic here was simplified due to performance.
313 // It won't check anymore if an event has its start time
314 // switched, as that would require a O(n) loop, with is
315 // too slow, specially on DVB-S/S2. So, we're letting the QMap
316 // to use a key with just channel/begin time, identifying
317 // obsolete entries only if the end time doesn't match.
318
319 // A new event conflicts with an existing one
320 if (end != enEnd) {
321 Debug("removed", existingEntry);
322 it = removeEntry(it);
323 break;
324 }
325 // New event data for the same event
326 if (existingEntry->details(FIRST_LANG).isEmpty() && !entry.details(FIRST_LANG).isEmpty()) {
327 emit entryAboutToBeUpdated(existingEntry);
328
329 QHashIterator<QString, DvbEpgLangEntry> i(entry.langEntry);
330
331 while (i.hasNext()) {
332 i.next();
333
334 DvbEpgLangEntry langEntry = i.value();
335
336 const_cast<DvbEpgEntry *>(existingEntry.constData())->langEntry[i.key()].details = langEntry.details;
337 }
338 emit entryUpdated(existingEntry);
339 Debug("updated", existingEntry);
340 }
341 return existingEntry;
342 }
343
344 if (entry.begin.addSecs(QTime(0, 0, 0).secsTo(entry.duration)) > currentDateTimeUtc) {
345 DvbSharedEpgEntry existingEntry = entries.value(DvbEpgEntryId(&entry));
346
347 if (existingEntry.isValid()) {
348 if (existingEntry->details(FIRST_LANG).isEmpty() && !entry.details(FIRST_LANG).isEmpty()) {
349 // needed for atsc
350 emit entryAboutToBeUpdated(existingEntry);
351
352 QHashIterator<QString, DvbEpgLangEntry> i(entry.langEntry);
353
354 while (i.hasNext()) {
355 i.next();
356
357 DvbEpgLangEntry langEntry = i.value();
358
359 const_cast<DvbEpgEntry *>(existingEntry.constData())->langEntry[i.key()].details = langEntry.details;
360 }
361 emit entryUpdated(existingEntry);
362 Debug("updated2", existingEntry);
363 }
364
365 return existingEntry;
366 }
367
368 DvbSharedEpgEntry newEntry(new DvbEpgEntry(entry));
369 entries.insert(DvbEpgEntryId(newEntry), newEntry);
370
371 if (newEntry->recording.isValid()) {
372 recordings.insert(newEntry->recording, newEntry);
373 }
374
375 if (++epgChannels[newEntry->channel] == 1) {
376 emit epgChannelAdded(newEntry->channel);
377 }
378
379 emit entryAdded(newEntry);
380 Debug("new", newEntry);
381 return newEntry;
382 }
383
384 return DvbSharedEpgEntry();
385 }
386
scheduleProgram(const DvbSharedEpgEntry & entry,int extraSecondsBefore,int extraSecondsAfter,bool checkForRecursion,int priority)387 void DvbEpgModel::scheduleProgram(const DvbSharedEpgEntry &entry, int extraSecondsBefore,
388 int extraSecondsAfter, bool checkForRecursion, int priority)
389 {
390 if (!entry.isValid() || (entries.value(DvbEpgEntryId(entry)) != entry)) {
391 qCWarning(logEpg, "Can't schedule program: invalid entry");
392 return;
393 }
394
395 if (hasPendingOperation) {
396 qCWarning(logEpg, "Illegal recursive call");
397 return;
398 }
399
400 EnsureNoPendingOperation ensureNoPendingOperation(hasPendingOperation);
401 emit entryAboutToBeUpdated(entry);
402 DvbSharedRecording oldRecording;
403
404 if (!entry->recording.isValid()) {
405 DvbRecording recording;
406 recording.priority = priority;
407 recording.name = entry->title(manager->currentEpgLanguage);
408 recording.channel = entry->channel;
409 recording.begin = entry->begin.addSecs(-extraSecondsBefore);
410 recording.beginEPG = entry->begin;
411 recording.duration =
412 entry->duration.addSecs(extraSecondsBefore + extraSecondsAfter);
413 recording.durationEPG =
414 entry->duration;
415 recording.subheading =
416 entry->subheading(manager->currentEpgLanguage);
417 recording.details =
418 entry->details(manager->currentEpgLanguage);
419 recording.disabled = false;
420 const_cast<DvbEpgEntry *>(entry.constData())->recording =
421 manager->getRecordingModel()->addRecording(recording, checkForRecursion);
422 recordings.insert(entry->recording, entry);
423 } else {
424 oldRecording = entry->recording;
425 recordings.remove(entry->recording);
426 const_cast<DvbEpgEntry *>(entry.constData())->recording = DvbSharedRecording();
427 }
428
429 emit entryUpdated(entry);
430
431 if (oldRecording.isValid()) {
432 // recordingRemoved() will be called
433 hasPendingOperation = false;
434 manager->getRecordingModel()->removeRecording(oldRecording);
435 }
436 }
437
startEventFilter(DvbDevice * device,const DvbSharedChannel & channel)438 void DvbEpgModel::startEventFilter(DvbDevice *device, const DvbSharedChannel &channel)
439 {
440 if (manager->disableEpg())
441 return;
442
443 switch (channel->transponder.getTransmissionType()) {
444 case DvbTransponderBase::Invalid:
445 break;
446 case DvbTransponderBase::DvbC:
447 case DvbTransponderBase::DvbS:
448 case DvbTransponderBase::DvbS2:
449 case DvbTransponderBase::DvbT:
450 case DvbTransponderBase::DvbT2:
451 case DvbTransponderBase::IsdbT:
452 dvbEpgFilters.append(QExplicitlySharedDataPointer<DvbEpgFilter>(
453 new DvbEpgFilter(manager, device, channel)));
454 break;
455 case DvbTransponderBase::Atsc:
456 atscEpgFilters.append(QExplicitlySharedDataPointer<AtscEpgFilter>(
457 new AtscEpgFilter(manager, device, channel)));
458 break;
459 }
460 }
461
stopEventFilter(DvbDevice * device,const DvbSharedChannel & channel)462 void DvbEpgModel::stopEventFilter(DvbDevice *device, const DvbSharedChannel &channel)
463 {
464 switch (channel->transponder.getTransmissionType()) {
465 case DvbTransponderBase::Invalid:
466 break;
467 case DvbTransponderBase::DvbC:
468 case DvbTransponderBase::DvbS:
469 case DvbTransponderBase::DvbS2:
470 case DvbTransponderBase::DvbT:
471 case DvbTransponderBase::DvbT2:
472 case DvbTransponderBase::IsdbT:
473 for (int i = 0; i < dvbEpgFilters.size(); ++i) {
474 const DvbEpgFilter *epgFilter = dvbEpgFilters.at(i).constData();
475
476 if ((epgFilter->device == device) &&
477 (epgFilter->source == channel->source) &&
478 (epgFilter->transponder.corresponds(channel->transponder))) {
479 dvbEpgFilters.removeAt(i);
480 break;
481 }
482 }
483
484 break;
485 case DvbTransponderBase::Atsc:
486 for (int i = 0; i < atscEpgFilters.size(); ++i) {
487 const AtscEpgFilter *epgFilter = atscEpgFilters.at(i).constData();
488
489 if ((epgFilter->device == device) &&
490 (epgFilter->source == channel->source) &&
491 (epgFilter->transponder.corresponds(channel->transponder))) {
492 atscEpgFilters.removeAt(i);
493 break;
494 }
495 }
496
497 break;
498 }
499 }
500
channelAboutToBeUpdated(const DvbSharedChannel & channel)501 void DvbEpgModel::channelAboutToBeUpdated(const DvbSharedChannel &channel)
502 {
503 updatingChannel = *channel;
504 }
505
channelUpdated(const DvbSharedChannel & channel)506 void DvbEpgModel::channelUpdated(const DvbSharedChannel &channel)
507 {
508 if (hasPendingOperation) {
509 qCWarning(logEpg, "Illegal recursive call");
510 return;
511 }
512
513 EnsureNoPendingOperation ensureNoPendingOperation(hasPendingOperation);
514
515 if (DvbChannelId(channel) != DvbChannelId(&updatingChannel)) {
516 DvbEpgEntry fakeEntry(channel);
517 Iterator it = entries.lowerBound(DvbEpgEntryId(&fakeEntry));
518
519 while ((ConstIterator(it) != entries.constEnd()) && ((*it)->channel == channel)) {
520 it = removeEntry(it);
521 }
522 }
523 }
524
channelRemoved(const DvbSharedChannel & channel)525 void DvbEpgModel::channelRemoved(const DvbSharedChannel &channel)
526 {
527 if (hasPendingOperation) {
528 qCWarning(logEpg, "Illegal recursive call");
529 return;
530 }
531
532 EnsureNoPendingOperation ensureNoPendingOperation(hasPendingOperation);
533 DvbEpgEntry fakeEntry(channel);
534 Iterator it = entries.lowerBound(DvbEpgEntryId(&fakeEntry));
535
536 while ((ConstIterator(it) != entries.constEnd()) && ((*it)->channel == channel)) {
537 it = removeEntry(it);
538 }
539 }
540
recordingRemoved(const DvbSharedRecording & recording)541 void DvbEpgModel::recordingRemoved(const DvbSharedRecording &recording)
542 {
543 if (hasPendingOperation) {
544 qCWarning(logEpg, "Illegal recursive call");
545 return;
546 }
547
548 EnsureNoPendingOperation ensureNoPendingOperation(hasPendingOperation);
549 DvbSharedEpgEntry entry = recordings.take(recording);
550
551 if (entry.isValid()) {
552 emit entryAboutToBeUpdated(entry);
553 const_cast<DvbEpgEntry *>(entry.constData())->recording = DvbSharedRecording();
554 emit entryUpdated(entry);
555 }
556 }
557
timerEvent(QTimerEvent * event)558 void DvbEpgModel::timerEvent(QTimerEvent *event)
559 {
560 Q_UNUSED(event)
561
562 if (hasPendingOperation) {
563 qCWarning(logEpg, "Illegal recursive call");
564 return;
565 }
566
567 EnsureNoPendingOperation ensureNoPendingOperation(hasPendingOperation);
568 currentDateTimeUtc = QDateTime::currentDateTime().toUTC();
569 Iterator it = entries.begin();
570
571 while (ConstIterator(it) != entries.constEnd()) {
572 const DvbSharedEpgEntry &entry = *it;
573
574 if (entry->begin.addSecs(QTime(0, 0, 0).secsTo(entry->duration)) > currentDateTimeUtc) {
575 ++it;
576 } else {
577 it = removeEntry(it);
578 }
579 }
580 }
581
removeEntry(Iterator it)582 DvbEpgModel::Iterator DvbEpgModel::removeEntry(Iterator it)
583 {
584 const DvbSharedEpgEntry &entry = *it;
585
586 if (entry->recording.isValid()) {
587 recordings.remove(entry->recording);
588 }
589
590 if (--epgChannels[entry->channel] == 0) {
591 epgChannels.remove(entry->channel);
592 emit epgChannelRemoved(entry->channel);
593 }
594
595 emit entryRemoved(entry);
596 return entries.erase(it);
597 }
598
DvbEpgFilter(DvbManager * manager_,DvbDevice * device_,const DvbSharedChannel & channel)599 DvbEpgFilter::DvbEpgFilter(DvbManager *manager_, DvbDevice *device_,
600 const DvbSharedChannel &channel) : device(device_)
601 {
602 manager = manager_;
603 source = channel->source;
604 transponder = channel->transponder;
605 device->addSectionFilter(0x12, this);
606 channelModel = manager->getChannelModel();
607 epgModel = manager->getEpgModel();
608 }
609
~DvbEpgFilter()610 DvbEpgFilter::~DvbEpgFilter()
611 {
612 device->removeSectionFilter(0x12, this);
613 }
614
bcdToTime(int bcd)615 QTime DvbEpgFilter::bcdToTime(int bcd)
616 {
617 return QTime(((bcd >> 20) & 0x0f) * 10 + ((bcd >> 16) & 0x0f),
618 ((bcd >> 12) & 0x0f) * 10 + ((bcd >> 8) & 0x0f),
619 ((bcd >> 4) & 0x0f) * 10 + (bcd & 0x0f));
620 }
621
622 static const QByteArray contentStr[16][16] = {
623 [0] = {},
624 [1] = {
625 /* Movie/Drama */
626 {},
627 {I18N_NOOP("Detective")},
628 {I18N_NOOP("Adventure")},
629 {I18N_NOOP("Science Fiction")},
630 {I18N_NOOP("Comedy")},
631 {I18N_NOOP("Soap")},
632 {I18N_NOOP("Romance")},
633 {I18N_NOOP("Classical")},
634 {I18N_NOOP("Adult")},
635 {I18N_NOOP("User defined")},
636 },
637 [2] = {
638 /* News/Current affairs */
639 {},
640 {I18N_NOOP("Weather")},
641 {I18N_NOOP("Magazine")},
642 {I18N_NOOP("Documentary")},
643 {I18N_NOOP("Discussion")},
644 {I18N_NOOP("User Defined")},
645 },
646 [3] = {
647 /* Show/Game show */
648 {},
649 {I18N_NOOP("Quiz")},
650 {I18N_NOOP("Variety")},
651 {I18N_NOOP("Talk")},
652 {I18N_NOOP("User Defined")},
653 },
654 [4] = {
655 /* Sports */
656 {},
657 {I18N_NOOP("Events")},
658 {I18N_NOOP("Magazine")},
659 {I18N_NOOP("Football")},
660 {I18N_NOOP("Tennis")},
661 {I18N_NOOP("Team")},
662 {I18N_NOOP("Athletics")},
663 {I18N_NOOP("Motor")},
664 {I18N_NOOP("Water")},
665 {I18N_NOOP("Winter")},
666 {I18N_NOOP("Equestrian")},
667 {I18N_NOOP("Martial")},
668 {I18N_NOOP("User Defined")},
669 },
670 [5] = {
671 /* Children's/Youth */
672 {},
673 {I18N_NOOP("Preschool")},
674 {I18N_NOOP("06 to 14")},
675 {I18N_NOOP("10 to 16")},
676 {I18N_NOOP("Educational")},
677 {I18N_NOOP("Cartoons")},
678 {I18N_NOOP("User Defined")},
679 },
680 [6] = {
681 /* Music/Ballet/Dance */
682 {},
683 {I18N_NOOP("Poprock")},
684 {I18N_NOOP("Classical")},
685 {I18N_NOOP("Folk")},
686 {I18N_NOOP("Jazz")},
687 {I18N_NOOP("Opera")},
688 {I18N_NOOP("Ballet")},
689 {I18N_NOOP("User Defined")},
690 },
691 [7] = {
692 /* Arts/Culture */
693 {},
694 {I18N_NOOP("Performance")},
695 {I18N_NOOP("Fine Arts")},
696 {I18N_NOOP("Religion")},
697 {I18N_NOOP("Traditional")},
698 {I18N_NOOP("Literature")},
699 {I18N_NOOP("Cinema")},
700 {I18N_NOOP("Experimental")},
701 {I18N_NOOP("Press")},
702 {I18N_NOOP("New Media")},
703 {I18N_NOOP("Magazine")},
704 {I18N_NOOP("Fashion")},
705 {I18N_NOOP("User Defined")},
706 },
707 [8] = {
708 /* Social/Political/Economics */
709 {},
710 {I18N_NOOP("Magazine")},
711 {I18N_NOOP("Advisory")},
712 {I18N_NOOP("People")},
713 {I18N_NOOP("User Defined")},
714 },
715 [9] = {
716 /* Education/Science/Factual */
717 {},
718 {I18N_NOOP("Nature")},
719 {I18N_NOOP("Technology")},
720 {I18N_NOOP("Medicine")},
721 {I18N_NOOP("Foreign")},
722 {I18N_NOOP("Social")},
723 {I18N_NOOP("Further")},
724 {I18N_NOOP("Language")},
725 {I18N_NOOP("User Defined")},
726 },
727 [10] = {
728 /* Leisure/Hobbies */
729 {},
730 {I18N_NOOP("Travel")},
731 {I18N_NOOP("Handicraft")},
732 {I18N_NOOP("Motoring")},
733 {I18N_NOOP("Fitness")},
734 {I18N_NOOP("Cooking")},
735 {I18N_NOOP("Shopping")},
736 {I18N_NOOP("Gardening")},
737 {I18N_NOOP("User Defined")},
738 },
739 [11] = {
740 /* Special characteristics */
741 {I18N_NOOP("Original Language")},
742 {I18N_NOOP("Black and White ")},
743 {I18N_NOOP("Unpublished")},
744 {I18N_NOOP("Live")},
745 {I18N_NOOP("Planostereoscopic")},
746 {I18N_NOOP("User Defined")},
747 {I18N_NOOP("User Defined 1")},
748 {I18N_NOOP("User Defined 2")},
749 {I18N_NOOP("User Defined 3")},
750 {I18N_NOOP("User Defined 4")}
751 }
752 };
753
754 static const QByteArray nibble1Str[16] = {
755 [0] = {I18N_NOOP("Undefined")},
756 [1] = {I18N_NOOP("Movie")},
757 [2] = {I18N_NOOP("News")},
758 [3] = {I18N_NOOP("Show")},
759 [4] = {I18N_NOOP("Sports")},
760 [5] = {I18N_NOOP("Children")},
761 [6] = {I18N_NOOP("Music")},
762 [7] = {I18N_NOOP("Culture")},
763 [8] = {I18N_NOOP("Social")},
764 [9] = {I18N_NOOP("Education")},
765 [10] = {I18N_NOOP("Leisure")},
766 [11] = {I18N_NOOP("Special")},
767 [12] = {I18N_NOOP("Reserved")},
768 [13] = {I18N_NOOP("Reserved")},
769 [14] = {I18N_NOOP("Reserved")},
770 [15] = {I18N_NOOP("User defined")},
771 };
772
773 static const QByteArray braNibble1Str[16] = {
774 [0] = {I18N_NOOP("News")},
775 [1] = {I18N_NOOP("Sports")},
776 [2] = {I18N_NOOP("Education")},
777 [3] = {I18N_NOOP("Soap opera")},
778 [4] = {I18N_NOOP("Mini-series")},
779 [5] = {I18N_NOOP("Series")},
780 [6] = {I18N_NOOP("Variety")},
781 [7] = {I18N_NOOP("Reality show")},
782 [8] = {I18N_NOOP("Information")},
783 [9] = {I18N_NOOP("Comical")},
784 [10] = {I18N_NOOP("Children")},
785 [11] = {I18N_NOOP("Erotic")},
786 [12] = {I18N_NOOP("Movie")},
787 [13] = {I18N_NOOP("Raffle, television sales, prizing")},
788 [14] = {I18N_NOOP("Debate/interview")},
789 [15] = {I18N_NOOP("Other")},
790 };
791
792 // Using the terms from the English version of NBR 15603-2:2007
793 // The table omits nibble2="Other", as it is better to show nibble 1
794 // definition instead.
795 // when nibble2[x][0] == nibble1[x] and it has no other definition,
796 // except for "Other", the field will be kept in blank, as the logic
797 // will fall back to the definition at nibble 1.
798 static QByteArray braNibble2Str[16][16] = {
799 [0] = {
800 {I18N_NOOP("News")},
801 {I18N_NOOP("Report")},
802 {I18N_NOOP("Documentary")},
803 {I18N_NOOP("Biography")},
804 },
805 [1] = {},
806 [2] = {
807 {I18N_NOOP("Educative")},
808 },
809 [3] = {},
810 [4] = {},
811 [5] = {},
812 [6] = {
813 {I18N_NOOP("Auditorium")},
814 {I18N_NOOP("Show")},
815 {I18N_NOOP("Musical")},
816 {I18N_NOOP("Making of")},
817 {I18N_NOOP("Feminine")},
818 {I18N_NOOP("Game show")},
819 },
820 [7] = {},
821 [8] = {
822 {I18N_NOOP("Cooking")},
823 {I18N_NOOP("Fashion")},
824 {I18N_NOOP("Country")},
825 {I18N_NOOP("Health")},
826 {I18N_NOOP("Travel")},
827 },
828 [9] = {},
829 [10] = {},
830 [11] = {},
831 [12] = {},
832 [13] = {
833 {I18N_NOOP("Raffle")},
834 {I18N_NOOP("Television sales")},
835 {I18N_NOOP("Prizing")},
836 },
837 [14] = {
838 {I18N_NOOP("Discussion")},
839 {I18N_NOOP("Interview")},
840 },
841 [15] = {
842 {I18N_NOOP("Adult cartoon")},
843 {I18N_NOOP("Interactive")},
844 {I18N_NOOP("Policy")},
845 {I18N_NOOP("Religion")},
846 },
847 };
848
getContent(DvbContentDescriptor & descriptor)849 QString DvbEpgFilter::getContent(DvbContentDescriptor &descriptor)
850 {
851 QString content;
852
853 for (DvbEitContentEntry entry = descriptor.contents(); entry.isValid(); entry.advance()) {
854 const int nibble1 = entry.contentNibbleLevel1();
855 const int nibble2 = entry.contentNibbleLevel2();
856 QByteArray s;
857
858 // FIXME: should do it only for ISDB-Tb (Brazilian variation),
859 // as the Japanese variation uses the same codes as DVB
860 if (transponder.getTransmissionType() == DvbTransponderBase::IsdbT) {
861 s = braNibble2Str[nibble1][nibble2];
862 if (s == "")
863 s = braNibble1Str[nibble1];
864 if (s != "")
865 content += i18n(s) + '\n';
866 } else {
867 s = contentStr[nibble1][nibble2];
868 if (s == "")
869 s = nibble1Str[nibble1];
870 if (s != "")
871 content += i18n(s) + '\n';
872 }
873 }
874
875 if (content != "") {
876 // xgettext:no-c-format
877 return (i18n("Genre: %1", content));
878 }
879 return content;
880 }
881
882 /* As defined at ABNT NBR 15603-2 */
883 static const QByteArray braRating[] = {
884 [0] = {I18N_NOOP("reserved")},
885 [1] = {I18N_NOOP("all audiences")},
886 [2] = {I18N_NOOP("10 years")},
887 [3] = {I18N_NOOP("12 years")},
888 [4] = {I18N_NOOP("14 years")},
889 [5] = {I18N_NOOP("16 years")},
890 [6] = {I18N_NOOP("18 years")},
891 };
892
893 #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
894
getParental(DvbParentalRatingDescriptor & descriptor)895 QString DvbEpgFilter::getParental(DvbParentalRatingDescriptor &descriptor)
896 {
897 QString parental;
898
899 for (DvbParentalRatingEntry entry = descriptor.contents(); entry.isValid(); entry.advance()) {
900 QString code;
901 code.append(QChar(entry.languageCode1()));
902 code.append(QChar(entry.languageCode2()));
903 code.append(QChar(entry.languageCode3()));
904
905 QString country;
906 IsoCodes::getCountry(code, &country);
907 if (country.isEmpty())
908 country = code;
909
910 // Rating from 0x10 to 0xff are broadcaster's specific
911 if (entry.rating() == 0) {
912 // xgettext:no-c-format
913 parental += i18n("Country %1: not rated\n", country);
914 } else if (entry.rating() < 0x10) {
915 if (code == "BRA" && transponder.getTransmissionType() == DvbTransponderBase::IsdbT) {
916 unsigned int rating = entry.rating();
917
918 if (rating >= ARRAY_SIZE(braRating))
919 rating = 0; // Reserved
920
921 QString GenStr;
922 int genre = entry.rating() >> 4;
923
924 if (genre & 0x2)
925 GenStr = i18n("violence / ");
926 if (genre & 0x4)
927 GenStr = i18n("sex / ");
928 if (genre & 0x1)
929 GenStr = i18n("drugs / ");
930 if (genre) {
931 GenStr.truncate(GenStr.size() - 2);
932 GenStr = " (" + GenStr + ')';
933 }
934
935 QString ratingStr = i18n(braRating[entry.rating()]);
936 // xgettext:no-c-format
937 parental += i18n("Country %1: rating: %2%3\n", country, ratingStr, GenStr);
938 } else {
939 // xgettext:no-c-format
940 parental += i18n("Country %1: rating: %2 years.\n", country, entry.rating() + 3);
941 }
942 }
943 }
944 return parental;
945 }
946
getLangEntry(DvbEpgEntry & epgEntry,int code1,int code2,int code3,bool add_code,QString * code_)947 DvbEpgLangEntry *DvbEpgFilter::getLangEntry(DvbEpgEntry &epgEntry,
948 int code1, int code2, int code3,
949 bool add_code,
950 QString *code_)
951 {
952 DvbEpgLangEntry *langEntry;
953 QString code;
954
955 if (!code1 || code1 == 0x20)
956 code = FIRST_LANG;
957 else {
958 code.append(QChar(code1));
959 code.append(QChar(code2));
960 code.append(QChar(code3));
961 code = code.toUpper();
962 }
963 if (code_)
964 code_ = new QString(code);
965
966 if (!epgEntry.langEntry.contains(code)) {
967 DvbEpgLangEntry e;
968 epgEntry.langEntry.insert(code, e);
969 if (add_code) {
970 if (!manager->languageCodes.contains(code)) {
971 manager->languageCodes[code] = true;
972 emit epgModel->languageAdded(code);
973 }
974 }
975 }
976 langEntry = &epgEntry.langEntry[code];
977
978 return langEntry;
979 }
980
981
processSection(const char * data,int size)982 void DvbEpgFilter::processSection(const char *data, int size)
983 {
984 unsigned char tableId = data[0];
985
986 if ((tableId < 0x4e) || (tableId > 0x6f)) {
987 return;
988 }
989
990 DvbEitSection eitSection(data, size);
991
992 if (!eitSection.isValid()) {
993 qCDebug(logEpg, "section is invalid");
994 return;
995 }
996
997 DvbChannel fakeChannel;
998 fakeChannel.source = source;
999 fakeChannel.transponder = transponder;
1000 fakeChannel.networkId = eitSection.originalNetworkId();
1001 fakeChannel.transportStreamId = eitSection.transportStreamId();
1002 fakeChannel.serviceId = eitSection.serviceId();
1003 DvbSharedChannel channel = channelModel->findChannelById(fakeChannel);
1004
1005 if (!channel.isValid()) {
1006 fakeChannel.networkId = -1;
1007 channel = channelModel->findChannelById(fakeChannel);
1008 }
1009
1010 if (!channel.isValid()) {
1011 qCDebug(logEpg, "channel invalid");
1012 return;
1013 }
1014
1015 if (eitSection.entries().getLength())
1016 qCDebug(logEpg, "table 0x%02x, extension 0x%04x, session %d/%d, size %d", eitSection.tableId(), eitSection.tableIdExtension(), eitSection.sectionNumber(), eitSection.lastSectionNumber(), eitSection.entries().getLength());
1017
1018 for (DvbEitSectionEntry entry = eitSection.entries(); entry.isValid(); entry.advance()) {
1019 DvbEpgEntry epgEntry;
1020 DvbEpgLangEntry *langEntry;
1021
1022 if (tableId == 0x4e)
1023 epgEntry.type = DvbEpgEntry::EitActualTsPresentFollowing;
1024 else if (tableId == 0x4f)
1025 epgEntry.type = DvbEpgEntry::EitOtherTsPresentFollowing;
1026 else if (tableId < 0x60)
1027 epgEntry.type = DvbEpgEntry::EitActualTsSchedule;
1028 else
1029 epgEntry.type = DvbEpgEntry::EitOtherTsSchedule;
1030
1031 epgEntry.channel = channel;
1032
1033 /*
1034 * ISDB-T Brazil uses time in UTC-3,
1035 * as defined by ABNT NBR 15603-2:2007.
1036 */
1037 if (channel->transponder.getTransmissionType() == DvbTransponderBase::IsdbT)
1038 epgEntry.begin = QDateTime(QDate::fromJulianDay(entry.startDate() + 2400001),
1039 bcdToTime(entry.startTime()), Qt::OffsetFromUTC, -10800).toUTC();
1040 else
1041 epgEntry.begin = QDateTime(QDate::fromJulianDay(entry.startDate() + 2400001),
1042 bcdToTime(entry.startTime()), Qt::UTC);
1043 epgEntry.duration = bcdToTime(entry.duration());
1044
1045 for (DvbDescriptor descriptor = entry.descriptors(); descriptor.isValid();
1046 descriptor.advance()) {
1047 switch (descriptor.descriptorTag()) {
1048 case 0x4d: {
1049 DvbShortEventDescriptor eventDescriptor(descriptor);
1050
1051 if (!eventDescriptor.isValid()) {
1052 break;
1053 }
1054
1055 langEntry = getLangEntry(epgEntry,
1056 eventDescriptor.languageCode1(),
1057 eventDescriptor.languageCode2(),
1058 eventDescriptor.languageCode3());
1059
1060 langEntry->title += eventDescriptor.eventName();
1061 langEntry->subheading += eventDescriptor.text();
1062
1063 break;
1064 }
1065 case 0x4e: {
1066 DvbExtendedEventDescriptor eventDescriptor(descriptor);
1067
1068 if (!eventDescriptor.isValid()) {
1069 break;
1070 }
1071
1072 langEntry = getLangEntry(epgEntry,
1073 eventDescriptor.languageCode1(),
1074 eventDescriptor.languageCode2(),
1075 eventDescriptor.languageCode3());
1076 langEntry->details += eventDescriptor.text();
1077 break;
1078 }
1079 case 0x54: {
1080 DvbContentDescriptor eventDescriptor(descriptor);
1081
1082 if (!eventDescriptor.isValid()) {
1083 break;
1084 }
1085
1086 epgEntry.content += getContent(eventDescriptor);
1087 break;
1088 }
1089 case 0x55: {
1090 DvbParentalRatingDescriptor eventDescriptor(descriptor);
1091
1092 if (!eventDescriptor.isValid()) {
1093 break;
1094 }
1095
1096 epgEntry.parental += getParental(eventDescriptor);
1097 break;
1098 }
1099 }
1100 }
1101
1102 epgModel->addEntry(epgEntry);
1103 }
1104 }
1105
processSection(const char * data,int size)1106 void AtscEpgMgtFilter::processSection(const char *data, int size)
1107 {
1108 epgFilter->processMgtSection(data, size);
1109 }
1110
processSection(const char * data,int size)1111 void AtscEpgEitFilter::processSection(const char *data, int size)
1112 {
1113 epgFilter->processEitSection(data, size);
1114 }
1115
processSection(const char * data,int size)1116 void AtscEpgEttFilter::processSection(const char *data, int size)
1117 {
1118 epgFilter->processEttSection(data, size);
1119 }
1120
AtscEpgFilter(DvbManager * manager,DvbDevice * device_,const DvbSharedChannel & channel)1121 AtscEpgFilter::AtscEpgFilter(DvbManager *manager, DvbDevice *device_,
1122 const DvbSharedChannel &channel) : device(device_), mgtFilter(this), eitFilter(this),
1123 ettFilter(this)
1124 {
1125 source = channel->source;
1126 transponder = channel->transponder;
1127 device->addSectionFilter(0x1ffb, &mgtFilter);
1128 channelModel = manager->getChannelModel();
1129 epgModel = manager->getEpgModel();
1130 }
1131
~AtscEpgFilter()1132 AtscEpgFilter::~AtscEpgFilter()
1133 {
1134 foreach (int pid, eitPids) {
1135 device->removeSectionFilter(pid, &eitFilter);
1136 }
1137
1138 foreach (int pid, ettPids) {
1139 device->removeSectionFilter(pid, &ettFilter);
1140 }
1141
1142 device->removeSectionFilter(0x1ffb, &mgtFilter);
1143 }
1144
processMgtSection(const char * data,int size)1145 void AtscEpgFilter::processMgtSection(const char *data, int size)
1146 {
1147 unsigned char tableId = data[0];
1148
1149 if (tableId != 0xc7) {
1150 return;
1151 }
1152
1153 AtscMgtSection mgtSection(data, size);
1154
1155 if (!mgtSection.isValid()) {
1156 return;
1157 }
1158
1159 int entryCount = mgtSection.entryCount();
1160 QList<int> newEitPids;
1161 QList<int> newEttPids;
1162
1163 AtscMgtSectionEntry entry = mgtSection.entries();
1164 for (int i = 0; i < entryCount; i++) {
1165 if (!entry.isValid())
1166 break;
1167
1168 int tableType = entry.tableType();
1169
1170 if ((tableType >= 0x0100) && (tableType <= 0x017f)) {
1171 int pid = entry.pid();
1172 int index = (qLowerBound(newEitPids, pid) - newEitPids.constBegin());
1173
1174 if ((index >= newEitPids.size()) || (newEitPids.at(index) != pid)) {
1175 newEitPids.insert(index, pid);
1176 }
1177 }
1178
1179 if ((tableType >= 0x0200) && (tableType <= 0x027f)) {
1180 int pid = entry.pid();
1181 int index = (qLowerBound(newEttPids, pid) - newEttPids.constBegin());
1182
1183 if ((index >= newEttPids.size()) || (newEttPids.at(index) != pid)) {
1184 newEttPids.insert(index, pid);
1185 }
1186 }
1187 if (i < entryCount - 1)
1188 entry.advance();
1189 }
1190
1191 for (int i = 0; i < eitPids.size(); ++i) {
1192 int pid = eitPids.at(i);
1193 int index = (qBinaryFind(newEitPids, pid) - newEitPids.constBegin());
1194
1195 if (index < newEitPids.size()) {
1196 newEitPids.removeAt(index);
1197 } else {
1198 device->removeSectionFilter(pid, &eitFilter);
1199 eitPids.removeAt(i);
1200 --i;
1201 }
1202 }
1203
1204 for (int i = 0; i < ettPids.size(); ++i) {
1205 int pid = ettPids.at(i);
1206 int index = (qBinaryFind(newEttPids, pid) - newEttPids.constBegin());
1207
1208 if (index < newEttPids.size()) {
1209 newEttPids.removeAt(index);
1210 } else {
1211 device->removeSectionFilter(pid, &ettFilter);
1212 ettPids.removeAt(i);
1213 --i;
1214 }
1215 }
1216
1217 for (int i = 0; i < newEitPids.size(); ++i) {
1218 int pid = newEitPids.at(i);
1219 eitPids.append(pid);
1220 device->addSectionFilter(pid, &eitFilter);
1221 }
1222
1223 for (int i = 0; i < newEttPids.size(); ++i) {
1224 int pid = newEttPids.at(i);
1225 ettPids.append(pid);
1226 device->addSectionFilter(pid, &ettFilter);
1227 }
1228 }
1229
processEitSection(const char * data,int size)1230 void AtscEpgFilter::processEitSection(const char *data, int size)
1231 {
1232 unsigned char tableId = data[0];
1233
1234 if (tableId != 0xcb) {
1235 return;
1236 }
1237
1238 AtscEitSection eitSection(data, size);
1239
1240 if (!eitSection.isValid()) {
1241 qCDebug(logEpg, "section is invalid");
1242 return;
1243 }
1244
1245 DvbChannel fakeChannel;
1246 fakeChannel.source = source;
1247 fakeChannel.transponder = transponder;
1248 fakeChannel.networkId = eitSection.sourceId();
1249 DvbSharedChannel channel = channelModel->findChannelById(fakeChannel);
1250
1251 if (!channel.isValid()) {
1252 qCDebug(logEpg, "channel is invalid");
1253 return;
1254 }
1255
1256 qCDebug(logEpg, "Processing EIT section with size %d", size);
1257
1258 int entryCount = eitSection.entryCount();
1259 // 1980-01-06T000000 minus 15 secs (= UTC - GPS in 2011)
1260 QDateTime baseDateTime = QDateTime(QDate(1980, 1, 5), QTime(23, 59, 45), Qt::UTC);
1261
1262 AtscEitSectionEntry eitEntry = eitSection.entries();
1263 for (int i = 0; i < entryCount; i++) {
1264 if (!eitEntry.isValid())
1265 break;
1266 DvbEpgEntry epgEntry;
1267 epgEntry.channel = channel;
1268 epgEntry.begin = baseDateTime.addSecs(eitEntry.startTime());
1269 epgEntry.duration = QTime(0, 0, 0).addSecs(eitEntry.duration());
1270
1271
1272 DvbEpgLangEntry *langEntry;
1273
1274 /* Should be similar to DvbEpgFilter::getLangEntry */
1275 if (!epgEntry.langEntry.contains(FIRST_LANG)) {
1276 DvbEpgLangEntry e;
1277 epgEntry.langEntry.insert(FIRST_LANG, e);
1278 }
1279 langEntry = &epgEntry.langEntry[FIRST_LANG];
1280
1281 langEntry->title = eitEntry.title();
1282
1283 quint32 id = ((quint32(fakeChannel.networkId) << 16) | quint32(eitEntry.eventId()));
1284 DvbSharedEpgEntry entry = epgEntries.value(id);
1285
1286 entry = epgModel->addEntry(epgEntry);
1287 epgEntries.insert(id, entry);
1288 if ( i < entryCount -1)
1289 eitEntry.advance();
1290 }
1291 }
1292
processEttSection(const char * data,int size)1293 void AtscEpgFilter::processEttSection(const char *data, int size)
1294 {
1295 unsigned char tableId = data[0];
1296
1297 if (tableId != 0xcc) {
1298 return;
1299 }
1300
1301 AtscEttSection ettSection(data, size);
1302
1303 if (!ettSection.isValid() || (ettSection.messageType() != 0x02)) {
1304 return;
1305 }
1306
1307 quint32 id = ((quint32(ettSection.sourceId()) << 16) | quint32(ettSection.eventId()));
1308 DvbSharedEpgEntry entry = epgEntries.value(id);
1309
1310 if (entry.isValid()) {
1311 QString details = ettSection.text();
1312
1313 if (entry->details() != details) {
1314 DvbEpgEntry modifiedEntry = *entry;
1315
1316 DvbEpgLangEntry *langEntry;
1317
1318 if (modifiedEntry.langEntry.contains(FIRST_LANG))
1319 langEntry = &modifiedEntry.langEntry[FIRST_LANG];
1320 else
1321 langEntry = new(DvbEpgLangEntry);
1322
1323 langEntry->details = details;
1324 entry = epgModel->addEntry(modifiedEntry);
1325 epgEntries.insert(id, entry);
1326 }
1327 }
1328 }
1329