00001 #include "cddb.h"
00002
00003 #include <cstddef>
00004 #include <cstdlib>
00005
00006 #include <QFile>
00007 #include <QFileInfo>
00008 #include <QDir>
00009 #include <QVector>
00010 #include <QMap>
00011
00012 #include <mythversion.h>
00013 #include <mythlogging.h>
00014 #include <mythcontext.h>
00015 #ifdef USING_HTTPCOMMS
00016 #error httpcomms is no longer supported
00017 #include <httpcomms.h>
00018 #else
00019 #include "mythdownloadmanager.h"
00020 #endif
00021
00022
00023
00024
00025
00026
00027
00028
00029 const int CDROM_LEADOUT_TRACK = 0xaa;
00030 const int CD_FRAMES_PER_SEC = 75;
00031 const int SECS_PER_MIN = 60;
00032
00033 static const char URL[] = "http://freedb.freedb.org/~cddb/cddb.cgi?cmd=";
00034
00035 static const QString& helloID();
00036
00037 namespace {
00038
00039
00040
00041 struct Dbase
00042 {
00043 static bool Search(Cddb::Matches&, Cddb::discid_t);
00044 static bool Search(Cddb::Album&, const QString& genre, Cddb::discid_t);
00045 static bool Write(const Cddb::Album&);
00046
00047 static void New(Cddb::discid_t, const Cddb::Toc&);
00048 static void MakeAlias(const Cddb::Album&, const Cddb::discid_t);
00049
00050 private:
00051 static bool CacheGet(Cddb::Matches&, Cddb::discid_t);
00052 static bool CacheGet(Cddb::Album&, const QString& genre, Cddb::discid_t);
00053 static void CachePut(const Cddb::Album&);
00054
00055
00056 typedef QMap< Cddb::discid_t, Cddb::Album > cache_t;
00057 static cache_t s_cache;
00058
00059 static const QString& GetDB();
00060 };
00061 QMap< Cddb::discid_t, Cddb::Album > Dbase::s_cache;
00062 }
00063
00064
00065
00066
00067
00068
00069 static inline unsigned long msf2lsn(const Cddb::Msf& msf)
00070 {
00071 return ((unsigned long)msf.min * SECS_PER_MIN + msf.sec) *
00072 CD_FRAMES_PER_SEC + msf.frame;
00073 }
00074 static inline Cddb::Msf lsn2msf(unsigned long lsn)
00075 {
00076 Cddb::Msf msf;
00077
00078 std::div_t d = std::div(lsn, CD_FRAMES_PER_SEC);
00079 msf.frame = d.rem;
00080 d = std::div(d.quot, SECS_PER_MIN);
00081 msf.sec = d.rem;
00082 msf.min = d.quot;
00083 return msf;
00084 }
00085
00086
00087 static inline int msf2sec(const Cddb::Msf& msf)
00088 {
00089 return msf.min * SECS_PER_MIN + msf.sec;
00090 }
00091 static inline Cddb::Msf sec2msf(unsigned sec)
00092 {
00093 Cddb::Msf msf;
00094
00095 std::div_t d = std::div(sec, SECS_PER_MIN);
00096 msf.sec = d.rem;
00097 msf.min = d.quot;
00098 msf.frame = 0;
00099 return msf;
00100 }
00101
00102
00106
00107 bool Cddb::Query(Matches& res, const Toc& toc)
00108 {
00109 if (toc.size() < 2)
00110 return false;
00111 const unsigned totalTracks = toc.size() - 1;
00112
00113 unsigned secs = 0;
00114 const discid_t discID = Discid(secs, toc.data(), totalTracks);
00115
00116
00117 if (Dbase::Search(res, discID))
00118 return res.matches.size() > 0;
00119
00120
00121
00122 QString URL2 = URL +
00123 QString("cddb+query+%1+%2+").arg(discID,0,16).arg(totalTracks);
00124 for (unsigned t = 0; t < totalTracks; ++t)
00125 URL2 += QString("%1+").arg(msf2lsn(toc[t]));
00126 URL2 += QString::number(secs);
00127
00128
00129 URL2 += "&hello=" + helloID() + "&proto=5";
00130 LOG(VB_MEDIA, LOG_INFO, "CDDB lookup: " + URL2);
00131 #ifdef USING_HTTPCOMMS
00132 QString cddb = HttpComms::getHttp(URL2);
00133 #else
00134 QString cddb;
00135 { QByteArray data;
00136 if (!GetMythDownloadManager()->download(URL2, &data))
00137 return false;
00138 cddb = data; }
00139 #endif
00140
00141
00142 const uint stat = cddb.left(3).toUInt();
00143 cddb = cddb.mid(4);
00144 switch (stat)
00145 {
00146 case 200:
00147 LOG(VB_MEDIA, LOG_INFO, "CDDB match: " + cddb.trimmed());
00148
00149 res.discID = discID;
00150 res.isExact = true;
00151 res.matches.push_back(Match(
00152 cddb.section(' ', 0, 0),
00153 cddb.section(' ', 1, 1).toUInt(0,16),
00154 cddb.section(' ', 2).section(" / ", 0, 0),
00155 cddb.section(' ', 2).section(" / ", 1)
00156 ));
00157 break;
00158
00159 case 202:
00160 LOG(VB_MEDIA, LOG_INFO, "CDDB no match");
00161 Dbase::New(discID, toc);
00162 return false;
00163
00164 case 210:
00165 case 211:
00166
00167
00168 res.discID = discID;
00169 res.isExact = 210 == stat;
00170
00171
00172 cddb = cddb.section('\n', 1);
00173
00174
00175 while (!cddb.isEmpty() && !cddb.startsWith("."))
00176 {
00177 LOG(VB_MEDIA, LOG_INFO, QString("CDDB %1 match: %2").
00178 arg(210 == stat ? "exact" : "inexact").
00179 arg(cddb.section('\n',0,0)));
00180 res.matches.push_back(Match(
00181 cddb.section(' ', 0, 0),
00182 cddb.section(' ', 1, 1).toUInt(0,16),
00183 cddb.section(' ', 2).section(" / ", 0, 0),
00184 cddb.section(' ', 2).section(" / ", 1)
00185 ));
00186 cddb = cddb.section('\n', 1);
00187 }
00188 if (res.matches.size() <= 0)
00189 Dbase::New(discID, toc);
00190 break;
00191
00192 default:
00193
00194 LOG(VB_GENERAL, LOG_INFO, QString("CDDB query error: %1").arg(stat) +
00195 cddb.trimmed());
00196 return false;
00197 }
00198 return true;
00199 }
00200
00204
00205 bool Cddb::Read(Album& album, const QString& genre, discid_t discID)
00206 {
00207
00208 if (Dbase::Search(album, genre.toLower(), discID))
00209 return true;
00210
00211
00212 QString URL2 = URL + QString("cddb+read+") + genre.toLower() +
00213 QString("+%1").arg(discID,0,16) + "&hello=" + helloID() + "&proto=5";
00214 LOG(VB_MEDIA, LOG_INFO, "CDDB read: " + URL2);
00215 #ifdef USING_HTTPCOMMS
00216 QString cddb = HttpComms::getHttp(URL2);
00217 #else
00218 QString cddb;
00219 { QByteArray data;
00220 if (!GetMythDownloadManager()->download(URL2, &data))
00221 return false;
00222 cddb = data; }
00223 #endif
00224
00225
00226 const uint stat = cddb.left(3).toUInt();
00227 cddb = cddb.mid(4);
00228 switch (stat)
00229 {
00230 case 210:
00231 LOG(VB_MEDIA, LOG_INFO, "CDDB read returned: " + cddb.section(' ',0,3));
00232 LOG(VB_MEDIA, LOG_DEBUG, cddb.section('\n',1).trimmed());
00233 break;
00234 default:
00235 LOG(VB_GENERAL, LOG_INFO, QString("CDDB read error: %1").arg(stat) +
00236 cddb.trimmed());
00237 return false;
00238 }
00239
00240 album = cddb;
00241 album.genre = cddb.section(' ', 0, 0);
00242 album.discID = discID;
00243
00244
00245 Dbase::Write(album);
00246
00247 return true;
00248 }
00249
00253
00254 bool Cddb::Write(const Album& album, bool )
00255 {
00256
00257 Dbase::Write(album);
00258 return true;
00259 }
00260
00261 static inline int cddb_sum(int i)
00262 {
00263 int total = 0;
00264 while (i > 0)
00265 {
00266 const std::div_t d = std::div(i,10);
00267 total += d.rem;
00268 i = d.quot;
00269 }
00270 return total;
00271 }
00272
00276
00277 Cddb::discid_t Cddb::Discid(unsigned& secs, const Msf v[], unsigned tracks)
00278 {
00279 int checkSum = 0;
00280 for (unsigned t = 0; t < tracks; ++t)
00281 checkSum += cddb_sum(v[t].min * SECS_PER_MIN + v[t].sec);
00282
00283 secs = v[tracks].min * SECS_PER_MIN + v[tracks].sec -
00284 (v[0].min * SECS_PER_MIN + v[0].sec);
00285
00286 const discid_t discID = ((discid_t)(checkSum % 255) << 24) |
00287 ((discid_t)secs << 8) | tracks;
00288 return discID;
00289 }
00290
00294
00295 void Cddb::Alias(const Album& album, discid_t discID)
00296 {
00297 Dbase::MakeAlias(album, discID);
00298 }
00299
00303 Cddb::Album& Cddb::Album::operator =(const QString& rhs)
00304 {
00305 genre.clear();
00306 discID = 0;
00307 artist.clear();
00308 title.clear();
00309 year = 0;
00310 submitter = "MythTV " MYTH_BINARY_VERSION;
00311 rev = 1;
00312 isCompilation = false;
00313 tracks.empty();
00314 toc.empty();
00315 extd.clear();
00316 ext.empty();
00317
00318 enum { kNorm, kToc } eState = kNorm;
00319
00320 QString cddb = rhs;
00321 while (!cddb.isEmpty())
00322 {
00323
00324 QString line = cddb.section(QRegExp("[\r\n]"), 0, 0);
00325
00326 if (line.startsWith("# Track frame offsets:"))
00327 {
00328 eState = kToc;
00329 }
00330 else if (line.startsWith("# Disc length:"))
00331 {
00332 QString s = line.section(QRegExp("[ \t]"), 3, 3);
00333 unsigned secs = s.toULong();
00334 if (toc.size())
00335 secs -= msf2sec(toc[0]);
00336 toc.push_back(sec2msf(secs));
00337 eState = kNorm;
00338 }
00339 else if (line.startsWith("# Revision:"))
00340 {
00341 QString s = line.section(QRegExp("[ \t]"), 2, 2);
00342 bool bValid = false;
00343 int v = s.toInt(&bValid);
00344 if (bValid)
00345 rev = v;
00346 }
00347 else if (line.startsWith("# Submitted via:"))
00348 {
00349 submitter = line.section(QRegExp("[ \t]"), 3, 3);
00350 }
00351 else if (line.startsWith("#"))
00352 {
00353 if (kToc == eState)
00354 {
00355 bool bValid = false;
00356 QString s = line.section(QRegExp("[ \t]"), 1).trimmed();
00357 unsigned long lsn = s.toUInt(&bValid);
00358 if (bValid)
00359 toc.push_back(lsn2msf(lsn));
00360 else
00361 eState = kNorm;
00362 }
00363 }
00364 else
00365 {
00366 QString value = line.section('=', 1, 1);
00367 QString art;
00368
00369 if (value.contains(" / "))
00370 {
00371 art = value.section(" / ", 0, 0);
00372 value = value.section(" / ", 1, 1);
00373 }
00374
00375 if (line.startsWith("DISCID="))
00376 {
00377 bool isValid = false;
00378 ulong discID = value.toULong(&isValid,16);
00379 if (isValid)
00380 discID = discID;
00381 }
00382 else if (line.startsWith("DTITLE="))
00383 {
00384
00385 artist += art;
00386 title += value;
00387 }
00388 else if (line.startsWith("DYEAR="))
00389 {
00390 bool isValid = false;
00391 int val = value.toInt(&isValid);
00392 if (isValid)
00393 year = val;
00394 }
00395 else if (line.startsWith("DGENRE="))
00396 {
00397 if (!value.isEmpty())
00398 genre = value;
00399 }
00400 else if (line.startsWith("TTITLE"))
00401 {
00402 int trk = line.remove("TTITLE").section('=', 0, 0).toInt();
00403 if (trk >= 0 && trk < CDROM_LEADOUT_TRACK)
00404 {
00405 if (trk >= tracks.size())
00406 tracks.resize(trk + 1);
00407
00408 Cddb::Track& track = tracks[trk];
00409
00410
00411 track.title += value;
00412 track.artist += art;
00413
00414 if (art.length())
00415 isCompilation = true;
00416 }
00417 }
00418 else if (line.startsWith("EXTD="))
00419 {
00420 if (!value.isEmpty())
00421 extd = value;
00422 }
00423 else if (line.startsWith("EXTT"))
00424 {
00425 int trk = line.remove("EXTT").section('=', 0, 0).toInt();
00426 if (trk >= 0 && trk < CDROM_LEADOUT_TRACK)
00427 {
00428 if (trk >= ext.size())
00429 ext.resize(trk + 1);
00430
00431 ext[trk] = value;
00432 }
00433 }
00434 }
00435
00436
00437 cddb = cddb.section('\n', 1);
00438 }
00439 return *this;
00440 }
00441
00445 Cddb::Album::operator QString() const
00446 {
00447 QString ret = "# xmcd\n"
00448 "#\n"
00449 "# Track frame offsets:\n";
00450 for (int i = 1; i < toc.size(); ++i)
00451 ret += "# " + QString::number(msf2lsn(toc[i - 1])) + '\n';
00452 ret += "#\n";
00453 ret += "# Disc length: " +
00454 QString::number( msf2sec(toc.last()) + msf2sec(toc[0]) ) +
00455 " seconds\n";
00456 ret += "#\n";
00457 ret += "# Revision: " + QString::number(rev) + '\n';
00458 ret += "#\n";
00459 ret += "# Submitted via: " + (!submitter.isEmpty() ? submitter :
00460 "MythTV " MYTH_BINARY_VERSION) + '\n';
00461 ret += "#\n";
00462 ret += "DISCID=" + QString::number(discID,16) + '\n';
00463 ret += "DTITLE=" + artist.toUtf8() + " / " + title + '\n';
00464 ret += "DYEAR=" + (year ? QString::number(year) : "")+ '\n';
00465 ret += "DGENRE=" + genre.toLower().toUtf8() + '\n';
00466 for (int t = 0; t < tracks.size(); ++t)
00467 ret += "TTITLE" + QString::number(t) + "=" +
00468 tracks[t].title.toUtf8() + '\n';
00469 ret += "EXTD=" + extd.toUtf8() + '\n';
00470 for (int t = 0; t < tracks.size(); ++t)
00471 ret += "EXTT" + QString::number(t) + "=" + ext[t].toUtf8() + '\n';
00472 ret += "PLAYORDER=\n";
00473
00474 return ret;
00475 }
00476
00477
00478
00479
00480
00481
00482
00483 bool Dbase::Search(Cddb::Matches& res, const Cddb::discid_t discID)
00484 {
00485 res.matches.empty();
00486
00487 if (CacheGet(res, discID))
00488 return true;
00489
00490 QFileInfoList list = QDir(GetDB()).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
00491 for (QFileInfoList::const_iterator it = list.begin(); it != list.end(); ++it)
00492 {
00493 QString genre = it->baseName();
00494
00495 QFileInfoList ids = QDir(it->canonicalFilePath()).entryInfoList(QDir::Files);
00496 for (QFileInfoList::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2)
00497 {
00498 if (it2->baseName().toUInt(0,16) == discID)
00499 {
00500 QFile file(it2->canonicalFilePath());
00501 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
00502 {
00503 Cddb::Album a = QTextStream(&file).readAll();
00504 a.genre = genre;
00505 a.discID = discID;
00506 LOG(VB_MEDIA, LOG_INFO, QString("LocalCDDB found %1 in ").
00507 arg(discID,0,16) + genre + " : " +
00508 a.artist + " / " + a.title);
00509
00510 CachePut(a);
00511 res.matches.push_back(Cddb::Match(genre,discID,a.artist,a.title));
00512 }
00513
00514 }
00515 }
00516 }
00517 return res.matches.size() > 0;
00518 }
00519
00520
00521 bool Dbase::Search(Cddb::Album& a, const QString& genre, const Cddb::discid_t discID)
00522 {
00523 if (CacheGet(a, genre, discID))
00524 return true;
00525
00526 QFile file(GetDB() + '/' + genre.toLower() + '/' + QString::number(discID,16));
00527 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
00528 {
00529 a = QTextStream(&file).readAll();
00530 a.genre = genre.toLower();
00531 a.discID = discID;
00532 LOG(VB_MEDIA, LOG_INFO, QString("LocalCDDB matched %1 ").arg(discID,0,16) +
00533 genre + " to " + a.artist + " / " + a.title);
00534
00535 CachePut(a);
00536
00537 return true;
00538 }
00539 return false;
00540 }
00541
00542
00543 bool Dbase::Write(const Cddb::Album& album)
00544 {
00545 CachePut(album);
00546
00547 const QString genre = !album.genre.isEmpty() ?
00548 album.genre.toLower().toUtf8() : "misc";
00549
00550 LOG(VB_MEDIA, LOG_INFO, "WriteDB " + genre +
00551 QString(" %1 ").arg(album.discID,0,16) +
00552 album.artist + " / " + album.title);
00553
00554 if (QDir(GetDB()).mkpath(genre))
00555 {
00556 QFile file(GetDB() + '/' + genre + '/' +
00557 QString::number(album.discID,16));
00558 if (file.open(QIODevice::WriteOnly | QIODevice::Text))
00559 {
00560 QTextStream(&file) << album;
00561 return true;
00562 }
00563 else
00564 LOG(VB_GENERAL, LOG_ERR, "Cddb can't write " + file.fileName());
00565 }
00566 else
00567 LOG(VB_GENERAL, LOG_ERR, "Cddb can't mkpath " + GetDB() + '/' + genre);
00568 return false;
00569 }
00570
00571
00572
00573 void Dbase::MakeAlias(const Cddb::Album& album, const Cddb::discid_t discID)
00574 {
00575 s_cache[ discID] = album;
00576 }
00577
00578
00579
00580 void Dbase::New(const Cddb::discid_t discID, const Cddb::Toc& toc)
00581 {
00582 (s_cache[ discID] = Cddb::Album(discID)).toc = toc;
00583 }
00584
00585
00586 void Dbase::CachePut(const Cddb::Album& album)
00587 {
00588 s_cache[ album.discID] = album;
00589 }
00590
00591
00592 bool Dbase::CacheGet(Cddb::Matches& res, const Cddb::discid_t discID)
00593 {
00594 bool ret = false;
00595 for (cache_t::const_iterator it = s_cache.find(discID); it != s_cache.end(); ++it)
00596 {
00597
00598 if (it->discID)
00599 {
00600 ret = true;
00601 res.discID = discID;
00602 LOG(VB_MEDIA, LOG_DEBUG, QString("Cddb CacheGet found %1 ").
00603 arg(discID,0,16) + it->genre + " " + it->artist + " / " + it->title);
00604
00605
00606 if (!it->genre.isEmpty())
00607 res.matches.push_back(Cddb::Match(*it));
00608 }
00609 }
00610 return ret;
00611 }
00612
00613
00614 bool Dbase::CacheGet(Cddb::Album& album, const QString& genre, const Cddb::discid_t discID)
00615 {
00616 const Cddb::Album& a = s_cache[ discID];
00617 if (a.discID && a.genre == genre)
00618 {
00619 album = a;
00620 return true;
00621 }
00622 return false;
00623 }
00624
00625
00626
00627 const QString& Dbase::GetDB()
00628 {
00629 static QString s_path;
00630 if (s_path.isEmpty())
00631 {
00632 s_path = getenv("HOME");
00633 #ifdef WIN32
00634 if (s_path.isEmpty())
00635 {
00636 s_path = getenv("HOMEDRIVE");
00637 s_path += getenv("HOMEPATH");
00638 }
00639 #endif
00640 if (s_path.isEmpty())
00641 s_path = ".";
00642 if (!s_path.endsWith('/'))
00643 s_path += '/';
00644 s_path += ".cddb/";
00645 }
00646 return s_path;
00647 }
00648
00649
00650 static const QString& helloID()
00651 {
00652 static QString helloID;
00653 if (helloID.isEmpty())
00654 {
00655 helloID = getenv("USER");
00656 if (helloID.isEmpty())
00657 helloID = "anon";
00658 helloID += QString("+%1+MythTV+%2+")
00659 .arg(gCoreContext->GetHostName()).arg(MYTH_BINARY_VERSION);
00660 }
00661 return helloID;
00662 }