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