diff --git a/CMakeLists.txt b/CMakeLists.txt
index c91a76de5..16b8b0423 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -98,7 +98,7 @@ set(CANTATA_SRCS gui/application.cpp gui/main.cpp gui/initialsettingswizard.cpp
models/playqueueproxymodel.cpp models/dirviewmodel.cpp models/dirviewproxymodel.cpp models/dirviewitem.cpp models/dirviewitemdir.cpp
models/streamsmodel.cpp models/streamsproxymodel.cpp models/albumsmodel.cpp models/albumsproxymodel.cpp models/proxymodel.cpp
models/actionmodel.cpp models/digitallyimported.cpp
- mpd/mpdconnection.cpp mpd/mpdparseutils.cpp mpd/mpdstats.cpp mpd/mpdstatus.cpp mpd/song.cpp mpd/mpduser.cpp
+ mpd/mpdconnection.cpp mpd/mpdparseutils.cpp mpd/mpdstats.cpp mpd/mpdstatus.cpp mpd/song.cpp mpd/mpduser.cpp mpd/cuefile.cpp
dynamic/dynamic.cpp dynamic/dynamicpage.cpp dynamic/dynamicproxymodel.cpp dynamic/dynamicruledialog.cpp dynamic/dynamicrulesdialog.cpp
widgets/treeview.cpp widgets/listview.cpp widgets/itemview.cpp widgets/autohidingsplitter.cpp widgets/timeslider.cpp
widgets/actionlabel.cpp widgets/playqueueview.cpp widgets/groupedview.cpp widgets/actionitemdelegate.cpp widgets/textbrowser.cpp
diff --git a/ChangeLog b/ChangeLog
index e03d9a626..8305e1bbd 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -103,6 +103,8 @@
65. Place search fields on bottom of views, and hide by default. Show when
Ctrl-F is used for views, and Ctrl-Shift-F for playqueue.
66. When searching in dynamic page, also search rules themselves.
+67. For MPD versions 0.17 and above, if Cantata can read a .cue file then it
+ will list each track as a separate entry in the artists and albums views.
1.0.3
-----
diff --git a/README b/README
index b64942fe2..ff54aa93f 100644
--- a/README
+++ b/README
@@ -482,7 +482,8 @@ Credits
Cantata contains code/icons from:
Amarok - amarok.kde.org (Transcoding, Cover fetching code in cover
dialog, Jamendo and Magnatune icons)
- Clementine - www.clementine-player.org (Lyrics searches)
+ Clementine - www.clementine-player.org (Lyrics searches, CueFile
+ parsing, and digitally imported support)
Be::MPC - Wikipedia parsing code
Quassel - quassel-irc.org (Qt-only keyboard short-cut config support)
Solid - solid.kde.org (Device detection for Qt-only builds)
diff --git a/TODO b/TODO
index 6a072c4d1..58f30136d 100644
--- a/TODO
+++ b/TODO
@@ -33,6 +33,9 @@
- Will need to be careful that songs are not from device
- Also, check that songs are not streams!
+- Cue files
+ - How to determine duration of last track?
+
- General
- Ratings (use KRatingWidget?)
- Not sure, would need support in cantata-dynamic
diff --git a/models/albumsmodel.cpp b/models/albumsmodel.cpp
index 13f1193a6..6802a8e0d 100644
--- a/models/albumsmodel.cpp
+++ b/models/albumsmodel.cpp
@@ -240,9 +240,9 @@ QVariant AlbumsModel::data(const QModelIndex &index, int role) const
? al->name
: (year>0 ? QString("%1\n%2 (%3)\n").arg(al->artist).arg(al->album).arg(QString::number(year)) : QString("%1\n%2\n").arg(al->artist).arg(al->album))+
#ifdef ENABLE_KDE_SUPPORT
- i18np("1 Track (%2)", "%1 Tracks (%2)", al->songs.count(), Song::formattedTime(al->totalTime()));
+ i18np("1 Track (%2)", "%1 Tracks (%2)", al->songs.count(), Song::formattedTime(al->totalTime(), true));
#else
- QTP_TRACKS_DURATION_STR(al->songs.count(), Song::formattedTime(al->totalTime()));
+ QTP_TRACKS_DURATION_STR(al->songs.count(), Song::formattedTime(al->totalTime(), true));
#endif
}
case Qt::DisplayRole:
@@ -274,7 +274,7 @@ QVariant AlbumsModel::data(const QModelIndex &index, int role) const
return si->parent->artist+QLatin1String("
")+
si->parent->album+(year>0 ? (QLatin1String(" (")+QString::number(year)+QChar(')')) : QString())+QLatin1String("
")+
data(index, Qt::DisplayRole).toString()+QLatin1String("
")+
- (Song::Playlist==si->type ? QString() : Song::formattedTime(si->time)+QLatin1String("
"))+
+ (Song::Playlist==si->type ? QString() : Song::formattedTime(si->time, true)+QLatin1String("
"))+
QLatin1String("")+si->file+QLatin1String("");
}
case Qt::DisplayRole:
@@ -288,7 +288,7 @@ QVariant AlbumsModel::data(const QModelIndex &index, int role) const
return si->trackAndTitleStr(Song::isVariousArtists(si->parent->artist) && !Song::isVariousArtists(si->artist));
}
case ItemView::Role_SubText: {
- return Song::formattedTime(si->time);
+ return Song::formattedTime(si->time, true);
}
}
}
diff --git a/models/musiclibraryitemalbum.cpp b/models/musiclibraryitemalbum.cpp
index 2cbf9b5ec..889b36729 100644
--- a/models/musiclibraryitemalbum.cpp
+++ b/models/musiclibraryitemalbum.cpp
@@ -43,7 +43,6 @@
#include
#include
-#include
static MusicLibraryItemAlbum::CoverSize coverSize=MusicLibraryItemAlbum::CoverNone;
static QPixmap *theDefaultIcon=0;
static QPixmap *theDefaultLargeIcon=0;
@@ -342,6 +341,22 @@ void MusicLibraryItemAlbum::remove(MusicLibraryItemSong *i)
}
}
+void MusicLibraryItemAlbum::removeAll(const QSet &fileNames)
+{
+ QSet fn=fileNames;
+ for (int i=0; i(m_childItems.at(i));
+ if (fn.contains(song->song().file)) {
+ fn.remove(song->song().file);
+ delete m_childItems.takeAt(i);
+ m_totalTime=0;
+ m_artists.clear();
+ } else {
+ ++i;
+ }
+ }
+}
+
bool MusicLibraryItemAlbum::detectIfIsMultipleArtists()
{
if (m_childItems.count()<2) {
diff --git a/models/musiclibraryitemalbum.h b/models/musiclibraryitemalbum.h
index a210a87d8..33cd61d27 100644
--- a/models/musiclibraryitemalbum.h
+++ b/models/musiclibraryitemalbum.h
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#include "musiclibraryitem.h"
#include "song.h"
@@ -75,6 +76,7 @@ public:
void append(MusicLibraryItem *i);
void remove(int row);
void remove(MusicLibraryItemSong *i);
+ void removeAll(const QSet &fileNames);
bool detectIfIsMultipleArtists();
bool isMultipleArtists() const { return Song::MultipleArtists==m_type; }
Song::Type songType() const { return m_type; }
diff --git a/models/musiclibrarymodel.cpp b/models/musiclibrarymodel.cpp
index 0f9e8d711..26a32744f 100644
--- a/models/musiclibrarymodel.cpp
+++ b/models/musiclibrarymodel.cpp
@@ -291,16 +291,16 @@ QVariant MusicLibraryModel::data(const QModelIndex &index, int role) const
? item->data()
: item->data()+"
"+
#ifdef ENABLE_KDE_SUPPORT
- i18np("1 Track (%2)", "%1 Tracks (%2)", item->childCount(), Song::formattedTime(static_cast(item)->totalTime()))
+ i18np("1 Track (%2)", "%1 Tracks (%2)", item->childCount(), Song::formattedTime(static_cast(item)->totalTime(), true))
#else
- QTP_TRACKS_DURATION_STR(item->childCount(), Song::formattedTime(static_cast(item)->totalTime()))
+ QTP_TRACKS_DURATION_STR(item->childCount(), Song::formattedTime(static_cast(item)->totalTime(), true))
#endif
);
case MusicLibraryItem::Type_Song: {
return item->parentItem()->parentItem()->data()+QLatin1String("
")+item->parentItem()->data()+QLatin1String("
")+
data(index, Qt::DisplayRole).toString()+QLatin1String("
")+
(Song::Playlist==static_cast(item)->song().type
- ? QString() : Song::formattedTime(static_cast(item)->time())+QLatin1String("
"))+
+ ? QString() : Song::formattedTime(static_cast(item)->time(), true)+QLatin1String("
"))+
QLatin1String("")+static_cast(item)->song().file+QLatin1String("");
}
default: return QVariant();
@@ -322,12 +322,12 @@ QVariant MusicLibraryModel::data(const QModelIndex &index, int role) const
#endif
break;
case MusicLibraryItem::Type_Song:
- return Song::formattedTime(static_cast(item)->time());
+ return Song::formattedTime(static_cast(item)->time(), true);
case MusicLibraryItem::Type_Album:
#ifdef ENABLE_KDE_SUPPORT
- return i18np("1 Track (%2)", "%1 Tracks (%2)", item->childCount(), Song::formattedTime(static_cast(item)->totalTime()));
+ return i18np("1 Track (%2)", "%1 Tracks (%2)", item->childCount(), Song::formattedTime(static_cast(item)->totalTime(), true));
#else
- return QTP_TRACKS_DURATION_STR(item->childCount(), Song::formattedTime(static_cast(item)->totalTime()));
+ return QTP_TRACKS_DURATION_STR(item->childCount(), Song::formattedTime(static_cast(item)->totalTime(), true));
#endif
default: return QVariant();
}
diff --git a/mpd/cuefile.cpp b/mpd/cuefile.cpp
new file mode 100644
index 000000000..90d742188
--- /dev/null
+++ b/mpd/cuefile.cpp
@@ -0,0 +1,347 @@
+/*
+ * Cantata
+ *
+ * Copyright (c) 2011-2013 Craig Drummond
+ *
+ */
+/* This file is part of Clementine.
+ Copyright 2010, David Sansome
+
+ 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 .
+*/
+
+#include "cuefile.h"
+#include "mpdconnection.h"
+#include "utils.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#if QT_VERSION >= 0x050000
+#include
+#endif
+
+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 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);
+ #if QT_VERSION < 0x050000
+ const QUrl &q=u;
+ #else
+ QUrlQuery q(u);
+ #endif
+
+ 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;
+ QString title;
+ QString artist;
+ QString albumArtist;
+ QString album;
+// QString composer;
+// QString albumComposer;
+ QString genre;
+ QString date;
+
+ CueEntry(QString &file, QString &index, QString &title, QString &artist, QString &albumArtist,
+ QString &album, /*QString &composer, QString &albumComposer,*/ QString &genre, QString &date) {
+ this->file = file;
+ this->index = index;
+ 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 qint64 indexToMarker(const QString& index)
+{
+ static const qint64 constNsecPerSec = 1000000000ll;
+
+ 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 * constNsecPerSec) / 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.genre=entry.genre;
+ song.year=entry.date.toInt();
+ song.time=(end-beginning)/1000000000ll;
+ 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.genre=entry.genre;
+ song.year=entry.date.toInt();
+ return true;
+}
+
+bool CueFile::parse(const QString &fileName, const QString &dir, QList &songList, QSet &files)
+{
+ QFile f(dir+fileName);
+ if (!f.open(QIODevice::ReadOnly|QIODevice::Text)) {
+ return false;
+ }
+
+ QTextStream textStream(&f);
+ textStream.setCodec(QTextCodec::codecForUtfText(f.peek(1024), QTextCodec::codecForName("UTF-8")));
+
+ // read the first line already
+ QString line = textStream.readLine();
+ QList entries;
+ QString fileDir=fileName.contains("/") ? Utils::getDir(fileName) : QString();
+ int fileCount=0;
+
+ // -- whole file
+ while (!textStream.atEnd()) {
+ QString albumArtist;
+ QString album;
+// QString albumComposer;
+ QString file;
+ QString fileType;
+ QString genre;
+ QString date;
+
+ // -- 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];
+ }
+ if (!files.contains(fileDir+file)) {
+ files.insert(fileDir+file);
+ }
+ } else if (lineName == constRem) {
+ if (splitted.size() < 3) {
+ break;
+ }
+
+ if (lineValue.toLower() == constGenre) {
+ genre = splitted[2];
+ } else if (lineValue.toLower() == constDate) {
+ date = splitted[2];
+ }
+ // end of the header -> go into the track mode
+ } else if (lineName == constTrack) {
+ fileCount++;
+ 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;
+
+ // 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, title, artist, albumArtist, album, /*composer, albumComposer,*/ genre, date));
+ }
+
+ // clear the state
+ trackType = index = artist = title = QString();
+
+ if (!lineAdditional.isEmpty()) {
+ trackType = lineAdditional;
+ }
+ } 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, 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);
+
+ // set track number only in single-file mode
+ if (1==fileCount) {
+ song.track=i+1;
+ }
+
+ // 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;
+}
diff --git a/mpd/cuefile.h b/mpd/cuefile.h
new file mode 100644
index 000000000..de1535af2
--- /dev/null
+++ b/mpd/cuefile.h
@@ -0,0 +1,41 @@
+/*
+ * Cantata
+ *
+ * Copyright (c) 2011-2013 Craig Drummond
+ *
+ */
+/* This file is part of Clementine.
+ Copyright 2010, David Sansome
+
+ 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 .
+*/
+
+#ifndef CUEFILE_H
+#define CUEFILE_H
+
+#include
+#include
+#include "song.h"
+
+// This parser will try to detect the real encoding of a .cue file but there's
+// a great chance it will fail so it's probably best to assume that the parser
+// is UTF compatible only.
+namespace CueFile
+{
+ extern bool isCue(const QString &str);
+ extern QByteArray getLoadLine(const QString &str);
+ extern bool parse(const QString &fileName, const QString &dir, QList &songList, QSet &files);
+}
+
+#endif // CUEFILE_H
diff --git a/mpd/mpdconnection.cpp b/mpd/mpdconnection.cpp
index da75942d2..28cc48e9d 100644
--- a/mpd/mpdconnection.cpp
+++ b/mpd/mpdconnection.cpp
@@ -37,6 +37,7 @@
#include
#include "thread.h"
#include "settings.h"
+#include "cuefile.h"
#include
static bool debugEnabled=false;
@@ -66,7 +67,7 @@ MPDConnection * MPDConnection::self()
#endif
}
-static QByteArray encodeName(const QString &name)
+QByteArray MPDConnection::encodeName(const QString &name)
{
return '\"'+name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"")+'\"';
}
@@ -521,14 +522,18 @@ void MPDConnection::add(const QStringList &files, quint32 pos, quint32 size, boo
bool havePlaylist=false;
bool usePrio=priority>0 && canUsePriority();
for (int i = 0; i < files.size(); i++) {
- if (isPlaylist(files.at(i))) {
- send+="load ";
- havePlaylist=true;
+ if (CueFile::isCue(files.at(i))) {
+ send += "load "+CueFile::getLoadLine(files.at(i))+"\n";
} else {
- addedFile=true;
- send += "add ";
+ if (isPlaylist(files.at(i))) {
+ send+="load ";
+ havePlaylist=true;
+ } else {
+ addedFile=true;
+ send += "add ";
+ }
+ send += encodeName(files.at(i))+"\n";
}
- send += encodeName(files.at(i))+"\n";
if (!havePlaylist) {
if (0!=size) {
send += "move "+QByteArray::number(curSize)+" "+QByteArray::number(curPos)+"\n";
@@ -1075,7 +1080,7 @@ void MPDConnection::loadLibrary()
emit updatingLibrary();
Response response=sendCommand("listallinfo");
if (response.ok) {
- emit musicLibraryUpdated(MPDParseUtils::parseLibraryItems(response.data), dbUpdate);
+ emit musicLibraryUpdated(MPDParseUtils::parseLibraryItems(response.data, details.dir, ver), dbUpdate);
}
emit updatedLibrary();
}
diff --git a/mpd/mpdconnection.h b/mpd/mpdconnection.h
index 0b594d170..c07365801 100644
--- a/mpd/mpdconnection.h
+++ b/mpd/mpdconnection.h
@@ -159,6 +159,7 @@ class MPDConnection : public QObject
public:
static MPDConnection * self();
+ static QByteArray encodeName(const QString &name);
struct Response {
Response(bool o=true, const QByteArray &d=QByteArray());
diff --git a/mpd/mpdparseutils.cpp b/mpd/mpdparseutils.cpp
index 38577603d..2462f0d3f 100644
--- a/mpd/mpdparseutils.cpp
+++ b/mpd/mpdparseutils.cpp
@@ -28,6 +28,7 @@
#include
#include
#include
+#include
#include "localize.h"
#include "dirviewitemroot.h"
#include "dirviewitemdir.h"
@@ -47,6 +48,8 @@
#include "httpserver.h"
#endif
#include "utils.h"
+#include "cuefile.h"
+#include "mpdconnection.h"
QList MPDParseUtils::parsePlaylists(const QByteArray &data)
{
@@ -363,8 +366,15 @@ void MPDParseUtils::setGroupMultiple(bool g)
groupMultipleArtists=g;
}
-MusicLibraryItemRoot * MPDParseUtils::parseLibraryItems(const QByteArray &data)
+struct ParsedCueFile
{
+ QList songs;
+ QSet files;
+};
+
+MusicLibraryItemRoot * MPDParseUtils::parseLibraryItems(const QByteArray &data, const QString &mpdDir, long mpdVersion)
+{
+ bool canSplitCue=mpdVersion>=MPD_MAKE_VERSION(0,17,0);
MusicLibraryItemRoot * const rootItem = new MusicLibraryItemRoot;
QByteArray currentItem;
QList lines = data.split('\n');
@@ -372,6 +382,7 @@ MusicLibraryItemRoot * MPDParseUtils::parseLibraryItems(const QByteArray &data)
MusicLibraryItemArtist *artistItem = 0;
MusicLibraryItemAlbum *albumItem = 0;
MusicLibraryItemSong *songItem = 0;
+ QList cueFiles;
for (int i = 0; i < amountOfLines; i++) {
currentItem += lines.at(i);
@@ -384,7 +395,11 @@ MusicLibraryItemRoot * MPDParseUtils::parseLibraryItems(const QByteArray &data)
}
if (Song::Playlist==currentSong.type) {
- if (songItem && Utils::getDir(songItem->file())==Utils::getDir(currentSong.file)) {
+ ParsedCueFile cf;
+ if (canSplitCue && currentSong.file.endsWith(".cue", Qt::CaseInsensitive) && CueFile::parse(currentSong.file, mpdDir, cf.songs, cf.files)) {
+ currentSong.fillEmptyFields();
+ cueFiles.append(cf);
+ } else if (songItem && Utils::getDir(songItem->file())==Utils::getDir(currentSong.file)) {
currentSong.albumartist=currentSong.artist=artistItem->data();
currentSong.album=albumItem->data();
songItem = new MusicLibraryItemSong(currentSong, albumItem);
@@ -403,7 +418,6 @@ MusicLibraryItemRoot * MPDParseUtils::parseLibraryItems(const QByteArray &data)
if (!albumItem || currentSong.year!=albumItem->year() || albumItem->parentItem()!=artistItem || currentSong.album!=albumItem->data()) {
albumItem = artistItem->album(currentSong);
}
-
songItem = new MusicLibraryItemSong(currentSong, albumItem);
albumItem->append(songItem);
albumItem->addGenre(currentSong.genre);
@@ -412,6 +426,32 @@ MusicLibraryItemRoot * MPDParseUtils::parseLibraryItems(const QByteArray &data)
}
}
+ // Split contents of cue files into tracks...
+ foreach (const ParsedCueFile &cf, cueFiles) {
+ QSet updatedAlbums;
+
+ foreach (Song s, cf.songs) {
+ s.fillEmptyFields();
+ if (!artistItem || s.albumArtist()!=artistItem->data()) {
+ artistItem = rootItem->artist(s);
+ }
+ if (!albumItem || s.year!=albumItem->year() || albumItem->parentItem()!=artistItem || s.album!=albumItem->data()) {
+ albumItem = artistItem->album(s);
+ }
+ songItem = new MusicLibraryItemSong(s, albumItem);
+ albumItem->append(songItem);
+ albumItem->addGenre(s.genre);
+ updatedAlbums.insert(albumItem);
+ artistItem->addGenre(s.genre);
+ rootItem->addGenre(s.genre);
+ }
+
+ // For each album that was updated/created, remove any source files referenced in cue file...
+ foreach (MusicLibraryItemAlbum *al, updatedAlbums) {
+ al->removeAll(cf.files);
+ }
+ }
+
if (groupSingleTracks) {
rootItem->groupSingleTracks();
}
diff --git a/mpd/mpdparseutils.h b/mpd/mpdparseutils.h
index 1245155e7..f74f48fc9 100644
--- a/mpd/mpdparseutils.h
+++ b/mpd/mpdparseutils.h
@@ -59,7 +59,7 @@ public:
static void setGroupSingle(bool g);
static bool groupMultiple();
static void setGroupMultiple(bool g);
- static MusicLibraryItemRoot * parseLibraryItems(const QByteArray &data);
+ static MusicLibraryItemRoot * parseLibraryItems(const QByteArray &data, const QString &mpdDir, long mpdVersion);
static DirViewItemRoot * parseDirViewItems(const QByteArray &data);
static QList