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 parseOuputs(const QByteArray &data); static QString formatDuration(const quint32 totalseconds); diff --git a/mpd/song.cpp b/mpd/song.cpp index 194986ba0..541a156d3 100644 --- a/mpd/song.cpp +++ b/mpd/song.cpp @@ -256,8 +256,12 @@ void Song::clear() type = Standard; } -QString Song::formattedTime(quint32 seconds) +QString Song::formattedTime(quint32 seconds, bool zeroIsUnknown) { + if (0==seconds && zeroIsUnknown) { + return i18n("Unknown"); + } + static const quint32 constHour=60*60; if (seconds>constHour) { return MPDParseUtils::formatDuration(seconds); diff --git a/mpd/song.h b/mpd/song.h index eda8cfed7..1aee324c1 100644 --- a/mpd/song.h +++ b/mpd/song.h @@ -85,7 +85,7 @@ struct Song void fillEmptyFields(); void setKey(); virtual void clear(); - static QString formattedTime(quint32 seconds); + static QString formattedTime(quint32 seconds, bool zeroIsUnknown=false); QString format(); QString entryName() const; QString artistSong() const; diff --git a/tags/tageditor.cpp b/tags/tageditor.cpp index b5242bc2d..a2fca4309 100644 --- a/tags/tageditor.cpp +++ b/tags/tageditor.cpp @@ -30,6 +30,7 @@ #include "inputdialog.h" #include "localize.h" #include "trackorganiser.h" +#include "cuefile.h" #ifdef ENABLE_KDE_SUPPORT #include #endif @@ -80,6 +81,9 @@ TagEditor::TagEditor(QWidget *parent, const QList &songs, { iCount++; foreach (const Song &s, songs) { + if (CueFile::isCue(s.file)) { + continue; + } if (s.guessed) { Song song(s); song.revertGuessedTags(); @@ -89,6 +93,11 @@ TagEditor::TagEditor(QWidget *parent, const QList &songs, } } + if (original.isEmpty()) { + deleteLater(); + return; + } + #ifdef ENABLE_DEVICES_SUPPORT if (deviceUdi.isEmpty()) { baseDir=MPDConnection::self()->getDetails().dir; diff --git a/tags/trackorganiser.cpp b/tags/trackorganiser.cpp index e95df646d..63284dbba 100644 --- a/tags/trackorganiser.cpp +++ b/tags/trackorganiser.cpp @@ -37,6 +37,7 @@ #include "messagebox.h" #include "icons.h" #include "treeview.h" +#include "cuefile.h" #include #include #include @@ -84,7 +85,17 @@ void TrackOrganiser::show(const QList &songs, const QString &udi) { Q_UNUSED(udi) - origSongs=songs; + foreach (const Song &s, songs) { + if (!CueFile::isCue(s.file)) { + origSongs.append(s); + } + } + + if (origSongs.isEmpty()) { + deleteLater(); + return; + } + #ifdef ENABLE_DEVICES_SUPPORT if (udi.isEmpty()) { opts.load(MPDConnectionDetails::configGroupName(MPDConnection::self()->getDetails().name), true);