audiocd.cpp 39.3 KB
Newer Older
Rik Hemsley's avatar
Rik Hemsley committed
1
/*
Benjamin Meyer's avatar
Benjamin Meyer committed
2
 * Copyright (C) 2000 Rik Hemsley (rikkus) <rik@kde.org>
Benjamin Meyer's avatar
Benjamin Meyer committed
3
 * Copyright (C) 2000-2002 Michael Matz <matz@kde.org>
Benjamin Meyer's avatar
Benjamin Meyer committed
4 5 6 7
 * Copyright (C) 2001 Carsten Duvenhorst <duvenhorst@m2.uni-hannover.de>
 * Copyright (C) 2001 Adrian Schroeter <adrian@suse.de>
 * Copyright (C) 2003 Richard Lärkäng <richard@goteborg.utfors.se>
 * Copyright (C) 2003 Scott Wheeler <wheeler@kde.org>
Benjamin Meyer's avatar
Benjamin Meyer committed
8
 * Copyright (C) 2004-2005 Benjamin Meyer <ben at meyerhome dot net>
Benjamin Meyer's avatar
Benjamin Meyer committed
9 10 11 12 13 14 15 16
 *
 * 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.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Benjamin Meyer's avatar
Benjamin Meyer committed
18 19 20 21
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
Dirk Mueller's avatar
Dirk Mueller committed
22
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Benjamin Meyer's avatar
Benjamin Meyer committed
23
 */
Rik Hemsley's avatar
Rik Hemsley committed
24

Yuri Chornoivan's avatar
Yuri Chornoivan committed
25
#include "audiocd.h"
26
#include <config-audiocd.h>
27 28 29 30 31 32 33
#include <kio_version.h>

#if KIO_VERSION >= QT_VERSION_CHECK(5, 48, 0)
    #define INSERT(A, B) fastInsert(A, B);
#else
    #define INSERT(A, B) insert(A, B);
#endif
Rik Hemsley's avatar
Rik Hemsley committed
34 35 36

extern "C"
{
Olivier Goffart's avatar
Compile  
Olivier Goffart committed
37
	//cdda_interface.h in cdparanoia 10.2 has a member called 'private' which the C++ compiler doesn't like
38 39
    //we will thus use a generated local copy which renames that member.
	#include "cdda_interface.hpp"
40 41
	#include <cdda_paranoia.h>
	void paranoiaCallback(long, int);
42

Yuri Chornoivan's avatar
Compile  
Yuri Chornoivan committed
43
	int Q_DECL_EXPORT kdemain(int argc, char ** argv);
Rik Hemsley's avatar
Rik Hemsley committed
44
}
Benjamin Meyer's avatar
Benjamin Meyer committed
45

46
#include "plugins/audiocdencoder.h"
Benjamin Meyer's avatar
Benjamin Meyer committed
47 48 49 50

#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
51 52 53
#include <sys/time.h>
#include <sys/resource.h>

54
#include <QApplication>
Laurent Montel's avatar
Laurent Montel committed
55
#include <QFile>
Yuri Chornoivan's avatar
Yuri Chornoivan committed
56 57
#include <QFileInfo>
#include <QGlobalStatic>
Laurent Montel's avatar
Laurent Montel committed
58
#include <QHash>
59 60 61 62 63
#include <QRegExp>
#include <QUrlQuery>

#include "audiocd_kio_debug.h"

64
#include <KLocalizedString>
65 66
#include <KMacroExpander>

67
// CDDB
Leslie Zhai's avatar
Leslie Zhai committed
68 69
#include <KCddb/Client>
#include <KCompactDisc/KCompactDisc>
Rik Hemsley's avatar
Rik Hemsley committed
70

71 72 73 74 75 76 77
// Pseudo plugin class to embed metadata
class KIOPluginForMetaData : public QObject
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.kde.kio.slave.audiocd" FILE "audiocd.json")
};

Rik Hemsley's avatar
Rik Hemsley committed
78 79
using namespace KIO;

Albert Astals Cid's avatar
Albert Astals Cid committed
80
#define CDDB_INFORMATION I18N_NOOP("CDDB Information")
Rik Hemsley's avatar
Rik Hemsley committed
81

82 83
using namespace AudioCD;

84
extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
Rik Hemsley's avatar
Rik Hemsley committed
85
{
86 87 88 89 90 91
    // QApplication uses libkcddb which needs a valid kapp pointer
    // GUIenabled must be true as libkcddb sometimes wants to communicate
    // with the user
    qunsetenv("SESSION_MANAGER");
    QApplication app(argc, argv);
    app.setApplicationName(QStringLiteral("kio_audiocd"));
92
    KLocalizedString::setApplicationDomain("kio_audiocd");
93 94 95 96 97 98 99 100 101 102 103 104 105 106

    if (argc != 4) {
        fprintf(stderr, "Usage: kio_audiocd protocol pool app\n");
        exit(-1);
    }

    qCDebug(AUDIOCD_KIO_LOG) << "Starting " << getpid();

    AudioCDProtocol slave(argv[1], argv[2], argv[3]);
    slave.dispatchLoop();

    qCDebug(AUDIOCD_KIO_LOG) << "Done";

    return 0;
Rik Hemsley's avatar
Rik Hemsley committed
107 108
}

Benjamin Meyer's avatar
Benjamin Meyer committed
109 110 111
enum Which_dir {
	Unknown = 0, // Error
	Info, // CDDB info
112
	Base, // The ioslave base directory showing all drives
Benjamin Meyer's avatar
Benjamin Meyer committed
113 114
	Root, // The root directory, shows all these :)
	FullCD, // Show a single file containing all of the data
115 116
	EncoderDir, // The root directory created by an encoder
	SubDir // A directory created from the Album name configuration
117
};
Michael Matz's avatar
Michael Matz committed
118

Benjamin Meyer's avatar
Benjamin Meyer committed
119 120
class AudioCDProtocol::Private {
public:
121 122
	Private() : s_info(i18n("Information")), s_fullCD(i18n("Full CD"))
	{
123
		clearURLargs();
Benjamin Meyer's avatar
Benjamin Meyer committed
124 125
	}

126
	void clearURLargs() {
Benjamin Meyer's avatar
Benjamin Meyer committed
127 128 129
		req_allTracks = false;
		which_dir = Unknown;
		req_track = -1;
130
		cddbUserChoice = -1;
Benjamin Meyer's avatar
Benjamin Meyer committed
131
	}
132

133 134 135 136 137
	bool tocsAreDifferent(struct cdrom_drive *drive)
	{
		if (tracks != (uint)drive->tracks) return true;
		for (int i = 0; i < drive->tracks; ++i)
		{
138
			if (disc_toc[i].dwStartSector != drive->disc_toc[i].dwStartSector ||
139 140 141 142 143
			    disc_toc[i].bFlags != drive->disc_toc[i].bFlags ||
			    disc_toc[i].bTrack != drive->disc_toc[i].bTrack) return true;
		}
		return false;
	}
144

145 146 147 148 149 150 151 152 153
	void setToc(struct cdrom_drive *drive)
	{
		for (int i = 0; i < drive->tracks; ++i)
		{
			disc_toc[i].dwStartSector = drive->disc_toc[i].dwStartSector;
			disc_toc[i].bFlags = drive->disc_toc[i].bFlags;
			disc_toc[i].bTrack = drive->disc_toc[i].bTrack;
		}
	}
Benjamin Meyer's avatar
Benjamin Meyer committed
154

155 156 157 158 159
	// The type/which of request
	bool req_allTracks;
	Which_dir which_dir;
	int req_track;
	QString fname;
160
	QString child_dir;
161 162 163 164 165
	AudioCDEncoder *encoder_dir_type;

	// Misc settings
	QString device; // URL settable
	int paranoiaLevel; // URL settable
Benjamin Meyer's avatar
Benjamin Meyer committed
166
	bool reportErrors;
167

168
	// Directory strings, never change after init
169 170
	const QString s_info;
	const QString s_fullCD;
Benjamin Meyer's avatar
Benjamin Meyer committed
171

172
	// Current CD
173
	TOC disc_toc[MAXTRK];
Benjamin Meyer's avatar
Benjamin Meyer committed
174
	unsigned tracks;
175
	bool trackIsAudio[100];
Benjamin Meyer's avatar
Benjamin Meyer committed
176

177
	// CDDB items
178
	KCDDB::Result cddbResult;
179 180 181 182 183 184
	CDInfoList cddbList;
	int cddbUserChoice; // URL settable
	KCDDB::CDInfo cddbBestChoice;

	// Template for ..
	QString fileNameTemplate; // URL settable
185 186
	QString albumNameTemplate; // URL settable
	QString fileLocationTemplate;  // URL settable
187 188
	QString rsearch;
	QString rreplace;
189

190 191
	// Current strings for this CD and or cddb selection
	QStringList templateTitles;
Benjamin Meyer's avatar
Benjamin Meyer committed
192
	QString templateAlbumName;
193
	QString templateFileLocation;
Rik Hemsley's avatar
Rik Hemsley committed
194 195
};

Benjamin Meyer's avatar
Benjamin Meyer committed
196 197
int paranoia_read_limited_error;

Laurent Montel's avatar
Laurent Montel committed
198
AudioCDProtocol::AudioCDProtocol(const QByteArray & protocol, const QByteArray & pool, const QByteArray & app)
199
	: SlaveBase(protocol, pool, app)
Rik Hemsley's avatar
Rik Hemsley committed
200
{
Benjamin Meyer's avatar
Benjamin Meyer committed
201 202
	d = new Private;
	// Add encoders
203
	AudioCDEncoder::findAllPlugins(this, encoders);
204 205 206 207 208
	if (encoders.isEmpty()) {
		qCCritical(AUDIOCD_KIO_LOG) << "No encoders available";
		return;
	}

209 210
	encoderTypeCDA = encoderFromExtension(QLatin1String( ".cda" ));
	encoderTypeWAV = encoderFromExtension(QLatin1String( ".wav" ));
Rik Hemsley's avatar
Rik Hemsley committed
211 212 213 214
}

AudioCDProtocol::~AudioCDProtocol()
{
215
    qDeleteAll(encoders);
Benjamin Meyer's avatar
Benjamin Meyer committed
216
    delete d;
Rik Hemsley's avatar
Rik Hemsley committed
217 218
}

219
AudioCDEncoder *AudioCDProtocol::encoderFromExtension(const QString& extension)
220
{
Benjamin Meyer's avatar
Benjamin Meyer committed
221
	AudioCDEncoder *encoder;
Benjamin Meyer's avatar
Benjamin Meyer committed
222 223
	for (int i = encoders.size()-1; i >= 0; --i) {
            encoder = encoders.at(i);
224
	    if(QLatin1String(".")+QLatin1String( encoder->fileType() ) == extension)
Benjamin Meyer's avatar
Benjamin Meyer committed
225 226
	        return encoder;
    	}
227 228 229

	qCWarning(AUDIOCD_KIO_LOG) << "No encoder available for format" << extension;
	return nullptr;
Richard Lärkäng's avatar
Richard Lärkäng committed
230
}
231

232
AudioCDEncoder *AudioCDProtocol::determineEncoder(const QString & filename)
233
{
234 235
	int pos = filename.lastIndexOf(QLatin1Char( '.' ));
	return encoderFromExtension(filename.mid(pos));
236 237
}

238
static void setDeviceToCd(KCompactDisc *cd, struct cdrom_drive *drive)
Rik Hemsley's avatar
Rik Hemsley committed
239
{
240
#if defined(HAVE_CDDA_IOCTL_DEVICE)
241
	cd->setDevice(QLatin1String( drive->ioctl_device_name ), 50, false);
242
#elif defined(__FreeBSD__) || defined(__DragonFly__)
Yuri Chornoivan's avatar
Yuri Chornoivan committed
243
	// FreeBSD's cdparanoia as of January 5th 2006 has rather broken
244 245 246 247 248 249 250 251 252 253
	// support for non-SCSI devices. Although it finds ATA cdroms just
	// fine, there is no straightforward way to discover the device
	// name associated with the device, which throws the rest of audiocd
	// for a loop.
	//
	if ( !(drive->dev) || (COOKED_IOCTL == drive->interface) )
	{
		// For ATAPI devices, we have no real choice. Use the
		// user selected value, even if there is none.
		//
254
		qCWarning(AUDIOCD_KIO_LOG) << "Found an ATAPI device, assuming it is the one specified by the user.";
255
		cd->setDevice( drive->cdda_device_name );
256 257 258
	}
	else
	{
259
		qCDebug(AUDIOCD_KIO_LOG) << "Found a SCSI or ATAPICAM device.";
260 261
		if ( strlen(drive->dev->device_path) > 0 )
		{
262
			cd->setDevice( drive->dev->device_path );
263 264 265 266 267 268 269 270 271 272
		}
		else
		{
			// But the device_path can be empty under some
			// circumstances, so build a representation from
			// the unit number and SCSI device name.
			//
			QString devname = QString::fromLatin1( "/dev/%1%2" )
				.arg( drive->dev->given_dev_name )
				.arg( drive->dev->given_unit_number ) ;
273
			qCDebug(AUDIOCD_KIO_LOG) << "  Using derived name " << devname;
274
			cd->setDevice( devname );
275 276
		}
	}
277 278 279 280
#else
#ifdef __GNUC__
	#warning audiocd ioslave is not going to work for you
#endif
David Faure's avatar
David Faure committed
281
#endif
282
}
David Faure's avatar
David Faure committed
283

284

285 286 287 288 289 290
// Initiate a request to access the CD drive.  If there is no valid drive
// specified or there is a problem, then error() must be (or have been)
// called before returning a null pointer.
struct cdrom_drive *AudioCDProtocol::initRequest(const QUrl &url)
{
	// Load our Settings.
291
	loadSettings();
292
	// Then URL parameters can overrule our settings.
293 294
	parseURLArgs(url);

295 296 297 298
	struct cdrom_drive *drive = getDrive();
	if (drive==nullptr) {
		return nullptr;
	}
299 300 301 302 303 304 305 306 307 308

	if (d->tocsAreDifferent(drive))
	{
		// Update our knowledge of the disc
		KCompactDisc cd(KCompactDisc::Asynchronous);
		setDeviceToCd(&cd, drive);
		d->setToc(drive);
		d->tracks = cd.tracks();
		for(uint i=0; i< cd.tracks(); i++)
			d->trackIsAudio[i] = cd.isAudio(i+1);
309

Benjamin Meyer's avatar
Benjamin Meyer committed
310
		KCDDB::Client c;
311
		d->cddbResult = c.lookup(cd.discSignature());
312 313 314
		if (d->cddbResult == Success)
		{
			d->cddbList = c.lookupResponse();
315 316
			// FIXME: not always the best choice, see bug 279485
			// for a similar problem with Amarok
317 318
			d->cddbBestChoice = d->cddbList.first();
		}
Benjamin Meyer's avatar
Benjamin Meyer committed
319
		generateTemplateTitles();
Benjamin Meyer's avatar
Benjamin Meyer committed
320
	}
321

322
	// Determine what file or folder that is wanted.
323
	QString path = url.path();
324
	if (!path.isEmpty() && path[0] == QLatin1Char( '/' ))
325
		path = path.mid(1);
326

327
	d->req_allTracks = false;
328

329 330
	// See which file and directory they want
	QString remainingDirPath;
Benjamin Meyer's avatar
Benjamin Meyer committed
331
	d->which_dir = Unknown;
332 333 334 335 336 337 338 339
	if (path.isEmpty()) {
		d->which_dir = Root;
		d->encoder_dir_type = encoderTypeWAV;
		remainingDirPath = d->templateFileLocation;
		d->fname = QString();
	} else {
		for (int i = encoders.size()-1; i >= 0; --i) {
			AudioCDEncoder *encoder = encoders.at(i);
340
			QString encoderFileLocation = encoder->type();
341
		        if (!d->templateFileLocation.isEmpty())	encoderFileLocation = encoderFileLocation + QLatin1String( "/" ) + d->templateFileLocation;
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
			if (path == encoder->type()) {
				d->which_dir = EncoderDir;
				d->encoder_dir_type = encoder;
				remainingDirPath = encoderFileLocation.mid(path.length());
				d->fname = QString();
				break;
			} else if (encoderFileLocation.startsWith(path)) {
				d->which_dir = SubDir;
				d->encoder_dir_type = encoder;
				remainingDirPath = encoderFileLocation.mid(path.length());
				d->fname = QString();
				break;
			} else if (path.startsWith(encoderFileLocation)) {
				d->which_dir = SubDir;
				d->encoder_dir_type = encoder;
				remainingDirPath = QString();
				d->fname = path.mid(encoderFileLocation.length() + 1);
				break;
			} else if (path.startsWith(encoder->type())) {
				d->which_dir = EncoderDir;
				d->encoder_dir_type = encoder;
				remainingDirPath = QString();
				d->fname = path.mid(encoder->type().length() + 1);
			}
		}
		if ( Unknown == d->which_dir ) {
			if (path.startsWith(d->s_info)) {
				d->which_dir = Info;
				d->fname = path.mid(d->s_info.length() + 1);
			} else if (path.startsWith(d->s_fullCD)) {
				d->which_dir = FullCD;
				d->fname = path.mid(d->s_fullCD.length() + 1);
				d->req_allTracks = true;
			} else if (d->templateFileLocation.startsWith(path)) {
				d->which_dir = SubDir;
				d->encoder_dir_type = encoderTypeWAV;
				remainingDirPath = d->templateFileLocation.mid(path.length());
				d->fname = QString();
			} else if (path.startsWith(d->templateFileLocation)) {
				d->encoder_dir_type = encoderTypeWAV;
				remainingDirPath = QString();
				d->fname = path.mid(d->templateFileLocation.length() + 1);
			} else  {
				d->encoder_dir_type = encoderTypeWAV;
				remainingDirPath = QString();
				d->fname = path;
			}
Benjamin Meyer's avatar
Benjamin Meyer committed
389 390
		}
	}
391
	if (!remainingDirPath.isEmpty() && remainingDirPath[0] == QLatin1Char( '/' ))
392
		remainingDirPath = remainingDirPath.mid(1);
393
	d->child_dir = remainingDirPath.split(QLatin1String( "/" )).first();
394

Benjamin Meyer's avatar
Benjamin Meyer committed
395 396 397 398 399 400
	// See if the url is a track
	d->req_track = -1;
	if (!d->fname.isEmpty()){
		QString name(d->fname);

		// Remove extension
401
		int dot = name.lastIndexOf( QLatin1Char( '.' ));
Benjamin Meyer's avatar
Benjamin Meyer committed
402 403 404 405 406 407 408 409 410 411 412 413 414 415
		if (dot >= 0)
			name.truncate(dot);

		// See if it matches a cddb title
		uint trackNumber;
		for (trackNumber = 0; trackNumber < d->tracks; trackNumber++){
			if (d->templateTitles[trackNumber] == name)
				break;
		}
		if (trackNumber < d->tracks)
			d->req_track = trackNumber;
		else {
			/* Not found in title list. Try hard to find a number in the
				 string. */
Benjamin Meyer's avatar
Benjamin Meyer committed
416 417
			int start = 0;
			int end = 0;
Benjamin Meyer's avatar
Benjamin Meyer committed
418 419 420 421 422 423
			// Find where the numbers start
			while (start < name.length()){
				if (name[start++].isDigit())
					break;
			}
			// Find where the numbers end
Yuri Chornoivan's avatar
Yuri Chornoivan committed
424
			for (end = start; end < name.length(); ++end)
Benjamin Meyer's avatar
Benjamin Meyer committed
425 426 427 428
				if (!name[end].isDigit())
					break;
			if (start < name.length()){
				bool ok;
Yuri Chornoivan's avatar
Yuri Chornoivan committed
429
				// The external representation counts from 1 so subtract 1.
Benjamin Meyer's avatar
Benjamin Meyer committed
430 431 432 433 434 435 436 437 438
				d->req_track = name.mid(start-1, end - start+2).toInt(&ok) - 1;
				if (!ok)
					d->req_track = -1;
			}
		}
	}
	if (d->req_track >= (int)d->tracks)
		d->req_track = -1;

439
	qCDebug(AUDIOCD_KIO_LOG) << "path=" << path << " file=" << d->fname
440
                << " req_track=" << d->req_track << " which_dir=" << d->which_dir << " full CD?=" << d->req_allTracks;
Benjamin Meyer's avatar
Benjamin Meyer committed
441
	return drive;
Michael Matz's avatar
Michael Matz committed
442 443
}

444 445
bool AudioCDProtocol::getSectorsForRequest(struct cdrom_drive * drive, long & firstSector, long & lastSector) const
{
Benjamin Meyer's avatar
Benjamin Meyer committed
446 447 448 449 450 451 452 453 454 455 456 457
	if (d->req_allTracks)
	{ // we rip all the tracks of the CD
		firstSector = cdda_track_firstsector(drive, 1);
		lastSector  = cdda_track_lastsector(drive, cdda_tracks(drive));
	}
	else
	{ // we only rip the selected track
		int trackNumber = d->req_track + 1;

		if (trackNumber <= 0 || trackNumber > cdda_tracks(drive))
			return false;
		firstSector = cdda_track_firstsector(drive, trackNumber);
Benjamin Meyer's avatar
Benjamin Meyer committed
458
		lastSector = cdda_track_lastsector(drive, trackNumber);
Benjamin Meyer's avatar
Benjamin Meyer committed
459 460
	}
	return true;
461 462
}

463 464 465 466 467 468 469 470 471 472 473 474 475
static uint findInformationFileNumber(const QString &filename, uint max) {
	if (filename == QString::fromLatin1("%1.txt").arg(i18n(CDDB_INFORMATION)))
		return 1;

	for (uint i = 2; i <= max; ++i) {
		if (filename == QString::fromLatin1("%1_%2.txt").arg(i18n(CDDB_INFORMATION)).arg(i)) {
			return i;
		}
	}

	return max + 1;
}

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526
// See whether this is the root of the ioslave (listing of all
// available CD drives) or the root of a drive (listing of encoder
// subdirectories and track WAV files).
//
// This needs to be deduced from the URL without calling initRequest()
// or getDrive(), either of which may call error().  It is not an
// error if there is no drive to be accessed and none needs to be, but
// subsequently calling finished() will assert.
//
// So parse the URL only.  Either Base, Root or Unknown (meaning "anywhere
// else") will be returned.
static Which_dir whichFromUrl(const QUrl &url)
{
	QUrlQuery query(url);
	if (!query.hasQueryItem("device")) {		// see if "device" query present
		return Base;				// if not, must be slave base
	}

	if (url.path()=="/") {				// see if the device root
		return Root;
	}

	return Unknown;					// elsewhere within device
}

// Check that the URL does not have a host specified, and return an error
// if it does.  Moved here because not all operations need to or should
// call initRequest().
bool AudioCDProtocol::checkNoHost(const QUrl &url)
{
	if (!url.host().isEmpty())
	{
		error(KIO::ERR_UNSUPPORTED_ACTION,
		      i18n("You cannot specify a host with this protocol. "
			   "Please use the audiocd:/ format instead."));
		return false;
	}

	return true;
}

// Escape any '/'es in what should be a file name.
static QString escapePath(const QString &in)
{
	QString result = in;
	// FIXME: maybe could use QUrl::toPercentEncoding()
	result.replace(QLatin1Char('/'), QLatin1String("%2F"));
	return result;
}


Leslie Zhai's avatar
Leslie Zhai committed
527
void AudioCDProtocol::get(const QUrl & url)
Michael Matz's avatar
Michael Matz committed
528
{
529 530 531 532 533 534 535
	if (!checkNoHost(url)) {
		return;
	}

	struct cdrom_drive *drive = initRequest(url);
	if (drive==nullptr) {
		// Do not call error(), getDrive() will already have done that.
Benjamin Meyer's avatar
Benjamin Meyer committed
536
		return;
Pali Rohár's avatar
Pali Rohár committed
537
	}
Benjamin Meyer's avatar
Benjamin Meyer committed
538 539

	if( d->fname.contains(i18n(CDDB_INFORMATION))){
540
		const uint choice = findInformationFileNumber(d->fname, d->cddbList.count());
Benjamin Meyer's avatar
Benjamin Meyer committed
541 542 543
		uint count = 1;
		CDInfoList::iterator it;
		bool found = false;
544
		for ( it = d->cddbList.begin(); it != d->cddbList.end(); ++it ){
Benjamin Meyer's avatar
Benjamin Meyer committed
545
			if(count == choice){
546
				// FIXME: should be "text/plain"?
547
				mimeType(QLatin1String( "text/html" ));
548
				data( (*it).toString().toUtf8() );
Benjamin Meyer's avatar
Benjamin Meyer committed
549 550 551 552 553 554 555 556
				// send an empty QByteArray to signal end of data.
				data(QByteArray());
				finished();
				found = true;
				break;
			}
			count++;
		}
557
		if(!found && d->fname.contains(i18n(CDDB_INFORMATION)+QLatin1Char( ':' ))){
558
			// FIXME: should be "text/plain"?
559
			mimeType(QLatin1String( "text/html" ));
Benjamin Meyer's avatar
Benjamin Meyer committed
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
			//data(QCString( d->fname.latin1() ));
			// send an empty QByteArray to signal end of data.
			data(QByteArray());
			finished();
			found = true;
		}
		if( !found )
			error(KIO::ERR_DOES_NOT_EXIST, url.path());
		cdda_close(drive);
		return;
	}

	long firstSector, lastSector;
	if (!getSectorsForRequest(drive, firstSector, lastSector))
	{
		error(KIO::ERR_DOES_NOT_EXIST, url.path());
		cdda_close(drive);
		return;
	}
Rik Hemsley's avatar
Rik Hemsley committed
579

Benjamin Meyer's avatar
Benjamin Meyer committed
580 581
	AudioCDEncoder *encoder = determineEncoder(d->fname);
	if(!encoder){
Pali Rohár's avatar
Pali Rohár committed
582
		error(KIO::ERR_DOES_NOT_EXIST, url.path());
Benjamin Meyer's avatar
Benjamin Meyer committed
583 584
		cdda_close(drive);
		return;
Benjamin Meyer's avatar
Benjamin Meyer committed
585
	}
586

Benjamin Meyer's avatar
Benjamin Meyer committed
587
	KCDDB::CDInfo info;
588
	if(d->cddbResult == KCDDB::Success){
Benjamin Meyer's avatar
Benjamin Meyer committed
589
		info = d->cddbBestChoice;
590

Benjamin Meyer's avatar
Benjamin Meyer committed
591
		int track = d->req_track+1;
592

Benjamin Meyer's avatar
Benjamin Meyer committed
593
		// hack
Benjamin Meyer's avatar
Benjamin Meyer committed
594
		// do we rip the whole CD?
Benjamin Meyer's avatar
Benjamin Meyer committed
595 596
		if (d->req_allTracks){
			track = 1;
Benjamin Meyer's avatar
Benjamin Meyer committed
597
			// YES => the title of the file is the title of the CD
Richard Lärkäng's avatar
Richard Lärkäng committed
598
			info.track(track-1).set(Title, info.get(Title));
Benjamin Meyer's avatar
Benjamin Meyer committed
599
		}
600
		encoder->fillSongInfo(info, track, QString());
601
	}
Benjamin Meyer's avatar
Benjamin Meyer committed
602 603 604 605 606
	long totalByteCount = CD_FRAMESIZE_RAW * (lastSector - firstSector + 1);
	long time_secs = (8 * totalByteCount) / (44100 * 2 * 16);

	unsigned long size = encoder->size(time_secs);
	totalSize(size);
Benjamin Meyer's avatar
Benjamin Meyer committed
607
	emit mimeType(QLatin1String(encoder->mimeType()));
Benjamin Meyer's avatar
Benjamin Meyer committed
608 609 610 611 612 613 614 615 616 617

	// Read data (track/disk) from the cd
	paranoiaRead(drive, firstSector, lastSector, encoder, url.fileName(), size);

	// send an empty QByteArray to signal end of data.
	data(QByteArray());

	cdda_close(drive);

	finished();
Rik Hemsley's avatar
Rik Hemsley committed
618 619
}

620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649

static void app_dir(UDSEntry& e, const QString & n, size_t s)
{
	const mode_t _umask = ::umask(0);
	::umask(_umask);

	e.clear();
	e.INSERT( KIO::UDSEntry::UDS_NAME, QFile::decodeName(n.toLocal8Bit()));
	e.INSERT( KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
	e.INSERT( KIO::UDSEntry::UDS_ACCESS, (0555 & ~_umask));
	e.INSERT( KIO::UDSEntry::UDS_SIZE, s);
	e.INSERT( KIO::UDSEntry::UDS_MIME_TYPE, QLatin1String("inode/directory"));
}

static void app_file(UDSEntry& e, const QString & n, size_t s, const QString &mimetype = QString())
{
	const mode_t _umask = ::umask(0);
	::umask(_umask);

	e.clear();
	e.INSERT( KIO::UDSEntry::UDS_NAME, QFile::decodeName(n.toLocal8Bit()));
	e.INSERT( KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
	// Use current date and time to avoid confusions. See https://bugs.kde.org/show_bug.cgi?id=400114
	e.INSERT( KIO::UDSEntry::UDS_MODIFICATION_TIME, QDateTime::currentDateTime().toSecsSinceEpoch ());
	e.INSERT( KIO::UDSEntry::UDS_ACCESS, (0444 & ~_umask));
	e.INSERT( KIO::UDSEntry::UDS_SIZE, s);
	if (!mimetype.isEmpty()) e.INSERT( KIO::UDSEntry::UDS_MIME_TYPE, mimetype);
}


Leslie Zhai's avatar
Leslie Zhai committed
650
void AudioCDProtocol::stat(const QUrl & url)
Rik Hemsley's avatar
Rik Hemsley committed
651
{
652 653 654 655 656 657 658
	if (!checkNoHost(url)) {
		return;
	}

	if (whichFromUrl(url)==Base) {
		// This is the top level directory with CDROM devices only.
		// No drive access is required.  url.fileName() is empty here.
659 660

		UDSEntry entry;
661 662 663
		// One subdirectory for each drive
		const QStringList &deviceNames = KCompactDisc::cdromDeviceNames();
		app_dir(entry, escapePath("/"), deviceNames.count());
664 665 666 667 668
		statEntry(entry);
		finished();
		return;
	}

669 670 671
	struct cdrom_drive *drive = initRequest(url);
	if (drive==nullptr) {
		// Do not call error(), getDrive() will already have done that.
Benjamin Meyer's avatar
Benjamin Meyer committed
672
		return;
Pali Rohár's avatar
Pali Rohár committed
673
	}
Benjamin Meyer's avatar
Benjamin Meyer committed
674

675 676 677 678 679
	if (d->which_dir == Info) {
		// This is the info dir or one of the info files
		if (d->fname.isEmpty()) {
			// The info dir
			UDSEntry entry;
680
			app_dir(entry, escapePath(url.fileName()), d->cddbList.count());
681 682 683 684 685 686 687 688 689
			statEntry(entry);
			finished();
			return;
		} else if( d->fname.contains(i18n(CDDB_INFORMATION))) {
			// choice is 1-indexed so we need <= and -1 when accessing d->cddbList
			const uint choice = findInformationFileNumber(d->fname, d->cddbList.count());
			if (choice <= (uint)d->cddbList.count()) {
				// It's a valid info file
				UDSEntry entry;
690 691 692
				app_file(entry, escapePath(url.fileName()),
					 d->cddbList.at(choice - 1).toString().toLatin1().size(),
					 QStringLiteral("text/plain"));
693 694 695 696 697 698 699
				statEntry(entry);
				finished();
				return;
			}
		}
	}

700
	const bool isFile = !d->fname.isEmpty();
Benjamin Meyer's avatar
Benjamin Meyer committed
701 702 703

	// the track number. 0 if ripping
	// the whole CD.
704
	const uint trackNumber = d->req_track + 1;
Benjamin Meyer's avatar
Benjamin Meyer committed
705 706 707 708 709 710 711

	if (!d->req_allTracks)
	{ // we only want to rip one track.
		// does this track exist?
		if (isFile && (trackNumber < 1 || trackNumber > d->tracks))
		{
			error(KIO::ERR_DOES_NOT_EXIST, url.path());
Pali Rohár's avatar
Pali Rohár committed
712
			cdda_close(drive);
Benjamin Meyer's avatar
Benjamin Meyer committed
713 714 715 716 717 718 719
			return;
		}
	}

	UDSEntry entry;
	if (!isFile)
	{
720
		app_dir(entry, escapePath(url.fileName()), cdda_tracks(drive));
Benjamin Meyer's avatar
Benjamin Meyer committed
721 722 723
	}
	else
	{
724 725 726 727
		AudioCDEncoder *encoder = determineEncoder(d->fname);
		long firstSector = 0, lastSector = 0;
		getSectorsForRequest(drive, firstSector, lastSector);
		app_file(entry, escapePath(url.fileName()), fileSize(firstSector, lastSector, encoder));
Benjamin Meyer's avatar
Benjamin Meyer committed
728 729 730 731 732
	}

	statEntry(entry);
	cdda_close(drive);
	finished();
Rik Hemsley's avatar
Rik Hemsley committed
733 734
}

735
void AudioCDProtocol::listDir(const QUrl & url)
Rik Hemsley's avatar
Rik Hemsley committed
736
{
737 738 739
	if (!checkNoHost(url)) {
		return;
	}
Rik Hemsley's avatar
Rik Hemsley committed
740

741 742 743
	UDSEntry entry;
	app_dir(entry, ".", 0);
	listEntry(entry);
Rik Hemsley's avatar
Rik Hemsley committed
744

745 746
	if (whichFromUrl(url)==Base) {
		// The top level directory with CDROM devices only.
Benjamin Meyer's avatar
Benjamin Meyer committed
747

748 749 750
		const QStringList &deviceNames = KCompactDisc::cdromDeviceNames();
		foreach (const QString &deviceName, deviceNames) {
			const QString &device = KCompactDisc::urlToDevice(KCompactDisc::cdromDeviceUrl(deviceName));
Leslie Zhai's avatar
Leslie Zhai committed
751
			QUrl targetUrl = url;
752 753 754
			QUrlQuery targetQuery;
			targetQuery.addQueryItem("device", device.toUtf8());
			targetUrl.setQuery(targetQuery);
755 756

			app_dir(entry, escapePath(device), 2+encoders.count());
757 758
			entry.INSERT(KIO::UDSEntry::UDS_TARGET_URL, targetUrl.url());
			entry.INSERT(KIO::UDSEntry::UDS_DISPLAY_NAME, deviceName);
759
			listEntry(entry);
760 761 762 763 764 765
		}
		totalSize(entry.count());
		finished();
		return;
	}

766 767
	struct cdrom_drive *drive = initRequest(url);

Benjamin Meyer's avatar
Benjamin Meyer committed
768
	// Some error checking before proceeding
769 770
	if (drive==nullptr) {
		// Do not call error(), getDrive() will already have done that.
Benjamin Meyer's avatar
Benjamin Meyer committed
771
		return;
Pali Rohár's avatar
Pali Rohár committed
772
	}
Benjamin Meyer's avatar
Benjamin Meyer committed
773 774 775 776 777 778 779

	if (d->which_dir == Unknown){
		error(KIO::ERR_DOES_NOT_EXIST, url.path());
		cdda_close(drive);
		return;
	}

Benjamin Meyer's avatar
Benjamin Meyer committed
780
	if ( !d->fname.isEmpty() ){
Benjamin Meyer's avatar
Benjamin Meyer committed
781 782 783 784 785 786 787 788 789
		error(KIO::ERR_IS_FILE, url.path());
		cdda_close(drive);
		return;
	}

	// Generate templated names every time
	// because the template might have changed.
	generateTemplateTitles();

Benjamin Meyer's avatar
Benjamin Meyer committed
790
	if (d->which_dir == Info){
Benjamin Meyer's avatar
Benjamin Meyer committed
791 792
		CDInfoList::iterator it;
		uint count = 1;
793
		for ( it = d->cddbList.begin(); it != d->cddbList.end(); ++it ){
Benjamin Meyer's avatar
Benjamin Meyer committed
794 795
			(*it).toString();
			if(count == 1)
796
                            app_file(entry, QString::fromLatin1("%1.txt").arg(i18n(CDDB_INFORMATION)), ((*it).toString().length())+1);
Benjamin Meyer's avatar
Benjamin Meyer committed
797
			else
798
                            app_file(entry, QString::fromLatin1("%1_%2.txt").arg(i18n(CDDB_INFORMATION)).arg(count), ((*it).toString().length())+1);
Benjamin Meyer's avatar
Benjamin Meyer committed
799
			count++;
800
			listEntry(entry);
Benjamin Meyer's avatar
Benjamin Meyer committed
801
		}
Benjamin Meyer's avatar
Benjamin Meyer committed
802
		// Error
Benjamin Meyer's avatar
Benjamin Meyer committed
803
		if( count == 1 ) {
804
                    app_file(entry, QString::fromLatin1("%1: %2.txt").arg(i18n(CDDB_INFORMATION)).arg(KCDDB::resultToString(d->cddbResult)), 0);
Benjamin Meyer's avatar
Benjamin Meyer committed
805
			count++;
806
			listEntry(entry);
Benjamin Meyer's avatar
Benjamin Meyer committed
807 808 809 810
		}
	}

	if (d->which_dir == Root){
Benjamin Meyer's avatar
Benjamin Meyer committed
811 812
		// List virtual directories.
		app_dir(entry, d->s_fullCD, encoders.count());
813
		listEntry(entry);
Benjamin Meyer's avatar
Benjamin Meyer committed
814 815 816

		// Either >0 cddb results or cddb error file
		app_dir(entry, d->s_info, d->cddbList.count());
817
		listEntry(entry);
Benjamin Meyer's avatar
Benjamin Meyer committed
818

Benjamin Meyer's avatar
Benjamin Meyer committed
819
		// List the encoders
Benjamin Meyer's avatar
Benjamin Meyer committed
820
		AudioCDEncoder *encoder;
Benjamin Meyer's avatar
Benjamin Meyer committed
821 822
		for (int i = encoders.size()-1; i >= 0; --i) {
            		encoder = encoders.at(i);
Benjamin Meyer's avatar
Benjamin Meyer committed
823 824 825 826
			// Skip the directory that is in the root (you can still go in it, just don't show it)
			if( encoder == encoderTypeWAV )
				continue;
			app_dir(entry, encoder->type(), d->tracks);
827
			listEntry(entry);
Benjamin Meyer's avatar
Benjamin Meyer committed
828 829 830 831
		}
	}

	// Now fill in the tracks for the current directory
832 833 834 835 836 837 838 839
	if (d->which_dir == FullCD) {
		AudioCDEncoder *encoder;
		for (int i = encoders.size()-1; i >= 0; --i) {
			encoder = encoders.at(i);
			if (d->cddbResult != KCDDB::Success)
				addEntry(d->s_fullCD, encoder, drive, -1);
			else
				addEntry(d->templateAlbumName, encoder, drive, -1);
Benjamin Meyer's avatar
Benjamin Meyer committed
840 841 842
		}
	}

843
	if (d->which_dir == SubDir || d->which_dir == Root || d->which_dir == EncoderDir) {
844
		if (d->child_dir.isEmpty())
Benjamin Meyer's avatar
Benjamin Meyer committed
845
		{
846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
			// we are at the end of the hierarchy, list the tracks
			for (uint trackNumber = 1; trackNumber <= d->tracks; trackNumber++)
			{
				// Skip data tracks
				if (!d->trackIsAudio[trackNumber-1])
					continue;

				switch (d->which_dir) {
					case EncoderDir:
					case SubDir:
					case Root:
						addEntry(d->templateTitles[trackNumber - 1],
							d->encoder_dir_type, drive, trackNumber);
						break;
					case Info:
					case Unknown:
					default:
						error(KIO::ERR_INTERNAL, url.path());
						cdda_close(drive);
						return;
Benjamin Meyer's avatar
Benjamin Meyer committed
866 867 868
				}
			}
		}
869
		else
870 871
		{
			app_dir(entry, d->child_dir, 1);
872
			listEntry(entry);
873
		}
Benjamin Meyer's avatar
Benjamin Meyer committed
874 875 876 877 878
	}

	totalSize(entry.count());
	cdda_close(drive);
	finished();
Rik Hemsley's avatar
Rik Hemsley committed
879 880
}

Benjamin Meyer's avatar
Benjamin Meyer committed
881
void AudioCDProtocol::addEntry(const QString& trackTitle, AudioCDEncoder *encoder, struct cdrom_drive * drive, int trackNo)
882
{
Benjamin Meyer's avatar
Benjamin Meyer committed
883 884
	if(!encoder || !drive)
		return;
Benjamin Meyer's avatar
Benjamin Meyer committed
885 886 887 888 889 890 891 892 893 894 895

	long theFileSize = 0;
	if (trackNo == -1)
	{ // adding entry for the full CD
		theFileSize = fileSize(cdda_track_firstsector(drive, 1),
				 cdda_track_lastsector(drive, cdda_tracks(drive)),
				 encoder);
	}
	else
	{ // adding one regular track
		long firstSector = cdda_track_firstsector(drive, trackNo);
Benjamin Meyer's avatar
Benjamin Meyer committed
896
		long lastSector = cdda_track_lastsector(drive, trackNo);
Benjamin Meyer's avatar
Benjamin Meyer committed
897 898 899
		theFileSize = fileSize(firstSector, lastSector, encoder);
	}
	UDSEntry entry;
900
	app_file(entry, trackTitle + QLatin1String(".")+QLatin1String( encoder->fileType() ), theFileSize, QLatin1String( encoder->mimeType() ));
901
	listEntry(entry);
902 903
}

Benjamin Meyer's avatar
Benjamin Meyer committed
904
long AudioCDProtocol::fileSize(long firstSector, long lastSector, AudioCDEncoder *encoder)
905
{
Benjamin Meyer's avatar
Benjamin Meyer committed
906 907
	if(!encoder)
		return 0;
Benjamin Meyer's avatar
Benjamin Meyer committed
908

Benjamin Meyer's avatar
Benjamin Meyer committed
909 910 911 912
	long filesize = CD_FRAMESIZE_RAW * ( lastSector - firstSector + 1 );
	long length_seconds = (filesize) / 176400;

	return encoder->size(length_seconds);
913 914
}

915 916 917
// If a null pointer (meaning no or an invalid drive) is to be returned,
// this must call error() first and the caller must not subsequently
// call error() or finished().
Benjamin Meyer's avatar
Benjamin Meyer committed
918
struct cdrom_drive *AudioCDProtocol::getDrive()
Rik Hemsley's avatar
Rik Hemsley committed
919
{
920
	const QByteArray device(QFile::encodeName(d->device));
921
	if (device.isEmpty()) {
922
		error(KIO::ERR_MALFORMED_URL, i18nc("The URL does not include a device name", "Missing device specification"));
923 924
		return nullptr;
	}
Rik Hemsley's avatar
Rik Hemsley committed
925

926 927
	struct cdrom_drive *drive = cdda_identify(device, CDDA_MESSAGE_FORGETIT, 0);
	if (drive==nullptr) {
928
		qCDebug(AUDIOCD_KIO_LOG) << "Can't find an audio CD on: \"" << d->device << "\"";
929

930
		const QFileInfo fi(d->device);
931
		if(!fi.isReadable())
Thomas Häber's avatar
GUI:  
Thomas Häber committed
932
			error(KIO::ERR_SLAVE_DEFINED, i18n("Device does not have read permissions for this account.  Check the read permissions on the device."));
933
		else if(!fi.isWritable())
Thomas Häber's avatar
GUI:  
Thomas Häber committed
934
			error(KIO::ERR_SLAVE_DEFINED, i18n("Device does not have write permissions for this account.  Check the write permissions on the device."));
935
		else if(!fi.exists())
Benjamin Meyer's avatar
Benjamin Meyer committed
936
			error(KIO::ERR_DOES_NOT_EXIST, d->device);
937
		else error(KIO::ERR_SLAVE_DEFINED,
Andrew Coles's avatar
 
Andrew Coles committed
938
i18n("Unknown error.  If you have a cd in the drive try running cdparanoia -vsQ as yourself (not root). Do you see a track list? If not, make sure you have permission to access the CD device. If you are using SCSI emulation (possible if you have an IDE CD writer) then make sure you check that you have read and write permissions on the generic SCSI device, which is probably /dev/sg0, /dev/sg1, etc.. If it still does not work, try typing audiocd:/?device=/dev/sg0 (or similar) to tell kio_audiocd which device your CD-ROM is."));
939
		return nullptr;
Benjamin Meyer's avatar
Benjamin Meyer committed
940
	}
941