|
MythTV
0.26-pre
|
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 * CDDB protocol docs: 00024 * http://ftp.freedb.org/pub/freedb/latest/CDDBPROTO 00025 * http://ftp.freedb.org/pub/freedb/misc/freedb_howto1.07.zip 00026 * http://ftp.freedb.org/pub/freedb/misc/freedb_CDDB_protcoldoc.zip 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 //static const char URL[] = "http://freedb.musicbrainz.org/~cddb/cddb.cgi?cmd="; 00035 static const QString& helloID(); 00036 00037 namespace { 00038 /* 00039 * Local cddb database 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 // DiscID to album info cache 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 * Inline helpers 00067 */ 00068 // min/sec/frame to/from lsn 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 // min/sec/frame to/from seconds 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 // static 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 // Is it cached? 00117 if (Dbase::Search(res, discID)) 00118 return res.matches.size() > 0; 00119 00120 // Construct query 00121 // cddb query discid ntrks off1 off2 ... nsecs 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 // Send the request 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 // Check returned status 00142 const uint stat = cddb.left(3).toUInt(); // Extract 3 digit status: 00143 cddb = cddb.mid(4); 00144 switch (stat) 00145 { 00146 case 200: // Unique match 00147 LOG(VB_MEDIA, LOG_INFO, "CDDB match: " + cddb.trimmed()); 00148 // e.g. "200 rock 960b5e0c Nichole Nordeman / Woven & Spun" 00149 res.discID = discID; 00150 res.isExact = true; 00151 res.matches.push_back(Match( 00152 cddb.section(' ', 0, 0), // genre 00153 cddb.section(' ', 1, 1).toUInt(0,16), // discID 00154 cddb.section(' ', 2).section(" / ", 0, 0), // artist 00155 cddb.section(' ', 2).section(" / ", 1) // title 00156 )); 00157 break; 00158 00159 case 202: // No match for disc ID... 00160 LOG(VB_MEDIA, LOG_INFO, "CDDB no match"); 00161 Dbase::New(discID, toc); // Stop future lookups 00162 return false; 00163 00164 case 210: // Multiple exact matches 00165 case 211: // Multiple inexact matches 00166 // 210 Found exact matches, list follows (until terminating `.') 00167 // 211 Found inexact matches, list follows (until terminating `.') 00168 res.discID = discID; 00169 res.isExact = 210 == stat; 00170 00171 // Remove status line 00172 cddb = cddb.section('\n', 1); 00173 00174 // Append all matches 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), // genre 00182 cddb.section(' ', 1, 1).toUInt(0,16), // discID 00183 cddb.section(' ', 2).section(" / ", 0, 0), // artist 00184 cddb.section(' ', 2).section(" / ", 1) // title 00185 )); 00186 cddb = cddb.section('\n', 1); 00187 } 00188 if (res.matches.size() <= 0) 00189 Dbase::New(discID, toc); // Stop future lookups 00190 break; 00191 00192 default: 00193 // TODO try a (telnet 8880) CDDB lookup 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 // static 00205 bool Cddb::Read(Album& album, const QString& genre, discid_t discID) 00206 { 00207 // Is it cached? 00208 if (Dbase::Search(album, genre.toLower(), discID)) 00209 return true; 00210 00211 // Lookup the details... 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 // Check returned status 00226 const uint stat = cddb.left(3).toUInt(); // Get 3 digit status: 00227 cddb = cddb.mid(4); 00228 switch (stat) 00229 { 00230 case 210: // OK, CDDB database entry follows (until terminating marker) 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 // Success - add to cache 00245 Dbase::Write(album); 00246 00247 return true; 00248 } 00249 00253 // static 00254 bool Cddb::Write(const Album& album, bool /*bLocalOnly =true*/) 00255 { 00256 // TODO send to cddb if !bLocalOnly 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 // static 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 // static 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 // Lines should be of the form "FIELD=value\r\n" 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); // Artist in *TITLE 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 // Albums (and maybe artists?) can wrap over multiple lines: 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 // Titles can wrap over multiple lines, so we load+store: 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 // Next response line: 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 * Local cddb database ops 00480 **********************************************************/ 00481 00482 // search local database for discID 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 // search local database for genre/discID 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 // Create CDDB file 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 // Create a local alias for a matched discID 00572 // static 00573 void Dbase::MakeAlias(const Cddb::Album& album, const Cddb::discid_t discID) 00574 { 00575 s_cache[ discID] = album; 00576 } 00577 00578 // Create a new entry for a discID 00579 // static 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 // static 00586 void Dbase::CachePut(const Cddb::Album& album) 00587 { 00588 s_cache[ album.discID] = album; 00589 } 00590 00591 // static 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 // NB it->discID may not equal discID if it's an alias 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 // If it's marker for 'no match' then genre is empty 00606 if (!it->genre.isEmpty()) 00607 res.matches.push_back(Cddb::Match(*it)); 00608 } 00609 } 00610 return ret; 00611 } 00612 00613 // static 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 // Local database path 00626 // static 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 // CDDB hello string 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 }
1.7.6.1