1 /****************************************************************************************
2  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #include "IpodParseTracksJob.h"
18 
19 #include "IpodCollection.h"
20 #include "IpodMeta.h"
21 #include "IpodPlaylistProvider.h"
22 #include "core/logger/Logger.h"
23 #include "core/support/Components.h"
24 #include "core/support/Debug.h"
25 #include "core-impl/meta/file/File.h"
26 
27 #include <QAction>
28 #include <QDir>
29 #include <QFileInfo>
30 
31 #include <gpod/itdb.h>
32 
IpodParseTracksJob(IpodCollection * collection)33 IpodParseTracksJob::IpodParseTracksJob( IpodCollection *collection )
34     : QObject()
35     , ThreadWeaver::Job()
36     , m_coll( collection )
37     , m_aborted( false )
38 {
39 }
40 
abort()41 void IpodParseTracksJob::abort()
42 {
43     m_aborted = true;
44 }
45 
46 void
run(ThreadWeaver::JobPointer self,ThreadWeaver::Thread * thread)47 IpodParseTracksJob::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread)
48 {
49     Q_UNUSED(self);
50     Q_UNUSED(thread);
51     DEBUG_BLOCK
52     Itdb_iTunesDB *itdb = m_coll->m_itdb;
53     if( !itdb )
54         return; // paranoia
55 
56     guint32 trackNumber = itdb_tracks_number( itdb );
57     QString operationText = i18nc( "operation when iPod is connected", "Reading iPod tracks" );
58     Amarok::Logger::newProgressOperation( this, operationText, trackNumber,
59                                                         this, &IpodParseTracksJob::abort );
60 
61     Meta::TrackList staleTracks;
62     QSet<QString> knownPaths;
63 
64     for( GList *tracklist = itdb->tracks; tracklist; tracklist = tracklist->next )
65     {
66         if( m_aborted )
67             break;
68 
69         Itdb_Track *ipodTrack = (Itdb_Track *) tracklist->data;
70         if( !ipodTrack )
71             continue; // paranoia
72         // IpodCollection::addTrack() locks and unlocks the m_itdbMutex mutex
73         Meta::TrackPtr proxyTrack = m_coll->addTrack( new IpodMeta::Track( ipodTrack ) );
74         if( proxyTrack )
75         {
76             QString canonPath = QFileInfo( proxyTrack->playableUrl().toLocalFile() ).canonicalFilePath();
77             if( !proxyTrack->isPlayable() )
78                 staleTracks.append( proxyTrack );
79             else if( !canonPath.isEmpty() )  // nonexistent files return empty canonical path
80                 knownPaths.insert( canonPath );
81         }
82 
83         incrementProgress();
84     }
85 
86     parsePlaylists( staleTracks, knownPaths );
87     Q_EMIT endProgressOperation( this );
88 }
89 
90 void
defaultBegin(const ThreadWeaver::JobPointer & self,ThreadWeaver::Thread * thread)91 IpodParseTracksJob::defaultBegin(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread)
92 {
93     Q_EMIT started(self);
94     ThreadWeaver::Job::defaultBegin(self, thread);
95 }
96 
97 void
defaultEnd(const ThreadWeaver::JobPointer & self,ThreadWeaver::Thread * thread)98 IpodParseTracksJob::defaultEnd(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread)
99 {
100     ThreadWeaver::Job::defaultEnd(self, thread);
101     if (!self->success()) {
102         Q_EMIT failed(self);
103     }
104     Q_EMIT done(self);
105 }
106 
107 void
parsePlaylists(const Meta::TrackList & staleTracks,const QSet<QString> & knownPaths)108 IpodParseTracksJob::parsePlaylists( const Meta::TrackList &staleTracks,
109                                     const QSet<QString> &knownPaths )
110 {
111     IpodPlaylistProvider *prov = m_coll->m_playlistProvider;
112     if( !prov || m_aborted )
113         return;
114 
115     if( !staleTracks.isEmpty() )
116     {
117         prov->m_stalePlaylist = Playlists::PlaylistPtr( new IpodPlaylist( staleTracks,
118             i18nc( "iPod playlist name", "Stale tracks" ), m_coll, IpodPlaylist::Stale ) );
119         prov->m_playlists << prov->m_stalePlaylist;  // we don't subscribe to this playlist, no need to update database
120         Q_EMIT prov->playlistAdded( prov->m_stalePlaylist );
121     }
122 
123     Meta::TrackList orphanedTracks = findOrphanedTracks( knownPaths );
124     if( !orphanedTracks.isEmpty() )
125     {
126         prov->m_orphanedPlaylist = Playlists::PlaylistPtr( new IpodPlaylist( orphanedTracks,
127             i18nc( "iPod playlist name", "Orphaned tracks" ), m_coll, IpodPlaylist::Orphaned ) );
128         prov->m_playlists << prov->m_orphanedPlaylist;  // we don't subscribe to this playlist, no need to update database
129         Q_EMIT prov->playlistAdded( prov->m_orphanedPlaylist );
130     }
131 
132     if( !m_coll->m_itdb || m_aborted )
133         return;
134     for( GList *playlists = m_coll->m_itdb->playlists; playlists; playlists = playlists->next )
135     {
136         Itdb_Playlist *playlist = (Itdb_Playlist *) playlists->data;
137         if( !playlist || itdb_playlist_is_mpl( playlist )  )
138             continue; // skip null (?) or master playlists
139         Playlists::PlaylistPtr playlistPtr( new IpodPlaylist( playlist, m_coll ) );
140         prov->m_playlists << playlistPtr;
141         prov->subscribeTo( playlistPtr );
142         Q_EMIT prov->playlistAdded( playlistPtr );
143     }
144 
145     if( !m_aborted && ( prov->m_stalePlaylist || prov->m_orphanedPlaylist ) )
146     {
147         QString text = i18n( "Stale and/or orphaned tracks detected on %1. You can resolve "
148             "the situation using the <b>%2</b> collection action. You can also view the tracks "
149             "under the Saved Playlists tab.", m_coll->prettyName(),
150             m_coll->m_consolidateAction->text() );
151         Amarok::Logger::longMessage( text );
152     }
153 }
154 
findOrphanedTracks(const QSet<QString> & knownPaths)155 Meta::TrackList IpodParseTracksJob::findOrphanedTracks(const QSet< QString >& knownPaths)
156 {
157     gchar *musicDirChar = itdb_get_music_dir( QFile::encodeName( m_coll->mountPoint() ) );
158     QString musicDirPath = QFile::decodeName( musicDirChar );
159     g_free( musicDirChar );
160     musicDirChar = 0;
161 
162     QStringList trackPatterns;
163     foreach( const QString &suffix, m_coll->supportedFormats() )
164     {
165         trackPatterns << QString( "*.%1" ).arg( suffix );
166     }
167 
168     Meta::TrackList orphanedTracks;
169     QDir musicDir( musicDirPath );
170     foreach( QString subdir, musicDir.entryList( QStringList( "F??" ), QDir::Dirs | QDir::NoDotAndDotDot ) )
171     {
172         if( m_aborted )
173             return Meta::TrackList();
174         subdir = musicDir.absoluteFilePath( subdir ); // make the path absolute
175         foreach( const QFileInfo &info, QDir( subdir ).entryInfoList( trackPatterns ) )
176         {
177             QString canonPath = info.canonicalFilePath();
178             if( knownPaths.contains( canonPath ) )
179                 continue;  // already in iTunes database
180             Meta::TrackPtr track( new MetaFile::Track( QUrl::fromLocalFile( canonPath ) ) );
181             orphanedTracks << track;
182         }
183     }
184     return orphanedTracks;
185 }
186