You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
k3b/src/rip/k3baudioripthread.cpp

603 lines
17 KiB

/*
*
* $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, TQT_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"