MythTV  0.26-pre
cddb.cpp
Go to the documentation of this file.
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 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends