Ticket #11914: programdata.cpp.v10

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