/* * * $Id: k3baudioripthread.cpp 619556 2007-01-03 17:38:12Z trueg $ * Copyright (C) 2003 Sebastian Trueg <trueg@k3b.org> * * This file is part of the K3b project. * Copyright (C) 1998-2007 Sebastian Trueg <trueg@k3b.org> * * 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. * See the file "COPYING" for the exact licensing terms. */ #include "k3baudioripthread.h" #include "k3bpatternparser.h" #include <k3bcdparanoialib.h> #include <k3bjob.h> #include <k3baudioencoder.h> #include <k3bwavefilewriter.h> #include <k3bglobalsettings.h> #include <k3bcore.h> #include "k3bcuefilewriter.h" #include <k3bdevice.h> #include <k3btoc.h> #include <k3btrack.h> #include <k3bglobals.h> #include <tqfile.h> #include <tqfileinfo.h> #include <tqtimer.h> #include <kdebug.h> #include <tdelocale.h> #include <kstandarddirs.h> class K3bAudioRipThread::Private { public: Private() : paranoiaRetries(5), neverSkip(false), encoder(0), waveFileWriter(0), paranoiaLib(0), canceled(false) { } // the index of the currently ripped track in m_tracks int currentTrackIndex; long overallSectorsRead; long overallSectorsToRead; int paranoiaMode; int paranoiaRetries; int neverSkip; K3bAudioEncoder* encoder; K3bWaveFileWriter* waveFileWriter; K3bCdparanoiaLib* paranoiaLib; bool canceled; K3bDevice::Toc toc; TQString fileType; }; K3bAudioRipThread::K3bAudioRipThread() : TQObject(), K3bThread(), m_device(0), m_useIndex0(false) { d = new Private(); } K3bAudioRipThread::~K3bAudioRipThread() { delete d->waveFileWriter; delete d->paranoiaLib; delete d; } void K3bAudioRipThread::setFileType( const TQString& t ) { d->fileType = t; } void K3bAudioRipThread::setParanoiaMode( int mode ) { d->paranoiaMode = mode; } void K3bAudioRipThread::setMaxRetries( int r ) { d->paranoiaRetries = r; } void K3bAudioRipThread::setNeverSkip( bool b ) { d->neverSkip = b; } void K3bAudioRipThread::setEncoder( K3bAudioEncoder* f ) { d->encoder = f; } void K3bAudioRipThread::run() { emitStarted(); emitNewTask( i18n("Extracting Digital Audio") ); if( !d->paranoiaLib ) { d->paranoiaLib = K3bCdparanoiaLib::create(); } if( !d->paranoiaLib ) { emitInfoMessage( i18n("Could not load libcdparanoia."), K3bJob::ERROR ); emitFinished(false); return; } // try to open the device if( !m_device ) { emitFinished(false); return; } m_device->block(true); emitInfoMessage( i18n("Reading CD table of contents."), K3bJob::INFO ); d->toc = m_device->readToc(); if( !d->paranoiaLib->initParanoia( m_device, d->toc ) ) { emitInfoMessage( i18n("Could not open device %1").arg(m_device->blockDeviceName()), K3bJob::ERROR ); m_device->block(false); // check if we have write access to the generic device if( m_device->interfaceType() == K3bDevice::SCSI && !m_device->genericDevice().isEmpty() && !TQFileInfo( m_device->genericDevice() ).isWritable() ) emitInfoMessage( i18n("You need write access to %1").arg( m_device->genericDevice() ), K3bJob::ERROR ); emitFinished(false); return; } d->paranoiaLib->setParanoiaMode( d->paranoiaMode ); d->paranoiaLib->setNeverSkip( d->neverSkip ); d->paranoiaLib->setMaxRetries( d->paranoiaRetries ); if( !d->encoder ) if( !d->waveFileWriter ) { d->waveFileWriter = new K3bWaveFileWriter(); } if( m_useIndex0 ) { emitNewSubTask( i18n("Searching index 0 for all tracks") ); m_device->indexScan( d->toc ); } d->canceled = false; d->overallSectorsRead = 0; d->overallSectorsToRead = 0; for( unsigned int i = 0; i < m_tracks.count(); ++i ) { if( m_useIndex0 ) d->overallSectorsToRead += d->toc[m_tracks[i].first-1].realAudioLength().lba(); else d->overallSectorsToRead += d->toc[m_tracks[i].first-1].length().lba(); } if( m_singleFile ) { TQString& filename = m_tracks[0].second; TQString dir = filename.left( filename.findRev("/") ); if( !TDEStandardDirs::makeDir( dir, 0777 ) ) { d->paranoiaLib->close(); emitInfoMessage( i18n("Unable to create directory %1").arg(dir), K3bJob::ERROR ); m_device->block(false); emitFinished(false); return; } // initialize bool isOpen = true; if( d->encoder ) { if( isOpen = d->encoder->openFile( d->fileType, filename, d->overallSectorsToRead ) ) { // here we use cd Title and Artist d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_ARTIST, m_cddbEntry.cdArtist ); d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_TITLE, m_cddbEntry.cdTitle ); d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_COMMENT, m_cddbEntry.cdExtInfo ); d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_ARTIST, m_cddbEntry.cdArtist ); d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_TITLE, m_cddbEntry.cdTitle ); d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_COMMENT, m_cddbEntry.cdExtInfo ); d->encoder->setMetaData( K3bAudioEncoder::META_YEAR, TQString::number(m_cddbEntry.year) ); d->encoder->setMetaData( K3bAudioEncoder::META_GENRE, m_cddbEntry.genre ); } else emitInfoMessage( d->encoder->lastErrorString(), K3bJob::ERROR ); } else { isOpen = d->waveFileWriter->open( filename ); } if( !isOpen ) { d->paranoiaLib->close(); emitInfoMessage( i18n("Unable to open '%1' for writing.").arg(filename), K3bJob::ERROR ); m_device->block(false); emitFinished(false); return; } emitInfoMessage( i18n("Ripping to single file '%1'.").arg(filename), K3bJob::INFO ); } emitInfoMessage( i18n("Starting digital audio extraction (ripping)."), K3bJob::INFO ); bool success = true; for( unsigned int i = 0; i < m_tracks.count(); ++i ) { d->currentTrackIndex = i; if( !ripTrack( m_tracks[i].first, m_singleFile ? m_tracks[0].second : m_tracks[i].second ) ) { success = false; break; } } if( m_singleFile ) { if( d->encoder ) d->encoder->closeFile(); else d->waveFileWriter->close(); if( success && !d->canceled ) { TQString& filename = m_tracks[0].second; emitInfoMessage( i18n("Successfully ripped to %2.").arg(filename), K3bJob::INFO ); } } if( !d->canceled && m_writePlaylist ) { success = success && writePlaylist(); } if( !d->canceled && m_writeCueFile && m_singleFile ) { if( !m_useIndex0 ) { emitNewSubTask( i18n("Searching index 0 for all tracks") ); m_device->indexScan( d->toc ); } success = success && writeCueFile(); } d->paranoiaLib->close(); m_device->block(false); if( d->canceled ) { emitCanceled(); emitFinished(false); } else { if( k3bcore->globalSettings()->ejectMedia() ) m_device->eject(); emitFinished(success); } } bool K3bAudioRipThread::ripTrack( int track, const TQString& filename ) { const K3bTrack& tt = d->toc[track-1]; long endSec = ( (m_useIndex0 && tt.index0() > 0) ? tt.firstSector().lba() + tt.index0().lba() - 1 : tt.lastSector().lba() ); if( d->paranoiaLib->initReading( tt.firstSector().lba(), endSec ) ) { long trackSectorsRead = 0; TQString dir = filename.left( filename.findRev("/") ); if( !TDEStandardDirs::makeDir( dir, 0777 ) ) { emitInfoMessage( i18n("Unable to create directory %1").arg(dir), K3bJob::ERROR ); return false; } // initialize bool isOpen = true; if( !m_singleFile ) { if( d->encoder ) { if( isOpen = d->encoder->openFile( d->fileType, filename, m_useIndex0 ? d->toc[track-1].realAudioLength() : d->toc[track-1].length() ) ) { d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_ARTIST, m_cddbEntry.artists[track-1] ); d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_TITLE, m_cddbEntry.titles[track-1] ); d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_COMMENT, m_cddbEntry.extInfos[track-1] ); d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_NUMBER, TQString::number(track).rightJustify( 2, '0' ) ); d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_ARTIST, m_cddbEntry.cdArtist ); d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_TITLE, m_cddbEntry.cdTitle ); d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_COMMENT, m_cddbEntry.cdExtInfo ); d->encoder->setMetaData( K3bAudioEncoder::META_YEAR, TQString::number(m_cddbEntry.year) ); d->encoder->setMetaData( K3bAudioEncoder::META_GENRE, m_cddbEntry.genre ); } else emitInfoMessage( d->encoder->lastErrorString(), K3bJob::ERROR ); } else { isOpen = d->waveFileWriter->open( filename ); } if( !isOpen ) { emitInfoMessage( i18n("Unable to open '%1' for writing.").arg(filename), K3bJob::ERROR ); return false; } } if( !m_cddbEntry.artists[track-1].isEmpty() && !m_cddbEntry.titles[track-1].isEmpty() ) emitNewSubTask( i18n("Ripping track %1 (%2 - %3)").arg(track).arg(m_cddbEntry.artists[track-1]).arg(m_cddbEntry.titles[track-1]) ); else emitNewSubTask( i18n("Ripping track %1").arg(track) ); int status; while( 1 ) { if( d->canceled ) { cleanupAfterCancellation(); return false; } char* buf = d->paranoiaLib->read( &status ); if( status == K3bCdparanoiaLib::S_OK ) { if( buf == 0 ) { if( m_singleFile ) emitInfoMessage( i18n("Successfully ripped track %1.").arg(track), K3bJob::INFO ); else emitInfoMessage( i18n("Successfully ripped track %1 to %2.").arg(track).arg(filename), K3bJob::INFO ); if( !m_singleFile ) { if( d->encoder ) d->encoder->closeFile(); else d->waveFileWriter->close(); } return true; } else { if( d->encoder ) { if( d->encoder->encode( buf, CD_FRAMESIZE_RAW ) < 0 ) { kdDebug() << "(K3bAudioRipThread) error while encoding." << endl; emitInfoMessage( d->encoder->lastErrorString(), K3bJob::ERROR ); emitInfoMessage( i18n("Error while encoding track %1.").arg(track), K3bJob::ERROR ); return false; } } else d->waveFileWriter->write( buf, CD_FRAMESIZE_RAW, K3bWaveFileWriter::LittleEndian ); trackSectorsRead++; d->overallSectorsRead++; emitSubPercent( 100*trackSectorsRead/d->toc[track-1].length().lba() ); emitPercent( 100*d->overallSectorsRead/d->overallSectorsToRead ); } } else { emitInfoMessage( i18n("Unrecoverable error while ripping track %1.").arg(track), K3bJob::ERROR ); return false; } } return true; } else { emitInfoMessage( i18n("Error while initializing audio ripping."), K3bJob::ERROR ); return false; } } void K3bAudioRipThread::cancel() { d->canceled = true; // what if paranoia is stuck in paranoia_read? // we need to terminate in that case // wait for 1 second. I the thread still is working terminate it // and trigger the finished slot manually emitInfoMessage( i18n("Cancellation could take a while..."), K3bJob::INFO ); TQTimer::singleShot( 1000, this, TQ_SLOT(slotCheckIfThreadStillRunning()) ); } void K3bAudioRipThread::slotCheckIfThreadStillRunning() { if( running() ) { d->paranoiaLib->close(); m_device->block(false); // this could happen if the thread is stuck in paranoia_read // because of an unreadable cd terminate(); cleanupAfterCancellation(); emitCanceled(); emitFinished(false); } } // this needs to be called if the thread was killed due to a hung paranoia_read void K3bAudioRipThread::cleanupAfterCancellation() { if( d->currentTrackIndex >= 0 && d->currentTrackIndex < (int)m_tracks.count() ) { if( TQFile::exists( m_tracks[d->currentTrackIndex].second ) ) { TQFile::remove( m_tracks[d->currentTrackIndex].second ); emitInfoMessage( i18n("Removed partial file '%1'.").arg(m_tracks[d->currentTrackIndex].second), K3bJob::INFO ); } } } bool K3bAudioRipThread::writePlaylist() { // this is an absolut path so there is always a "/" TQString playlistDir = m_playlistFilename.left( m_playlistFilename.findRev( "/" ) ); if( !TDEStandardDirs::makeDir( playlistDir ) ) { emitInfoMessage( i18n("Unable to create directory %1").arg(playlistDir), K3bJob::ERROR ); return false; } emitInfoMessage( i18n("Writing playlist to %1.").arg( m_playlistFilename ), K3bJob::INFO ); TQFile f( m_playlistFilename ); if( f.open( IO_WriteOnly ) ) { TQTextStream t( &f ); // format descriptor t << "#EXTM3U" << endl; // now write the entries (or the entry if m_singleFile) if( m_singleFile ) { // extra info t << "#EXTINF:" << d->overallSectorsToRead/75 << ","; if( !m_cddbEntry.cdArtist.isEmpty() && !m_cddbEntry.cdTitle.isEmpty() ) t << m_cddbEntry.cdArtist << " - " << m_cddbEntry.cdTitle << endl; else t << m_tracks[0].second.mid(m_tracks[0].second.findRev("/") + 1, m_tracks[0].second.length() - m_tracks[0].second.findRev("/") - 5) << endl; // filename without extension // filename if( m_relativePathInPlaylist ) t << findRelativePath( m_tracks[0].second, playlistDir ) << endl; else t << m_tracks[0].second << endl; } else { for( unsigned int i = 0; i < m_tracks.count(); ++i ) { int trackIndex = m_tracks[i].first-1; // extra info t << "#EXTINF:" << d->toc[trackIndex].length().totalFrames()/75 << ","; if( !m_cddbEntry.artists[trackIndex].isEmpty() && !m_cddbEntry.titles[trackIndex].isEmpty() ) t << m_cddbEntry.artists[trackIndex] << " - " << m_cddbEntry.titles[trackIndex] << endl; else t << m_tracks[i].second.mid(m_tracks[i].second.findRev("/") + 1, m_tracks[i].second.length() - m_tracks[i].second.findRev("/") - 5) << endl; // filename without extension // filename if( m_relativePathInPlaylist ) t << findRelativePath( m_tracks[i].second, playlistDir ) << endl; else t << m_tracks[i].second << endl; } } return ( t.device()->status() == IO_Ok ); } else { emitInfoMessage( i18n("Unable to open '%1' for writing.").arg(m_playlistFilename), K3bJob::ERROR ); kdDebug() << "(K3bAudioRipThread) could not open file " << m_playlistFilename << " for writing." << endl; return false; } } bool K3bAudioRipThread::writeCueFile() { K3bCueFileWriter cueWriter; // create a new toc and cd-text K3bDevice::Toc toc; K3bDevice::CdText text; text.setPerformer( m_cddbEntry.cdArtist ); text.setTitle( m_cddbEntry.cdTitle ); text.reserve( m_tracks.count() ); K3b::Msf currentSector; for( unsigned int i = 0; i < m_tracks.count(); ++i ) { int trackNum = m_tracks[i].first; const K3bDevice::Track& oldTrack = d->toc[trackNum-1]; K3bDevice::Track newTrack( oldTrack ); newTrack.setFirstSector( currentSector ); newTrack.setLastSector( (currentSector+=oldTrack.length()) - 1 ); toc.append( newTrack ); K3bDevice::TrackCdText trackText; trackText.setPerformer( m_cddbEntry.artists[trackNum-1] ); trackText.setTitle( m_cddbEntry.titles[trackNum-1] ); text.append( trackText ); } cueWriter.setData( toc ); cueWriter.setCdText( text ); // we always use a relative filename here TQString imageFile = m_tracks[0].second.section( '/', -1 ); cueWriter.setImage( imageFile, ( d->fileType.isEmpty() ? TQString("WAVE") : d->fileType ) ); // use the same base name as the image file TQString cueFile = m_tracks[0].second; cueFile.truncate( cueFile.findRev(".") ); cueFile += ".cue"; emitInfoMessage( i18n("Writing cue file to %1.").arg(cueFile), K3bJob::INFO ); return cueWriter.save( cueFile ); } TQString K3bAudioRipThread::findRelativePath( const TQString& absPath, const TQString& baseDir ) { TQString baseDir_ = K3b::prepareDir( K3b::fixupPath(baseDir) ); TQString path = K3b::fixupPath( absPath ); // both paths have an equal beginning. That's just how it's configured by K3b int pos = baseDir_.find( "/" ); int oldPos = pos; while( pos != -1 && path.left( pos+1 ) == baseDir_.left( pos+1 ) ) { oldPos = pos; pos = baseDir_.find( "/", pos+1 ); } // now the paths are equal up to oldPos, so that's how "deep" we go path = path.mid( oldPos+1 ); baseDir_ = baseDir_.mid( oldPos+1 ); int numberOfDirs = baseDir_.contains( '/' ); for( int i = 0; i < numberOfDirs; ++i ) path.prepend( "../" ); return path; } TQString K3bAudioRipThread::jobDescription() const { if( m_cddbEntry.cdTitle.isEmpty() ) return i18n("Ripping Audio Tracks"); else return i18n("Ripping Audio Tracks From '%1'").arg(m_cddbEntry.cdTitle); } TQString K3bAudioRipThread::jobDetails() const { if( d->encoder ) return i18n("1 track (encoding to %1)", "%n tracks (encoding to %1)", m_tracks.count() ).arg(d->encoder->fileTypeComment(d->fileType)); else return i18n("1 track", "%n tracks", m_tracks.count() ); } #include "k3baudioripthread.moc"