File indexing completed on 2024-04-28 04:50:21

0001 /*
0002     SPDX-FileCopyrightText: 2011 Michal Malek <michalm@jabster.pl>
0003     SPDX-FileCopyrightText: 1998-2010 Sebastian Trueg <trueg@k3b.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "k3bmassaudioencodingjob.h"
0009 #include "k3baudioencoder.h"
0010 #include "k3bcuefilewriter.h"
0011 #include "k3bwavefilewriter.h"
0012 
0013 #include <KLocalizedString>
0014 #include <KCDDB/CDInfo>
0015 
0016 #include <QDebug>
0017 #include <QDir>
0018 #include <QFileInfo>
0019 #include <QIODevice>
0020 #include <QTextStream>
0021 
0022 #include <vector>
0023 #include <algorithm>
0024 
0025 namespace K3b {
0026 
0027 namespace
0028 {
0029 
0030     struct SortByTrackNumber
0031     {
0032         SortByTrackNumber( MassAudioEncodingJob::Tracks const& tracks ) : m_tracks( tracks ) {}
0033 
0034         bool operator()( QString const& lhs, QString const& rhs )
0035         {
0036             return m_tracks.value( lhs ) < m_tracks.value( rhs );
0037         }
0038 
0039         MassAudioEncodingJob::Tracks const& m_tracks;
0040     };
0041 
0042     struct Task {
0043         Task(MassAudioEncodingJob::Tracks::const_iterator const & it) :
0044             tracknumber(it.value()),
0045             filename   (it.key()),
0046             track      (it)
0047         { }
0048 
0049         static
0050         bool sort_by_filename(Task const & lhs, Task const & rhs)
0051         {
0052           return lhs.filename < rhs.filename;
0053         }
0054 
0055         static
0056         bool sort_by_tracknumber(Task const & lhs, Task const & rhs)
0057         {
0058           return lhs.tracknumber < rhs.tracknumber;
0059         }
0060 
0061         int     tracknumber;
0062         QString filename;
0063         MassAudioEncodingJob::Tracks::const_iterator track;
0064     };
0065 
0066 } // namespace
0067 
0068 
0069 class MassAudioEncodingJob::Private
0070 {
0071 public:
0072     Private( bool be )
0073     :
0074         bigEndian( be ),
0075         overallBytesRead( 0 ),
0076         overallBytesToRead( 0 ),
0077         encoder( 0 ),
0078         waveFileWriter( 0 ),
0079         relativePathInPlaylist( false ),
0080         writeCueFile( false )
0081     {
0082     }
0083 
0084     const bool bigEndian;
0085     Tracks tracks;
0086     QHash<QString,Msf> lengths;
0087     qint64 overallBytesRead;
0088     qint64 overallBytesToRead;
0089     AudioEncoder* encoder;
0090     WaveFileWriter* waveFileWriter;
0091     QString fileType;
0092     KCDDB::CDInfo cddbEntry;
0093 
0094     QString playlistFilename;
0095     bool relativePathInPlaylist;
0096     bool writeCueFile;
0097 };
0098 
0099 
0100 MassAudioEncodingJob::MassAudioEncodingJob( bool bigEndian, JobHandler* jobHandler, QObject* parent )
0101     : ThreadJob( jobHandler, parent ),
0102       d( new Private( bigEndian ) )
0103 {
0104 }
0105 
0106 
0107 MassAudioEncodingJob::~MassAudioEncodingJob()
0108 {
0109 }
0110 
0111 
0112 void MassAudioEncodingJob::setCddbEntry( const KCDDB::CDInfo& cddbEntry )
0113 {
0114     d->cddbEntry = cddbEntry;
0115 }
0116 
0117 
0118 const KCDDB::CDInfo& MassAudioEncodingJob::cddbEntry() const
0119 {
0120     return d->cddbEntry;
0121 }
0122 
0123 
0124 void MassAudioEncodingJob::setEncoder( AudioEncoder* encoder )
0125 {
0126     d->encoder = encoder;
0127 }
0128 
0129 
0130 AudioEncoder* MassAudioEncodingJob::encoder() const
0131 {
0132     return d->encoder;
0133 }
0134 
0135 
0136 void MassAudioEncodingJob::setFileType( const QString& fileType )
0137 {
0138     d->fileType = fileType;
0139 }
0140 
0141 
0142 
0143 const QString& MassAudioEncodingJob::fileType() const
0144 {
0145     return d->fileType;
0146 }
0147 
0148 
0149 void MassAudioEncodingJob::setTrackList( const Tracks& tracks )
0150 {
0151     d->tracks = tracks;
0152 }
0153 
0154 
0155 const MassAudioEncodingJob::Tracks& MassAudioEncodingJob::trackList() const
0156 {
0157     return d->tracks;
0158 }
0159 
0160 
0161 void MassAudioEncodingJob::setWritePlaylist( const QString& filename, bool relativePaths )
0162 {
0163     d->playlistFilename = filename;
0164     d->relativePathInPlaylist = relativePaths;
0165 }
0166 
0167 
0168 void MassAudioEncodingJob::setWriteCueFile( bool writeCueFile )
0169 {
0170     d->writeCueFile = writeCueFile;
0171 }
0172 
0173 
0174 QString MassAudioEncodingJob::jobDetails() const
0175 {
0176     if( d->encoder )
0177         return i18np( "1 track (encoding to %2)",
0178                       "%1 tracks (encoding to %2)",
0179                       d->tracks.count(),
0180                       d->encoder->fileTypeComment(d->fileType) );
0181     else
0182         return i18np( "1 track", "%1 tracks",
0183                       d->tracks.count() );
0184 }
0185 
0186 
0187 QString MassAudioEncodingJob::jobTarget() const
0188 {
0189     Tracks::const_iterator it = d->tracks.constBegin();
0190     if( it != d->tracks.constEnd() )
0191         return QFileInfo( it.key() ).absolutePath();
0192     else
0193         return QString();
0194 }
0195 
0196 
0197 bool MassAudioEncodingJob::init()
0198 {
0199     return true;
0200 }
0201         
0202         
0203 void MassAudioEncodingJob::cleanup()
0204 {
0205 }
0206 
0207 
0208 bool MassAudioEncodingJob::run()
0209 {
0210     if ( !init() )
0211         return false;
0212 
0213     if( !d->encoder )
0214         if( !d->waveFileWriter )
0215             d->waveFileWriter = new K3b::WaveFileWriter();
0216 
0217     d->overallBytesRead = 0;
0218     d->overallBytesToRead = 0;
0219     d->lengths.clear();
0220 
0221     const QStringList tracksKeys = d->tracks.keys();
0222     const QSet<QString> fileNames = QSet<QString>(tracksKeys.begin(), tracksKeys.end());
0223     Q_FOREACH( const QString& filename, fileNames ) {
0224         d->lengths.insert( filename, 0 );
0225         Q_FOREACH( int trackNumber, d->tracks.values( filename ) ) {
0226             const Msf length = trackLength( trackNumber );
0227             d->lengths[ filename ] += length;
0228             d->overallBytesToRead += length.audioBytes();
0229         }
0230     }
0231 
0232     // rip tracks in *numerical* order
0233     std::vector<Task> tasks;
0234     tasks.reserve( d->tracks.size() );
0235     for( Tracks::const_iterator i = d->tracks.constBegin(); i != d->tracks.constEnd(); ++i )
0236         tasks.push_back( Task(i) );
0237     std::sort( tasks.begin(), tasks.end(), Task::sort_by_tracknumber );
0238 
0239     bool success = true;
0240     QString lastFilename;
0241     std::vector<Task>::const_iterator currentTask;
0242     for( currentTask = tasks.begin(); success && currentTask != tasks.end(); ++currentTask ) {
0243         success = encodeTrack( currentTask->track.value(), currentTask->track.key(), lastFilename );
0244         lastFilename = currentTask->track.key();
0245     }
0246 
0247     if( d->encoder )
0248         d->encoder->closeFile();
0249     if( d->waveFileWriter )
0250         d->waveFileWriter->close();
0251 
0252     if( !canceled() && success && !d->playlistFilename.isNull() ) {
0253         success = success && writePlaylist();
0254     }
0255 
0256     if( !canceled() && success && d->writeCueFile ) {
0257         success = success && writeCueFile();
0258     }
0259 
0260     if( canceled() ) {
0261         if( currentTask != tasks.end() ) {
0262             if( QFile::exists( currentTask->track.key() ) ) {
0263                 QFile::remove( currentTask->track.key() );
0264                 emit infoMessage( i18n("Removed partial file '%1'.", currentTask->track.key()), K3b::Job::MessageInfo );
0265             }
0266         }
0267 
0268         success = false;
0269     }
0270     
0271     cleanup();
0272     return success;
0273 }
0274 
0275 
0276 bool K3b::MassAudioEncodingJob::encodeTrack( int trackIndex, const QString& filename, const QString& prevFilename )
0277 {
0278     QScopedPointer<QIODevice> source( createReader( trackIndex ) );
0279     if( source.isNull() ) {
0280         return false;
0281     }
0282     
0283     QDir dir = QFileInfo( filename ).dir();
0284     if( !QDir().mkpath( dir.path() ) ) {
0285         emit infoMessage( i18n("Unable to create folder %1",dir.path()), K3b::Job::MessageError );
0286         return false;
0287     }
0288 
0289     // Close the previous file if the new filename is different
0290     if( prevFilename != filename ) {
0291         if( d->encoder )
0292             d->encoder->closeFile();
0293         if( d->waveFileWriter )
0294             d->waveFileWriter->close();
0295     }
0296 
0297     // Open the file to write if it is not already opened
0298     if( (d->encoder && !d->encoder->isOpen()) ||
0299         (d->waveFileWriter && !d->waveFileWriter->isOpen()) ) {
0300         bool isOpen = true;
0301         if( d->encoder ) {
0302             AudioEncoder::MetaData metaData;
0303             metaData.insert( AudioEncoder::META_ALBUM_ARTIST, d->cddbEntry.get( KCDDB::Artist ) );
0304             metaData.insert( AudioEncoder::META_ALBUM_TITLE, d->cddbEntry.get( KCDDB::Title ) );
0305             metaData.insert( AudioEncoder::META_ALBUM_COMMENT, d->cddbEntry.get( KCDDB::Comment ) );
0306             metaData.insert( AudioEncoder::META_YEAR, d->cddbEntry.get( KCDDB::Year ) );
0307             metaData.insert( AudioEncoder::META_GENRE, d->cddbEntry.get( KCDDB::Genre ) );
0308             if( d->tracks.count( filename ) == 1 ) {
0309                 metaData.insert( AudioEncoder::META_TRACK_NUMBER, QString::number(trackIndex).rightJustified( 2, '0' ) );
0310                 metaData.insert( AudioEncoder::META_TRACK_ARTIST, d->cddbEntry.track( trackIndex-1 ).get( KCDDB::Artist ) );
0311                 metaData.insert( AudioEncoder::META_TRACK_TITLE, d->cddbEntry.track( trackIndex-1 ).get( KCDDB::Title ) );
0312                 metaData.insert( AudioEncoder::META_TRACK_COMMENT, d->cddbEntry.track( trackIndex-1 ).get( KCDDB::Comment ) );
0313             }
0314             else {
0315                 metaData.insert( AudioEncoder::META_TRACK_ARTIST, d->cddbEntry.get( KCDDB::Artist ) );
0316                 metaData.insert( AudioEncoder::META_TRACK_TITLE, d->cddbEntry.get( KCDDB::Title ) );
0317                 metaData.insert( AudioEncoder::META_TRACK_COMMENT, d->cddbEntry.get( KCDDB::Comment ) );
0318             }
0319 
0320             isOpen = d->encoder->openFile( d->fileType, filename, d->lengths[ filename ], metaData );
0321             if( !isOpen )
0322                 emit infoMessage( d->encoder->lastErrorString(), K3b::Job::MessageError );
0323         }
0324         else {
0325             isOpen = d->waveFileWriter->open( filename );
0326         }
0327 
0328         if( !isOpen ) {
0329             emit infoMessage( i18n("Unable to open '%1' for writing.",filename), K3b::Job::MessageError );
0330             return false;
0331         }
0332     }
0333 
0334     trackStarted( trackIndex );
0335 
0336     // do the conversion
0337     // ----------------------
0338 
0339     char buffer[10*1024];
0340     const qint64 bufferLength = 10LL*1024LL;
0341     qint64 readLength = 0;
0342     qint64 readFile = 0;
0343 
0344     if( !source->open( QIODevice::ReadOnly ) ) {
0345         emit infoMessage( source->errorString(), Job::MessageError );
0346         return false;
0347     }
0348 
0349     while( !canceled() && !source->atEnd() && ( readLength = source->read( buffer, bufferLength ) ) > 0 ) {
0350 
0351         if( d->encoder ) {
0352 
0353             if( d->bigEndian ) {
0354                 // the tracks produce big endian samples
0355                 // and encoder encoder consumes little endian
0356                 // so we need to swap the bytes here
0357                 char b;
0358                 for( qint64 i = 0; i < bufferLength-1; i+=2 ) {
0359                     b = buffer[i];
0360                     buffer[i] = buffer[i+1];
0361                     buffer[i+1] = b;
0362                 }
0363             }
0364 
0365             if( d->encoder->encode( buffer, readLength ) < 0 ) {
0366                 qDebug() << "error while encoding.";
0367                 emit infoMessage( d->encoder->lastErrorString(), K3b::Job::MessageError );
0368                 emit infoMessage( i18n("Error while encoding track %1.",trackIndex), K3b::Job::MessageError );
0369                 return false;
0370             }
0371         }
0372         else {
0373             d->waveFileWriter->write( buffer,
0374                                       readLength,
0375                                       d->bigEndian ? WaveFileWriter::BigEndian : WaveFileWriter::LittleEndian );
0376         }
0377 
0378         d->overallBytesRead += readLength;
0379         readFile += readLength;
0380         emit subPercent( 100LL*readFile/source->size() );
0381         emit percent( 100LL*d->overallBytesRead/d->overallBytesToRead );
0382     }
0383 
0384     if( !canceled() && !source->atEnd() ) {
0385         emit infoMessage( source->errorString(), Job::MessageError );
0386         return false;
0387     }
0388 
0389     trackFinished( trackIndex, filename );
0390     return source->atEnd();
0391 }
0392 
0393 
0394 bool MassAudioEncodingJob::writePlaylist()
0395 {
0396     QFileInfo playlistInfo( d->playlistFilename );
0397     QDir playlistDir( playlistInfo.dir() );
0398 
0399     if( !QDir().mkpath( playlistDir.path() ) ) {
0400         emit infoMessage( i18n("Unable to create folder %1",playlistDir.path()), Job::MessageError );
0401         return false;
0402     }
0403 
0404     emit infoMessage( i18n("Writing playlist to %1.", d->playlistFilename ), Job::MessageInfo );
0405 
0406     QFile f( d->playlistFilename );
0407     if( f.open( QIODevice::WriteOnly ) ) {
0408         QTextStream t( &f );
0409 
0410         // format descriptor
0411         t << "#EXTM3U" << Qt::endl;
0412 
0413         // Get list of the ripped filenames sorted by track number
0414         QStringList filenames = d->tracks.keys();
0415         filenames.removeDuplicates();
0416         std::sort(filenames.begin(), filenames.end(), SortByTrackNumber(d->tracks));
0417 
0418         Q_FOREACH( const QString& trackFile, filenames ) {
0419 
0420             // extra info
0421             t << "#EXTINF:" << d->lengths[trackFile].totalFrames()/75 << ",";
0422 
0423             QVariant artist;
0424             QVariant title;
0425 
0426             QList<int> trackNums = d->tracks.values( trackFile );
0427             if( trackNums.count() == 1 ) {
0428                 int trackIndex = trackNums.first()-1;
0429                 artist = d->cddbEntry.track( trackIndex ).get( KCDDB::Artist );
0430                 title = d->cddbEntry.track( trackIndex ).get( KCDDB::Title );
0431             }
0432             else {
0433                 artist = d->cddbEntry.get( KCDDB::Artist );
0434                 title = d->cddbEntry.get( KCDDB::Title );
0435             }
0436 
0437             if( !artist.toString().isEmpty() && !title.toString().isEmpty() ) {
0438                 t << artist.toString() << " - " << title.toString() << Qt::endl;
0439             }
0440             else {
0441                 t << QFileInfo( trackFile ).baseName() << Qt::endl;
0442             }
0443 
0444             // filename
0445             if( d->relativePathInPlaylist )
0446                 t << playlistDir.relativeFilePath( trackFile ) << Qt::endl;
0447             else
0448                 t << trackFile << Qt::endl;
0449         }
0450 
0451         return ( t.status() == QTextStream::Ok );
0452     }
0453     else {
0454         emit infoMessage( i18n("Unable to open '%1' for writing.",d->playlistFilename), Job::MessageError );
0455         qDebug() << "could not open file " << d->playlistFilename << " for writing.";
0456         return false;
0457     }
0458 }
0459 
0460 
0461 bool MassAudioEncodingJob::writeCueFile()
0462 {
0463     bool success = true;
0464     const QStringList tracksKeys = d->tracks.keys();
0465     const QSet<QString> fileNames = QSet<QString>(tracksKeys.begin(), tracksKeys.end());
0466     Q_FOREACH( const QString& filename, fileNames ) {
0467         CueFileWriter cueWriter;
0468 
0469         // create a new toc and cd-text
0470         Device::Toc toc;
0471         Device::CdText text;
0472         text.setPerformer( d->cddbEntry.get( KCDDB::Artist ).toString() );
0473         text.setTitle( d->cddbEntry.get( KCDDB::Title ).toString() );
0474         Msf currentSector;
0475 
0476         QList<int> trackNums = d->tracks.values( filename );
0477         for( int i = 0; i < trackNums.size(); ++i ) {
0478             const int trackNum = trackNums[ i ];
0479             const Msf length = trackLength( trackNum );
0480             Device::Track newTrack( currentSector, currentSector + length - 1, Device::Track::TYPE_AUDIO );
0481             toc.append( newTrack );
0482 
0483             text.track(i).setPerformer( d->cddbEntry.track( trackNum-1 ).get( KCDDB::Artist ).toString() );
0484             text.track(i).setTitle( d->cddbEntry.track( trackNum-1 ).get( KCDDB::Title ).toString() );
0485             
0486             currentSector += length;
0487         }
0488 
0489         cueWriter.setData( toc );
0490         cueWriter.setCdText( text );
0491         
0492         QFileInfo fileInfo( filename );
0493 
0494         // we always use a relative filename here
0495         cueWriter.setImage( fileInfo.fileName(), ( d->fileType.isEmpty() ? QString("WAVE") : d->fileType ) );
0496 
0497         // use the same base name as the image file
0498         QString cueFile = fileInfo.dir().filePath( fileInfo.baseName() + ".cue" );
0499 
0500         emit infoMessage( i18n("Writing cue file to %1.",cueFile), Job::MessageInfo );
0501 
0502         success = success && cueWriter.save( cueFile );
0503     }
0504     return success;
0505 }
0506 
0507 } // namespace K3b
0508 
0509 #include "moc_k3bmassaudioencodingjob.cpp"