/*
 * Copyright (C) 2000 Rik Hemsley (rikkus) <rik@kde.org>
 * Copyright (C) 2000-2002 Michael Matz <matz@kde.org>
 * 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>
 * Copyright (C) 2004-2005 Benjamin Meyer <ben at meyerhome dot net>
 *
 * 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
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * 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
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */

#include "audiocd.h"
#include <config-audiocd.h>

extern "C" {
// cdda_interface.h in cdparanoia 10.2 has a member called 'private' which the
// C++ compiler doesn't like
// we will thus use a generated local copy which renames that member.
#include "cdda_interface.hpp"
#include <cdda_paranoia.h>
void paranoiaCallback(long, int);

int Q_DECL_EXPORT kdemain(int argc, char **argv);
}

#include "plugins/audiocdencoder.h"

#include <sys/resource.h>
#include <sys/stat.h>
#include <unistd.h>

#include <cstdlib>
#include <ctime>

#include <QApplication>
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QRegularExpression>
#include <QUrlQuery>

#include "audiocd_kio_debug.h"

#include <KLocalizedString>
#include <KMacroExpander>
#include <QRegularExpression>

// CDDB
#include <KCDDB/Client>
#include <KCompactDisc>

// Pseudo plugin class to embed metadata
class KIOPluginForMetaData : public QObject
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.kde.kio.worker.audiocd" FILE "audiocd.json")
};

using namespace KIO;

using namespace AudioCD;

extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
{
    // 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"));
    KLocalizedString::setApplicationDomain(QByteArrayLiteral("kio_audiocd"));

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

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

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

    qCDebug(AUDIOCD_KIO_LOG) << "Done";

    return 0;
}

enum Which_dir {
    Unknown = 0, // Error
    Info, // CDDB info
    Base, // The KIO worker base directory showing all drives
    Root, // The root directory, shows all these :)
    FullCD, // Show a single file containing all of the data
    EncoderDir, // The root directory created by an encoder
    SubDir // A directory created from the Album name configuration
};

class AudioCDProtocol::Private
{
public:
    Private()
        : s_info(i18n("Information"))
        , s_fullCD(i18n("Full CD"))
    {
        clearURLargs();
    }

    void clearURLargs()
    {
        req_allTracks = false;
        which_dir = Unknown;
        req_track = -1;
        cddbUserChoice = -1;
    }

    bool tocsAreDifferent(struct cdrom_drive *drive)
    {
        if (tracks != (uint)drive->tracks)
            return true;
        for (int i = 0; i < drive->tracks; ++i) {
            if (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)
                return true;
        }
        return false;
    }

    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;
        }
    }

    // The type/which of request
    bool req_allTracks;
    Which_dir which_dir;
    int req_track;
    QString fname;
    QString child_dir;
    AudioCDEncoder *encoder_dir_type;

    // Misc settings
    QString device; // URL settable
    int paranoiaLevel; // URL settable
    bool reportErrors;

    // Directory strings, never change after init
    const QString s_info;
    const QString s_fullCD;

    // Current CD
    TOC disc_toc[MAXTRK];
    unsigned tracks;
    bool trackIsAudio[100];

    // CDDB items
    KCDDB::Result cddbResult;
    CDInfoList cddbList;
    int cddbUserChoice; // URL settable
    KCDDB::CDInfo cddbBestChoice;

    // Template for ..
    QString fileNameTemplate; // URL settable
    QString albumNameTemplate; // URL settable
    QString fileLocationTemplate; // URL settable
    QString rsearch;
    QString rreplace;

    // Current strings for this CD and or cddb selection
    QStringList templateTitles;
    QString templateAlbumName;
    QString templateFileLocation;
};

int paranoia_read_limited_error;

AudioCDProtocol::AudioCDProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app)
    : WorkerBase(protocol, pool, app)
{
    d = new Private;
    // Add encoders
    AudioCDEncoder::findAllPlugins(this, encoders);
    if (encoders.isEmpty()) {
        qCCritical(AUDIOCD_KIO_LOG) << "No encoders available";
        return;
    }

    encoderTypeCDA = encoderFromExtension(QStringLiteral(".cda"));
    encoderTypeWAV = encoderFromExtension(QStringLiteral(".wav"));
}

AudioCDProtocol::~AudioCDProtocol()
{
    qDeleteAll(encoders);
    delete d;
}

AudioCDEncoder *AudioCDProtocol::encoderFromExtension(const QString &extension)
{
    AudioCDEncoder *encoder;
    for (int i = encoders.size() - 1; i >= 0; --i) {
        encoder = encoders.at(i);
        if (QLatin1String(".") + QLatin1String(encoder->fileType()) == extension)
            return encoder;
    }

    qCWarning(AUDIOCD_KIO_LOG) << "No encoder available for format" << extension;
    return nullptr;
}

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

static void setDeviceToCd(KCompactDisc *cd, struct cdrom_drive *drive)
{
#if defined(HAVE_CDDA_IOCTL_DEVICE)
    cd->setDevice(QLatin1String(drive->ioctl_device_name), 50, false);
#elif defined(__FreeBSD__) || defined(__DragonFly__)
    // FreeBSD's cdparanoia as of January 5th 2006 has rather broken
    // 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.
        //
        qCWarning(AUDIOCD_KIO_LOG) << "Found an ATAPI device, assuming it is the "
                                      "one specified by the user.";
        cd->setDevice(QString::fromLatin1(drive->cdda_device_name));
    } else {
        qCDebug(AUDIOCD_KIO_LOG) << "Found a SCSI or ATAPICAM device.";
        if (strlen(drive->dev->device_path) > 0) {
            cd->setDevice(QString::fromLatin1(drive->dev->device_path));
        } 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 = QStringLiteral("/dev/%1%2").arg(QString::fromLatin1(drive->dev->given_dev_name)).arg(drive->dev->given_unit_number);
            qCDebug(AUDIOCD_KIO_LOG) << "  Using derived name " << devname;
            cd->setDevice(devname);
        }
    }
#else
#ifdef __GNUC__
#warning audiocd KIO worker is not going to work for you
#endif
#endif
}

// 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.
KIO::WorkerResult AudioCDProtocol::initRequest(const QUrl &url, struct cdrom_drive **drive)
{
    // Load our Settings.
    loadSettings();
    // Then URL parameters can overrule our settings.
    parseURLArgs(url);

    const KIO::WorkerResult result = getDrive(drive);
    if (!result.success()) {
        return result;
    }

    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);

        KCDDB::Client c;
        d->cddbResult = c.lookup(cd.discSignature());
        if (d->cddbResult == Success) {
            d->cddbList = c.lookupResponse();
            // FIXME: not always the best choice, see bug 279485
            // for a similar problem with Amarok
            d->cddbBestChoice = d->cddbList.first();
        }
        generateTemplateTitles();
    }

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

    d->req_allTracks = false;

    // See which file and directory they want
    QString remainingDirPath;
    d->which_dir = Unknown;
    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);
            QString encoderFileLocation = encoder->type();
            if (!d->templateFileLocation.isEmpty())
                encoderFileLocation = encoderFileLocation + QLatin1String("/") + d->templateFileLocation;
            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;
            }
        }
    }
    if (!remainingDirPath.isEmpty() && remainingDirPath[0] == QLatin1Char('/'))
        remainingDirPath = remainingDirPath.mid(1);
    d->child_dir = remainingDirPath.split(QStringLiteral("/")).first();

    // See if the url is a track
    d->req_track = -1;
    if (!d->fname.isEmpty()) {
        QString name(d->fname);

        // Remove extension
        int dot = name.lastIndexOf(QLatin1Char('.'));
        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. */
            int start = 0;
            int end = 0;
            // Find where the numbers start
            while (start < name.length()) {
                if (name[start++].isDigit())
                    break;
            }
            // Find where the numbers end
            for (end = start; end < name.length(); ++end)
                if (!name[end].isDigit())
                    break;
            if (start < name.length()) {
                bool ok;
                // The external representation counts from 1 so subtract 1.
                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;

    qCDebug(AUDIOCD_KIO_LOG) << "path=" << path << " file=" << d->fname << " req_track=" << d->req_track << " which_dir=" << d->which_dir
                             << " full CD?=" << d->req_allTracks;
    return KIO::WorkerResult::pass();
}

bool AudioCDProtocol::getSectorsForRequest(struct cdrom_drive *drive, long &firstSector, long &lastSector) const
{
    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);
        lastSector = cdda_track_lastsector(drive, trackNumber);
    }
    return true;
}

static uint findInformationFileNumber(const QString &filename, uint max)
{
    if (filename == QStringLiteral("%1.txt").arg(i18n("CDDB Information")))
        return 1;

    for (uint i = 2; i <= max; ++i) {
        if (filename == QStringLiteral("%1_%2.txt").arg(i18n("CDDB Information")).arg(i)) {
            return i;
        }
    }

    return max + 1;
}

// See whether this is the root of the io worker (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 return KIO::WorkerResult::fail().  It is not an
// error if there is no drive to be accessed and none needs to be.
//
// 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(QStringLiteral("device"))) { // see if "device" query present
        return Base; // if not, must be KIO worker base
    }

    if (url.path() == QLatin1String("/")) { // 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().
KIO::WorkerResult AudioCDProtocol::checkNoHost(const QUrl &url)
{
    if (!url.host().isEmpty()) {
        return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION,
                                       i18n("You cannot specify a host with this protocol. "
                                            "Please use the audiocd:/ format instead."));
    }

    return KIO::WorkerResult::pass();
}

// 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;
}

KIO::WorkerResult AudioCDProtocol::get(const QUrl &url)
{
    const KIO::WorkerResult noHostResult = checkNoHost(url);
    if (!noHostResult.success()) {
        return noHostResult;
    }

    struct cdrom_drive *drive;
    const KIO::WorkerResult initResult = initRequest(url, &drive);
    if (!initResult.success()) {
        return initResult;
    }

    if (d->fname.contains(i18n("CDDB Information"))) {
        const uint choice = findInformationFileNumber(d->fname, d->cddbList.count());
        uint count = 1;
        CDInfoList::iterator it;
        bool found = false;
        for (it = d->cddbList.begin(); it != d->cddbList.end(); ++it) {
            if (count == choice) {
                // FIXME: should be "text/plain"?
                mimeType(QStringLiteral("text/html"));
                data((*it).toString().toUtf8());
                // send an empty QByteArray to signal end of data.
                data(QByteArray());
                found = true;
                break;
            }
            count++;
        }
        if (!found && d->fname.contains(i18n("CDDB Information") + QLatin1Char(':'))) {
            // FIXME: should be "text/plain"?
            mimeType(QStringLiteral("text/html"));
            // data(QCString( d->fname.latin1() ));
            // send an empty QByteArray to signal end of data.
            data(QByteArray());
            found = true;
        }
        cdda_close(drive);
        if (!found)
            return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path());
        return KIO::WorkerResult::pass();
    }

    long firstSector, lastSector;
    if (!getSectorsForRequest(drive, firstSector, lastSector)) {
        cdda_close(drive);
        return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path());
    }

    AudioCDEncoder *encoder = determineEncoder(d->fname);
    if (!encoder) {
        cdda_close(drive);
        return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path());
    }

    KCDDB::CDInfo info;
    if (d->cddbResult == KCDDB::Success) {
        info = d->cddbBestChoice;

        int track = d->req_track + 1;

        // hack
        // do we rip the whole CD?
        if (d->req_allTracks) {
            track = 1;
            // YES => the title of the file is the title of the CD
            info.track(track - 1).set(Title, info.get(Title));
        }
        encoder->fillSongInfo(info, track, QString());
    }
    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);
    mimeType(QLatin1String(encoder->mimeType()));

    // Read data (track/disk) from the cd
    const KIO::WorkerResult readResult = paranoiaRead(drive, firstSector, lastSector, encoder, url.fileName(), size);

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

    cdda_close(drive);

    return readResult;
}

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

    e.clear();
    e.reserve(5);
    e.fastInsert(KIO::UDSEntry::UDS_NAME, QFile::decodeName(n.toLocal8Bit()));
    e.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
    e.fastInsert(KIO::UDSEntry::UDS_ACCESS, (0555 & ~_umask));
    e.fastInsert(KIO::UDSEntry::UDS_SIZE, s);
    e.fastInsert(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.reserve(6);
    e.fastInsert(KIO::UDSEntry::UDS_NAME, QFile::decodeName(n.toLocal8Bit()));
    e.fastInsert(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.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, QDateTime::currentDateTime().toSecsSinceEpoch());
    e.fastInsert(KIO::UDSEntry::UDS_ACCESS, (0444 & ~_umask));
    e.fastInsert(KIO::UDSEntry::UDS_SIZE, s);
    if (!mimetype.isEmpty())
        e.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, mimetype);
}

KIO::WorkerResult AudioCDProtocol::stat(const QUrl &url)
{
    const KIO::WorkerResult result = checkNoHost(url);
    if (!result.success()) {
        return result;
    }

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

        UDSEntry entry;
        // One subdirectory for each drive
        const QStringList &deviceNames = KCompactDisc::cdromDeviceNames();
        app_dir(entry, escapePath(QStringLiteral("/")), deviceNames.count());
        statEntry(entry);
        return KIO::WorkerResult::pass();
    }

    struct cdrom_drive *drive;
    const KIO::WorkerResult initResult = initRequest(url, &drive);
    if (!initResult.success()) {
        return initResult;
    }

    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;
            app_dir(entry, escapePath(url.fileName()), d->cddbList.count());
            statEntry(entry);
            return KIO::WorkerResult::pass();
        }
        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;
                app_file(entry, escapePath(url.fileName()), d->cddbList.at(choice - 1).toString().toLatin1().size(), QStringLiteral("text/plain"));
                statEntry(entry);
                return KIO::WorkerResult::pass();
            }
        }
    }

    const bool isFile = !d->fname.isEmpty();

    // the track number. 0 if ripping
    // the whole CD.
    const uint trackNumber = d->req_track + 1;

    if (!d->req_allTracks) { // we only want to rip one track.
        // does this track exist?
        if (isFile && (trackNumber < 1 || trackNumber > d->tracks)) {
            cdda_close(drive);
            return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path());
        }
    }

    UDSEntry entry;
    if (!isFile) {
        app_dir(entry, escapePath(url.fileName()), cdda_tracks(drive));
    } else {
        AudioCDEncoder *encoder = determineEncoder(d->fname);
        long firstSector = 0, lastSector = 0;
        getSectorsForRequest(drive, firstSector, lastSector);
        app_file(entry, escapePath(url.fileName()), fileSize(firstSector, lastSector, encoder));
    }

    statEntry(entry);
    cdda_close(drive);
    return KIO::WorkerResult::pass();
}

KIO::WorkerResult AudioCDProtocol::listDir(const QUrl &url)
{
    const KIO::WorkerResult result = checkNoHost(url);
    if (!result.success()) {
        return result;
    }

    UDSEntry entry;
    app_dir(entry, QStringLiteral("."), 0);
    listEntry(entry);

    if (whichFromUrl(url) == Base) {
        // The top level directory with CDROM devices only.

        const QStringList deviceNames = KCompactDisc::cdromDeviceNames();
        for (const QString &deviceName : deviceNames) {
            const QString &device = KCompactDisc::urlToDevice(KCompactDisc::cdromDeviceUrl(deviceName));
            QUrl targetUrl = url;
            QUrlQuery targetQuery;
            targetQuery.addQueryItem(QStringLiteral("device"), device);
            targetUrl.setQuery(targetQuery);

            app_dir(entry, escapePath(device), 2 + encoders.count());
            entry.fastInsert(KIO::UDSEntry::UDS_TARGET_URL, targetUrl.url());
            entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, deviceName);
            listEntry(entry);
        }
        totalSize(entry.count());
        return KIO::WorkerResult::pass();
    }

    struct cdrom_drive *drive;
    const KIO::WorkerResult initResult = initRequest(url, &drive);
    if (!initResult.success()) {
        return initResult;
    }

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

    if (!d->fname.isEmpty()) {
        cdda_close(drive);
        return KIO::WorkerResult::fail(KIO::ERR_IS_FILE, url.path());
    }

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

    if (d->which_dir == Info) {
        CDInfoList::iterator it;
        uint count = 1;
        for (it = d->cddbList.begin(); it != d->cddbList.end(); ++it) {
            (*it).toString();
            if (count == 1)
                app_file(entry, QString::fromLatin1("%1.txt").arg(i18n("CDDB Information")), ((*it).toString().length()) + 1);
            else
                app_file(entry, QString::fromLatin1("%1_%2.txt").arg(i18n("CDDB Information")).arg(count), ((*it).toString().length()) + 1);
            count++;
            listEntry(entry);
        }
        // Error
        if (count == 1) {
            app_file(entry, QString::fromLatin1("%1: %2.txt").arg(i18n("CDDB Information")).arg(KCDDB::resultToString(d->cddbResult)), 0);
            count++;
            listEntry(entry);
        }
    }

    if (d->which_dir == Root) {
        // List virtual directories.
        app_dir(entry, d->s_fullCD, encoders.count());
        listEntry(entry);

        // Either >0 cddb results or cddb error file
        app_dir(entry, d->s_info, d->cddbList.count());
        listEntry(entry);

        // List the encoders
        AudioCDEncoder *encoder;
        for (int i = encoders.size() - 1; i >= 0; --i) {
            encoder = encoders.at(i);
            // 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);
            listEntry(entry);
        }
    }

    // Now fill in the tracks for the current directory
    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);
        }
    }

    if (d->which_dir == SubDir || d->which_dir == Root || d->which_dir == EncoderDir) {
        if (d->child_dir.isEmpty()) {
            // 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:
                    cdda_close(drive);
                    return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, url.path());
                }
            }
        } else {
            app_dir(entry, d->child_dir, 1);
            listEntry(entry);
        }
    }

    totalSize(entry.count());
    cdda_close(drive);
    return KIO::WorkerResult::pass();
}

void AudioCDProtocol::addEntry(const QString &trackTitle, AudioCDEncoder *encoder, struct cdrom_drive *drive, int trackNo)
{
    if (!encoder || !drive)
        return;

    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);
        long lastSector = cdda_track_lastsector(drive, trackNo);
        theFileSize = fileSize(firstSector, lastSector, encoder);
    }
    UDSEntry entry;
    app_file(entry, trackTitle + QLatin1String(".") + QLatin1String(encoder->fileType()), theFileSize, QLatin1String(encoder->mimeType()));
    listEntry(entry);
}

long AudioCDProtocol::fileSize(long firstSector, long lastSector, AudioCDEncoder *encoder)
{
    if (!encoder)
        return 0;

    long filesize = CD_FRAMESIZE_RAW * (lastSector - firstSector + 1);
    long length_seconds = (filesize) / 176400;

    return encoder->size(length_seconds);
}

KIO::WorkerResult AudioCDProtocol::getDrive(struct cdrom_drive **drive)
{
    const QByteArray device(QFile::encodeName(d->device));
    if (device.isEmpty()) {
        *drive = nullptr;
        return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, i18nc("The URL does not include a device name", "Missing device specification"));
    }

    *drive = cdda_identify(device.data(), CDDA_MESSAGE_FORGETIT, nullptr);
    if (*drive == nullptr) {
        qCDebug(AUDIOCD_KIO_LOG) << "Can't find an audio CD on: \"" << d->device << "\"";

        const QFileInfo fi(d->device);
        if (!fi.isReadable())
            return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED,
                                           i18n("Device does not have read permissions for this account.  "
                                                "Check the read permissions on the device."));
        if (!fi.isWritable())
            return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED,
                                           i18n("Device does not have write permissions for this account.  "
                                                "Check the write permissions on the device."));
        if (!fi.exists())
            return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, d->device);

        return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED,
                                       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."));
    }

    if (cdda_open(*drive) != 0) {
        qCDebug(AUDIOCD_KIO_LOG) << "cdda_open failed";
        cdda_close(*drive);
        *drive = nullptr;
        return KIO::WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, d->device);
    }

    return KIO::WorkerResult::pass();
}

KIO::WorkerResult AudioCDProtocol::paranoiaRead(struct cdrom_drive *drive,
                                                long firstSector,
                                                long lastSector,
                                                AudioCDEncoder *encoder,
                                                const QString &fileName,
                                                unsigned long size)
{
    if (!encoder || !drive)
        return KIO::WorkerResult::fail();

    cdrom_paranoia *paranoia = paranoia_init(drive);
    if (nullptr == paranoia) {
        qCDebug(AUDIOCD_KIO_LOG) << "paranoia_init failed";
        return KIO::WorkerResult::fail();
    }

    int paranoiaLevel = PARANOIA_MODE_FULL ^ PARANOIA_MODE_NEVERSKIP;
    switch (d->paranoiaLevel) {
    case 0:
        paranoiaLevel = PARANOIA_MODE_DISABLE;
        break;

    case 1:
        paranoiaLevel |= PARANOIA_MODE_OVERLAP;
        paranoiaLevel &= ~PARANOIA_MODE_VERIFY;
        break;

    case 2:
        paranoiaLevel |= PARANOIA_MODE_NEVERSKIP;
    default:
        break;
    }

    paranoia_modeset(paranoia, paranoiaLevel);

    cdda_verbose_set(drive, CDDA_MESSAGE_PRINTIT, CDDA_MESSAGE_FORGETIT);

    paranoia_seek(paranoia, firstSector, SEEK_SET);

    long currentSector(firstSector);

    unsigned long processed = encoder->readInit(CD_FRAMESIZE_RAW * (lastSector - firstSector + 1));
    // TODO test for errors (processed<0)?
    processedSize(processed);
    bool ok = true;
    QString errorString;

    unsigned long lastSize = size;
    unsigned long diff = 0;

    paranoia_read_limited_error = 0;
    int warned = 0;
    while (currentSector <= lastSector) {
        // TODO make the 5 configurable? The default in the lib is 20 fyi
        qint16 *buf = paranoia_read_limited(paranoia, paranoiaCallback, 5);
        if (warned == 0 && paranoia_read_limited_error >= 5 && d->reportErrors) {
            warning(
                i18n("AudioCD: Disk damage detected on this track, risk of data "
                     "corruption."));
            warned = 1;
        }
        if (nullptr == buf) {
            qCDebug(AUDIOCD_KIO_LOG) << "Unrecoverable error in paranoia_read";
            ok = false;
            errorString = i18n("Error reading audio data for %1 from the CD", fileName);
            break;
        }

        ++currentSector;

        int encoderProcessed = encoder->read(buf, CD_FRAMESAMPLES);
        if (encoderProcessed == -1) {
            qCDebug(AUDIOCD_KIO_LOG) << "Encoder processing error, stopping.";
            ok = false;
            errorString = i18n("Could not read %1: encoding failed", fileName);
            const QString details = encoder->lastErrorMessage();
            if (!details.isEmpty())
                errorString += QLatin1Char('\n') + details;
            break;
        }
        processed += encoderProcessed;

        /**
         * Because compression size is so 'unknown' use some guesswork
         *
         * 1) First assume that the reported size is correct and
         * only change the totalSize if the guess it outside a range of %5.
         * 2) Only increase in size unless the decrease is %5 of last estimate.
         * This prevents continues small changes which is just annoying.
         */
        unsigned long end = lastSector - firstSector;
        unsigned long cur = currentSector - firstSector;
        unsigned long estSize = (processed / cur) * end;

        // If our guess is within 5% of reported
        // size then use the reported size.
        unsigned long guess = (long)((100 / (float)size) * estSize);
        if ((guess > 97 && guess < 103) || estSize == 0) {
            if (processed > lastSize) {
                totalSize(processed + 1);
                lastSize = processed;
            }
        } else {
            float percentDone = ((float)cur / (float)end);
            // Calculate estimated amount that will be wrong
            diff = estSize - lastSize;
            diff = (diff * (unsigned long)((100 / (float)end) * (end - cur))) / 2;
            // Need 1% of data calculated as initial buffer, use %2 to be safe
            if (percentDone < .02) {
                // qCDebug(AUDIOCD_KIO_LOG) << "val: " << (float)cur/(float)end << "
                // diff: " << diff;
                diff = 0;
            }

            // We are growing larger, increase total.
            if (lastSize < estSize) {
                // qCDebug(AUDIOCD_KIO_LOG) << "lastGuess: " << lastSize << ", guess: "
                // << estSize << ", diff: " << diff;
                totalSize(estSize + diff);
                lastSize = estSize + diff;
            } else {
                int margin = (int)((percentDone) * 75);
                // Don't bother really trying until almost half way done.
                if (percentDone <= .40)
                    margin = 7;
                unsigned long low = lastSize - lastSize / margin;
                if (estSize < low) {
                    // qCDebug(AUDIOCD_KIO_LOG) << "low: " << low << ", estSize: " <<
                    // estSize << ", num: " << margin;
                    totalSize(estSize);
                    lastSize = estSize;
                }
            }
        }
        /**
         * End estimation.
         */
        // qCDebug(AUDIOCD_KIO_LOG) << "processed: " << processed << ", totalSize: "
        // << estSize;
        processedSize(processed);
    }

    if (processed > size)
        totalSize(processed);

    long encoderProcessed = encoder->readCleanup();
    if (encoderProcessed >= 0) {
        processed += encoderProcessed;
        if (processed > size)
            totalSize(processed);
        processedSize(processed);
    } else if (ok) // i.e. no error message already emitted
        errorString = i18n("Could not read %1: encoding failed", fileName);

    paranoia_free(paranoia);
    paranoia = nullptr;

    if (!errorString.isEmpty()) {
        return KIO::WorkerResult::fail(ERR_WORKER_DEFINED, errorString);
    }

    return KIO::WorkerResult::pass();
}

/**
 * Read the settings from the URL
 * @see loadSettings()
 */
void AudioCDProtocol::parseURLArgs(const QUrl &url)
{
    d->clearURLargs();

    // FIXME: Use QUrlQuery for parsing
    QString query(QUrl::fromPercentEncoding(url.query().toLatin1()));

    if (query.isEmpty())
        return;

    const QStringList tokens(query.split(QLatin1Char('&'), Qt::SkipEmptyParts));

    for (QStringList::ConstIterator it(tokens.constBegin()); it != tokens.constEnd(); ++it) {
        const QString token(*it);

        int equalsPos = token.indexOf(QLatin1Char('='));
        if (-1 == equalsPos)
            continue;

        const QString attribute(token.left(equalsPos));
        const QString value(token.mid(equalsPos + 1));

        if (attribute == QLatin1String("device"))
            d->device = value;
        else if (attribute == QLatin1String("paranoia_level"))
            d->paranoiaLevel = value.toInt();
        else if (attribute == QLatin1String("fileNameTemplate"))
            d->fileNameTemplate = value;
        else if (attribute == QLatin1String("albumNameTemplate"))
            d->albumNameTemplate = value;
        else if (attribute == QLatin1String("fileLocationTemplate"))
            d->fileLocationTemplate = value;
        else if (attribute == QLatin1String("cddbChoice"))
            d->cddbUserChoice = value.toInt();
        else if (attribute == QLatin1String("niceLevel")) {
            int niceLevel = value.toInt();
            if (setpriority(PRIO_PROCESS, getpid(), niceLevel) != 0)
                qCDebug(AUDIOCD_KIO_LOG) << "Setting nice level to (" << niceLevel << ") failed.";
        }
    }
}

/**
 * Read the settings set by the kcm modules
 * @see parseURLArgs()
 */
void AudioCDProtocol::loadSettings()
{
    const KConfig *config = new KConfig(QStringLiteral("kcmaudiocdrc"), KConfig::NoGlobals);
    const KConfigGroup groupCDDA(config, QStringLiteral("CDDA"));

    d->device = QString(); // clear device
    d->paranoiaLevel = 1; // enable paranoia error correction, but allow skipping

    if (groupCDDA.readEntry("disable_paranoia", false)) {
        d->paranoiaLevel = 0; // disable all paranoia error correction
    }

    if (groupCDDA.readEntry("never_skip", true)) {
        d->paranoiaLevel = 2;
        // never skip on errors of the medium, should be default for high quality
    }

    d->reportErrors = groupCDDA.readEntry("report_errors", false);

    if (groupCDDA.hasKey("niceLevel")) {
        int niceLevel = groupCDDA.readEntry("niceLevel", 0);
        if (setpriority(PRIO_PROCESS, getpid(), niceLevel) != 0)
            qCDebug(AUDIOCD_KIO_LOG) << "Setting nice level to (" << niceLevel << ") failed.";
    }

    // The default track filename template
    const KConfigGroup groupFileName(config, QStringLiteral("FileName"));
    d->fileNameTemplate = groupFileName.readEntry("file_name_template", "%{trackartist} - %{number} - %{title}");
    d->albumNameTemplate = groupFileName.readEntry("album_name_template", "%{albumartist} - %{albumtitle}");
    if (groupFileName.readEntry("show_file_location", false))
        d->fileLocationTemplate = groupFileName.readEntry("file_location_template", QString());
    else
        d->fileLocationTemplate = QString();
    d->rsearch = groupFileName.readEntry("regexp_search");
    d->rreplace = groupFileName.readEntry("regexp_replace");
    // if the regular expressions are enclosed in quotes. Remove them
    // otherwise it is not possible to search for a space " ", since an empty
    // (only spaces) value is not supported by KConfig, so the space has to be
    // quoted, but then here the regexp searches really for " " instead of just
    // the space. Alex
    const QRegularExpression qoutedString(QLatin1String("^\".*\"$"));
    if (const auto match = qoutedString.match(d->rsearch); match.hasMatch()) {
        d->rsearch = d->rsearch.mid(1, d->rsearch.length() - 2);
    }
    if (const auto match = qoutedString.match(d->rreplace); match.hasMatch()) {
        d->rreplace = d->rreplace.mid(1, d->rreplace.length() - 2);
    }

    // Tell the encoders to load their settings
    AudioCDEncoder *encoder;
    for (int i = encoders.size() - 1; i >= 0; --i) {
        encoder = encoders.at(i);
        if (encoder->init())
            encoder->loadSettings();
        else
            encoders.removeAt(i);
    }

    delete config;
}

/**
 * Generates the track titles from the template using the cddb information.
 */
void AudioCDProtocol::generateTemplateTitles()
{
    d->templateTitles.clear();
    if (d->cddbResult != KCDDB::Success) {
        for (unsigned int i = 0; i < d->tracks; i++) {
            d->templateTitles.append(i18n("Track %1", QString::asprintf("%02d", i + 1)));
        }
        return;
    }

    KCDDB::CDInfo info = d->cddbBestChoice;
    if (d->cddbUserChoice >= 0 && ((d->cddbUserChoice) < d->cddbList.count()))
        info = d->cddbList[d->cddbUserChoice];

    // Then generate the templates
    d->templateTitles.clear();
    for (uint i = 0; i < d->tracks; i++) {
        QHash<QString, QString> macros;
        macros[QStringLiteral("albumartist")] = info.get(Artist).toString();
        macros[QStringLiteral("albumtitle")] = info.get(Title).toString();
        macros[QStringLiteral("title")] = info.track(i).get(Title).toString();
        macros[QStringLiteral("trackartist")] = info.track(i).get(Artist).toString();
        macros[QStringLiteral("number")] = QString::asprintf("%02d", i + 1);
        // macros["number"] = QString("%1").arg(i+1, 2, 10);
        macros[QStringLiteral("genre")] = info.get(Genre).toString();
        macros[QStringLiteral("year")] = info.get(Year).toString();

        QString title = escapePath(KMacroExpander::expandMacros(d->fileNameTemplate, macros, QLatin1Char('%')));
        title.replace(QRegularExpression(d->rsearch), d->rreplace);
        d->templateTitles.append(title);
    }

    QHash<QString, QString> macros;
    macros[QStringLiteral("albumartist")] = info.get(Artist).toString();
    macros[QStringLiteral("albumtitle")] = info.get(Title).toString();
    macros[QStringLiteral("genre")] = info.get(Genre).toString();
    macros[QStringLiteral("year")] = info.get(Year).toString();
    d->templateAlbumName = escapePath(KMacroExpander::expandMacros(d->albumNameTemplate, macros, QLatin1Char('%')));
    d->templateAlbumName.replace(QRegularExpression(d->rsearch), d->rreplace);

    d->templateFileLocation = KMacroExpander::expandMacros(d->fileLocationTemplate, macros, QLatin1Char('%'));
}

/**
 * Based upon the cdparanoia ripping application
 * Only output BAD stuff
 * The higher the paranoia_read_limited_error the worse the problem is
 * FYI: PARANOIA_CB_READ & PARANOIA_CB_VERIFY happen continuously when ripping
 */
void paranoiaCallback(long, int function)
{
    switch (function) {
    case PARANOIA_CB_VERIFY:
        // qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_VERIFY";
        break;

    case PARANOIA_CB_READ:
        // qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_READ";
        break;

    case PARANOIA_CB_FIXUP_EDGE:
        // qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_FIXUP_EDGE";
        paranoia_read_limited_error = 2;
        break;

    case PARANOIA_CB_FIXUP_ATOM:
        // qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_FIXUP_ATOM";
        paranoia_read_limited_error = 6;
        break;

    case PARANOIA_CB_READERR:
        qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_READERR";
        paranoia_read_limited_error = 6;
        break;

    case PARANOIA_CB_SKIP:
        qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_SKIP";
        paranoia_read_limited_error = 8;
        break;

    case PARANOIA_CB_OVERLAP:
        // qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_OVERLAP";
        break;

    case PARANOIA_CB_SCRATCH:
        qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_SCRATCH";
        paranoia_read_limited_error = 7;
        break;

    case PARANOIA_CB_DRIFT:
        // qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_DRIFT";
        paranoia_read_limited_error = 4;
        break;

    case PARANOIA_CB_FIXUP_DROPPED:
        qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_FIXUP_DROPPED";
        paranoia_read_limited_error = 5;
        break;

    case PARANOIA_CB_FIXUP_DUPED:
        qCDebug(AUDIOCD_KIO_LOG) << "PARANOIA_CB_FIXUP_DUPED";
        paranoia_read_limited_error = 5;
        break;
    }
}

// needed for JSON file embedding
#include "audiocd.moc"
