File indexing completed on 2025-01-19 04:24:27
0001 /**************************************************************************************** 0002 * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz> * 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 "IpodCopyTracksJob.h" 0018 0019 #include "IpodMeta.h" 0020 #include "core/collections/QueryMaker.h" 0021 #include "core/logger/Logger.h" 0022 #include "core/support/Components.h" 0023 #include "core/support/Debug.h" 0024 #include "core/transcoding/TranscodingController.h" 0025 #include "MetaTagLib.h" 0026 #include "FileType.h" 0027 #include "transcoding/TranscodingJob.h" 0028 0029 #include <KIO/CopyJob> 0030 #include <KIO/Job> 0031 #include <KMessageBox> 0032 0033 #include <QFile> 0034 0035 #include <gpod/itdb.h> 0036 #include <unistd.h> // fsync() 0037 0038 IpodCopyTracksJob::IpodCopyTracksJob( const QMap<Meta::TrackPtr,QUrl> &sources, 0039 const QPointer<IpodCollection> &collection, 0040 const Transcoding::Configuration &configuration, 0041 bool goingToRemoveSources ) 0042 : Job() 0043 , m_coll( collection ) 0044 , m_transcodingConfig( configuration ) 0045 , m_sources( sources ) 0046 , m_aborted( false ) 0047 , m_goingToRemoveSources( goingToRemoveSources ) 0048 { 0049 connect( this, &IpodCopyTracksJob::startDuplicateTrackSearch, 0050 this, &IpodCopyTracksJob::slotStartDuplicateTrackSearch ); 0051 connect( this, &IpodCopyTracksJob::startCopyOrTranscodeJob, 0052 this, &IpodCopyTracksJob::slotStartCopyOrTranscodeJob ); 0053 connect( this, &IpodCopyTracksJob::displayErrorDialog, this, &IpodCopyTracksJob::slotDisplayErrorDialog ); 0054 } 0055 0056 void 0057 IpodCopyTracksJob::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) 0058 { 0059 Q_UNUSED(self); 0060 Q_UNUSED(thread); 0061 if( !m_coll ) 0062 return; // destructed behind our back 0063 float totalSafeCapacity = m_coll->totalCapacity() - m_coll->capacityMargin(); 0064 QByteArray mountPoint = QFile::encodeName( m_coll->mountPoint() ); 0065 QString collectionPrettyName = m_coll->prettyName(); 0066 0067 itdb_start_sync( m_coll->m_itdb ); 0068 QMapIterator<Meta::TrackPtr, QUrl> it( m_sources ); 0069 while( it.hasNext() ) 0070 { 0071 if( m_aborted || !m_coll ) 0072 break; 0073 0074 it.next(); 0075 Meta::TrackPtr track = it.key(); 0076 QUrl sourceUrl = it.value(); 0077 Q_EMIT startDuplicateTrackSearch( track ); 0078 0079 // wait for searching to finish: 0080 m_searchingForDuplicates.acquire( 1 ); 0081 if( m_duplicateTrack ) 0082 { 0083 trackProcessed( Duplicate, track, m_duplicateTrack ); 0084 continue; 0085 } 0086 0087 if( !m_coll ) 0088 break; // destructed behind our back 0089 0090 bool isJustCopy = m_transcodingConfig.isJustCopy( track, m_coll->supportedFormats() ); 0091 0092 if( isJustCopy // if not copying, we catch big files later 0093 && track->filesize() > totalSafeCapacity - m_coll->usedCapacity() ) 0094 { 0095 // this is a best effort check, we do one definite one after the file is copied 0096 debug() << "Refusing to copy" << track->prettyUrl() << "to iPod: there are only" 0097 << totalSafeCapacity - m_coll->usedCapacity() << "free bytes (not" 0098 << "counting a safety margin) on iPod and track has" << track->filesize() 0099 << "bytes."; 0100 trackProcessed( ExceededingSafeCapacity, track ); 0101 continue; 0102 } 0103 QString fileExtension; 0104 if( isJustCopy ) 0105 fileExtension = track->type(); 0106 else 0107 fileExtension = Amarok::Components::transcodingController()->format( 0108 m_transcodingConfig.encoder() )->fileExtension(); 0109 if( !m_coll->supportedFormats().contains( fileExtension ) ) 0110 { 0111 m_notPlayableFormats.insert( fileExtension ); 0112 trackProcessed( NotPlayable, track ); 0113 continue; 0114 } 0115 QByteArray fakeSrcName( "filename." ); // only for file extension 0116 fakeSrcName.append( QFile::encodeName( fileExtension ) ); 0117 0118 /* determine destination filename; we cannot use ipodTrack because as it has no itdb 0119 * (and thus mountpoint) set */ 0120 GError *error = nullptr; 0121 gchar *destFilename = itdb_cp_get_dest_filename( nullptr, mountPoint, fakeSrcName, &error ); 0122 if( error ) 0123 { 0124 warning() << "Cannot construct iPod track filename:" << error->message; 0125 g_error_free( error ); 0126 error = nullptr; 0127 } 0128 if( !destFilename ) 0129 { 0130 trackProcessed( InternalError, track ); 0131 continue; 0132 } 0133 0134 // start the physical copying 0135 QUrl destUrl = QUrl::fromLocalFile( QFile::decodeName( destFilename ) ); 0136 Q_EMIT startCopyOrTranscodeJob( sourceUrl, destUrl, isJustCopy ); 0137 0138 // wait for copying to finish: 0139 m_copying.acquire( 1 ); 0140 /* fsync so that progress bar gives correct info and user doesn't remove the iPod 0141 * prematurely */ 0142 QFile destFile( QFile::decodeName( destFilename ) ); 0143 if( !destFile.exists() ) 0144 { 0145 debug() << destFile.fileName() << "does not exist even though we thought we copied it to iPod"; 0146 trackProcessed( CopyingFailed, track ); 0147 continue; 0148 } 0149 if( !m_coll ) 0150 break; // destructed behind our back 0151 if( m_coll->usedCapacity() > totalSafeCapacity ) 0152 { 0153 debug() << "We exceeded total safe-to-use capacity on iPod (safe-to-use:" 0154 << totalSafeCapacity << "B, used:" << m_coll->usedCapacity() 0155 << "): removing copied track from iPod"; 0156 destFile.remove(); 0157 trackProcessed( ExceededingSafeCapacity, track ); 0158 continue; 0159 } 0160 // fsync needs a file opened for writing, and without Apped it truncates files (?) 0161 if( !destFile.open( QIODevice::WriteOnly | QIODevice::Append ) ) 0162 { 0163 warning() << "Cannot open file copied to ipod (for writing):" << destFile.fileName() 0164 << ": removing it"; 0165 destFile.remove(); 0166 trackProcessed( InternalError, track ); 0167 continue; 0168 } 0169 if( destFile.size() ) 0170 fsync( destFile.handle() ); // should flush all kernel buffers to disk 0171 destFile.close(); 0172 0173 // create a new track object by copying meta-data from existing one: 0174 IpodMeta::Track *ipodTrack = new IpodMeta::Track( track ); 0175 // tell the track it has been copied: 0176 bool accepted = ipodTrack->finalizeCopying( mountPoint, destFilename ); 0177 g_free( destFilename ); 0178 destFilename = nullptr; 0179 if( !accepted ) 0180 { 0181 debug() << "ipodTrack->finalizeCopying( destFilename ) returned false!"; 0182 delete ipodTrack; 0183 trackProcessed( InternalError, track ); 0184 continue; 0185 } 0186 if( !isJustCopy ) 0187 { 0188 // we need to reread some metadata in case the file was transcoded 0189 Meta::FieldHash fields = Meta::Tag::readTags( destFile.fileName() ); 0190 ipodTrack->setBitrate( fields.value( Meta::valBitrate, 0 ).toInt() ); 0191 ipodTrack->setLength( fields.value( Meta::valLength, 0 ).toLongLong() ); 0192 ipodTrack->setSampleRate( fields.value( Meta::valSamplerate, 0 ).toInt() ); 0193 Amarok::FileType type = Amarok::FileType( fields.value( Meta::valFormat, 0 ).toInt() ); 0194 ipodTrack->setType( Amarok::FileTypeSupport::toString( type ) ); 0195 // we retain ReplayGain, tags etc - these shouldn't change; size is read 0196 // in finalizeCopying() 0197 } 0198 0199 // add the track to collection 0200 if( !m_coll ) 0201 { 0202 delete ipodTrack; 0203 break; // we were waiting for copying, m_coll may got destroyed 0204 } 0205 Meta::TrackPtr newTrack = m_coll->addTrack( ipodTrack ); 0206 if( !newTrack ) 0207 { 0208 destFile.remove(); 0209 trackProcessed( InternalError, track ); 0210 continue; 0211 } 0212 trackProcessed( Success, track, newTrack ); 0213 } 0214 0215 if( m_coll ) 0216 itdb_stop_sync( m_coll->m_itdb ); 0217 Q_EMIT endProgressOperation( this ); 0218 0219 int sourceSize = m_sources.size(); 0220 int successCount = m_sourceTrackStatus.count( Success ); 0221 int duplicateCount = m_sourceTrackStatus.count( Duplicate ); 0222 QString transferredText; 0223 if ( m_transcodingConfig.isJustCopy() ) 0224 transferredText = i18ncp( "%2 is collection name", "Transferred one track to %2.", 0225 "Transferred %1 tracks to %2.", successCount, collectionPrettyName ); 0226 else 0227 transferredText = i18ncp( "%2 is collection name", "Transcoded one track to %2.", 0228 "Transcoded %1 tracks to %2.", successCount, collectionPrettyName ); 0229 0230 if( successCount == sourceSize ) 0231 { 0232 Amarok::Logger::shortMessage( transferredText ); 0233 } 0234 else if( m_aborted ) 0235 { 0236 QString text = i18np( "Transfer aborted. Managed to transfer one track.", 0237 "Transfer aborted. Managed to transfer %1 tracks.", 0238 successCount ); 0239 Amarok::Logger::longMessage( text ); 0240 } 0241 else if( successCount + duplicateCount == sourceSize ) 0242 { 0243 QString text = i18ncp( "%2 is the 'Transferred 123 tracks to Some collection.' message", 0244 "%2 One track was already there.", "%2 %1 tracks were already there.", 0245 duplicateCount, transferredText ); 0246 Amarok::Logger::longMessage( text ); 0247 } 0248 else 0249 { 0250 // something more severe failed, notify user using a dialog 0251 Q_EMIT displayErrorDialog(); 0252 } 0253 } 0254 0255 void 0256 IpodCopyTracksJob::defaultBegin(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) 0257 { 0258 Q_EMIT started(self); 0259 ThreadWeaver::Job::defaultBegin(self, thread); 0260 } 0261 0262 void 0263 IpodCopyTracksJob::defaultEnd(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) 0264 { 0265 ThreadWeaver::Job::defaultEnd(self, thread); 0266 if (!self->success()) { 0267 Q_EMIT failed(self); 0268 } 0269 Q_EMIT done(self); 0270 } 0271 0272 void 0273 IpodCopyTracksJob::abort() 0274 { 0275 m_aborted = true; 0276 } 0277 0278 void 0279 IpodCopyTracksJob::slotStartDuplicateTrackSearch( const Meta::TrackPtr &track ) 0280 { 0281 Collections::QueryMaker *qm = m_coll->queryMaker(); 0282 qm->setQueryType( Collections::QueryMaker::Track ); 0283 0284 // we cannot qm->addMatch( track ) - it matches by uidUrl() 0285 qm->addFilter( Meta::valTitle, track->name(), true, true ); 0286 qm->addMatch( track->album() ); 0287 qm->addMatch( track->artist(), Collections::QueryMaker::TrackArtists ); 0288 qm->addMatch( track->composer() ); 0289 qm->addMatch( track->year() ); 0290 qm->addNumberFilter( Meta::valTrackNr, track->trackNumber(), Collections::QueryMaker::Equals ); 0291 qm->addNumberFilter( Meta::valDiscNr, track->discNumber(), Collections::QueryMaker::Equals ); 0292 // we don't want to match by filesize, track length, filetype etc - these change during 0293 // transcoding. We don't match album artist because handling of it is inconsistent 0294 0295 connect( qm, &Collections::QueryMaker::newTracksReady, 0296 this, &IpodCopyTracksJob::slotDuplicateTrackSearchNewResult ); 0297 connect( qm, &Collections::QueryMaker::queryDone, this, &IpodCopyTracksJob::slotDuplicateTrackSearchQueryDone ); 0298 qm->setAutoDelete( true ); 0299 m_duplicateTrack = Meta::TrackPtr(); // reset duplicate track from previous query 0300 qm->run(); 0301 } 0302 0303 void 0304 IpodCopyTracksJob::slotDuplicateTrackSearchNewResult( const Meta::TrackList &tracks ) 0305 { 0306 if( !tracks.isEmpty() ) 0307 // we don't really know which one, but be sure to allow multiple results 0308 m_duplicateTrack = tracks.last(); 0309 } 0310 0311 void 0312 IpodCopyTracksJob::slotDuplicateTrackSearchQueryDone() 0313 { 0314 m_searchingForDuplicates.release( 1 ); // wakeup run() 0315 } 0316 0317 void 0318 IpodCopyTracksJob::slotStartCopyOrTranscodeJob( const QUrl &sourceUrlOrig, const QUrl &destUrl, 0319 bool isJustCopy ) 0320 { 0321 QUrl sourceUrl( sourceUrlOrig ); 0322 // KIO::file_copy in KF5 needs scheme 0323 if (sourceUrl.isRelative() && sourceUrl.host().isEmpty()) { 0324 sourceUrl.setScheme("file"); 0325 } 0326 0327 KJob *job = nullptr; 0328 if( isJustCopy ) 0329 { 0330 if( m_goingToRemoveSources && m_coll && 0331 sourceUrl.toLocalFile().startsWith( m_coll->mountPoint() ) ) 0332 { 0333 // special case for "add orphaned tracks" to either save space and significantly 0334 // speed-up the process: 0335 debug() << "Moving from" << sourceUrl << "to" << destUrl; 0336 job = KIO::file_move( sourceUrl, destUrl, -1, KIO::HideProgressInfo | KIO::Overwrite ); 0337 } 0338 else 0339 { 0340 debug() << "Copying from" << sourceUrl << "to" << destUrl; 0341 job = KIO::file_copy( sourceUrl, destUrl, -1, KIO::HideProgressInfo | KIO::Overwrite ); 0342 } 0343 } 0344 else 0345 { 0346 debug() << "Transcoding from" << sourceUrl << "to" << destUrl; 0347 job = new Transcoding::Job( sourceUrl, destUrl, m_transcodingConfig ); 0348 } 0349 job->setUiDelegate( nullptr ); // be non-interactive 0350 connect( job, &Transcoding::Job::finished, // we must use this instead of result() to prevent deadlock 0351 this, &IpodCopyTracksJob::slotCopyOrTranscodeJobFinished ); 0352 job->start(); // no-op for KIO job, but matters for transcoding job 0353 } 0354 0355 void 0356 IpodCopyTracksJob::slotCopyOrTranscodeJobFinished( KJob *job ) 0357 { 0358 if( job->error() != 0 && m_copyErrors.count() < 10 ) 0359 m_copyErrors.insert( job->errorString() ); 0360 m_copying.release( 1 ); // wakeup run() 0361 } 0362 0363 void 0364 IpodCopyTracksJob::slotDisplayErrorDialog() 0365 { 0366 int sourceSize = m_sources.size(); 0367 int successCount = m_sourceTrackStatus.count( Success ); 0368 0369 // match string with IpodCollectionLocation::prettyLocation() 0370 QString collName = m_coll ? m_coll->prettyName() : i18n( "Disconnected iPod/iPad/iPhone" ); 0371 QString caption = i18nc( "%1 is collection pretty name, e.g. My Little iPod", 0372 "Transferred Tracks to %1", collName ); 0373 QString text; 0374 if( successCount ) 0375 text = i18np( "One track successfully transferred, but transfer of some other tracks failed.", 0376 "%1 tracks successfully transferred, but transfer of some other tracks failed.", 0377 successCount ); 0378 else 0379 text = i18n( "Transfer of tracks failed." ); 0380 QString details; 0381 int exceededingSafeCapacityCount = m_sourceTrackStatus.count( ExceededingSafeCapacity ); 0382 if( exceededingSafeCapacityCount ) 0383 { 0384 details += i18np( "One track was not transferred because it would exceed iPod capacity.<br>", 0385 "%1 tracks were not transferred because it would exceed iPod capacity.<br>", 0386 exceededingSafeCapacityCount ); 0387 0388 QString reservedSpace = m_coll ? QLocale().toString( 0389 m_coll->capacityMargin(), 1 ) : QString( "???" ); // improbable, don't bother translators 0390 0391 details += i18nc( "Example of %1 would be: 20.0 MiB", 0392 "<i>Amarok reserves %1 on iPod for iTunes database writing.</i><br>", 0393 reservedSpace ); 0394 } 0395 int notPlayableCount = m_sourceTrackStatus.count( NotPlayable ); 0396 if( notPlayableCount ) 0397 { 0398 QString formats = QStringList( m_notPlayableFormats.values() ).join( ", " ); 0399 details += i18np( "One track was not copied because it wouldn't be playable - its " 0400 " %2 format is unsupported.<br>", 0401 "%1 tracks were not copied because they wouldn't be playable - " 0402 "they are in unsupported formats (%2).<br>", 0403 notPlayableCount, formats ); 0404 } 0405 int copyingFailedCount = m_sourceTrackStatus.count( CopyingFailed ); 0406 if( copyingFailedCount ) 0407 { 0408 details += i18np( "Copy/move/transcode of one file failed.<br>", 0409 "Copy/move/transcode of %1 files failed.<br>", copyingFailedCount ); 0410 } 0411 int internalErrorCount = m_sourceTrackStatus.count( InternalError ); 0412 if( internalErrorCount ) 0413 { 0414 details += i18np( "One track was not transferred due to an internal Amarok error.<br>", 0415 "%1 tracks were not transferred due to an internal Amarok error.<br>", 0416 internalErrorCount ); 0417 details += i18n( "<i>You can find details in Amarok debugging output.</i><br>" ); 0418 } 0419 if( m_sourceTrackStatus.size() != sourceSize ) 0420 { 0421 // aborted case was already caught in run() 0422 details += i18n( "The rest was not transferred because iPod collection disappeared.<br>" ); 0423 } 0424 if( !m_copyErrors.isEmpty() ) 0425 { 0426 details += i18nc( "%1 is a list of errors that occurred during copying of tracks", 0427 "Error causes: %1<br>", QStringList( m_copyErrors.values() ).join( "<br>" ) ); 0428 } 0429 KMessageBox::detailedError( nullptr, text, details, caption ); 0430 } 0431 0432 void 0433 IpodCopyTracksJob::trackProcessed( CopiedStatus status, const Meta::TrackPtr &srcTrack, const Meta::TrackPtr &destTrack ) 0434 { 0435 m_sourceTrackStatus.insert( status, srcTrack ); 0436 Q_EMIT incrementProgress(); 0437 Q_EMIT signalTrackProcessed( srcTrack, destTrack, status ); 0438 } 0439