better .cue spec conformity (allow many files in one sheet, use the SONGWRITER field as song's composer, ignore data files)

This commit is contained in:
Paweł Bara 2011-01-05 20:23:53 +00:00
parent bb79a68be0
commit d4d7f19fab
8 changed files with 438 additions and 111 deletions

View File

@ -29,6 +29,7 @@ const char* CueParser::kIndexRegExp = "(\\d{2}):(\\d{2}):(\\d{2})";
const char* CueParser::kPerformer = "performer";
const char* CueParser::kTitle = "title";
const char* CueParser::kSongWriter = "songwriter";
const char* CueParser::kFile = "file";
const char* CueParser::kTrack = "track";
const char* CueParser::kIndex = "index";
@ -46,123 +47,155 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
text_stream.setCodec(QTextCodec::codecForUtfText(device->peek(1024), QTextCodec::codecForName("UTF-8")));
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;
// read the first line already
QString line = text_stream.readLine();
QList<CueEntry> entries;
int files = 0;
// tracks
do {
QStringList splitted = SplitCueLine(line);
// -- whole file
while (!text_stream.atEnd()) {
// uninteresting or incorrect line
if(splitted.size() < 2) {
continue;
QString album_artist;
QString album;
QString album_composer;
QString file;
QString file_type;
// -- FILE section
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];
// PERFORMER
if(line_name == kPerformer) {
album_artist = line_value;
// TITLE
} else if(line_name == kTitle) {
album = line_value;
// SONGWRITER
} else if(line_name == kSongWriter) {
album_composer = line_value;
// FILE
} else if(line_name == kFile) {
file = QDir::isAbsolutePath(line_value)
? line_value
: dir.absoluteFilePath(line_value);
if(splitted.size() > 2) {
file_type = splitted[2];
}
// end of the header -> go into the track mode
} else if(line_name == kTrack) {
files++;
break;
}
// just ignore the rest of possible field types for now...
} while(!(line = text_stream.readLine()).isNull());
if(line.isNull()) {
qWarning() << "the .cue file from " << dir_path << " defines no tracks!";
return ret;
}
QString line_name = splitted[0].toLower();
QString line_value = splitted[1];
QString line_additional = splitted.size() > 2 ? splitted[2].toLower() : "";
// if this is a data file, all of it's tracks will be ignored
bool valid_file = file_type.compare("BINARY", Qt::CaseInsensitive) &&
file_type.compare("MOTOROLA", Qt::CaseInsensitive);
if(line_name == kTrack) {
QString track_type;
QString index;
QString artist;
QString composer;
QString title;
// 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));
// TRACK section
do {
QStringList splitted = SplitCueLine(line);
// uninteresting or incorrect line
if(splitted.size() < 2) {
continue;
}
// clear the state
track_type = index = artist = title = "";
QString line_name = splitted[0].toLower();
QString line_value = splitted[1];
QString line_additional = splitted.size() > 2 ? splitted[2].toLower() : "";
if(!line_additional.isEmpty()) {
track_type = line_additional;
}
if(line_name == kTrack) {
} else if(line_name == kIndex) {
// 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(valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) {
entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer));
}
// we need the index's position field
if(!line_additional.isEmpty()) {
// clear the state
track_type = index = artist = title = "";
// 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()) {
if(!line_additional.isEmpty()) {
track_type = line_additional;
}
index = 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;
} else if(line_name == kSongWriter) {
composer = line_value;
// end of track's for the current file -> parse next one
} else if(line_name == kFile) {
break;
}
} 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(valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) {
entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer));
}
// 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
@ -181,7 +214,12 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const {
// overwrite the stuff, we may have read from the file or library, using
// the current .cue metadata
song.set_track(i + 1);
// set track number only in single-file mode
if(files == 1) {
song.set_track(i + 1);
}
if(i + 1 < entries.size()) {
// incorrect indices?
if(!UpdateSong(entry, entries.at(i + 1).index, &song)) {
@ -227,6 +265,7 @@ bool CueParser::UpdateSong(const CueEntry& entry, const QString& next_index, Son
song->Init(entry.title, entry.PrettyArtist(),
entry.album, beginning, end);
song->set_albumartist(entry.album_artist);
song->set_composer(entry.PrettyComposer());
return true;
}
@ -245,6 +284,7 @@ bool CueParser::UpdateLastSong(const CueEntry& entry, Song* song) const {
song->set_artist(entry.PrettyArtist());
song->set_album(entry.album);
song->set_albumartist(entry.album_artist);
song->set_composer(entry.PrettyComposer());
// we don't do anything with the end here because it's already set to
// the end of the media file (if it exists)

View File

@ -34,6 +34,7 @@ class CueParser : public ParserBase {
static const char* kPerformer;
static const char* kTitle;
static const char* kSongWriter;
static const char* kFile;
static const char* kTrack;
static const char* kIndex;
@ -62,16 +63,22 @@ class CueParser : public ParserBase {
QString album_artist;
QString album;
QString composer;
QString album_composer;
QString PrettyArtist() const { return artist.isEmpty() ? album_artist : artist; }
QString PrettyComposer() const { return composer.isEmpty() ? album_composer : composer; }
CueEntry(QString& file, QString& index, QString& title, QString& artist,
QString& album_artist, QString& album) {
QString& album_artist, QString& album, QString& composer, QString& album_composer) {
this->file = file;
this->index = index;
this->title = title;
this->artist = artist;
this->album_artist = album_artist;
this->album = album;
this->composer = composer;
this->album_composer = album_composer;
}
};

View File

@ -53,6 +53,7 @@ TEST_F(CueParserTest, ParsesASong) {
ASSERT_EQ("Zucchero himself", first_song.albumartist());
ASSERT_EQ("", first_song.album());
ASSERT_EQ(1, first_song.beginning());
ASSERT_EQ(1, first_song.track());
}
TEST_F(CueParserTest, ParsesTwoSongs) {
@ -66,19 +67,22 @@ TEST_F(CueParserTest, ParsesTwoSongs) {
// with the specified metadata
Song first_song = song_list.at(0);
Song second_song = song_list.at(1);
ASSERT_EQ("Un soffio caldo", first_song.title());
ASSERT_EQ("Chocabeck", first_song.album());
ASSERT_EQ("Zucchero himself", first_song.artist());
ASSERT_EQ("Zucchero himself", first_song.albumartist());
ASSERT_EQ(1, first_song.beginning());
ASSERT_EQ((5 * 60 + 3) - 1, first_song.length());
ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length());
ASSERT_EQ(1, first_song.track());
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("Zucchero himself", second_song.albumartist());
ASSERT_EQ(5 * 60 + 3, second_song.beginning());
ASSERT_EQ(2, second_song.track());
}
TEST_F(CueParserTest, SkipsBrokenSongs) {
@ -92,6 +96,8 @@ TEST_F(CueParserTest, SkipsBrokenSongs) {
// with the specified metadata
Song first_song = song_list.at(0);
Song second_song = song_list.at(1);
ASSERT_EQ("Un soffio caldo", first_song.title());
ASSERT_EQ("Chocabeck", first_song.album());
ASSERT_EQ("Zucchero himself", first_song.artist());
@ -99,12 +105,183 @@ TEST_F(CueParserTest, SkipsBrokenSongs) {
ASSERT_EQ(1, first_song.beginning());
// includes the broken song too; this entry will span from it's
// INDEX (beginning) to the end of the next correct song
ASSERT_EQ((5 * 60) - 1, first_song.length());
ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length());
ASSERT_EQ(1, first_song.track());
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("Zucchero himself", second_song.albumartist());
ASSERT_EQ(5 * 60, second_song.beginning());
ASSERT_EQ(2, second_song.track());
}
TEST_F(CueParserTest, UsesAllMetadataInformation) {
QFile file(":testdata/fullmetadata.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);
Song second_song = song_list.at(1);
ASSERT_TRUE(first_song.filename().endsWith("a_file.mp3"));
ASSERT_EQ("Un soffio caldo", first_song.title());
ASSERT_EQ("Album", first_song.album());
ASSERT_EQ("Zucchero", first_song.artist());
ASSERT_EQ("Zucchero himself", first_song.albumartist());
ASSERT_EQ("Some guy", first_song.composer());
ASSERT_EQ(1, first_song.beginning());
ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length());
ASSERT_EQ(1, first_song.track());
ASSERT_TRUE(second_song.filename().endsWith("a_file.mp3"));
ASSERT_EQ("Hey you!", second_song.title());
ASSERT_EQ("Album", second_song.album());
ASSERT_EQ("Zucchero himself", second_song.artist());
ASSERT_EQ("Zucchero himself", second_song.albumartist());
ASSERT_EQ("Some other guy", second_song.composer());
ASSERT_EQ(2, second_song.beginning());
ASSERT_EQ(2, second_song.track());
}
TEST_F(CueParserTest, AcceptsMultipleFileBasedCues) {
QFile file(":testdata/manyfiles.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
// five songs
ASSERT_EQ(5, song_list.size());
// with the specified metadata
Song first_song = song_list.at(0);
Song second_song = song_list.at(1);
Song third_song = song_list.at(2);
Song fourth_song = song_list.at(3);
Song fifth_song = song_list.at(4);
ASSERT_TRUE(first_song.filename().endsWith("files/longer_one.mp3"));
ASSERT_EQ("A1Song1", first_song.title());
ASSERT_EQ("Artist One Album", first_song.album());
ASSERT_EQ("Artist One", first_song.artist());
ASSERT_EQ("Artist One", first_song.albumartist());
ASSERT_EQ(1, first_song.beginning());
ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length());
ASSERT_EQ(-1, first_song.track());
ASSERT_TRUE(second_song.filename().endsWith("files/longer_one.mp3"));
ASSERT_EQ("A1Song2", second_song.title());
ASSERT_EQ("Artist One Album", second_song.album());
ASSERT_EQ("Artist One", second_song.artist());
ASSERT_EQ("Artist One", second_song.albumartist());
ASSERT_EQ((5 * 60 + 3), second_song.beginning());
ASSERT_EQ(-1, second_song.track());
ASSERT_TRUE(third_song.filename().endsWith("files/longer_two_p1.mp3"));
ASSERT_EQ("A2P1Song1", third_song.title());
ASSERT_EQ("Artist Two Album", third_song.album());
ASSERT_EQ("Artist X", third_song.artist());
ASSERT_EQ("Artist Two", third_song.albumartist());
ASSERT_EQ(0, third_song.beginning());
ASSERT_EQ(fourth_song.beginning() - third_song.beginning(), third_song.length());
ASSERT_EQ(-1, third_song.track());
ASSERT_TRUE(fourth_song.filename().endsWith("files/longer_two_p1.mp3"));
ASSERT_EQ("A2P1Song2", fourth_song.title());
ASSERT_EQ("Artist Two Album", fourth_song.album());
ASSERT_EQ("Artist Two", fourth_song.artist());
ASSERT_EQ("Artist Two", fourth_song.albumartist());
ASSERT_EQ(4 * 60, fourth_song.beginning());
ASSERT_EQ(-1, fourth_song.track());
ASSERT_TRUE(fifth_song.filename().endsWith("files/longer_two_p2.mp3"));
ASSERT_EQ("A2P2Song1", fifth_song.title());
ASSERT_EQ("Artist Two Album", fifth_song.album());
ASSERT_EQ("Artist Two", fifth_song.artist());
ASSERT_EQ("Artist Two", fifth_song.albumartist());
ASSERT_EQ(1, fifth_song.beginning());
ASSERT_EQ(-1, fifth_song.track());
}
TEST_F(CueParserTest, SkipsBrokenSongsInMultipleFileBasedCues) {
QFile file(":testdata/manyfilesbroken.cue");
file.open(QIODevice::ReadOnly);
SongList song_list = parser_.Load(&file, QDir(""));
// four songs
ASSERT_EQ(4, song_list.size());
// with the specified metadata
Song first_song = song_list.at(0);
Song second_song = song_list.at(1);
Song third_song = song_list.at(2);
Song fourth_song = song_list.at(3);
// A* - broken song in the middle
ASSERT_TRUE(first_song.filename().endsWith("file1.mp3"));
ASSERT_EQ("Artist One", first_song.artist());
ASSERT_EQ("Artist One Album", first_song.album());
ASSERT_EQ("A1", first_song.title());
ASSERT_EQ(1, first_song.beginning());
ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length());
ASSERT_EQ(-1, first_song.track());
ASSERT_TRUE(second_song.filename().endsWith("file1.mp3"));
ASSERT_EQ("Artist One", second_song.artist());
ASSERT_EQ("Artist One Album", second_song.album());
ASSERT_EQ("A3", second_song.title());
ASSERT_EQ(60, second_song.beginning());
ASSERT_EQ(-1, second_song.track());
// all B* songs are broken
// C* - broken song at the end
ASSERT_TRUE(third_song.filename().endsWith("file3.mp3"));
ASSERT_EQ("Artist Three", third_song.artist());
ASSERT_EQ("Artist Three Album", third_song.album());
ASSERT_EQ("C1", third_song.title());
ASSERT_EQ(1, third_song.beginning());
ASSERT_EQ(-1, third_song.track());
// D* - broken song at the beginning
ASSERT_TRUE(fourth_song.filename().endsWith("file4.mp3"));
ASSERT_EQ("Artist Four", fourth_song.artist());
ASSERT_EQ("Artist Four Album", fourth_song.album());
ASSERT_EQ("D2", fourth_song.title());
ASSERT_EQ(61, fourth_song.beginning());
ASSERT_EQ(-1, fourth_song.track());
}
TEST_F(CueParserTest, SkipsDataFiles) {
QFile file(":testdata/withdatafiles.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);
Song second_song = song_list.at(1);
ASSERT_TRUE(first_song.filename().endsWith("file1.mp3"));
ASSERT_EQ("Artist One", first_song.artist());
ASSERT_EQ("Artist One Album", first_song.album());
ASSERT_EQ("A1", first_song.title());
ASSERT_EQ(1, first_song.beginning());
ASSERT_EQ(-1, first_song.track());
ASSERT_TRUE(second_song.filename().endsWith("file4.mp3"));
ASSERT_EQ("Artist Four", second_song.artist());
ASSERT_EQ("Artist Four Album", second_song.album());
ASSERT_EQ("D1", second_song.title());
ASSERT_EQ(61, second_song.beginning());
ASSERT_EQ(-1, second_song.track());
}

View File

@ -0,0 +1,13 @@
FILE a_file.mp3 WAVE
SONGWRITER "Some guy"
TITLE "Album"
PERFORMER "Zucchero himself"
TRACK 01 AUDIO
TITLE "Un soffio caldo"
PERFORMER Zucchero
INDEX 00 00:01:00
INDEX 01 00:01:00
TRACK 02 AUDIO
TITLE "Hey you!"
SONGWRITER "Some other guy"
INDEX 05 00:02:00

25
tests/data/manyfiles.cue Normal file
View File

@ -0,0 +1,25 @@
FILE files/longer_one.mp3 WAVE
PERFORMER "Artist One"
TITLE "Artist One Album"
TRACK 01 AUDIO
TITLE "A1Song1"
INDEX 01 00:01:00
TRACK 02 AUDIO
TITLE "A1Song2"
INDEX 01 05:03:68
FILE files/longer_two_p1.mp3 WAVE
PERFORMER "Artist Two"
TITLE "Artist Two Album"
TRACK 01 AUDIO
TITLE "A2P1Song1"
PERFORMER "Artist X"
INDEX 01 00:00:00
TRACK 02 AUDIO
TITLE "A2P1Song2"
INDEX 01 04:00:00
FILE files/longer_two_p2.mp3 WAVE
PERFORMER "Artist Two"
TITLE "Artist Two Album"
TRACK 01 AUDIO
TITLE "A2P2Song1"
INDEX 01 00:01:00

View File

@ -0,0 +1,33 @@
FILE file1.mp3 WAVE
PERFORMER "Artist One"
TITLE "Artist One Album"
TRACK 01 AUDIO
TITLE "A1"
INDEX 01 00:01:00
TRACK 02 AUDIO
TITLE "A2"
TRACK 03 AUDIO
TITLE "A3"
INDEX 01 01:00:00
FILE file2.mp3 WAVE
PERFORMER "Artist Two"
TITLE "Artist Two Album"
TRACK 01 CDG
TITLE "B1"
INDEX 00 00:01:00
FILE file3.mp3 WAVE
PERFORMER "Artist Three"
TITLE "Artist Three Album"
TRACK 01 AUDIO
TITLE "C1"
INDEX 00 00:01:00
TRACK 02 AUDIO
TITLE "C2"
FILE file4.mp3 WAVE
PERFORMER "Artist Four"
TITLE "Artist Four Album"
TRACK 01 AUDIO
TITLE "D1"
TRACK 02 AUDIO
TITLE "D2"
INDEX 00 01:01:00

View File

@ -8,21 +8,25 @@
<file>beep.wma</file>
<file>beep.m4a</file>
<file>brokensong.cue</file>
<file>onesong.cue</file>
<file>twosongs.cue</file>
<file>pls_one.pls</file>
<file>pls_somafm.pls</file>
<file>test.m3u</file>
<file>test.xspf</file>
<file>test.asx</file>
<file>secretagent.asx</file>
<file>secretagent.pls</file>
<file>test.asxini</file>
<file>fmpsplaycount.mp3</file>
<file>fmpsplaycountboth.mp3</file>
<file>fmpsplaycountuser.mp3</file>
<file>fmpsrating.mp3</file>
<file>fmpsratingboth.mp3</file>
<file>fmpsratinguser.mp3</file>
<file>fullmetadata.cue</file>
<file>manyfiles.cue</file>
<file>manyfilesbroken.cue</file>
<file>onesong.cue</file>
<file>pls_one.pls</file>
<file>pls_somafm.pls</file>
<file>secretagent.asx</file>
<file>secretagent.pls</file>
<file>test.m3u</file>
<file>test.xspf</file>
<file>test.asx</file>
<file>test.asxini</file>
<file>twosongs.cue</file>
<file>withdatafiles.cue</file>
</qresource>
</RCC>

View File

@ -0,0 +1,28 @@
FILE file1.mp3
PERFORMER "Artist One"
TITLE "Artist One Album"
TRACK 01 AUDIO
TITLE "A1"
INDEX 01 00:01:00
FILE file2.mp3 BINARY
PERFORMER "Artist Two"
TITLE "Artist Two Album"
TRACK 01 AUDIO
TITLE "B1"
INDEX 00 00:01:00
FILE file3.mp3 MOTOROLA
PERFORMER "Artist Three"
TITLE "Artist Three Album"
TRACK 01 AUDIO
TITLE "C1"
INDEX 00 00:01:00
FILE file4.mp3 MP3
PERFORMER "Artist Four"
TITLE "Artist Four Album"
TRACK 01 AUDIO
TITLE "D1"
INDEX 00 01:01:00