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