KCDDB

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

KDE's Doxygen guidelines are available online.