mirror of
https://github.com/clementine-player/Clementine
synced 2025-01-31 03:27:40 +01:00
an initial version of .cue sheets parser
initial support for multipart media files in Song ('beginning' and 'end' fields)
This commit is contained in:
parent
cd4ecc1f8e
commit
d7fe4600b8
@ -116,6 +116,7 @@ set(SOURCES
|
||||
|
||||
playlistparsers/asxparser.cpp
|
||||
playlistparsers/asxiniparser.cpp
|
||||
playlistparsers/cueparser.cpp
|
||||
playlistparsers/m3uparser.cpp
|
||||
playlistparsers/parserbase.cpp
|
||||
playlistparsers/playlistparser.cpp
|
||||
@ -279,6 +280,7 @@ set(HEADERS
|
||||
|
||||
playlistparsers/asxparser.h
|
||||
playlistparsers/asxiniparser.h
|
||||
playlistparsers/cueparser.h
|
||||
playlistparsers/m3uparser.h
|
||||
playlistparsers/parserbase.h
|
||||
playlistparsers/playlistparser.h
|
||||
|
@ -158,7 +158,8 @@ Song::Private::Private()
|
||||
skipcount_(0),
|
||||
lastplayed_(-1),
|
||||
score_(0),
|
||||
length_(-1),
|
||||
beginning_(0),
|
||||
end_(-1),
|
||||
bitrate_(-1),
|
||||
samplerate_(-1),
|
||||
directory_id_(-1),
|
||||
@ -193,10 +194,23 @@ Song::Song(FileRefFactory* factory)
|
||||
|
||||
void Song::Init(const QString& title, const QString& artist, const QString& album, int length) {
|
||||
d->valid_ = true;
|
||||
|
||||
d->title_ = title;
|
||||
d->artist_ = artist;
|
||||
d->album_ = album;
|
||||
d->length_ = length;
|
||||
|
||||
set_length(length);
|
||||
}
|
||||
|
||||
void Song::Init(const QString& title, const QString& artist, const QString& album, int beginning, int end) {
|
||||
d->valid_ = true;
|
||||
|
||||
d->title_ = title;
|
||||
d->artist_ = artist;
|
||||
d->album_ = album;
|
||||
|
||||
d->beginning_ = beginning;
|
||||
d->end_ = end;
|
||||
}
|
||||
|
||||
void Song::set_genre(int id) {
|
||||
@ -339,8 +353,8 @@ void Song::InitFromFile(const QString& filename, int directory_id) {
|
||||
|
||||
if (fileref->audioProperties()) {
|
||||
d->bitrate_ = fileref->audioProperties()->bitrate();
|
||||
d->length_ = fileref->audioProperties()->length();
|
||||
d->samplerate_ = fileref->audioProperties()->sampleRate();
|
||||
set_length(fileref->audioProperties()->length());
|
||||
}
|
||||
|
||||
// Get the filetype if we can
|
||||
@ -452,7 +466,8 @@ void Song::InitFromQuery(const SqlRow& q, int col) {
|
||||
d->comment_ = tostr(col + 11);
|
||||
d->compilation_ = q.value(col + 12).toBool();
|
||||
|
||||
d->length_ = toint(col + 13);
|
||||
// TODO: this should be replaced by beginning and end
|
||||
set_length(toint(col + 13));
|
||||
d->bitrate_ = toint(col + 14);
|
||||
d->samplerate_ = toint(col + 15);
|
||||
|
||||
@ -496,7 +511,8 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
d->album_ = track.album();
|
||||
d->artist_ = track.artist();
|
||||
d->track_ = track.trackNumber();
|
||||
d->length_ = track.duration();
|
||||
|
||||
set_length(track.duration());
|
||||
}
|
||||
#endif // HAVE_LIBLASTFM
|
||||
|
||||
@ -516,7 +532,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
d->genre_ = QString::fromUtf8(track->genre);
|
||||
d->comment_ = QString::fromUtf8(track->comment);
|
||||
d->compilation_ = track->compilation;
|
||||
d->length_ = track->tracklen / 1000;
|
||||
set_length(track->tracklen / 1000);
|
||||
d->bitrate_ = track->bitrate;
|
||||
d->samplerate_ = track->samplerate;
|
||||
d->mtime_ = track->time_modified;
|
||||
@ -546,7 +562,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
track->genre = strdup(d->genre_.toUtf8().constData());
|
||||
track->comment = strdup(d->comment_.toUtf8().constData());
|
||||
track->compilation = d->compilation_;
|
||||
track->tracklen = d->length_ * 1000;
|
||||
track->tracklen = length() * 1000;
|
||||
track->bitrate = d->bitrate_;
|
||||
track->samplerate = d->samplerate_;
|
||||
track->time_modified = d->mtime_;
|
||||
@ -575,7 +591,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
d->basefilename_ = d->filename_;
|
||||
|
||||
d->track_ = track->tracknumber;
|
||||
d->length_ = track->duration / 1000;
|
||||
set_length(track->duration / 1000);
|
||||
d->samplerate_ = track->samplerate;
|
||||
d->bitrate_ = track->bitrate;
|
||||
d->filesize_ = track->filesize;
|
||||
@ -615,7 +631,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
track->filename = strdup(d->basefilename_.toUtf8().constData());
|
||||
|
||||
track->tracknumber = d->track_;
|
||||
track->duration = d->length_ * 1000;
|
||||
track->duration = length() * 1000;
|
||||
track->samplerate = d->samplerate_;
|
||||
track->nochannels = 0;
|
||||
track->wavecodec = 0;
|
||||
@ -749,7 +765,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
d->filename_ = item_value.toString();
|
||||
|
||||
else if (wcscmp(name, g_wszWMDMDuration) == 0)
|
||||
d->length_ = item_value.toULongLong() / 10000000ll;
|
||||
set_length(item_value.toULongLong() / 10000000ll);
|
||||
|
||||
else if (wcscmp(name, L"WMDM/FileSize") == 0)
|
||||
d->filesize_ = item_value.toULongLong();
|
||||
@ -813,7 +829,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
if (!d->title_.isEmpty() || !d->artist_.isEmpty() ||
|
||||
!d->album_.isEmpty() || !d->comment_.isEmpty() ||
|
||||
!d->genre_.isEmpty() || d->track_ != -1 || d->year_ != -1 ||
|
||||
d->length_ != -1) {
|
||||
length() != -1) {
|
||||
d->filetype_ = Song::Type_Unknown;
|
||||
break;
|
||||
}
|
||||
@ -847,7 +863,7 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
|
||||
AddWmdmItem(metadata, g_wszWMDMComposer, d->composer_);
|
||||
AddWmdmItem(metadata, g_wszWMDMBitrate, d->bitrate_);
|
||||
AddWmdmItem(metadata, g_wszWMDMFileName, d->basefilename_);
|
||||
AddWmdmItem(metadata, g_wszWMDMDuration, qint64(d->length_) * 10000000ll);
|
||||
AddWmdmItem(metadata, g_wszWMDMDuration, qint64(length()) * 10000000ll);
|
||||
AddWmdmItem(metadata, L"WMDM/FileSize", d->filesize_);
|
||||
|
||||
WMDM_FORMATCODE format;
|
||||
@ -885,7 +901,7 @@ void Song::MergeFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle) {
|
||||
if (!bundle.genre.isEmpty()) d->genre_ = Decode(bundle.genre, codec);
|
||||
if (!bundle.bitrate.isEmpty()) d->bitrate_ = bundle.bitrate.toInt();
|
||||
if (!bundle.samplerate.isEmpty()) d->samplerate_ = bundle.samplerate.toInt();
|
||||
if (!bundle.length.isEmpty()) d->length_ = bundle.length.toInt();
|
||||
if (!bundle.length.isEmpty()) set_length(bundle.length.toInt());
|
||||
if (!bundle.year.isEmpty()) d->year_ = bundle.year.toInt();
|
||||
if (!bundle.tracknr.isEmpty()) d->track_ = bundle.tracknr.toInt();
|
||||
}
|
||||
@ -910,7 +926,8 @@ void Song::BindToQuery(QSqlQuery *query) const {
|
||||
query->bindValue(":comment", strval(d->comment_));
|
||||
query->bindValue(":compilation", d->compilation_ ? 1 : 0);
|
||||
|
||||
query->bindValue(":length", intval(d->length_));
|
||||
// TODO: replace this with beginning and end
|
||||
query->bindValue(":length", intval(length()));
|
||||
query->bindValue(":bitrate", intval(d->bitrate_));
|
||||
query->bindValue(":samplerate", intval(d->samplerate_));
|
||||
|
||||
@ -959,7 +976,7 @@ void Song::ToLastFM(lastfm::Track* track) const {
|
||||
mtrack.setArtist(d->artist_);
|
||||
mtrack.setAlbum(d->album_);
|
||||
mtrack.setTitle(d->title_);
|
||||
mtrack.setDuration(d->length_);
|
||||
mtrack.setDuration(length());
|
||||
mtrack.setTrackNumber(d->track_);
|
||||
mtrack.setSource(lastfm::Track::Player);
|
||||
}
|
||||
@ -987,10 +1004,10 @@ QString Song::PrettyTitleWithArtist() const {
|
||||
}
|
||||
|
||||
QString Song::PrettyLength() const {
|
||||
if (d->length_ == -1)
|
||||
if (length() == -1)
|
||||
return QString::null;
|
||||
|
||||
return Utilities::PrettyTime(d->length_);
|
||||
return Utilities::PrettyTime(length());
|
||||
}
|
||||
|
||||
QString Song::PrettyYear() const {
|
||||
@ -1025,7 +1042,8 @@ bool Song::IsMetadataEqual(const Song& other) const {
|
||||
d->genre_ == other.d->genre_ &&
|
||||
d->comment_ == other.d->comment_ &&
|
||||
d->compilation_ == other.d->compilation_ &&
|
||||
d->length_ == other.d->length_ &&
|
||||
// this should be replaced by beginning and end
|
||||
length() == other.length() &&
|
||||
d->bitrate_ == other.d->bitrate_ &&
|
||||
d->samplerate_ == other.d->samplerate_ &&
|
||||
d->sampler_ == other.d->sampler_ &&
|
||||
|
@ -112,6 +112,7 @@ class Song {
|
||||
|
||||
// Constructors
|
||||
void Init(const QString& title, const QString& artist, const QString& album, int length);
|
||||
void Init(const QString& title, const QString& artist, const QString& album, int beginning, int end);
|
||||
void InitFromFile(const QString& filename, int directory_id);
|
||||
void InitFromQuery(const SqlRow& query, int col = 0);
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
@ -170,7 +171,11 @@ class Song {
|
||||
int lastplayed() const { return d->lastplayed_; }
|
||||
int score() const { return d->score_; }
|
||||
|
||||
int length() const { return d->length_; }
|
||||
int beginning() const { return d->beginning_; }
|
||||
int end() const { return d->end_; }
|
||||
|
||||
int length() const { return d->end_ - d->beginning_; }
|
||||
|
||||
int bitrate() const { return d->bitrate_; }
|
||||
int samplerate() const { return d->samplerate_; }
|
||||
|
||||
@ -217,7 +222,9 @@ class Song {
|
||||
void set_comment(const QString& v) { d->comment_ = v; }
|
||||
void set_compilation(bool v) { d->compilation_ = v; }
|
||||
void set_sampler(bool v) { d->sampler_ = v; }
|
||||
void set_length(int v) { d->length_ = v; }
|
||||
void set_beginning(int v) { d->beginning_ = v; }
|
||||
void set_end(int v) { d->end_ = v; }
|
||||
void set_length(int v) { d->end_ = d->beginning_ + v; }
|
||||
void set_bitrate(int v) { d->bitrate_ = v; }
|
||||
void set_samplerate(int v) { d->samplerate_ = v; }
|
||||
void set_mtime(int v) { d->mtime_ = v; }
|
||||
@ -281,7 +288,17 @@ class Song {
|
||||
int lastplayed_;
|
||||
int score_;
|
||||
|
||||
int length_; // Seconds.
|
||||
// The beginning of the song in seconds. In case of single-part media
|
||||
// streams, this will equal to 0. In case of multi-part streams on the
|
||||
// other hand, this will mark the beginning of a section represented by
|
||||
// this Song object.
|
||||
int beginning_;
|
||||
// The end of the song in seconds. In case of single-part media
|
||||
// streams, this will equal to the song's length. In case of multi-part
|
||||
// streams on the other hand, this will mark the end of a section
|
||||
// represented by this Song object.
|
||||
int end_;
|
||||
|
||||
int bitrate_;
|
||||
int samplerate_;
|
||||
|
||||
|
289
src/playlistparsers/cueparser.cpp
Normal file
289
src/playlistparsers/cueparser.cpp
Normal file
@ -0,0 +1,289 @@
|
||||
/* 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 "cueparser.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QStringBuilder>
|
||||
#include <QRegExp>
|
||||
#include <QTextStream>
|
||||
#include <QtDebug>
|
||||
|
||||
const char* CueParser::kFileLineRegExp = "([^ \t\r\n]+)\\s+(?:\"([^\"]+)\"|([^ \t\r\n]+))\\s*(?:\"([^\"]+)\"|([^ \t\r\n]+))?";
|
||||
const char* CueParser::kIndexRegExp = "(\\d{2}):(\\d{2}):(\\d{2})";
|
||||
|
||||
const char* CueParser::kPerformer = "performer";
|
||||
const char* CueParser::kTitle = "title";
|
||||
const char* CueParser::kFile = "file";
|
||||
const char* CueParser::kTrack = "track";
|
||||
const char* CueParser::kIndex = "index";
|
||||
const char* CueParser::kAudioTrackType = "audio";
|
||||
|
||||
// TODO: if some song misses it's next one (because the next one was somehow
|
||||
// broken), we need to discard the song too (can't really determine where it
|
||||
// ends
|
||||
// TODO: utf and regexps (check on Zucchero - there's something wrong)
|
||||
|
||||
CueParser::CueParser(LibraryBackendInterface* library, QObject* parent)
|
||||
: ParserBase(library, parent)
|
||||
{
|
||||
}
|
||||
|
||||
SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
|
||||
SongList ret;
|
||||
|
||||
QTextStream text_stream(device);
|
||||
QString dir_path = dir.absolutePath();
|
||||
|
||||
QString line;
|
||||
|
||||
QString album_artist;
|
||||
QString album;
|
||||
QString file;
|
||||
|
||||
// header
|
||||
while (!(line = text_stream.readLine()).isNull()) {
|
||||
QStringList splitted = SplitCueLine(line);
|
||||
|
||||
// uninteresting or incorrect line
|
||||
if(splitted.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString line_name = splitted[0].toLower();
|
||||
QString line_value = splitted[1];
|
||||
|
||||
// PERFORMER
|
||||
if(line_name == kPerformer) {
|
||||
|
||||
album_artist = line_value;
|
||||
|
||||
// TITLE
|
||||
} else if(line_name == kTitle) {
|
||||
|
||||
album = line_value;
|
||||
|
||||
// FILE
|
||||
} else if(line_name == kFile) {
|
||||
|
||||
file = QDir::isAbsolutePath(line_value)
|
||||
? line_value
|
||||
: dir.absoluteFilePath(line_value);
|
||||
|
||||
// end of the header -> go into the track mode
|
||||
} else if(line_name == kTrack) {
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// just ignore the rest of possible field types for now...
|
||||
}
|
||||
|
||||
if(line.isNull()) {
|
||||
qWarning() << "the .cue file from " << dir_path << " defines no tracks!";
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString track_type;
|
||||
QString index;
|
||||
QString artist;
|
||||
QString title;
|
||||
|
||||
QList<CueEntry> entries;
|
||||
|
||||
// tracks
|
||||
do {
|
||||
QStringList splitted = SplitCueLine(line);
|
||||
|
||||
// uninteresting or incorrect line
|
||||
if(splitted.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString line_name = splitted[0].toLower();
|
||||
QString line_value = splitted[1];
|
||||
QString line_additional = splitted.size() > 2 ? splitted[2].toLower() : "";
|
||||
|
||||
if(line_name == kTrack) {
|
||||
|
||||
// the beginning of another track's definition - we're saving the current one
|
||||
// for later (if it's valid of course)
|
||||
if(!index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) {
|
||||
entries.append(CueEntry(file, index, title, artist, album_artist, album));
|
||||
}
|
||||
|
||||
// clear the state
|
||||
track_type = index = artist = title = "";
|
||||
|
||||
if(!line_additional.isEmpty()) {
|
||||
track_type = line_additional;
|
||||
}
|
||||
|
||||
} else if(line_name == kIndex) {
|
||||
|
||||
// we need the index's position field
|
||||
if(!line_additional.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(line_value == "01" || index.isEmpty()) {
|
||||
|
||||
index = line_additional;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else if(line_name == kPerformer) {
|
||||
|
||||
artist = line_value;
|
||||
|
||||
} else if(line_name == kTitle) {
|
||||
|
||||
title = line_value;
|
||||
|
||||
}
|
||||
|
||||
// just ignore the rest of possible field types for now...
|
||||
} while(!(line = text_stream.readLine()).isNull());
|
||||
|
||||
// we didn't add the last song yet...
|
||||
if(!index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) {
|
||||
entries.append(CueEntry(file, index, title, artist, album_artist, album));
|
||||
}
|
||||
|
||||
// finalize parsing songs
|
||||
for(int i = 0; i < entries.length(); i++) {
|
||||
CueEntry entry = entries.at(i);
|
||||
|
||||
Song current;
|
||||
if (!ParseTrackLocation(entry.file, dir, ¤t)) {
|
||||
qWarning() << "failed to parse location in .cue file from " << dir_path;
|
||||
|
||||
} else {
|
||||
// overwrite the stuff, we may have read from the file, using
|
||||
// the .cue's metadata
|
||||
if(i + 1 < entries.size()) {
|
||||
// incorrect indices?
|
||||
if(!UpdateSong(entry, entries.at(i + 1).index, ¤t)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// incorrect index?
|
||||
if(!UpdateLastSong(entry, ¤t)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Song final;
|
||||
// TODO: make this work
|
||||
// load this song from the library if it's there
|
||||
// Song final = LoadLibrarySong(current.filename());
|
||||
|
||||
Song to_add = final.is_valid() ? final : current;
|
||||
|
||||
ret << to_add;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// This and the kFileLineRegExp 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.
|
||||
// This also validates the input: if returned list has less than two positions, the given
|
||||
// line should be considered irrelevant.
|
||||
QStringList CueParser::SplitCueLine(const QString& line) const {
|
||||
QRegExp line_regexp(kFileLineRegExp);
|
||||
if(!line_regexp.exactMatch(line.trimmed())) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
// let's remove the empty entries while we're at it
|
||||
return line_regexp.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.
|
||||
bool CueParser::UpdateSong(const CueEntry& entry, const QString& next_index, Song* song) const {
|
||||
int beginning = IndexToMarker(entry.index);
|
||||
int end = IndexToMarker(next_index);
|
||||
|
||||
// incorrect indices (we won't be able to calculate beginning or end)
|
||||
if(beginning == -1 || end == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
song->Init(entry.title, entry.PrettyArtist(),
|
||||
entry.album, beginning, end);
|
||||
|
||||
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.
|
||||
bool CueParser::UpdateLastSong(const CueEntry& entry, Song* song) const {
|
||||
int beginning = IndexToMarker(entry.index);
|
||||
|
||||
// incorrect index (we won't be able to calculate beginning)
|
||||
if(beginning == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
song->set_title(entry.title);
|
||||
song->set_artist(entry.PrettyArtist());
|
||||
song->set_album(entry.album);
|
||||
|
||||
// we don't do anything with the end here because it's already set to
|
||||
// the end of the media file (if it exists)
|
||||
song->set_beginning(beginning);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int CueParser::IndexToMarker(const QString& index) const {
|
||||
QRegExp index_regexp(kIndexRegExp);
|
||||
if(!index_regexp.exactMatch(index)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
QStringList splitted = index_regexp.capturedTexts().mid(1, -1);
|
||||
// TODO: use frames when #1166 is fixed
|
||||
return splitted.at(0).toInt() * 60 + splitted.at(1).toInt();
|
||||
}
|
||||
|
||||
void CueParser::Save(const SongList &songs, QIODevice *device, const QDir &dir) const {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Looks for a track starting with one of the .cue's keywords.
|
||||
bool CueParser::TryMagic(const QByteArray &data) const {
|
||||
QStringList splitted = QString::fromUtf8(data.constData()).split('\n');
|
||||
|
||||
for(int i = 0; i < splitted.length(); i++) {
|
||||
QString line = splitted.at(i).trimmed();
|
||||
if(line.startsWith(kPerformer, Qt::CaseInsensitive) ||
|
||||
line.startsWith(kTitle, Qt::CaseInsensitive) ||
|
||||
line.startsWith(kFile, Qt::CaseInsensitive) ||
|
||||
line.startsWith(kTrack, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
82
src/playlistparsers/cueparser.h
Normal file
82
src/playlistparsers/cueparser.h
Normal file
@ -0,0 +1,82 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef CUEPARSER_H
|
||||
#define CUEPARSER_H
|
||||
|
||||
#include "parserbase.h"
|
||||
|
||||
#include <QRegExp>
|
||||
|
||||
class CueParser : public ParserBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static const char* kFileLineRegExp;
|
||||
static const char* kIndexRegExp;
|
||||
|
||||
static const char* kPerformer;
|
||||
static const char* kTitle;
|
||||
static const char* kFile;
|
||||
static const char* kTrack;
|
||||
static const char* kIndex;
|
||||
static const char* kAudioTrackType;
|
||||
|
||||
CueParser(LibraryBackendInterface* library, QObject* parent = 0);
|
||||
|
||||
QString name() const { return "CUE"; }
|
||||
QStringList file_extensions() const { return QStringList() << "cue"; }
|
||||
QString mime_type() const { return "application/x-cue"; }
|
||||
|
||||
bool TryMagic(const QByteArray& data) const;
|
||||
|
||||
SongList Load(QIODevice* device, const QDir& dir = QDir()) const;
|
||||
void Save(const SongList& songs, QIODevice* device, const QDir& dir = QDir()) const;
|
||||
|
||||
private:
|
||||
// A single TRACK entry in .cue file.
|
||||
struct CueEntry {
|
||||
QString file;
|
||||
|
||||
QString index;
|
||||
|
||||
QString title;
|
||||
QString artist;
|
||||
QString album_artist;
|
||||
QString album;
|
||||
|
||||
QString PrettyArtist() const { return artist.isEmpty() ? album_artist : artist; }
|
||||
|
||||
CueEntry(QString& file, QString& index, QString& title, QString& artist,
|
||||
QString& album_artist, QString& album) {
|
||||
this->file = file;
|
||||
this->index = index;
|
||||
this->title = title;
|
||||
this->artist = artist;
|
||||
this->album_artist = album_artist;
|
||||
this->album = album;
|
||||
}
|
||||
};
|
||||
|
||||
bool UpdateSong(const CueEntry& entry, const QString& next_index, Song* song) const;
|
||||
bool UpdateLastSong(const CueEntry& entry, Song* song) const;
|
||||
|
||||
QStringList SplitCueLine(const QString& line) const;
|
||||
int IndexToMarker(const QString& index) const;
|
||||
};
|
||||
|
||||
#endif // CUEPARSER_H
|
@ -19,6 +19,7 @@
|
||||
#include "xspfparser.h"
|
||||
#include "m3uparser.h"
|
||||
#include "plsparser.h"
|
||||
#include "cueparser.h"
|
||||
#include "asxparser.h"
|
||||
#include "asxiniparser.h"
|
||||
|
||||
@ -35,6 +36,7 @@ PlaylistParser::PlaylistParser(LibraryBackendInterface* library, QObject *parent
|
||||
parsers_ << new PLSParser(library, this);
|
||||
parsers_ << new ASXParser(library, this);
|
||||
parsers_ << new AsxIniParser(library, this);
|
||||
parsers_ << new CueParser(library, this);
|
||||
}
|
||||
|
||||
QStringList PlaylistParser::file_extensions() const {
|
||||
|
@ -98,6 +98,7 @@ endmacro (add_test_file)
|
||||
add_test_file(albumcovermanager_test.cpp true)
|
||||
add_test_file(asxparser_test.cpp false)
|
||||
add_test_file(asxiniparser_test.cpp false)
|
||||
add_test_file(cueparser_test.cpp false)
|
||||
add_test_file(database_test.cpp false)
|
||||
add_test_file(fileformats_test.cpp false)
|
||||
add_test_file(fmpsparser_test.cpp false)
|
||||
|
79
tests/cueparser_test.cpp
Normal file
79
tests/cueparser_test.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
/* 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 "gmock/gmock-matchers.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "test_utils.h"
|
||||
#include "mock_taglib.h"
|
||||
|
||||
#include "playlistparsers/cueparser.h"
|
||||
|
||||
class CueParserTest : public ::testing::Test {
|
||||
protected:
|
||||
static void SetUpTestCase() {
|
||||
testing::DefaultValue<TagLib::String>::Set("foobarbaz");
|
||||
}
|
||||
|
||||
CueParserTest()
|
||||
: parser_(NULL) {
|
||||
}
|
||||
|
||||
CueParser parser_;
|
||||
MockFileRefFactory taglib_;
|
||||
};
|
||||
|
||||
TEST_F(CueParserTest, ParsesASong) {
|
||||
QFile file(":testdata/onesong.cue");
|
||||
file.open(QIODevice::ReadOnly);
|
||||
|
||||
SongList song_list = parser_.Load(&file, QDir(""));
|
||||
|
||||
// one song
|
||||
ASSERT_EQ(1, song_list.size());
|
||||
|
||||
// with the specified metadata
|
||||
Song first_song = song_list.at(0);
|
||||
ASSERT_EQ("Un soffio caldo", first_song.title());
|
||||
ASSERT_EQ("Zucchero", first_song.artist());
|
||||
ASSERT_EQ("", first_song.album());
|
||||
ASSERT_EQ(1, first_song.beginning());
|
||||
}
|
||||
|
||||
TEST_F(CueParserTest, ParsesTwoSongs) {
|
||||
QFile file(":testdata/twosongs.cue");
|
||||
file.open(QIODevice::ReadOnly);
|
||||
|
||||
SongList song_list = parser_.Load(&file, QDir(""));
|
||||
|
||||
// two songs
|
||||
ASSERT_EQ(2, song_list.size());
|
||||
|
||||
// with the specified metadata
|
||||
Song first_song = song_list.at(0);
|
||||
ASSERT_EQ("Un soffio caldo", first_song.title());
|
||||
ASSERT_EQ("Chocabeck", first_song.album());
|
||||
ASSERT_EQ("Zucchero himself", first_song.artist());
|
||||
ASSERT_EQ(1, first_song.beginning());
|
||||
ASSERT_EQ((5 * 60 + 3) - 1, first_song.length());
|
||||
|
||||
Song second_song = song_list.at(1);
|
||||
ASSERT_EQ("Somewon Else's Tears", second_song.title());
|
||||
ASSERT_EQ("Chocabeck", second_song.album());
|
||||
ASSERT_EQ("Zucchero himself", second_song.artist());
|
||||
ASSERT_EQ(5 * 60 + 3, second_song.beginning());
|
||||
}
|
6
tests/data/onesong.cue
Normal file
6
tests/data/onesong.cue
Normal file
@ -0,0 +1,6 @@
|
||||
PERFORMER "Zucchero"
|
||||
FILE "file.mp3" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Un soffio caldo"
|
||||
PERFORMER Zucchero
|
||||
INDEX 01 00:01:00
|
@ -7,6 +7,8 @@
|
||||
<file>beep.wav</file>
|
||||
<file>beep.wma</file>
|
||||
<file>beep.m4a</file>
|
||||
<file>onesong.cue</file>
|
||||
<file>twosongs.cue</file>
|
||||
<file>pls_one.pls</file>
|
||||
<file>pls_somafm.pls</file>
|
||||
<file>test.m3u</file>
|
||||
|
9
tests/data/twosongs.cue
Normal file
9
tests/data/twosongs.cue
Normal file
@ -0,0 +1,9 @@
|
||||
PERFORMER "Zucchero himself"
|
||||
TITLE "Chocabeck"
|
||||
FILE files/longer.mp3 WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Un soffio caldo"
|
||||
INDEX 01 00:01:00
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Somewon Else's Tears"
|
||||
INDEX 01 05:03:68
|
Loading…
x
Reference in New Issue
Block a user