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