/***************************************************************************
* Copyright(C) 2006-2021 by Ilya Kotov *
* forkotov02@ya.ru *
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
* This program 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 this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
***************************************************************************/
#include <QWidget>
#include <QtAlgorithms>
#include <QTextStream>
#include <algorithm>
#include <time.h>
#include <qmmp/metadatamanager.h>
#include "metadatahelper_p.h"
#include "playlistparser.h"
#include "playlistformat.h"
#include "playlistcontainer_p.h"
#include "groupedcontainer_p.h"
#include "normalcontainer_p.h"
#include "playlisttask_p.h"
#include "fileloader_p.h"
#include "playstate_p.h"
#include "detailsdialog.h"
#include "qmmpuisettings.h"
#include "playlistmodel.h"
#define INVALID_INDEX -1
PlayListModel::PlayListModel(const QString &name, QObject *parent)
: QObject(parent) , m_name(name)
{
qsrand(time(nullptr));
m_ui_settings = QmmpUiSettings::instance();
m_loader = new FileLoader(this);
m_task = new PlayListTask(this);
if(m_ui_settings->isGroupsEnabled())
m_container = new GroupedContainer;
else
m_container = new NormalContainer;
if(m_ui_settings->isShuffle())
m_play_state = new ShufflePlayState(this);
else
m_play_state = new NormalPlayState(this);
connect(m_ui_settings, SIGNAL(groupsChanged(bool)), SLOT(prepareGroups(bool)));
connect(m_ui_settings, SIGNAL(shuffleChanged(bool)), SLOT(prepareForShufflePlaying(bool)));
connect(m_loader, SIGNAL(newTracksToInsert(PlayListItem*, QList<PlayListTrack*>)),
SLOT(insert(PlayListItem*, QList<PlayListTrack*>)), Qt::QueuedConnection);
connect(m_loader, SIGNAL(finished()), SLOT(preparePlayState()));
connect(m_loader, SIGNAL(finished()), SIGNAL(loaderFinished()));
connect(m_task, SIGNAL(finished()), SLOT(onTaskFinished()));
}
PlayListModel::~PlayListModel()
{
blockSignals(true);
m_loader->finish();
clear();
delete m_play_state;
delete m_container;
}
QString PlayListModel::name() const
{
return m_name;
}
void PlayListModel::setName(const QString &name)
{
if(m_name != name)
{
m_name = name;
emit nameChanged(name);
}
}
void PlayListModel::add(PlayListTrack *track)
{
m_container->addTrack(track);
m_total_duration += track->duration();
int flags = 0;
if(m_container->trackCount() == 1)
{
m_current_track = track;
m_current = m_container->indexOf(track);
flags |= CURRENT;
}
else if(m_ui_settings->isGroupsEnabled())
{
//update current index for grouped container only
m_current = m_container->indexOf(m_current_track);
}
flags |= STRUCTURE;
emit listChanged(flags);
}
void PlayListModel::add(const QList<PlayListTrack *> &tracks)
{
if(tracks.isEmpty())
return;
int flags = 0;
m_container->addTracks(tracks);
if(m_container->trackCount() == tracks.count())
{
m_current_track = tracks.first();
m_current = m_container->indexOf(m_current_track);
flags |= CURRENT;
}
else if(m_ui_settings->isGroupsEnabled())
{
//update current index for grouped container only
m_current = m_container->indexOf(m_current_track);
}
for(PlayListTrack *track : qAsConst(tracks))
{
m_total_duration += track->duration();
emit trackAdded(track);
}
preparePlayState();
flags |= STRUCTURE;
emit listChanged(flags);
}
void PlayListModel::add(const QString &path)
{
m_loader->add(path);
}
void PlayListModel::add(const QStringList &paths)
{
m_loader->add(paths);
}
void PlayListModel::insert(int index, PlayListTrack *track)
{
m_container->insertTrack(index, track);
m_total_duration += track->duration();
int flags = 0;
if(m_container->trackCount() == 1)
{
m_current_track = track;
m_current = m_container->indexOf(track);
flags |= CURRENT;
}
else
{
//update current index
m_current = m_container->indexOf(m_current_track);
}
emit trackAdded(track);
flags |= STRUCTURE;
emit listChanged(flags);
}
void PlayListModel::insert(PlayListItem *before, PlayListTrack *track)
{
if(before)
insert(m_container->indexOf(before), track);
else
add(track);
}
void PlayListModel::insert(int index, const QList<PlayListTrack *> &tracks)
{
if(tracks.isEmpty())
return;
int flags = 0;
for(PlayListTrack *track : qAsConst(tracks))
{
m_container->insertTrack(index, track);
m_total_duration += track->duration();
if(m_container->trackCount() == 1)
{
m_current_track = track;
m_current = m_container->indexOf(track);
flags |= CURRENT;
}
emit trackAdded(track);
}
//update current index
m_current = m_container->indexOf(m_current_track);
preparePlayState();
flags |= STRUCTURE;
emit listChanged(flags);
}
void PlayListModel::insert(int index, const QByteArray &json)
{
insert(index, PlayListParser::deserialize(json));
}
void PlayListModel::insert(PlayListItem *before, const QList<PlayListTrack *> &tracks)
{
if(before)
insert(m_container->indexOf(before), tracks);
else
add(tracks);
}
void PlayListModel::insert(int index, const QString &path)
{
insert(index, QStringList() << path);
}
void PlayListModel::insert(int index, const QStringList &paths)
{
if(index < 0 || index >= m_container->count())
add(paths);
else
{
PlayListItem *before = m_container->item(index);
m_loader->insert(before, paths);
}
}
void PlayListModel::insert(int index, const QList<QUrl> &urls)
{
QStringList paths;
for(const QUrl &url : qAsConst(urls))
{
if(url.scheme() == "file")
paths.append(QFileInfo(url.toLocalFile()).canonicalFilePath());
else
paths.append(url.toString());
}
insert(index, paths);
}
int PlayListModel::count() const
{
return m_container->count();
}
int PlayListModel::trackCount() const
{
return m_container->trackCount();
}
bool PlayListModel::isEmpty() const
{
return m_container->isEmpty();
}
int PlayListModel::columnCount() const
{
return MetaDataHelper::instance()->columnCount();
}
PlayListTrack* PlayListModel::currentTrack() const
{
return m_container->isEmpty() ? nullptr : m_current_track;
}
PlayListTrack *PlayListModel::nextTrack() const
{
if(m_container->isEmpty() || !m_play_state)
return nullptr;
if(m_stop_track && m_stop_track == currentTrack())
return nullptr;
if(!isEmptyQueue())
return m_queued_songs.at(0);
int index = m_play_state->nextIndex();
if(index < 0 || (index + 1 > m_container->count()))
return nullptr;
return m_container->track(index);
}
int PlayListModel::indexOf(PlayListItem* item) const
{
return m_container->indexOf(item);
}
PlayListItem* PlayListModel::item(int index) const
{
return m_container->item(index);
}
PlayListTrack* PlayListModel::track(int index) const
{
return m_container->track(index);
}
PlayListGroup* PlayListModel::group(int index) const
{
return m_container->group(index);
}
int PlayListModel::currentIndex() const
{
return m_current;
}
bool PlayListModel::setCurrent(int index)
{
if (index > count()-1 || index < 0)
return false;
PlayListItem *item = m_container->item(index);
if(item->isGroup())
{
index++;
item = m_container->item(index);
}
m_current = index;
m_current_track = dynamic_cast<PlayListTrack*> (item);
emit listChanged(CURRENT);
return true;
}
bool PlayListModel::setCurrent(PlayListTrack *track)
{
if(!m_container->contains(track))
return false;
return setCurrent(m_container->indexOf(track));
}
bool PlayListModel::isTrack(int index) const
{
if (index > count()-1 || index < 0)
return false;
return !m_container->item(index)->isGroup();
}
bool PlayListModel::isGroup(int index) const
{
if (index > count()-1 || index < 0)
return false;
return m_container->item(index)->isGroup();
}
bool PlayListModel::next()
{
if(m_stop_track == currentTrack())
{
m_stop_track = nullptr;
emit listChanged(STOP_AFTER);
return false;
}
if (!m_queued_songs.isEmpty())
{
m_current_track = m_queued_songs.dequeue();
m_current = m_container->indexOf(m_current_track);
emit listChanged(CURRENT | QUEUE);
return true;
}
if(m_loader->isRunning())
m_play_state->prepare();
return m_play_state->next();
}
bool PlayListModel::previous()
{
if (m_loader->isRunning())
m_play_state->prepare();
return m_play_state->previous();
}
void PlayListModel::clear()
{
m_loader->finish();
m_current = 0;
m_stop_track = nullptr;
m_container->clear();
m_queued_songs.clear();
m_total_duration = 0;
m_play_state->resetState();
emit listChanged(STRUCTURE | QUEUE | STOP_AFTER | CURRENT | SELECTION);
}
void PlayListModel::clearSelection()
{
m_container->clearSelection();
emit listChanged(SELECTION);
}
QList<PlayListItem *> PlayListModel::mid(int pos, int count) const
{
return m_container->mid(pos, count);
}
bool PlayListModel::isSelected(int index) const
{
return m_container->isSelected(index);
}
bool PlayListModel::contains(const QString &url)
{
for(int i = 0; i < m_container->count(); ++i)
{
PlayListTrack *t = track(i);
if(!t)
continue;
if(t->path() == url)
return true;
}
return false;
}
int PlayListModel::indexOfTrack(int index) const
{
return m_container->indexOfTrack(index);
}
PlayListTrack *PlayListModel::findTrack(int track_index) const
{
return m_container->findTrack(track_index);
}
QList<PlayListItem *> PlayListModel::findTracks(const QString &str) const
{
QList<PlayListItem *> items;
PlayListItem *item = nullptr;
if(str.isEmpty())
return items;
for(int i = 0; i < m_container->count(); ++i)
{
item = m_container->item(i);
if(item->isGroup())
continue;
if(!item->formattedTitles().filter(str, Qt::CaseInsensitive).isEmpty())
items.append(item);
}
return items;
}
void PlayListModel::setSelected(int index, bool selected)
{
m_container->setSelected(index, selected);
emit listChanged(SELECTION);
}
void PlayListModel::setSelected(const QList<PlayListTrack *> &tracks, bool selected)
{
for(PlayListTrack *t : qAsConst(tracks))
t->setSelected(selected);
emit listChanged(SELECTION);
}
void PlayListModel::setSelected(const QList<PlayListItem *> &items, bool selected)
{
for(PlayListItem *i : qAsConst(items))
i->setSelected(selected);
emit listChanged(SELECTION);
}
void PlayListModel::setSelected(int first, int last, bool selected)
{
if(first > last)
{
setSelected(last, first, selected);
return;
}
for(int index = first; index <= last; ++index)
{
PlayListItem *i = item(index);
if(!i)
continue;
i->setSelected(selected);
}
emit listChanged(SELECTION);
}
void PlayListModel::setSelected(const QList<int> &indexes, bool selected)
{
for(const int &idx : qAsConst(indexes))
m_container->setSelected(idx, selected);
emit listChanged(SELECTION);
}
void PlayListModel::removeSelected()
{
removeSelection(false);
}
void PlayListModel::removeUnselected()
{
removeSelection(true);
}
void PlayListModel::removeTrack (int i)
{
int flags = removeTrackInternal(i);
if(flags)
emit listChanged(flags);
}
void PlayListModel::removeTrack (PlayListItem *track)
{
if(m_container->contains(track))
removeTrack (m_container->indexOf(track));
}
void PlayListModel::removeTracks(const QList<PlayListItem *> &items)
{
int i = 0;
int select_after_delete = -1;
int flags = 0;
while (!m_container->isEmpty() && i < m_container->count())
{
PlayListItem *item = m_container->item(i);
if (!item->isGroup() && items.contains(item))
{
flags |= removeTrackInternal(i);
if(m_container->isEmpty())
continue;
select_after_delete = i;
}
else
i++;
}
select_after_delete = qMin(select_after_delete, m_container->count() - 1);
if(select_after_delete >= 0)
{
m_container->setSelected(select_after_delete, true);
flags |= SELECTION;
}
m_play_state->prepare();
if(flags)
emit listChanged(flags);
}
void PlayListModel::removeTracks(const QList<PlayListTrack *> &tracks)
{
QList<PlayListItem *> items;
for(PlayListTrack *track : tracks)
items << dynamic_cast<PlayListItem *>(track);
removeTracks(items);
}
void PlayListModel::removeSelection(bool inverted)
{
int i = 0;
int select_after_delete = -1;
int flags = 0;
while (!m_container->isEmpty() && i < m_container->count())
{
PlayListItem *item = m_container->item(i);
if (!item->isGroup() && item->isSelected() ^ inverted)
{
flags |= removeTrackInternal(i);
if(m_container->isEmpty())
continue;
select_after_delete = i;
}
else
i++;
}
select_after_delete = qMin(select_after_delete, m_container->count() - 1);
if(select_after_delete >= 0)
{
m_container->setSelected(select_after_delete, true);
flags |= SELECTION;
}
m_play_state->prepare();
if(flags)
emit listChanged(flags);
}
int PlayListModel::removeTrackInternal(int i)
{
if((i < 0) || (i >= count()))
return 0;
int flags = 0;
PlayListTrack* track = m_container->track(i);
if(!track)
return flags;
if(m_queued_songs.removeAll(track) > 0)
flags |= QUEUE;
m_container->removeTrack(track);
if(m_stop_track == track)
{
flags |= STOP_AFTER;
m_stop_track = nullptr;
}
if(track->isSelected())
flags |= SELECTION;
m_total_duration -= track->duration();
m_total_duration = qMax(Q_INT64_C(0), m_total_duration);
if(m_current_track == track)
{
flags |= CURRENT;
if(m_container->isEmpty())
m_current_track = nullptr;
else
{
m_current = i > 0 ? qMin(i - 1, m_container->count() - 1) : 0;
if(!(m_current_track = m_container->track(m_current)))
{
m_current_track = m_current > 0 ? m_container->track(m_current - 1) :
m_container->track(1);
}
}
}
if (track->isUsed())
track->deleteLater();
else
delete track;
m_current = m_current_track ? m_container->indexOf(m_current_track) : -1;
m_play_state->prepare();
flags |= STRUCTURE;
return flags;
}
void PlayListModel::invertSelection()
{
for (int i = 0; i < m_container->count(); ++i)
m_container->setSelected(i, !m_container->isSelected(i));
emit listChanged(SELECTION);
}
void PlayListModel::selectAll()
{
for (int i = 0; i < m_container->count(); ++i)
m_container->setSelected(i, true);
emit listChanged(SELECTION);
}
void PlayListModel::showDetails(QWidget *parent)
{
QList<PlayListTrack *> selected_tracks;
for (int i = 0; i < m_container->count(); ++i)
{
if(!m_container->isSelected(i))
continue;
PlayListTrack *track = m_container->track(i);
if(track)
selected_tracks.append(track);
}
if(!selected_tracks.isEmpty())
{
QDialog *d = new DetailsDialog(selected_tracks, parent);
d->setAttribute(Qt::WA_DeleteOnClose, true);
connect(d, SIGNAL(destroyed(QObject *)),SLOT(updateMetaData()));
d->show();
}
}
void PlayListModel::showDetailsForCurrent(QWidget *parent)
{
if (m_current_track)
{
QList<PlayListTrack *> l;
l.append(m_current_track);
QDialog *d = new DetailsDialog(l, parent);
d->setAttribute(Qt::WA_DeleteOnClose, true);
connect(d, SIGNAL(destroyed(QObject *)),SLOT(updateMetaData()));
d->show();
}
}
int PlayListModel::firstSelectedUpper(int row)
{
for (int i = row - 1;i >= 0;i--)
{
if (isSelected(i))
return i;
}
return -1;
}
int PlayListModel::firstSelectedLower(int row)
{
for (int i = row + 1;i < count() ;i++)
{
if (isSelected(i))
return i;
}
return -1;
}
qint64 PlayListModel::totalDuration() const
{
return m_total_duration;
}
void PlayListModel::moveItems(int from, int to)
{
// Get rid of useless work
if (from == to)
return;
QList<int> selected_indexes = selectedIndexes();
if(selected_indexes.isEmpty())
return;
if(std::any_of(selected_indexes.cbegin(), selected_indexes.cend(), [this](int i){ return !isTrack(i); }))
return;
if (bottommostInSelection(from) == INVALID_INDEX ||
from == INVALID_INDEX ||
topmostInSelection(from) == INVALID_INDEX)
return;
if(m_container->move(selected_indexes, from, to))
{
m_current = m_container->indexOf(m_current_track);
emit listChanged(STRUCTURE);
}
}
int PlayListModel::topmostInSelection(int row)
{
if (row == 0)
return 0;
for (int i = row - 1;i >= 0;i--)
{
if (isSelected(i))
continue;
else
return i + 1;
}
return 0;
}
int PlayListModel::bottommostInSelection(int row)
{
if (row >= count() - 1)
return row;
for (int i = row + 1; i < count(); i++)
{
if (isSelected(i))
continue;
else
return i - 1;
}
return count() - 1;
}
const SimpleSelection& PlayListModel::getSelection(int row)
{
m_selection.m_top = topmostInSelection(row);
m_selection.m_bottom = bottommostInSelection(row);
m_selection.m_selected_indexes = selectedIndexes();
return m_selection;
}
QList<int> PlayListModel::selectedIndexes() const
{
QList<int> selected_rows;
for (int i = 0; i < m_container->count(); i++)
{
if (m_container->item(i)->isSelected())
{
selected_rows.append(i);
}
}
return selected_rows;
}
QList<PlayListTrack *> PlayListModel::selectedTracks() const
{
QList<PlayListTrack*> selected_tracks;
for(PlayListItem *item : m_container->items())
{
if(!item->isGroup() && item->isSelected())
selected_tracks.append(dynamic_cast<PlayListTrack *>(item));
}
return selected_tracks;
}
QList<PlayListItem *> PlayListModel::items() const
{
return m_container->items();
}
void PlayListModel::addToQueue()
{
const QList<PlayListTrack*> selected_tracks = selectedTracks();
blockSignals(true);
for(PlayListTrack *track : qAsConst(selected_tracks))
setQueued(track);
blockSignals(false);
emit listChanged(QUEUE);
}
void PlayListModel::setQueued(PlayListTrack *item)
{
if (isQueued(item))
m_queued_songs.removeAll(item);
else
m_queued_songs.enqueue(item);
emit listChanged(QUEUE);
}
bool PlayListModel::isQueued(PlayListTrack *f) const
{
return m_queued_songs.contains(f);
}
bool PlayListModel::isEmptyQueue() const
{
return m_queued_songs.isEmpty();
}
int PlayListModel::queuedIndex(PlayListTrack *track) const
{
return m_queued_songs.indexOf(track);
}
int PlayListModel::queueSize() const
{
return m_queued_songs.size();
}
bool PlayListModel::isStopAfter(const PlayListItem *item) const
{
return m_stop_track == item;
}
void PlayListModel::randomizeList()
{
if(m_container->isEmpty())
return;
m_container->randomizeList();
m_current = m_container->indexOf(m_current_track);
emit listChanged(STRUCTURE);
}
void PlayListModel::reverseList()
{
if(m_container->isEmpty())
return;
m_container->reverseList();
m_current = m_container->indexOf(m_current_track);
emit listChanged(STRUCTURE);
}
void PlayListModel::sortSelection(SortMode mode)
{
if(m_container->isEmpty())
return;
m_task->sortSelection(m_container->tracks(), mode);
}
void PlayListModel::sort(SortMode mode)
{
if(m_container->isEmpty())
return;
m_task->sort(m_container->tracks(), mode);
}
void PlayListModel::sortByColumn(int column)
{
if(m_container->isEmpty())
return;
if(column < 0 || column >= columnCount())
return;
m_task->sortByColumn(m_container->tracks(), column);
}
void PlayListModel::prepareForShufflePlaying(bool val)
{
if (m_play_state)
delete m_play_state;
if (val)
m_play_state = new ShufflePlayState(this);
else
m_play_state = new NormalPlayState(this);
}
void PlayListModel::prepareGroups(bool enabled)
{
PlayListContainer *container = nullptr;
if(enabled)
container = new GroupedContainer;
else
container = new NormalContainer;
container->addTracks(m_container->takeAllTracks());
delete m_container;
m_container = container;
if(!m_container->isEmpty())
m_current = m_container->indexOf(m_current_track);
emit listChanged(STRUCTURE);
}
void PlayListModel::updateMetaData()
{
emit listChanged(METADATA);
}
void PlayListModel::onTaskFinished()
{
if(m_task->isChanged(m_container)) //update unchanged container only
{
m_task->clear();
return;
}
if(m_task->type() == PlayListTask::SORT || m_task->type() == PlayListTask::SORT_SELECTION)
{
m_container->replaceTracks(m_task->takeResults(&m_current_track));
m_current = m_container->indexOf(m_current_track);
emit listChanged(STRUCTURE);
}
else if(m_task->type() == PlayListTask::SORT_BY_COLUMN)
{
m_container->replaceTracks(m_task->takeResults(&m_current_track));
m_current = m_container->indexOf(m_current_track);
emit listChanged(STRUCTURE);
emit sortingByColumnFinished(m_task->column(), m_task->isReverted());
}
else if(m_task->type() == PlayListTask::REMOVE_INVALID
|| m_task->type() == PlayListTask::REMOVE_DUPLICATES
|| m_task->type() == PlayListTask::REFRESH)
{
PlayListTrack *prev_current_track = m_current_track;
int prev_count = m_container->count();
m_container->replaceTracks(m_task->takeResults(&m_current_track));
if(prev_count != m_container->count())
{
int flags = STRUCTURE;
m_current = m_container->indexOf(m_current_track);
if(prev_current_track != m_current_track)
flags |= CURRENT;
if(m_stop_track && !m_container->contains(m_stop_track))
{
m_stop_track = nullptr;
flags |= STOP_AFTER;
}
QList<PlayListTrack *>::iterator it = m_queued_songs.begin();
while(it != m_queued_songs.end())
{
if(!m_container->contains(*it))
{
flags |= QUEUE;
it = m_queued_songs.erase(it);
}
else
{
++it;
}
}
emit listChanged(flags);
}
}
}
void PlayListModel::doCurrentVisibleRequest()
{
if(!m_container->isEmpty() && m_current >= 0)
emit scrollToRequest(currentIndex());
}
void PlayListModel::scrollTo(int index)
{
if(index >= 0 && index < m_container->count())
emit scrollToRequest(index);
}
void PlayListModel::loadPlaylist(const QString &f_name)
{
m_loader->add(f_name);
}
void PlayListModel::loadPlaylist(const QString &fmt, const QByteArray &data)
{
m_loader->addPlayList(fmt, data);
}
void PlayListModel::savePlaylist(const QString &f_name)
{
QList <PlayListTrack *> songs;
for(int i = 0; i < m_container->count(); ++i)
{
if(isTrack(i))
songs << m_container->track(i);
}
PlayListParser::savePlayList(songs, f_name);
}
bool PlayListModel::isLoaderRunning() const
{
return m_loader->isRunning();
}
void PlayListModel::preparePlayState()
{
m_play_state->prepare();
}
void PlayListModel::removeInvalidTracks()
{
m_task->removeInvalidTracks(m_container->tracks(), m_current_track);
}
void PlayListModel::removeDuplicates()
{
m_task->removeDuplicates(m_container->tracks(), m_current_track);
}
void PlayListModel::refresh()
{
m_task->refresh(m_container->tracks(), m_current_track);
}
void PlayListModel::clearQueue()
{
m_queued_songs.clear();
m_stop_track = nullptr;
emit listChanged(QUEUE);
}
void PlayListModel::stopAfterSelected()
{
QList<PlayListTrack*> selected_tracks = selectedTracks();
int flags = STOP_AFTER;
if(!m_queued_songs.isEmpty())
{
m_stop_track = m_stop_track != m_queued_songs.last() ? m_queued_songs.last() : nullptr;
}
else if(selected_tracks.count() == 1)
{
m_stop_track = m_stop_track != selected_tracks.at(0) ? selected_tracks.at(0) : nullptr;
}
else if(selected_tracks.count() > 1)
{
blockSignals(true);
addToQueue();
blockSignals(false);
flags |= QUEUE;
m_stop_track = m_queued_songs.last();
}
else
return;
emit listChanged(flags);
}
void PlayListModel::rebuildGroups()
{
if(m_ui_settings->isGroupsEnabled())
prepareGroups(true);
}