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 "RhythmboxProvider.h"
0018 
0019 #include "RhythmboxTrack.h"
0020 #include "core/support/Debug.h"
0021 
0022 #include <QFile>
0023 #include <QMutexLocker>
0024 #include <QTemporaryFile>
0025 #include <QXmlStreamReader>
0026 #include <QXmlStreamWriter>
0027 
0028 using namespace StatSyncing;
0029 
0030 RhythmboxProvider::RhythmboxProvider( const QVariantMap &config,
0031                                       ImporterManager *importer )
0032     : ImporterProvider( config, importer )
0033 {
0034 }
0035 
0036 RhythmboxProvider::~RhythmboxProvider()
0037 {
0038 }
0039 
0040 qint64
0041 RhythmboxProvider::reliableTrackMetaData() const
0042 {
0043     return Meta::valTitle | Meta::valArtist | Meta::valAlbum | Meta::valTrackNr
0044             | Meta::valDiscNr;
0045 }
0046 
0047 qint64
0048 RhythmboxProvider::writableTrackStatsData() const
0049 {
0050     return Meta::valRating | Meta::valLastPlayed | Meta::valPlaycount;
0051 }
0052 
0053 QSet<QString>
0054 RhythmboxProvider::artists()
0055 {
0056     readXml( QString() );
0057     QSet<QString> result;
0058     result.swap( m_artists );
0059     return result;
0060 }
0061 
0062 TrackList
0063 RhythmboxProvider::artistTracks( const QString &artistName )
0064 {
0065     readXml( artistName );
0066     TrackList result;
0067     result.swap( m_artistTracks );
0068     return result;
0069 }
0070 
0071 void
0072 RhythmboxProvider::readXml( const QString &byArtist )
0073 {
0074     QFile dbFile( m_config.value( "dbPath" ).toString() );
0075     if( dbFile.open( QIODevice::ReadOnly ) )
0076     {
0077         QXmlStreamReader xml( &dbFile );
0078         if( xml.readNextStartElement() )
0079         {
0080             if( xml.name() == "rhythmdb" )
0081             {
0082                 if( xml.attributes().value("version") != "1.8" )
0083                     warning() << __PRETTY_FUNCTION__ << "unsupported database version";
0084 
0085                 readRhythmdb( xml, byArtist );
0086             }
0087             else
0088                 xml.raiseError( "the database file is ill-formatted" );
0089         }
0090 
0091         if( xml.hasError() )
0092             warning() << "There was an error reading" << dbFile.fileName() << ":"
0093                       << xml.errorString();
0094     }
0095     else
0096         warning() << __PRETTY_FUNCTION__ << "couldn't open" << dbFile.fileName();
0097 }
0098 
0099 void
0100 RhythmboxProvider::readRhythmdb( QXmlStreamReader &xml, const QString &byArtist )
0101 {
0102     Q_ASSERT( xml.isStartElement() && xml.name() == "rhythmdb" );
0103 
0104     while( xml.readNextStartElement() )
0105     {
0106         if( xml.name() == "entry" && xml.attributes().value( "type" ) == "song" )
0107             readSong( xml, byArtist );
0108         else
0109             xml.skipCurrentElement();
0110     }
0111 }
0112 
0113 void
0114 RhythmboxProvider::readSong( QXmlStreamReader &xml, const QString &byArtist )
0115 {
0116     Q_ASSERT( xml.isStartElement() && xml.name() == "entry" );
0117 
0118     Meta::FieldHash metadata;
0119     QString currentArtist;
0120     QString location;
0121 
0122     while( xml.readNextStartElement() )
0123     {
0124         if( byArtist.isEmpty() && currentArtist.isEmpty() )
0125         {
0126             if( xml.name() == "artist" )
0127                 currentArtist = readValue( xml );
0128             else
0129                 xml.skipCurrentElement();
0130         }
0131         else if( currentArtist.isEmpty() || currentArtist == byArtist )
0132         {
0133             if( xml.name() == "title" )
0134                 metadata.insert( Meta::valTitle, readValue( xml ) );
0135             else if( xml.name() == "artist" )
0136             {
0137                 currentArtist = readValue( xml );
0138                 metadata.insert( Meta::valArtist, currentArtist );
0139             }
0140             else if( xml.name() == "album" )
0141                 metadata.insert( Meta::valAlbum, readValue( xml ) );
0142             else if( xml.name() == "track-number" )
0143                 metadata.insert( Meta::valTrackNr, readValue( xml ) );
0144             else if( xml.name() == "disc-number" )
0145                 metadata.insert( Meta::valDiscNr, readValue( xml ) );
0146             else if( xml.name() == "rating" )
0147                 metadata.insert( Meta::valRating, readValue( xml ) );
0148             else if( xml.name() == "last-played" )
0149                 metadata.insert( Meta::valLastPlayed, readValue( xml ) );
0150             else if( xml.name() == "play-count" )
0151                 metadata.insert( Meta::valPlaycount, readValue( xml ) );
0152             else if( xml.name() == "location" )
0153                 location = readValue( xml );
0154             else
0155                 xml.skipCurrentElement();
0156         }
0157         else
0158             xml.skipCurrentElement();
0159     }
0160 
0161     if( !byArtist.isEmpty() && currentArtist == byArtist )
0162     {
0163         RhythmboxTrack *track = new RhythmboxTrack( location, metadata );
0164         connect( track, &RhythmboxTrack::commitCalled,
0165                  this, &RhythmboxProvider::trackUpdated, Qt::DirectConnection );
0166         m_artistTracks << TrackPtr( track );
0167     }
0168     else if( byArtist.isEmpty() )
0169         m_artists << currentArtist;
0170 }
0171 
0172 QString
0173 RhythmboxProvider::readValue( QXmlStreamReader &xml )
0174 {
0175     return xml.readElementText();
0176 }
0177 
0178 void
0179 RhythmboxProvider::writeSong( QXmlStreamReader &reader, QXmlStreamWriter &writer,
0180                               const QMap<QString, Meta::FieldHash> &dirtyData )
0181 {
0182     Q_ASSERT( reader.isStartElement() && reader.name() == "entry" );
0183 
0184     Meta::FieldHash metadata;
0185     QString location;
0186 
0187     writer.writeCurrentToken( reader );
0188     while( !reader.isEndElement() || reader.name() != "entry" )
0189     {
0190         reader.readNext();
0191 
0192         if( reader.error() )
0193         {
0194             warning() << __PRETTY_FUNCTION__ << "Error reading song:"
0195                       << reader.errorString();
0196             return;
0197         }
0198 
0199         if( reader.isWhitespace() )
0200             continue; // autoformat will deal with whitespace for us
0201 
0202         if( reader.isStartElement() )
0203         {
0204             if( reader.name() == "rating" )
0205                 metadata.insert( Meta::valRating, readValue( reader ) );
0206             else if( reader.name() == "last-played" )
0207                 metadata.insert( Meta::valLastPlayed, readValue( reader ) );
0208             else if( reader.name() == "play-count" )
0209                 metadata.insert( Meta::valPlaycount, readValue( reader ) );
0210             else if( reader.name() == "location" )
0211             {
0212                 location = readValue( reader );
0213                 writer.writeTextElement( "location", location );
0214             }
0215             else
0216                 writer.writeCurrentToken( reader );
0217         }
0218         else if( !reader.isEndElement() || reader.name() != "entry" )
0219             writer.writeCurrentToken( reader );
0220     }
0221 
0222     // Override read statistics
0223     if( dirtyData.contains( location ) )
0224         metadata = dirtyData.value( location );
0225 
0226     if( metadata.value( Meta::valRating ).toInt() != 0 )
0227         writer.writeTextElement( "rating", metadata.value( Meta::valRating ).toString() );
0228     if( metadata.value( Meta::valLastPlayed ).toUInt() != 0 )
0229         writer.writeTextElement( "last-played",
0230                                  metadata.value( Meta::valLastPlayed ).toString() );
0231     if( metadata.value( Meta::valPlaycount ).toInt() != 0 )
0232         writer.writeTextElement( "play-count",
0233                                  metadata.value( Meta::valPlaycount ).toString() );
0234 
0235     writer.writeCurrentToken( reader );
0236 }
0237 
0238 void
0239 RhythmboxProvider::trackUpdated( const QString &location,
0240                                  const Meta::FieldHash &statistics )
0241 {
0242     QMutexLocker lock( &m_dirtyMutex );
0243     m_dirtyData.insert( location, statistics );
0244 }
0245 
0246 void
0247 RhythmboxProvider::commitTracks()
0248 {
0249     QMutexLocker lock( &m_dirtyMutex );
0250     if( m_dirtyData.empty() )
0251         return;
0252 
0253     QMap<QString, Meta::FieldHash> dirtyData;
0254     dirtyData.swap( m_dirtyData );
0255 
0256     QFile dbFile( m_config.value( "dbPath" ).toString() );
0257     if( !dbFile.open( QIODevice::ReadOnly ) )
0258     {
0259         warning() << __PRETTY_FUNCTION__ << dbFile.fileName() << "is not readable";
0260         return;
0261     }
0262 
0263     QTemporaryFile tmpFile;
0264     if( !tmpFile.open() )
0265     {
0266         warning() << __PRETTY_FUNCTION__ << tmpFile.fileName() << "is not writable";
0267         return;
0268     }
0269 
0270     QXmlStreamReader reader( &dbFile );
0271     QXmlStreamWriter writer( &tmpFile );
0272     writer.setAutoFormatting( true );
0273     writer.setAutoFormattingIndent( 2 );
0274 
0275     while( !reader.atEnd() )
0276     {
0277         reader.readNext();
0278 
0279         if( reader.error() )
0280         {
0281             warning() << __PRETTY_FUNCTION__ << "Error reading" << dbFile.fileName();
0282             return;
0283         }
0284 
0285         if( reader.isStartElement() && reader.name() == "entry" &&
0286             reader.attributes().value( "type" ) == "song" )
0287             writeSong( reader, writer, dirtyData );
0288         else if( reader.isStartDocument() ) // writeCurrentToken doesn't add 'standalone'
0289             writer.writeStartDocument( reader.documentVersion().toString(),
0290                                        reader.isStandaloneDocument() );
0291         else
0292             writer.writeCurrentToken( reader );
0293     }
0294 
0295     const QString dbName = dbFile.fileName();
0296     QFile::remove( dbName + ".bak" );
0297     dbFile.rename( dbName + ".bak" );
0298     tmpFile.copy( dbName );
0299 }