MythTV  master
eitfixup.cpp
Go to the documentation of this file.
1 // C++ headers
2 #include <algorithm>
3 #include <array>
4 
5 // Qt Headers
6 #include <QRegularExpression>
7 
8 // MythTV headers
10 #include "libmythbase/programinfo.h" // for CategoryType, subtitle types and audio and video properties
11 
12 #include "channelutil.h" // for GetDefaultAuthority()
13 #include "eitfixup.h"
14 #include "mpeg/dishdescriptors.h" // for dish_theme_type_to_string
15 
16 /*------------------------------------------------------------------------
17  * Event Fix Up Scripts - Turned on by entry in dtv_privatetype table
18  *------------------------------------------------------------------------*/
19 
20 static const QRegularExpression kStereo { R"(\b\(?[sS]tereo\)?\b)" };
21 static const QRegularExpression kUKSpaceColonStart { R"(^[ |:]*)" };
22 static const QRegularExpression kDotAtEnd { "\\.$" };
23 
24 static const QMap<QChar,quint16> r2v = {
25  {'I' , 1}, {'V' , 5}, {'X' , 10}, {'L' , 50},
26  {'C' , 100}, {'D' , 500}, {'M' , 1000},
27  {QChar(0x399), 1}, // Greek Ι
28 };
29 
30 int EITFixUp::parseRoman (QString roman)
31 {
32  if (roman.isEmpty())
33  return 0;
34 
35  uint result = 0;
36  for (int i = 0; i < roman.size() - 1; i++)
37  {
38  int v1 = r2v[roman.at(i)];
39  int v2 = r2v[roman.at(i+1)];
40  result += (v1 >= v2) ? v1 : -v1;
41  }
42  return result + r2v[roman.back()];
43 }
44 
45 
47 {
48  if (event.m_fixup)
49  {
50  if (event.m_subtitle == event.m_title)
51  event.m_subtitle = QString("");
52 
53  if (event.m_description.isEmpty() && !event.m_subtitle.isEmpty())
54  {
55  event.m_description = event.m_subtitle;
56  event.m_subtitle = QString("");
57  }
58  }
59 
60  if (kFixHTML & event.m_fixup)
61  FixStripHTML(event);
62 
63  if (kFixHDTV & event.m_fixup)
64  event.m_videoProps |= VID_HDTV;
65 
66  if (kFixBell & event.m_fixup)
67  FixBellExpressVu(event);
68 
69  if (kFixDish & event.m_fixup)
70  FixBellExpressVu(event);
71 
72  if (kFixUK & event.m_fixup)
73  FixUK(event);
74 
75  if (kFixPBS & event.m_fixup)
76  FixPBS(event);
77 
78  if (kFixComHem & event.m_fixup)
79  FixComHem(event, (kFixSubtitle & event.m_fixup) != 0U);
80 
81  if (kFixAUStar & event.m_fixup)
82  FixAUStar(event);
83 
84  if (kFixAUDescription & event.m_fixup)
85  FixAUDescription(event);
86 
87  if (kFixAUFreeview & event.m_fixup)
88  FixAUFreeview(event);
89 
90  if (kFixAUNine & event.m_fixup)
91  FixAUNine(event);
92 
93  if (kFixAUSeven & event.m_fixup)
94  FixAUSeven(event);
95 
96  if (kFixMCA & event.m_fixup)
97  FixMCA(event);
98 
99  if (kFixRTL & event.m_fixup)
100  FixRTL(event);
101 
102  if (kFixP7S1 & event.m_fixup)
103  FixPRO7(event);
104 
105  if (kFixATV & event.m_fixup)
106  FixATV(event);
107 
108  if (kFixDisneyChannel & event.m_fixup)
109  FixDisneyChannel(event);
110 
111  if (kFixFI & event.m_fixup)
112  FixFI(event);
113 
114  if (kFixPremiere & event.m_fixup)
115  FixPremiere(event);
116 
117  if (kFixNL & event.m_fixup)
118  FixNL(event);
119 
120  if (kFixNO & event.m_fixup)
121  FixNO(event);
122 
123  if (kFixNRK_DVBT & event.m_fixup)
124  FixNRK_DVBT(event);
125 
126  if (kFixDK & event.m_fixup)
127  FixDK(event);
128 
129  if (kFixCategory & event.m_fixup)
130  FixCategory(event);
131 
132  if (kFixGreekSubtitle & event.m_fixup)
133  FixGreekSubtitle(event);
134 
135  if (kFixGreekEIT & event.m_fixup)
136  FixGreekEIT(event);
137 
138  if (kFixGreekCategories & event.m_fixup)
139  FixGreekCategories(event);
140 
141  if (kFixUnitymedia & event.m_fixup)
142  FixUnitymedia(event);
143 
144  // Clean up text strings after all fixups have been applied.
145  if (event.m_fixup)
146  {
147  static const QRegularExpression emptyParens { R"(\(\s*\))" };
148  if (!event.m_title.isEmpty())
149  {
150  event.m_title.remove(QChar('\0')).remove(emptyParens);
151  event.m_title = event.m_title.simplified();
152  }
153 
154  if (!event.m_subtitle.isEmpty())
155  {
156  event.m_subtitle.remove(QChar('\0'));
157  event.m_subtitle.remove(emptyParens);
158  event.m_subtitle = event.m_subtitle.simplified();
159  }
160 
161  if (!event.m_description.isEmpty())
162  {
163  event.m_description.remove(QChar('\0'));
164  event.m_description.remove(emptyParens);
165  event.m_description = event.m_description.simplified();
166  }
167  }
168 
169  if (kFixGenericDVB & event.m_fixup)
170  {
171  event.m_programId = AddDVBEITAuthority(event.m_chanid, event.m_programId);
172  event.m_seriesId = AddDVBEITAuthority(event.m_chanid, event.m_seriesId);
173  }
174 
175  // Are any items left unhandled? report them to allow fixups improvements
176  if (!event.m_items.empty())
177  {
178  for (auto i = event.m_items.begin(); i != event.m_items.end(); ++i)
179  {
180  LOG(VB_EIT, LOG_DEBUG, QString("Unhandled item in EIT for"
181  " channel id \"%1\", \"%2\": %3").arg(event.m_chanid)
182  .arg(i.key(), i.value()));
183  }
184  }
185 }
186 
202 QString EITFixUp::AddDVBEITAuthority(uint chanid, const QString &id)
203 {
204  if (id.isEmpty())
205  return id;
206 
207  // CRIDs are not case sensitive, so change all to lower case
208  QString crid = id.toLower();
209 
210  // remove "crid://"
211  if (crid.startsWith("crid://"))
212  crid.remove(0,7);
213 
214  // if id is a CRID with authority, return it
215  if (crid.length() >= 1 && crid[0] != '/')
216  return crid;
217 
218  QString authority = ChannelUtil::GetDefaultAuthority(chanid);
219  if (authority.isEmpty())
220  return ""; // no authority, not a valid CRID, return empty
221 
222  return authority + crid;
223 }
224 
230 {
231  // A 0x0D character is present between the content
232  // and the subtitle if its present
233  int position = event.m_description.indexOf('\r');
234 
235  if (position != -1)
236  {
237  // Subtitle present in the title, so get
238  // it and adjust the description
239  event.m_subtitle = event.m_description.left(position);
240  event.m_description = event.m_description.right(
241  event.m_description.length() - position - 2);
242  }
243 
244  // Take out the content description which is
245  // always next with a period after it
246  position = event.m_description.indexOf(".");
247  // Make sure they didn't leave it out and
248  // you come up with an odd category
249  if (position < 10)
250  {
251  }
252  else
253  {
254  event.m_category = "Unknown";
255  }
256 
257  // If the content descriptor didn't come up with anything, try parsing the category
258  // out of the description.
259  if (event.m_category.isEmpty())
260  {
261  // Take out the content description which is
262  // always next with a period after it
263  position = event.m_description.indexOf(".");
264  if ((position + 1) < event.m_description.length())
265  position = event.m_description.indexOf(". ");
266  // Make sure they didn't leave it out and
267  // you come up with an odd category
268  if ((position > -1) && position < 20)
269  {
270  const QString stmp = event.m_description;
271  event.m_description = stmp.right(stmp.length() - position - 2);
272  event.m_category = stmp.left(position);
273 
274  int position_p = event.m_category.indexOf("(");
275  if (position_p == -1)
276  event.m_description = stmp.right(stmp.length() - position - 2);
277  else
278  event.m_category = "Unknown";
279  }
280  else
281  {
282  event.m_category = "Unknown";
283  }
284 
285  // When a channel is off air the category is "-"
286  // so leave the category as blank
287  if (event.m_category == "-")
288  event.m_category = "OffAir";
289 
290  if (event.m_category.length() > 20)
291  event.m_category = "Unknown";
292  }
293  else if (event.m_categoryType)
294  {
295  QString theme = dish_theme_type_to_string(event.m_categoryType);
296  event.m_description = event.m_description.replace(theme, "");
297  if (event.m_description.startsWith("."))
298  event.m_description = event.m_description.right(event.m_description.length() - 1);
299  if (event.m_description.startsWith(" "))
300  event.m_description = event.m_description.right(event.m_description.length() - 1);
301  }
302 
303  // See if a year is present as (xxxx)
304  static const QRegularExpression bellYear { R"(\([0-9]{4}\))" };
305  position = event.m_description.indexOf(bellYear);
306  if (position != -1 && !event.m_category.isEmpty())
307  {
308  // Parse out the year
309  bool ok = false;
310  uint y = event.m_description.mid(position + 1, 4).toUInt(&ok);
311  if (ok)
312  {
313  event.m_originalairdate = QDate(y, 1, 1);
314  event.m_airdate = y;
315  event.m_previouslyshown = true;
316  }
317 
318  // Get the actors if they exist
319  if (position > 3)
320  {
321  static const QRegularExpression bellActors { R"(\set\s|,)" };
322  QString tmp = event.m_description.left(position-3);
323  QStringList actors =
324  tmp.split(bellActors, Qt::SkipEmptyParts);
325 
326  /* Possible TODO: if EIT inlcude the priority and/or character
327  * names for the actors, include them in AddPerson call. */
328  for (const auto & actor : std::as_const(actors))
329  event.AddPerson(DBPerson::kActor, actor);
330  }
331  // Remove the year and actors from the description
332  event.m_description = event.m_description.right(
333  event.m_description.length() - position - 7);
334  }
335 
336  // Check for (CC) in the decription and
337  // set the <subtitles type="teletext"> flag
338  position = event.m_description.indexOf("(CC)");
339  if (position != -1)
340  {
341  event.m_subtitleType |= SUB_HARDHEAR;
342  event.m_description = event.m_description.replace("(CC)", "");
343  }
344 
345  // Check for (Stereo) in the decription and set the <audio> tags
346  auto match = kStereo.match(event.m_description);
347  if (match.hasMatch())
348  {
349  event.m_audioProps |= AUD_STEREO;
350  event.m_description.remove(match.capturedStart(0),
351  match.capturedLength(0));
352  }
353 
354  // Check for "title (All Day, HD)" in the title
355  static const QRegularExpression bellPPVTitleAllDayHD { R"(\s*\(All Day\, HD\)\s*$)" };
356  match = bellPPVTitleAllDayHD.match(event.m_title);
357  if (match.hasMatch())
358  {
359  event.m_title.remove(match.capturedStart(), match.capturedLength());
360  event.m_videoProps |= VID_HDTV;
361  }
362 
363  // Check for "title (All Day)" in the title
364  static const QRegularExpression bellPPVTitleAllDay { R"(\s*\(All Day.*\)\s*$)" };
365  match = bellPPVTitleAllDay.match(event.m_title);
366  if (match.hasMatch())
367  event.m_title.remove(match.capturedStart(), match.capturedLength());
368 
369  // Check for "HD - title" in the title
370  static const QRegularExpression bellPPVTitleHD { R"(^HD\s?-\s?)" };
371  match = bellPPVTitleHD.match(event.m_title);
372  if (match.hasMatch())
373  {
374  event.m_title.remove(match.capturedStart(), match.capturedLength());
375  event.m_videoProps |= VID_HDTV;
376  }
377 
378  // Check for (HD) in the decription
379  position = event.m_description.indexOf("(HD)");
380  if (position != -1)
381  {
382  event.m_description = event.m_description.replace("(HD)", "");
383  event.m_videoProps |= VID_HDTV;
384  }
385 
386  // Check for (HD) in the title
387  position = event.m_title.indexOf("(HD)");
388  if (position != -1)
389  {
390  event.m_title = event.m_title.replace("(HD)", "");
391  event.m_videoProps |= VID_HDTV;
392  }
393 
394  // Check for HD at the end of the title
395  static const QRegularExpression dishPPVTitleHD { R"(\sHD\s*$)" };
396  match = dishPPVTitleHD.match(event.m_title);
397  if (match.hasMatch())
398  {
399  event.m_title.remove(match.capturedStart(), match.capturedLength());
400  event.m_videoProps |= VID_HDTV;
401  }
402 
403  // Check for (DD) at the end of the description
404  position = event.m_description.indexOf("(DD)");
405  if (position != -1)
406  {
407  event.m_description = event.m_description.replace("(DD)", "");
408  event.m_audioProps |= AUD_DOLBY;
409  event.m_audioProps |= AUD_STEREO;
410  }
411 
412  // Remove SAP from Dish descriptions
413  position = event.m_description.indexOf("(SAP)");
414  if (position != -1)
415  {
416  event.m_description = event.m_description.replace("(SAP", "");
417  event.m_subtitleType |= SUB_HARDHEAR;
418  }
419 
420  // Remove any trailing colon in title
421  static const QRegularExpression dishPPVTitleColon { R"(\:\s*$)" };
422  match = dishPPVTitleColon.match(event.m_title);
423  if (match.hasMatch())
424  event.m_title.remove(match.capturedStart(), match.capturedLength());
425 
426  // Remove New at the end of the description
427  static const QRegularExpression dishDescriptionNew { R"(\s*New\.\s*)" };
428  match = dishDescriptionNew.match(event.m_description);
429  if (match.hasMatch())
430  {
431  event.m_previouslyshown = false;
432  event.m_description.remove(match.capturedStart(), match.capturedLength());
433  }
434 
435  // Remove Series Finale at the end of the desciption
436  static const QRegularExpression dishDescriptionFinale { R"(\s*(Series|Season)\sFinale\.\s*)" };
437  match = dishDescriptionFinale.match(event.m_description);
438  if (match.hasMatch())
439  {
440  event.m_previouslyshown = false;
441  event.m_description.remove(match.capturedStart(), match.capturedLength());
442  }
443 
444  // Remove Series Finale at the end of the desciption
445  static const QRegularExpression dishDescriptionFinale2 { R"(\s*Finale\.\s*)" };
446  match = dishDescriptionFinale2.match(event.m_description);
447  if (match.hasMatch())
448  {
449  event.m_previouslyshown = false;
450  event.m_description.remove(match.capturedStart(), match.capturedLength());
451  }
452 
453  // Remove Series Premiere at the end of the description
454  static const QRegularExpression dishDescriptionPremiere { R"(\s*(Series|Season)\s(Premier|Premiere)\.\s*)" };
455  match = dishDescriptionPremiere.match(event.m_description);
456  if (match.hasMatch())
457  {
458  event.m_previouslyshown = false;
459  event.m_description.remove(match.capturedStart(), match.capturedLength());
460  }
461 
462  // Remove Series Premiere at the end of the description
463  static const QRegularExpression dishDescriptionPremiere2 { R"(\s*(Premier|Premiere)\.\s*)" };
464  match = dishDescriptionPremiere2.match(event.m_description);
465  if (match.hasMatch())
466  {
467  event.m_previouslyshown = false;
468  event.m_description.remove(match.capturedStart(), match.capturedLength());
469  }
470 
471  // Remove Dish's PPV code at the end of the description
472  static const QRegularExpression ppvcode { R"(\s*\(([A-Z]|[0-9]){5}\)\s*$)",
473  QRegularExpression::CaseInsensitiveOption };
474  match = ppvcode.match(event.m_description);
475  if (match.hasMatch())
476  event.m_description.remove(match.capturedStart(), match.capturedLength());
477 
478  // Remove trailing garbage
479  static const QRegularExpression dishPPVSpacePerenEnd { R"(\s\)\s*$)" };
480  match = dishPPVSpacePerenEnd.match(event.m_description);
481  if (match.hasMatch())
482  event.m_description.remove(match.capturedStart(), match.capturedLength());
483 
484  // Check for subtitle "All Day (... Eastern)" in the subtitle
485  static const QRegularExpression bellPPVSubtitleAllDay { R"(^All Day \(.*\sEastern\)\s*$)" };
486  match = bellPPVSubtitleAllDay.match(event.m_subtitle);
487  if (match.hasMatch())
488  event.m_subtitle.remove(match.capturedStart(), match.capturedLength());
489 
490  // Check for description "(... Eastern)" in the description
491  static const QRegularExpression bellPPVDescriptionAllDay { R"(^\(.*\sEastern\))" };
492  match = bellPPVDescriptionAllDay.match(event.m_description);
493  if (match.hasMatch())
494  event.m_description.remove(match.capturedStart(), match.capturedLength());
495 
496  // Check for description "(... ET)" in the description
497  static const QRegularExpression bellPPVDescriptionAllDay2 { R"(^\([0-9].*am-[0-9].*am\sET\))" };
498  match = bellPPVDescriptionAllDay2.match(event.m_description);
499  if (match.hasMatch())
500  event.m_description.remove(match.capturedStart(), match.capturedLength());
501 
502  // Check for description "(nnnnn)" in the description
503  static const QRegularExpression bellPPVDescriptionEventId { R"(\([0-9]{5}\))" };
504  match = bellPPVDescriptionEventId.match(event.m_description);
505  if (match.hasMatch())
506  event.m_description.remove(match.capturedStart(), match.capturedLength());
507 }
508 
513 {
514  QStringList strListColon = event.m_description.split(":");
515  QStringList strListEnd;
516 
517  bool fColon = false;
518  bool fQuotedSubtitle = false;
519  QString strEnd;
520  if (strListColon.count()>1)
521  {
522  bool fDoubleDot = false;
523  bool fSingleDot = true;
524  int nLength = strListColon[0].length();
525 
526  int nPosition1 = event.m_description.indexOf("..");
527  if ((nPosition1 < nLength) && (nPosition1 >= 0))
528  fDoubleDot = true;
529  nPosition1 = event.m_description.indexOf(".");
530  if (nPosition1==-1)
531  fSingleDot = false;
532  if (nPosition1 > nLength)
533  fSingleDot = false;
534  else
535  {
536  QString strTmp = event.m_description.mid(nPosition1+1,
537  nLength-nPosition1);
538 
539  QStringList tmp = strTmp.split(" ");
540  if (((uint) tmp.size()) < kMaxDotToColon)
541  fSingleDot = false;
542  }
543 
544  if (fDoubleDot)
545  {
546  strListEnd = strListColon;
547  fColon = true;
548  }
549  else if (!fSingleDot)
550  {
551  QStringList strListTmp;
552  uint nTitle=0;
553  int nTitleMax=-1;
554  for (int i =0; (i<strListColon.count()) && (nTitleMax==-1);i++)
555  {
556  const QStringList tmp = strListColon[i].split(" ");
557 
558  nTitle += tmp.size();
559 
560  if (nTitle < kMaxToTitle)
561  strListTmp.push_back(strListColon[i]);
562  else
563  nTitleMax=i;
564  }
565  QString strPartial;
566  for (int i=0;i<(nTitleMax-1);i++)
567  strPartial+=strListTmp[i]+":";
568  if (nTitleMax>0)
569  {
570  strPartial+=strListTmp[nTitleMax-1];
571  strListEnd.push_back(strPartial);
572  }
573  for (int i=nTitleMax+1;i<strListColon.count();i++)
574  strListEnd.push_back(strListColon[i]);
575  fColon = true;
576  }
577  }
578  static const QRegularExpression ukQuotedSubtitle { R"(^'([\w\s\-,]+?)\.' )" };
579  auto match = ukQuotedSubtitle.match(event.m_description);
580  if (match.hasMatch())
581  {
582  event.m_subtitle = match.captured(1);
583  event.m_description.remove(match.capturedStart(0),
584  match.capturedLength(0));
585  fQuotedSubtitle = true;
586  }
587  QStringList strListPeriod;
588  QStringList strListQuestion;
589  QStringList strListExcl;
590  if (!(fColon || fQuotedSubtitle))
591  {
592  strListPeriod = event.m_description.split(".");
593  if (strListPeriod.count() >1)
594  {
595  int nPosition1 = event.m_description.indexOf(".");
596  int nPosition2 = event.m_description.indexOf("..");
597  if ((nPosition1 < nPosition2) || (nPosition2==-1))
598  strListEnd = strListPeriod;
599  }
600 
601  strListQuestion = event.m_description.split("?");
602  strListExcl = event.m_description.split("!");
603  if ((strListQuestion.size() > 1) &&
604  ((uint)strListQuestion.size() <= kMaxQuestionExclamation))
605  {
606  strListEnd = strListQuestion;
607  strEnd = "?";
608  }
609  else if ((strListExcl.size() > 1) &&
610  ((uint)strListExcl.size() <= kMaxQuestionExclamation))
611  {
612  strListEnd = strListExcl;
613  strEnd = "!";
614  }
615  else
616  {
617  strEnd.clear();
618  }
619  }
620 
621  if (!strListEnd.empty())
622  {
623  QStringList strListSpace = strListEnd[0].split(
624  " ", Qt::SkipEmptyParts);
625  if (fColon && ((uint)strListSpace.size() > kMaxToTitle))
626  return;
627  if ((uint)strListSpace.size() > kDotToTitle)
628  return;
629  static const QRegularExpression ukExclusionFromSubtitle {
630  "(starring|stars\\s|drama|seres|sitcom)",
631  QRegularExpression::CaseInsensitiveOption };
632  if (strListSpace.filter(ukExclusionFromSubtitle).empty())
633  {
634  event.m_subtitle = strListEnd[0]+strEnd;
635  event.m_subtitle.remove(kUKSpaceColonStart);
636  event.m_description=
637  event.m_description.mid(strListEnd[0].length()+1);
638  event.m_description.remove(kUKSpaceColonStart);
639  }
640  }
641 }
642 
643 
648 {
649  static const QRegularExpression uk24ep { R"(^\d{1,2}:00[ap]m to \d{1,2}:00[ap]m: )" };
650  static const QRegularExpression ukTime { R"(\d{1,2}[\.:]\d{1,2}\s*(am|pm|))" };
651  QString strFull;
652 
653  bool isMovie = event.m_category.startsWith("Movie",Qt::CaseInsensitive) ||
654  event.m_category.startsWith("Film",Qt::CaseInsensitive);
655  // BBC three case (could add another record here ?)
656  static const QRegularExpression ukThen { R"(\s*?(Then|Followed by) 60 Seconds\.)",
657  QRegularExpression::CaseInsensitiveOption };
658  static const QRegularExpression ukNew { R"((New\.|\s*?(Brand New|New)\s*?(Series|Episode)\s*?[:\.\-]))",
659  QRegularExpression::CaseInsensitiveOption };
660  static const QRegularExpression ukNewTitle { R"(^(Brand New|New:)\s*)",
661  QRegularExpression::CaseInsensitiveOption };
662  event.m_description = event.m_description.remove(ukThen);
663  event.m_description = event.m_description.remove(ukNew);
664  event.m_title = event.m_title.remove(ukNewTitle);
665 
666  // Removal of Class TV, CBBC and CBeebies etc..
667  static const QRegularExpression ukTitleRemove { "^(?:[tT]4:|Schools\\s*?:)" };
668  static const QRegularExpression ukDescriptionRemove { R"(^(?:CBBC\s*?\.|CBeebies\s*?\.|Class TV\s*?:|BBC Switch\.))" };
669  event.m_title = event.m_title.remove(ukTitleRemove);
670  event.m_description = event.m_description.remove(ukDescriptionRemove);
671 
672  // Removal of BBC FOUR and BBC THREE
673  static const QRegularExpression ukBBC34 { R"(BBC (?:THREE|FOUR) on BBC (?:ONE|TWO)\.)",
674  QRegularExpression::CaseInsensitiveOption };
675  event.m_description = event.m_description.remove(ukBBC34);
676 
677  // BBC 7 [Rpt of ...] case.
678  static const QRegularExpression ukBBC7rpt { R"(\[Rptd?[^]]+?\d{1,2}\.\d{1,2}[ap]m\]\.)" };
679  event.m_description = event.m_description.remove(ukBBC7rpt);
680 
681  // "All New To 4Music!
682  static const QRegularExpression ukAllNew { R"(All New To 4Music!\s?)" };
683  event.m_description = event.m_description.remove(ukAllNew);
684 
685  // Removal of 'Also in HD' text
686  static const QRegularExpression ukAlsoInHD { R"(\s*Also in HD\.)",
687  QRegularExpression::CaseInsensitiveOption };
688  event.m_description = event.m_description.remove(ukAlsoInHD);
689 
690  // Remove [AD,S] etc.
691  static const QRegularExpression ukCC { R"(\[(?:(AD|SL|S|W|HD),?)+\])" };
692  auto match = ukCC.match(event.m_description);
693  while (match.hasMatch())
694  {
695  QStringList tmpCCitems = match.captured(0).remove("[").remove("]").split(",");
696  if (tmpCCitems.contains("AD"))
697  event.m_audioProps |= AUD_VISUALIMPAIR;
698  if (tmpCCitems.contains("HD"))
699  event.m_videoProps |= VID_HDTV;
700  if (tmpCCitems.contains("S"))
701  event.m_subtitleType |= SUB_NORMAL;
702  if (tmpCCitems.contains("SL"))
703  event.m_subtitleType |= SUB_SIGNED;
704  if (tmpCCitems.contains("W"))
705  event.m_videoProps |= VID_WIDESCREEN;
706  event.m_description.remove(match.capturedStart(0),
707  match.capturedLength(0));
708  match = ukCC.match(event.m_description, match.capturedStart(0));
709  }
710 
711  event.m_title = event.m_title.trimmed();
712  event.m_description = event.m_description.trimmed();
713 
714  // Constituents of UK season regexp, decomposed for clarity
715 
716  // Matches Season 2, S 2 and "Series 2," etc but not "hits 2"
717  // cap1 = season
718  static const QString seasonStr = R"(\b(?:Season|Series|S)\s*(\d+)\s*,?)";
719 
720  // Work out the season and episode numbers (if any)
721  // Matching pattern "Season 2 Episode|Ep 3 of 14|3/14" etc
722 
723  // Matches Episode 3, Ep 3/4, Ep 3 of 4 etc but not "step 1"
724  // cap1 = ep, cap2 = total
725  static const QString longEp = R"(\b(?:Ep|Episode)\s*(\d+)\s*(?:(?:/|of)\s*(\d*))?)";
726 
727  // Matches S2 Ep 3/4, "Season 2, Ep 3 of 4", Episode 3 etc
728  // cap1 = season, cap2 = ep, cap3 = total
729  static const QString longSeasEp = QString("\\(?(?:%1)?\\s*%2").arg(seasonStr, longEp);
730 
731  // Matches long seas/ep with surrounding parenthesis & trailing period
732  // cap1 = season, cap2 = ep, cap3 = total
733  static const QString longContext = QString(R"(\(*%1\s*\)?\s*\.?)").arg(longSeasEp);
734 
735  // Matches 3/4, 3 of 4
736  // cap1 = ep, cap2 = total
737  static const QString shortEp = R"((\d+)\s*(?:/|of)\s*(\d+))";
738 
739  // Matches short ep/total, ignoring Parts and idioms such as 9/11, 24/7 etc.
740  // ie. x/y in parenthesis or has no leading or trailing text in the sentence.
741  // cap0 may include previous/anchoring period
742  // cap1 = shortEp with surrounding parenthesis & trailing period (to remove)
743  // cap2 = ep, cap3 = total,
744  static const QString shortContext =
745  QString(R"((?:^|\.)(\s*\(*\s*%1[\s)]*(?:[).:]|$)))").arg(shortEp);
746 
747  // Prefer long format resorting to short format
748  // cap0 = long match to remove, cap1 = long season, cap2 = long ep, cap3 = long total,
749  // cap4 = short match to remove, cap5 = short ep, cap6 = short total
750  static const QRegularExpression ukSeries { "(?:" + longContext + "|" + shortContext + ")",
751  QRegularExpression::CaseInsensitiveOption };
752 
753  bool series = false;
754  bool fromTitle = true;
755  match = ukSeries.match(event.m_title);
756  if (!match.hasMatch())
757  {
758  fromTitle = false;
759  match = ukSeries.match(event.m_description);
760  }
761  if (match.hasMatch())
762  {
763  if (!match.captured(1).isEmpty())
764  {
765  event.m_season = match.captured(1).toUInt();
766  series = true;
767  }
768 
769  if (!match.captured(2).isEmpty())
770  {
771  event.m_episode = match.captured(2).toUInt();
772  series = true;
773  }
774  else if (!match.captured(5).isEmpty())
775  {
776  event.m_episode = match.captured(5).toUInt();
777  series = true;
778  }
779 
780  if (!match.captured(3).isEmpty())
781  {
782  event.m_totalepisodes = match.captured(3).toUInt();
783  series = true;
784  }
785  else if (!match.captured(6).isEmpty())
786  {
787  event.m_totalepisodes = match.captured(6).toUInt();
788  series = true;
789  }
790 
791  // Remove long or short match. Short text doesn't start at position2
792  int form = match.captured(4).isEmpty() ? 0 : 4;
793 
794  if (fromTitle)
795  {
796  LOG(VB_EIT, LOG_DEBUG, QString("Extracted S%1E%2/%3 from title (%4) \"%5\"")
797  .arg(event.m_season).arg(event.m_episode).arg(event.m_totalepisodes)
798  .arg(event.m_title, event.m_description));
799 
800  event.m_title.remove(match.capturedStart(form),
801  match.capturedLength(form));
802  }
803  else
804  {
805  LOG(VB_EIT, LOG_DEBUG, QString("Extracted S%1E%2/%3 from description (%4) \"%5\"")
806  .arg(event.m_season).arg(event.m_episode).arg(event.m_totalepisodes)
807  .arg(event.m_title, event.m_description));
808 
809  if (match.capturedStart(form) == 0)
810  {
811  // Remove from the start of the description.
812  // Otherwise it ends up in the subtitle.
813  event.m_description.remove(match.capturedStart(form),
814  match.capturedLength(form));
815  }
816  }
817  }
818 
819  if (isMovie)
820  event.m_categoryType = ProgramInfo::kCategoryMovie;
821  else if (series)
822  event.m_categoryType = ProgramInfo::kCategorySeries;
823 
824  // Multi-part episodes, or films (e.g. ITV film split by news)
825  // Matches Part 1, Pt 1/2, Part 1 of 2 etc.
826  static const QRegularExpression ukPart { R"([-(\:,.]\s*(?:Part|Pt)\s*(\d+)\s*(?:(?:of|/)\s*(\d+))?\s*[-):,.])",
827  QRegularExpression::CaseInsensitiveOption };
828  match = ukPart.match(event.m_title);
829  auto match2 = ukPart.match(event.m_description);
830  if (match.hasMatch())
831  {
832  event.m_partnumber = match.captured(1).toUInt();
833  event.m_parttotal = match.captured(2).toUInt();
834 
835  LOG(VB_EIT, LOG_DEBUG, QString("Extracted Part %1/%2 from title (%3)")
836  .arg(event.m_partnumber).arg(event.m_parttotal).arg(event.m_title));
837 
838  // Remove from the title
839  event.m_title.remove(match.capturedStart(0),
840  match.capturedLength(0));
841  }
842  else if (match2.hasMatch())
843  {
844  event.m_partnumber = match2.captured(1).toUInt();
845  event.m_parttotal = match2.captured(2).toUInt();
846 
847  LOG(VB_EIT, LOG_DEBUG, QString("Extracted Part %1/%2 from description (%3) \"%4\"")
848  .arg(event.m_partnumber).arg(event.m_parttotal)
849  .arg(event.m_title, event.m_description));
850 
851  // Remove from the start of the description.
852  // Otherwise it ends up in the subtitle.
853  if (match2.capturedStart(0) == 0)
854  {
855  // Retain a single colon (subtitle separator) if we remove any
856  QString sub = match2.captured(0).contains(":") ? ":" : "";
857  event.m_description = event.m_description.replace(match2.captured(0), sub);
858  }
859  }
860 
861  static const QRegularExpression ukStarring { R"((?:Western\s)?[Ss]tarring ([\w\s\-']+?)[Aa]nd\s([\w\s\-']+?)[\.|,]\s*(\d{4})?(?:\.\s)?)" };
862  match = ukStarring.match(event.m_description);
863  if (match.hasMatch())
864  {
865  // if we match this we've captured 2 actors and an (optional) airdate
866  /* Possible TODO: if EIT inlcude the priority and/or character
867  * names for the actors, include them in AddPerson call. */
868  event.AddPerson(DBPerson::kActor, match.captured(1));
869  event.AddPerson(DBPerson::kActor, match.captured(2));
870  if (match.captured(3).length() > 0)
871  {
872  bool ok = false;
873  uint y = match.captured(3).toUInt(&ok);
874  if (ok)
875  {
876  event.m_airdate = y;
877  event.m_originalairdate = QDate(y, 1, 1);
878  }
879  }
880  }
881 
882  static const QRegularExpression ukLaONoSplit { "^Law & Order: (?:Criminal Intent|LA|"
883  "Special Victims Unit|Trial by Jury|UK|You the Jury)" };
884  if (!event.m_title.startsWith("CSI:") && !event.m_title.startsWith("CD:") &&
885  !event.m_title.contains(ukLaONoSplit) &&
886  !event.m_title.startsWith("Mission: Impossible"))
887  {
888  static const QRegularExpression ukDoubleDotStart { R"(^\.\.+)" };
889  static const QRegularExpression ukDoubleDotEnd { R"(\.\.+$)" };
890  if ((event.m_title.indexOf(ukDoubleDotEnd) != -1) &&
891  (event.m_description.indexOf(ukDoubleDotStart) != -1))
892  {
893  QString strPart=event.m_title.remove(ukDoubleDotEnd)+" ";
894  strFull = strPart + event.m_description.remove(ukDoubleDotStart);
895  static const QRegularExpression ukCEPQ { R"([:\!\.\?]\s)" };
896  static const QRegularExpression ukSpaceStart { "^ " };
897  int position1 = strFull.indexOf(ukCEPQ,strPart.length());
898  if (isMovie && (position1 != -1))
899  {
900  if (strFull[position1] == '!' || strFull[position1] == '?'
901  || (position1>2 && strFull[position1] == '.' && strFull[position1-2] == '.'))
902  position1++;
903  event.m_title = strFull.left(position1);
904  event.m_description = strFull.mid(position1 + 1);
905  event.m_description.remove(ukSpaceStart);
906  }
907  else
908  {
909  position1 = strFull.indexOf(ukCEPQ);
910  if (position1 != -1)
911  {
912  if (strFull[position1] == '!' || strFull[position1] == '?'
913  || (position1>2 && strFull[position1] == '.' && strFull[position1-2] == '.'))
914  position1++;
915  event.m_title = strFull.left(position1);
916  event.m_description = strFull.mid(position1 + 1);
917  event.m_description.remove(ukSpaceStart);
918  SetUKSubtitle(event);
919  }
920  }
921  }
922  else if (event.m_description.indexOf(uk24ep) != -1)
923  {
924  auto match24 = uk24ep.match(event.m_description);
925  if (match24.hasMatch())
926  {
927  // Special case for episodes of 24.
928  // -2 from the length cause we don't want ": " on the end
929  event.m_subtitle = event.m_description.mid(match24.capturedStart(0),
930  match24.captured(0).length() - 2);
931  event.m_description = event.m_description.remove(match24.captured(0));
932  }
933  }
934  else if (event.m_description.indexOf(ukTime) == -1)
935  {
936  static const QRegularExpression ukYearColon { R"(^[\d]{4}:)" };
937  if (!isMovie && (event.m_title.indexOf(ukYearColon) < 0))
938  {
939  int position1 = event.m_title.indexOf(":");
940  if ((position1 != -1) &&
941  (event.m_description.indexOf(":") < 0 ))
942  {
943  static const QRegularExpression ukCompleteDots { R"(^\.\.+$)" };
944  if (event.m_title.mid(position1+1).indexOf(ukCompleteDots)==0)
945  {
946  SetUKSubtitle(event);
947  QString strTmp = event.m_title.mid(position1+1);
948  event.m_title.resize(position1);
949  event.m_subtitle = strTmp+event.m_subtitle;
950  }
951  else if ((uint)position1 < kSubtitleMaxLen)
952  {
953  event.m_subtitle = event.m_title.mid(position1 + 1);
954  event.m_title = event.m_title.left(position1);
955  }
956  }
957  else
958  {
959  SetUKSubtitle(event);
960  }
961  }
962  }
963  }
964 
965  if (!isMovie && event.m_subtitle.isEmpty() &&
966  !event.m_title.startsWith("The X-Files"))
967  {
968  int position1 = event.m_description.indexOf(ukTime);
969  if (position1 != -1)
970  {
971  static const QRegularExpression ukColonPeriod { R"([:\.])" };
972  int position2 = event.m_description.indexOf(ukColonPeriod);
973  if ((position2>=0) && (position2 < (position1-2)))
974  SetUKSubtitle(event);
975  }
976  else
977  {
978  position1 = event.m_title.indexOf("-");
979  if (position1 != -1)
980  {
981  if ((uint)position1 < kSubtitleMaxLen)
982  {
983  event.m_subtitle = event.m_title.mid(position1 + 1);
984  event.m_subtitle.remove(kUKSpaceColonStart);
985  event.m_title = event.m_title.left(position1);
986  }
987  }
988  else
989  {
990  SetUKSubtitle(event);
991  }
992  }
993  }
994 
995  // Work out the year (if any)
996  static const QRegularExpression ukYear { R"([\[\(]([\d]{4})[\)\]])" };
997  match = ukYear.match(event.m_description);
998  if (match.hasMatch())
999  {
1000  event.m_description.remove(match.capturedStart(0),
1001  match.capturedLength(0));
1002  bool ok = false;
1003  uint y = match.captured(1).toUInt(&ok);
1004  if (ok)
1005  {
1006  event.m_airdate = y;
1007  event.m_originalairdate = QDate(y, 1, 1);
1008  }
1009  }
1010 
1011  // Trim leading/trailing '.'
1012  static const QRegularExpression ukDotSpaceStart { R"(^\. )" };
1013  static const QRegularExpression ukDotEnd { R"(\.$)" };
1014  event.m_subtitle.remove(ukDotSpaceStart);
1015  if (event.m_subtitle.lastIndexOf("..") != (event.m_subtitle.length()-2))
1016  event.m_subtitle.remove(ukDotEnd);
1017 
1018  // Reverse the subtitle and empty description
1019  if (event.m_description.isEmpty() && !event.m_subtitle.isEmpty())
1020  {
1021  event.m_description=event.m_subtitle;
1022  event.m_subtitle.clear();
1023  }
1024 }
1025 
1030 {
1031  /* Used for PBS ATSC Subtitles are separated by a colon */
1032  int position = event.m_description.indexOf(':');
1033  if (position != -1)
1034  {
1035  const QString stmp = event.m_description;
1036  event.m_subtitle = stmp.left(position);
1037  event.m_description = stmp.right(stmp.length() - position - 2);
1038  }
1039 }
1040 
1044 void EITFixUp::FixComHem(DBEventEIT &event, bool process_subtitle)
1045 {
1046  static const QRegularExpression comHemPersSeparator { R"((, |\soch\s))" };
1047 
1048  // Reverse what EITFixUp::Fix() did
1049  if (event.m_subtitle.isEmpty() && !event.m_description.isEmpty())
1050  {
1051  event.m_subtitle = event.m_description;
1052  event.m_description = "";
1053  }
1054 
1055  // Remove subtitle, it contains the category and we already know that
1056  event.m_subtitle = "";
1057 
1058  bool isSeries = false;
1059  // Try to find episode numbers
1060  static const QRegularExpression comHemSeries1
1061  { R"(\s?(?:[dD]el|[eE]pisode)\s([0-9]+)(?:\s?(?:/|:|av)\s?([0-9]+))?\.)" };
1062  static const QRegularExpression comHemSeries2 { R"(\s?-?\s?([Dd]el\s+([0-9]+)))" };
1063  auto match = comHemSeries1.match(event.m_description);
1064  auto match2 = comHemSeries2.match(event.m_title);
1065  if (match2.hasMatch())
1066  {
1067  event.m_partnumber = match2.capturedView(2).toUInt();
1068  event.m_title.remove(match2.capturedStart(), match2.capturedLength());
1069  }
1070  else if (match.hasMatch())
1071  {
1072  if (match.capturedStart(1) != -1)
1073  event.m_partnumber = match.capturedView(1).toUInt();
1074  if (match.capturedStart(2) != -1)
1075  event.m_parttotal = match.capturedView(2).toUInt();
1076 
1077  // Remove the episode numbers, but only if it's not at the begining
1078  // of the description (subtitle code might use it)
1079  if (match.capturedStart() > 0)
1080  event.m_description.remove(match.capturedStart(),
1081  match.capturedLength());
1082  isSeries = true;
1083  }
1084 
1085  // Add partnumber/parttotal to subtitle
1086  // This will be overwritten if we find a better subtitle
1087  if (event.m_partnumber > 0)
1088  {
1089  event.m_subtitle = QString("Del %1").arg(event.m_partnumber);
1090  if (event.m_parttotal > 0)
1091  event.m_subtitle += QString(" av %1").arg(event.m_parttotal);
1092  }
1093 
1094  // Move subtitle info from title to subtitle
1095  static const QRegularExpression comHemTSub { R"(\s+-\s+([^\-]+))" };
1096  match = comHemTSub.match(event.m_title);
1097  if (match.hasMatch())
1098  {
1099  event.m_subtitle = match.captured(1);
1100  event.m_title.remove(match.capturedStart(), match.capturedLength());
1101  }
1102 
1103  // No need to continue without a description.
1104  if (event.m_description.length() <= 0)
1105  return;
1106 
1107  // Try to find country category, year and possibly other information
1108  // from the begining of the description
1109  static const QRegularExpression comHemCountry
1110  { R"(^(\(.+\))?\s?([^ ]+)\s([^\.0-9]+)\sfrån\s([0-9]{4})(?:\smed\s([^\.]+))?\.?)" };
1111  match = comHemCountry.match(event.m_description);
1112  if (match.hasMatch())
1113  {
1114  QString replacement;
1115 
1116  // Original title, usually english title
1117  // note: list[1] contains extra () around the text that needs removing
1118  if (!match.capturedView(1).isEmpty())
1119  {
1120  replacement = match.captured(1) + " ";
1121  //store it somewhere?
1122  }
1123 
1124  // Countr(y|ies)
1125  if (!match.capturedView(2).isEmpty())
1126  {
1127  replacement += match.captured(2) + " ";
1128  //store it somewhere?
1129  }
1130 
1131  // Category
1132  if (!match.capturedView(3).isEmpty())
1133  {
1134  replacement += match.captured(3) + ".";
1135  if(event.m_category.isEmpty())
1136  {
1137  event.m_category = match.captured(3);
1138  }
1139 
1140  if(match.captured(3).indexOf("serie")!=-1)
1141  {
1142  isSeries = true;
1143  }
1144  }
1145 
1146  // Year
1147  if (!match.capturedView(4).isEmpty())
1148  {
1149  bool ok = false;
1150  uint y = match.capturedView(4).trimmed().toUInt(&ok);
1151  if (ok)
1152  event.m_airdate = y;
1153  }
1154 
1155  // Actors
1156  if (!match.capturedView(5).isEmpty())
1157  {
1158  const QStringList actors =
1159  match.captured(5).split(comHemPersSeparator, Qt::SkipEmptyParts);
1160  /* Possible TODO: if EIT inlcude the priority and/or character
1161  * names for the actors, include them in AddPerson call. */
1162  for (const auto & actor : std::as_const(actors))
1163  event.AddPerson(DBPerson::kActor, actor);
1164  }
1165 
1166  // Remove year and actors.
1167  // The reason category is left in the description is because otherwise
1168  // the country would look wierd like "Amerikansk. Rest of description."
1169  event.m_description = event.m_description.replace(match.captured(0),replacement);
1170  }
1171 
1172  if (isSeries)
1173  event.m_categoryType = ProgramInfo::kCategorySeries;
1174 
1175  // Look for additional persons in the description
1176  static const QRegularExpression comHemPersons
1177  { R"(\s?([Rr]egi|[Ss]kådespelare|[Pp]rogramledare|[Ii] rollerna):\s([^\.]+)\.)" };
1178  auto iter = comHemPersons.globalMatch(event.m_description);
1179  while (iter.hasNext())
1180  {
1181  auto pmatch = iter.next();
1183 
1184  static const QRegularExpression comHemDirector { "[Rr]egi" };
1185  static const QRegularExpression comHemActor { "[Ss]kådespelare|[Ii] rollerna" };
1186  static const QRegularExpression comHemHost { "[Pp]rogramledare" };
1187  auto dmatch = comHemDirector.match(pmatch.capturedView(1));
1188  auto amatch = comHemActor.match(pmatch.capturedView(1));
1189  auto hmatch = comHemHost.match(pmatch.capturedView(1));
1190  if (dmatch.hasMatch())
1191  role = DBPerson::kDirector;
1192  else if (amatch.hasMatch())
1193  role = DBPerson::kActor;
1194  else if (hmatch.hasMatch())
1195  role = DBPerson::kHost;
1196  else
1197  {
1198  event.m_description.remove(pmatch.capturedStart(), pmatch.capturedLength());
1199  continue;
1200  }
1201 
1202  const QStringList actors =
1203  pmatch.captured(2).split(comHemPersSeparator, Qt::SkipEmptyParts);
1204  /* Possible TODO: if EIT inlcude the priority and/or character
1205  * names for the actors, include them in AddPerson call. */
1206  for (const auto & actor : std::as_const(actors))
1207  event.AddPerson(role, actor);
1208 
1209  // Remove it
1210  event.m_description=event.m_description.replace(pmatch.captured(0),"");
1211  }
1212 
1213  // Is this event on a channel we shoud look for a subtitle?
1214  // The subtitle is the first sentence in the description, but the
1215  // subtitle can't be the only thing in the description and it must be
1216  // shorter than 55 characters or we risk picking up the wrong thing.
1217  if (process_subtitle)
1218  {
1219  static const QRegularExpression comHemSub { R"([.\?\!] )" };
1220  int pos2 = event.m_description.indexOf(comHemSub);
1221  bool pvalid = pos2 != -1 && pos2 <= 55;
1222  if (pvalid && (event.m_description.length() - (pos2 + 2)) > 0)
1223  {
1224  event.m_subtitle = event.m_description.left(
1225  pos2 + (event.m_description[pos2] == '?' ? 1 : 0));
1226  event.m_description = event.m_description.mid(pos2 + 2);
1227  }
1228  }
1229 
1230  // Teletext subtitles?
1231  static const QRegularExpression comHemTT { "[Tt]ext-[Tt][Vv]" };
1232  if (event.m_description.indexOf(comHemTT) != -1)
1233  event.m_subtitleType |= SUB_NORMAL;
1234 
1235  // Try to findout if this is a rerun and if so the date.
1236  static const QRegularExpression comHemRerun1 { R"([Rr]epris\sfrån\s([^\.]+)(?:\.|$))" };
1237  static const QRegularExpression comHemRerun2 { R"(([0-9]+)/([0-9]+)(?:\s-\s([0-9]{4}))?)" };
1238  match = comHemRerun1.match(event.m_description);
1239  if (!match.hasMatch())
1240  return;
1241 
1242  // Rerun from today
1243  if (match.captured(1) == "i dag")
1244  {
1245  event.m_originalairdate = event.m_starttime.date();
1246  return;
1247  }
1248 
1249  // Rerun from yesterday afternoon
1250  if (match.captured(1) == "eftermiddagen")
1251  {
1252  event.m_originalairdate = event.m_starttime.date().addDays(-1);
1253  return;
1254  }
1255 
1256  // Rerun with day, month and possibly year specified
1257  match2 = comHemRerun2.match(match.capturedView(1));
1258  if (match2.hasMatch())
1259  {
1260  int day = match2.capturedView(1).toInt();
1261  int month = match2.capturedView(2).toInt();
1262  //int year;
1263  //if (match2.capturedLength(3) > 0)
1264  // year = match2.capturedView(3).toInt();
1265  //else
1266  // year = event.m_starttime.date().year();
1267 
1268  if (day > 0 && month > 0)
1269  {
1270  QDate date(event.m_starttime.date().year(), month, day);
1271  // it's a rerun so it must be in the past
1272  if (date > event.m_starttime.date())
1273  date = date.addYears(-1);
1274  event.m_originalairdate = date;
1275  }
1276  return;
1277  }
1278 }
1279 
1284 {
1285  event.m_category = event.m_subtitle;
1286  /* Used for DVB-S Subtitles are separated by a colon */
1287  int position = event.m_description.indexOf(':');
1288  if (position != -1)
1289  {
1290  const QString stmp = event.m_description;
1291  event.m_subtitle = stmp.left(position);
1292  event.m_description = stmp.right(stmp.length() - position - 2);
1293  }
1294 }
1295 
1300 {
1301  if (event.m_description.startsWith("[Program data ") || event.m_description.startsWith("[Program info "))//TEN
1302  {
1303  event.m_description = "";//event.m_subtitle;
1304  }
1305  if (event.m_description.endsWith("Copyright West TV Ltd. 2011)"))
1306  event.m_description.resize(event.m_description.length()-40);
1307 
1308  if (event.m_description.isEmpty() && !event.m_subtitle.isEmpty())//due to ten's copyright info, this won't be caught before
1309  {
1310  event.m_description = event.m_subtitle;
1311  event.m_subtitle.clear();
1312  }
1313  if (event.m_description.startsWith(event.m_title+" - "))
1314  event.m_description.remove(0,event.m_title.length()+3);
1315  if (event.m_title.startsWith("LIVE: ", Qt::CaseInsensitive))
1316  {
1317  event.m_title.remove(0, 6);
1318  event.m_description.prepend("(Live) ");
1319  }
1320 }
1321 
1326 {
1327  static const QRegularExpression rating { "\\((G|PG|M|MA)\\)" };
1328  auto match = rating.match(event.m_description);
1329  if (match.hasMatch())
1330  {
1331  EventRating prograting;
1332  prograting.m_system="AU"; prograting.m_rating = match.captured(1);
1333  event.m_ratings.push_back(prograting);
1334  event.m_description.remove(0,match.capturedLength()+1);
1335  }
1336  if (event.m_description.startsWith("[HD]"))
1337  {
1338  event.m_videoProps |= VID_HDTV;
1339  event.m_description.remove(0,5);
1340  }
1341  if (event.m_description.startsWith("[CC]"))
1342  {
1343  event.m_subtitleType |= SUB_NORMAL;
1344  event.m_description.remove(0,5);
1345  }
1346  if (event.m_subtitle == "Movie")
1347  {
1348  event.m_subtitle.clear();
1349  event.m_categoryType = ProgramInfo::kCategoryMovie;
1350  }
1351  if (event.m_description.startsWith(event.m_title))
1352  event.m_description.remove(0,event.m_title.length()+1);
1353 }
1354 
1359 {
1360  if (event.m_description.endsWith(" Rpt"))
1361  {
1362  event.m_previouslyshown = true;
1363  event.m_description.resize(event.m_description.size()-4);
1364  }
1365  static const QRegularExpression year { "(\\d{4})$" };
1366  auto match = year.match(event.m_description);
1367  if (match.hasMatch())
1368  {
1369  event.m_airdate = match.capturedView(1).toUInt();
1370  event.m_description.resize(event.m_description.size()-5);
1371  }
1372  if (event.m_description.endsWith(" CC"))
1373  {
1374  event.m_subtitleType |= SUB_NORMAL;
1375  event.m_description.resize(event.m_description.size()-3);
1376  }
1377  QString advisories;//store the advisories to append later
1378  static const QRegularExpression adv { "(\\([A-Z,]+\\))$" };
1379  match = adv.match(event.m_description);
1380  if (match.hasMatch())
1381  {
1382  advisories = match.captured(1);
1383  event.m_description.remove(match.capturedStart()-1, match.capturedLength()+1);
1384  }
1385  static const QRegularExpression rating { "(C|G|PG|M|MA)$" };
1386  match = rating.match(event.m_description);
1387  if (match.hasMatch())
1388  {
1389  EventRating prograting;
1390  prograting.m_system="AU"; prograting.m_rating = match.captured(1);
1391  if (!advisories.isEmpty())
1392  prograting.m_rating.append(" ").append(advisories);
1393  event.m_ratings.push_back(prograting);
1394  event.m_description.remove(match.capturedStart()-1, match.capturedLength()+1);
1395  }
1396 }
1401 {
1402  // If the description has been truncated to fit within the
1403  // 'subtitle' eit field, none of the following will work (ABC)
1404  if (event.m_description.endsWith(".."))
1405  return;
1406  event.m_description = event.m_description.trimmed();
1407 
1408  static const QRegularExpression auFreeviewSY { R"((.*) \((.+)\) \(([12][0-9][0-9][0-9])\)$)" };
1409  auto match = auFreeviewSY.match(event.m_description);
1410  if (match.hasMatch())
1411  {
1412  if (event.m_subtitle.isEmpty())//nine sometimes has an actual subtitle field and the brackets thingo)
1413  event.m_subtitle = match.captured(2);
1414  event.m_airdate = match.capturedView(3).toUInt();
1415  event.m_description = match.captured(1);
1416  return;
1417  }
1418  static const QRegularExpression auFreeviewY { "(.*) \\(([12][0-9][0-9][0-9])\\)$" };
1419  match = auFreeviewY.match(event.m_description);
1420  if (match.hasMatch())
1421  {
1422  event.m_airdate = match.capturedView(2).toUInt();
1423  event.m_description = match.captured(1);
1424  return;
1425  }
1426  static const QRegularExpression auFreeviewSYC { R"((.*) \((.+)\) \(([12][0-9][0-9][0-9])\) \((.+)\)$)" };
1427  match = auFreeviewSYC.match(event.m_description);
1428  if (match.hasMatch())
1429  {
1430  if (event.m_subtitle.isEmpty())
1431  event.m_subtitle = match.captured(2);
1432  event.m_airdate = match.capturedView(3).toUInt();
1433  QStringList actors = match.captured(4).split("/");
1434  /* Possible TODO: if EIT inlcude the priority and/or character
1435  * names for the actors, include them in AddPerson call. */
1436  for (const QString& actor : std::as_const(actors))
1437  event.AddPerson(DBPerson::kActor, actor);
1438  event.m_description = match.captured(1);
1439  return;
1440  }
1441  static const QRegularExpression auFreeviewYC { R"((.*) \(([12][0-9][0-9][0-9])\) \((.+)\)$)" };
1442  match = auFreeviewYC.match(event.m_description);
1443  if (match.hasMatch())
1444  {
1445  event.m_airdate = match.capturedView(2).toUInt();
1446  QStringList actors = match.captured(3).split("/");
1447  /* Possible TODO: if EIT inlcude the priority and/or character
1448  * names for the actors, include them in AddPerson call. */
1449  for (const QString& actor : std::as_const(actors))
1450  event.AddPerson(DBPerson::kActor, actor);
1451  event.m_description = match.captured(1);
1452  }
1453 }
1454 
1459 {
1460  const uint SUBTITLE_PCT = 60; // % of description to allow subtitle to
1461  const uint lSUBTITLE_MAX_LEN = 128;// max length of subtitle field in db.
1462 
1463  // Remove subtitle, it contains category information too specific to use
1464  event.m_subtitle = QString("");
1465 
1466  // No need to continue without a description.
1467  if (event.m_description.length() <= 0)
1468  return;
1469 
1470  // Replace incomplete title if the full one is in the description
1471  static const QRegularExpression mcaIncompleteTitle { R"((.*).\.\.\.$)" };
1472  auto match = mcaIncompleteTitle.match(event.m_title);
1473  if (match.hasMatch())
1474  {
1475  static const QString mcaCompleteTitlea { "^'?(" };
1476  static const QString mcaCompleteTitleb { R"([^\.\?]+[^\'])'?[\.\?]\s+(.+))" };
1477  static const QRegularExpression mcaCompleteTitle
1478  { mcaCompleteTitlea + match.captured(1) + mcaCompleteTitleb,
1479  QRegularExpression::CaseInsensitiveOption};
1480  match = mcaCompleteTitle.match(event.m_description);
1481  if (match.hasMatch())
1482  {
1483  event.m_title = match.captured(1).trimmed();
1484  event.m_description = match.captured(2).trimmed();
1485  }
1486  }
1487 
1488  // Try to find subtitle in description
1489  static const QRegularExpression mcaSubtitle { R"(^'([^\.]+)'\.\s+(.+))" };
1490  match = mcaSubtitle.match(event.m_description);
1491  if (match.hasMatch())
1492  {
1493  uint matchLen = match.capturedLength(1);
1494 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1495  uint evDescLen = std::max(event.m_description.length(), 1);
1496 #else
1497  uint evDescLen = std::max(event.m_description.length(), 1LL);
1498 #endif
1499 
1500  if ((matchLen < lSUBTITLE_MAX_LEN) &&
1501  ((matchLen * 100 / evDescLen) < SUBTITLE_PCT))
1502  {
1503  event.m_subtitle = match.captured(1);
1504  event.m_description = match.captured(2);
1505  }
1506  }
1507 
1508  // Try to find episode numbers in subtitle
1509  static const QRegularExpression mcaSeries { R"(^S?(\d+)\/E?(\d+)\s-\s(.*)$)" };
1510  match = mcaSeries.match(event.m_subtitle);
1511  if (match.hasMatch())
1512  {
1513  uint season = match.capturedView(1).toUInt();
1514  uint episode = match.capturedView(2).toUInt();
1515  event.m_subtitle = match.captured(3).trimmed();
1516  event.m_syndicatedepisodenumber =
1517  QString("S%1E%2").arg(season).arg(episode);
1518  event.m_season = season;
1519  event.m_episode = episode;
1520  event.m_categoryType = ProgramInfo::kCategorySeries;
1521  }
1522 
1523  // Closed captioned?
1524  static const QRegularExpression mcaCC { R"(,?\s(HI|English) Subtitles\.?)" };
1525  int position = event.m_description.indexOf(mcaCC);
1526  if (position > 0)
1527  {
1528  event.m_subtitleType |= SUB_HARDHEAR;
1529  event.m_description.remove(mcaCC);
1530  }
1531 
1532  // Dolby Digital 5.1?
1533  static const QRegularExpression mcaDD { R"(,?\sDD\.?)" };
1534  position = event.m_description.indexOf(mcaDD);
1535  if ((position > 0) && (position > event.m_description.length() - 7))
1536  {
1537  event.m_audioProps |= AUD_DOLBY;
1538  event.m_description.remove(mcaDD);
1539  }
1540 
1541  // Remove bouquet tags
1542  static const QRegularExpression mcaAvail { R"(\s(Only available on [^\.]*bouquet|Not available in RSA [^\.]*)\.?)" };
1543  event.m_description.remove(mcaAvail);
1544 
1545  // Try to find year and director from the end of the description
1546  bool isMovie = false;
1547  static const QRegularExpression mcaCredits { R"((.*)\s\((\d{4})\)\s*([^\.]+)\.?\s*$)" };
1548  match = mcaCredits.match(event.m_description);
1549  if (match.hasMatch())
1550  {
1551  isMovie = true;
1552  event.m_description = match.captured(1).trimmed();
1553  bool ok = false;
1554  uint y = match.captured(2).trimmed().toUInt(&ok);
1555  if (ok)
1556  event.m_airdate = y;
1557  event.AddPerson(DBPerson::kDirector, match.captured(3).trimmed());
1558  }
1559  else
1560  {
1561  // Try to find year only from the end of the description
1562  static const QRegularExpression mcaYear { R"((.*)\s\((\d{4})\)\s*$)" };
1563  match = mcaYear.match(event.m_description);
1564  if (match.hasMatch())
1565  {
1566  isMovie = true;
1567  event.m_description = match.captured(1).trimmed();
1568  bool ok = false;
1569  uint y = match.captured(2).trimmed().toUInt(&ok);
1570  if (ok)
1571  event.m_airdate = y;
1572  }
1573  }
1574 
1575  if (isMovie)
1576  {
1577  static const QRegularExpression mcaActors { R"((.*\.)\s+([^\.]+\s[A-Z][^\.]+)\.\s*)" };
1578  match = mcaActors.match(event.m_description);
1579  if (match.hasMatch())
1580  {
1581  static const QRegularExpression mcaActorsSeparator { "(,\\s+)" };
1582  const QStringList actors = match.captured(2).split(
1583  mcaActorsSeparator, Qt::SkipEmptyParts);
1584  /* Possible TODO: if EIT inlcude the priority and/or character
1585  * names for the actors, include them in AddPerson call. */
1586  for (const auto & actor : std::as_const(actors))
1587  event.AddPerson(DBPerson::kActor, actor.trimmed());
1588  event.m_description = match.captured(1).trimmed();
1589  }
1590  event.m_categoryType = ProgramInfo::kCategoryMovie;
1591  }
1592 }
1593 
1598 {
1599  // subtitle with episode number: "Folge *: 'subtitle'
1600  static const QRegularExpression superRTLSubtitle { R"(^Folge\s(\d{1,3}):\s'(.*)')" };
1601  auto match = superRTLSubtitle.match(event.m_subtitle);
1602  if (match.hasMatch())
1603  {
1604  event.m_season = 0;
1605  event.m_episode = match.capturedView(1).toUInt();
1606  event.m_subtitle = match.captured(2);
1607  }
1608 
1609  // No need to continue without a description or with an subtitle.
1610  if (event.m_description.length() <= 0 || event.m_subtitle.length() > 0)
1611  return;
1612 
1613  // Repeat
1614  static const QRegularExpression rtlRepeat
1615  { R"([\s\(]?Wiederholung.+vo[m|n].+(\d{2}\.\d{2}\.\d{4}|\d{2}[:\.]\d{2}\sUhr)\)?)" };
1616  match = rtlRepeat.match(event.m_description);
1617  if (match.hasMatch())
1618  {
1619  // remove '.' if it matches at the beginning of the description
1620  int pos = match.capturedStart(0);
1621  int length = match.capturedLength(0) + (pos ? 0 : 1);
1622  event.m_description = event.m_description.remove(pos, length).trimmed();
1623  }
1624 
1625  // should be (?:\x{8a}|\\.\\s*|$) but 0x8A gets replaced with 0x20
1626  static const QRegularExpression rtlSubtitle1 { R"(^Folge\s(\d{1,4})\s*:\s+'(.*)'(?:\s|\.\s*|$))" };
1627  static const QRegularExpression rtlSubtitle2 { R"(^Folge\s(\d{1,4})\s+(.{0,5}[^\?!\.]{0,120})[\?!\.]\s*)" };
1628  static const QRegularExpression rtlSubtitle3 { R"(^(?:Folge\s)?(\d{1,4}(?:\/[IVX]+)?)\s+(.{0,5}[^\?!\.]{0,120})[\?!\.]\s*)" };
1629  static const QRegularExpression rtlSubtitle4 { R"(^Thema.{0,5}:\s([^\.]+)\.\s*)" };
1630  static const QRegularExpression rtlSubtitle5 { "^'(.+)'\\.\\s*" };
1631  static const QRegularExpression rtlEpisodeNo1 { R"(^(Folge\s\d{1,4})\.*\s*)" };
1632  static const QRegularExpression rtlEpisodeNo2 { R"(^(\d{1,2}\/[IVX]+)\.*\s*)" };
1633 
1634  auto match1 = rtlSubtitle1.match(event.m_description);
1635  auto match2 = rtlSubtitle2.match(event.m_description);
1636  auto match3 = rtlSubtitle3.match(event.m_description);
1637  auto match4 = rtlSubtitle4.match(event.m_description);
1638  auto match5 = rtlSubtitle5.match(event.m_description);
1639  auto match6 = rtlEpisodeNo1.match(event.m_description);
1640  auto match7 = rtlEpisodeNo2.match(event.m_description);
1641 
1642  // subtitle with episode number: "Folge *: 'subtitle'. description
1643  if (match1.hasMatch())
1644  {
1645  event.m_syndicatedepisodenumber = match1.captured(1);
1646  event.m_subtitle = match1.captured(2);
1647  event.m_description =
1648  event.m_description.remove(0, match1.capturedLength());
1649  }
1650  // episode number subtitle
1651  else if (match2.hasMatch())
1652  {
1653  event.m_syndicatedepisodenumber = match2.captured(1);
1654  event.m_subtitle = match2.captured(2);
1655  event.m_description =
1656  event.m_description.remove(0, match2.capturedLength());
1657  }
1658  // episode number subtitle
1659  else if (match3.hasMatch())
1660  {
1661  event.m_syndicatedepisodenumber = match3.captured(1);
1662  event.m_subtitle = match3.captured(2);
1663  event.m_description =
1664  event.m_description.remove(0, match3.capturedLength());
1665  }
1666  // "Thema..."
1667  else if (match4.hasMatch())
1668  {
1669  event.m_subtitle = match4.captured(1);
1670  event.m_description =
1671  event.m_description.remove(0, match4.capturedLength());
1672  }
1673  // "'...'"
1674  else if (match5.hasMatch())
1675  {
1676  event.m_subtitle = match5.captured(1);
1677  event.m_description =
1678  event.m_description.remove(0, match5.capturedLength());
1679  }
1680  // episode number
1681  else if (match6.hasMatch())
1682  {
1683  event.m_syndicatedepisodenumber = match6.captured(2);
1684  event.m_subtitle = match6.captured(1);
1685  event.m_description =
1686  event.m_description.remove(0, match6.capturedLength());
1687  }
1688  // episode number
1689  else if (match7.hasMatch())
1690  {
1691  event.m_syndicatedepisodenumber = match7.captured(2);
1692  event.m_subtitle = match7.captured(1);
1693  event.m_description =
1694  event.m_description.remove(0, match7.capturedLength());
1695  }
1696 
1697  /* got an episode title now? (we did not have one at the start of this function) */
1698  if (!event.m_subtitle.isEmpty())
1700 
1701  /* if we do not have an episode title by now try some guessing as last resort */
1702  if (event.m_subtitle.length() == 0)
1703  {
1704  const uint SUBTITLE_PCT = 35; // % of description to allow subtitle up to
1705  const uint lSUBTITLE_MAX_LEN = 50; // max length of subtitle field in db
1706 
1707  static const QRegularExpression rtlSubtitle { R"(^([^\.]{3,})\.\s+(.+))" };
1708  match = rtlSubtitle.match(event.m_description);
1709  if (match.hasMatch())
1710  {
1711  uint matchLen = match.capturedLength(1);
1712 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1713  uint evDescLen = std::max(event.m_description.length(), 1);
1714 #else
1715  uint evDescLen = std::max(event.m_description.length(), 1LL);
1716 #endif
1717 
1718  if ((matchLen < lSUBTITLE_MAX_LEN) &&
1719  (matchLen * 100 / evDescLen < SUBTITLE_PCT))
1720  {
1721  event.m_subtitle = match.captured(1);
1722  event.m_description = match.captured(2);
1723  }
1724  }
1725  }
1726 }
1727 
1728 // FIXME add more jobs
1729 static const QMap<QString,DBPerson::Role> deCrewTitle {
1730  { "Regie", DBPerson::kDirector },
1731  { "Drehbuch", DBPerson::kWriter },
1732  { "Autor", DBPerson::kWriter },
1733 };
1734 
1739 {
1740  static const QRegularExpression pro7Subtitle { R"(,{0,1}([^,]*?),([^,]+?)\s{0,1}(\d{4})$)" };
1741  auto match = pro7Subtitle.match(event.m_subtitle);
1742  if (match.hasMatch())
1743  {
1744  if (event.m_airdate == 0)
1745  {
1746  event.m_airdate = match.captured(3).toUInt();
1747  }
1748  event.m_subtitle.remove(match.capturedStart(0),
1749  match.capturedLength(0));
1750  }
1751 
1752  /* handle cast, the very last in description */
1753  static const QRegularExpression pro7Cast { "\n\nDarsteller:\n(.*)$",
1754  QRegularExpression::DotMatchesEverythingOption };
1755  match = pro7Cast.match(event.m_description);
1756  if (match.hasMatch())
1757  {
1758  QStringList cast = match.captured(1).split("\n");
1759  for (const auto& line : std::as_const(cast))
1760  {
1761  static const QRegularExpression pro7CastOne { R"(^([^\(]*?)\((.*)\)$)" };
1762  auto match2 = pro7CastOne.match(line);
1763  if (match2.hasMatch())
1764  {
1765  /* Possible TODO: if EIT inlcude the priority and/or character
1766  * names for the actors, include them in AddPerson call. */
1767  event.AddPerson (DBPerson::kActor, match2.captured(1).simplified());
1768  }
1769  }
1770  event.m_description.remove(match.capturedStart(0),
1771  match.capturedLength(0));
1772  }
1773 
1774  /* handle crew, the new very last in description
1775  * format: "Role: Name" or "Role: Name1, Name2"
1776  */
1777  static const QRegularExpression pro7Crew { "\n\n(Regie:.*)$",
1778  QRegularExpression::DotMatchesEverythingOption };
1779  match = pro7Crew.match(event.m_description);
1780  if (match.hasMatch())
1781  {
1782  QStringList crew = match.captured(1).split("\n");
1783  for (const auto& line : std::as_const(crew))
1784  {
1785  static const QRegularExpression pro7CrewOne { R"(^(.*?):\s+(.*)$)" };
1786  auto match2 = pro7CrewOne.match(line);
1787  if (match2.hasMatch())
1788  {
1790  if (deCrewTitle.contains(match2.captured(1)))
1791  role = deCrewTitle[match2.captured(1)];
1792  QStringList names = match2.captured(2).simplified().split(R"(\s*,\s*)");
1793  for (const auto & name : std::as_const(names))
1794  {
1795  /* Possible TODO: if EIT inlcude the priority
1796  * and/or character names for the actors, include
1797  * them in AddPerson call. */
1798  event.AddPerson (role, name);
1799  }
1800  }
1801  }
1802  event.m_description.remove(match.capturedStart(0),
1803  match.capturedLength(0));
1804  }
1805 
1806  /* FIXME unless its Jamie Oliver, then there is neither Crew nor Cast only
1807  * \n\nKoch: Jamie Oliver
1808  */
1809 }
1810 
1815 {
1816  static const QRegularExpression deDisneyChannelSubtitle { R"(,([^,]+?)\s{0,1}(\d{4})$)" };
1817  auto match = deDisneyChannelSubtitle.match(event.m_subtitle);
1818  if (match.hasMatch())
1819  {
1820  if (event.m_airdate == 0)
1821  {
1822  event.m_airdate = match.captured(3).toUInt();
1823  }
1824  event.m_subtitle.remove(match.capturedStart(0),
1825  match.capturedLength(0));
1826  }
1827  static const QRegularExpression tmp { R"(\s[^\s]+?-(Serie))" };
1828  match = tmp.match(event.m_subtitle);
1829  if (match.hasMatch())
1830  {
1831  event.m_categoryType = ProgramInfo::kCategorySeries;
1832  event.m_category=match.captured(0).trimmed();
1833  event.m_subtitle.remove(match.capturedStart(0),
1834  match.capturedLength(0));
1835  }
1836 }
1837 
1842 {
1843  static const QRegularExpression atvSubtitle { R"(,{0,1}\sFolge\s(\d{1,3})$)" };
1844  event.m_subtitle.replace(atvSubtitle, "");
1845 }
1846 
1847 
1852 {
1853  static const QRegularExpression fiRerun { R"(\s?Uusinta[a-zA-Z\s]*\.?)" };
1854  auto match = fiRerun.match(event.m_description);
1855  if (match.hasMatch())
1856  {
1857  event.m_previouslyshown = true;
1858  event.m_description.remove(match.capturedStart(), match.capturedLength());
1859  }
1860 
1861  static const QRegularExpression fiRerun2 { R"(\([Uu]\))" };
1862  match = fiRerun2.match(event.m_description);
1863  if (match.hasMatch())
1864  {
1865  event.m_previouslyshown = true;
1866  event.m_description.remove(match.capturedStart(), match.capturedLength());
1867  }
1868 
1869  // Check for (Stereo) in the decription and set the <audio> tags
1870  match = kStereo.match(event.m_description);
1871  if (match.hasMatch())
1872  {
1873  event.m_audioProps |= AUD_STEREO;
1874  event.m_description.remove(match.capturedStart(), match.capturedLength());
1875  }
1876 
1877  // Remove age limit in parenthesis at end of title
1878  static const QRegularExpression fiAgeLimit { R"(\((\d{1,2}|[ST])\)$)" };
1879  match = fiAgeLimit.match(event.m_title);
1880  if (match.hasMatch())
1881  {
1882  EventRating prograting;
1883  prograting.m_system="FI"; prograting.m_rating = match.captured(1);
1884  event.m_ratings.push_back(prograting);
1885  event.m_title.remove(match.capturedStart(), match.capturedLength());
1886  }
1887 
1888  // Remove Film or Elokuva at start of title
1889  static const QRegularExpression fiFilm { "^(Film|Elokuva): " };
1890  match = fiFilm.match(event.m_title);
1891  if (match.hasMatch())
1892  {
1893  event.m_category = "Film";
1894  event.m_categoryType = ProgramInfo::kCategoryMovie;
1895  event.m_title.remove(match.capturedStart(), match.capturedLength());
1896  }
1897 }
1898 
1904 {
1905  QString country = "";
1906 
1907  static const QRegularExpression dePremiereLength { R"(\s?[0-9]+\sMin\.)" };
1908  event.m_description = event.m_description.replace(dePremiereLength, "");
1909 
1910  static const QRegularExpression dePremiereAirdate { R"(\s?([^\s^\.]+)\s((?:1|2)[0-9]{3})\.)" };
1911  auto match = dePremiereAirdate.match(event.m_description);
1912  if ( match.hasMatch())
1913  {
1914  country = match.captured(1).trimmed();
1915  bool ok = false;
1916  uint y = match.captured(2).toUInt(&ok);
1917  if (ok)
1918  event.m_airdate = y;
1919  event.m_description.remove(match.capturedStart(0),
1920  match.capturedLength(0));
1921  }
1922 
1923  static const QRegularExpression dePremiereCredits { R"(\sVon\s([^,]+)(?:,|\su\.\sa\.)\smit\s([^\.]*)\.)" };
1924  match = dePremiereCredits.match(event.m_description);
1925  if (match.hasMatch())
1926  {
1927  event.AddPerson(DBPerson::kDirector, match.captured(1));
1928  const QStringList actors = match.captured(2).split(
1929  ", ", Qt::SkipEmptyParts);
1930  /* Possible TODO: if EIT inlcude the priority and/or character
1931  * names for the actors, include them in AddPerson call. */
1932  for (const auto & actor : std::as_const(actors))
1933  event.AddPerson(DBPerson::kActor, actor);
1934  event.m_description.remove(match.capturedStart(0),
1935  match.capturedLength(0));
1936  }
1937 
1938  event.m_description = event.m_description.replace("\u000A$", "");
1939  event.m_description = event.m_description.replace("\u000A", " ");
1940 
1941  // move the original titel from the title to subtitle
1942  static const QRegularExpression dePremiereOTitle { R"(\s*\(([^\)]*)\)$)" };
1943  match = dePremiereOTitle.match(event.m_title);
1944  if (match.hasMatch())
1945  {
1946  event.m_subtitle = QString("%1, %2").arg(match.captured(1), country);
1947  event.m_title.remove(match.capturedStart(0),
1948  match.capturedLength(0));
1949  }
1950 
1951  // Find infos about season and episode number
1952  static const QRegularExpression deSkyDescriptionSeasonEpisode { R"(^(\d{1,2}).\sStaffel,\sFolge\s(\d{1,2}):\s)" };
1953  match = deSkyDescriptionSeasonEpisode.match(event.m_description);
1954  if (match.hasMatch())
1955  {
1956  event.m_season = match.captured(1).trimmed().toUInt();
1957  event.m_episode = match.captured(2).trimmed().toUInt();
1958  event.m_description.remove(match.capturedStart(0),
1959  match.capturedLength(0));
1960  }
1961 }
1962 
1963 /*
1964  * Mapping table from English category names to Dutch names and types
1965  */
1966 struct NLMapResult {
1967  QString name;
1969 };
1970 static const QMap<QString, NLMapResult> categoryTrans = {
1971  { "Documentary", { "Documentaire", ProgramInfo::kCategoryNone } },
1972  { "News", { "Nieuws/actualiteiten", ProgramInfo::kCategoryNone } },
1973  { "Kids", { "Jeugd", ProgramInfo::kCategoryNone } },
1974  { "Show/game Show", { "Amusement", ProgramInfo::kCategoryTVShow } },
1975  { "Music/Ballet/Dance", { "Muziek", ProgramInfo::kCategoryNone } },
1976  { "News magazine", { "Informatief", ProgramInfo::kCategoryNone } },
1977  { "Movie", { "Film", ProgramInfo::kCategoryMovie } },
1978  { "Nature/animals/Environment", { "Natuur", ProgramInfo::kCategoryNone } },
1979  { "Movie - Adult", { "Erotiek", ProgramInfo::kCategoryNone } },
1980  { "Movie - Soap/melodrama/folkloric",
1981  { "Serie/soap", ProgramInfo::kCategorySeries } },
1982  { "Arts/Culture", { "Kunst/Cultuur", ProgramInfo::kCategoryNone } },
1983  { "Sports", { "Sport", ProgramInfo::kCategorySports } },
1984  { "Cartoons/Puppets", { "Animatie", ProgramInfo::kCategoryNone } },
1985  { "Movie - Comedy", { "Comedy", ProgramInfo::kCategorySeries } },
1986  { "Movie - Detective/Thriller", { "Misdaad", ProgramInfo::kCategoryNone } },
1987  { "Social/Spiritual Sciences", { "Religieus", ProgramInfo::kCategoryNone } },
1988 };
1989 
1994 {
1995  QString fullinfo = event.m_subtitle + event.m_description;
1996  event.m_subtitle = "";
1997 
1998  // Convert categories to Dutch categories Myth knows.
1999  // nog invoegen: comedy, sport, misdaad
2000 
2001  if (categoryTrans.contains(event.m_category))
2002  {
2003  auto [name, type] = categoryTrans[event.m_category];
2004  event.m_category = name;
2005  event.m_categoryType = type;
2006  }
2007 
2008  // Film - categories are usually not Films
2009  if (event.m_category.startsWith("Film -"))
2010  event.m_categoryType = ProgramInfo::kCategorySeries;
2011 
2012  // Get stereo info
2013  auto match = kStereo.match(fullinfo);
2014  if (match.hasMatch())
2015  {
2016  event.m_audioProps |= AUD_STEREO;
2017  fullinfo.remove(match.capturedStart(), match.capturedLength());
2018  }
2019 
2020  //Get widescreen info
2021  static const QRegularExpression nlWide { "breedbeeld" };
2022  match = nlWide.match(fullinfo);
2023  if (match.hasMatch())
2024  {
2025  event.m_videoProps |= VID_WIDESCREEN;
2026  fullinfo = fullinfo.replace("breedbeeld", ".");
2027  }
2028 
2029  // Get repeat info
2030  static const QRegularExpression nlRepeat { "herh." };
2031  match = nlRepeat.match(fullinfo);
2032  if (match.hasMatch())
2033  fullinfo = fullinfo.replace("herh.", ".");
2034 
2035  // Get teletext subtitle info
2036  static const QRegularExpression nlTxt { "txt" };
2037  match = nlTxt.match(fullinfo);
2038  if (match.hasMatch())
2039  {
2040  event.m_subtitleType |= SUB_NORMAL;
2041  fullinfo = fullinfo.replace("txt", ".");
2042  }
2043 
2044  // Get HDTV information
2045  static const QRegularExpression nlHD { R"(\sHD$)" };
2046  match = nlHD.match(event.m_title);
2047  if (match.hasMatch())
2048  {
2049  event.m_videoProps |= VID_HDTV;
2050  event.m_title.remove(match.capturedStart(), match.capturedLength());
2051  }
2052 
2053  // Try to make subtitle from Afl.:
2054  static const QRegularExpression nlSub { R"(\sAfl\.:\s([^\.]+)\.)" };
2055  match = nlSub.match(fullinfo);
2056  if (match.hasMatch())
2057  {
2058  QString tmpSubString = match.captured(0);
2059  tmpSubString = tmpSubString.right(match.capturedLength() - 7);
2060  event.m_subtitle = tmpSubString.left(tmpSubString.length() -1);
2061  fullinfo.remove(match.capturedStart(), match.capturedLength());
2062  }
2063 
2064  // Try to make subtitle from " "
2065  static const QRegularExpression nlSub2 { R"(\s\"([^\"]+)\")" };
2066  match = nlSub2.match(fullinfo);
2067  if (match.hasMatch())
2068  {
2069  QString tmpSubString = match.captured(0);
2070  tmpSubString = tmpSubString.right(match.capturedLength() - 2);
2071  event.m_subtitle = tmpSubString.left(tmpSubString.length() -1);
2072  fullinfo.remove(match.capturedStart(), match.capturedLength());
2073  }
2074 
2075 
2076  // This is trying to catch the case where the subtitle is in the main title
2077  // but avoid cases where it isn't a subtitle e.g cd:uk
2078  int position = event.m_title.indexOf(":");
2079  if ((position != -1) &&
2080  (event.m_title[position + 1].toUpper() == event.m_title[position + 1]) &&
2081  (event.m_subtitle.isEmpty()))
2082  {
2083  event.m_subtitle = event.m_title.mid(position + 1);
2084  event.m_title = event.m_title.left(position);
2085  }
2086 
2087 
2088  // Get the actors
2089  static const QRegularExpression nlActors { R"(\sMet:\s.+e\.a\.)" };
2090  static const QRegularExpression nlPersSeparator { R"((, |\sen\s))" };
2091  match = nlActors.match(fullinfo);
2092  if (match.hasMatch())
2093  {
2094  QString tmpActorsString = match.captured(0);
2095  tmpActorsString = tmpActorsString.right(tmpActorsString.length() - 6);
2096  tmpActorsString = tmpActorsString.left(tmpActorsString.length() - 5);
2097  const QStringList actors =
2098  tmpActorsString.split(nlPersSeparator, Qt::SkipEmptyParts);
2099  /* Possible TODO: if EIT inlcude the priority and/or character
2100  * names for the actors, include them in AddPerson call. */
2101  for (const auto & actor : std::as_const(actors))
2102  event.AddPerson(DBPerson::kActor, actor);
2103  fullinfo.remove(match.capturedStart(), match.capturedLength());
2104  }
2105 
2106  // Try to find presenter
2107  static const QRegularExpression nlPres { R"(\sPresentatie:\s([^\.]+)\.)" };
2108  match = nlPres.match(fullinfo);
2109  if (match.hasMatch())
2110  {
2111  QString tmpPresString = match.captured(0);
2112  tmpPresString = tmpPresString.right(tmpPresString.length() - 14);
2113  tmpPresString = tmpPresString.left(tmpPresString.length() -1);
2114  const QStringList presenters =
2115  tmpPresString.split(nlPersSeparator, Qt::SkipEmptyParts);
2116  for (const auto & presenter : std::as_const(presenters))
2117  event.AddPerson(DBPerson::kPresenter, presenter);
2118  fullinfo.remove(match.capturedStart(), match.capturedLength());
2119  }
2120 
2121  // Try to find year
2122  static const QRegularExpression nlYear1 { R"(\suit\s([1-2][0-9]{3}))" };
2123  static const QRegularExpression nlYear2 { R"((\s\([A-Z]{0,3}/?)([1-2][0-9]{3})\))",
2124  QRegularExpression::CaseInsensitiveOption };
2125  match = nlYear1.match(fullinfo);
2126  if (match.hasMatch())
2127  {
2128  bool ok = false;
2129  uint y = match.capturedView(1).toUInt(&ok);
2130  if (ok)
2131  event.m_originalairdate = QDate(y, 1, 1);
2132  }
2133 
2134  match = nlYear2.match(fullinfo);
2135  if (match.hasMatch())
2136  {
2137  bool ok = false;
2138  uint y = match.capturedView(2).toUInt(&ok);
2139  if (ok)
2140  event.m_originalairdate = QDate(y, 1, 1);
2141  }
2142 
2143  // Try to find director
2144  static const QRegularExpression nlDirector { R"(\svan\s(([A-Z][a-z]+\s)|([A-Z]\.\s)))" };
2145  match = nlDirector.match(fullinfo);
2146  if (match.hasMatch())
2147  event.AddPerson(DBPerson::kDirector, match.captured(1));
2148 
2149  // Strip leftovers
2150  static const QRegularExpression nlRub { R"(\s?\(\W+\)\s?)" };
2151  fullinfo.remove(nlRub);
2152 
2153  // Strip category info from description
2154  static const QRegularExpression nlCat { "^(Amusement|Muziek|Informatief|Nieuws/actualiteiten|Jeugd|Animatie|Sport|Serie/soap|Kunst/Cultuur|Documentaire|Film|Natuur|Erotiek|Comedy|Misdaad|Religieus)\\.\\s" };
2155  fullinfo.remove(nlCat);
2156 
2157  // Remove omroep from title
2158  static const QRegularExpression nlOmroep { R"(\s\(([A-Z]+/?)+\)$)" };
2159  event.m_title.remove(nlOmroep);
2160 
2161  // Put information back in description
2162 
2163  event.m_description = fullinfo;
2164 }
2165 
2167 {
2168  // remove category movie from short events
2170  event.m_starttime.secsTo(event.m_endtime) < kMinMovieDuration)
2171  {
2172  /* default taken from ContentDescriptor::GetMythCategory */
2173  event.m_categoryType = ProgramInfo::kCategoryTVShow;
2174  }
2175 }
2176 
2181 {
2182  // Check for "title (R)" in the title
2183  static const QRegularExpression noRerun { "\\(R\\)" };
2184  auto match = noRerun.match(event.m_title);
2185  if (match.hasMatch())
2186  {
2187  event.m_previouslyshown = true;
2188  event.m_title.remove(match.capturedStart(), match.capturedLength());
2189  }
2190  // Check for "subtitle (HD)" in the subtitle
2191  static const QRegularExpression noHD { R"([\(\[]HD[\)\]])" };
2192  match = noHD.match(event.m_subtitle);
2193  if (match.hasMatch())
2194  {
2195  event.m_videoProps |= VID_HDTV;
2196  event.m_subtitle.remove(match.capturedStart(), match.capturedLength());
2197  }
2198  // Check for "description (HD)" in the description
2199  match = noHD.match(event.m_description);
2200  if (match.hasMatch())
2201  {
2202  event.m_videoProps |= VID_HDTV;
2203  event.m_description.remove(match.capturedStart(), match.capturedLength());
2204  }
2205 }
2206 
2211 {
2212  // Check for "title (R)" in the title
2213  static const QRegularExpression noRerun { "\\(R\\)" };
2214  auto match = noRerun.match(event.m_title);
2215  if (match.hasMatch())
2216  {
2217  event.m_previouslyshown = true;
2218  event.m_title.remove(match.capturedStart(), match.capturedLength());
2219  }
2220  // Check for "(R)" in the description
2221  match = noRerun.match(event.m_description);
2222  if (match.hasMatch())
2223  {
2224  event.m_previouslyshown = true;
2225  }
2226 
2227  // Move colon separated category from program-titles into description
2228  // Have seen "NRK2s historiekveld: Film: bla-bla"
2229  static const QRegularExpression noNRKCategories
2230  { "^(Superstrek[ea]r|Supersomm[ea]r|Superjul|Barne-tv|Fantorangen|Kuraffen|Supermorg[eo]n|Julemorg[eo]n|Sommermorg[eo]n|"
2231  "Kuraffen-TV|Sport i dag|NRKs sportsl.rdag|NRKs sportss.ndag|Dagens dokumentar|"
2232  "NRK2s historiekveld|Detektimen|Nattkino|Filmklassiker|Film|Kortfilm|P.skemorg[eo]n|"
2233  "Radioteatret|Opera|P2-Akademiet|Nyhetsmorg[eo]n i P2 og Alltid Nyheter:): (.+)" };
2234  match = noNRKCategories.match(event.m_title);
2235  if (match.hasMatch() && (match.capturedLength(2) > 1))
2236  {
2237  event.m_title = match.captured(2);
2238  event.m_description = "(" + match.captured(1) + ") " + event.m_description;
2239  }
2240 
2241  // Remove season premiere markings
2242  static const QRegularExpression noPremiere { "\\s+-\\s+(Sesongpremiere|Premiere|premiere)!?$" };
2243  match = noPremiere.match(event.m_title);
2244  if (match.hasMatch() && (match.capturedStart() >= 3))
2245  event.m_title.remove(match.capturedStart(), match.capturedLength());
2246 
2247  // Try to find colon-delimited subtitle in title, only tested for NRK channels
2248  if (!event.m_title.startsWith("CSI:") &&
2249  !event.m_title.startsWith("CD:") &&
2250  !event.m_title.startsWith("Distriktsnyheter: fra"))
2251  {
2252  static const QRegularExpression noColonSubtitle { "^([^:]+): (.+)" };
2253  match = noColonSubtitle.match(event.m_title);
2254  if (match.hasMatch())
2255  {
2256  if (event.m_subtitle.length() <= 0)
2257  {
2258  event.m_title = match.captured(1);
2259  event.m_subtitle = match.captured(2);
2260  }
2261  else if (event.m_subtitle == match.captured(2))
2262  {
2263  event.m_title = match.captured(1);
2264  }
2265  }
2266  }
2267 }
2268 
2273 {
2274  // Source: YouSee Rules of Operation v1.16
2275  // url: http://yousee.dk/~/media/pdf/CPE/Rules_Operation.ashx
2276  int episode = -1;
2277  int season = -1;
2278 
2279  // Title search
2280  // episode and part/part total
2281  static const QRegularExpression dkEpisode { R"(\(([0-9]+)\))" };
2282  auto match = dkEpisode.match(event.m_title);
2283  if (match.hasMatch())
2284  {
2285  episode = match.capturedView(1).toInt();
2286  event.m_partnumber = match.capturedView(1).toInt();
2287  event.m_title.remove(match.capturedStart(), match.capturedLength());
2288  }
2289 
2290  static const QRegularExpression dkPart { R"(\(([0-9]+):([0-9]+)\))" };
2291  match = dkPart.match(event.m_title);
2292  if (match.hasMatch())
2293  {
2294  episode = match.capturedView(1).toInt();
2295  event.m_partnumber = match.capturedView(1).toInt();
2296  event.m_parttotal = match.capturedView(2).toInt();
2297  event.m_title.remove(match.capturedStart(), match.capturedLength());
2298  }
2299 
2300  // subtitle delimiters
2301  static const QRegularExpression dkSubtitle1 { "^([^:]+): (.+)" };
2302  match = dkSubtitle1.match(event.m_title);
2303  if (match.hasMatch())
2304  {
2305  event.m_title = match.captured(1);
2306  event.m_subtitle = match.captured(2);
2307  }
2308  else
2309  {
2310  static const QRegularExpression dkSubtitle2 { "^([^:]+) - (.+)" };
2311  match = dkSubtitle2.match(event.m_title);
2312  if (match.hasMatch())
2313  {
2314  event.m_title = match.captured(1);
2315  event.m_subtitle = match.captured(2);
2316  }
2317  }
2318 
2319  // Description search
2320  // Season (Sæson [:digit:]+.) => episode = season episode number
2321  // or year (- år [:digit:]+(\\)|:) ) => episode = total episode number
2322  static const QRegularExpression dkSeason1 { "Sæson ([0-9]+)\\." };
2323  match = dkSeason1.match(event.m_description);
2324  if (match.hasMatch())
2325  {
2326  season = match.capturedView(1).toInt();
2327  }
2328  else
2329  {
2330  static const QRegularExpression dkSeason2 { "- år ([0-9]+) :" };
2331  match = dkSeason2.match(event.m_description);
2332  if (match.hasMatch())
2333  {
2334  season = match.capturedView(1).toInt();
2335  }
2336  }
2337 
2338  if (episode > 0)
2339  event.m_episode = episode;
2340 
2341  if (season > 0)
2342  event.m_season = season;
2343 
2344  //Feature:
2345  static const QRegularExpression dkFeatures { "Features:(.+)" };
2346  match = dkFeatures.match(event.m_description);
2347  if (match.hasMatch())
2348  {
2349  QString features = match.captured(1);
2350  event.m_description.remove(match.capturedStart(),
2351  match.capturedLength());
2352  // 16:9
2353  static const QRegularExpression dkWidescreen { " 16:9" };
2354  if (features.indexOf(dkWidescreen) != -1)
2355  event.m_videoProps |= VID_WIDESCREEN;
2356  // HDTV
2357  static const QRegularExpression dkHD { " HD" };
2358  if (features.indexOf(dkHD) != -1)
2359  event.m_videoProps |= VID_HDTV;
2360  // Dolby Digital surround
2361  static const QRegularExpression dkDolby { " 5:1" };
2362  if (features.indexOf(dkDolby) != -1)
2363  event.m_audioProps |= AUD_DOLBY;
2364  // surround
2365  static const QRegularExpression dkSurround { R"( \(\(S\)\))" };
2366  if (features.indexOf(dkSurround) != -1)
2367  event.m_audioProps |= AUD_SURROUND;
2368  // stereo
2369  static const QRegularExpression dkStereo { " S" };
2370  if (features.indexOf(dkStereo) != -1)
2371  event.m_audioProps |= AUD_STEREO;
2372  // (G)
2373  static const QRegularExpression dkReplay { " \\(G\\)" };
2374  if (features.indexOf(dkReplay) != -1)
2375  event.m_previouslyshown = true;
2376  // TTV
2377  static const QRegularExpression dkTxt { " TTV" };
2378  if (features.indexOf(dkTxt) != -1)
2379  event.m_subtitleType |= SUB_NORMAL;
2380  }
2381 
2382  // Series and program id
2383  // programid is currently not transmitted
2384  // YouSee doesn't use a default authority but uses the first byte after
2385  // the / to indicate if the seriesid is global unique or unique on the
2386  // service id
2387  if (event.m_seriesId.length() >= 1 && event.m_seriesId[0] == '/')
2388  {
2389  QString newid;
2390  if (event.m_seriesId[1] == '1')
2391  {
2392  newid = QString("%1%2").arg(event.m_chanid).
2393  arg(event.m_seriesId.mid(2,8));
2394  }
2395  else
2396  {
2397  newid = event.m_seriesId.mid(2,8);
2398  }
2399  event.m_seriesId = newid;
2400  }
2401 
2402  if (event.m_programId.length() >= 1 && event.m_programId[0] == '/')
2403  event.m_programId[0]='_';
2404 
2405  // Add season and episode number to subtitle
2406  if (episode > 0)
2407  {
2408  event.m_subtitle = QString("%1 (%2").arg(event.m_subtitle).arg(episode);
2409  if (event.m_parttotal >0)
2410  event.m_subtitle = QString("%1:%2").arg(event.m_subtitle).
2411  arg(event.m_parttotal);
2412  if (season > 0)
2413  {
2414  event.m_season = season;
2415  event.m_episode = episode;
2416  event.m_syndicatedepisodenumber =
2417  QString("S%1E%2").arg(season).arg(episode);
2418  event.m_subtitle = QString("%1 Sæson %2").arg(event.m_subtitle).
2419  arg(season);
2420  }
2421  event.m_subtitle = QString("%1)").arg(event.m_subtitle);
2422  }
2423 
2424  // Find actors and director in description
2425  static const QRegularExpression dkDirector { "(?:Instr.: |Instrukt.r: )(.+)$" };
2426  static const QRegularExpression dkPersonsSeparator { "(, )|(og )" };
2427  QStringList directors {};
2428  match = dkDirector.match(event.m_description);
2429  if (match.hasMatch())
2430  {
2431  QString tmpDirectorsString = match.captured(1);
2432  directors = tmpDirectorsString.split(dkPersonsSeparator, Qt::SkipEmptyParts);
2433  for (const auto & director : std::as_const(directors))
2434  {
2435  tmpDirectorsString = director.split(":").last().trimmed().
2436  remove(kDotAtEnd);
2437  if (tmpDirectorsString != "")
2438  event.AddPerson(DBPerson::kDirector, tmpDirectorsString);
2439  }
2440  //event.m_description.remove(match.capturedStart(), match.capturedLength());
2441  }
2442 
2443  static const QRegularExpression dkActors { "(?:Medvirkende: |Medv\\.: )(.+)" };
2444  match = dkActors.match(event.m_description);
2445  if (match.hasMatch())
2446  {
2447  QString tmpActorsString = match.captured(1);
2448  const QStringList actors =
2449  tmpActorsString.split(dkPersonsSeparator, Qt::SkipEmptyParts);
2450  for (const auto & actor : std::as_const(actors))
2451  {
2452  tmpActorsString = actor.split(":").last().trimmed().remove(kDotAtEnd);
2453  if (!tmpActorsString.isEmpty() && !directors.contains(tmpActorsString))
2454  event.AddPerson(DBPerson::kActor, tmpActorsString);
2455  }
2456  //event.m_description.remove(match.capturedStart(), match.capturedLength());
2457  }
2458 
2459  //find year
2460  static const QRegularExpression dkYear { " fra ([0-9]{4})[ \\.]" };
2461  match = dkYear.match(event.m_description);
2462  if (match.hasMatch())
2463  {
2464  bool ok = false;
2465  uint y = match.capturedView(1).toUInt(&ok);
2466  if (ok)
2467  event.m_originalairdate = QDate(y, 1, 1);
2468  }
2469 }
2470 
2475 {
2476  LOG(VB_EIT, LOG_INFO, QString("Applying html strip to %1").arg(event.m_title));
2477  static const QRegularExpression html { "</?EM>", QRegularExpression::CaseInsensitiveOption };
2478  event.m_title.remove(html);
2479 }
2480 
2481 // Moves the subtitle field into the description since it's just used
2482 // as more description field. All the sort-out will happen in the description
2483 // field. Also, sometimes the description is just a repeat of the title. If so,
2484 // we remove it.
2486 {
2487  if (event.m_title == event.m_description)
2488  {
2489  event.m_description = QString("");
2490  }
2491  if (!event.m_subtitle.isEmpty())
2492  {
2493  if (event.m_subtitle.trimmed().right(1) != ".'" )
2494  event.m_subtitle = event.m_subtitle.trimmed() + ".";
2495  event.m_description = event.m_subtitle.trimmed() + QString(" ") + event.m_description;
2496  event.m_subtitle = QString("");
2497  }
2498 }
2499 
2501 {
2502  // Program ratings
2503  static const QRegularExpression grRating { R"(\[(K|Κ|8|12|16|18)\]\s*)",
2504  QRegularExpression::CaseInsensitiveOption };
2505  auto match = grRating.match(event.m_title);
2506  if (match.hasMatch())
2507  {
2508  EventRating prograting;
2509  prograting.m_system="GR"; prograting.m_rating = match.captured(1);
2510  event.m_ratings.push_back(prograting);
2511  event.m_title.remove(match.capturedStart(), match.capturedLength());
2512  event.m_title = event.m_title.trimmed();
2513  }
2514 
2515  //Live show
2516  int position = event.m_title.indexOf("(Ζ)");
2517  if (position != -1)
2518  {
2519  event.m_title = event.m_title.replace("(Ζ)", "");
2520  event.m_description.prepend("Ζωντανή Μετάδοση. ");
2521  }
2522 
2523  // Greek not previously Shown
2524  static const QRegularExpression grNotPreviouslyShown {
2525  R"(\W?(?:-\s*)*(?:\b[Α1]['΄η]?\s*(?:τηλεοπτικ[ηή]\s*)?(?:μετ[αά]δοση|προβολ[ηή]))\W?)",
2526  QRegularExpression::CaseInsensitiveOption|QRegularExpression::UseUnicodePropertiesOption };
2527  match = grNotPreviouslyShown.match(event.m_title);
2528  if (match.hasMatch())
2529  {
2530  event.m_previouslyshown = false;
2531  event.m_title.remove(match.capturedStart(), match.capturedLength());
2532  }
2533 
2534  // Greek Replay (Ε)
2535  // it might look redundant compared to previous check but at least it helps
2536  // remove the (Ε) From the title.
2537  static const QRegularExpression grReplay { R"(\([ΕE]\))" };
2538  match = grReplay.match(event.m_title);
2539  if (match.hasMatch())
2540  {
2541  event.m_previouslyshown = true;
2542  event.m_title.remove(match.capturedStart(), match.capturedLength());
2543  }
2544 
2545  // Check for (HD) in the decription
2546  position = event.m_description.indexOf("(HD)");
2547  if (position != -1)
2548  {
2549  event.m_description = event.m_description.replace("(HD)", "");
2550  event.m_videoProps |= VID_HDTV;
2551  }
2552 
2553  // Check for (Full HD) in the decription
2554  position = event.m_description.indexOf("(Full HD)");
2555  if (position != -1)
2556  {
2557  event.m_description = event.m_description.replace("(Full HD)", "");
2558  event.m_videoProps |= VID_HDTV;
2559  }
2560 
2561  static const QRegularExpression grFixnofullstopActors { R"(\w\s(Παίζουν:|Πρωταγων))" };
2562  match = grFixnofullstopActors.match(event.m_description);
2563  if (match.hasMatch())
2564  event.m_description.insert(match.capturedStart() + 1, ".");
2565 
2566  // If they forgot the "." at the end of the sentence before the actors/directors begin, let's insert it.
2567  static const QRegularExpression grFixnofullstopDirectors { R"(\w\s(Σκηνοθ[εέ]))" };
2568  match = grFixnofullstopDirectors.match(event.m_description);
2569  if (match.hasMatch())
2570  event.m_description.insert(match.capturedStart() + 1, ".");
2571 
2572  // Find actors and director in description
2573  // I am looking for actors first and then for directors/presenters because
2574  // sometimes punctuation is missing and the "Παίζουν:" label is mistaken
2575  // for a director's/presenter's surname (directors/presenters are shown
2576  // before actors in the description field.). So removing the text after
2577  // adding the actors AND THEN looking for dir/pres helps to clear things up.
2578  static const QRegularExpression grActors { R"((?:[Ππ]α[ιί]ζουν:|[ΜMμ]ε τους:|Πρωταγωνιστο[υύ]ν:|Πρωταγωνιστε[ιί]:?)(?:\s+στο ρόλο(?: του| της)?\s(?:\w+\s[οη]\s))?([-\w\s']+(?:,[-\w\s']+)*)(?:κ\.[αά])?\W?)" };
2579  // cap(1) actors, just names
2580  static const QRegularExpression grPeopleSeparator { R"(([,-]\s+))" };
2581  match = grActors.match(event.m_description);
2582  if (match.hasMatch())
2583  {
2584  QString tmpActorsString = match.captured(1);
2585  const QStringList actors =
2586  tmpActorsString.split(grPeopleSeparator, Qt::SkipEmptyParts);
2587  for (const auto & actor : std::as_const(actors))
2588  {
2589  tmpActorsString = actor.split(":").last().trimmed().remove(kDotAtEnd);
2590  if (tmpActorsString != "")
2591  event.AddPerson(DBPerson::kActor, tmpActorsString);
2592  }
2593  event.m_description.remove(match.capturedStart(), match.capturedLength());
2594  }
2595 
2596  // Director
2597  static const QRegularExpression grDirector { R"((?:Σκηνοθεσία: |Σκηνοθέτης: |Σκηνοθέτης - Επιμέλεια: )(\w+\s\w+\s?)(?:\W?))" };
2598  match = grDirector.match(event.m_description);
2599  if (match.hasMatch())
2600  {
2601  QString tmpDirectorsString = match.captured(1);
2602  const QStringList directors =
2603  tmpDirectorsString.split(grPeopleSeparator, Qt::SkipEmptyParts);
2604  for (const auto & director : std::as_const(directors))
2605  {
2606  tmpDirectorsString = director.split(":").last().trimmed().
2607  remove(kDotAtEnd);
2608  if (tmpDirectorsString != "")
2609  {
2610  event.AddPerson(DBPerson::kDirector, tmpDirectorsString);
2611  }
2612  }
2613  event.m_description.remove(match.capturedStart(), match.capturedLength());
2614  }
2615 
2616  //Try to find presenter
2617  static const QRegularExpression grPres { R"((?:Παρουσ[ιί]αση:(?:\b)*|Παρουσι[αά]ζ(?:ουν|ει)(?::|\sο|\sη)|Παρουσι[αά]στ(?:[ηή]ς|ρια|ριες|[εέ]ς)(?::|\sο|\sη)|Με τ(?:ον |ην )(?:[\s|:|ο|η])*(?:\b)*)([-\w\s]+(?:,[-\w\s]+)*)\W?)",
2618  QRegularExpression::CaseInsensitiveOption|QRegularExpression::UseUnicodePropertiesOption };
2619  match = grPres.match(event.m_description);
2620  if (match.hasMatch())
2621  {
2622  QString tmpPresentersString = match.captured(1);
2623  const QStringList presenters =
2624  tmpPresentersString.split(grPeopleSeparator, Qt::SkipEmptyParts);
2625  for (const auto & presenter : std::as_const(presenters))
2626  {
2627  tmpPresentersString = presenter.split(":").last().trimmed().
2628  remove(kDotAtEnd);
2629  if (tmpPresentersString != "")
2630  {
2631  event.AddPerson(DBPerson::kPresenter, tmpPresentersString);
2632  }
2633  }
2634  event.m_description.remove(match.capturedStart(), match.capturedLength());
2635  }
2636 
2637  //find year e.g Παραγωγής 1966 ή ΝΤΟΚΙΜΑΝΤΕΡ - 1998 Κατάλληλο για όλους
2638  // Used in Private channels (not 'secret', just not owned by Government!)
2639  static const QRegularExpression grYear { R"(\W?(?:\s?παραγωγ[ηή]ς|\s?-|,)\s*([1-2][0-9]{3})(?:-\d{1,4})?)",
2640  QRegularExpression::CaseInsensitiveOption };
2641  match = grYear.match(event.m_description);
2642  if (match.hasMatch())
2643  {
2644  bool ok = false;
2645  uint y = match.capturedView(1).toUInt(&ok);
2646  if (ok)
2647  {
2648  event.m_originalairdate = QDate(y, 1, 1);
2649  event.m_description.remove(match.capturedStart(), match.capturedLength());
2650  }
2651  }
2652  // Remove " ."
2653  event.m_description = event.m_description.replace(" .",".").trimmed();
2654 
2655  //find country of origin and remove it from description.
2656  static const QRegularExpression grCountry {
2657  R"((?:\W|\b)(?:(ελλην|τουρκ|αμερικ[αά]ν|γαλλ|αγγλ|βρεττ?αν|γερμαν|ρωσσ?|ιταλ|ελβετ|σουηδ|ισπαν|πορτογαλ|μεξικ[αά]ν|κιν[εέ]ζικ|ιαπων|καναδ|βραζιλι[αά]ν)(ικ[ηή][ςσ])))",
2658  QRegularExpression::CaseInsensitiveOption|QRegularExpression::UseUnicodePropertiesOption };
2659  match = grCountry.match(event.m_description);
2660  if (match.hasMatch())
2661  event.m_description.remove(match.capturedStart(), match.capturedLength());
2662 
2663  // Work out the season and episode numbers (if any)
2664  // Matching pattern "Επεισ[όο]διο:?|Επ 3 από 14|3/14" etc
2665  bool series = false;
2666  static const QRegularExpression grSeason {
2667  R"((?:\W-?)*(?:\(-\s*)?\b(([Α-Ω|A|B|E|Z|H|I|K|M|N]{1,2})(?:'|΄)?|(\d{1,2})(?:ος|ου|oς|os)?)(?:\s*[ΚκKk][υύ]κλο(?:[σς]|υ))\s?)",
2668  QRegularExpression::CaseInsensitiveOption|QRegularExpression::UseUnicodePropertiesOption };
2669  // cap(2) is the season for ΑΒΓΔ
2670  // cap(3) is the season for 1234
2671  match = grSeason.match(event.m_title);
2672  if (match.hasMatch())
2673  {
2674  if (!match.capturedView(2).isEmpty()) // we found a letter representing a number
2675  {
2676  //sometimes Nat. TV writes numbers as letters, i.e Α=1, Β=2, Γ=3, etc
2677  //must convert them to numbers.
2678  int tmpinteger = match.capturedView(2).toUInt();
2679  if (tmpinteger < 1)
2680  {
2681  if (match.captured(2) == "ΣΤ") // 6, don't ask!
2682  event.m_season = 6;
2683  else
2684  {
2685  static const QString LettToNumber = "0ΑΒΓΔΕ6ΖΗΘΙΚΛΜΝ";
2686  tmpinteger = LettToNumber.indexOf(match.capturedView(2));
2687  if (tmpinteger != -1)
2688  event.m_season = tmpinteger;
2689  else
2690  //sometimes they use english letters instead of greek. Compensating:
2691  {
2692  static const QString LettToNumber2 = "0ABΓΔE6ZHΘIKΛMN";
2693  tmpinteger = LettToNumber2.indexOf(match.capturedView(2));
2694  if (tmpinteger != -1)
2695  event.m_season = tmpinteger;
2696  }
2697  }
2698  }
2699  }
2700  else if (!match.capturedView(3).isEmpty()) //number
2701  {
2702  event.m_season = match.capturedView(3).toUInt();
2703  }
2704  series = true;
2705  event.m_title.remove(match.capturedStart(), match.capturedLength());
2706  }
2707 
2708  // I have to search separately for season in title and description because it wouldn't work when in both.
2709  match = grSeason.match(event.m_description);
2710  if (match.hasMatch())
2711  {
2712  if (!match.capturedView(2).isEmpty()) // we found a letter representing a number
2713  {
2714  //sometimes Nat. TV writes numbers as letters, i.e Α=1, Β=2, Γ=3, etc
2715  //must convert them to numbers.
2716  int tmpinteger = match.capturedView(2).toUInt();
2717  if (tmpinteger < 1)
2718  {
2719  if (match.captured(2) == "ΣΤ") // 6, don't ask!
2720  event.m_season = 6;
2721  else
2722  {
2723  static const QString LettToNumber = "0ΑΒΓΔΕ6ΖΗΘΙΚΛΜΝ";
2724  tmpinteger = LettToNumber.indexOf(match.capturedView(2));
2725  if (tmpinteger != -1)
2726  event.m_season = tmpinteger;
2727  }
2728  }
2729  }
2730  else if (!match.capturedView(3).isEmpty()) //number
2731  {
2732  event.m_season = match.capturedView(3).toUInt();
2733  }
2734  series = true;
2735  event.m_description.remove(match.capturedStart(), match.capturedLength());
2736  }
2737 
2738 
2739  // If Season is in Roman Numerals (I,II,etc)
2740  static const QRegularExpression grSeasonAsRomanNumerals { ",\\s*([MDCLXVIΙΧ]+)$",
2741  QRegularExpression::CaseInsensitiveOption };
2742  match = grSeasonAsRomanNumerals.match(event.m_title);
2743  auto match2 = grSeasonAsRomanNumerals.match(event.m_description);
2744  if (match.hasMatch())
2745  {
2746  if (!match.capturedView(1).isEmpty()) //number
2747  event.m_season = parseRoman(match.captured(1).toUpper());
2748  series = true;
2749  event.m_title.remove(match.capturedStart(), match.capturedLength());
2750  event.m_title = event.m_title.trimmed();
2751  if (event.m_title.right(1) == ",")
2752  event.m_title.chop(1);
2753  }
2754  else if (match2.hasMatch())
2755  {
2756  if (!match2.capturedView(1).isEmpty()) //number
2757  event.m_season = parseRoman(match2.captured(1).toUpper());
2758  series = true;
2759  event.m_description.remove(match2.capturedStart(), match2.capturedLength());
2760  event.m_description = event.m_description.trimmed();
2761  if (event.m_description.right(1) == ",")
2762  event.m_description.chop(1);
2763  }
2764 
2765  static const QRegularExpression grlongEp { R"(\b(?:Επ.|επεισ[οό]διο:?)\s*(\d+)\W?)",
2766  QRegularExpression::CaseInsensitiveOption|QRegularExpression::UseUnicodePropertiesOption };
2767  // cap(1) is the Episode No.
2768  match = grlongEp.match(event.m_title);
2769  match2 = grlongEp.match(event.m_description);
2770  if (match.hasMatch() || match2.hasMatch())
2771  {
2772  if (!match.capturedView(1).isEmpty())
2773  {
2774  event.m_episode = match.capturedView(1).toUInt();
2775  series = true;
2776  event.m_title.remove(match.capturedStart(), match.capturedLength());
2777  }
2778  else if (!match2.capturedView(1).isEmpty())
2779  {
2780  event.m_episode = match2.capturedView(1).toUInt();
2781  series = true;
2782  event.m_description.remove(match2.capturedStart(), match2.capturedLength());
2783  }
2784  // Sometimes description omits Season if it's 1. We fix this
2785  if (0 == event.m_season)
2786  event.m_season = 1;
2787  }
2788 
2789  // Sometimes, especially on greek national tv, they include comments in the
2790  // title, e.g "connection to ert1", "ert archives".
2791  // Because they obscure the real title, I'll isolate and remove them.
2792 
2793  static const QRegularExpression grCommentsinTitle { R"(\(([Α-Ωα-ω\s\d-]+)\)(?:\s*$)*)" };
2794  // cap1 = real title
2795  // cap0 = real title in parentheses.
2796  match = grCommentsinTitle.match(event.m_title);
2797  if (match.hasMatch()) // found in title instead
2798  event.m_title.remove(match.capturedStart(), match.capturedLength());
2799 
2800  // Sometimes the real (mostly English) title of a movie or series is
2801  // enclosed in parentheses in the event title, subtitle or description.
2802  // Since the subtitle has been moved to the description field by
2803  // EITFixUp::FixGreekSubtitle, I will search for it only in the description.
2804  // It will replace the translated one to get better chances of metadata
2805  // retrieval. The old title will be moved in the description.
2806  static const QRegularExpression grRealTitleInDescription { R"(^\(([A-Za-z\s\d-]+)\)\s*)" };
2807  // cap1 = real title
2808  // cap0 = real title in parentheses.
2809  match = grRealTitleInDescription.match(event.m_description);
2810  if (match.hasMatch())
2811  {
2812  event.m_description.remove(0, match.capturedLength());
2813  if (match.captured(0) != event.m_title.trimmed())
2814  {
2815  event.m_description = "(" + event.m_title.trimmed() + "). " + event.m_description;
2816  }
2817  event.m_title = match.captured(1);
2818  // Remove the real title from the description
2819  }
2820  else // search in title
2821  {
2822  static const QRegularExpression grRealTitleInTitle { R"(\(([A-Za-z\s\d-]+)\)(?:\s*$)?)" };
2823  // cap1 = real title
2824  // cap0 = real title in parentheses.
2825  match = grRealTitleInTitle.match(event.m_title);
2826  if (match.hasMatch()) // found in title instead
2827  {
2828  event.m_title.remove(match.capturedStart(), match.capturedLength());
2829  QString tmpTranslTitle = event.m_title;
2830  //QString tmpTranslTitle = event.m_title.replace(tmptitle.cap(0),"");
2831  event.m_title = match.captured(1);
2832  event.m_description = "(" + tmpTranslTitle.trimmed() + "). " + event.m_description;
2833  }
2834  }
2835 
2836  // Description field: "^Episode: Lion in the cage. (Description follows)"
2837  static const QRegularExpression grEpisodeAsSubtitle { R"(^Επεισ[οό]διο:\s?([\w\s\-,']+)\.\s?)" };
2838  match = grEpisodeAsSubtitle.match(event.m_description);
2839  if (match.hasMatch())
2840  {
2841  event.m_subtitle = match.captured(1).trimmed();
2842  event.m_description.remove(match.capturedStart(), match.capturedLength());
2843  }
2844  static const QRegularExpression grMovie { R"(\bταιν[ιί]α\b)",
2845  QRegularExpression::CaseInsensitiveOption|QRegularExpression::UseUnicodePropertiesOption };
2846  bool isMovie = (event.m_description.indexOf(grMovie) !=-1) ;
2847  if (isMovie)
2848  event.m_categoryType = ProgramInfo::kCategoryMovie;
2849  else if (series)
2850  event.m_categoryType = ProgramInfo::kCategorySeries;
2851  // clear double commas.
2852  event.m_description.replace(",,", ",");
2853 
2854 // να σβήσω τα κομμάτια που περισσεύουν από την περιγραφή πχ παραγωγής χχχχ
2855 }
2856 
2858 {
2859  struct grCategoryEntry {
2860  QRegularExpression expr;
2861  QString category;
2862  };
2863  static const QRegularExpression grCategFood { "\\W?(?:εκπομπ[ηή]\\W)?(Γαστρονομ[ιί]α[σς]?|μαγειρικ[ηή][σς]?|chef|συνταγ[εέηή]|διατροφ|wine|μ[αά]γειρα[σς]?)\\W?",
2864  QRegularExpression::CaseInsensitiveOption };
2865  static const QRegularExpression grCategDrama { "\\W?(κοινωνικ[ηήό]|δραματικ[ηή]|δρ[αά]μα)\\W(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?",
2866  QRegularExpression::CaseInsensitiveOption};
2867  static const QRegularExpression grCategComedy { "\\W?(κωμικ[ηήοό]|χιουμοριστικ[ηήοό]|κωμωδ[ιί]α)\\W(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?",
2868  QRegularExpression::CaseInsensitiveOption};
2869  static const QRegularExpression grCategChildren { "\\W?(παιδικ[ηήοό]|κινο[υύ]μ[εέ]ν(ων|α)\\sσχ[εέ]δ[ιί](ων|α))\\W(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?",
2870  QRegularExpression::CaseInsensitiveOption};
2871  static const QRegularExpression grCategMystery { "(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?\\W?(μυστηρ[ιί]ου)\\W?",
2872  QRegularExpression::CaseInsensitiveOption};
2873  static const QRegularExpression grCategFantasy { "(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?\\W?(φαντασ[ιί]ας)\\W?",
2874  QRegularExpression::CaseInsensitiveOption};
2875  static const QRegularExpression grCategHistory { "\\W?(ιστορικ[ηήοό])\\W?(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?",
2876  QRegularExpression::CaseInsensitiveOption};
2877  static const QRegularExpression grCategTeleMag { "\\W?(ενημερωτικ[ηή]|ψυχαγωγικ[ηή]|τηλεπεριοδικ[οό]|μαγκαζ[ιί]νο)\\W?(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?",
2878  QRegularExpression::CaseInsensitiveOption};
2879  static const QRegularExpression grCategTeleShop { "\\W?(οδηγ[οό][σς]?\\sαγορ[ωώ]ν|τηλεπ[ωώ]λ[ηή]σ|τηλεαγορ|τηλεμ[αά]ρκετ|telemarket)\\W?(?:(?:εκπομπ[ηή]|σειρ[αά]|ταιν[ιί]α)\\W)?",
2880  QRegularExpression::CaseInsensitiveOption};
2881  static const QRegularExpression grCategGameShow { "\\W?(τηλεπαιχν[ιί]δι|quiz)\\W?",
2882  QRegularExpression::CaseInsensitiveOption};
2883  static const QRegularExpression grCategDocumentary { "\\W?(ντοκ[ιυ]μαντ[εέ]ρ)\\W?",
2884  QRegularExpression::CaseInsensitiveOption};
2885  static const QRegularExpression grCategBiography { "\\W?(βιογραφ[ιί]α|βιογραφικ[οό][σς]?)\\W?",
2886  QRegularExpression::CaseInsensitiveOption};
2887  static const QRegularExpression grCategNews { "\\W?(δελτ[ιί]ο\\W?|ειδ[ηή]σε(ι[σς]|ων))\\W?",
2888  QRegularExpression::CaseInsensitiveOption};
2889  static const QRegularExpression grCategSports { "\\W?(champion|αθλητικ[αάοόηή]|πρωτ[αά]θλημα|ποδ[οό]σφαιρο(ου)?|κολ[υύ]μβηση|πατιν[αά]ζ|formula|μπ[αά]σκετ|β[οό]λε[ιϊ])\\W?",
2890  QRegularExpression::CaseInsensitiveOption};
2891  static const QRegularExpression grCategMusic { "\\W?(μουσικ[οόηή]|eurovision|τραγο[υύ]δι)\\W?",
2892  QRegularExpression::CaseInsensitiveOption};
2893  static const QRegularExpression grCategReality { "\\W?(ρι[αά]λιτι|reality)\\W?",
2894  QRegularExpression::CaseInsensitiveOption};
2895  static const QRegularExpression grCategReligion { "\\W?(θρησκε[ιί]α|θρησκευτικ|να[οό][σς]?|θε[ιί]α λειτουργ[ιί]α)\\W?",
2896  QRegularExpression::CaseInsensitiveOption};
2897  static const QRegularExpression grCategCulture { "\\W?(τ[εέ]χν(η|ε[σς])|πολιτισμ)\\W?",
2898  QRegularExpression::CaseInsensitiveOption};
2899  static const QRegularExpression grCategNature { "\\W?(φ[υύ]ση|περιβ[αά]λλο|κατασκευ|επιστ[ηή]μ(?!ονικ[ηή]ς φαντασ[ιί]ας))\\W?",
2900  QRegularExpression::CaseInsensitiveOption};
2901  static const QRegularExpression grCategSciFi { "\\W?(επιστ(.|ημονικ[ηή]ς)\\s?φαντασ[ιί]ας)\\W?",
2902  QRegularExpression::CaseInsensitiveOption};
2903  static const QRegularExpression grCategHealth { "\\W?(υγε[ιί]α|υγειιν|ιατρικ|διατροφ)\\W?",
2904  QRegularExpression::CaseInsensitiveOption};
2905  static const QRegularExpression grCategSpecial { "\\W?(αφι[εέ]ρωμα)\\W?",
2906  QRegularExpression::CaseInsensitiveOption};
2907  static const QList<grCategoryEntry> grCategoryDescData = {
2908  { grCategComedy, "Κωμωδία" },
2909  { grCategTeleMag, "Τηλεπεριοδικό" },
2910  { grCategNature, "Επιστήμη/Φύση" },
2911  { grCategHealth, "Υγεία" },
2912  { grCategReality, "Ριάλιτι" },
2913  { grCategDrama, "Κοινωνικό" },
2914  { grCategChildren, "Παιδικό" },
2915  { grCategSciFi, "Επιστ.Φαντασίας" },
2916  { grCategMystery, "Μυστηρίου" },
2917  { grCategFantasy, "Φαντασίας" },
2918  { grCategHistory, "Ιστορικό" },
2919  { grCategTeleShop, "Τηλεπωλήσεις" },
2920  { grCategFood, "Γαστρονομία" },
2921  { grCategGameShow, "Τηλεπαιχνίδι" },
2922  { grCategBiography, "Βιογραφία" },
2923  { grCategSports, "Αθλητικά" },
2924  { grCategMusic, "Μουσική" },
2925  { grCategDocumentary, "Ντοκιμαντέρ" },
2926  { grCategReligion, "Θρησκεία" },
2927  { grCategCulture, "Τέχνες/Πολιτισμός" },
2928  { grCategSpecial, "Αφιέρωμα" },
2929  };
2930  static const QList<grCategoryEntry> grCategoryTitleData = {
2931  { grCategTeleShop, "Τηλεπωλήσεις" },
2932  { grCategGameShow, "Τηλεπαιχνίδι" },
2933  { grCategMusic, "Μουσική" },
2934  { grCategNews, "Ειδήσεις" },
2935  };
2936 
2937  // Handle special cases
2938  if ((event.m_description.indexOf(grCategFantasy) != -1)
2939  && (event.m_description.indexOf(grCategMystery) != -1))
2940  {
2941  event.m_category = "Φαντασίας/Μυστηρίου";
2942  return;
2943  }
2944 
2945  // Find categories in the description
2946  for (const auto& [expression, category] : grCategoryDescData)
2947  {
2948  if (event.m_description.indexOf(expression) != -1) {
2949  event.m_category = category;
2950  return;
2951  }
2952  }
2953 
2954  // Find categories in the title
2955  for (const auto& [expression, category] : grCategoryTitleData)
2956  {
2957  if (event.m_title.indexOf(expression) != -1) {
2958  event.m_category = category;
2959  return;
2960  }
2961  }
2962 }
2963 
2965 {
2966  // TODO handle scraping the category and category_type from localized text in the short/long description
2967  // TODO remove short description (stored as episode title) which is just the beginning of the long description (actual description)
2968 
2969  // drop the short description if its copy the start of the long description
2970  if (event.m_description.startsWith (event.m_subtitle))
2971  {
2972  event.m_subtitle = "";
2973  }
2974 
2975  // handle cast and crew in items in the DVB Extended Event Descriptor
2976  // remove handled items from the map, so the left overs can be reported
2977  auto i = event.m_items.begin();
2978  while (i != event.m_items.end())
2979  {
2980  /* Possible TODO: if EIT inlcude the priority and/or character
2981  * names for the actors, include them in AddPerson call. */
2982  if ((QString::compare (i.key(), "Role Player") == 0) ||
2983  (QString::compare (i.key(), "Performing Artist") == 0))
2984  {
2985  event.AddPerson (DBPerson::kActor, i.value());
2986  i = event.m_items.erase (i);
2987  }
2988  else if (QString::compare (i.key(), "Director") == 0)
2989  {
2990  event.AddPerson (DBPerson::kDirector, i.value());
2991  i = event.m_items.erase (i);
2992  }
2993  else if (QString::compare (i.key(), "Commentary or Commentator") == 0)
2994  {
2995  event.AddPerson (DBPerson::kCommentator, i.value());
2996  i = event.m_items.erase (i);
2997  }
2998  else if (QString::compare (i.key(), "Presenter") == 0)
2999  {
3000  event.AddPerson (DBPerson::kPresenter, i.value());
3001  i = event.m_items.erase (i);
3002  }
3003  else if (QString::compare (i.key(), "Producer") == 0)
3004  {
3005  event.AddPerson (DBPerson::kProducer, i.value());
3006  i = event.m_items.erase (i);
3007  }
3008  else if (QString::compare (i.key(), "Scriptwriter") == 0)
3009  {
3010  event.AddPerson (DBPerson::kWriter, i.value());
3011  i = event.m_items.erase (i);
3012  }
3013  else
3014  {
3015  ++i;
3016  }
3017  }
3018 
3019  // handle star rating in the description
3020  static const QRegularExpression unitymediaImdbrating { R"(\s*IMDb Rating: (\d\.\d)\s?/10$)" };
3021  auto match = unitymediaImdbrating.match(event.m_description);
3022  if (match.hasMatch())
3023  {
3024  float stars = match.captured(1).toFloat();
3025  event.m_stars = stars / 10.0F;
3026  event.m_description.remove(match.capturedStart(0),
3027  match.capturedLength(0));
3028  }
3029 }
EITFixUp::FixGreekEIT
static void FixGreekEIT(DBEventEIT &event)
Definition: eitfixup.cpp:2500
DBEvent::m_season
uint m_season
Definition: programdata.h:172
EITFixUp::FixDK
static void FixDK(DBEventEIT &event)
Use this to clean YouSee's DVB-C guide in Denmark.
Definition: eitfixup.cpp:2272
EITFixUp::kFixUK
@ kFixUK
Definition: eitfixup.h:35
EITFixUp::kFixBell
@ kFixBell
Definition: eitfixup.h:34
NLMapResult::type
ProgramInfo::CategoryType type
Definition: eitfixup.cpp:1968
EITFixUp::kFixAUDescription
@ kFixAUDescription
Definition: eitfixup.h:52
EITFixUp::FixAUNine
static void FixAUNine(DBEventEIT &event)
Use this to standardize DVB-T guide in Australia.
Definition: eitfixup.cpp:1325
EventRating::m_system
QString m_system
Definition: programdata.h:78
kUKSpaceColonStart
static const QRegularExpression kUKSpaceColonStart
Definition: eitfixup.cpp:21
EITFixUp::kFixNO
@ kFixNO
Definition: eitfixup.h:47
DBPerson::kDirector
@ kDirector
Definition: programdata.h:32
DBEvent::m_totalepisodes
uint m_totalepisodes
Definition: programdata.h:174
EITFixUp::kFixNRK_DVBT
@ kFixNRK_DVBT
Definition: eitfixup.h:48
EITFixUp::FixBellExpressVu
static void FixBellExpressVu(DBEventEIT &event)
Use this for the Canadian BellExpressVu to standardize DVB-S guide.
Definition: eitfixup.cpp:229
ProgramInfo::kCategorySeries
@ kCategorySeries
Definition: programinfo.h:77
EITFixUp::Fix
static void Fix(DBEventEIT &event)
Definition: eitfixup.cpp:46
EITFixUp::FixFI
static void FixFI(DBEventEIT &event)
Use this to clean DVB-T guide in Finland.
Definition: eitfixup.cpp:1851
EITFixUp::kFixAUStar
@ kFixAUStar
Definition: eitfixup.h:39
EITFixUp::kFixPremiere
@ kFixPremiere
Definition: eitfixup.h:43
EITFixUp::kFixATV
@ kFixATV
Definition: eitfixup.h:58
DBEventEIT::m_fixup
FixupValue m_fixup
Definition: programdata.h:222
EITFixUp::kFixHTML
@ kFixHTML
Definition: eitfixup.h:56
EventRating
Definition: programdata.h:75
EITFixUp::FixATV
static void FixATV(DBEventEIT &event)
Use this to standardise the ATV/ATV2 guide in Germany.
Definition: eitfixup.cpp:1841
DBPerson::kProducer
@ kProducer
Definition: programdata.h:33
ProgramInfo::CategoryType
CategoryType
Definition: programinfo.h:76
DBEvent::m_starttime
QDateTime m_starttime
Definition: programdata.h:152
DBPerson::kActor
@ kActor
Definition: programdata.h:31
EITFixUp::kDotToTitle
static const uint kDotToTitle
Definition: eitfixup.h:19
EITFixUp::FixComHem
static void FixComHem(DBEventEIT &event, bool process_subtitle)
Use this to standardize ComHem DVB-C service in Sweden.
Definition: eitfixup.cpp:1044
ProgramInfo::kCategorySports
@ kCategorySports
Definition: programinfo.h:78
DBEvent::m_partnumber
uint16_t m_partnumber
Definition: programdata.h:157
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
EITFixUp::kFixSubtitle
@ kFixSubtitle
Definition: eitfixup.h:38
EITFixUp::kMaxToTitle
static const uint kMaxToTitle
Definition: eitfixup.h:17
dish_theme_type_to_string
QString dish_theme_type_to_string(uint theme_type)
Definition: dishdescriptors.cpp:304
EITFixUp::FixGreekCategories
static void FixGreekCategories(DBEventEIT &event)
Definition: eitfixup.cpp:2857
DBPerson::kWriter
@ kWriter
Definition: programdata.h:35
EITFixUp::FixAUStar
static void FixAUStar(DBEventEIT &event)
Use this to standardize DVB-S guide in Australia.
Definition: eitfixup.cpp:1283
DBEvent::m_category
QString m_category
Definition: programdata.h:151
NLMapResult::name
QString name
Definition: eitfixup.cpp:1967
kStereo
static const QRegularExpression kStereo
Definition: eitfixup.cpp:20
EITFixUp::parseRoman
static int parseRoman(QString roman)
Definition: eitfixup.cpp:30
EITFixUp::FixNRK_DVBT
static void FixNRK_DVBT(DBEventEIT &event)
Use this to clean DVB-T guide in Norway (NRK)
Definition: eitfixup.cpp:2210
tmp
static guint32 * tmp
Definition: goom_core.cpp:26
r2v
static const QMap< QChar, quint16 > r2v
Definition: eitfixup.cpp:24
EITFixUp::FixCategory
static void FixCategory(DBEventEIT &event)
Definition: eitfixup.cpp:2166
DBEvent::m_seriesId
QString m_seriesId
Definition: programdata.h:165
ProgramInfo::kCategoryNone
@ kCategoryNone
Definition: programinfo.h:77
DBEvent::m_parttotal
uint16_t m_parttotal
Definition: programdata.h:158
EITFixUp::kFixCategory
@ kFixCategory
Definition: eitfixup.h:46
DBEvent::m_programId
QString m_programId
Definition: programdata.h:166
programinfo.h
DBEventEIT::m_chanid
uint32_t m_chanid
Definition: programdata.h:221
mythlogging.h
DBEvent::m_categoryType
ProgramInfo::CategoryType m_categoryType
Definition: programdata.h:164
DBPerson::kHost
@ kHost
Definition: programdata.h:37
EITFixUp::SetUKSubtitle
static void SetUKSubtitle(DBEventEIT &event)
Use this in the United Kingdom to standardize DVB-T guide.
Definition: eitfixup.cpp:512
deCrewTitle
static const QMap< QString, DBPerson::Role > deCrewTitle
Definition: eitfixup.cpp:1729
DBEvent::m_title
QString m_title
Definition: programdata.h:148
EITFixUp::FixAUSeven
static void FixAUSeven(DBEventEIT &event)
Use this to standardize DVB-T guide in Australia.
Definition: eitfixup.cpp:1358
EITFixUp::kMinMovieDuration
static const int kMinMovieDuration
Definition: eitfixup.h:25
DBEvent::m_subtitle
QString m_subtitle
Definition: programdata.h:149
EITFixUp::kFixNL
@ kFixNL
Definition: eitfixup.h:45
EITFixUp::kFixGreekEIT
@ kFixGreekEIT
Definition: eitfixup.h:69
EITFixUp::kFixDisneyChannel
@ kFixDisneyChannel
Definition: eitfixup.h:59
hardwareprofile.scan.rating
def rating(profile, smoonURL, gate)
Definition: scan.py:37
ProgramInfo::kCategoryMovie
@ kCategoryMovie
Definition: programinfo.h:77
EITFixUp::kSubtitleMaxLen
static const uint kSubtitleMaxLen
Definition: eitfixup.h:15
EITFixUp::FixNO
static void FixNO(DBEventEIT &event)
Use this to clean DVB-S guide in Norway.
Definition: eitfixup.cpp:2180
EITFixUp::FixNL
static void FixNL(DBEventEIT &event)
Use this to standardize @Home DVB-C guide in the Netherlands.
Definition: eitfixup.cpp:1993
EITFixUp::kFixDK
@ kFixDK
Definition: eitfixup.h:50
EITFixUp::kFixGreekCategories
@ kFixGreekCategories
Definition: eitfixup.h:70
EITFixUp::FixPremiere
static void FixPremiere(DBEventEIT &event)
Use this to standardize DVB-C guide in Germany for the providers Kabel Deutschland and Premiere.
Definition: eitfixup.cpp:1903
categoryTrans
static const QMap< QString, NLMapResult > categoryTrans
Definition: eitfixup.cpp:1970
uint
unsigned int uint
Definition: compat.h:81
DBEvent::m_episode
uint m_episode
Definition: programdata.h:173
EITFixUp::kFixAUFreeview
@ kFixAUFreeview
Definition: eitfixup.h:51
channelutil.h
EITFixUp::kFixFI
@ kFixFI
Definition: eitfixup.h:42
EITFixUp::kFixComHem
@ kFixComHem
Definition: eitfixup.h:37
EITFixUp::kFixRTL
@ kFixRTL
Definition: eitfixup.h:41
EITFixUp::kFixDish
@ kFixDish
Definition: eitfixup.h:49
EITFixUp::FixAUDescription
static void FixAUDescription(DBEventEIT &event)
Use this to standardize DVB-T guide in Australia.
Definition: eitfixup.cpp:1299
EITFixUp::kFixGenericDVB
@ kFixGenericDVB
Definition: eitfixup.h:33
eitfixup.h
DBEventEIT
Definition: programdata.h:177
EITFixUp::kFixPBS
@ kFixPBS
Definition: eitfixup.h:36
DBEvent::m_description
QString m_description
Definition: programdata.h:150
EITFixUp::kFixAUSeven
@ kFixAUSeven
Definition: eitfixup.h:54
DBEventEIT::m_items
QMultiMap< QString, QString > m_items
Definition: programdata.h:223
EITFixUp::FixAUFreeview
static void FixAUFreeview(DBEventEIT &event)
Use this to standardize DVB-T guide in Australia.
Definition: eitfixup.cpp:1400
EITFixUp::AddDVBEITAuthority
static QString AddDVBEITAuthority(uint chanid, const QString &id)
This adds a DVB EIT default authority to series id or program id if one exists in the DB for that cha...
Definition: eitfixup.cpp:202
EITFixUp::FixPBS
static void FixPBS(DBEventEIT &event)
Use this to standardize PBS ATSC guide in the USA.
Definition: eitfixup.cpp:1029
EITFixUp::FixDisneyChannel
static void FixDisneyChannel(DBEventEIT &event)
Use this to standardise the Disney Channel guide in Germany.
Definition: eitfixup.cpp:1814
DBPerson::kUnknown
@ kUnknown
Definition: programdata.h:30
EITFixUp::kMaxDotToColon
static const uint kMaxDotToColon
Definition: eitfixup.h:23
EITFixUp::kFixHDTV
@ kFixHDTV
Definition: eitfixup.h:44
EITFixUp::kFixGreekSubtitle
@ kFixGreekSubtitle
Definition: eitfixup.h:68
DBEvent::m_endtime
QDateTime m_endtime
Definition: programdata.h:153
EITFixUp::kFixMCA
@ kFixMCA
Definition: eitfixup.h:40
NLMapResult
Definition: eitfixup.cpp:1966
EITFixUp::kFixP7S1
@ kFixP7S1
Definition: eitfixup.h:55
DBPerson::kPresenter
@ kPresenter
Definition: programdata.h:39
EITFixUp::FixUK
static void FixUK(DBEventEIT &event)
Use this in the United Kingdom to standardize DVB-T guide.
Definition: eitfixup.cpp:647
dishdescriptors.h
EITFixUp::kFixUnitymedia
@ kFixUnitymedia
Definition: eitfixup.h:57
EITFixUp::FixStripHTML
static void FixStripHTML(DBEventEIT &event)
Use this to clean HTML Tags from EIT Data.
Definition: eitfixup.cpp:2474
kDotAtEnd
static const QRegularExpression kDotAtEnd
Definition: eitfixup.cpp:22
EITFixUp::FixGreekSubtitle
static void FixGreekSubtitle(DBEventEIT &event)
Definition: eitfixup.cpp:2485
EITFixUp::kFixAUNine
@ kFixAUNine
Definition: eitfixup.h:53
DBPerson::kCommentator
@ kCommentator
Definition: programdata.h:40
DBEvent::m_airdate
uint16_t m_airdate
movie year / production year
Definition: programdata.h:154
EventRating::m_rating
QString m_rating
Definition: programdata.h:79
EITFixUp::FixRTL
static void FixRTL(DBEventEIT &event)
Use this to standardise the RTL group guide in Germany.
Definition: eitfixup.cpp:1597
ChannelUtil::GetDefaultAuthority
static QString GetDefaultAuthority(uint chanid)
Returns the DVB default authority for the chanid given.
Definition: channelutil.cpp:1179
EITFixUp::FixMCA
static void FixMCA(DBEventEIT &event)
Use this to standardise the MultiChoice Africa DVB-S guide.
Definition: eitfixup.cpp:1458
ProgramInfo::kCategoryTVShow
@ kCategoryTVShow
Definition: programinfo.h:78
DBPerson::Role
Role
Definition: programdata.h:28
EITFixUp::kMaxQuestionExclamation
static const uint kMaxQuestionExclamation
Definition: eitfixup.h:21
EITFixUp::FixUnitymedia
static void FixUnitymedia(DBEventEIT &event)
Definition: eitfixup.cpp:2964
EITFixUp::FixPRO7
static void FixPRO7(DBEventEIT &event)
Use this to standardise the PRO7/Sat1 group guide in Germany.
Definition: eitfixup.cpp:1738