File indexing completed on 2024-04-14 04:49:14

0001 /*
0002     SPDX-FileCopyrightText: 2002 Rik Hemsley (rikkus) <rik@kde.org>
0003     SPDX-FileCopyrightText: 2002-2005 Benjamin Meyer <ben-devel@meyerhome.net>
0004     SPDX-FileCopyrightText: 2002-2004 Nadeem Hasan <nhasan@nadmm.com>
0005     SPDX-FileCopyrightText: 2006 Richard Lärkäng <nouseforaname@home.se>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "cdinfo.h"
0011 
0012 #include "client.h"
0013 #include "cddb.h"
0014 #include "logging.h"
0015 
0016 #include <KStringHandler>
0017 #include <QDebug>
0018 
0019 #include <QMap>
0020 #include <QRegExp>
0021 #include <QRegularExpression>
0022 
0023 namespace KCDDB
0024 {
0025   class InfoBasePrivate {
0026     public:
0027       /**
0028        * Creates a line in the form NAME=VALUE, and splits it into several
0029        * lines if the line gets longer than 256 chars
0030        */
0031         static QString
0032       createLine(const QString& name, const QString& value)
0033       {
0034         Q_ASSERT(name.length() < 254);
0035 
0036         int maxLength = 256 - name.length() - 2;
0037 
0038         QString tmpValue = escape(value);
0039 
0040         QString lines;
0041 
0042         while (tmpValue.length() > maxLength)
0043         {
0044             lines += QString::fromLatin1("%1=%2\n").arg(name,tmpValue.left(maxLength));
0045           tmpValue = tmpValue.mid(maxLength);
0046         }
0047 
0048         lines += QString::fromLatin1("%1=%2\n").arg(name,tmpValue);
0049 
0050         return lines;
0051       }
0052 
0053       /**
0054        * escape's string for CDDB processing
0055        */
0056         static QString
0057       escape( const QString &value )
0058       {
0059         QString s = value;
0060         s.replace( QLatin1String( "\\" ), QLatin1String( "\\\\" ) );
0061         s.replace( QLatin1String( "\n" ), QLatin1String( "\\n" ) );
0062         s.replace( QLatin1String( "\t" ), QLatin1String( "\\t" ) );
0063 
0064         return s;
0065       }
0066 
0067       /**
0068        * fixes an escaped string that has been CDDB processed
0069        */
0070         static QString
0071       unescape( const QString &value )
0072       {
0073         QString s = value;
0074 
0075         s.replace( QLatin1String( "\\n" ), QLatin1String( "\n" ) );
0076         s.replace( QLatin1String( "\\t" ), QLatin1String( "\t" ) );
0077         s.replace( QLatin1String( "\\\\" ), QLatin1String( "\\" ) );
0078 
0079         return s;
0080       }
0081 
0082         QVariant
0083       get(const QString& type)
0084       {
0085         return data[type.toUpper()];
0086       }
0087         QVariant
0088       get(Type type)
0089       {
0090         switch(type){
0091           case(Title):
0092             return get(QLatin1String( "title" ));
0093           case(Comment):
0094             return get(QLatin1String( "comment" ));
0095           case(Artist):
0096             return get(QLatin1String( "artist" ));
0097           case(Genre):
0098             return get(QLatin1String( "genre" ));
0099           case(Year):
0100             return get(QLatin1String( "year" ));
0101           case(Length):
0102             return get(QLatin1String( "length" ));
0103           case(Category):
0104             return get(QLatin1String( "category" ));
0105         }
0106         return QVariant();
0107       }
0108 
0109         void
0110       set(const QString& type, const QVariant &d)
0111       {
0112         //qDebug() << "set: " << type << ", " << d.toString();
0113         if(type.contains(QRegularExpression( QLatin1String( "^T.*_.*$" )) )){
0114           qCDebug(LIBKCDDB) << "Error: custom cdinfo::set data can not start with T and contain a _";
0115           return;
0116         }
0117         if(type.toUpper() == QLatin1String( "DTITLE" )){
0118           qCDebug(LIBKCDDB) << "Error: type: DTITLE is reserved and can not be set.";
0119           return;
0120         }
0121 
0122         data[type.toUpper()] = d;
0123       }
0124         void
0125       set(Type type, const QVariant &d)
0126       {
0127         switch(type)
0128         {
0129           case(Title):
0130             set(QLatin1String( "title" ), d);
0131             return;
0132           case(Comment):
0133             set(QLatin1String( "comment" ), d);
0134             return;
0135           case(Artist):
0136             set(QLatin1String( "artist" ), d);
0137             return;
0138           case(Genre):
0139             set(QLatin1String( "genre" ), d);
0140             return;
0141           case(Year):
0142             set(QLatin1String( "year" ), d);
0143             return;
0144           case(Length):
0145             set(QLatin1String( "length" ), d);
0146             return;
0147           case(Category):
0148             set(QLatin1String( "category" ), d);
0149             return;
0150         }
0151 
0152         Q_ASSERT(false);
0153       }
0154 
0155       // Appends text to data instead of overwriting it
0156         void
0157       append(const QString& type, const QString& text)
0158       {
0159         set(type, get(type).toString().append(text));
0160       }
0161         void
0162       append(Type type, const QString& text)
0163       {
0164         set(type, get(type).toString().append(text));
0165       }
0166 
0167       QMap<QString, QVariant> data;
0168   } ;
0169 
0170   class TrackInfoPrivate : public InfoBasePrivate {
0171   };
0172 
0173   TrackInfo::TrackInfo()
0174   {
0175     d = new TrackInfoPrivate();
0176   }
0177 
0178   TrackInfo::TrackInfo(const TrackInfo& clone)
0179       : d(new TrackInfoPrivate)
0180   {
0181     d->data = clone.d->data;
0182   }
0183 
0184   TrackInfo::~TrackInfo()
0185   {
0186      delete d;
0187   }
0188 
0189   TrackInfo& TrackInfo::operator=(const TrackInfo& clone)
0190   {
0191     d->data = clone.d->data;
0192     return *this;
0193   }
0194 
0195   QVariant TrackInfo::get(Type type) const {
0196     return d->get(type);
0197   }
0198 
0199   QVariant TrackInfo::get(const QString &type) const {
0200     return d->get(type);
0201   }
0202 
0203   void TrackInfo::set(const QString &type, const QVariant &data){
0204     d->set(type, data);
0205   }
0206 
0207   void TrackInfo::set(Type type, const QVariant &data) {
0208     d->set(type, data);
0209   }
0210 
0211   void TrackInfo::clear(){
0212     d->data.clear();
0213   }
0214 
0215   QString TrackInfo::toString() const {
0216     QString out;
0217     bool ok;
0218     int track = get(QLatin1String( "tracknumber" )).toInt(&ok);
0219     if(!ok)
0220       qCDebug(LIBKCDDB) << "Warning toString() on a track that doesn't have track number assigned.";
0221     QMap<QString, QVariant>::const_iterator i = d->data.constBegin();
0222     while (i != d->data.constEnd()) {
0223         if(i.key() != QLatin1String( "COMMENT" ) && i.key() != QLatin1String( "TITLE" ) && i.key() != QLatin1String( "ARTIST" ) && i.key() != QLatin1String( "TRACKNUMBER" )) {
0224             out += d->createLine(QString::fromLatin1("T%1_%2").arg(i.key()).arg(track),i.value().toString());
0225         }
0226         ++i;
0227     }
0228     return out;
0229   }
0230 
0231     bool TrackInfo::operator==( const TrackInfo& other ) const
0232     {
0233         return d->data == other.d->data;
0234     }
0235 
0236     bool TrackInfo::operator!=( const TrackInfo& other ) const
0237     {
0238         return d->data != other.d->data;
0239     }
0240 
0241   class CDInfoPrivate : public InfoBasePrivate {
0242     public:
0243       TrackInfoList trackInfoList;
0244   };
0245 
0246   CDInfo::CDInfo()
0247     : d(new CDInfoPrivate())
0248   {
0249     set(QLatin1String( "revision" ), 0);
0250   }
0251 
0252   CDInfo::CDInfo(const CDInfo& clone)
0253     : d(new CDInfoPrivate())
0254   {
0255     d->data = clone.d->data;
0256     d->trackInfoList = clone.d->trackInfoList;
0257   }
0258 
0259   CDInfo::~CDInfo()
0260   {
0261     delete d;
0262   }
0263 
0264   CDInfo& CDInfo::operator=(const CDInfo& clone)
0265   {
0266     d->trackInfoList = clone.d->trackInfoList;
0267     d->data = clone.d->data;
0268     return *this;
0269   }
0270 
0271     bool
0272   CDInfo::load(const QString & string)
0273   {
0274     return load(string.split(QLatin1Char( '\n' ),Qt::SkipEmptyParts));
0275   }
0276 
0277     bool
0278   CDInfo::load(const QStringList & lineList)
0279   {
0280     clear();
0281 
0282     // We'll append to this until we've seen all the lines, then parse it after.
0283     QString dtitle;
0284 
0285     QStringList::ConstIterator it = lineList.begin();
0286 
0287     QRegExp rev(QLatin1String( "# Revision: (\\d+)" ));
0288     const static QRegularExpression eol(QLatin1String( "[\r\n]" ));
0289 
0290     while ( it != lineList.end() )
0291     {
0292       QString line(*it);
0293       line.remove(eol);
0294       ++it;
0295 
0296       if (rev.indexIn(line) != -1)
0297       {
0298         set(QLatin1String( "revision" ), rev.cap(1).toUInt());
0299         continue;
0300       }
0301 
0302       QStringList tokenList = KStringHandler::perlSplit(QLatin1Char( '=' ), line, 2);
0303 
0304       if (2 != tokenList.count())
0305       {
0306         continue;
0307       }
0308 
0309       QString key = tokenList[0].trimmed();
0310       QString value = d->unescape ( tokenList[1] );
0311 
0312       if ( QLatin1String( "DTITLE" ) == key )
0313       {
0314         dtitle += value;
0315       }
0316       else if ( key.startsWith(QLatin1String( "TTITLE" )) )
0317       {
0318         uint trackNumber = key.mid(6).toUInt();
0319 
0320         TrackInfo& ti = track(trackNumber);
0321         ti.set(Title, ti.get(Title).toString().append(value));
0322       }
0323 
0324       else if ( QLatin1String( "EXTD" ) == key )
0325       {
0326         d->append(Comment, value);
0327       }
0328       else if ( QLatin1String( "DGENRE" ) == key )
0329       {
0330         d->append(Genre, value);
0331       }
0332       else if ( QLatin1String( "DYEAR" ) == key )
0333       {
0334         set(Year, value);
0335       }
0336       else if ( key.startsWith(QLatin1String( "EXTT" )) )
0337       {
0338         uint trackNumber = key.mid( 4 ).toUInt();
0339 
0340         checkTrack( trackNumber );
0341 
0342         QString extt = track(trackNumber).get(Comment).toString();
0343         track(trackNumber).set(Comment, QVariant(extt + value));
0344       }
0345       else if ( key.startsWith(QLatin1String( "T" )) )
0346       {
0347         // Custom Track data
0348         uint trackNumber = key.mid( key.indexOf(QLatin1Char( '_' ))+1 ).toUInt();
0349         checkTrack( trackNumber );
0350 
0351         QRegularExpression data(QString::fromLatin1("^T.*_%1$").arg(trackNumber));
0352         if  ( key.contains( data ) )
0353         {
0354           QString k = key.mid(1, key.indexOf(QLatin1Char( '_' ))-1);
0355           TrackInfo& ti = track(trackNumber);
0356           ti.set( k, ti.get(k).toString().append(value) );
0357         }
0358       }
0359       else
0360       {
0361         // Custom Disk data
0362         d->append( key, value );
0363       }
0364     }
0365 
0366     int slashPos = dtitle.indexOf(QLatin1String( " / " ));
0367 
0368     if (-1 == slashPos)
0369     {
0370       // Use string for title _and_ artist.
0371       set(Artist, dtitle);
0372       set(Title, dtitle);
0373     }
0374     else
0375     {
0376       set(Artist, dtitle.left(slashPos).trimmed());
0377       set(Title, dtitle.mid(slashPos + 3).trimmed());
0378     }
0379 
0380     bool isSampler = true;
0381     for (TrackInfoList::Iterator it = d->trackInfoList.begin(); it != d->trackInfoList.end(); ++it)
0382     {
0383       if (!(*it).get(Title).toString().contains(QLatin1String( " / " )))
0384       {
0385         isSampler = false;
0386       }
0387     }
0388     for (TrackInfoList::Iterator it = d->trackInfoList.begin(); it != d->trackInfoList.end(); ++it)
0389     {
0390       if (isSampler)
0391       {
0392         int delimiter = (*it).get(Title).toString().indexOf(QLatin1String( " / " ));
0393         (*it).set(Artist, (*it).get(Title).toString().left(delimiter));
0394         (*it).set(Title, (*it).get(Title).toString().mid(delimiter + 3));
0395       }
0396       else
0397       {
0398         (*it).set(Artist, get(Artist));
0399       }
0400     }
0401 
0402     if ( get(Genre).toString().isEmpty() )
0403       set(Genre, QLatin1String( "Unknown" ));
0404 
0405     qCDebug(LIBKCDDB) << "Loaded CDInfo for " << get(QLatin1String( "discid" )).toString();
0406 
0407     return true;
0408   }
0409 
0410     QString
0411   CDInfo::toString(bool submit) const
0412   {
0413     QString s;
0414 
0415     if (get(QLatin1String( "revision" )) != 0)
0416       s += QLatin1String( "# Revision: " ) + get(QLatin1String( "revision" )).toString() + QLatin1Char( '\n' );
0417 
0418     // If we are submiting make it a fully compliant CDDB entry
0419     if (submit)
0420     {
0421       s += QLatin1String( "#\n" );
0422       s += QString::fromLatin1("# Submitted via: %1 %2\n").arg(CDDB::clientName(),
0423         CDDB::clientVersion());
0424     }
0425 
0426     s += d->createLine(QLatin1String( "DISCID" ), get(QLatin1String( "discid" )).toString() );
0427     QString artist = get(Artist).toString();
0428     s += d->createLine(QLatin1String( "DTITLE" ), artist + QLatin1String( " / " ) + get(Title).toString() );
0429     int year = get(Year).toInt();
0430     s += QLatin1String( "DYEAR=" ) + (0 == year ? QString() : QString::number(year)) + QLatin1Char( '\n' ); //krazy:exclude=nullstrassign for old broken gcc
0431     if (get(Genre) == QLatin1String( "Unknown" ))
0432       s += d->createLine(QLatin1String( "DGENRE" ), QString());
0433     else
0434       s += d->createLine(QLatin1String( "DGENRE" ),get(Genre).toString());
0435 
0436     bool isSampler = false;
0437     for (int i = 0; i < d->trackInfoList.count(); ++i){
0438       QString trackArtist = d->trackInfoList[i].get(Artist).toString();
0439       if (!trackArtist.isEmpty() && trackArtist != artist)
0440       {
0441         isSampler = true;
0442         break;
0443       }
0444     }
0445 
0446     for (int i = 0; i < d->trackInfoList.count(); ++i){
0447       QString trackTitle = d->trackInfoList[i].get(Title).toString();
0448       QString trackArtist = d->trackInfoList[i].get(Artist).toString();
0449       if (isSampler)
0450       {
0451         if (trackArtist.isEmpty())
0452           s += d->createLine(QString::fromLatin1("TTITLE%1").arg(i), QString::fromLatin1("%1 / %2").arg(artist).arg(trackTitle));
0453         else
0454           s += d->createLine(QString::fromLatin1("TTITLE%1").arg(i), QString::fromLatin1("%1 / %2").arg(trackArtist).arg(trackTitle));
0455       }
0456       else
0457       {
0458           s += d->createLine(QString::fromLatin1("TTITLE%1").arg(i), trackTitle);
0459       }
0460     }
0461 
0462     s += d->createLine(QLatin1String("EXTD"), get(Comment).toString());
0463 
0464     for (int i = 0; i < d->trackInfoList.count(); ++i)
0465         s += d->createLine(QString::fromLatin1("EXTT%1").arg(i), d->trackInfoList[i].get(Comment).toString());
0466 
0467     if (submit)
0468     {
0469       s += d->createLine(QLatin1String( "PLAYORDER" ), QString());
0470       return s;
0471     }
0472 
0473     s += d->createLine(QLatin1String( "PLAYORDER" ), get(QLatin1String( "playorder" )).toString() );
0474 
0475     // Custom track data
0476     for (int i = 0; i < d->trackInfoList.count(); ++i)
0477       s += d->trackInfoList[i].toString();
0478 
0479     QStringList cddbKeywords;
0480     cddbKeywords
0481       << QLatin1String( "DISCID" )
0482       << QLatin1String( "ARTIST" )
0483       << QLatin1String( "TITLE" )
0484       << QLatin1String( "COMMENT" )
0485       << QLatin1String( "YEAR" )
0486       << QLatin1String( "GENRE" )
0487       << QLatin1String( "PLAYORDER" )
0488       << QLatin1String( "CATEGORY" )
0489       << QLatin1String( "REVISION" );
0490 
0491     // Custom disc data
0492     QMap<QString, QVariant>::const_iterator i = d->data.constBegin();
0493     while (i != d->data.constEnd()){
0494       if (!cddbKeywords.contains(i.key()) && i.key() != QLatin1String( "SOURCE" ))
0495       {
0496         s+= d->createLine(i.key(), i.value().toString());
0497       }
0498       ++i;
0499     }
0500 
0501     return s;
0502   }
0503 
0504   QVariant CDInfo::get(Type type) const {
0505     return d->get(type);
0506   }
0507 
0508   QVariant CDInfo::get(const QString &type) const {
0509     return d->get(type);
0510   }
0511 
0512   void CDInfo::set(const QString &type, const QVariant &data){
0513     d->set(type, data);
0514   }
0515 
0516   void CDInfo::set(Type type, const QVariant &data) {
0517     d->set(type, data);
0518   }
0519 
0520 
0521     void
0522   CDInfo::checkTrack( int trackNumber )
0523   {
0524     while ( d->trackInfoList.count() <= trackNumber ){
0525       int count = d->trackInfoList.count();
0526       d->trackInfoList.append(TrackInfo());
0527       d->trackInfoList[count].set(QLatin1String( "tracknumber" ), count);
0528     }
0529   }
0530 
0531     void
0532   CDInfo::clear()
0533   {
0534     d->data.clear();
0535     d->trackInfoList.clear();
0536   }
0537 
0538     bool
0539   CDInfo::isValid() const
0540   {
0541     QString discid = get(QLatin1String( "DISCID" )).toString();
0542     if (discid.isEmpty())
0543       return false;
0544 
0545     if (discid == QLatin1String( "0" ))
0546       return false;
0547 
0548     return true;
0549   }
0550 
0551     TrackInfo &
0552   CDInfo::track( int trackNumber )
0553   {
0554     checkTrack( trackNumber );
0555     return d->trackInfoList[trackNumber];
0556   }
0557 
0558     TrackInfo
0559   CDInfo::track( int trackNumber ) const
0560   {
0561     if (trackNumber < d->trackInfoList.count())
0562       return d->trackInfoList[trackNumber];
0563     else
0564     {
0565       qWarning() << "Couldn't find track " << trackNumber;
0566       return TrackInfo();
0567     }
0568   }
0569 
0570     int
0571   CDInfo::numberOfTracks() const
0572   {
0573     return d->trackInfoList.count();
0574   }
0575 
0576     bool CDInfo::operator==( const CDInfo& other ) const
0577     {
0578         return(  d->data == other.d->data &&
0579                  d->trackInfoList == other.d->trackInfoList );
0580     }
0581 
0582     bool CDInfo::operator!=( const CDInfo& other ) const
0583     {
0584         return(  d->data != other.d->data ||
0585                  d->trackInfoList != other.d->trackInfoList );
0586     }
0587 }
0588 
0589 // vim:tabstop=2:shiftwidth=2:expandtab:cinoptions=(s,U1,m1