Files
cantata/mpd-interface/cuefile.cpp
2017-04-02 18:43:34 +01:00

406 lines
14 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2016 Craig Drummond <craig.p.drummond@gmail.com>
*
*/
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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 3 of the License, or
(at your option) any later version.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cuefile.h"
#include "mpdconnection.h"
#include "support/utils.h"
#include "gui/settings.h"
#include <QBuffer>
#include <QDateTime>
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QStringBuilder>
#include <QRegExp>
#include <QTextCodec>
#include <QTextStream>
#include <QStringList>
#include <QUrl>
#include <QUrlQuery>
static const QString constFileLineRegExp = QLatin1String("(\\S+)\\s+(?:\"([^\"]+)\"|(\\S+))\\s*(?:\"([^\"]+)\"|(\\S+))?");
static const QString constIndexRegExp = QLatin1String("(\\d{2,3}):(\\d{2}):(\\d{2})");
static const QString constPerformer = QLatin1String("performer");
static const QString constTitle = QLatin1String("title");
static const QString constSongWriter = QLatin1String("songwriter");
static const QString constFile = QLatin1String("file");
static const QString constTrack = QLatin1String("track");
static const QString constDisc = QLatin1String("discnumber");
static const QString constIndex = QLatin1String("index");
static const QString constAudioTrackType = QLatin1String("audio");
static const QString constRem = QLatin1String("rem");
static const QString constGenre = QLatin1String("genre");
static const QString constDate = QLatin1String("date");
static const QString constCueProtocol = QLatin1String("cue:///");
bool CueFile::isCue(const QString &str)
{
return str.startsWith(constCueProtocol);
}
QByteArray CueFile::getLoadLine(const QString &str)
{
QUrl u(str);
QUrlQuery q(u);
if (q.hasQueryItem("pos")) {
QString pos=q.queryItemValue("pos");
QString path=u.path();
if (path.startsWith("/")) {
path=path.mid(1);
}
return MPDConnection::encodeName(path)+" \""+pos.toLatin1()+":"+QString::number(pos.toInt()+1).toLatin1()+"\"";
}
return MPDConnection::encodeName(str);
}
// A single TRACK entry in .cue file.
struct CueEntry {
QString file;
QString index;
int trackNo;
int discNo;
QString title;
QString artist;
QString albumArtist;
QString album;
QString composer;
QString albumComposer;
QString genre;
QString date;
CueEntry(QString &file, QString &index, int trackNo, int discNo, QString &title, QString &artist, QString &albumArtist,
QString &album, QString &composer, QString &albumComposer, QString &genre, QString &date) {
this->file = file;
this->index = index;
this->trackNo = trackNo;
this->discNo = discNo;
this->title = title;
this->artist = artist;
this->albumArtist = albumArtist;
this->album = album;
this->composer = composer;
this->albumComposer = albumComposer;
this->genre = genre;
this->date = date;
}
};
static const qint64 constMsecPerSec = 1000;
static qint64 indexToMarker(const QString& index)
{
QRegExp indexRegexp(constIndexRegExp);
if (!indexRegexp.exactMatch(index)) {
return -1;
}
QStringList splitted = indexRegexp.capturedTexts().mid(1, -1);
qlonglong frames = splitted.at(0).toLongLong() * 60 * 75 + splitted.at(1).toLongLong() * 75 + splitted.at(2).toLongLong();
return (frames * constMsecPerSec) / 75;
}
// This and the constFileLineRegExp do most of the "dirty" work, namely: splitting the raw .cue
// line into logical parts and getting rid of all the unnecessary whitespaces and quoting.
static QStringList splitCueLine(const QString &line)
{
QRegExp lineRegexp(constFileLineRegExp);
if (!lineRegexp.exactMatch(line.trimmed())) {
return QStringList();
}
// let's remove the empty entries while we're at it
return lineRegexp.capturedTexts().filter(QRegExp(".+")).mid(1, -1);
}
// Updates the song with data from the .cue entry. This one mustn't be used for the
// last song in the .cue file.
static bool updateSong(const CueEntry &entry, const QString &nextIndex, Song &song)
{
qint64 beginning = indexToMarker(entry.index);
qint64 end = indexToMarker(nextIndex);
// incorrect indices (we won't be able to calculate beginning or end)
if (-1==beginning || -1==end) {
return false;
}
song.title=entry.title;
song.artist=entry.artist;
song.album=entry.album;
song.albumartist=entry.albumArtist;
song.addGenre(entry.genre);
song.year=entry.date.toInt();
song.time=(end-beginning)/constMsecPerSec;
if (!entry.composer.isEmpty()) {
song.setComposer(entry.composer);
} else if (!entry.albumComposer.isEmpty()) {
song.setComposer(entry.albumComposer);
}
return true;
}
// Updates the song with data from the .cue entry. This one must be used only for the
// last song in the .cue file.
static bool updateLastSong(const CueEntry &entry, Song &song)
{
qint64 beginning = indexToMarker(entry.index);
// incorrect index (we won't be able to calculate beginning)
if (-1==beginning ) {
return false;
}
song.title=entry.title;
song.artist=entry.artist;
song.album=entry.album;
song.albumartist=entry.albumArtist;
song.addGenre(entry.genre);
song.year=entry.date.toInt();
if (!entry.composer.isEmpty()) {
song.setComposer(entry.composer);
} else if (!entry.albumComposer.isEmpty()) {
song.setComposer(entry.albumComposer);
}
return true;
}
// Get list of text codecs used to decode CUE files...
static const QList<QTextCodec *> & codecList()
{
static QList<QTextCodec *> codecs;
if (codecs.isEmpty()) {
codecs.append(QTextCodec::codecForName("UTF-8"));
QTextCodec *codec=QTextCodec::codecForLocale();
if (codec && !codecs.contains(codec)) {
codecs.append(codec);
}
codec=QTextCodec::codecForName("System");
if (codec && !codecs.contains(codec)) {
codecs.append(codec);
}
QStringList configCodecs=Settings::self()->cueFileCodecs();
foreach (const QString &cfg, configCodecs) {
codec=QTextCodec::codecForName(cfg.toLatin1());
if (codec && !codecs.contains(codec)) {
codecs.append(codec);
}
}
}
return codecs;
}
bool CueFile::parse(const QString &fileName, const QString &dir, QList<Song> &songList, QSet<QString> &files)
{
QScopedPointer<QTextStream> textStream;
QString decoded;
QFile f(dir+fileName);
if (f.open(QIODevice::ReadOnly)) {
// First attempt to use QTextDecoder to decode cue file contents into a QString
QByteArray contents=f.readAll();
foreach (QTextCodec *codec, codecList()) {
QTextDecoder decoder(codec);
decoded=decoder.toUnicode(contents);
if (!decoder.hasFailure()) {
textStream.reset(new QTextStream(&decoded, QIODevice::ReadOnly));
break;
}
}
f.close();
if (!textStream) {
decoded.clear();
// Failed to use text decoders, fall back to old method...
f.open(QIODevice::ReadOnly|QIODevice::Text);
textStream.reset(new QTextStream(&f));
textStream->setCodec(QTextCodec::codecForUtfText(f.peek(1024), QTextCodec::codecForName("UTF-8")));
}
}
if (!textStream) {
return false;
}
// read the first line already
QString line = textStream->readLine();
QList<CueEntry> entries;
QString fileDir=fileName.contains("/") ? Utils::getDir(fileName) : QString();
// -- whole file
while (!textStream->atEnd()) {
QString albumArtist;
QString album;
QString albumComposer;
QString file;
QString fileType;
QString genre;
QString date;
int disc = 0;
// -- FILE section
do {
QStringList splitted = splitCueLine(line);
// uninteresting or incorrect line
if (splitted.size() < 2) {
continue;
}
QString lineName = splitted[0].toLower();
QString lineValue = splitted[1];
if (lineName == constPerformer) {
albumArtist = lineValue;
} else if (lineName == constTitle) {
album = lineValue;
} else if (lineName == constSongWriter) {
albumComposer = lineValue;
} else if (lineName == constFile) {
file = lineValue;
if (splitted.size() > 2) {
fileType = splitted[2];
}
} else if (lineName == constRem) {
if (splitted.size() < 3) {
break;
}
if (lineValue.toLower() == constGenre) {
genre = splitted[2];
} else if (lineValue.toLower() == constDate) {
date = splitted[2];
} else if (lineValue.toLower() == constDisc) {
disc = splitted[2].toInt();
}
// end of the header -> go into the track mode
} else if (lineName == constTrack) {
break;
}
// just ignore the rest of possible field types for now...
} while (!(line = textStream->readLine()).isNull());
if (line.isNull()) {
return false;
}
// if this is a data file, all of it's tracks will be ignored
bool validFile = fileType.compare("BINARY", Qt::CaseInsensitive) && fileType.compare("MOTOROLA", Qt::CaseInsensitive);
QString trackType;
QString index;
QString artist;
QString composer;
QString title;
int trackNo=0;
// TRACK section
do {
QStringList splitted = splitCueLine(line);
// uninteresting or incorrect line
if (splitted.size() < 2) {
continue;
}
QString lineName = splitted[0].toLower();
QString lineValue = splitted[1];
QString lineAdditional = splitted.size() > 2 ? splitted[2].toLower() : QString();
if (lineName == constTrack) {
// the beginning of another track's definition - we're saving the current one
// for later (if it's valid of course)
// please note that the same code is repeated just after this 'do-while' loop
if (validFile && !index.isEmpty() && (trackType.isEmpty() || trackType == constAudioTrackType)) {
entries.append(CueEntry(file, index, trackNo, disc, title, artist, albumArtist, album, composer, albumComposer, genre, date));
}
// clear the state
trackType = index = artist = title = QString();
if (!lineAdditional.isEmpty()) {
trackType = lineAdditional;
}
trackNo = lineValue.toInt();
} else if (lineName == constIndex) {
// we need the index's position field
if (!lineAdditional.isEmpty()) {
// if there's none "01" index, we'll just take the first one
// also, we'll take the "01" index even if it's the last one
if (QLatin1String("01")==lineValue || index.isEmpty()) {
index = lineAdditional;
}
}
} else if (lineName == constPerformer) {
artist = lineValue;
} else if (lineName == constTitle) {
title = lineValue;
} else if (lineName == constSongWriter) {
composer = lineValue;
// end of track's for the current file -> parse next one
} else if (lineName == constFile) {
break;
}
// just ignore the rest of possible field types for now...
} while (!(line = textStream->readLine()).isNull());
// we didn't add the last song yet...
if (validFile && !index.isEmpty() && (trackType.isEmpty() || trackType == constAudioTrackType)) {
entries.append(CueEntry(file, index, trackNo, disc, title, artist, albumArtist, album, composer, albumComposer, genre, date));
}
}
// finalize parsing songs
for(int i = 0; i < entries.length(); i++) {
CueEntry entry = entries.at(i);
Song song;
song.file=constCueProtocol+fileName+"?pos="+QString::number(i);
song.track=entry.trackNo;
song.disc=entry.discNo;
QString songFile=fileDir+entry.file;
song.setName(songFile); // HACK!!!
if (!files.contains(songFile)) {
files.insert(songFile);
}
// the last TRACK for every FILE gets it's 'end' marker from the media file's
// length
if (i+1 < entries.size() && entries.at(i).file == entries.at(i+1).file) {
// incorrect indices?
if (!updateSong(entry, entries.at(i+1).index, song)) {
continue;
}
} else {
// incorrect index?
if (!updateLastSong(entry, song)) {
continue;
}
}
songList.append(song);
}
return true;
}