/*************************************************************************** * Copyright (C) 2004 by Michael Schulze * * mike.s@genion.de * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307, USA. * ***************************************************************************/ #include #include #include "itunesdb.h" #include "itunesdb/itunesdbparser.h" #include "itunesdb/itunesdbwriter.h" #include "containerutils.h" #include #include #include //Added by qt3to4: #include #include #include #define RECENTLY_PLAYED_LIST_NAME "KPOD:Recently Played" // TODO make these 2 classes part of the reader/writer adapters struct PlayCountEntry { PlayCountEntry( TrackMetadata * playedTrack, Q_UINT32 lastPlayedAt ) : track( playedTrack ), lastplayed( lastPlayedAt) {} TrackMetadata * track; Q_UINT32 lastplayed; }; class ITunesDB::PlaycountSorter : public Q3PtrList { public: PlaycountSorter() : Q3PtrList() {} ~PlaycountSorter() { clear(); } void addPlaycount( TrackMetadata * track, Q_UINT32 lastplayed ) { append( new PlayCountEntry(track, lastplayed) ); } virtual int compareItems( Q3PtrCollection::Item s1, Q3PtrCollection::Item s2 ) { return (*(PlayCountEntry*)s2).lastplayed - (*(PlayCountEntry*)s1).lastplayed; } }; class ITunesDB::PlaylistContainer : public Q3PtrList { public: PlaylistContainer() : Q3PtrList() {} ~PlaylistContainer() { clear(); } virtual int compareItems( Q3PtrCollection::Item s1, Q3PtrCollection::Item s2 ) { return (*(TrackList*)s1).getTitle().toLower().localeAwareCompare( (*(TrackList*)s2).getTitle().toLower() ); } }; using namespace itunesdb; ITunesDB::ITunesDB(bool resolve_slashes) : artistmap( 101 ), playlists( new PlaylistContainer() ), resolveslashes( resolve_slashes ), maxtrackid(0), maxTrackDBID(0), m_recentlyPlayed(NULL), currentDataSet( 0 ), hasPodcastsFlag( false ) { resolveslashes= resolve_slashes; artistmap.setAutoDelete(true); playlists->setAutoDelete(true); } bool ITunesDB::open(const QString& ipod_base) { // TODO remove trailing slash from ipod_base if there is one m_recentlyPlayed = new PlaycountSorter(); m_recentlyPlayed->setAutoDelete( TRUE ); ItunesDBParser parser( *this ); itunesdbfile.setFileName(ipod_base + "/iPod_Control/iTunes/iTunesDB"); itunessdfile.setFileName(ipod_base + "/iPod_Control/iTunes/iTunesSD"); if(itunesdbfile.exists()) { timestamp = QFileInfo(itunesdbfile).lastModified(); parser.parse(itunesdbfile); } else { delete m_recentlyPlayed; m_recentlyPlayed = NULL; return false; } kdDebug() << "ITunesDB::open() Reading OTG lists" << endl; QDir dir(ipod_base + "/iPod_Control/iTunes/"); dir.setNameFilter( "OTGPlaylistInfo*" ); for ( unsigned int i = 0; i < dir.count(); i++ ) { if (QFileInfo(dir.filePath( dir[i] ) ).size()) { QFile file( dir.filePath( dir[i] ) ); kdDebug() << "ITunesDB::open() Reading OTG list " << file.fileName() << endl; parser.parseOTG( file ); } } kdDebug() << "ITunesDB::open() Parsing Playcounts" << endl; QFile myfile ( ipod_base + "/iPod_Control/iTunes/Play Counts" ); if (myfile.exists()) { parser.parsePlaycount( myfile ); if ( m_recentlyPlayed->count() ) { m_recentlyPlayed->sort(); // Dave hack // removePlaylist( RECENTLY_PLAYED_LIST_NAME , TRUE); TrackList * recentlyPlayed = new TrackList( ); for ( PlayCountEntry * entry = m_recentlyPlayed->first(); entry; entry = m_recentlyPlayed->next() ) { recentlyPlayed->addPlaylistItem( *entry->track ); } recentlyPlayed->setTitle( RECENTLY_PLAYED_LIST_NAME ); recentlyPlayed->setSortOrderField( Playlist::SORTORDER_TIME_PLAYED ); playlists->append( recentlyPlayed ); } } m_recentlyPlayed->clear(); delete m_recentlyPlayed; m_recentlyPlayed = NULL; kdDebug() << "iTunesDBL finished parsing playcounts" << endl; return true; } QString ITunesDB::createPlaylistTitle( const QString &title ) { QString newtitle; for (unsigned int i=1;i<100;i++) { newtitle = QString("%1 %2").arg(title).arg(QString::number(i)); if ( getPlaylistByTitle(newtitle) == NULL ) { return newtitle; } } return QString::null; } bool ITunesDB::isOpen() { return timestamp.isValid(); } bool ITunesDB::writeDatabase(const QString& filename) { QFile outfile(filename); if(filename.isEmpty()) { outfile.setName(itunesdbfile.fileName()); } ItunesDBWriter writer( this ); writer.write(outfile); QDir dir(QFileInfo(outfile).dir(TRUE)); dir.setNameFilter( "OTGPlaylistInfo*" ); for ( unsigned int i = 0; i < dir.count(); i++ ) { if (QFileInfo(dir.filePath( dir[i] ) ).size()) dir.remove( dir[i] ); } dir.rename("Play Counts", "Play Counts.bak"); QFile outfilesd( itunessdfile.name() ); writer.writeSD( outfilesd ); return true; } bool ITunesDB::dbFileChanged() { return !itunesdbfile.exists() || QFileInfo(itunesdbfile.name()).lastModified() != timestamp; } ITunesDB::~ITunesDB() { clear(); delete playlists; } /****************************************************** * * ItunesDBDataSource Methods * * for documentation of the following methods * see itunesdb/itunesdbdatasource.h *****************************************************/ void ITunesDB::writeInit() { error= QString::null; // remove deleted tracklist items removeFromAllPlaylists( LISTITEM_DELETED ); playlists->sort(); } void ITunesDB::writeFinished() { changed= false; // container is in sync with the database now } Q_UINT32 ITunesDB::getNumPlaylists() { return playlists->count(); } /*! \fn ITunesDB::getNumTracks() */ Q_UINT32 ITunesDB::getNumTracks() { return trackmap.count(); } Playlist * ITunesDB::getMainplaylist() { return &mainlist; } Playlist * ITunesDB::firstPlaylist() { return playlists->first(); } Playlist * ITunesDB::nextPlaylist() { return playlists->next(); } Track * ITunesDB::firstTrack() { trackiterator= trackmap.begin(); if (trackiterator == trackmap.end()) return NULL; TrackMetadata * track = *trackiterator; return track; } Track* ITunesDB::nextTrack() { if( trackiterator == trackmap.end() || ++trackiterator == trackmap.end()) return NULL; TrackMetadata * track = *trackiterator; TrackList * album = getAlbum(track->getArtist(), track->getAlbum()); if (album != NULL) track->setNumTracksInAlbum(album->getNumTracks()); return track; } /************************************************* * * ItunesDBListener Methods * * for documentation of the following methods * see itunesdb/itunesdblistener.h *************************************************/ void ITunesDB::parseStarted() { // kdDebug() << "ITunesDB::parseStarted()" << endl; error= QString::null; changed= true; currentDataSet = 0; } void ITunesDB::parseFinished() { // kdDebug() << "ITunesDB::parseFinished()" << endl; changed= false; if (mainlist.getTitle().isEmpty()) { mainlist.setTitle("kpod"); } if ( maxtrackid == 0 ) { maxtrackid = 2000; maxTrackDBID = 16384; } if ( maxTrackDBID == 0) { maxTrackDBID = maxtrackid; } // set all album hnge flags to false (quick hack: this should be handled elsewhere) // iterate over artists for (ArtistMapIterator artistiter(artistmap); artistiter.current(); ++artistiter) { for (ArtistIterator albums(*(artistiter.current())); albums.current(); ++albums) { albums.current()->setChangeFlag(false); } } for( Playlist * playlist= firstPlaylist(); playlist != NULL; playlist= nextPlaylist()) { Playlist::Iterator track_iter= playlist->getTrackIDs(); while( track_iter.hasNext()) { Track * track= getTrackByID( track_iter.next()); if( track == NULL) { // track couldn't be found playlist->removeTrackAt( track_iter); changed= true; } } } // check for Track Metadata for ( TrackMap::iterator track = trackmap.begin(); track != trackmap.end(); ++track ) { if ( (*track)->getDBID() == 0 ) { maxTrackDBID += 2; (*track)->setDBID( maxTrackDBID ); } } } /*! \fn ITunesDB::handleTrack( Track * track) */ void ITunesDB::handleTrack(const Track& track) { // kdDebug() << "ITunesDB::handleTrack(" << track.getArtist() << "/" << track.getAlbum() << "/" << track.getTitle() << ")" << endl; if(track.getID() == 0) { // not initialized - don't care about this one return; } TrackMetadata * trackmetadata = new TrackMetadata( track ); if ( maxtrackid < track.getID() ) { maxtrackid = track.getID(); } if ( maxTrackDBID < track.getDBID() ) { maxTrackDBID = track.getDBID(); } insertTrackToDataBase( *trackmetadata); mainlist.addPlaylistItem(track); changed = true; } void ITunesDB::handlePlaycount( Q_UINT32 idx, Q_UINT32 lastplayed, Q_UINT32 stars, Q_UINT32 count, Q_UINT32) { // kdDebug() << "handlePlaycount()" << endl; QDateTime date; date.setTime_t( lastplayed ); idx = mainlist.getTrackIDAt( idx ); //Same trick as in the OTG Playlists this is a offset TrackMetadata * track = getTrackByID(idx); if (!track) return; kdDebug() << "ID " << idx << " was " << count << "x played on " << date.toString() << ": " << track->getArtist() << " - " << track->getTitle() << endl; //We really changed if ((stars != 0 && track->getRating() != stars) || (track->getPlayCount() != count) // || (track->getLastPlayed() != lastplayed) ) { if (stars != 0) track->setRating((unsigned char)stars); track->setLastPlayed(lastplayed); track->setPlayCount(track->getPlayCount()); // Now insert track into the sorted List if ( m_recentlyPlayed ) m_recentlyPlayed->addPlaycount( track, lastplayed ); } } void ITunesDB::convertOffsetsToIDs(itunesdb::Playlist& playlist) { Q_UINT32 id; if (mainlist.getTitle().isEmpty()) { return; // Something has gone VERY wrong here } for (uint i = 0; i <= playlist.getNumTracks(); i++) { id = playlist.getTrackIDAt( i ); //Get the ID the playlist uses id = mainlist.getTrackIDAt( id ); //Now transform in a trackid playlist.setTrackIDAt( i, id ); //Write it back } } void ITunesDB::handleOTGPlaylist(const Playlist& playlist) { QString title; if (mainlist.getTitle().isEmpty()) { return; // Something has gone VERY wrong here } if (!playlist.getNumTracks()) return; convertOffsetsToIDs((Playlist&)playlist); TrackList * pTracklist = new TrackList( playlist ); title = createPlaylistTitle("OTG Playlist"); if (title.isNull()) return; kdDebug() << "ITunesDB::handleOTGPlaylist(): " << title << endl; pTracklist->setTitle( title ); playlists->append( pTracklist ); changed= true; } void ITunesDB::handlePlaylist(const Playlist& playlist) { if ( currentDataSet == 3 ) { hasPodcastsFlag |= playlist.getNumTracks(); return; // can't handle podcasts at the moment } // TODO find out another way to find out if this is the mainlist (maybe a handleMainlist() or have some state thingy if ( mainlist.getTitle().isEmpty() ) { mainlist.setTitle(playlist.getTitle()); return; // that's all we wanna know for now } if ( playlist.isHidden() ) { return; // dunno what to do with it } TrackList * pTracklist = new TrackList( playlist); // consistency checks if( getPlaylistByTitle( pTracklist->getTitle()) == NULL ) { // dont overwrite existing playlists TrackList::Iterator trackid_iter = pTracklist->getTrackIDs(); while (trackid_iter.hasNext()) { Q_UINT32 trackid = trackid_iter.next(); TrackMetadata * track = getTrackByID(trackid); if (track != NULL && (track->getTrackNumber() > pTracklist->getMaxTrackNumber())) pTracklist->setMaxTrackNumber(track->getTrackNumber()); } playlists->append( pTracklist ); } else delete pTracklist; changed= true; } void ITunesDB::handleError(const QString &message) { error= message; } void ITunesDB::handleDataSet( Q_UINT32 type ) { currentDataSet = type; } void ITunesDB::setNumPlaylists(Q_UINT32) { // oh really? !nteresting! } void ITunesDB::setNumTracks(Q_UINT32) { // oh really? !nteresting! } /************************************************* * * Service Methods * *************************************************/ /** * adds a new track to the collection * @parameter track the Track to add */ void ITunesDB::addTrack(TrackMetadata& track) { if ( track.getDBID() == 0 ) { track.setDBID( maxTrackDBID + 2); } handleTrack(track); } /** * returns the Track corresponding to the given ID * @param id ID of the track * @return the Track corresponding to the given ID */ TrackMetadata* ITunesDB::getTrackByID( const Q_UINT32 id) const { TrackMap::const_iterator track = trackmap.find( id ); if( track == trackmap.end()) return NULL; else return *track; } /** * returns the Track found by the given information or NULL if no such Track could be found * @param artistname the name of the artist * @param albumname the name of the album * @param title the title of the track */ TrackMetadata * ITunesDB::findTrack(const QString& artistname, const QString& albumname, const QString& title) const { TrackMetadata * result = NULL; TrackList * album = getAlbum( artistname, albumname ); if ( album == NULL ) { return NULL; } TrackList::Iterator trackIDIter = album->getTrackIDs(); while ( trackIDIter.hasNext() && result == NULL ) { result = getTrackByID( trackIDIter.next() ); if ( result->getTitle() != title ) { result = NULL; } } return result; } /*! \fn ITunesDB::getArtists( QStringList &buffer) */ QStringList* ITunesDB::getArtists( QStringList &buffer) const { for( ArtistMapIterator artist( artistmap); artist.current(); ++artist) { buffer.append( artist.currentKey()); } return &buffer; } Artist * ITunesDB::getArtistByName(const QString& artistname) const { return artistmap.find(artistname); } /*! \fn ITunesDB::getAlbumsByArtist( QString &artist, QStringList &buffer) */ Artist * ITunesDB::getArtistByName(const QString& artistname, bool create) { Artist * artist = artistmap.find(artistname); if (artist == NULL && create) { // artist not in the map yet: create default entry artist = new Artist( 17 ); artist->setAutoDelete(true); artistmap.insert(artistname, artist); } return artist; } /*! \fn ITunesDB::getPlaylistByTitle( const QString& playlisttitle) */ class PlaylistByTitleFinder { public: PlaylistByTitleFinder(const QString& title) : _title_( title ) { } bool operator() ( const TrackList * playlist ) const { return playlist->getTitle() == _title_; } private: const QString _title_; }; TrackList * ITunesDB::getPlaylistByTitle( const QString& playlisttitle) const { PlaylistByTitleFinder finder( playlisttitle ); return * find( playlists->begin(), playlists->end(), finder ); } /*! \fn ITunesDB::getAlbum(QString &artist, QString &album) */ TrackList * ITunesDB::getAlbum(const QString &artistname, const QString &albumname) const { Artist * artist = artistmap.find( artistname); TrackList * album; // check if artist exists if (artist == NULL) { // artist not in the map return NULL; } // find the album if( ( album = artist->find( albumname)) == NULL) { // album not in the map return NULL; } return album; } /*! \fn ITunesDB::clear() */ void ITunesDB::clear() { // if( trackmap.empty()) // return; // delete all tracks TrackMap::iterator track_it= trackmap.begin(); for( ; track_it!= trackmap.end(); ++track_it) { delete *track_it; } trackmap.clear(); // delete all albums artistmap.clear(); // clear playlists playlists->clear(); itunesdbfile.setName(QString()); timestamp = QDateTime(); maxtrackid = 0; maxTrackDBID = 0; mainlist = TrackList(); } bool ITunesDB::removeArtist(const QString& artistname) { Artist * artist = artistmap.find(artistname); if (!artist || !artist->isEmpty()) { return false; } return artistmap.remove(artistname); } /*! \fn ITunesDB::removePlaylist( const QString& title) */ bool ITunesDB::removePlaylist( const QString& title, bool delete_instance) { TrackList * toRemove = getPlaylistByTitle( title ); if ( ! toRemove ) { return false; } if ( delete_instance ) { playlists->remove( toRemove ); } else { if ( playlists->find( toRemove ) != -1 ) { playlists->take(); } else { // that's impossible return false; } } changed = true; return true; } /*! \fn ITunesDB::removeTrack(Q_UINT32 trackid, bool delete_instance = true) */ Q_UINT32 ITunesDB::removeTrack(Q_UINT32 trackid, bool delete_instance) { TrackMetadata * track = getTrackByID(trackid); if(track == NULL) return 0; // remove track from track table trackmap.remove(trackid); // remove track from album TrackList * album = getAlbum(track->getArtist(), track->getAlbum()); if ( album != NULL ) { album->removeAll(trackid); } // remove track from playlists removeFromAllPlaylists( trackid ); // remove track from main playlists mainlist.removeAll(trackid); if(delete_instance) { delete track; } return trackid; } TrackList * ITunesDB::renameAlbum(TrackList& album, const QString& newartistname, const QString& newtitle) { QString artistname; // update track info TrackList::Iterator trackiter = album.getTrackIDs(); while ( trackiter.hasNext() ) { TrackMetadata * track = getTrackByID( trackiter.next() ); if( track == NULL ) { continue; } if (artistname.isEmpty()) { artistname = track->getArtist(); } track->setArtist( newartistname ); if ( !newtitle.isEmpty() ) { track->setAlbum( newtitle ); } } Artist * artist = getArtistByName(artistname); if (artist != NULL) { artist->take(album.getTitle()); } else { kdDebug() << "ITunesDB::renameAlbum() Artist " << artistname << " not found" << endl; } artist = getArtistByName(newartistname, true); if (artist == NULL) { kdDebug() << "ITunesDB::renameAlbum(): serious problem occured while creating new artist" << endl; return NULL; } if ( !newtitle.isEmpty() ) { album.setTitle( newtitle ); } else { if ( newartistname != artistname ) { album.setChangeFlag( true ); } } artist->insert( album.getTitle() , &album ); album.setChangeFlag( true ); return getAlbum( newartistname, album.getTitle() ); } bool ITunesDB::moveTrack(TrackMetadata& track, const QString& newartist, const QString& newalbum) { TrackList * album = getAlbum(track.getArtist(), track.getAlbum()); if (album == NULL) return false; album->removeAll(track.getID()); trackmap.remove(track.getID()); track.setArtist(newartist); track.setAlbum(newalbum); insertTrackToDataBase(track); return true; } /*! \fn ITunesDB::isChanged() */ bool ITunesDB::isChanged() { return changed; } /*************************************************************************** * * private/protected methods * ***************************************************************************/ void ITunesDB::removeFromAllPlaylists( Q_UINT32 trackid ) { for ( TrackList * playlist = playlists->first(); playlist; playlist = playlists->next() ) { playlist->removeAll( trackid ); } } void ITunesDB::lock(bool write_lock) { if(!itunesdbfile.isOpen()) itunesdbfile.open(QIODevice::ReadOnly); if(write_lock) flock(itunesdbfile.handle(), LOCK_EX); else flock(itunesdbfile.handle(), LOCK_SH); } void ITunesDB::unlock() { flock(itunesdbfile.handle(), LOCK_UN); itunesdbfile.close(); } /*! \fn ITunesDB::insertTrackToDataBase() */ void ITunesDB::insertTrackToDataBase(TrackMetadata& track) { TrackList * album; QString artiststr= track.getArtist(); QString albumstr= track.getAlbum(); trackmap.insert(track.getID(), &track); if (resolveslashes) { albumstr= albumstr.replace( "/", "%2f"); artiststr= artiststr.replace( "/", "%2f"); } // find the artist Artist * artist = getArtistByName(artiststr, true); if (artist == NULL) { // shouldn't happen return; } // find the album if((album = artist->find( albumstr)) == NULL) { // album not in the map yet: create default entry album = new TrackList(); album->setTitle(albumstr); artist->insert(albumstr, album); } int trackpos = album->addPlaylistItem(track); // if tracknum is not set yet - set it to the position in the album if(track.getTrackNumber() == 0) { track.setTrackNumber(trackpos + 1); } }