/*
 * This file is part of cyanrip.
 *
 * cyanrip is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * cyanrip 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with cyanrip; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <time.h>
#include <sys/stat.h>

#ifdef _WIN32
#include <locale.h>
#endif

#include <libavutil/bprint.h>
#include <libavutil/avstring.h>
#include <libavutil/time.h>

#include "cyanrip_main.h"
#include "cyanrip_log.h"
#include "fun512.h"
#include "cue_writer.h"
#include "checksums.h"
#include "discid.h"
#include "musicbrainz.h"
#include "qrcode.h"
#include "coverart.h"
#include "accurip.h"
#include "os_compat.h"
#include "cyanrip_encode.h"

static char cyanrip_helpstr[128];

#define GEN_OPT_MAX_ARR 198
#define GEN_OPT_HELPSTRING cyanrip_helpstr
#include "genopt.h"

static int genopt_nb_vals(GenOpt *opts_list, int n, const char *name)
{
    for (int i = 0; i < n; i++)
        if (opts_list[i].name && !strcmp(opts_list[i].name, name))
            return opts_list[i].present ? opts_list[i].nb_vals : 0;
    return 0;
}

int quit_now = 0;

const cyanrip_out_fmt crip_fmt_info[] = {
    [CYANRIP_FORMAT_FLAC]     = { "flac",     "FLAC", "flac",  "flac",  1, 11, 1, AV_CODEC_ID_FLAC,      },
    [CYANRIP_FORMAT_MP3]      = { "mp3",      "MP3",  "mp3",   "mp3",   1,  0, 0, AV_CODEC_ID_MP3,       },
    [CYANRIP_FORMAT_TTA]      = { "tta",      "TTA",  "tta",   "tta",   0,  0, 1, AV_CODEC_ID_TTA,       },
    [CYANRIP_FORMAT_OPUS]     = { "opus",     "OPUS", "opus",  "ogg",   0, 10, 0, AV_CODEC_ID_OPUS,      },
    [CYANRIP_FORMAT_AAC]      = { "aac",      "AAC",  "m4a",   "adts",  0,  0, 0, AV_CODEC_ID_AAC,       },
    [CYANRIP_FORMAT_AAC_MP4]  = { "aac_mp4",  "AAC",  "mp4",   "mp4",   1,  0, 0, AV_CODEC_ID_AAC,       },
    [CYANRIP_FORMAT_WAVPACK]  = { "wavpack",  "WV",   "wv",    "wv",    0,  3, 1, AV_CODEC_ID_WAVPACK,   },
    [CYANRIP_FORMAT_VORBIS]   = { "vorbis",   "OGG",  "ogg",   "ogg",   0,  0, 0, AV_CODEC_ID_VORBIS,    },
    [CYANRIP_FORMAT_ALAC]     = { "alac",     "ALAC", "m4a",   "ipod",  0,  2, 1, AV_CODEC_ID_ALAC,      },
    [CYANRIP_FORMAT_ALAC_MP4] = { "alac_mp4", "ALAC", "mp4",   "mp4",   1,  2, 1, AV_CODEC_ID_ALAC,      },
    [CYANRIP_FORMAT_WAV]      = { "wav",      "WAV",  "wav",   "wav",   0,  0, 1, AV_CODEC_ID_NONE,      },
    [CYANRIP_FORMAT_OPUS_MP4] = { "opus_mp4", "OPUS", "mp4",   "mp4",   1, 10, 0, AV_CODEC_ID_OPUS,      },
    [CYANRIP_FORMAT_PCM]      = { "pcm",      "PCM",  "pcm",   "s16le", 0,  0, 1, AV_CODEC_ID_NONE,      },
};

static int get_media_changed(CdIo_t *cdio) {
    const int ret = cdio_get_media_changed(cdio);
    return ret != 0 && ret != DRIVER_OP_UNSUPPORTED;
}

static void free_track(cyanrip_ctx *ctx, cyanrip_track *t)
{
    if (quit_now)
        cyanrip_immediate_stop_encoding(ctx, t);

    for (int i = 0; i < ctx->settings.outputs_num; i++)
        cyanrip_end_track_encoding(&t->enc_ctx[i]);

    cyanrip_free_dec_ctx(ctx, &t->dec_ctx);

    crip_free_art(&t->art);
    av_dict_free(&t->meta);
    av_free(t->ar_db_entries);
}

static void cyanrip_ctx_end(cyanrip_ctx **s)
{
    cyanrip_ctx *ctx;
    if (!s || !*s)
        return;
    ctx = *s;

    for (int i = 0; i < ctx->nb_tracks; i++)
        free_track(ctx, &ctx->tracks[i]);

    for (int i = 0; i < ctx->nb_cover_arts; i++)
        crip_free_art(&ctx->cover_arts[i]);

    cyanrip_finalize_ebur128(ctx, 0);
    av_free(ctx->mb_submission_url);

    if (ctx->paranoia)
        cdio_paranoia_free(ctx->paranoia);
    if (ctx->drive)
        cdio_cddap_close_no_free_cdio(ctx->drive);
    if (ctx->settings.eject_on_success_rip && !ctx->total_error_count &&
        (ctx->mcap & CDIO_DRIVE_CAP_MISC_EJECT) && ctx->cdio && !quit_now)
        cdio_eject_media(&ctx->cdio);
    else if (ctx->cdio)
        cdio_destroy(ctx->cdio);

    free(ctx->settings.dev_path);
    av_dict_free(&ctx->meta);
    av_freep(&ctx);

    *s = NULL;
}

static const paranoia_mode_t paranoia_level_map[] = {
    [0] = PARANOIA_MODE_DISABLE, /* Disable everything */
    [1] = PARANOIA_MODE_OVERLAP, /* Perform overlapped reads */
    [2] = PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY, /* Perform and verify overlapped reads */
    [3] = PARANOIA_MODE_FULL ^ PARANOIA_MODE_NEVERSKIP, /* Maximum, but do allow skipping sectors */
};
const int crip_max_paranoia_level = (sizeof(paranoia_level_map) / sizeof(paranoia_level_map[0])) - 1;

/*
* Whether the string ends with suffix
*/
static int cyanrip_ends_with(const char *str, const char *suffix)
{
    size_t lenstr = strlen(str);
    size_t lensuffix = strlen(suffix);

    if (lensuffix > lenstr)
        return 0;

    return !strncmp(str + lenstr - lensuffix, suffix, lensuffix);
}

/*
* Open device
 */
static CdIo_t *cyanrip_open_dev(const char *dev_path)
{
    if (cyanrip_ends_with(dev_path, ".bin"))
        return cdio_open_bincue(dev_path);
    else if (cyanrip_ends_with(dev_path, ".cue"))
        return cdio_open_cue(dev_path);
    else if (cyanrip_ends_with(dev_path, ".nrg"))
        return cdio_open_nrg(dev_path);
    else if (cyanrip_ends_with(dev_path, ".toc"))
        return cdio_open_cdrdao(dev_path);

    return cdio_open(dev_path, DRIVER_UNKNOWN);
}

static int cyanrip_ctx_init(cyanrip_ctx **s, cyanrip_settings *settings)
{
    cyanrip_ctx *ctx = av_mallocz(sizeof(cyanrip_ctx));

    memcpy(&ctx->settings, settings, sizeof(cyanrip_settings));

    if (ctx->settings.print_info_only)
        ctx->settings.eject_on_success_rip = 0;

    cdio_init();

    if (!ctx->settings.dev_path) {
        ctx->settings.dev_path = cdio_get_default_device(NULL);
        if (!ctx->settings.dev_path) {
            cyanrip_log(ctx, 0, "No device specified and unable to get default device!\n");
            cyanrip_ctx_end(&ctx);
            return AVERROR(EINVAL);
        }
    }

    ctx->cdio = cyanrip_open_dev(ctx->settings.dev_path);
    if (!ctx->cdio) {
        cyanrip_log(ctx, 0, "Unable to open device: %s\n", ctx->settings.dev_path);
        cyanrip_ctx_end(&ctx);
        return AVERROR(EINVAL);
    }

    cdio_get_drive_cap(ctx->cdio, &ctx->rcap, &ctx->wcap, &ctx->mcap);

    char *msg = NULL;
    if (!(ctx->drive = cdio_cddap_identify_cdio(ctx->cdio, CDDA_MESSAGE_LOGIT, &msg))) {
        cyanrip_log(ctx, 0, "Unable to init cddap context!\n");
        if (msg) {
            cyanrip_log(ctx, 0, "cdio: \"%s\"\n", msg);
            cdio_cddap_free_messages(msg);
        }
        return AVERROR(EINVAL);
    }

    if (msg) {
        cyanrip_log(ctx, 0, "%s\n", msg);
        cdio_cddap_free_messages(msg);
    }

    cyanrip_log(ctx, 0, "Opening drive...\n");
    int ret = cdio_cddap_open(ctx->drive);
    if (ret < 0) {
        cyanrip_log(ctx, 0, "Unable to open device!\n");
        cyanrip_ctx_end(&ctx);
        return AVERROR(EINVAL);
    }

    cdio_cddap_verbose_set(ctx->drive, CDDA_MESSAGE_LOGIT, CDDA_MESSAGE_FORGETIT);

    if (settings->speed) {
        if (!(ctx->mcap & CDIO_DRIVE_CAP_MISC_SELECT_SPEED)) {
            cyanrip_log(ctx, 0, "Device does not support changing speeds!\n");
            cyanrip_ctx_end(&ctx);
            return AVERROR(EINVAL);
        }

        ret = cdio_cddap_speed_set(ctx->drive, settings->speed);
        msg = cdio_cddap_errors(ctx->drive);
        if (msg) {
            cyanrip_log(ctx, 0, "cdio error: %s\n", msg);
            cdio_cddap_free_messages(msg);
        }
        if (ret)
            return AVERROR(EINVAL);
    }

    ctx->paranoia = cdio_paranoia_init(ctx->drive);
    if (!ctx->paranoia) {
        cyanrip_log(ctx, 0, "Unable to init paranoia!\n");
        cyanrip_ctx_end(&ctx);
        return AVERROR(EINVAL);
    }

    cdio_paranoia_modeset(ctx->paranoia, paranoia_level_map[settings->paranoia_level]);

    /* Disc images have no hardware cache to defeat; paranoia's cache probe
     * reads cachemodel sectors past the seek target on backseeks, which
     * beyond the leadout gets counted as a read error. 1, not 0, as the
     * cachemodel size is also the c_block read chunk size, and 0 never
     * makes progress */
    switch (cdio_get_driver_id(ctx->cdio)) {
    case DRIVER_BINCUE:
    case DRIVER_NRG:
    case DRIVER_CDRDAO:
        cdio_paranoia_cachemodel_size(ctx->paranoia, 1);
        break;
    default:
        break;
    }

    ctx->start_lsn = 0;

    ctx->end_lsn = cdio_get_track_lsn(ctx->cdio, CDIO_CDROM_LEADOUT_TRACK) - 1;
    ctx->duration_frames = ctx->end_lsn - ctx->start_lsn + 1;

    ctx->nb_tracks = ctx->nb_cd_tracks = cdio_cddap_tracks(ctx->drive);
    if ((ctx->nb_tracks < 1) || (ctx->nb_tracks > CDIO_CD_MAX_TRACKS)) {
        cyanrip_log(ctx, 0, "Invalid number of tracks: %i!\n", ctx->nb_tracks);
        ctx->nb_tracks = 0;
        cyanrip_ctx_end(&ctx);
        return AVERROR(EINVAL);
    }

    int first_track_nb = cdio_get_first_track_num(ctx->cdio);
    for (int i = 0; i < ctx->nb_cd_tracks; i++) {
        cyanrip_track *t = &ctx->tracks[i];

        t->index = i + 1;
        t->number = t->cd_track_number = i + first_track_nb;
        t->track_is_data = !cdio_cddap_track_audiop(ctx->drive, t->number);
        t->pregap_lsn = cdio_get_track_pregap_lsn(ctx->cdio, t->number);
        t->dropped_pregap_start = CDIO_INVALID_LSN;
        t->merged_pregap_end = CDIO_INVALID_LSN;
        t->start_lsn = cdio_get_track_lsn(ctx->cdio, t->number);
        t->end_lsn = cdio_get_track_last_lsn(ctx->cdio, t->number);

        if ((i == (ctx->nb_cd_tracks - 1)) && (t->end_lsn == CDIO_INVALID_LSN)) {
            t->end_lsn = ctx->end_lsn;
        } else if (t->end_lsn == CDIO_INVALID_LSN) {
            cyanrip_log(ctx, 0, "CDIO returned invalid track %i end LSN\n", t->index);
            return AVERROR(EINVAL);
        }

        t->start_lsn_sig = t->start_lsn;
        t->end_lsn_sig = t->end_lsn;

        if (t->track_is_data) {
            if (ctx->settings.pregap_action[t->number - 1] == CYANRIP_PREGAP_DEFAULT)
                ctx->settings.pregap_action[t->number - 1] = CYANRIP_PREGAP_DROP;
            if (ctx->settings.pregap_action[t->number - 0] == CYANRIP_PREGAP_DEFAULT)
                ctx->settings.pregap_action[t->number - 0] = CYANRIP_PREGAP_DROP;
            if (i == (ctx->nb_cd_tracks - 1)) {
                ctx->tracks[i - 1].end_lsn -= 11400;
                t->pregap_lsn = ctx->tracks[i - 1].end_lsn + 1;
            }
        }
    }

    for (int i = 0; i < ctx->nb_cd_tracks; i++) {
        cyanrip_track *t = &ctx->tracks[i];
        if (t->track_is_data)
            continue;
        t->acurip_track_is_first = 1;
        break;
    }

    for (int i = ctx->nb_cd_tracks - 1; i >= 0; i--) {
        cyanrip_track *t = &ctx->tracks[i];
        if (t->track_is_data)
            continue;
        t->acurip_track_is_last = 1;
        break;
    }

    /* For hot removal detection - init this so we can detect changes */
    get_media_changed(ctx->cdio);

    *s = ctx;
    return 0;
}

#define REPLAYGAIN_REF_LOUDNESS (-18.0)

static int crip_replaygain_meta_track(cyanrip_ctx *ctx, cyanrip_track *t)
{
    char temp[32];

    snprintf(temp, sizeof(temp), "%.2f dB", REPLAYGAIN_REF_LOUDNESS - t->ebu_integrated);
    av_dict_set(&t->meta, "REPLAYGAIN_TRACK_GAIN", temp, 0);

    av_dict_set_int(&t->meta, "R128_TRACK_GAIN", lrintf((REPLAYGAIN_REF_LOUDNESS - t->ebu_integrated + 5) * 256), 0);

    snprintf(temp, sizeof(temp), "%.2f dB", t->ebu_range);
    av_dict_set(&t->meta, "REPLAYGAIN_TRACK_RANGE", temp, 0);

    snprintf(temp, sizeof(temp), "%.6f", powf(10, t->ebu_true_peak / 20.0));
    av_dict_set(&t->meta, "REPLAYGAIN_TRACK_PEAK", temp, 0);

    snprintf(temp, sizeof(temp), "%.2f LUFS", REPLAYGAIN_REF_LOUDNESS);
    av_dict_set(&t->meta, "REPLAYGAIN_REFERENCE_LOUDNESS", temp, 0);

    return 0;
}

static int crip_replaygain_meta_album(cyanrip_ctx *ctx)
{
    char temp[32];

    for (int i = 0; i < ctx->nb_cd_tracks; i++) {
        cyanrip_track *t = &ctx->tracks[i];

        snprintf(temp, sizeof(temp), "%.2f dB", REPLAYGAIN_REF_LOUDNESS - ctx->ebu_integrated);
        av_dict_set(&t->meta, "REPLAYGAIN_ALBUM_GAIN", temp, 0);

        av_dict_set_int(&t->meta, "R128_ALBUM_GAIN", lrintf((REPLAYGAIN_REF_LOUDNESS - ctx->ebu_integrated + 5) * 256), 0);

        snprintf(temp, sizeof(temp), "%.2f dB", ctx->ebu_range);
        av_dict_set(&t->meta, "REPLAYGAIN_ALBUM_RANGE", temp, 0);

        snprintf(temp, sizeof(temp), "%.6f", powf(10, ctx->ebu_true_peak / 20.0));
        av_dict_set(&t->meta, "REPLAYGAIN_ALBUM_PEAK", temp, 0);
    }

    return 0;
}

static void crip_fill_mcn(cyanrip_ctx *ctx)
{
    /* Get disc MCN */
    if (ctx->rcap & CDIO_DRIVE_CAP_READ_MCN) {
        const char *mcn = cdio_get_mcn(ctx->cdio);
        if (mcn) {
            if (strlen(mcn))
                av_dict_set(&ctx->meta, "disc_mcn", mcn, 0);
            cdio_free((void *)mcn);
        }
    }
}

static void track_set_creation_time(cyanrip_ctx *ctx, cyanrip_track *t)
{
    if (dict_get(t->meta, "creation_time"))
        return;

    char t_s[64];
    time_t t_c = time(NULL);
    struct tm *t_l = localtime(&t_c);
    strftime(t_s, sizeof(t_s), "%Y-%m-%dT%H:%M:%S", t_l);
    av_dict_set(&t->meta, "creation_time", t_s, 0);
}

static void copy_album_to_track_meta(cyanrip_ctx *ctx)
{
    for (int i = 0; i < ctx->nb_tracks; i++) {
        av_dict_set_int(&ctx->tracks[i].meta, "track", ctx->tracks[i].number, 0);
        av_dict_set_int(&ctx->tracks[i].meta, "tracktotal", ctx->nb_tracks, 0);
        av_dict_copy(&ctx->tracks[i].meta, ctx->meta, AV_DICT_DONT_OVERWRITE);
    }
}

static const uint8_t silent_frame[CDIO_CD_FRAMESIZE_RAW] = { 0 };

uint64_t paranoia_status[PARANOIA_CB_FINISHED + 1] = { 0 };

static void status_cb(long int n, paranoia_cb_mode_t status)
{
    if (status >= PARANOIA_CB_READ && status <= PARANOIA_CB_FINISHED)
        paranoia_status[status]++;
}

static const uint8_t *cyanrip_read_frame(cyanrip_ctx *ctx)
{
    int err = 0;
    char *msg = NULL;

    const uint8_t *data;
    data = (void *)cdio_paranoia_read_limited(ctx->paranoia, &status_cb,
                                              ctx->settings.max_retries);

    msg = cdio_cddap_errors(ctx->drive);
    if (msg) {
        cyanrip_log(ctx, 0, "\ncdio error: %s\n", msg);
        cdio_cddap_free_messages(msg);
        err = 1;
    }

    if (!data) {
        if (!msg) {
            cyanrip_log(ctx, 0, "\nFrame read failed!\n");
            err = 1;
        }
        data = silent_frame;
    }

    ctx->total_error_count += err;

    return data;
}

static int search_for_offset(cyanrip_track *t, int *offset_found,
                             const uint8_t *mem, int dir,
                             int guess, int bytes)
{
    if (guess) {
        const uint8_t *start_addr = mem + guess*4;
        uint32_t accurip_v1 = 0x0;
        for (int j = 0; j < (CDIO_CD_FRAMESIZE_RAW >> 2); j++)
            accurip_v1 += AV_RL32(&start_addr[j*4]) * (j + 1);
        if (crip_find_ar(t, accurip_v1, 1) == t->ar_db_max_confidence && accurip_v1) {
            *offset_found = guess;
            return 1;
        }
    }

    for (int byte_off = ((dir < 0) * 4); byte_off < bytes; byte_off += 4) {
        if (quit_now)
            return 0;

        int offset = dir * (byte_off >> 2);
        if (guess == offset)
            continue;

        const uint8_t *start_addr = mem + dir * byte_off;
        uint32_t accurip_v1 = 0x0;
        for (int j = 0; j < (CDIO_CD_FRAMESIZE_RAW >> 2); j++)
            accurip_v1 += AV_RL32(&start_addr[j*4]) * (j + 1);
        if (crip_find_ar(t, accurip_v1, 1) == t->ar_db_max_confidence && accurip_v1) {
            *offset_found = offset;
            return 1;
        }
    }

    return 0;
}

static void search_for_drive_offset(cyanrip_ctx *ctx, int range)
{
    int had_ar = 0, did_check = 0;
    int offset_found = 0, offset_found_samples = 0;
    uint8_t *mem = av_malloc(2 * range * CDIO_CD_FRAMESIZE_RAW);

    if (ctx->ar_db_status != CYANRIP_ACCUDB_FOUND)
        goto end;

    for (int t_idx = 0; t_idx < ctx->nb_tracks; t_idx++) {
        cyanrip_track *t = &ctx->tracks[t_idx];
        lsn_t start = cdio_get_track_lsn(ctx->cdio, t_idx + 1);
        lsn_t end = cdio_get_track_last_lsn(ctx->cdio, t_idx + 1);

        if (ctx->tracks[t_idx].ar_db_status != CYANRIP_ACCUDB_FOUND)
            continue;
        else
            had_ar |= 1;

        if ((end - start) < (450 + range))
            continue;

        did_check |= 1;

        /* Set start/end ranges */
        start += 450 - range;
        end = start + 2*range;

        size_t bytes = 0;

        cyanrip_log(ctx, 0, "Loading data for track %i...\n", t_idx + 1);
        cdio_paranoia_seek(ctx->paranoia, start, SEEK_SET);
        for (int i = 0; i < 2*range; i++) {
            const uint8_t *data = cyanrip_read_frame(ctx);
            memcpy(mem + bytes, data, CDIO_CD_FRAMESIZE_RAW);
            bytes += CDIO_CD_FRAMESIZE_RAW;
            if (quit_now) {
                cyanrip_log(ctx, 0, "Stopping, offset finding incomplete!\n");
                goto end;
            }
        }

        int found, offset;
        int dir = (offset_found && (offset_found_samples < 0)) ? -1 : +1;

        cyanrip_log(ctx, 0, "Data loaded, searching for offsets...\n");

        found = search_for_offset(t, &offset, mem + (bytes >> 1), dir, offset_found_samples,
                                  range * CDIO_CD_FRAMESIZE_RAW);
        if (!found)
            found = search_for_offset(t, &offset, mem + (bytes >> 1), -dir, 0,
                                      range * CDIO_CD_FRAMESIZE_RAW);

        if (!found) {
            cyanrip_log(ctx, 0, "Nothing found for track %i%s\n", t_idx + 1,
                        t_idx != (ctx->nb_tracks - 1) ? ", trying another track" : "");
        } else if (!offset_found) {
            offset_found_samples = offset;
            offset_found++;
            cyanrip_log(ctx, 0, "Offset of %c%i found in track %i%s\n",
                        offset >= 0 ? '+' : '-', abs(offset), t_idx + 1,
                        t_idx != (ctx->nb_tracks - 1) ? ", trying to confirm with another track" : "");
        } else if (offset_found_samples == offset) {
            offset_found++;
            cyanrip_log(ctx, 0, "Offset of %c%i confirmed (confidence: %i) in track %i%s\n",
                        offset >= 0 ? '+' : '-', abs(offset), offset_found, t_idx + 1,
                        t_idx != (ctx->nb_tracks - 1) ? ", trying to confirm with another track" : "");
        } else {
            cyanrip_log(ctx, 0, "New offset of %c%i found at track %i, scrapping old offset of %c%i%s\n",
                        offset >= 0 ? '+' : '-', abs(offset), t_idx + 1,
                        offset_found_samples >= 0 ? '+' : '-', abs(offset_found_samples),
                        t_idx != (ctx->nb_tracks - 1) ? ", trying to confirm with another track" : "");
            offset_found_samples = offset;
            offset_found = 1;
        }
    }

end:
    av_free(mem);

    if (!offset_found) {
        if (!had_ar) {
            cyanrip_log(ctx, 0, "No track had AccuRip entry, cannot find offset!\n");
        } else if (had_ar && !did_check) {
            cyanrip_log(ctx, 0, "No track was long enough, unable to find drive offset!\n");
        } else {
            cyanrip_log(ctx, 0, "Was not able to find drive offset with a radius of %i frames"
                        ", trying again with a larger radius...\n", range);
            search_for_drive_offset(ctx, 2*range);
        }
        return;
    } else {
        cyanrip_log(ctx, 0, "Drive offset of %c%i found (confidence: %i)!\n",
                    offset_found_samples >= 0 ? '+' : '-', abs(offset_found_samples), offset_found);
    }
}

static void track_read_extra(cyanrip_ctx *ctx, cyanrip_track *t)
{
    if (!t->track_is_data) {
        /* ISRC code */
        if (!ctx->disregard_cd_isrc && (ctx->rcap & CDIO_DRIVE_CAP_READ_ISRC) && !dict_get(t->meta, "isrc")) {
            const char *isrc_str = cdio_get_track_isrc(ctx->cdio, t->cd_track_number);
            if (isrc_str) {
                if (strlen(isrc_str))
                    av_dict_set(&t->meta, "isrc", isrc_str, 0);
                else
                    ctx->disregard_cd_isrc = 1;
                cdio_free((void *)isrc_str);
            } else {
                ctx->disregard_cd_isrc = 1;
            }
        }

        /* TOC preemphasis flag*/
        t->preemphasis = cdio_cddap_track_preemp(ctx->drive, t->cd_track_number);

        /* Subcode preemphasis flag */
        if (!t->preemphasis && (ctx->rcap & CDIO_DRIVE_CAP_READ_ISRC)) {
            cdio_subchannel_t subchannel_data = { 0 };
            driver_return_code_t ret = cdio_audio_read_subchannel(ctx->cdio, &subchannel_data);
            if (ret != DRIVER_OP_SUCCESS) {
                cyanrip_log(ctx, 0, "Unable to read track %i subchannel info!\n", t->number);
            } else {
                t->preemphasis = t->preemphasis_in_subcode = subchannel_data.control & 0x01;
            }
        }
    }
}

static double sample_peak_rel_amp(const uint8_t *data, const int bytes)
{
    const int16_t* samples = (int16_t*)data;
    const int sample_num = bytes / 2;

    int sample_peak = 0;
    for (int i = 0; i < sample_num; ++i) {
        sample_peak = FFMAX(sample_peak, abs(samples[i]));
    }

    /* The greatest sample absolute value is abs(INT16_MIN) = 32768 */
    return (double)sample_peak/32768.0;
}

static int cyanrip_rip_track(cyanrip_ctx *ctx, cyanrip_track *t)
{
    int ret = 0;
    int line_len = 0;
    int max_line_len = 0;
    char line[4096];

    if (t->track_is_data) {
        cyanrip_log(ctx, 0, "Track %i is data:\n", t->number);
        cyanrip_log_track_end(ctx, t);
        cyanrip_cue_track(ctx, t);
        return 0;
    }

    /* Set creation time at the start of ripping */
    track_set_creation_time(ctx, t);

    uint32_t start_frames_read;
    uint32_t *last_checksums = NULL;
    uint32_t nb_last_checksums = 0;
    uint32_t repeat_mode_encode = 0;
    uint32_t total_repeats = 0;
    int calc_global_peak_set = 0;
    int calc_global_peak = !ctx->settings.ripping_retries;
repeat_ripping:;
    const int frames_before_disc_start = t->frames_before_disc_start;
    const int frames = t->frames;
    const int frames_after_disc_end = t->frames_after_disc_end;
    const ptrdiff_t offs = t->partial_frame_byte_offs;
    start_frames_read = ctx->frames_read;

    cdio_paranoia_seek(ctx->paranoia, t->start_lsn, SEEK_SET);

    int start_err = ctx->total_error_count;

    /* Checksum */
    cyanrip_checksum_ctx checksum_ctx;
    crip_init_checksum_ctx(ctx, &checksum_ctx, t);

    /* Reset sample peak for this attempt - a bad sample from a discarded
     * (non-matching) repeat ripping pass must not stick around forever. */
    t->sample_peak_rel_amp = 0.0;

    /* Fill with silence to maintain track length */
    for (int i = 0; i < frames_before_disc_start; i++) {
        int bytes = CDIO_CD_FRAMESIZE_RAW;
        const uint8_t *data = silent_frame;

        if (!i && offs) {
            data += CDIO_CD_FRAMESIZE_RAW + offs;
            bytes = -offs;
        }

        crip_process_checksums(&checksum_ctx, data, bytes);

        if (!ctx->settings.ripping_retries || repeat_mode_encode) {
            ret = cyanrip_send_pcm_to_encoders(ctx, t->enc_ctx, ctx->settings.outputs_num,
                                               t->dec_ctx, data, bytes, calc_global_peak);
            if (ret) {
                cyanrip_log(ctx, 0, "Error in decoding/sending frame: %s\n", av_err2str(ret));
                goto fail;
            }
        }
    }

    int64_t frame_last_read = av_gettime_relative();

    /* Read the actual CD data */
    for (int i = 0; i < frames; i++) {
        /* Detect disc removals */
        if (get_media_changed(ctx->cdio)) {
            cyanrip_log(ctx, 0, "\nDrive media changed, stopping!\n");
            ret = AVERROR(EINVAL);
            goto fail;
        }

        /* Flush paranoia cache if overreading into lead-out - no idea why */
        if ((t->start_lsn + i) > ctx->end_lsn)
            cdio_paranoia_seek(ctx->paranoia, t->start_lsn + i, SEEK_SET);

        int bytes = CDIO_CD_FRAMESIZE_RAW;
        const uint8_t *data = cyanrip_read_frame(ctx);

        /* Account for partial frames caused by the offset */
        if (offs > 0) {
            if (!i) {
                data  += offs;
                bytes -= offs;
            } else if ((i == (frames - 1)) && !frames_after_disc_end) {
                bytes = offs;
            }
        } else if (offs < 0) {
            if (!i && !frames_before_disc_start) {
                data += CDIO_CD_FRAMESIZE_RAW + offs;
                bytes = -offs;
            } else if (i == (frames - 1)) {
                bytes += offs;
            }
        }

        /* Stop now if requested */
        if (quit_now) {
            cyanrip_log(ctx, 0, "\nStopping, ripping incomplete!\n");
            break;
        }

        /* Update checksums */
        crip_process_checksums(&checksum_ctx, data, bytes);

         /* Update sample peak */
        t->sample_peak_rel_amp = FFMAX(sample_peak_rel_amp(data, bytes), t->sample_peak_rel_amp);

        /* Decode and encode */
        if (!ctx->settings.ripping_retries || repeat_mode_encode) {
            ret = cyanrip_send_pcm_to_encoders(ctx, t->enc_ctx, ctx->settings.outputs_num,
                                               t->dec_ctx, data, bytes, calc_global_peak);
            if (ret < 0) {
                cyanrip_log(ctx, 0, "\nError in decoding/sending frame: %s\n", av_err2str(ret));
                goto fail;
            }
        }

        if (line_len > 0) {
            cyanrip_log(NULL, 0, "\r", line);
            line_len = 0;
        }

        /* Report progress */
        line_len += snprintf(line, sizeof(line),
                             "Ripping%strack %i, progress - %0.2f%%",
                             (!ctx->settings.ripping_retries || repeat_mode_encode) ? " and encoding " : " ",
                             t->number, ((double)(i + 1)/frames)*100.0f);

        ctx->frames_read++;

        int64_t cur_time   = av_gettime_relative();
        int64_t frame_diff = cur_time - frame_last_read;
        frame_last_read = cur_time;

        int64_t diff = cr_sliding_win(&ctx->eta_ctx, frame_diff, cur_time,
                                      av_make_q(1, 1000000),
                                      1000000LL * 1200LL, 1);

        int64_t seconds = (ctx->frames_to_read - ctx->frames_read) * diff;

        int hours = 0;
        while (seconds >= (3600LL * 1000000LL)) {
            seconds -= (3600LL * 1000000LL);
            hours++;
        }

        int minutes = 0;
        while (seconds >= (60LL * 1000000LL)) {
            seconds -= (60LL * 1000000LL);
            minutes++;
        }

        seconds = av_rescale(seconds, 1, 1000000);

        if (seconds == 60) {
            minutes++;
            seconds = 0;
        }

        if (minutes == 60) {
            hours++;
            minutes = 0;
        }

        if (hours)
            line_len += snprintf(line + line_len, sizeof(line) - line_len,
                                 ", ETA - %ih %im", hours, minutes);
        else if (minutes)
            line_len += snprintf(line + line_len, sizeof(line) - line_len,
                                 ", ETA - %im", minutes);
        else
            line_len += snprintf(line + line_len, sizeof(line) - line_len,
                                 ", ETA - %" PRId64 "s", seconds);

        if (ctx->total_error_count - start_err)
            line_len += snprintf(line + line_len, sizeof(line) - line_len,
                                 ", errors - %i", ctx->total_error_count - start_err);

        max_line_len = FFMAX(line_len, max_line_len);
        if (line_len < max_line_len) {
            for (int c = line_len; c < max_line_len; c++)
                line_len += snprintf(line + line_len, sizeof(line) - line_len, " ");
        }

        cyanrip_log(NULL, 0, "%s", line);
    }

    /* Fill with silence to maintain track length */
    for (int i = 0; i < frames_after_disc_end; i++) {
        int bytes = CDIO_CD_FRAMESIZE_RAW;
        const uint8_t *data = silent_frame;

        if ((i == (frames_after_disc_end - 1)) && offs)
            bytes = offs;

        crip_process_checksums(&checksum_ctx, data, bytes);

        if (!ctx->settings.ripping_retries || repeat_mode_encode) {
            ret = cyanrip_send_pcm_to_encoders(ctx, t->enc_ctx, ctx->settings.outputs_num,
                                               t->dec_ctx, data, bytes, calc_global_peak);
            if (ret < 0) {
                cyanrip_log(ctx, 0, "Error in decoding/sending frame: %s\n", av_err2str(ret));
                goto fail;
            }
        }
    }

    calc_global_peak = 0;

    crip_finalize_checksums(&checksum_ctx, t);

    if (ctx->settings.ripping_retries) {
        int matches = 0;
        for (int i = 0; i < nb_last_checksums; i++)
            matches += last_checksums[i] == checksum_ctx.eac_crc;

        total_repeats++;
        if (matches >= ctx->settings.ripping_retries) {
            cyanrip_log(ctx, 0, "\nDone; (%i out of %i matches for current checksum %08X)\n",
                        matches, ctx->settings.ripping_retries, checksum_ctx.eac_crc);
            goto finalize_ripping;
        }
        if (total_repeats >= ctx->settings.max_retries) {
            cyanrip_log(ctx, 0, "\nDone; (no matches found, but hit repeat limit of %i)\n",
                        ctx->settings.max_retries);
            goto finalize_ripping;
        }

        /* If the next match may be the last one, start encoding */
        if ((matches + 1) >= ctx->settings.ripping_retries ||
            (total_repeats + 1) >= ctx->settings.max_retries) {
            repeat_mode_encode = 1;
            if (!calc_global_peak_set) {
                calc_global_peak_set = 1;
                calc_global_peak = 1;
            }
        }

        cyanrip_log(ctx, 0, "\nRepeating ripping (%i out of %i matches for current checksum %08X)\n",
                    matches, ctx->settings.ripping_retries, checksum_ctx.eac_crc);

        last_checksums = av_realloc(last_checksums,
                                    (nb_last_checksums + 1)*sizeof(*last_checksums));
        if (!last_checksums) {
            ret = AVERROR(ENOMEM);
            goto end;
        }

        last_checksums[nb_last_checksums] = checksum_ctx.eac_crc;
        nb_last_checksums++;

        int err = cyanrip_reset_encoding(ctx, t);
        if (err < 0) {
            cyanrip_log(ctx, 0, "Error in encoding: %s\n", av_err2str(err));
            ret = err;
            goto end;
        }

        ctx->frames_read = start_frames_read;
        goto repeat_ripping;
    }

finalize_ripping:
    cyanrip_log(NULL, 0, "\nFlushing encoders...\n");

    /* Flush encoders */
    ret = cyanrip_send_pcm_to_encoders(ctx, t->enc_ctx, ctx->settings.outputs_num,
                                       t->dec_ctx, NULL, 0, 0);
    if (ret) {
        cyanrip_log(ctx, 0, "Error sending flush signal to encoders: %s\n", av_err2str(ret));
        return ret;
    }

fail:
    if (!ret && !quit_now) {
        if (ctx->total_error_count - start_err)
            cyanrip_log(ctx, 0, "Track %i ripped and encoded with errors.\n", t->number);
        else
            cyanrip_log(ctx, 0, "Track %i ripped and encoded successfully!\n", t->number);
    }

end:
    av_free(last_checksums);

    t->total_repeats = total_repeats;
    if (!quit_now && !ret) {
        cyanrip_finalize_encoding(ctx, t);
        if (ctx->settings.enable_replaygain)
            crip_replaygain_meta_track(ctx, t);
        cyanrip_log_track_end(ctx, t);
        cyanrip_cue_track(ctx, t);
    } else {
        ctx->total_error_count++;
    }

    return ret;
}

static void on_quit_signal(int signo)
{
    if (quit_now) {
        cyanrip_log(NULL, 0, "Force quitting\n");
        exit(1);
    }
    cyanrip_log(NULL, 0, "\r\nTrying to quit\n");
    quit_now = 1;
}

static void setup_track_lsn(cyanrip_ctx *ctx, cyanrip_track *t)
{
    lsn_t first_frame = t->start_lsn;
    lsn_t last_frame  = t->end_lsn;

    /* Duration doesn't depend on adjustments we make to frames */
    int frames = last_frame - first_frame + 1;

    t->nb_samples = frames*(CDIO_CD_FRAMESIZE_RAW >> 2);

    /* Move the seek position coarsely */
    const int extra_frames = ctx->settings.over_under_read_frames;
    int sign = (extra_frames < 0) ? -1 : ((extra_frames > 0) ? +1 : 0);
    first_frame += sign*FFMAX(FFABS(extra_frames) - 1, 0);
    last_frame += sign*FFMAX(FFABS(extra_frames) - 1, 0);

    /* Bump the lower/higher frame in the offset direction */
    first_frame -= sign < 0;
    last_frame  += sign > 0;

    /* Don't read into the lead in/out */
    if (!ctx->settings.overread_leadinout) {
        t->frames_before_disc_start = FFMAX(ctx->start_lsn - first_frame, 0);
        t->frames_after_disc_end = FFMAX(last_frame - ctx->end_lsn, 0);

        first_frame += t->frames_before_disc_start;
        last_frame  -= t->frames_after_disc_end;
    } else {
        t->frames_before_disc_start = 0;
        t->frames_after_disc_end = 0;
    }

    /* Offset accounted start/end sectors */
    t->start_lsn = first_frame;
    t->end_lsn = last_frame;
    t->frames = last_frame - first_frame + 1;

    /* Last/first frame partial offset */
    ptrdiff_t offs = ctx->settings.offset*4;
    offs -= sign*FFMAX(FFABS(extra_frames) - 1, 0)*CDIO_CD_FRAMESIZE_RAW;

    t->partial_frame_byte_offs = offs;
}

static void setup_track_offsets_and_report(cyanrip_ctx *ctx)
{
    int gaps = 0;

    cyanrip_log(ctx, 0, "Gaps:\n");

    /* Before pregap */
    if (ctx->tracks[0].pregap_lsn != CDIO_INVALID_LSN &&
        ctx->tracks[0].pregap_lsn > ctx->start_lsn) {
        cyanrip_log(ctx, 0, "    %i frame gap between lead-in and track 1 pregap, merging into pregap\n",
                    ctx->tracks[0].pregap_lsn - ctx->start_lsn);
        gaps++;

        ctx->tracks[0].pregap_lsn = ctx->start_lsn;
    } else if (ctx->tracks[0].pregap_lsn == CDIO_INVALID_LSN &&
               ctx->tracks[0].start_lsn > ctx->start_lsn) {
        cyanrip_log(ctx, 0, "    %i frame unmarked gap between lead-in and track 1, marking as a pregap\n",
                    ctx->tracks[0].start_lsn - ctx->start_lsn);
        gaps++;

        ctx->tracks[0].pregap_lsn = ctx->start_lsn;
    }

    /* Pregaps */
    for (int i = 0; i < ctx->nb_tracks; i++) {
        cyanrip_track *ct = &ctx->tracks[i - 0];
        cyanrip_track *lt = ct->number > 1 ? &ctx->tracks[i - 1] : NULL;

        if (ct->pregap_lsn == CDIO_INVALID_LSN)
            continue;

        cyanrip_log(ctx, 0, "    %i frame pregap in track %i, ",
                    ct->start_lsn - ct->pregap_lsn, ct->number);
        gaps++;

        switch (ctx->settings.pregap_action[ct->number - 1]) {
        case CYANRIP_PREGAP_DEFAULT:
            if (!lt)
                cyanrip_log(ctx, 0, "unmerged\n");
            else
                cyanrip_log(ctx, 0, "merging into track %i\n", lt->number);

            if ((ct->number - 1) == 0)
                ct->dropped_pregap_start = ct->pregap_lsn;
            break;
        case CYANRIP_PREGAP_DROP:
            cyanrip_log(ctx, 0, "dropping\n");
            ct->dropped_pregap_start = ct->pregap_lsn;
            if (lt)
                lt->end_lsn = ct->pregap_lsn - 1;
            break;
        case CYANRIP_PREGAP_MERGE:
            cyanrip_log(ctx, 0, "merging\n");
            ct->merged_pregap_end = ct->start_lsn;
            ct->start_lsn = ct->pregap_lsn;
            if (lt)
                lt->end_lsn = ct->pregap_lsn - 1;
            break;
        case CYANRIP_PREGAP_TRACK:
            cyanrip_log(ctx, 0, "splitting off into a new track, number %i\n", ct->number > 1 ? ct->number : 0);

            if (lt)
                lt->end_lsn = ct->pregap_lsn - 1;

            if (ct->number > 1) /* Push all track numbers up if needed */
                for (int j = i; j < ctx->nb_tracks; j++)
                    ctx->tracks[j].number++;

            memmove(&ctx->tracks[i + 1], &ctx->tracks[i],
                    sizeof(cyanrip_track)*(ctx->nb_tracks - i));

            ct = &ctx->tracks[i + 1];

            ctx->nb_tracks++;
            cyanrip_track *nt = &ctx->tracks[i];

            memset(nt, 0, sizeof(*nt));

            nt->number = ct->number - 1;
            nt->pregap_lsn = CDIO_INVALID_LSN;
            nt->dropped_pregap_start = CDIO_INVALID_LSN;
            nt->merged_pregap_end = CDIO_INVALID_LSN;
            nt->start_lsn = ct->pregap_lsn;
            nt->end_lsn = ct->start_lsn - 1;
            nt->cd_track_number = ct->cd_track_number;

            ct->pregap_lsn = CDIO_INVALID_LSN;
        }
    }

    /* Between tracks */
    for (int i = 1; i < ctx->nb_tracks; i++) {
        cyanrip_track *ct = &ctx->tracks[i - 0];
        cyanrip_track *lt = &ctx->tracks[i - 1];

        if (ct->start_lsn == (lt->end_lsn + 1))
            continue;

        int discont_frames = ct->start_lsn - lt->end_lsn - 1;

        cyanrip_log(ctx, 0, "    %i frame discontinuity between tracks %i and %i, ",
                    discont_frames, ct->number, lt->number);
        gaps++;

        if (ctx->settings.pregap_action[ct->number - 1] != CYANRIP_PREGAP_DROP) {
            cyanrip_log(ctx, 0, "padding track %i\n", lt->number);
            lt->end_lsn = ct->start_lsn - 1;
        } else {
            cyanrip_log(ctx, 0, "ignoring\n");
        }
    }

    /* After last track */
    cyanrip_track *lt = &ctx->tracks[ctx->nb_tracks - 1];
    if (ctx->end_lsn > lt->end_lsn && !lt->track_is_data) {
        int discont_frames = ctx->end_lsn - lt->end_lsn;
        cyanrip_log(ctx, 0, "    %i frame gap between last track and lead-out, padding track\n",
                    discont_frames);
        gaps++;

        lt->end_lsn = ctx->end_lsn;
    }

    /* Finally set up the internals with the set start_lsn/end_lsn */
    for (int i = 0; i < ctx->nb_tracks; i++) {
        cyanrip_track *t = &ctx->tracks[i];

        if (t->track_is_data)
            t->frames = t->end_lsn - t->start_lsn + 1;
        else
            setup_track_lsn(ctx, t);
    }

    /* Setup next/previous pointers and redo track indices */
    for (int i = 0; i < ctx->nb_tracks; i++) {
        cyanrip_track *t = &ctx->tracks[i];

        t->index = i + 1;
        t->pt = i ? &ctx->tracks[i - 1] : NULL;
        t->nt = i != (ctx->nb_tracks - 1) ? &ctx->tracks[i + 1] : NULL;
    }

    cyanrip_log(ctx, 0, "%s\n", gaps ? "" : "    None signalled\n");
}

int main(int argc, char **argv)
{
    cyanrip_ctx *ctx = NULL;
    cyanrip_settings settings = { 0 };

#ifdef _WIN32
    setlocale(LC_ALL, ".UTF8");
#endif

    av_log_set_level(AV_LOG_QUIET);

    if (signal(SIGINT, on_quit_signal) == SIG_ERR)
        cyanrip_log(ctx, 0, "Can't init signal handler!\n");

    /* Default settings */
    settings.dev_path = NULL;
    settings.folder_name_scheme = "{album}{if #releasecomment# > #0# (|releasecomment|)} [{format}]";
    settings.track_name_scheme = "{if #totaldiscs# > #1#|disc|.}{track} - {title}";
    settings.log_name_scheme = "{album}{if #totaldiscs# > #1# CD|disc|}";
    settings.cue_name_scheme = "{album}{if #totaldiscs# > #1# CD|disc|}";
    settings.sanitize_method = CRIP_SANITIZE_UNICODE;
    settings.speed = 0;
    settings.max_retries = 10;
    settings.over_under_read_frames = 0;
    settings.offset = 0;
    settings.ripping_retries = 0;
    settings.print_info_only = 0;
    settings.disable_mb = 0;
    settings.disable_coverart_db = 0;
    settings.coverart_lookup_size = COVERART_LOOKUP_SIZE_ORIGINAL;
    settings.decode_hdcd = 0;
    settings.deemphasis = 1;
    settings.force_deemphasis = 0;
    settings.bitrate = 256.0f;
    settings.overread_leadinout = 0;
    settings.rip_indices_count = -1;
    settings.disable_accurip = 0;
    settings.eject_on_success_rip = 0;
    settings.outputs[0] = CYANRIP_FORMAT_FLAC;
    settings.outputs_num = 1;
    settings.disable_coverart_embedding = 0;
    settings.enable_replaygain = 1;
    settings.paranoia_level = FF_ARRAY_ELEMS(paranoia_level_map) - 1;

    memset(settings.pregap_action, CYANRIP_PREGAP_DEFAULT, 198*sizeof(*settings.pregap_action));

    int idx;
    char *p_save, *p;
    int mb_release_idx = -1;
    char *mb_release_str = NULL;
    int discnumber = 0, totaldiscs = 0;
    char *album_metadata_ptr = NULL;
    char *track_metadata_ptr[198] = { NULL };
    int track_metadata_ptr_cnt = 0;
    int find_drive_offset_range = 0;
    int offset_set = 0;

    CRIPArt cover_arts[32] = { 0 };
    int nb_cover_arts = 0;

    CRIPArt track_cover_arts[198] = { 0 };
    int track_cover_arts_map[198] = { 0 };
    int nb_track_cover_arts = 0;

    snprintf(cyanrip_helpstr, sizeof(cyanrip_helpstr),
             "cyanrip %s (%s)", PROJECT_VERSION_STRING, vcstag);

    GEN_OPT_INIT(opts_list, 64);

    GEN_OPT_SEC(opts_list, "Ripping options");
    GEN_OPT_ONE(opts_list, char *,  device, "d", 1, 1, NULL, 0, 0,
                "Set device path (can be a TOC file)");
    GEN_OPT_ONE(opts_list, int32_t, offset, "s", 1, 1, 0, INT32_MIN, INT32_MAX,
                "CD drive offset in samples");
    GEN_OPT_ONE(opts_list, int32_t, retries, "r", 1, 1, 10, 0, INT32_MAX,
                "Maximum number of retries for frames and repeated rips");
    GEN_OPT_ONE(opts_list, int32_t, repeat_rips, "Z", 1, 1, 0, 0, INT32_MAX,
                "Rip tracks until checksums match N times (for damaged CDs)");
    GEN_OPT_ONE(opts_list, int32_t, speed, "S", 1, 1, 0, 0, INT32_MAX,
                "Set drive speed");
    GEN_OPT_ARR(opts_list, char *,  pregap, "p", 0, 0, 198, 0, 0,
                "Track pregap handling: N=default|drop|merge|track (repeatable)");
    GEN_OPT_ONE(opts_list, char *,  paranoia, "P", 1, 1, NULL, 0, 0,
                "Paranoia level (0..max, or 'none'/'max')");
    GEN_OPT_ONE(opts_list, bool,    overread, "O", 0, 0, 0, 0, 0,
                "Enable overreading into lead-in and lead-out");
    GEN_OPT_ONE(opts_list, bool,    hdcd, "H", 0, 0, 0, 0, 0,
                "Enable HDCD decoding");
    GEN_OPT_ONE(opts_list, bool,    force_deemphasis, "E", 0, 0, 0, 0, 0,
                "Force CD deemphasis");
    GEN_OPT_ONE(opts_list, bool,    no_deemphasis, "W", 0, 0, 0, 0, 0,
                "Disable automatic CD deemphasis");
    GEN_OPT_ONE(opts_list, bool,    no_replaygain, "K", 0, 0, 0, 0, 0,
                "Disable ReplayGain tagging");

    GEN_OPT_SEC(opts_list, "Output options");
    GEN_OPT_ARR(opts_list, char *,  outputs, "o", ',', 0, 32, 0, 0,
                "Comma separated list of output formats ('help' lists all)");
    GEN_OPT_ONE(opts_list, float,   bitrate, "b", 1, 1, 256.0f, 0.0f, 10000.0f,
                "Bitrate of lossy files in kbps");
    GEN_OPT_ONE(opts_list, char *,  folder_scheme, "D", 1, 1,
                settings.folder_name_scheme, 0, 0,
                "Directory naming scheme");
    GEN_OPT_ONE(opts_list, char *,  track_scheme, "F", 1, 1,
                settings.track_name_scheme, 0, 0,
                "Track naming scheme");
    GEN_OPT_ONE(opts_list, char *,  log_scheme, "L", 1, 1,
                settings.log_name_scheme, 0, 0,
                "Log file name scheme");
    GEN_OPT_ONE(opts_list, char *,  cue_scheme, "M", 1, 1,
                settings.cue_name_scheme, 0, 0,
                "CUE file name scheme");
    GEN_OPT_ARR(opts_list, int32_t, tracks, "l", ',', 0, 198, 0, 198,
                "Comma separated list of tracks to rip (default: all)");
    GEN_OPT_ONE(opts_list, char *,  sanitize, "T", 1, 1, NULL, 0, 0,
                "Filename sanitation: simple, os_simple, unicode, os_unicode");

    GEN_OPT_SEC(opts_list, "Metadata options");
    GEN_OPT_ONE(opts_list, bool,    info, "I", 0, 0, 0, 0, 0,
                "Only print CD and track info");
    GEN_OPT_ONE(opts_list, bool,    cue_only, "J", 0, 0, 0, 0, 0,
                "Only generate and print a CUE sheet, don't rip");
    GEN_OPT_ONE(opts_list, char *,  album_meta, "a", 1, 1, NULL, 0, 0,
                "Album metadata, key=value:key=value");
    GEN_OPT_ARR(opts_list, char *,  track_meta, "t", 0, 0, 198, 0, 0,
                "Track metadata as N=key=value:key=value (repeatable)");
    GEN_OPT_ONE(opts_list, char *,  release, "R", 1, 1, NULL, 0, 0,
                "MusicBrainz release: 1-based index or ID string");
    GEN_OPT_ONE(opts_list, char *,  disc, "c", 1, 1, NULL, 0, 0,
                "Multi-disc tag: disc/totaldiscs");
    GEN_OPT_ARR(opts_list, char *,  cover, "C", 0, 0, 32, 0, 0,
                "Cover art: title=path (or N=path per-track, repeatable)");
    GEN_OPT_ONE(opts_list, bool,    no_musicbrainz, "N", 0, 0, 0, 0, 0,
                "Disable MusicBrainz lookup");
    GEN_OPT_ONE(opts_list, bool,    no_accurip, "A", 0, 0, 0, 0, 0,
                "Disable AccurateRip database query and validation");
    GEN_OPT_ONE(opts_list, bool,    no_coverart_db, "U", 0, 0, 0, 0, 0,
                "Disable Cover art DB query and retrieval");
    GEN_OPT_ONE(opts_list, int32_t, cover_size, "m", 1, 1, -1, -1, 1200,
                "Cover art max size: 250, 500, 1200, or -1 for original");
    GEN_OPT_ONE(opts_list, bool,    no_coverart_embed, "G", 0, 0, 0, 0, 0,
                "Disable embedding of cover art images");

    GEN_OPT_SEC(opts_list, "Misc. options");
    GEN_OPT_ONE(opts_list, bool,    eject, "Q", 0, 0, 0, 0, 0,
                "Eject tray once successfully done");
    GEN_OPT_ONE(opts_list, bool,    find_offset, "f", 0, 0, 0, 0, 0,
                "Find drive offset (requires a disc with an AccuRip entry)");
    GEN_OPT_ONE(opts_list, char *,  verify_log, "Y", 1, 1, NULL, 0, 0,
                "Verify a rip log's FUN512 checksum");

    {
        int r = GEN_OPT_PARSE(NULL, opts_list, argc, argv);
        if (r == -EAGAIN)
            return 0;
        if (r < 0)
            return 1;
    }

    for (int i = 1; i < argc; i++) {
        if (!strcmp(argv[i], "-s") || !strcmp(argv[i], "--offset")) {
            offset_set = 1;
            break;
        }
    }

    if (verify_log) {
        switch (cyanrip_verify_log(verify_log)) {
        case CRIP_LOG_VALID:
            cyanrip_log(NULL, 0, "Log \"%s\" checksum valid.\n", verify_log);
            return 0;
        case CRIP_LOG_MISMATCH:
            cyanrip_log(NULL, 0, "Log \"%s\" checksum mismatch, "
                        "the file has been modified!\n", verify_log);
            break;
        case CRIP_LOG_TRAILING_DATA:
            cyanrip_log(NULL, 0, "Log \"%s\" has data after the checksum, "
                        "the file has been modified!\n", verify_log);
            break;
        case CRIP_LOG_NO_CHECKSUM:
            cyanrip_log(NULL, 0, "No FUN512 checksum found in \"%s\"!\n",
                        verify_log);
            break;
        case CRIP_LOG_IO_ERROR:
            cyanrip_log(NULL, 0, "Couldn't read \"%s\"!\n", verify_log);
            break;
        }
        return 1;
    }

    if (device)
        settings.dev_path = strdup(device);

    settings.offset = offset;
    settings.over_under_read_frames =
        (offset < 0 ? -1 : 1) *
        (int)ceilf(abs(offset) / (float)(CDIO_CD_FRAMESIZE_RAW >> 2));

    settings.max_retries                = retries;
    settings.ripping_retries            = repeat_rips;
    settings.speed                      = speed;
    settings.bitrate                    = bitrate;
    settings.overread_leadinout         = overread;
    settings.decode_hdcd                = hdcd;
    settings.force_deemphasis           = force_deemphasis;
    settings.deemphasis                 = !no_deemphasis;
    settings.enable_replaygain          = !no_replaygain;
    settings.disable_mb                 = no_musicbrainz;
    settings.disable_accurip            = no_accurip;
    settings.disable_coverart_db        = no_coverart_db;
    settings.disable_coverart_embedding = no_coverart_embed;
    settings.print_info_only            = info;
    settings.generate_cue_only          = cue_only;
    settings.eject_on_success_rip       = eject;
    settings.folder_name_scheme         = folder_scheme;
    settings.track_name_scheme          = track_scheme;
    settings.log_name_scheme            = log_scheme;
    settings.cue_name_scheme            = cue_scheme;

    find_drive_offset_range = find_offset ? 6 : 0;
    album_metadata_ptr = album_meta;

    if (paranoia) {
        if (!strcmp(paranoia, "none"))
            settings.paranoia_level = 0;
        else if (!strcmp(paranoia, "max"))
            settings.paranoia_level = crip_max_paranoia_level;
        else
            settings.paranoia_level = (int)strtol(paranoia, NULL, 10);
        if (settings.paranoia_level < 0 ||
            settings.paranoia_level > crip_max_paranoia_level) {
            cyanrip_log(ctx, 0,
                        "Invalid paranoia level %i must be between 0 and %i!\n",
                        settings.paranoia_level, crip_max_paranoia_level);
            return 1;
        }
    }

    switch (cover_size) {
    case -1:   settings.coverart_lookup_size = COVERART_LOOKUP_SIZE_ORIGINAL; break;
    case 250:  settings.coverart_lookup_size = COVERART_LOOKUP_SIZE_250;      break;
    case 500:  settings.coverart_lookup_size = COVERART_LOOKUP_SIZE_500;      break;
    case 1200: settings.coverart_lookup_size = COVERART_LOOKUP_SIZE_1200;     break;
    default:
        cyanrip_log(ctx, 0,
                    "Invalid max coverart size %i (must be 250, 500, 1200 or -1)\n",
                    cover_size);
        return 1;
    }

    if (sanitize) {
        if      (!strcmp(sanitize, "simple"))     settings.sanitize_method = CRIP_SANITIZE_SIMPLE;
        else if (!strcmp(sanitize, "os_simple"))  settings.sanitize_method = CRIP_SANITIZE_OS_SIMPLE;
        else if (!strcmp(sanitize, "unicode"))    settings.sanitize_method = CRIP_SANITIZE_UNICODE;
        else if (!strcmp(sanitize, "os_unicode")) settings.sanitize_method = CRIP_SANITIZE_OS_UNICODE;
        else {
            cyanrip_log(ctx, 0, "Invalid sanitation method %s\n", sanitize);
            return 1;
        }
    }

    if (release) {
        p = NULL;
        mb_release_idx = strtol(release, &p, 10);
        if (p != NULL && p[0] != ' ' && p[0] != '\0') {
            mb_release_str = release;
            mb_release_idx = -1;
        } else if (mb_release_idx <= 0) {
            cyanrip_log(ctx, 0, "Invalid release index %i!\n", mb_release_idx);
            return 1;
        }
    }

    if (disc) {
        p = av_strtok(disc, "/", &p_save);
        discnumber = strtol(p, NULL, 10);
        if (discnumber <= 0) {
            cyanrip_log(ctx, 0, "Invalid discnumber %i\n", discnumber);
            return 1;
        }
        p = av_strtok(NULL, "/", &p_save);
        if (p) {
            totaldiscs = strtol(p, NULL, 10);
            if (totaldiscs <= 0) {
                cyanrip_log(ctx, 0, "Invalid totaldiscs %i\n", totaldiscs);
                return 1;
            }
            if (discnumber > totaldiscs) {
                cyanrip_log(ctx, 0,
                            "discnumber %i is larger than totaldiscs %i\n",
                            discnumber, totaldiscs);
                return 1;
            }
        }
    }

    int nb_outputs = 0;
    while (nb_outputs < 32 && outputs[nb_outputs])
        nb_outputs++;
    if (nb_outputs > 0) {
        if (!strcmp(outputs[0], "help")) {
            cyanrip_log(ctx, 0, "Supported output codecs:\n");
            cyanrip_print_codecs();
            return 0;
        }
        settings.outputs_num = 0;
        for (int i = 0; i < nb_outputs; i++) {
            int res = cyanrip_validate_fmt(outputs[i]);
            if (res == -1) {
                cyanrip_log(ctx, 0, "Invalid format \"%s\"\n", outputs[i]);
                return 1;
            }
            for (int k = 0; k < settings.outputs_num; k++) {
                if (settings.outputs[k] == res) {
                    cyanrip_log(ctx, 0, "Duplicated format \"%s\"\n", outputs[i]);
                    return 1;
                }
            }
            settings.outputs[settings.outputs_num++] = res;
        }
    }

    int nb_track_indices = genopt_nb_vals(opts_list, opts_list_nb, "tracks");
    if (nb_track_indices > 0) {
        settings.rip_indices_count = 0;
        for (int i = 0; i < nb_track_indices; i++) {
            int t = tracks[i];
            for (int k = 0; k < settings.rip_indices_count; k++) {
                if (settings.rip_indices[k] == t) {
                    cyanrip_log(ctx, 0, "Duplicated rip idx %i\n", t);
                    return 1;
                }
            }
            settings.rip_indices[settings.rip_indices_count++] = t;
        }
        qsort(settings.rip_indices, settings.rip_indices_count,
              sizeof(int), cmp_numbers);
    }

    for (int i = 0; i < 198 && pregap[i]; i++) {
        p = av_strtok(pregap[i], "=", &p_save);
        idx = strtol(p, NULL, 10);
        if (idx < 1 || idx > 197) {
            cyanrip_log(ctx, 0, "Invalid track idx for pregap: %i\n", idx);
            return 1;
        }
        enum cyanrip_pregap_action act = CYANRIP_PREGAP_DEFAULT;
        p = av_strtok(NULL, "=", &p_save);
        if (!p) {
            cyanrip_log(ctx, 0, "Missing pregap action\n");
            return 1;
        }
        if      (!strncmp(p, "default", strlen("default"))) act = CYANRIP_PREGAP_DEFAULT;
        else if (!strncmp(p, "drop",    strlen("drop")))    act = CYANRIP_PREGAP_DROP;
        else if (!strncmp(p, "merge",   strlen("merge")))   act = CYANRIP_PREGAP_MERGE;
        else if (!strncmp(p, "track",   strlen("track")))   act = CYANRIP_PREGAP_TRACK;
        else {
            cyanrip_log(ctx, 0, "Invalid pregap action %s\n", p);
            return 1;
        }
        settings.pregap_action[idx - 1] = act;
    }

    for (int i = 0; i < 198 && track_meta[i]; i++)
        track_metadata_ptr[track_metadata_ptr_cnt++] = track_meta[i];

    for (int i = 0; i < 32 && cover[i]; i++) {
        /* Split on the first "=" only, paths may contain them */
        char *next = strchr(cover[i], '=');
        if (next)
            *next++ = '\0';
        p = cover[i];
        CRIPArt *dst = NULL;

        if (!next) {
            int have_front = 0;
            int have_back = 0;
            for (int k = 0; k < nb_cover_arts; k++) {
                if (!strcmp(cover_arts[k].title, "Front")) have_front = 1;
                if (!strcmp(cover_arts[k].title, "Back"))  have_back  = 1;
            }
            if (!have_front) {
                next = p;
                p = "Front";
            } else if (!have_back) {
                next = p;
                p = "Back";
            } else {
                cyanrip_log(ctx, 0,
                            "No cover art location specified for \"%s\"\n", p);
                return 1;
            }
        }

        if (crip_is_integer(p)) {
            idx = strtol(p, NULL, 10);
            if (idx < 0 || idx > 198) {
                cyanrip_log(ctx, 0,
                            "Invalid track idx for cover art: %i\n", idx);
                return 1;
            }
            for (int k = 0; k < nb_track_cover_arts; k++) {
                if (track_cover_arts_map[k] == idx) {
                    cyanrip_log(ctx, 0,
                                "Cover art already specified for track idx %i!\n",
                                idx);
                    return 1;
                }
            }
            track_cover_arts_map[nb_track_cover_arts] = idx;
            dst = &track_cover_arts[nb_track_cover_arts++];
            p = "title";
        } else {
            for (int k = 0; k < nb_cover_arts; k++) {
                if (!strcmp(cover_arts[k].title, p)) {
                    cyanrip_log(ctx, 0, "Cover art \"%s\" already specified!\n", p);
                    return 1;
                }
            }
            dst = &cover_arts[nb_cover_arts++];
            if (nb_cover_arts > 31) {
                cyanrip_log(ctx, 0, "Too many cover arts specified!\n");
                return 1;
            }
        }
        dst->source_url = next;
        dst->title = p;
    }


    if (settings.outputs_num > 1 && !strstr(settings.folder_name_scheme, "{format}")) {
        cyanrip_log(ctx, 0, "Directory name scheme must contain {format} with multiple output formats!\n");
        return 1;
    }

    if (settings.print_info_only && settings.generate_cue_only) {
        cyanrip_log(ctx, 0, "-J (only generate a CUE sheet) cannot be used with -I (only print info)!\n");
        return 1;
    }

    if (settings.generate_cue_only) {
        /* No audio is read or checksummed, and no files to embed art into */
        settings.disable_accurip = 1;
        settings.disable_coverart_db = 1;
    }

    if (find_drive_offset_range) {
        settings.disable_accurip = 0;
        settings.disable_mb = 1;
        settings.disable_coverart_db = 1;
        settings.offset = 0;
        settings.eject_on_success_rip = 0;
        cyanrip_log(ctx, 0, "Searching for drive offset, enabling AccuRip and disabling MusicBrainz and Cover art fetching...\n");
    }

    if (cyanrip_ctx_init(&ctx, &settings))
        return 1;

    if (!settings.offset && !offset_set && !settings.print_info_only &&
        !find_drive_offset_range && (ctx->rcap & CDIO_DRIVE_CAP_READ_ISRC)) {
        cyanrip_log(ctx, 0, "Offset is unset! To continue with an offset of 0, run with -s 0!\n");
        goto end;
    }

    /* Fill disc MCN */
    crip_fill_mcn(ctx);

    /* Fill discid */
    if (crip_fill_discid(ctx)) {
        ctx->total_error_count++;
        goto end;
    }

    /* Default album title */
    av_dict_set(&ctx->meta, "album", "Unknown disc", 0);
    av_dict_set(&ctx->meta, "comment", "cyanrip "PROJECT_VERSION_STRING, 0);
    av_dict_set(&ctx->meta, "media",
                ctx->settings.decode_hdcd ? "HDCD" : "CD", 0);
    const char *barcode_id = dict_get(ctx->meta, "barcode");
    const char *mcn_id = dict_get(ctx->meta, "disc_mcn");
    const char *did_id = dict_get(ctx->meta, "musicbrainz_discid");

    if (barcode_id || mcn_id || did_id) {
        char fourcc_id[5] = { '0', '0', '0', '0', '\0' };
        const char *id = NULL;

        /* Try the barcode first */
        if (barcode_id)
            id = barcode_id;

        /* Try MCN */
        if (!id && mcn_id) {
            /* Check MCN is not all zeroes (this happens often) */
            for (int i = 0; i < strlen(mcn_id); i++) {
                if (mcn_id[i] != '0') {
                    id = mcn_id;
                    break;
                }
            }
        }

        /* Otherwise just grab the discid */
        if (!id)
            id = did_id;

        strncpy(fourcc_id, id, 4);
        for (int i = 0; i < 4; i++)
            fourcc_id[i] = av_toupper(fourcc_id[i]);

        av_dict_set(&ctx->meta, "album", " (", AV_DICT_APPEND);
        av_dict_set(&ctx->meta, "album", fourcc_id, AV_DICT_APPEND);
        av_dict_set(&ctx->meta, "album", ")", AV_DICT_APPEND);
    }

    /* Set default track title */
    for (int i = 0; i < ctx->nb_tracks; i++)
        av_dict_set(&ctx->meta, "title", "Unknown track", 0);

    /* Fill musicbrainz metadata */
    if (crip_fill_metadata(ctx,
                           !!album_metadata_ptr || track_metadata_ptr_cnt,
                           mb_release_idx, mb_release_str, discnumber)) {
        ctx->total_error_count++;
        goto end;
    }

    /* Print this for easy access */
    if (ctx->settings.print_info_only) {
        cyanrip_log(ctx, 0, "MusicBrainz URL:\n%s\n", ctx->mb_submission_url);
        crip_print_qrcode(ctx->mb_submission_url);
    }

    /* Copy album cover arts */
    ctx->nb_cover_arts = nb_cover_arts;
    for (int i = 0; i < nb_cover_arts; i++) {
        ctx->cover_arts[i].source_url = av_strdup(cover_arts[i].source_url);
        av_dict_set(&ctx->cover_arts[i].meta, "title", cover_arts[i].title, 0);
    }

    /* Album cover art (down)loading, and DB quering */
    if (crip_fill_coverart(ctx, ctx->settings.print_info_only) < 0) {
        ctx->total_error_count++;
        goto end;
    }

    /* Fill in accurip data */
    if (crip_fill_accurip(ctx)) {
        ctx->total_error_count++;
        goto end;
    }

    if (find_drive_offset_range) {
        search_for_drive_offset(ctx, find_drive_offset_range);
        goto end;
    }

    if (mb_release_str && !dict_get(ctx->meta, "musicbrainz_albumid"))
        av_dict_set(&ctx->meta, "musicbrainz_albumid", mb_release_str, 0);

    if (discnumber)
        av_dict_set_int(&ctx->meta, "disc", discnumber, 0);

    if (totaldiscs)
        av_dict_set_int(&ctx->meta, "totaldiscs", totaldiscs, 0);

    /* Read user album metadata */
    if (album_metadata_ptr) {
        /* Fixup */
        char *copy = append_missing_keys(album_metadata_ptr, "album=", "album_artist=");

        /* Parse */
        int err = av_dict_parse_string(&ctx->meta, copy, "=", ":", 0);
        av_free(copy);
        if (err) {
            cyanrip_log(ctx, 0, "Error reading album tags: %s\n",
                        av_err2str(err));
            ctx->total_error_count++;
            goto end;
        }

        /* Fixup title tag mistake */
        const char *title = dict_get(ctx->meta, "title");
        const char *album = dict_get(ctx->meta, "album");
        if (title && !album) {
            av_dict_set(&ctx->meta, "album", title, 0);
            av_dict_set(&ctx->meta, "title", "", 0);
        }

        /* Populate artist tag if missing/unspecified */
        const char *album_artist = dict_get(ctx->meta, "album_artist");
        const char *artist = dict_get(ctx->meta, "artist");
        if (album_artist && !artist)
            av_dict_set(&ctx->meta, "artist", album_artist, 0);
        else if (artist && !album_artist)
            av_dict_set(&ctx->meta, "album_artist", artist, 0);
    }

    /* Create log file */
    if (!ctx->settings.print_info_only) {
        if (!ctx->settings.generate_cue_only && cyanrip_log_init(ctx))
            return 1;
        if (cyanrip_cue_init(ctx))
            return 1;
    } else {
        cyanrip_log(ctx, 0, "Log(s) will be written to:\n");
        for (int f = 0; f < ctx->settings.outputs_num; f++) {
            char *logfile = crip_get_path(ctx, CRIP_PATH_LOG, 0,
                                          &crip_fmt_info[ctx->settings.outputs[f]],
                                          NULL);
            cyanrip_log(ctx, 0, "    %s\n", logfile);
            av_free(logfile);
        }
        cyanrip_log(ctx, 0, "CUE files will be written to:\n");
        for (int f = 0; f < ctx->settings.outputs_num; f++) {
            char *cuefile = crip_get_path(ctx, CRIP_PATH_CUE, 0,
                                          &crip_fmt_info[ctx->settings.outputs[f]],
                                          NULL);
            cyanrip_log(ctx, 0, "    %s\n", cuefile);
            av_free(cuefile);
        }
    }

    cyanrip_log_start_report(ctx);
    if (!ctx->settings.print_info_only)
        cyanrip_cue_start(ctx);
    setup_track_offsets_and_report(ctx);

    copy_album_to_track_meta(ctx);

    /* Read user track metadata */
    for (int i = 0; i < track_metadata_ptr_cnt; i++) {
        if (!track_metadata_ptr[i])
            continue;

        char *end = NULL;
        int u_nb = strtol(track_metadata_ptr[i], &end, 10);

        /* Verify all indices */
        int track_idx = 0;
        for (; track_idx < ctx->nb_tracks; track_idx++) {
            if (ctx->tracks[track_idx].number == u_nb)
                break;
        }
        if (track_idx >= ctx->nb_tracks) {
            cyanrip_log(ctx, 0, "Invalid track number %i, list has %i tracks!\n",
                        u_nb, ctx->nb_tracks);
            ctx->total_error_count++;
            goto end;
        }

        end += 1; /* Move past equal sign */

        /* Fixup */
        char *copy = append_missing_keys(end, "title=", "artist=");

        /* Parse */
        int err = av_dict_parse_string(&ctx->tracks[track_idx].meta,
                                       copy, "=", ":", 0);
        av_free(copy);
        if (err) {
            cyanrip_log(ctx, 0, "Error reading track tags: %s\n",
                        av_err2str(err));
            ctx->total_error_count++;
            goto end;
        }
    }

    /* Only generate the CUE sheet, print it, and exit */
    if (ctx->settings.generate_cue_only) {
        for (int i = 0; i < ctx->nb_tracks; i++) {
            cyanrip_track *t = &ctx->tracks[i];
            track_read_extra(ctx, t); /* ISRC and preemphasis flags */
            cyanrip_cue_track(ctx, t);
        }

        /* Print it to the terminal too. The log files were never
         * opened in this mode, so this only goes to stdout. */
        if (ctx->cuefile[0]) {
            char line[4096];
            cyanrip_log(ctx, 0, "\n");
            rewind(ctx->cuefile[0]);
            while (fgets(line, sizeof(line), ctx->cuefile[0]))
                cyanrip_log(ctx, 0, "%s", line);
        }

        goto end;
    }

    /* Copy track cover arts */
    for (int i = 0; i < nb_track_cover_arts; i++) {
        idx = track_cover_arts_map[i];
        int track_idx = 0;
        for (; track_idx < ctx->nb_tracks; track_idx++) {
            if (ctx->tracks[track_idx].number == idx)
                break;
        }
        if (track_idx >= ctx->nb_tracks) {
            cyanrip_log(ctx, 0, "Invalid track number %i, list has %i tracks!\n",
                        idx, ctx->nb_tracks);
            ctx->total_error_count++;
            goto end;
        }
        ctx->tracks[track_idx].art.source_url = av_strdup(track_cover_arts[i].source_url);
        av_dict_set(&ctx->tracks[track_idx].art.meta, "title", "Front", 0);
    }

    /* Track cover art (down)loading */
    if (crip_fill_track_coverart(ctx, ctx->settings.print_info_only) < 0) {
        ctx->total_error_count++;
        goto end;
    }

    /* Write non-track cover arts */
    if (ctx->nb_cover_arts) {
        cyanrip_log(ctx, 0, "Cover art destination(s):\n");
        for (int f = 0; f < ctx->settings.outputs_num; f++) {
            for (int i = 0; i < ctx->nb_cover_arts; i++) {
                char *file = crip_get_path(ctx, CRIP_PATH_COVERART, 0,
                                           &crip_fmt_info[ctx->settings.outputs[f]],
                                           &ctx->cover_arts[i]);
                cyanrip_log(ctx, 0, "    %s\n", file);
                av_free(file);

                if (!ctx->settings.print_info_only) {
                    int err = crip_save_art(ctx, &ctx->cover_arts[i],
                                            &crip_fmt_info[ctx->settings.outputs[f]]);
                    if (err) {
                        ctx->total_error_count++;
                        goto end;
                    }
                }
            }
        }
        cyanrip_log(ctx, 0, "\n");
    }

    /* Warn if the naming scheme sends multiple tracks to the same file */
    if (!ctx->settings.print_info_only) {
        for (int f = 0; f < ctx->settings.outputs_num; f++) {
            const cyanrip_out_fmt *cfmt = &crip_fmt_info[ctx->settings.outputs[f]];
            char **paths = av_calloc(ctx->nb_tracks, sizeof(*paths));
            if (!paths)
                break;
            for (int i = 0; i < ctx->nb_tracks; i++)
                paths[i] = crip_get_path(ctx, CRIP_PATH_TRACK, 0, cfmt,
                                         &ctx->tracks[i]);
            for (int i = 0; i < ctx->nb_tracks; i++)
                for (int j = i + 1; j < ctx->nb_tracks; j++)
                    if (paths[i] && paths[j] && !strcmp(paths[i], paths[j]))
                        cyanrip_log(ctx, 0, "WARNING: tracks %i and %i resolve to "
                                    "the same file \"%s\", one will overwrite "
                                    "the other!\n",
                                    ctx->tracks[i].number, ctx->tracks[j].number,
                                    paths[i]);
            for (int i = 0; i < ctx->nb_tracks; i++)
                av_free(paths[i]);
            av_free(paths);
        }
    }

    cyanrip_log(ctx, 0, "Tracks:\n");
    if (ctx->settings.rip_indices_count == -1) {
        ctx->frames_to_read = ctx->duration_frames;

        if (!ctx->settings.print_info_only)
            cyanrip_initialize_ebur128(ctx);

        for (int i = 0; i < ctx->nb_tracks; i++) {
            cyanrip_track *t = &ctx->tracks[i];
            if (ctx->settings.print_info_only) {
                cyanrip_log(ctx, 0, "Track %i info:\n", t->number);
                track_read_extra(ctx, t);
                cyanrip_log_track_end(ctx, t);

                if (get_media_changed(ctx->cdio)) {
                    cyanrip_log(ctx, 0, "Drive media changed, stopping!\n");
                    break;
                }
            } else {
                /* Initialize, unless the track is data, which is never
                 * decoded or encoded */
                if (!t->track_is_data) {
                    /* Read ISRC and the preemphasis flags before creating
                     * the decoder, which needs them to decide on deemphasis */
                    track_read_extra(ctx, t);

                    int ret = cyanrip_create_dec_ctx(ctx, &t->dec_ctx, t);
                    if (ret < 0) {
                        cyanrip_log(ctx, 0, "Error initializing decoder: %s\n", av_err2str(ret));
                        ctx->total_error_count++;
                        goto end;
                    }

                    for (int j = 0; j < ctx->settings.outputs_num; j++) {
                        ret = cyanrip_init_track_encoding(ctx, &t->enc_ctx[j], t,
                                                          ctx->settings.outputs[j]);
                        if (ret < 0) {
                            cyanrip_log(ctx, 0, "Error initializing encoder: %s\n", av_err2str(ret));
                            ctx->total_error_count++;
                            goto end;
                        }
                    }
                }

                if (cyanrip_rip_track(ctx, t))
                    break;
            }

            if (quit_now)
                break;
        }

        if (!ctx->settings.print_info_only)
            cyanrip_finalize_ebur128(ctx, 1);

        if (ctx->settings.enable_replaygain &&
            !ctx->settings.print_info_only) {
            crip_replaygain_meta_album(ctx);

            /**
             * Writeout tracks.
             */
            for (int i = 0; i < ctx->nb_tracks; i++) {
                cyanrip_track *t = &ctx->tracks[i];

                if (t->track_is_data)
                    continue;

                for (int j = 0; j < ctx->settings.outputs_num; j++) {
                    int ret = cyanrip_writeout_track(ctx, t->enc_ctx[j]);
                    if (ret < 0) {
                        cyanrip_log(ctx, 0, "Error encoding: %s\n", av_err2str(ret));
                        goto end;
                    }
                }

                if (quit_now)
                    break;
            }
        }
    } else {
        for (int i = 0; i < ctx->settings.rip_indices_count; i++) {
            idx = ctx->settings.rip_indices[i];

            /* Verify all indices */
            int j = 0;
            for (; j < ctx->nb_tracks; j++) {
                if (ctx->tracks[j].number == idx)
                    break;
            }
            if (j >= ctx->nb_tracks) {
                cyanrip_log(ctx, 0, "Invalid rip index %i, list has %i tracks!\n",
                            idx, ctx->nb_tracks);
                ctx->total_error_count++;
                goto end;
            }

            ctx->frames_to_read += ctx->tracks[j].frames;
        }

        /**
         * Print-only mode, if requested.
         */
        if (ctx->settings.print_info_only) {
            for (int i = 0; i < ctx->settings.rip_indices_count; i++) {
                idx = ctx->settings.rip_indices[i];

                int j = 0;
                for (; j < ctx->nb_tracks; j++) {
                    if (ctx->tracks[j].number == idx)
                        break;
                }

                cyanrip_track *t = &ctx->tracks[j];

                cyanrip_log(ctx, 0, "Track %i info:\n", t->number);
                track_read_extra(ctx, t);
                cyanrip_log_track_end(ctx, t);

                if (get_media_changed(ctx->cdio)) {
                    cyanrip_log(ctx, 0, "Drive media changed, stopping!\n");
                    break;
                }
            }

            goto end;
        }

        cyanrip_initialize_ebur128(ctx);

        /**
         * Rip tracks.
         */
        for (int i = 0; i < ctx->settings.rip_indices_count; i++) {
            idx = ctx->settings.rip_indices[i];

            int j = 0;
            for (; j < ctx->nb_tracks; j++) {
                if (ctx->tracks[j].number == idx)
                    break;
            }

            cyanrip_track *t = &ctx->tracks[j];

            /* Initialize, unless the track is data, which is never
             * decoded or encoded */
            int ret = 0;
            if (!t->track_is_data) {
                /* Read ISRC and the preemphasis flags before creating
                 * the decoder, which needs them to decide on deemphasis */
                track_read_extra(ctx, t);

                ret = cyanrip_create_dec_ctx(ctx, &t->dec_ctx, t);
                if (ret < 0) {
                    cyanrip_log(ctx, 0, "Error initializing decoder: %s\n", av_err2str(ret));
                    ctx->total_error_count++;
                    goto end;
                }

                for (j = 0; j < ctx->settings.outputs_num; j++) {
                    ret = cyanrip_init_track_encoding(ctx, &t->enc_ctx[j], t,
                                                      ctx->settings.outputs[j]);
                    if (ret < 0) {
                        cyanrip_log(ctx, 0, "Error initializing encoder: %s\n", av_err2str(ret));
                        ctx->total_error_count++;
                        goto end;
                    }
                }
            }

            /* Rip */
            ret = cyanrip_rip_track(ctx, t);
            if (ret < 0) {
                cyanrip_log(ctx, 0, "Error ripping: %s\n", av_err2str(ret));
                goto end;
            }

            if (quit_now)
                break;
        }

        cyanrip_finalize_ebur128(ctx, 1);

        if (ctx->settings.enable_replaygain) {
            crip_replaygain_meta_album(ctx);

            /**
             * Writeout tracks.
             */
            for (int i = 0; i < ctx->settings.rip_indices_count; i++) {
                idx = ctx->settings.rip_indices[i];

                int j = 0;
                for (; j < ctx->nb_tracks; j++) {
                    if (ctx->tracks[j].number == idx)
                        break;
                }

                cyanrip_track *t = &ctx->tracks[j];

                if (t->track_is_data)
                    continue;

                for (j = 0; j < ctx->settings.outputs_num; j++) {
                    int ret = cyanrip_writeout_track(ctx, t->enc_ctx[j]);
                    if (ret < 0) {
                        cyanrip_log(ctx, 0, "Error encoding: %s\n", av_err2str(ret));
                        goto end;
                    }
                }

                if (quit_now)
                    break;
            }
        }
    }

    if (!ctx->settings.print_info_only)
        cyanrip_log_finish_report(ctx);
end:
    /* Wait for the encoders to finish and collect their status */
    for (int i = 0; i < ctx->nb_tracks; i++) {
        cyanrip_track *t = &ctx->tracks[i];
        for (int j = 0; j < ctx->settings.outputs_num; j++)
            if (cyanrip_end_track_encoding(&t->enc_ctx[j]) < 0)
                ctx->total_error_count++;
    }

    cyanrip_log_end(ctx);
    cyanrip_cue_end(ctx);

    int err_cnt = ctx->total_error_count;

    cyanrip_ctx_end(&ctx);

    return !!err_cnt;
}

#ifdef HAVE_WMAIN
int wmain(int argc, wchar_t *argv[])
{
    char *argstr_flat, **win32_argv_utf8 = NULL;
    int i, ret, buffsize = 0, offset = 0;

    /* determine the UTF-8 buffer size (including NULL-termination symbols) */
    for (i = 0; i < argc; i++)
        buffsize += WideCharToMultiByte(CP_UTF8, 0, argv[i], -1,
                                        NULL, 0, NULL, NULL);

    win32_argv_utf8 = av_mallocz(sizeof(char *) * (argc + 1) + buffsize);
    argstr_flat     = (char *)win32_argv_utf8 + sizeof(char *) * (argc + 1);

    for (i = 0; i < argc; i++) {
        win32_argv_utf8[i] = &argstr_flat[offset];
        offset += WideCharToMultiByte(CP_UTF8, 0, argv[i], -1,
                                      &argstr_flat[offset],
                                      buffsize - offset, NULL, NULL);
    }
    win32_argv_utf8[i] = NULL;

    ret = main(argc, win32_argv_utf8);

    av_free(win32_argv_utf8);
    return ret;
}
#endif
