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 }