MythTV  master
cddb.cpp
Go to the documentation of this file.
1 #include "cddb.h"
2 
3 #include <cstddef>
4 #include <cstdlib>
5 
6 #include <QFile>
7 #include <QFileInfo>
8 #include <QDir>
9 #include <QVector>
10 #include <QMap>
11 
12 #include <mythversion.h>
13 #include <mythlogging.h>
14 #include <mythcontext.h>
15 #include "mythdownloadmanager.h"
16 
17 /*
18  * CDDB protocol docs:
19  * http://ftp.freedb.org/pub/freedb/latest/CDDBPROTO
20  * http://ftp.freedb.org/pub/freedb/misc/freedb_howto1.07.zip
21  * http://ftp.freedb.org/pub/freedb/misc/freedb_CDDB_protcoldoc.zip
22  */
23 
24 const int CDROM_LEADOUT_TRACK = 0xaa;
25 const int CD_FRAMES_PER_SEC = 75;
26 const int SECS_PER_MIN = 60;
27 
28 //static const char URL[] = "http://freedb.freedb.org/~cddb/cddb.cgi?cmd=";
29 static const char URL[] = "http://freedb.musicbrainz.org/~cddb/cddb.cgi?cmd=";
30 static const QString& helloID();
31 
32 /*
33  * Local cddb database
34  */
35 struct Dbase
36 {
37  static bool Search(Cddb::Matches& res, Cddb::discid_t discID);
38  static bool Search(Cddb::Album& a, const QString& genre, Cddb::discid_t discID);
39  static bool Write(const Cddb::Album& album);
40 
41  static void New(Cddb::discid_t discID, const Cddb::Toc& toc);
42  static void MakeAlias(const Cddb::Album& album, Cddb::discid_t discID);
43 
44 private:
45  static bool CacheGet(Cddb::Matches& res, Cddb::discid_t discID);
46  static bool CacheGet(Cddb::Album& album, const QString& genre, Cddb::discid_t discID);
47  static void CachePut(const Cddb::Album& album);
48 
49  // DiscID to album info cache
50  typedef QMap< Cddb::discid_t, Cddb::Album > cache_t;
51  static cache_t s_cache;
52 
53  static const QString& GetDB();
54 };
55 QMap< Cddb::discid_t, Cddb::Album > Dbase::s_cache;
56 
57 
58 /*
59  * Inline helpers
60  */
61 // min/sec/frame to/from lsn
62 static inline unsigned long msf2lsn(const Cddb::Msf& msf)
63 {
64  return ((unsigned long)msf.min * SECS_PER_MIN + msf.sec) *
66 }
67 static inline Cddb::Msf lsn2msf(unsigned long lsn)
68 {
69  Cddb::Msf msf;
70 
71  std::div_t d = std::div(lsn, CD_FRAMES_PER_SEC);
72  msf.frame = d.rem;
73  d = std::div(d.quot, SECS_PER_MIN);
74  msf.sec = d.rem;
75  msf.min = d.quot;
76  return msf;
77 }
78 
79 // min/sec/frame to/from seconds
80 static inline int msf2sec(const Cddb::Msf& msf)
81 {
82  return msf.min * SECS_PER_MIN + msf.sec;
83 }
84 static inline Cddb::Msf sec2msf(unsigned sec)
85 {
86  Cddb::Msf msf;
87 
88  std::div_t d = std::div(sec, SECS_PER_MIN);
89  msf.sec = d.rem;
90  msf.min = d.quot;
91  msf.frame = 0;
92  return msf;
93 }
94 
95 
99 // static
100 bool Cddb::Query(Matches& res, const Toc& toc)
101 {
102  if (toc.size() < 2)
103  return false;
104  const unsigned totalTracks = toc.size() - 1;
105 
106  unsigned secs = 0;
107  const discid_t discID = Discid(secs, toc.data(), totalTracks);
108 
109  // Is it cached?
110  if (Dbase::Search(res, discID))
111  return !res.matches.empty();
112 
113  // Construct query
114  // cddb query discid ntrks off1 off2 ... nsecs
115  QString URL2 = URL + QString("cddb+query+%1+%2+").arg(discID,0,16).arg(totalTracks);
116 
117  for (unsigned t = 0; t < totalTracks; ++t)
118  URL2 += QString("%1+").arg(msf2lsn(toc[t]));
119 
120  URL2 += QString::number(secs);
121 
122  // Send the request
123  URL2 += "&hello=" + helloID() + "&proto=5";
124  LOG(VB_MEDIA, LOG_INFO, "CDDB lookup: " + URL2);
125 
126  QString cddb;
127  QByteArray data;
128  if (!GetMythDownloadManager()->download(URL2, &data))
129  return false;
130  cddb = data;
131 
132  // Check returned status
133  const uint stat = cddb.left(3).toUInt(); // Extract 3 digit status:
134  cddb = cddb.mid(4);
135  switch (stat)
136  {
137  case 200: // Unique match
138  LOG(VB_MEDIA, LOG_INFO, "CDDB match: " + cddb.trimmed());
139  // e.g. "200 rock 960b5e0c Nichole Nordeman / Woven & Spun"
140  res.discID = discID;
141  res.isExact = true;
142  res.matches.push_back(Match(
143  cddb.section(' ', 0, 0), // genre
144  cddb.section(' ', 1, 1).toUInt(nullptr,16), // discID
145  cddb.section(' ', 2).section(" / ", 0, 0), // artist
146  cddb.section(' ', 2).section(" / ", 1) // title
147  ));
148  break;
149 
150  case 202: // No match for disc ID...
151  LOG(VB_MEDIA, LOG_INFO, "CDDB no match");
152  Dbase::New(discID, toc); // Stop future lookups
153  return false;
154 
155  case 210: // Multiple exact matches
156  case 211: // Multiple inexact matches
157  // 210 Found exact matches, list follows (until terminating `.')
158  // 211 Found inexact matches, list follows (until terminating `.')
159  res.discID = discID;
160  res.isExact = 210 == stat;
161 
162  // Remove status line
163  cddb = cddb.section('\n', 1);
164 
165  // Append all matches
166  while (!cddb.isEmpty() && !cddb.startsWith("."))
167  {
168  LOG(VB_MEDIA, LOG_INFO, QString("CDDB %1 match: %2").
169  arg(210 == stat ? "exact" : "inexact").
170  arg(cddb.section('\n',0,0)));
171  res.matches.push_back(Match(
172  cddb.section(' ', 0, 0), // genre
173  cddb.section(' ', 1, 1).toUInt(nullptr,16), // discID
174  cddb.section(' ', 2).section(" / ", 0, 0), // artist
175  cddb.section(' ', 2).section(" / ", 1) // title
176  ));
177  cddb = cddb.section('\n', 1);
178  }
179  if (res.matches.empty())
180  Dbase::New(discID, toc); // Stop future lookups
181  break;
182 
183  default:
184  // TODO try a (telnet 8880) CDDB lookup
185  LOG(VB_GENERAL, LOG_INFO, QString("CDDB query error: %1").arg(stat) +
186  cddb.trimmed());
187  return false;
188  }
189  return true;
190 }
191 
195 // static
196 bool Cddb::Read(Album& album, const QString& genre, discid_t discID)
197 {
198  // Is it cached?
199  if (Dbase::Search(album, genre.toLower(), discID))
200  return true;
201 
202  // Lookup the details...
203  QString URL2 = URL + QString("cddb+read+") + genre.toLower() +
204  QString("+%1").arg(discID,0,16) + "&hello=" + helloID() + "&proto=5";
205  LOG(VB_MEDIA, LOG_INFO, "CDDB read: " + URL2);
206 
207  QString cddb;
208  QByteArray data;
209  if (!GetMythDownloadManager()->download(URL2, &data))
210  return false;
211  cddb = data;
212 
213  // Check returned status
214  const uint stat = cddb.left(3).toUInt(); // Get 3 digit status:
215  cddb = cddb.mid(4);
216  switch (stat)
217  {
218  case 210: // OK, CDDB database entry follows (until terminating marker)
219  LOG(VB_MEDIA, LOG_INFO, "CDDB read returned: " + cddb.section(' ',0,3));
220  LOG(VB_MEDIA, LOG_DEBUG, cddb.section('\n',1).trimmed());
221  break;
222  default:
223  LOG(VB_GENERAL, LOG_INFO, QString("CDDB read error: %1").arg(stat) +
224  cddb.trimmed());
225  return false;
226  }
227 
228  album = cddb;
229  album.discGenre = genre;
230  album.discID = discID;
231 
232  // Success - add to cache
233  Dbase::Write(album);
234 
235  return true;
236 }
237 
241 // static
242 bool Cddb::Write(const Album& album, bool /*bLocalOnly =true*/)
243 {
244 // TODO send to cddb if !bLocalOnly
245  Dbase::Write(album);
246  return true;
247 }
248 
249 static inline int cddb_sum(int i)
250 {
251  int total = 0;
252  while (i > 0)
253  {
254  const std::div_t d = std::div(i,10);
255  total += d.rem;
256  i = d.quot;
257  }
258  return total;
259 }
260 
264 // static
265 Cddb::discid_t Cddb::Discid(unsigned& secs, const Msf v[], unsigned tracks)
266 {
267  int checkSum = 0;
268  for (unsigned t = 0; t < tracks; ++t)
269  checkSum += cddb_sum(v[t].min * SECS_PER_MIN + v[t].sec);
270 
271  secs = v[tracks].min * SECS_PER_MIN + v[tracks].sec -
272  (v[0].min * SECS_PER_MIN + v[0].sec);
273 
274  const discid_t discID = ((discid_t)(checkSum % 255) << 24) |
275  ((discid_t)secs << 8) | tracks;
276  return discID;
277 }
278 
282 // static
283 void Cddb::Alias(const Album& album, discid_t discID)
284 {
285  Dbase::MakeAlias(album, discID);
286 }
287 
292 {
293  discGenre.clear();
294  discID = 0;
295  artist.clear();
296  title.clear();
297  genre.clear();
298  year = 0;
299  submitter = "MythTV " MYTH_BINARY_VERSION;
300  rev = 1;
301  isCompilation = false;
302  tracks.clear();
303  toc.clear();
304  extd.clear();
305  ext.clear();
306 
307  enum { kNorm, kToc } eState = kNorm;
308 
309  QString cddb = QString::fromUtf8(rhs.toLatin1().constData());
310  while (!cddb.isEmpty())
311  {
312  // Lines should be of the form "FIELD=value\r\n"
313  QString line = cddb.section(QRegExp("[\r\n]"), 0, 0);
314 
315  if (line.startsWith("# Track frame offsets:"))
316  {
317  eState = kToc;
318  }
319  else if (line.startsWith("# Disc length:"))
320  {
321  QString s = line.section(QRegExp("[ \t]"), 3, 3);
322  unsigned secs = s.toULong();
323  if (!toc.empty())
324  secs -= msf2sec(toc[0]);
325  toc.push_back(sec2msf(secs));
326  eState = kNorm;
327  }
328  else if (line.startsWith("# Revision:"))
329  {
330  QString s = line.section(QRegExp("[ \t]"), 2, 2);
331  bool bValid = false;
332  int v = s.toInt(&bValid);
333  if (bValid)
334  rev = v;
335  }
336  else if (line.startsWith("# Submitted via:"))
337  {
338  submitter = line.section(QRegExp("[ \t]"), 3, 3);
339  }
340  else if (line.startsWith("#"))
341  {
342  if (kToc == eState)
343  {
344  bool bValid = false;
345  QString s = line.section(QRegExp("[ \t]"), 1).trimmed();
346  unsigned long lsn = s.toUInt(&bValid);
347  if (bValid)
348  toc.push_back(lsn2msf(lsn));
349  else
350  eState = kNorm;
351  }
352  }
353  else
354  {
355  QString value = line.section('=', 1, 1);
356  QString art;
357 
358  if (value.contains(" / "))
359  {
360  art = value.section(" / ", 0, 0); // Artist in *TITLE
361  value = value.section(" / ", 1, 1);
362  }
363 
364  if (line.startsWith("DISCID="))
365  {
366  bool isValid = false;
367  ulong discID2 = value.toULong(&isValid,16);
368  if (isValid)
369  discID = discID2;
370  }
371  else if (line.startsWith("DTITLE="))
372  {
373  // Albums (and maybe artists?) can wrap over multiple lines:
374  artist += art;
375  title += value;
376  }
377  else if (line.startsWith("DYEAR="))
378  {
379  bool isValid = false;
380  int val = value.toInt(&isValid);
381  if (isValid)
382  year = val;
383  }
384  else if (line.startsWith("DGENRE="))
385  {
386  if (!value.isEmpty())
387  genre = value;
388  }
389  else if (line.startsWith("TTITLE"))
390  {
391  int trk = line.remove("TTITLE").section('=', 0, 0).toInt();
392  if (trk >= 0 && trk < CDROM_LEADOUT_TRACK)
393  {
394  if (trk >= tracks.size())
395  tracks.resize(trk + 1);
396 
397  Cddb::Track& track = tracks[trk];
398 
399  // Titles can wrap over multiple lines, so we load+store:
400  track.title += value;
401  track.artist += art;
402 
403  if (art.length())
404  isCompilation = true;
405  }
406  }
407  else if (line.startsWith("EXTD="))
408  {
409  if (!value.isEmpty())
410  extd = value;
411  }
412  else if (line.startsWith("EXTT"))
413  {
414  int trk = line.remove("EXTT").section('=', 0, 0).toInt();
415  if (trk >= 0 && trk < CDROM_LEADOUT_TRACK)
416  {
417  if (trk >= ext.size())
418  ext.resize(trk + 1);
419 
420  ext[trk] = value;
421  }
422  }
423  }
424 
425  // Next response line:
426  cddb = cddb.section('\n', 1);
427  }
428  return *this;
429 }
430 
434 Cddb::Album::operator QString() const
435 {
436  QString ret = "# xmcd\n"
437  "#\n"
438  "# Track frame offsets:\n";
439  for (int i = 1; i < toc.size(); ++i)
440  ret += "# " + QString::number(msf2lsn(toc[i - 1])) + '\n';
441  ret += "#\n";
442  ret += "# Disc length: " +
443  QString::number( msf2sec(toc.last()) + msf2sec(toc[0]) ) +
444  " seconds\n";
445  ret += "#\n";
446  ret += "# Revision: " + QString::number(rev) + '\n';
447  ret += "#\n";
448  ret += "# Submitted via: " + (!submitter.isEmpty() ? submitter :
449  "MythTV " MYTH_BINARY_VERSION) + '\n';
450  ret += "#\n";
451  ret += "DISCID=" + QString::number(discID,16) + '\n';
452  ret += "DTITLE=" + artist.toUtf8() + " / " + title + '\n';
453  ret += "DYEAR=" + (year ? QString::number(year) : "")+ '\n';
454  ret += "DGENRE=" + genre.toUtf8() + '\n';
455  for (int t = 0; t < tracks.size(); ++t)
456  ret += "TTITLE" + QString::number(t) + "=" +
457  tracks[t].title.toUtf8() + '\n';
458  ret += "EXTD=" + extd.toUtf8() + '\n';
459  for (int t = 0; t < tracks.size(); ++t)
460  ret += "EXTT" + QString::number(t) + "=" + ext[t].toUtf8() + '\n';
461  ret += "PLAYORDER=\n";
462 
463  return ret;
464 }
465 
466 
467 /**********************************************************
468  * Local cddb database ops
469  **********************************************************/
470 
471 // search local database for discID
473 {
474  res.matches.clear();
475  res.discID = discID;
476 
477  if (CacheGet(res, discID))
478  return true;
479 
480  QFileInfoList list = QDir(GetDB()).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
481  for (QFileInfoList::const_iterator it = list.begin(); it != list.end(); ++it)
482  {
483  QString genre = it->baseName();
484 
485  QFileInfoList ids = QDir(it->canonicalFilePath()).entryInfoList(QDir::Files);
486  for (QFileInfoList::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2)
487  {
488  if (it2->baseName().toUInt(nullptr,16) == discID)
489  {
490  QFile file(it2->canonicalFilePath());
491  if (file.open(QIODevice::ReadOnly | QIODevice::Text))
492  {
493  Cddb::Album a(QTextStream(&file).readAll());
494  a.discGenre = genre;
495  a.discID = discID;
496  LOG(VB_MEDIA, LOG_INFO, QString("LocalCDDB found %1 in ").
497  arg(discID,0,16) + genre + " : " +
498  a.artist + " / " + a.title);
499 
500  CachePut(a);
501  res.matches.push_back(Cddb::Match(genre,discID,a.artist,a.title));
502  }
503 
504  }
505  }
506  }
507  return !res.matches.empty();
508 }
509 
510 // search local database for genre/discID
511 bool Dbase::Search(Cddb::Album& a, const QString& genre, const Cddb::discid_t discID)
512 {
513  if (CacheGet(a, genre, discID))
514  return true;
515 
516  QFile file(GetDB() + '/' + genre.toLower() + '/' + QString::number(discID,16));
517  if (file.open(QIODevice::ReadOnly | QIODevice::Text))
518  {
519  a = QTextStream(&file).readAll();
520  a.discGenre = genre.toLower();
521  a.discID = discID;
522  LOG(VB_MEDIA, LOG_INFO, QString("LocalCDDB matched %1 ").arg(discID,0,16) +
523  genre + " to " + a.artist + " / " + a.title);
524 
525  CachePut(a);
526 
527  return true;
528  }
529  return false;
530 }
531 
532 // Create CDDB file
533 bool Dbase::Write(const Cddb::Album& album)
534 {
535  CachePut(album);
536 
537  const QString genre = !album.discGenre.isEmpty() ?
538  album.discGenre.toLower().toUtf8() : "misc";
539 
540  LOG(VB_MEDIA, LOG_INFO, "WriteDB " + genre +
541  QString(" %1 ").arg(album.discID,0,16) +
542  album.artist + " / " + album.title);
543 
544  if (QDir(GetDB()).mkpath(genre))
545  {
546  QFile file(GetDB() + '/' + genre + '/' +
547  QString::number(album.discID,16));
548  if (file.open(QIODevice::WriteOnly | QIODevice::Text))
549  {
550  QTextStream(&file) << album;
551  return true;
552  }
553  LOG(VB_GENERAL, LOG_ERR, "Cddb can't write " + file.fileName());
554  }
555  else
556  LOG(VB_GENERAL, LOG_ERR, "Cddb can't mkpath " + GetDB() + '/' + genre);
557  return false;
558 }
559 
560 // Create a local alias for a matched discID
561 // static
562 void Dbase::MakeAlias(const Cddb::Album& album, const Cddb::discid_t discID)
563 {
564  LOG(VB_MEDIA, LOG_DEBUG, QString("Cddb MakeAlias %1 for %2 ")
565  .arg(discID,0,16).arg(album.discID,0,16)
566  + album.genre + " " + album.artist + " / " + album.title);
567  s_cache.insert(discID, album)->discID = discID;
568 }
569 
570 // Create a new entry for a discID
571 // static
572 void Dbase::New(const Cddb::discid_t discID, const Cddb::Toc& toc)
573 {
574  s_cache.insert(discID, Cddb::Album(discID))->toc = toc;
575 }
576 
577 // static
578 void Dbase::CachePut(const Cddb::Album& album)
579 {
580  LOG(VB_MEDIA, LOG_DEBUG, QString("Cddb CachePut %1 ")
581  .arg(album.discID,0,16)
582  + album.genre + " " + album.artist + " / " + album.title);
583  s_cache.insertMulti(album.discID, album);
584 }
585 
586 // static
588 {
589  bool ret = false;
590  for (cache_t::const_iterator it = s_cache.find(discID); it != s_cache.end(); ++it)
591  {
592  if (it->discID == discID)
593  {
594  ret = true;
595  res.discID = discID;
596  LOG(VB_MEDIA, LOG_DEBUG, QString("Cddb CacheGet %1 found %2 ")
597  .arg(discID,0,16).arg(it->discID,0,16)
598  + it->discGenre + " " + it->artist + " / " + it->title);
599 
600  // If it's marker for 'no match' then genre is empty
601  if (!it->discGenre.isEmpty())
602  res.matches.push_back(Cddb::Match(*it));
603  }
604  }
605  return ret;
606 }
607 
608 // static
609 bool Dbase::CacheGet(Cddb::Album& album, const QString& genre, const Cddb::discid_t discID)
610 {
611  const Cddb::Album& a = s_cache[ discID];
612  if (a.discID && a.discGenre == genre)
613  {
614  album = a;
615  return true;
616  }
617  return false;
618 }
619 
620 // Local database path
621 // static
622 const QString& Dbase::GetDB()
623 {
624  static QString s_path;
625  if (s_path.isEmpty())
626  {
627  s_path = getenv("HOME");
628 #ifdef WIN32
629  if (s_path.isEmpty())
630  {
631  s_path = getenv("HOMEDRIVE");
632  s_path += getenv("HOMEPATH");
633  }
634 #endif
635  if (s_path.isEmpty())
636  s_path = ".";
637  if (!s_path.endsWith('/'))
638  s_path += '/';
639  s_path += ".cddb/";
640  }
641  return s_path;
642 }
643 
644 // CDDB hello string
645 static const QString& helloID()
646 {
647  static QString helloID;
648  if (helloID.isEmpty())
649  {
650  helloID = getenv("USER");
651  if (helloID.isEmpty())
652  helloID = "anon";
653  helloID += QString("+%1+MythTV+%2+")
655  }
656  return helloID;
657 }
QVector< Msf > Toc
Definition: cddb.h:52
QString artist
Definition: cddb.h:65
static int cddb_sum(int i)
Definition: cddb.cpp:249
QString discGenre
Definition: cddb.h:63
static bool Write(const Cddb::Album &album)
Definition: cddb.cpp:533
static cache_t s_cache
Definition: cddb.cpp:51
static Cddb::Msf sec2msf(unsigned sec)
Definition: cddb.cpp:84
static void New(Cddb::discid_t discID, const Cddb::Toc &toc)
Definition: cddb.cpp:572
QString artist
Definition: cddb.h:56
const int SECS_PER_MIN
Definition: cddb.cpp:26
const int CD_FRAMES_PER_SEC
Definition: cddb.cpp:25
static void MakeAlias(const Cddb::Album &album, Cddb::discid_t discID)
Definition: cddb.cpp:562
static const char URL[]
Definition: cddb.cpp:29
static unsigned long msf2lsn(const Cddb::Msf &msf)
Definition: cddb.cpp:62
unsigned int uint
Definition: compat.h:140
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
const int CDROM_LEADOUT_TRACK
Definition: cddb.cpp:24
unsigned long discid_t
Definition: cddb.h:13
QMap< Cddb::discid_t, Cddb::Album > cache_t
Definition: cddb.cpp:50
QString extd
Definition: cddb.h:74
static const QString & GetDB()
Definition: cddb.cpp:622
QString genre
Definition: cddb.h:67
ext_t ext
Definition: cddb.h:76
bool isCompilation
Definition: cddb.h:71
QString title
Definition: cddb.h:57
int year
Definition: cddb.h:68
static Cddb::Msf lsn2msf(unsigned long lsn)
Definition: cddb.cpp:67
static const QString & helloID()
Definition: cddb.cpp:645
discid_t discID
Definition: cddb.h:64
static const uint16_t * d
unsigned char t
Definition: ParseText.cpp:329
static void Alias(const Album &, discid_t)
Create a local alias for a matched discID.
Definition: cddb.cpp:283
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
QString submitter
Definition: cddb.h:69
static void CachePut(const Cddb::Album &album)
Definition: cddb.cpp:578
Toc toc
Definition: cddb.h:77
int rev
Definition: cddb.h:70
static discid_t Discid(unsigned &secs, const Msf [], unsigned tracks)
discID calculation.
Definition: cddb.cpp:265
int sec
Definition: cddb.h:49
static bool CacheGet(Cddb::Matches &res, Cddb::discid_t discID)
Definition: cddb.cpp:587
discid_t discID
Definition: cddb.h:39
bool isExact
Definition: cddb.h:40
static bool Write(const Album &, bool bLocalOnly=true)
CDDB write.
Definition: cddb.cpp:242
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
match_t matches
Definition: cddb.h:42
static int msf2sec(const Cddb::Msf &msf)
Definition: cddb.cpp:80
Definition: cddb.h:47
static bool Search(Cddb::Matches &res, Cddb::discid_t discID)
Definition: cddb.cpp:472
static bool Read(Album &, const QString &genre, discid_t)
CDDB read.
Definition: cddb.cpp:196
Definition: cddb.cpp:35
static bool Query(Matches &, const Toc &)
CDDB query.
Definition: cddb.cpp:100
#define MYTH_BINARY_VERSION
Update this whenever the plug-in ABI changes.
Definition: mythversion.h:16
Album & operator=(const QString &)
Parse CDDB text.
Definition: cddb.cpp:291
track_t tracks
Definition: cddb.h:73
int frame
Definition: cddb.h:49
QString title
Definition: cddb.h:66
eState
Definition: dvbci.cpp:368
QString GetHostName(void)
int min
Definition: cddb.h:49