File indexing completed on 2024-05-19 04:49:45

0001 /****************************************************************************************
0002  * Copyright (c) 2013 Konrad Zemek <konrad.zemek@gmail.com>                             *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "ITunesProvider.h"
0018 
0019 #include "ITunesTrack.h"
0020 #include "core/support/Debug.h"
0021 
0022 #include <QFile>
0023 #include <QTemporaryFile>
0024 #include <QXmlStreamReader>
0025 #include <QXmlStreamWriter>
0026 
0027 using namespace StatSyncing;
0028 
0029 ITunesProvider::ITunesProvider( const QVariantMap &config, ImporterManager *importer )
0030     : ImporterProvider( config, importer )
0031 {
0032 }
0033 
0034 ITunesProvider::~ITunesProvider()
0035 {
0036 }
0037 
0038 qint64
0039 ITunesProvider::reliableTrackMetaData() const
0040 {
0041     return Meta::valTitle | Meta::valArtist | Meta::valAlbum | Meta::valComposer
0042             | Meta::valYear | Meta::valTrackNr | Meta::valDiscNr;
0043 }
0044 
0045 qint64
0046 ITunesProvider::writableTrackStatsData() const
0047 {
0048     return Meta::valRating | Meta::valPlaycount;
0049 }
0050 
0051 QSet<QString>
0052 ITunesProvider::artists()
0053 {
0054     readXml( QString() );
0055     QSet<QString> result;
0056     result.swap( m_artists );
0057     return result;
0058 }
0059 
0060 TrackList
0061 ITunesProvider::artistTracks( const QString &artistName )
0062 {
0063     readXml( artistName );
0064     TrackList result;
0065     result.swap( m_artistTracks );
0066     return result;
0067 }
0068 
0069 void
0070 ITunesProvider::readXml( const QString &byArtist )
0071 {
0072     QFile dbFile( m_config.value( "dbPath" ).toString() );
0073     if( dbFile.open( QIODevice::ReadOnly ) )
0074     {
0075         QXmlStreamReader xml( &dbFile );
0076         if( xml.readNextStartElement() )
0077         {
0078             if( xml.name() == "plist" && xml.attributes().value("version") == "1.0" )
0079                 readPlist( xml, byArtist );
0080             else
0081                 xml.raiseError( "the database is ill-formed or version unsupported" );
0082         }
0083 
0084         if( xml.hasError() )
0085             warning() << "There was an error reading" << dbFile.fileName() << ":"
0086                       << xml.errorString();
0087     }
0088     else
0089         warning() << __PRETTY_FUNCTION__ << "couldn't open" << dbFile.fileName();
0090 }
0091 
0092 void
0093 ITunesProvider::readPlist( QXmlStreamReader &xml, const QString &byArtist )
0094 {
0095     Q_ASSERT( xml.isStartElement() && xml.name() == "plist" );
0096     xml.readNextStartElement();
0097     Q_ASSERT( xml.isStartElement() && xml.name() == "dict" );
0098 
0099     while( xml.readNextStartElement() )
0100     {
0101         if( xml.name() == "key" )
0102         {
0103             if( xml.readElementText() == "Tracks" )
0104                 readTracks( xml, byArtist );
0105         }
0106         else
0107             xml.skipCurrentElement();
0108     }
0109 }
0110 
0111 void
0112 ITunesProvider::readTracks( QXmlStreamReader &xml, const QString &byArtist )
0113 {
0114     Q_ASSERT( xml.isEndElement() && xml.name() == "key" );
0115     xml.readNextStartElement();
0116     Q_ASSERT( xml.isStartElement() && xml.name() == "dict" );
0117 
0118     while( xml.readNextStartElement() )
0119         readTrack( xml, byArtist );
0120 }
0121 
0122 void
0123 ITunesProvider::readTrack( QXmlStreamReader &xml, const QString &byArtist )
0124 {
0125     Q_ASSERT( xml.isStartElement() && xml.name() == "key" );
0126     xml.skipCurrentElement();
0127     xml.readNextStartElement();
0128     Q_ASSERT( xml.isStartElement() && xml.name() == "dict" );
0129 
0130     Meta::FieldHash metadata;
0131     QString currentArtist;
0132     int trackId = -1;
0133 
0134     while( xml.readNextStartElement() )
0135     {
0136         // We're only interested in this track if it's by right artist, or if we haven't
0137         // found the artist yet
0138         if( xml.name() == "key"
0139                  && ( currentArtist.isEmpty() || currentArtist == byArtist ) )
0140         {
0141             const QString type = xml.readElementText();
0142 
0143             // If byArtist param is not set, we're only interested in track Artist
0144             if( byArtist.isEmpty() )
0145             {
0146                 if( type == "Artist" )
0147                     currentArtist = readValue( xml );
0148             }
0149             else
0150             {
0151                 if( type == "Track ID" )
0152                 {
0153                     trackId = readValue( xml ).toInt();
0154                 }
0155                 else if( type == "Name" )
0156                     metadata.insert( Meta::valTitle, readValue( xml ) );
0157                 else if( type == "Artist" )
0158                 {
0159                     currentArtist = readValue( xml );
0160                     metadata.insert( Meta::valArtist, currentArtist );
0161                 }
0162                 else if( type == "Album" )
0163                     metadata.insert( Meta::valAlbum, readValue( xml ) );
0164                 else if( type == "Composer" )
0165                     metadata.insert( Meta::valComposer, readValue( xml ) );
0166                 else if( type == "Year" )
0167                     metadata.insert( Meta::valYear, readValue( xml ) );
0168                 else if( type == "Track Number" )
0169                     metadata.insert( Meta::valTrackNr, readValue( xml ) );
0170                 else if( type == "Disc Number" )
0171                     metadata.insert( Meta::valDiscNr, readValue( xml ) );
0172                 else if( type == "Rating" )
0173                     metadata.insert( Meta::valRating, readValue( xml ) );
0174                 else if( type == "Play Date UTC" )
0175                     metadata.insert( Meta::valLastPlayed, readValue( xml ) );
0176                 else if( type == "Play Count" )
0177                     metadata.insert( Meta::valPlaycount, readValue( xml ) );
0178             }
0179         }
0180         else
0181             xml.skipCurrentElement();
0182     }
0183 
0184     if( !byArtist.isEmpty() && currentArtist == byArtist && trackId != -1 )
0185     {
0186         ITunesTrack *track = new ITunesTrack( trackId, metadata );
0187         connect( track, &ITunesTrack::commitCalled,
0188                  this, &ITunesProvider::trackUpdated, Qt::DirectConnection );
0189         m_artistTracks << TrackPtr( track );
0190     }
0191     else if( byArtist.isEmpty() )
0192         m_artists << currentArtist;
0193 }
0194 
0195 QString
0196 ITunesProvider::readValue( QXmlStreamReader &xml )
0197 {
0198     Q_ASSERT( xml.isEndElement() && xml.name() == "key" );
0199     xml.readNextStartElement();
0200     Q_ASSERT( xml.isStartElement() );
0201     return xml.readElementText();
0202 }
0203 
0204 void
0205 ITunesProvider::writeTracks( QXmlStreamReader &reader, QXmlStreamWriter &writer,
0206                              const QMap<int, Meta::FieldHash> &dirtyData )
0207 {
0208     int dictDepth = 0;
0209     while( !reader.isEndElement() || reader.name() != "dict" || dictDepth != 0 )
0210     {
0211         reader.readNext();
0212 
0213         if( reader.error() )
0214         {
0215             warning() << __PRETTY_FUNCTION__ << reader.errorString();
0216             return;
0217         }
0218 
0219         writer.writeCurrentToken( reader );
0220 
0221         if( reader.isStartElement() && reader.name() == "key" && dictDepth == 1 )
0222         {
0223             int trackId = reader.readElementText().toInt();
0224             writer.writeCharacters( QString::number( trackId ) );
0225             writer.writeCurrentToken( reader );
0226 
0227             if( dirtyData.contains( trackId ) )
0228                 writeTrack( reader, writer, dirtyData.value( trackId ) );
0229         }
0230         else if( reader.isStartElement() && reader.name() == "dict" )
0231             ++dictDepth;
0232         else if( reader.isEndElement() && reader.name() == "dict" )
0233             --dictDepth;
0234     }
0235 }
0236 
0237 void
0238 ITunesProvider::writeTrack( QXmlStreamReader &reader, QXmlStreamWriter &writer,
0239                             const Meta::FieldHash &dirtyData )
0240 {
0241     QString keyWhitespace;
0242     QString lastWhitespace;
0243 
0244     while( !reader.isEndElement() || reader.name() != "dict" )
0245     {
0246         reader.readNext();
0247 
0248         if( reader.error() )
0249         {
0250             warning() << __PRETTY_FUNCTION__ << reader.errorString();
0251             return;
0252         }
0253 
0254         if( reader.isWhitespace() ) // control whitespace, we want nicely formatted file
0255         {
0256             lastWhitespace = reader.text().toString();
0257         }
0258         else if( reader.isStartElement() && reader.name() == "key" )
0259         {
0260             keyWhitespace = lastWhitespace;
0261             const QString type = reader.readElementText();
0262 
0263             if( type == "Rating" || type == "Play Count" )
0264             {
0265                 reader.readNextStartElement(); // <integer>
0266                 reader.skipCurrentElement();
0267             }
0268             else
0269             {
0270                 writer.writeCharacters( lastWhitespace );
0271                 writer.writeTextElement( "key", type );
0272             }
0273 
0274             lastWhitespace.clear();
0275         }
0276         else if( !reader.isEndElement() || reader.name() != "dict" )
0277         {
0278             writer.writeCharacters( lastWhitespace );
0279             writer.writeCurrentToken( reader );
0280             lastWhitespace.clear();
0281         }
0282     }
0283 
0284     if( const int rating = dirtyData.value( Meta::valRating ).toInt() )
0285     {
0286         writer.writeCharacters( keyWhitespace );
0287         writer.writeTextElement( "key", "Rating" );
0288         writer.writeTextElement( "integer", QString::number( rating ) );
0289     }
0290     if( const int playCount = dirtyData.value( Meta::valPlaycount ).toInt() )
0291     {
0292         writer.writeCharacters( keyWhitespace );
0293         writer.writeTextElement( "key", "Play Count" );
0294         writer.writeTextElement( "integer", QString::number( playCount ) );
0295     }
0296 
0297     writer.writeCharacters( lastWhitespace );
0298     writer.writeCurrentToken( reader );
0299     reader.readNext();
0300 }
0301 
0302 void
0303 ITunesProvider::trackUpdated( const int trackId, const Meta::FieldHash &statistics )
0304 {
0305     QMutexLocker lock( &m_dirtyMutex );
0306     m_dirtyData.insert( trackId, statistics );
0307 }
0308 
0309 void
0310 ITunesProvider::commitTracks()
0311 {
0312     QMutexLocker lock( &m_dirtyMutex );
0313     if( m_dirtyData.empty() )
0314         return;
0315 
0316     QMap<int, Meta::FieldHash> dirtyData;
0317     dirtyData.swap( m_dirtyData );
0318 
0319     QFile dbFile( m_config.value( "dbPath" ).toString() );
0320     if( !dbFile.open( QIODevice::ReadOnly ) )
0321     {
0322         warning() << __PRETTY_FUNCTION__ << dbFile.fileName() << "is not readable";
0323         return;
0324     }
0325 
0326     QTemporaryFile tmpFile;
0327     if( !tmpFile.open() )
0328     {
0329         warning() << __PRETTY_FUNCTION__ << tmpFile.fileName() << "is not writable";
0330         return;
0331     }
0332 
0333     QXmlStreamReader reader( &dbFile );
0334     QXmlStreamWriter writer( &tmpFile );
0335 
0336     while( !reader.atEnd() )
0337     {
0338         reader.readNext();
0339 
0340         if( reader.error() )
0341         {
0342             warning() << __PRETTY_FUNCTION__ << "Error reading" << dbFile.fileName();
0343             return;
0344         }
0345 
0346         if( reader.isStartElement() && reader.name() == "key" )
0347         {
0348             const QString text = reader.readElementText();
0349             writer.writeTextElement( "key", text );
0350 
0351             if( text == "Tracks" )
0352                 writeTracks( reader, writer, dirtyData );
0353         }
0354         else if( reader.isStartDocument() )
0355             writer.writeStartDocument( reader.documentVersion().toString(),
0356                                        reader.isStandaloneDocument() );
0357         else
0358             writer.writeCurrentToken( reader );
0359     }
0360 
0361     const QString dbName = dbFile.fileName();
0362     QFile::remove( dbName + ".bak" );
0363     dbFile.rename( dbName + ".bak" );
0364     tmpFile.copy( dbName );
0365 }