1 /* 2 SPDX-FileCopyrightText: 2002 Rik Hemsley (rikkus) <rik@kde.org> 3 SPDX-FileCopyrightText: 2002-2005 Benjamin Meyer <ben-devel@meyerhome.net> 4 SPDX-FileCopyrightText: 2002-2004 Nadeem Hasan <nhasan@nadmm.com> 5 SPDX-FileCopyrightText: 2006 Richard Lärkäng <nouseforaname@home.se> 6 7 SPDX-License-Identifier: LGPL-2.0-or-later 8 */ 9 10 #include "cdinfo.h" 11 12 #include "client.h" 13 #include "cddb.h" 14 #include "logging.h" 15 16 #include <KStringHandler> 17 #include <QDebug> 18 19 #include <QMap> 20 21 namespace KCDDB 22 { 23 class InfoBasePrivate { 24 public: 25 /** 26 * Creates a line in the form NAME=VALUE, and splits it into several 27 * lines if the line gets longer than 256 chars 28 */ 29 static QString createLine(const QString & name,const QString & value)30 createLine(const QString& name, const QString& value) 31 { 32 Q_ASSERT(name.length() < 254); 33 34 int maxLength = 256 - name.length() - 2; 35 36 QString tmpValue = escape(value); 37 38 QString lines; 39 40 while (tmpValue.length() > maxLength) 41 { 42 lines += QString::fromLatin1("%1=%2\n").arg(name,tmpValue.left(maxLength)); 43 tmpValue = tmpValue.mid(maxLength); 44 } 45 46 lines += QString::fromLatin1("%1=%2\n").arg(name,tmpValue); 47 48 return lines; 49 } 50 51 /** 52 * escape's string for CDDB processing 53 */ 54 static QString escape(const QString & value)55 escape( const QString &value ) 56 { 57 QString s = value; 58 s.replace( QLatin1String( "\\" ), QLatin1String( "\\\\" ) ); 59 s.replace( QLatin1String( "\n" ), QLatin1String( "\\n" ) ); 60 s.replace( QLatin1String( "\t" ), QLatin1String( "\\t" ) ); 61 62 return s; 63 } 64 65 /** 66 * fixes an escaped string that has been CDDB processed 67 */ 68 static QString unescape(const QString & value)69 unescape( const QString &value ) 70 { 71 QString s = value; 72 73 s.replace( QLatin1String( "\\n" ), QLatin1String( "\n" ) ); 74 s.replace( QLatin1String( "\\t" ), QLatin1String( "\t" ) ); 75 s.replace( QLatin1String( "\\\\" ), QLatin1String( "\\" ) ); 76 77 return s; 78 } 79 80 QVariant get(const QString & type)81 get(const QString& type) 82 { 83 return data[type.toUpper()]; 84 } 85 QVariant get(Type type)86 get(Type type) 87 { 88 switch(type){ 89 case(Title): 90 return get(QLatin1String( "title" )); 91 case(Comment): 92 return get(QLatin1String( "comment" )); 93 case(Artist): 94 return get(QLatin1String( "artist" )); 95 case(Genre): 96 return get(QLatin1String( "genre" )); 97 case(Year): 98 return get(QLatin1String( "year" )); 99 case(Length): 100 return get(QLatin1String( "length" )); 101 case(Category): 102 return get(QLatin1String( "category" )); 103 } 104 return QVariant(); 105 } 106 107 void set(const QString & type,const QVariant & d)108 set(const QString& type, const QVariant &d) 109 { 110 //qDebug() << "set: " << type << ", " << d.toString(); 111 if(type.contains(QRegExp( QLatin1String( "^T.*_.*$" )) )){ 112 qCDebug(LIBKCDDB) << "Error: custom cdinfo::set data can not start with T and contain a _"; 113 return; 114 } 115 if(type.toUpper() == QLatin1String( "DTITLE" )){ 116 qCDebug(LIBKCDDB) << "Error: type: DTITLE is reserved and can not be set."; 117 return; 118 } 119 120 data[type.toUpper()] = d; 121 } 122 void set(Type type,const QVariant & d)123 set(Type type, const QVariant &d) 124 { 125 switch(type) 126 { 127 case(Title): 128 set(QLatin1String( "title" ), d); 129 return; 130 case(Comment): 131 set(QLatin1String( "comment" ), d); 132 return; 133 case(Artist): 134 set(QLatin1String( "artist" ), d); 135 return; 136 case(Genre): 137 set(QLatin1String( "genre" ), d); 138 return; 139 case(Year): 140 set(QLatin1String( "year" ), d); 141 return; 142 case(Length): 143 set(QLatin1String( "length" ), d); 144 return; 145 case(Category): 146 set(QLatin1String( "category" ), d); 147 return; 148 } 149 150 Q_ASSERT(false); 151 } 152 153 // Appends text to data instead of overwriting it 154 void append(const QString & type,const QString & text)155 append(const QString& type, const QString& text) 156 { 157 set(type, get(type).toString().append(text)); 158 } 159 void append(Type type,const QString & text)160 append(Type type, const QString& text) 161 { 162 set(type, get(type).toString().append(text)); 163 } 164 165 QMap<QString, QVariant> data; 166 } ; 167 168 class TrackInfoPrivate : public InfoBasePrivate { 169 }; 170 TrackInfo()171 TrackInfo::TrackInfo() 172 { 173 d = new TrackInfoPrivate(); 174 } 175 TrackInfo(const TrackInfo & clone)176 TrackInfo::TrackInfo(const TrackInfo& clone) 177 : d(new TrackInfoPrivate) 178 { 179 d->data = clone.d->data; 180 } 181 ~TrackInfo()182 TrackInfo::~TrackInfo() 183 { 184 delete d; 185 } 186 operator =(const TrackInfo & clone)187 TrackInfo& TrackInfo::operator=(const TrackInfo& clone) 188 { 189 d->data = clone.d->data; 190 return *this; 191 } 192 get(Type type) const193 QVariant TrackInfo::get(Type type) const { 194 return d->get(type); 195 } 196 get(const QString & type) const197 QVariant TrackInfo::get(const QString &type) const { 198 return d->get(type); 199 } 200 set(const QString & type,const QVariant & data)201 void TrackInfo::set(const QString &type, const QVariant &data){ 202 d->set(type, data); 203 } 204 set(Type type,const QVariant & data)205 void TrackInfo::set(Type type, const QVariant &data) { 206 d->set(type, data); 207 } 208 clear()209 void TrackInfo::clear(){ 210 d->data.clear(); 211 } 212 toString() const213 QString TrackInfo::toString() const { 214 QString out; 215 bool ok; 216 int track = get(QLatin1String( "tracknumber" )).toInt(&ok); 217 if(!ok) 218 qCDebug(LIBKCDDB) << "Warning toString() on a track that doesn't have track number assigned."; 219 QMap<QString, QVariant>::const_iterator i = d->data.constBegin(); 220 while (i != d->data.constEnd()) { 221 if(i.key() != QLatin1String( "COMMENT" ) && i.key() != QLatin1String( "TITLE" ) && i.key() != QLatin1String( "ARTIST" ) && i.key() != QLatin1String( "TRACKNUMBER" )) { 222 out += d->createLine(QString::fromLatin1("T%1_%2").arg(i.key()).arg(track),i.value().toString()); 223 } 224 ++i; 225 } 226 return out; 227 } 228 operator ==(const TrackInfo & other) const229 bool TrackInfo::operator==( const TrackInfo& other ) const 230 { 231 return d->data == other.d->data; 232 } 233 operator !=(const TrackInfo & other) const234 bool TrackInfo::operator!=( const TrackInfo& other ) const 235 { 236 return d->data != other.d->data; 237 } 238 239 class CDInfoPrivate : public InfoBasePrivate { 240 public: 241 TrackInfoList trackInfoList; 242 }; 243 CDInfo()244 CDInfo::CDInfo() 245 : d(new CDInfoPrivate()) 246 { 247 set(QLatin1String( "revision" ), 0); 248 } 249 CDInfo(const CDInfo & clone)250 CDInfo::CDInfo(const CDInfo& clone) 251 : d(new CDInfoPrivate()) 252 { 253 d->data = clone.d->data; 254 d->trackInfoList = clone.d->trackInfoList; 255 } 256 ~CDInfo()257 CDInfo::~CDInfo() 258 { 259 delete d; 260 } 261 operator =(const CDInfo & clone)262 CDInfo& CDInfo::operator=(const CDInfo& clone) 263 { 264 d->trackInfoList = clone.d->trackInfoList; 265 d->data = clone.d->data; 266 return *this; 267 } 268 269 bool load(const QString & string)270 CDInfo::load(const QString & string) 271 { 272 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) 273 return load(string.split(QLatin1Char( '\n' ),QString::SkipEmptyParts)); 274 #else 275 return load(string.split(QLatin1Char( '\n' ),Qt::SkipEmptyParts)); 276 #endif 277 } 278 279 bool load(const QStringList & lineList)280 CDInfo::load(const QStringList & lineList) 281 { 282 clear(); 283 284 // We'll append to this until we've seen all the lines, then parse it after. 285 QString dtitle; 286 287 QStringList::ConstIterator it = lineList.begin(); 288 289 QRegExp rev(QLatin1String( "# Revision: (\\d+)" )); 290 QRegExp eol(QLatin1String( "[\r\n]" )); 291 292 while ( it != lineList.end() ) 293 { 294 QString line(*it); 295 line.replace(eol,QLatin1String( "" )); 296 ++it; 297 298 if (rev.indexIn(line) != -1) 299 { 300 set(QLatin1String( "revision" ), rev.cap(1).toUInt()); 301 continue; 302 } 303 304 QStringList tokenList = KStringHandler::perlSplit(QLatin1Char( '=' ), line, 2); 305 306 if (2 != tokenList.count()) 307 { 308 continue; 309 } 310 311 QString key = tokenList[0].trimmed(); 312 QString value = d->unescape ( tokenList[1] ); 313 314 if ( QLatin1String( "DTITLE" ) == key ) 315 { 316 dtitle += value; 317 } 318 else if ( key.startsWith(QLatin1String( "TTITLE" )) ) 319 { 320 uint trackNumber = key.mid(6).toUInt(); 321 322 TrackInfo& ti = track(trackNumber); 323 ti.set(Title, ti.get(Title).toString().append(value)); 324 } 325 326 else if ( QLatin1String( "EXTD" ) == key ) 327 { 328 d->append(Comment, value); 329 } 330 else if ( QLatin1String( "DGENRE" ) == key ) 331 { 332 d->append(Genre, value); 333 } 334 else if ( QLatin1String( "DYEAR" ) == key ) 335 { 336 set(Year, value); 337 } 338 else if ( key.startsWith(QLatin1String( "EXTT" )) ) 339 { 340 uint trackNumber = key.mid( 4 ).toUInt(); 341 342 checkTrack( trackNumber ); 343 344 QString extt = track(trackNumber).get(Comment).toString(); 345 track(trackNumber).set(Comment, QVariant(extt + value)); 346 } 347 else if ( key.startsWith(QLatin1String( "T" )) ) 348 { 349 // Custom Track data 350 uint trackNumber = key.mid( key.indexOf(QLatin1Char( '_' ))+1 ).toUInt(); 351 checkTrack( trackNumber ); 352 353 QRegExp data(QString::fromLatin1("^T.*_%1$").arg(trackNumber)); 354 if ( key.contains( data ) ) 355 { 356 QString k = key.mid(1, key.indexOf(QLatin1Char( '_' ))-1); 357 TrackInfo& ti = track(trackNumber); 358 ti.set( k, ti.get(k).toString().append(value) ); 359 } 360 } 361 else 362 { 363 // Custom Disk data 364 d->append( key, value ); 365 } 366 } 367 368 int slashPos = dtitle.indexOf(QLatin1String( " / " )); 369 370 if (-1 == slashPos) 371 { 372 // Use string for title _and_ artist. 373 set(Artist, dtitle); 374 set(Title, dtitle); 375 } 376 else 377 { 378 set(Artist, dtitle.left(slashPos).trimmed()); 379 set(Title, dtitle.mid(slashPos + 3).trimmed()); 380 } 381 382 bool isSampler = true; 383 for (TrackInfoList::Iterator it = d->trackInfoList.begin(); it != d->trackInfoList.end(); ++it) 384 { 385 if (!(*it).get(Title).toString().contains(QLatin1String( " / " ))) 386 { 387 isSampler = false; 388 } 389 } 390 for (TrackInfoList::Iterator it = d->trackInfoList.begin(); it != d->trackInfoList.end(); ++it) 391 { 392 if (isSampler) 393 { 394 int delimiter = (*it).get(Title).toString().indexOf(QLatin1String( " / " )); 395 (*it).set(Artist, (*it).get(Title).toString().left(delimiter)); 396 (*it).set(Title, (*it).get(Title).toString().mid(delimiter + 3)); 397 } 398 else 399 { 400 (*it).set(Artist, get(Artist)); 401 } 402 } 403 404 if ( get(Genre).toString().isEmpty() ) 405 set(Genre, QLatin1String( "Unknown" )); 406 407 qCDebug(LIBKCDDB) << "Loaded CDInfo for " << get(QLatin1String( "discid" )).toString(); 408 409 return true; 410 } 411 412 QString toString(bool submit) const413 CDInfo::toString(bool submit) const 414 { 415 QString s; 416 417 if (get(QLatin1String( "revision" )) != 0) 418 s += QLatin1String( "# Revision: " ) + get(QLatin1String( "revision" )).toString() + QLatin1Char( '\n' ); 419 420 // If we are submiting make it a fully compliant CDDB entry 421 if (submit) 422 { 423 s += QLatin1String( "#\n" ); 424 s += QString::fromLatin1("# Submitted via: %1 %2\n").arg(CDDB::clientName(), 425 CDDB::clientVersion()); 426 } 427 428 s += d->createLine(QLatin1String( "DISCID" ), get(QLatin1String( "discid" )).toString() ); 429 QString artist = get(Artist).toString(); 430 s += d->createLine(QLatin1String( "DTITLE" ), artist + QLatin1String( " / " ) + get(Title).toString() ); 431 int year = get(Year).toInt(); 432 s += QLatin1String( "DYEAR=" ) + (0 == year ? QString() : QString::number(year)) + QLatin1Char( '\n' ); //krazy:exclude=nullstrassign for old broken gcc 433 if (get(Genre) == QLatin1String( "Unknown" )) 434 s += d->createLine(QLatin1String( "DGENRE" ), QString()); 435 else 436 s += d->createLine(QLatin1String( "DGENRE" ),get(Genre).toString()); 437 438 bool isSampler = false; 439 for (int i = 0; i < d->trackInfoList.count(); ++i){ 440 QString trackArtist = d->trackInfoList[i].get(Artist).toString(); 441 if (!trackArtist.isEmpty() && trackArtist != artist) 442 { 443 isSampler = true; 444 break; 445 } 446 } 447 448 for (int i = 0; i < d->trackInfoList.count(); ++i){ 449 QString trackTitle = d->trackInfoList[i].get(Title).toString(); 450 QString trackArtist = d->trackInfoList[i].get(Artist).toString(); 451 if (isSampler) 452 { 453 if (trackArtist.isEmpty()) 454 s += d->createLine(QString::fromLatin1("TTITLE%1").arg(i), QString::fromLatin1("%1 / %2").arg(artist).arg(trackTitle)); 455 else 456 s += d->createLine(QString::fromLatin1("TTITLE%1").arg(i), QString::fromLatin1("%1 / %2").arg(trackArtist).arg(trackTitle)); 457 } 458 else 459 { 460 s += d->createLine(QString::fromLatin1("TTITLE%1").arg(i), trackTitle); 461 } 462 } 463 464 s += d->createLine(QLatin1String("EXTD"), get(Comment).toString()); 465 466 for (int i = 0; i < d->trackInfoList.count(); ++i) 467 s += d->createLine(QString::fromLatin1("EXTT%1").arg(i), d->trackInfoList[i].get(Comment).toString()); 468 469 if (submit) 470 { 471 s += d->createLine(QLatin1String( "PLAYORDER" ), QString()); 472 return s; 473 } 474 475 s += d->createLine(QLatin1String( "PLAYORDER" ), get(QLatin1String( "playorder" )).toString() ); 476 477 // Custom track data 478 for (int i = 0; i < d->trackInfoList.count(); ++i) 479 s += d->trackInfoList[i].toString(); 480 481 QStringList cddbKeywords; 482 cddbKeywords 483 << QLatin1String( "DISCID" ) 484 << QLatin1String( "ARTIST" ) 485 << QLatin1String( "TITLE" ) 486 << QLatin1String( "COMMENT" ) 487 << QLatin1String( "YEAR" ) 488 << QLatin1String( "GENRE" ) 489 << QLatin1String( "PLAYORDER" ) 490 << QLatin1String( "CATEGORY" ) 491 << QLatin1String( "REVISION" ); 492 493 // Custom disc data 494 QMap<QString, QVariant>::const_iterator i = d->data.constBegin(); 495 while (i != d->data.constEnd()){ 496 if (!cddbKeywords.contains(i.key()) && i.key() != QLatin1String( "SOURCE" )) 497 { 498 s+= d->createLine(i.key(), i.value().toString()); 499 } 500 ++i; 501 } 502 503 return s; 504 } 505 get(Type type) const506 QVariant CDInfo::get(Type type) const { 507 return d->get(type); 508 } 509 get(const QString & type) const510 QVariant CDInfo::get(const QString &type) const { 511 return d->get(type); 512 } 513 set(const QString & type,const QVariant & data)514 void CDInfo::set(const QString &type, const QVariant &data){ 515 d->set(type, data); 516 } 517 set(Type type,const QVariant & data)518 void CDInfo::set(Type type, const QVariant &data) { 519 d->set(type, data); 520 } 521 522 523 void checkTrack(int trackNumber)524 CDInfo::checkTrack( int trackNumber ) 525 { 526 while ( d->trackInfoList.count() <= trackNumber ){ 527 int count = d->trackInfoList.count(); 528 d->trackInfoList.append(TrackInfo()); 529 d->trackInfoList[count].set(QLatin1String( "tracknumber" ), count); 530 } 531 } 532 533 void clear()534 CDInfo::clear() 535 { 536 d->data.clear(); 537 d->trackInfoList.clear(); 538 } 539 540 bool isValid() const541 CDInfo::isValid() const 542 { 543 QString discid = get(QLatin1String( "DISCID" )).toString(); 544 if (discid.isEmpty()) 545 return false; 546 547 if (discid == QLatin1String( "0" )) 548 return false; 549 550 return true; 551 } 552 553 TrackInfo & track(int trackNumber)554 CDInfo::track( int trackNumber ) 555 { 556 checkTrack( trackNumber ); 557 return d->trackInfoList[trackNumber]; 558 } 559 560 TrackInfo track(int trackNumber) const561 CDInfo::track( int trackNumber ) const 562 { 563 if (trackNumber < d->trackInfoList.count()) 564 return d->trackInfoList[trackNumber]; 565 else 566 { 567 qWarning() << "Couldn't find track " << trackNumber; 568 return TrackInfo(); 569 } 570 } 571 572 int numberOfTracks() const573 CDInfo::numberOfTracks() const 574 { 575 return d->trackInfoList.count(); 576 } 577 operator ==(const CDInfo & other) const578 bool CDInfo::operator==( const CDInfo& other ) const 579 { 580 return( d->data == other.d->data && 581 d->trackInfoList == other.d->trackInfoList ); 582 } 583 operator !=(const CDInfo & other) const584 bool CDInfo::operator!=( const CDInfo& other ) const 585 { 586 return( d->data != other.d->data || 587 d->trackInfoList != other.d->trackInfoList ); 588 } 589 } 590 591 // vim:tabstop=2:shiftwidth=2:expandtab:cinoptions=(s,U1,m1 592