Ticket #13057: programdata.cpp.master.kdw

File programdata.cpp.master.kdw, 51.6 KB (added by klaas@…, 3 years ago)

libs/libmythtv/programdata.cpp with fix

Line 
1// -*- Mode: c++ -*-
2
3#include <limits.h>
4
5// C++ includes
6#include <algorithm>
7using namespace std;
8
9// Qt includes
10#include <QtCore> // for qAbs
11
12// MythTV headers
13#include "programdata.h"
14#include "channelutil.h"
15#include "mythdb.h"
16#include "mythlogging.h"
17#include "dvbdescriptors.h"
18
19#define LOC      QString("ProgramData: ")
20
21static const char *roles[] =
22{
23    "",
24    "actor",     "director",    "producer", "executive_producer",
25    "writer",    "guest_star",  "host",     "adapter",
26    "presenter", "commentator", "guest",
27};
28
29static QString denullify(const QString &str)
30{
31    return str.isNull() ? "" : str;
32}
33
34static QVariant denullify(const QDateTime &dt)
35{
36    return dt.isNull() ? QVariant("0000-00-00 00:00:00") : QVariant(dt);
37}
38
39static void add_genres(MSqlQuery &query, const QStringList &genres,
40                uint chanid, const QDateTime &starttime)
41{
42    QString relevance = QStringLiteral("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
43    QStringList::const_iterator it = genres.constBegin();
44    for (; (it != genres.end()) &&
45             ((it - genres.constBegin()) < relevance.size()); ++it)
46    {
47        query.prepare(
48           "INSERT INTO programgenres "
49           "       ( chanid,  starttime, genre,  relevance) "
50           "VALUES (:CHANID, :START,    :genre, :relevance)");
51        query.bindValue(":CHANID",    chanid);
52        query.bindValue(":START",     starttime);
53        query.bindValue(":genre",     *it);
54        query.bindValue(":relevance", relevance.at(it - genres.constBegin()));
55
56        if (!query.exec())
57            MythDB::DBError("programgenres insert", query);
58    }
59}
60
61DBPerson::DBPerson(const DBPerson &other) :
62    role(other.role), name(other.name)
63{
64    name.squeeze();
65}
66
67DBPerson::DBPerson(Role _role, const QString &_name) :
68    role(_role), name(_name)
69{
70    name.squeeze();
71}
72
73DBPerson::DBPerson(const QString &_role, const QString &_name) :
74    role(kUnknown), name(_name)
75{
76    if (!_role.isEmpty())
77    {
78        for (uint i = 0; i < sizeof(roles) / sizeof(char *); i++)
79        {
80            if (_role == QString(roles[i]))
81                role = (Role) i;
82        }
83    }
84    name.squeeze();
85}
86
87QString DBPerson::GetRole(void) const
88{
89    if ((role < kActor) || (role > kGuest))
90        return "guest";
91    return roles[role];
92}
93
94uint DBPerson::InsertDB(MSqlQuery &query, uint chanid,
95                        const QDateTime &starttime) const
96{
97    uint personid = GetPersonDB(query);
98    if (!personid && InsertPersonDB(query))
99        personid = GetPersonDB(query);
100
101    return InsertCreditsDB(query, personid, chanid, starttime);
102}
103
104uint DBPerson::GetPersonDB(MSqlQuery &query) const
105{
106    query.prepare(
107        "SELECT person "
108        "FROM people "
109        "WHERE name = :NAME");
110    query.bindValue(":NAME", name);
111
112    if (!query.exec())
113        MythDB::DBError("get_person", query);
114    else if (query.next())
115        return query.value(0).toUInt();
116
117    return 0;
118}
119
120uint DBPerson::InsertPersonDB(MSqlQuery &query) const
121{
122    query.prepare(
123        "INSERT IGNORE INTO people (name) "
124        "VALUES (:NAME);");
125    query.bindValue(":NAME", name);
126
127    if (query.exec())
128        return 1;
129
130    MythDB::DBError("insert_person", query);
131    return 0;
132}
133
134uint DBPerson::InsertCreditsDB(MSqlQuery &query, uint personid, uint chanid,
135                               const QDateTime &starttime) const
136{
137    if (!personid)
138        return 0;
139
140    query.prepare(
141        "REPLACE INTO credits "
142        "       ( person,  chanid,  starttime,  role) "
143        "VALUES (:PERSON, :CHANID, :STARTTIME, :ROLE) ");
144    query.bindValue(":PERSON",    personid);
145    query.bindValue(":CHANID",    chanid);
146    query.bindValue(":STARTTIME", starttime);
147    query.bindValue(":ROLE",      GetRole());
148
149    if (query.exec())
150        return 1;
151
152    MythDB::DBError("insert_credits", query);
153    return 0;
154}
155
156DBEvent &DBEvent::operator=(const DBEvent &other)
157{
158    if (this == &other)
159        return *this;
160
161    title           = other.title;
162    subtitle        = other.subtitle;
163    description     = other.description;
164    category        = other.category;
165    starttime       = other.starttime;
166    endtime         = other.endtime;
167    airdate         = other.airdate;
168    originalairdate = other.originalairdate;
169
170    if (credits != other.credits)
171    {
172        if (credits)
173        {
174            delete credits;
175            credits = NULL;
176        }
177
178        if (other.credits)
179        {
180            credits = new DBCredits;
181            credits->insert(credits->end(),
182                            other.credits->begin(),
183                            other.credits->end());
184        }
185    }
186
187    partnumber      = other.partnumber;
188    parttotal       = other.parttotal;
189    syndicatedepisodenumber = other.syndicatedepisodenumber;
190    subtitleType    = other.subtitleType;
191    audioProps      = other.audioProps;
192    videoProps      = other.videoProps;
193    stars           = other.stars;
194    categoryType    = other.categoryType;
195    seriesId        = other.seriesId;
196    programId       = other.programId;
197    inetref         = other.inetref;
198    previouslyshown = other.previouslyshown;
199    ratings         = other.ratings;
200    listingsource   = other.listingsource;
201    season          = other.season;
202    episode         = other.episode;
203    totalepisodes   = other.totalepisodes;
204    genres          = other.genres;
205
206    Squeeze();
207
208    return *this;
209}
210
211void DBEvent::Squeeze(void)
212{
213    title.squeeze();
214    subtitle.squeeze();
215    description.squeeze();
216    category.squeeze();
217    syndicatedepisodenumber.squeeze();
218    seriesId.squeeze();
219    programId.squeeze();
220    inetref.squeeze();
221}
222
223void DBEvent::AddPerson(DBPerson::Role role, const QString &name)
224{
225    if (!credits)
226        credits = new DBCredits;
227
228    credits->push_back(DBPerson(role, name.simplified()));
229}
230
231void DBEvent::AddPerson(const QString &role, const QString &name)
232{
233    if (!credits)
234        credits = new DBCredits;
235
236    credits->push_back(DBPerson(role, name.simplified()));
237}
238
239bool DBEvent::HasTimeConflict(const DBEvent &o) const
240{
241    return ((starttime <= o.starttime && o.starttime < endtime) ||
242            (o.endtime <= endtime     && starttime   < o.endtime));
243}
244
245// Processing new EIT entry starts here
246uint DBEvent::UpdateDB(
247    MSqlQuery &query, uint chanid, int match_threshold) const
248{
249    // List the program that we are going to add
250    LOG(VB_EIT, LOG_DEBUG,
251        QString("EIT: new program: %1 %2 '%3' chanid %4")
252                .arg(starttime.toString(Qt::ISODate))
253                .arg(endtime.toString(Qt::ISODate))
254                .arg(title.left(35))
255                .arg(chanid));
256
257    // Do not insert or update when the program is in the past
258    QDateTime now = QDateTime::currentDateTimeUtc();
259    if (endtime < now)
260    {
261        LOG(VB_EIT, LOG_DEBUG,
262            QString("EIT: skip '%1' endtime is in the past")
263                    .arg(title.left(35)));
264        return 0;
265    }
266
267    // Get all programs already in the database that overlap
268    // with our new program.
269    vector<DBEvent> programs;
270    uint count = GetOverlappingPrograms(query, chanid, programs);
271    int  match = INT_MIN;
272    int  i     = -1;
273
274    // If there are no programs already in the database that overlap
275    // with our new program then we can simply insert it in the database.
276    if (!count)
277        return InsertDB(query, chanid);
278
279    // List all overlapping programs with start- and endtime.
280    for (uint j=0; j<count; j++)
281    {
282        LOG(VB_EIT, LOG_DEBUG,
283            QString("EIT: overlap[%1] : %2 %3 '%4'")
284                .arg(j)
285                .arg(programs[j].starttime.toString(Qt::ISODate))
286                .arg(programs[j].endtime.toString(Qt::ISODate))
287                .arg(programs[j].title.left(35)));
288    }
289
290    // Determine which of the overlapping programs is a match with
291    // our new program; if we have a match then our new program is considered
292    // to be an update of the matching program.
293    // The 2nd parameter "i" is the index of the best matching program.
294    match = GetMatch(programs, i);
295
296    // Update an existing program or insert a new program.
297    if (match >= match_threshold)
298    {
299        // We have a good match; update program[i] in the database
300        // with the new program data and move the overlapping programs
301        // out of the way.
302        LOG(VB_EIT, LOG_DEBUG,
303            QString("EIT: accept match[%1]: %2 '%3' vs. '%4'")
304                .arg(i).arg(match).arg(title.left(35))
305                .arg(programs[i].title.left(35)));
306        return UpdateDB(query, chanid, programs, i);
307    }
308    else
309    {
310        // If we are here then either we have a match but the match is
311        // not good enough (the "i >= 0" case) or we did not find
312        // a match at all.
313        if (i >= 0)
314        {
315            LOG(VB_EIT, LOG_DEBUG,
316                QString("EIT: reject match[%1]: %2 '%3' vs. '%4'")
317                    .arg(i).arg(match).arg(title.left(35))
318                    .arg(programs[i].title.left(35)));
319        }
320
321        // Move the overlapping programs out of the way and
322        // insert the new program.
323        return UpdateDB(query, chanid, programs, -1);
324    }
325}
326
327// Get all programs in the database that overlap with our new program.
328// We check for three ways in which we can have an overlap:
329// (1)   Start of old program is inside our new program:
330//           old program starts at or after our program AND
331//           old program starts before end of our program;
332//       e.g. new program  s-------------e
333//            old program      s-------------e
334//         or old program      s-----e
335//       This is the STIME1/ETIME1 comparison.
336// (2)   End of old program is inside our new program:
337//           old program ends after our program starts AND
338//           old program ends before end of our program
339//       e.g. new program      s-------------e
340//            old program  s-------------e
341//         or old program          s-----e
342//       This is the STIME2/ETIME2 comparison.
343// (3)   We can have a new program is "inside" the old program:
344//           old program starts before our program AND
345//          old program ends after end of our program
346//       e.g. new program      s---------e
347//            old program  s-----------------e
348//       This is the STIME3/ETIME3 comparison.
349//
350uint DBEvent::GetOverlappingPrograms(
351    MSqlQuery &query, uint chanid, vector<DBEvent> &programs) const
352{
353    uint count = 0;
354    query.prepare(
355        "SELECT title,          subtitle,      description, "
356        "       category,       category_type, "
357        "       starttime,      endtime, "
358        "       subtitletypes+0,audioprop+0,   videoprop+0, "
359        "       seriesid,       programid, "
360        "       partnumber,     parttotal, "
361        "       syndicatedepisodenumber, "
362        "       airdate,        originalairdate, "
363        "       previouslyshown,listingsource, "
364        "       stars+0, "
365        "       season,         episode,       totalepisodes, "
366        "       inetref "
367        "FROM program "
368        "WHERE chanid   = :CHANID AND "
369        "      manualid = 0       AND "
370        "      ( ( starttime >= :STIME1 AND starttime <  :ETIME1 ) OR "
371        "        ( endtime   >  :STIME2 AND endtime   <= :ETIME2 ) OR "
372        "        ( starttime <  :STIME3 AND endtime   >  :ETIME3 ) )");
373    query.bindValue(":CHANID", chanid);
374    query.bindValue(":STIME1", starttime);
375    query.bindValue(":ETIME1", endtime);
376    query.bindValue(":STIME2", starttime);
377    query.bindValue(":ETIME2", endtime);
378    query.bindValue(":STIME3", starttime);
379    query.bindValue(":ETIME3", endtime);
380
381    if (!query.exec())
382    {
383        MythDB::DBError("GetOverlappingPrograms 1", query);
384        return 0;
385    }
386
387    while (query.next())
388    {
389        ProgramInfo::CategoryType category_type =
390            string_to_myth_category_type(query.value(4).toString());
391
392        DBEvent prog(
393            query.value(0).toString(),
394            query.value(1).toString(),
395            query.value(2).toString(),
396            query.value(3).toString(),
397            category_type,
398            MythDate::as_utc(query.value(5).toDateTime()),
399            MythDate::as_utc(query.value(6).toDateTime()),
400            query.value(7).toUInt(),
401            query.value(8).toUInt(),
402            query.value(9).toUInt(),
403            query.value(19).toDouble(),
404            query.value(10).toString(),
405            query.value(11).toString(),
406            query.value(18).toUInt(),
407            query.value(20).toUInt(),  // Season
408            query.value(21).toUInt(),  // Episode
409            query.value(22).toUInt()); // Total Episodes
410
411        prog.inetref    = query.value(23).toString();
412        prog.partnumber = query.value(12).toUInt();
413        prog.parttotal  = query.value(13).toUInt();
414        prog.syndicatedepisodenumber = query.value(14).toString();
415        prog.airdate    = query.value(15).toUInt();
416        prog.originalairdate  = query.value(16).toDate();
417        prog.previouslyshown  = query.value(17).toBool();
418
419        programs.push_back(prog);
420        count++;
421    }
422
423    return count;
424}
425
426
427static int score_words(const QStringList &al, const QStringList &bl)
428{
429    QStringList::const_iterator ait = al.begin();
430    QStringList::const_iterator bit = bl.begin();
431    int score = 0;
432    for (; (ait != al.end()) && (bit != bl.end()); ++ait)
433    {
434        QStringList::const_iterator bit2 = bit;
435        int dist = 0;
436        int bscore = 0;
437        for (; bit2 != bl.end(); ++bit2)
438        {
439            if (*ait == *bit)
440            {
441                bscore = max(1000, 2000 - (dist * 500));
442                // lower score for short words
443                if (ait->length() < 5)
444                    bscore /= 5 - ait->length();
445                break;
446            }
447            dist++;
448        }
449        if (bscore && dist < 3)
450        {
451            for (int i = 0; (i < dist) && bit != bl.end(); i++)
452                ++bit;
453        }
454        score += bscore;
455    }
456
457    return score / al.size();
458}
459
460static int score_match(const QString &a, const QString &b)
461{
462    if (a.isEmpty() || b.isEmpty())
463        return 0;
464    else if (a == b)
465        return 1000;
466
467    QString A = a.simplified().toUpper();
468    QString B = b.simplified().toUpper();
469    if (A == B)
470        return 1000;
471
472    QStringList al, bl;
473    al = A.split(" ", QString::SkipEmptyParts);
474    if (al.isEmpty())
475        return 0;
476
477    bl = B.split(" ", QString::SkipEmptyParts);
478    if (bl.isEmpty())
479        return 0;
480
481    // score words symmetrically
482    int score = (score_words(al, bl) + score_words(bl, al)) / 2;
483
484    return min(900, score);
485}
486
487int DBEvent::GetMatch(const vector<DBEvent> &programs, int &bestmatch) const
488{
489    bestmatch = -1;
490    int match_val = INT_MIN;
491    int overlap = 0;
492    int duration = starttime.secsTo(endtime);
493
494    for (uint i = 0; i < programs.size(); i++)
495    {
496        int mv = 0;
497        int duration_loop = programs[i].starttime.secsTo(programs[i].endtime);
498
499        mv -= qAbs(starttime.secsTo(programs[i].starttime));
500        mv -= qAbs(endtime.secsTo(programs[i].endtime));
501        mv -= qAbs(duration - duration_loop);
502        mv += score_match(title, programs[i].title) * 10;
503        mv += score_match(subtitle, programs[i].subtitle);
504        mv += score_match(description, programs[i].description);
505
506        /* determine overlap of both programs
507         * we don't know which one starts first */
508        if (starttime < programs[i].starttime)
509            overlap = programs[i].starttime.secsTo(endtime);
510        else if (starttime > programs[i].starttime)
511            overlap = starttime.secsTo(programs[i].endtime);
512        else
513        {
514            if (endtime <= programs[i].endtime)
515                overlap = starttime.secsTo(endtime);
516            else
517                overlap = starttime.secsTo(programs[i].endtime);
518        }
519
520        /* scale the score depending on the overlap length
521         * full score is preserved if the overlap is at least 1/2 of the length
522         * of the shorter program */
523        if (overlap > 0)
524        {
525            /* crappy providers apparently have events without duration
526             * ensure that the minimal duration is 2 second to avoid
527             * multiplying and more importantly dividing by zero */
528            int min_dur = max(2, min(duration, duration_loop));
529            overlap = min(overlap, min_dur/2);
530            mv *= overlap * 2;
531            mv /= min_dur;
532        }
533        else
534        {
535            LOG(VB_GENERAL, LOG_ERR,
536                QString("Unexpected result: shows don't "
537                        "overlap\n\t%1: %2 - %3\n\t%4: %5 - %6")
538                    .arg(title.left(35))
539                    .arg(starttime.toString(Qt::ISODate))
540                    .arg(endtime.toString(Qt::ISODate))
541                    .arg(programs[i].title.left(35), 35)
542                    .arg(programs[i].starttime.toString(Qt::ISODate))
543                    .arg(programs[i].endtime.toString(Qt::ISODate))
544                );
545        }
546
547        if (mv > match_val)
548        {
549            LOG(VB_EIT, LOG_DEBUG,
550                QString("GM : '%1' new best match '%2' with score %3")
551                    .arg(title.left(35))
552                    .arg(programs[i].title.left(35)).arg(mv));
553            bestmatch = i;
554            match_val = mv;
555        }
556    }
557
558    return match_val;
559}
560
561uint DBEvent::UpdateDB(
562    MSqlQuery &q, uint chanid, const vector<DBEvent> &p, int match) const
563{
564    // Adjust/delete overlaps;
565    bool ok = true;
566    for (uint i = 0; i < p.size(); i++)
567    {
568        if (i != (uint)match)
569            ok &= MoveOutOfTheWayDB(q, chanid, p[i]);
570    }
571
572    // If we failed to move programs out of the way, don't insert new ones..
573    if (!ok)
574    {
575        LOG(VB_EIT, LOG_DEBUG,
576            QString("EIT: cannot insert '%1' MoveOutOfTheWayDB failed")
577                    .arg(title.left(35)));
578        return 0;
579    }
580
581    // No match, insert current item
582    if ((match < 0) || ((uint)match >= p.size()))
583    {
584        LOG(VB_EIT, LOG_DEBUG,
585            QString("EIT: insert '%1'")
586                    .arg(title.left(35)));
587        return InsertDB(q, chanid);
588    }
589
590    // Changing a starttime of a program that is being recorded can
591    // start another recording of the same program.
592    // Therefore we skip updates that change a starttime in the past
593    // unless the endtime is later.
594    if (starttime != p[match].starttime)
595    {
596        QDateTime now = QDateTime::currentDateTimeUtc();
597        if (starttime < now && endtime <= p[match].endtime)
598        {
599            LOG(VB_EIT, LOG_DEBUG,
600                QString("EIT:  skip '%1' starttime is in the past")
601                        .arg(title.left(35)));
602            return 0;
603        }
604    }
605
606    // Update matched item with current data
607    LOG(VB_EIT, LOG_DEBUG,
608         QString("EIT: update '%1' with '%2'")
609                 .arg(p[match].title.left(35))
610                 .arg(title.left(35)));
611    return UpdateDB(q, chanid, p[match]);
612}
613
614// Update matched item with current data.
615//
616uint DBEvent::UpdateDB(
617    MSqlQuery &query, uint chanid, const DBEvent &match)  const
618{
619    QString  ltitle     = title;
620    QString  lsubtitle  = subtitle;
621    QString  ldesc      = description;
622    QString  lcategory  = category;
623    uint16_t lairdate   = airdate;
624    QString  lprogramId = programId;
625    QString  lseriesId  = seriesId;
626    QString  linetref   = inetref;
627    QDate loriginalairdate = originalairdate;
628
629    ProgramInfo::CategoryType tmp = categoryType;
630    QString lcattype = myth_category_type_to_string(tmp);
631
632    unsigned char lsubtype = subtitleType;
633    unsigned char laudio   = audioProps;
634    unsigned char lvideo   = videoProps;
635
636    uint lseason = season;
637    uint lepisode = episode;
638    uint lepisodeTotal = totalepisodes;
639
640    uint lpartnumber = partnumber;
641    uint lparttotal  = parttotal;
642
643    bool lpreviouslyshown = previouslyshown;
644
645    uint32_t llistingsource = listingsource;
646
647    QString lsyndicatedepisodenumber = syndicatedepisodenumber;
648
649    query.prepare(
650        "UPDATE program "
651        "SET title          = :TITLE,     subtitle      = :SUBTITLE, "
652        "    description    = :DESC, "
653        "    category       = :CATEGORY,  category_type = :CATTYPE, "
654        "    starttime      = :STARTTIME, endtime       = :ENDTIME, "
655        "    closecaptioned = :CC,        subtitled     = :HASSUBTITLES, "
656        "    stereo         = :STEREO,    hdtv          = :HDTV, "
657        "    subtitletypes  = :SUBTYPE, "
658        "    audioprop      = :AUDIOPROP, videoprop     = :VIDEOPROP, "
659        "    season         = :SEASON,  "
660        "    episode        = :EPISODE,   totalepisodes = :TOTALEPS, "
661        "    partnumber     = :PARTNO,    parttotal     = :PARTTOTAL, "
662        "    syndicatedepisodenumber = :SYNDICATENO, "
663        "    airdate        = :AIRDATE,   originalairdate=:ORIGAIRDATE, "
664        "    listingsource  = :LSOURCE, "
665        "    seriesid       = :SERIESID,  programid     = :PROGRAMID, "
666        "    previouslyshown = :PREVSHOWN, inetref      = :INETREF "
667        "WHERE chanid    = :CHANID AND "
668        "      starttime = :OLDSTART ");
669
670    query.bindValue(":CHANID",      chanid);
671    query.bindValue(":OLDSTART",    match.starttime);
672    query.bindValue(":TITLE",       denullify(ltitle));
673    query.bindValue(":SUBTITLE",    denullify(lsubtitle));
674    query.bindValue(":DESC",        denullify(ldesc));
675    query.bindValue(":CATEGORY",    denullify(lcategory));
676    query.bindValue(":CATTYPE",     lcattype);
677    query.bindValue(":STARTTIME",   starttime);
678    query.bindValue(":ENDTIME",     endtime);
679    query.bindValue(":CC",          (lsubtype & SUB_HARDHEAR) ? true : false);
680    query.bindValue(":HASSUBTITLES",(lsubtype & SUB_NORMAL)   ? true : false);
681    query.bindValue(":STEREO",      (laudio   & AUD_STEREO)   ? true : false);
682    query.bindValue(":HDTV",        (lvideo   & VID_HDTV)     ? true : false);
683    query.bindValue(":SUBTYPE",     lsubtype);
684    query.bindValue(":AUDIOPROP",   laudio);
685    query.bindValue(":VIDEOPROP",   lvideo);
686    query.bindValue(":SEASON",      lseason);
687    query.bindValue(":EPISODE",     lepisode);
688    query.bindValue(":TOTALEPS",    lepisodeTotal);
689    query.bindValue(":PARTNO",      lpartnumber);
690    query.bindValue(":PARTTOTAL",   lparttotal);
691    query.bindValue(":SYNDICATENO", denullify(lsyndicatedepisodenumber));
692    query.bindValue(":AIRDATE",     lairdate ? QString::number(lairdate) : "0000");
693    query.bindValue(":ORIGAIRDATE", loriginalairdate);
694    query.bindValue(":LSOURCE",     llistingsource);
695    query.bindValue(":SERIESID",    denullify(lseriesId));
696    query.bindValue(":PROGRAMID",   denullify(lprogramId));
697    query.bindValue(":PREVSHOWN",   lpreviouslyshown);
698    query.bindValue(":INETREF",     linetref);
699
700    if (!query.exec())
701    {
702        MythDB::DBError("InsertDB", query);
703        return 0;
704    }
705
706    if (credits)
707    {
708        for (uint i = 0; i < credits->size(); i++)
709            (*credits)[i].InsertDB(query, chanid, starttime);
710    }
711
712    QList<EventRating>::const_iterator j = ratings.begin();
713    for (; j != ratings.end(); ++j)
714    {
715        query.prepare(
716            "INSERT IGNORE INTO programrating "
717            "       ( chanid, starttime, system, rating) "
718            "VALUES (:CHANID, :START,    :SYS,  :RATING)");
719        query.bindValue(":CHANID", chanid);
720        query.bindValue(":START",  starttime);
721        query.bindValue(":SYS",    (*j).system);
722        query.bindValue(":RATING", (*j).rating);
723
724        if (!query.exec())
725            MythDB::DBError("programrating insert", query);
726    }
727
728    add_genres(query, genres, chanid, starttime);
729
730    return 1;
731}
732
733static bool delete_program(MSqlQuery &query, uint chanid, const QDateTime &st)
734{
735    query.prepare(
736        "DELETE from program "
737        "WHERE chanid    = :CHANID AND "
738        "      starttime = :STARTTIME");
739
740    query.bindValue(":CHANID",    chanid);
741    query.bindValue(":STARTTIME", st);
742
743    if (!query.exec())
744    {
745        MythDB::DBError("delete_program", query);
746        return false;
747    }
748
749    query.prepare(
750        "DELETE from credits "
751        "WHERE chanid    = :CHANID AND "
752        "      starttime = :STARTTIME");
753
754    query.bindValue(":CHANID",    chanid);
755    query.bindValue(":STARTTIME", st);
756
757    if (!query.exec())
758    {
759        MythDB::DBError("delete_credits", query);
760        return false;
761    }
762
763    query.prepare(
764        "DELETE from programrating "
765        "WHERE chanid    = :CHANID AND "
766        "      starttime = :STARTTIME");
767
768    query.bindValue(":CHANID",    chanid);
769    query.bindValue(":STARTTIME", st);
770
771    if (!query.exec())
772    {
773        MythDB::DBError("delete_rating", query);
774        return false;
775    }
776
777    query.prepare(
778        "DELETE from programgenres "
779        "WHERE chanid    = :CHANID AND "
780        "      starttime = :STARTTIME");
781
782    query.bindValue(":CHANID",    chanid);
783    query.bindValue(":STARTTIME", st);
784
785    if (!query.exec())
786    {
787        MythDB::DBError("delete_genres", query);
788        return false;
789    }
790
791    return true;
792}
793
794static bool program_exists(MSqlQuery &query, uint chanid, const QDateTime &st)
795{
796    query.prepare(
797        "SELECT title FROM program "
798        "WHERE chanid    = :CHANID AND "
799        "      starttime = :OLDSTART");
800    query.bindValue(":CHANID",   chanid);
801    query.bindValue(":OLDSTART", st);
802    if (!query.exec())
803    {
804        MythDB::DBError("program_exists", query);
805    }
806    if (query.next())
807    {
808        return true;
809    }
810    return false;
811}
812
813static bool change_program(MSqlQuery &query, uint chanid, const QDateTime &st,
814                           const QDateTime &new_st, const QDateTime &new_end)
815{
816    query.prepare(
817        "UPDATE program "
818        "SET starttime = :NEWSTART, "
819        "    endtime   = :NEWEND "
820        "WHERE chanid    = :CHANID AND "
821        "      starttime = :OLDSTART");
822
823    query.bindValue(":CHANID",   chanid);
824    query.bindValue(":OLDSTART", st);
825    query.bindValue(":NEWSTART", new_st);
826    query.bindValue(":NEWEND",   new_end);
827
828    if (!query.exec())
829    {
830        MythDB::DBError("change_program", query);
831        return false;
832    }
833
834    query.prepare(
835        "UPDATE credits "
836        "SET starttime = :NEWSTART "
837        "WHERE chanid    = :CHANID AND "
838        "      starttime = :OLDSTART");
839
840    query.bindValue(":CHANID",   chanid);
841    query.bindValue(":OLDSTART", st);
842    query.bindValue(":NEWSTART", new_st);
843
844    if (!query.exec())
845    {
846        MythDB::DBError("change_credits", query);
847        return false;
848    }
849
850    query.prepare(
851        "UPDATE programrating "
852        "SET starttime = :NEWSTART "
853        "WHERE chanid    = :CHANID AND "
854        "      starttime = :OLDSTART");
855
856    query.bindValue(":CHANID",   chanid);
857    query.bindValue(":OLDSTART", st);
858    query.bindValue(":NEWSTART", new_st);
859
860    if (!query.exec())
861    {
862        MythDB::DBError("change_rating", query);
863        return false;
864    }
865
866    query.prepare(
867        "UPDATE programgenres "
868        "SET starttime = :NEWSTART "
869        "WHERE chanid    = :CHANID AND "
870        "      starttime = :OLDSTART");
871
872    query.bindValue(":CHANID",   chanid);
873    query.bindValue(":OLDSTART", st);
874    query.bindValue(":NEWSTART", new_st);
875
876    if (!query.exec())
877    {
878        MythDB::DBError("change_genres", query);
879        return false;
880    }
881
882    return true;
883}
884
885// Move the program "prog" (3rd parameter) out of the way
886// because it overlaps with our new program.
887bool DBEvent::MoveOutOfTheWayDB(
888    MSqlQuery &query, uint chanid, const DBEvent &prog) const
889{
890    if (prog.starttime >= starttime && prog.endtime <= endtime)
891    {
892        // Old program completely inside our new program.
893        // Delete the old program completely.
894        LOG(VB_EIT, LOG_DEBUG,
895            QString("EIT: delete '%1' %2 - %3")
896                    .arg(prog.title.left(35))
897                    .arg(prog.starttime.toString(Qt::ISODate))
898                    .arg(prog.endtime.toString(Qt::ISODate)));
899        return delete_program(query, chanid, prog.starttime);
900    }
901    else if (prog.starttime < starttime && prog.endtime > starttime)
902    {
903        // Old program starts before, but ends during or after our new program.
904        // Adjust the end time of the old program to the start time
905        // of our new program.
906        // This will leave a hole after our new program when the end time of
907        // the old program was after the end time of the new program!!
908        LOG(VB_EIT, LOG_DEBUG,
909            QString("EIT: change '%1' endtime to %2")
910                    .arg(prog.title.left(35))
911                    .arg(starttime.toString(Qt::ISODate)));
912        return change_program(query, chanid, prog.starttime,
913                              prog.starttime, // Keep the start time
914                              starttime);     // New end time is our start time
915    }
916    else if (prog.starttime < endtime && prog.endtime > endtime)
917    {
918        // Old program starts during, but ends after our new program.
919        // Adjust the starttime of the old program to the end time
920        // of our new program.
921        // If there is already a program starting just when our
922        // new program ends we cannot move the old program
923        // so then we have to delete the old program.
924        if (program_exists(query, chanid, endtime))
925        {
926            LOG(VB_EIT, LOG_DEBUG,
927                QString("EIT: delete '%1' %2 - %3")
928                        .arg(prog.title.left(35))
929                        .arg(prog.starttime.toString(Qt::ISODate))
930                        .arg(prog.endtime.toString(Qt::ISODate)));
931            return delete_program(query, chanid, prog.starttime);
932        }
933        LOG(VB_EIT, LOG_DEBUG,
934            QString("EIT: change '%1' starttime to %2")
935                    .arg(prog.title.left(35))
936                    .arg(endtime.toString(Qt::ISODate)));
937
938        return change_program(query, chanid, prog.starttime,
939                              endtime,        // New start time is our endtime
940                              prog.endtime);  // Keep the end time
941    }
942    // must be non-conflicting...
943    return true;
944}
945
946uint DBEvent::InsertDB(MSqlQuery &query, uint chanid) const
947{
948    query.prepare(
949        "REPLACE INTO program ("
950        "  chanid,         title,          subtitle,        description, "
951        "  category,       category_type, "
952        "  starttime,      endtime, "
953        "  closecaptioned, stereo,         hdtv,            subtitled, "
954        "  subtitletypes,  audioprop,      videoprop, "
955        "  stars,          partnumber,     parttotal, "
956        "  syndicatedepisodenumber, "
957        "  airdate,        originalairdate,listingsource, "
958        "  seriesid,       programid,      previouslyshown, "
959        "  season,         episode,        totalepisodes, "
960        "  inetref ) "
961        "VALUES ("
962        " :CHANID,        :TITLE,         :SUBTITLE,       :DESCRIPTION, "
963        " :CATEGORY,      :CATTYPE, "
964        " :STARTTIME,     :ENDTIME, "
965        " :CC,            :STEREO,        :HDTV,           :HASSUBTITLES, "
966        " :SUBTYPES,      :AUDIOPROP,     :VIDEOPROP, "
967        " :STARS,         :PARTNUMBER,    :PARTTOTAL, "
968        " :SYNDICATENO, "
969        " :AIRDATE,       :ORIGAIRDATE,   :LSOURCE, "
970        " :SERIESID,      :PROGRAMID,     :PREVSHOWN, "
971        " :SEASON,        :EPISODE,       :TOTALEPISODES, "
972        " :INETREF ) ");
973
974    QString cattype = myth_category_type_to_string(categoryType);
975    QString empty("");
976    query.bindValue(":CHANID",      chanid);
977    query.bindValue(":TITLE",       denullify(title));
978    query.bindValue(":SUBTITLE",    denullify(subtitle));
979    query.bindValue(":DESCRIPTION", denullify(description));
980    query.bindValue(":CATEGORY",    denullify(category));
981    query.bindValue(":CATTYPE",     cattype);
982    query.bindValue(":STARTTIME",   starttime);
983    query.bindValue(":ENDTIME",     endtime);
984    query.bindValue(":CC",          (subtitleType & SUB_HARDHEAR) ? true : false);
985    query.bindValue(":STEREO",      (audioProps   & AUD_STEREO)   ? true : false);
986    query.bindValue(":HDTV",        (videoProps   & VID_HDTV)     ? true : false);
987    query.bindValue(":HASSUBTITLES",(subtitleType & SUB_NORMAL)   ? true : false);
988    query.bindValue(":SUBTYPES",    subtitleType);
989    query.bindValue(":AUDIOPROP",   audioProps);
990    query.bindValue(":VIDEOPROP",   videoProps);
991    query.bindValue(":STARS",       stars);
992    query.bindValue(":PARTNUMBER",  partnumber);
993    query.bindValue(":PARTTOTAL",   parttotal);
994    query.bindValue(":SYNDICATENO", denullify(syndicatedepisodenumber));
995    query.bindValue(":AIRDATE",     airdate ? QString::number(airdate) : "0000");
996    query.bindValue(":ORIGAIRDATE", originalairdate);
997    query.bindValue(":LSOURCE",     listingsource);
998    query.bindValue(":SERIESID",    denullify(seriesId));
999    query.bindValue(":PROGRAMID",   denullify(programId));
1000    query.bindValue(":PREVSHOWN",   previouslyshown);
1001    query.bindValue(":SEASON",      season);
1002    query.bindValue(":EPISODE",     episode);
1003    query.bindValue(":TOTALEPISODES", totalepisodes);
1004    query.bindValue(":INETREF",     inetref);
1005
1006    if (!query.exec())
1007    {
1008        MythDB::DBError("InsertDB", query);
1009        return 0;
1010    }
1011
1012    QList<EventRating>::const_iterator j = ratings.begin();
1013    for (; j != ratings.end(); ++j)
1014    {
1015        query.prepare(
1016            "INSERT IGNORE INTO programrating "
1017            "       ( chanid, starttime, system, rating) "
1018            "VALUES (:CHANID, :START,    :SYS,  :RATING)");
1019        query.bindValue(":CHANID", chanid);
1020        query.bindValue(":START",  starttime);
1021        query.bindValue(":SYS",    (*j).system);
1022        query.bindValue(":RATING", (*j).rating);
1023
1024        if (!query.exec())
1025            MythDB::DBError("programrating insert", query);
1026    }
1027
1028    if (credits)
1029    {
1030        for (uint i = 0; i < credits->size(); i++)
1031            (*credits)[i].InsertDB(query, chanid, starttime);
1032    }
1033
1034    add_genres(query, genres, chanid, starttime);
1035
1036    return 1;
1037}
1038
1039ProgInfo::ProgInfo(const ProgInfo &other) :
1040    DBEvent(other.listingsource)
1041{
1042    *this = other;
1043}
1044
1045ProgInfo &ProgInfo::operator=(const ProgInfo &other)
1046{
1047    if (this == &other)
1048        return *this;
1049
1050    DBEvent::operator=(other);
1051
1052    channel         = other.channel;
1053    startts         = other.startts;
1054    endts           = other.endts;
1055    title_pronounce = other.title_pronounce;
1056    showtype        = other.showtype;
1057    colorcode       = other.colorcode;
1058    clumpidx        = other.clumpidx;
1059    clumpmax        = other.clumpmax;
1060
1061    channel.squeeze();
1062    startts.squeeze();
1063    endts.squeeze();
1064    title_pronounce.squeeze();
1065    showtype.squeeze();
1066    colorcode.squeeze();
1067    clumpidx.squeeze();
1068    clumpmax.squeeze();
1069
1070    return *this;
1071}
1072
1073void ProgInfo::Squeeze(void)
1074{
1075    DBEvent::Squeeze();
1076    channel.squeeze();
1077    startts.squeeze();
1078    endts.squeeze();
1079    title_pronounce.squeeze();
1080    showtype.squeeze();
1081    colorcode.squeeze();
1082    clumpidx.squeeze();
1083    clumpmax.squeeze();
1084}
1085
1086uint ProgInfo::InsertDB(MSqlQuery &query, uint chanid) const
1087{
1088    LOG(VB_XMLTV, LOG_INFO,
1089        QString("Inserting new program    : %1 - %2 %3 %4")
1090            .arg(starttime.toString(Qt::ISODate))
1091            .arg(endtime.toString(Qt::ISODate))
1092            .arg(channel)
1093            .arg(title));
1094
1095    query.prepare(
1096        "REPLACE INTO program ("
1097        "  chanid,         title,          subtitle,        description, "
1098        "  category,       category_type,  "
1099        "  starttime,      endtime, "
1100        "  closecaptioned, stereo,         hdtv,            subtitled, "
1101        "  subtitletypes,  audioprop,      videoprop, "
1102        "  partnumber,     parttotal, "
1103        "  syndicatedepisodenumber, "
1104        "  airdate,        originalairdate,listingsource, "
1105        "  seriesid,       programid,      previouslyshown, "
1106        "  stars,          showtype,       title_pronounce, colorcode, "
1107        "  season,         episode,        totalepisodes, "
1108        "  inetref ) "
1109
1110        "VALUES("
1111        " :CHANID,        :TITLE,         :SUBTITLE,       :DESCRIPTION, "
1112        " :CATEGORY,      :CATTYPE,       "
1113        " :STARTTIME,     :ENDTIME, "
1114        " :CC,            :STEREO,        :HDTV,           :HASSUBTITLES, "
1115        " :SUBTYPES,      :AUDIOPROP,     :VIDEOPROP, "
1116        " :PARTNUMBER,    :PARTTOTAL, "
1117        " :SYNDICATENO, "
1118        " :AIRDATE,       :ORIGAIRDATE,   :LSOURCE, "
1119        " :SERIESID,      :PROGRAMID,     :PREVSHOWN, "
1120        " :STARS,         :SHOWTYPE,      :TITLEPRON,      :COLORCODE, "
1121        " :SEASON,        :EPISODE,       :TOTALEPISODES, "
1122        " :INETREF )");
1123
1124    QString cattype = myth_category_type_to_string(categoryType);
1125
1126    query.bindValue(":CHANID",      chanid);
1127    query.bindValue(":TITLE",       denullify(title));
1128    query.bindValue(":SUBTITLE",    denullify(subtitle));
1129    query.bindValue(":DESCRIPTION", denullify(description));
1130    query.bindValue(":CATEGORY",    denullify(category));
1131    query.bindValue(":CATTYPE",     cattype);
1132    query.bindValue(":STARTTIME",   starttime);
1133    query.bindValue(":ENDTIME",     denullify(endtime));
1134    query.bindValue(":CC",
1135                    (subtitleType & SUB_HARDHEAR) ? true : false);
1136    query.bindValue(":STEREO",
1137                    (audioProps   & AUD_STEREO)   ? true : false);
1138    query.bindValue(":HDTV",
1139                    (videoProps   & VID_HDTV)     ? true : false);
1140    query.bindValue(":HASSUBTITLES",
1141                    (subtitleType & SUB_NORMAL)   ? true : false);
1142    query.bindValue(":SUBTYPES",    subtitleType);
1143    query.bindValue(":AUDIOPROP",   audioProps);
1144    query.bindValue(":VIDEOPROP",   videoProps);
1145    query.bindValue(":PARTNUMBER",  partnumber);
1146    query.bindValue(":PARTTOTAL",   parttotal);
1147    query.bindValue(":SYNDICATENO", denullify(syndicatedepisodenumber));
1148    query.bindValue(":AIRDATE",     airdate ? QString::number(airdate):"0000");
1149    query.bindValue(":ORIGAIRDATE", originalairdate);
1150    query.bindValue(":LSOURCE",     listingsource);
1151    query.bindValue(":SERIESID",    denullify(seriesId));
1152    query.bindValue(":PROGRAMID",   denullify(programId));
1153    query.bindValue(":PREVSHOWN",   previouslyshown);
1154    query.bindValue(":STARS",       stars);
1155    query.bindValue(":SHOWTYPE",    showtype);
1156    query.bindValue(":TITLEPRON",   title_pronounce);
1157    query.bindValue(":COLORCODE",   colorcode);
1158    query.bindValue(":SEASON",      season);
1159    query.bindValue(":EPISODE",     episode);
1160    query.bindValue(":TOTALEPISODES", totalepisodes);
1161    query.bindValue(":INETREF",     inetref);
1162
1163    if (!query.exec())
1164    {
1165        MythDB::DBError("program insert", query);
1166        return 0;
1167    }
1168
1169    QList<EventRating>::const_iterator j = ratings.begin();
1170    for (; j != ratings.end(); ++j)
1171    {
1172        query.prepare(
1173            "INSERT IGNORE INTO programrating "
1174            "       ( chanid, starttime, system, rating) "
1175            "VALUES (:CHANID, :START,    :SYS,  :RATING)");
1176        query.bindValue(":CHANID", chanid);
1177        query.bindValue(":START",  starttime);
1178        query.bindValue(":SYS",    (*j).system);
1179        query.bindValue(":RATING", (*j).rating);
1180
1181        if (!query.exec())
1182            MythDB::DBError("programrating insert", query);
1183    }
1184
1185    if (credits)
1186    {
1187        for (uint i = 0; i < credits->size(); ++i)
1188            (*credits)[i].InsertDB(query, chanid, starttime);
1189    }
1190
1191    add_genres(query, genres, chanid, starttime);
1192
1193    return 1;
1194}
1195
1196bool ProgramData::ClearDataByChannel(
1197    uint chanid, const QDateTime &from, const QDateTime &to,
1198    bool use_channel_time_offset)
1199{
1200    int secs = 0;
1201    if (use_channel_time_offset)
1202        secs = ChannelUtil::GetTimeOffset(chanid) * 60;
1203
1204    QDateTime newFrom = from.addSecs(secs);
1205    QDateTime newTo   = to.addSecs(secs);
1206
1207    MSqlQuery query(MSqlQuery::InitCon());
1208    query.prepare("DELETE FROM program "
1209                  "WHERE starttime >= :FROM AND starttime < :TO "
1210                  "AND chanid = :CHANID ;");
1211    query.bindValue(":FROM", newFrom);
1212    query.bindValue(":TO", newTo);
1213    query.bindValue(":CHANID", chanid);
1214    bool ok = query.exec();
1215
1216    query.prepare("DELETE FROM programrating "
1217                  "WHERE starttime >= :FROM AND starttime < :TO "
1218                  "AND chanid = :CHANID ;");
1219    query.bindValue(":FROM", newFrom);
1220    query.bindValue(":TO", newTo);
1221    query.bindValue(":CHANID", chanid);
1222    ok &= query.exec();
1223
1224    query.prepare("DELETE FROM credits "
1225                  "WHERE starttime >= :FROM AND starttime < :TO "
1226                  "AND chanid = :CHANID ;");
1227    query.bindValue(":FROM", newFrom);
1228    query.bindValue(":TO", newTo);
1229    query.bindValue(":CHANID", chanid);
1230    ok &= query.exec();
1231
1232    query.prepare("DELETE FROM programgenres "
1233                  "WHERE starttime >= :FROM AND starttime < :TO "
1234                  "AND chanid = :CHANID ;");
1235    query.bindValue(":FROM", newFrom);
1236    query.bindValue(":TO", newTo);
1237    query.bindValue(":CHANID", chanid);
1238    ok &= query.exec();
1239
1240    return ok;
1241}
1242
1243bool ProgramData::ClearDataBySource(
1244    uint sourceid, const QDateTime &from, const QDateTime &to,
1245    bool use_channel_time_offset)
1246{
1247    vector<uint> chanids = ChannelUtil::GetChanIDs(sourceid);
1248
1249    bool ok = true;
1250    for (uint i = 0; i < chanids.size(); i++)
1251        ok &= ClearDataByChannel(chanids[i], from, to, use_channel_time_offset);
1252
1253    return ok;
1254}
1255
1256static bool start_time_less_than(const DBEvent *a, const DBEvent *b)
1257{
1258    return (a->starttime < b->starttime);
1259}
1260
1261void ProgramData::FixProgramList(QList<ProgInfo*> &fixlist)
1262{
1263    qStableSort(fixlist.begin(), fixlist.end(), start_time_less_than);
1264
1265    QList<ProgInfo*>::iterator it = fixlist.begin();
1266    while (1)
1267    {
1268        QList<ProgInfo*>::iterator cur = it;
1269        ++it;
1270
1271        // fill in miss stop times
1272        if ((*cur)->endts.isEmpty() || (*cur)->startts > (*cur)->endts)
1273        {
1274            if (it != fixlist.end())
1275            {
1276                (*cur)->endts   = (*it)->startts;
1277                (*cur)->endtime = (*it)->starttime;
1278            }
1279            /* if its the last programme in the file then leave its
1280               endtime as 0000-00-00 00:00:00 so we can find it easily in
1281               fix_end_times() */
1282        }
1283
1284        if (it == fixlist.end())
1285            break;
1286
1287        // remove overlapping programs
1288        if ((*cur)->HasTimeConflict(**it))
1289        {
1290            QList<ProgInfo*>::iterator tokeep, todelete;
1291
1292            if ((*cur)->endtime <= (*cur)->starttime)
1293                tokeep = it, todelete = cur;
1294            else if ((*it)->endtime <= (*it)->starttime)
1295                tokeep = cur, todelete = it;
1296            else if (!(*cur)->subtitle.isEmpty() &&
1297                     (*it)->subtitle.isEmpty())
1298                tokeep = cur, todelete = it;
1299            else if (!(*it)->subtitle.isEmpty()  &&
1300                     (*cur)->subtitle.isEmpty())
1301                tokeep = it, todelete = cur;
1302            else if (!(*cur)->description.isEmpty() &&
1303                     (*it)->description.isEmpty())
1304                tokeep = cur, todelete = it;
1305            else
1306                tokeep = it, todelete = cur;
1307
1308
1309            LOG(VB_XMLTV, LOG_INFO,
1310                QString("Removing conflicting program: %1 - %2 %3 %4")
1311                    .arg((*todelete)->starttime.toString(Qt::ISODate))
1312                    .arg((*todelete)->endtime.toString(Qt::ISODate))
1313                    .arg((*todelete)->channel)
1314                    .arg((*todelete)->title));
1315
1316            LOG(VB_XMLTV, LOG_INFO,
1317                QString("Conflicted with            : %1 - %2 %3 %4")
1318                    .arg((*tokeep)->starttime.toString(Qt::ISODate))
1319                    .arg((*tokeep)->endtime.toString(Qt::ISODate))
1320                    .arg((*tokeep)->channel)
1321                    .arg((*tokeep)->title));
1322
1323            bool step_back = todelete == it;
1324            it = fixlist.erase(todelete);
1325            if (step_back)
1326                --it;
1327        }
1328    }
1329}
1330
1331void ProgramData::HandlePrograms(
1332    uint sourceid, QMap<QString, QList<ProgInfo> > &proglist)
1333{
1334    uint unchanged = 0, updated = 0;
1335
1336    MSqlQuery query(MSqlQuery::InitCon());
1337
1338    QMap<QString, QList<ProgInfo> >::const_iterator mapiter;
1339    for (mapiter = proglist.begin(); mapiter != proglist.end(); ++mapiter)
1340    {
1341        if (mapiter.key().isEmpty())
1342            continue;
1343
1344        query.prepare(
1345            "SELECT chanid "
1346            "FROM channel "
1347            "WHERE sourceid = :ID AND "
1348            "      xmltvid  = :XMLTVID");
1349        query.bindValue(":ID",      sourceid);
1350        query.bindValue(":XMLTVID", mapiter.key());
1351
1352        if (!query.exec())
1353        {
1354            MythDB::DBError("ProgramData::HandlePrograms", query);
1355            continue;
1356        }
1357
1358        vector<uint> chanids;
1359        while (query.next())
1360            chanids.push_back(query.value(0).toUInt());
1361
1362        if (chanids.empty())
1363        {
1364            LOG(VB_GENERAL, LOG_NOTICE,
1365                QString("Unknown xmltv channel identifier: %1"
1366                        " - Skipping channel.").arg(mapiter.key()));
1367            continue;
1368        }
1369
1370        QList<ProgInfo> &list = proglist[mapiter.key()];
1371        QList<ProgInfo*> sortlist;
1372        QList<ProgInfo>::iterator it = list.begin();
1373        for (; it != list.end(); ++it)
1374            sortlist.push_back(&(*it));
1375
1376        FixProgramList(sortlist);
1377
1378        for (uint i = 0; i < chanids.size(); ++i)
1379        {
1380            HandlePrograms(query, chanids[i], sortlist, unchanged, updated);
1381        }
1382    }
1383
1384    LOG(VB_GENERAL, LOG_INFO,
1385        QString("Updated programs: %1 Unchanged programs: %2")
1386                .arg(updated) .arg(unchanged));
1387}
1388
1389void ProgramData::HandlePrograms(MSqlQuery             &query,
1390                                 uint                   chanid,
1391                                 const QList<ProgInfo*> &sortlist,
1392                                 uint &unchanged,
1393                                 uint &updated)
1394{
1395    QList<ProgInfo*>::const_iterator it = sortlist.begin();
1396    for (; it != sortlist.end(); ++it)
1397    {
1398        if (IsUnchanged(query, chanid, **it))
1399        {
1400            unchanged++;
1401            continue;
1402        }
1403
1404        if (!DeleteOverlaps(query, chanid, **it))
1405            continue;
1406
1407        updated += (*it)->InsertDB(query, chanid);
1408    }
1409}
1410
1411int ProgramData::fix_end_times(void)
1412{
1413    int count = 0;
1414    QString chanid, starttime, endtime, querystr;
1415    MSqlQuery query1(MSqlQuery::InitCon()), query2(MSqlQuery::InitCon());
1416
1417    querystr = "SELECT chanid, starttime, endtime FROM program "
1418               "WHERE endtime = '0000-00-00 00:00:00' "
1419               "ORDER BY chanid, starttime;";
1420
1421    if (!query1.exec(querystr))
1422    {
1423        LOG(VB_GENERAL, LOG_ERR,
1424            QString("fix_end_times query failed: %1").arg(querystr));
1425        return -1;
1426    }
1427
1428    while (query1.next())
1429    {
1430        starttime = query1.value(1).toString();
1431        chanid = query1.value(0).toString();
1432        endtime = query1.value(2).toString();
1433
1434        querystr = QString("SELECT chanid, starttime, endtime FROM program "
1435                           "WHERE starttime > '%1' "
1436                           "AND chanid = '%2' "
1437                           "ORDER BY starttime LIMIT 1;")
1438                           .arg(starttime)
1439                           .arg(chanid);
1440
1441        if (!query2.exec(querystr))
1442        {
1443            LOG(VB_GENERAL, LOG_ERR,
1444                QString("fix_end_times query failed: %1").arg(querystr));
1445            return -1;
1446        }
1447
1448        if (query2.next() && (endtime != query2.value(1).toString()))
1449        {
1450            count++;
1451            endtime = query2.value(1).toString();
1452            querystr = QString("UPDATE program SET "
1453                               "endtime = '%2' WHERE (chanid = '%3' AND "
1454                               "starttime = '%4');")
1455                               .arg(endtime)
1456                               .arg(chanid)
1457                               .arg(starttime);
1458
1459            if (!query2.exec(querystr))
1460            {
1461                LOG(VB_GENERAL, LOG_ERR,
1462                    QString("fix_end_times query failed: %1").arg(querystr));
1463                return -1;
1464            }
1465        }
1466    }
1467
1468    return count;
1469}
1470
1471bool ProgramData::IsUnchanged(
1472    MSqlQuery &query, uint chanid, const ProgInfo &pi)
1473{
1474    query.prepare(
1475        "SELECT count(*) "
1476        "FROM program "
1477        "WHERE chanid          = :CHANID     AND "
1478        "      starttime       = :START      AND "
1479        "      endtime         = :END        AND "
1480        "      title           = :TITLE      AND "
1481        "      subtitle        = :SUBTITLE   AND "
1482        "      description     = :DESC       AND "
1483        "      category        = :CATEGORY   AND "
1484        "      category_type   = :CATEGORY_TYPE AND "
1485        "      airdate         = :AIRDATE    AND "
1486        "      stars >= (:STARS1 - 0.001)    AND "
1487        "      stars <= (:STARS2 + 0.001)    AND "
1488        "      previouslyshown = :PREVIOUSLYSHOWN AND "
1489        "      title_pronounce = :TITLE_PRONOUNCE AND "
1490        "      audioprop       = :AUDIOPROP  AND "
1491        "      videoprop       = :VIDEOPROP  AND "
1492        "      subtitletypes   = :SUBTYPES   AND "
1493        "      partnumber      = :PARTNUMBER AND "
1494        "      parttotal       = :PARTTOTAL  AND "
1495        "      seriesid        = :SERIESID   AND "
1496        "      showtype        = :SHOWTYPE   AND "
1497        "      colorcode       = :COLORCODE  AND "
1498        "      syndicatedepisodenumber = :SYNDICATEDEPISODENUMBER AND "
1499        "      programid       = :PROGRAMID  AND "
1500        "      inetref         = :INETREF");
1501
1502    QString cattype = myth_category_type_to_string(pi.categoryType);
1503
1504    query.bindValue(":CHANID",     chanid);
1505    query.bindValue(":START",      pi.starttime);
1506    query.bindValue(":END",        pi.endtime);
1507    query.bindValue(":TITLE",      denullify(pi.title));
1508    query.bindValue(":SUBTITLE",   denullify(pi.subtitle));
1509    query.bindValue(":DESC",       denullify(pi.description));
1510    query.bindValue(":CATEGORY",   denullify(pi.category));
1511    query.bindValue(":CATEGORY_TYPE", cattype);
1512    query.bindValue(":AIRDATE",    pi.airdate);
1513    query.bindValue(":STARS1",     pi.stars);
1514    query.bindValue(":STARS2",     pi.stars);
1515    query.bindValue(":PREVIOUSLYSHOWN", pi.previouslyshown);
1516    query.bindValue(":TITLE_PRONOUNCE", pi.title_pronounce);
1517    query.bindValue(":AUDIOPROP",  pi.audioProps);
1518    query.bindValue(":VIDEOPROP",  pi.videoProps);
1519    query.bindValue(":SUBTYPES",   pi.subtitleType);
1520    query.bindValue(":PARTNUMBER", pi.partnumber);
1521    query.bindValue(":PARTTOTAL",  pi.parttotal);
1522    query.bindValue(":SERIESID",   denullify(pi.seriesId));
1523    query.bindValue(":SHOWTYPE",   pi.showtype);
1524    query.bindValue(":COLORCODE",  pi.colorcode);
1525    query.bindValue(":SYNDICATEDEPISODENUMBER",
1526                    denullify(pi.syndicatedepisodenumber));
1527    query.bindValue(":PROGRAMID",  denullify(pi.programId));
1528    query.bindValue(":INETREF",    pi.inetref);
1529
1530    if (query.exec() && query.next())
1531        return query.value(0).toUInt() > 0;
1532
1533    return false;
1534}
1535
1536bool ProgramData::DeleteOverlaps(
1537    MSqlQuery &query, uint chanid, const ProgInfo &pi)
1538{
1539    if (VERBOSE_LEVEL_CHECK(VB_XMLTV, LOG_INFO))
1540    {
1541        // Get overlaps..
1542        query.prepare(
1543            "SELECT title,starttime,endtime "
1544            "FROM program "
1545            "WHERE chanid     = :CHANID AND "
1546            "      starttime >= :START AND "
1547            "      starttime <  :END;");
1548        query.bindValue(":CHANID", chanid);
1549        query.bindValue(":START",  pi.starttime);
1550        query.bindValue(":END",    pi.endtime);
1551
1552        if (!query.exec())
1553            return false;
1554
1555        if (!query.next())
1556            return true;
1557
1558        do
1559        {
1560            LOG(VB_XMLTV, LOG_INFO,
1561                QString("Removing existing program: %1 - %2 %3 %4")
1562                .arg(MythDate::as_utc(query.value(1).toDateTime()).toString(Qt::ISODate))
1563                .arg(MythDate::as_utc(query.value(2).toDateTime()).toString(Qt::ISODate))
1564                .arg(pi.channel)
1565                .arg(query.value(0).toString()));
1566        } while (query.next());
1567    }
1568
1569    if (!ClearDataByChannel(chanid, pi.starttime, pi.endtime, false))
1570    {
1571        LOG(VB_XMLTV, LOG_ERR,
1572            QString("Program delete failed    : %1 - %2 %3 %4")
1573                .arg(pi.starttime.toString(Qt::ISODate))
1574                .arg(pi.endtime.toString(Qt::ISODate))
1575                .arg(pi.channel)
1576                .arg(pi.title));
1577        return false;
1578    }
1579
1580    return true;
1581}