You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
552 lines
17 KiB
552 lines
17 KiB
/*****************************************************************************
|
|
* Copyright (C) 2021 VLC authors and VideoLAN
|
|
*
|
|
* 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.
|
|
*****************************************************************************/
|
|
|
|
#if HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
|
|
#include "mlplaylistmodel.hpp"
|
|
|
|
// Util includes
|
|
#include "util/qmlinputitem.hpp"
|
|
|
|
// MediaLibrary includes
|
|
#include "mlhelper.hpp"
|
|
#include "mlplaylistmedia.hpp"
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// Static variables
|
|
|
|
static const QHash<QByteArray, vlc_ml_sorting_criteria_t> criterias =
|
|
{
|
|
{"id", VLC_ML_SORTING_DEFAULT},
|
|
{"title", VLC_ML_SORTING_ALPHA},
|
|
{"duration", VLC_ML_SORTING_DURATION},
|
|
{"playcount", VLC_ML_SORTING_PLAYCOUNT},
|
|
};
|
|
|
|
//=================================================================================================
|
|
// MLPlaylistModel
|
|
//=================================================================================================
|
|
|
|
/* explicit */ MLPlaylistModel::MLPlaylistModel(QObject * parent)
|
|
: MLBaseModel(parent) {}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// Interface
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
/* Q_INVOKABLE */ void MLPlaylistModel::insert(const QVariantList & items, int at)
|
|
{
|
|
assert(m_mediaLib);
|
|
|
|
int64_t id = parentId().id;
|
|
|
|
assert(id);
|
|
|
|
if (unlikely(m_transactionPending))
|
|
return;
|
|
|
|
//build the list of MRL to insert in the playlist
|
|
std::vector<QString> mrlList;
|
|
for (const QVariant & variant : items)
|
|
{
|
|
if (variant.canConvert<QmlInputItem>() == false)
|
|
continue;
|
|
|
|
const QmlInputItem & item = variant.value<QmlInputItem>();
|
|
|
|
const char * psz_uri = item.item ? item.item->psz_uri : nullptr;
|
|
|
|
if (psz_uri == nullptr)
|
|
continue;
|
|
|
|
mrlList.emplace_back(psz_uri);
|
|
}
|
|
|
|
m_transactionPending = true;
|
|
|
|
m_mediaLib->runOnMLThread(this,
|
|
//ML thread
|
|
[mrlList, id, at](vlc_medialibrary_t* ml) {
|
|
int insertPos = at;
|
|
for (const QString& uri : mrlList)
|
|
{
|
|
vlc_ml_media_t * media = vlc_ml_get_media_by_mrl(ml, qtu(uri));
|
|
|
|
if (media == nullptr)
|
|
{
|
|
media = vlc_ml_new_external_media(ml, qtu(uri));
|
|
if (media == nullptr)
|
|
continue;
|
|
}
|
|
|
|
vlc_ml_playlist_insert(ml, id, media->i_id, insertPos);
|
|
vlc_ml_media_release(media);
|
|
|
|
insertPos++;
|
|
}
|
|
|
|
},
|
|
//UI thread
|
|
[this]() {
|
|
m_need_reset = true;
|
|
endTransaction();
|
|
});
|
|
}
|
|
|
|
|
|
void MLPlaylistModel::moveImpl(int64_t playlistId, HighLowRanges&& ranges)
|
|
{
|
|
struct Ctx {
|
|
int newTo;
|
|
};
|
|
int low;
|
|
int high;
|
|
if (ranges.lowRangeIt > 0)
|
|
{
|
|
std::tie(low, high) = ranges.lowRanges[ranges.lowRangeIt - 1];
|
|
|
|
assert(low <= high);
|
|
m_mediaLib->runOnMLThread<Ctx>(this,
|
|
//ML thread
|
|
[playlistId, high, low, to = ranges.lowTo]
|
|
(vlc_medialibrary_t* ml, Ctx& ctx) {
|
|
int localTo = to;
|
|
for (int i = high; i >= low; i--)
|
|
{
|
|
vlc_ml_playlist_move(ml, playlistId, i, localTo - 1);
|
|
localTo--;
|
|
}
|
|
ctx.newTo = localTo;
|
|
},
|
|
//UI thread
|
|
[this, playlistId, high, low, r = std::move(ranges)](quint64, Ctx& ctx) mutable {
|
|
moveRangeInCache(low, high, r.lowTo);
|
|
r.lowTo = ctx.newTo;
|
|
--r.lowRangeIt;
|
|
moveImpl(playlistId, std::move(r));
|
|
});
|
|
}
|
|
else if (ranges.highRangeIt < ranges.highRanges.size())
|
|
{
|
|
std::tie(low, high) = ranges.highRanges[ranges.highRangeIt];
|
|
assert(low <= high);
|
|
|
|
m_mediaLib->runOnMLThread<Ctx>(this,
|
|
//ML thread
|
|
[playlistId, high, low, to = ranges.highTo](vlc_medialibrary_t* ml, Ctx& ctx) {
|
|
int localTo = to;
|
|
for (int i = low; i <= high; i++)
|
|
{
|
|
vlc_ml_playlist_move(ml, playlistId, i, localTo);
|
|
localTo++;
|
|
}
|
|
ctx.newTo = localTo;
|
|
},
|
|
//UI thread
|
|
[this, playlistId, low, high, r = std::move(ranges)](quint64, Ctx& ctx) mutable {
|
|
moveRangeInCache(low, high, r.highTo);
|
|
r.highTo = ctx.newTo;
|
|
++r.highRangeIt;
|
|
moveImpl(playlistId, std::move(r));
|
|
});
|
|
}
|
|
else
|
|
{
|
|
//we're done
|
|
endTransaction();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* the move operation is done in separate phases
|
|
* * we sort the items, and organise them as "ranges" of consecutive items to
|
|
* be move (think beginMoveRows/endMoveRows)
|
|
*
|
|
* * and split ranges between thoose who are before and
|
|
* those who are after the destination point
|
|
*
|
|
* * for each range to move, we will
|
|
* * beginMoveRows(range)
|
|
* * move the items in the range on ML thread
|
|
* * endMoveRows on the UI thread
|
|
* * move the next range
|
|
*
|
|
* * the ranges before the destination needs to be moved from the closest to the
|
|
* destination point to the farthest, so this doesn't mess the indexe, the
|
|
* same policy applies when moving the items from a given range for the same
|
|
* reason. Having the data split in before/after the destination point, allows to
|
|
* do it sequentially without having to guess what is the next segment to move
|
|
*/
|
|
/* Q_INVOKABLE */ void MLPlaylistModel::move(const QModelIndexList & indexes, int to)
|
|
{
|
|
assert(m_mediaLib);
|
|
|
|
if (indexes.size() == 0)
|
|
return;
|
|
|
|
if (unlikely(m_transactionPending))
|
|
return;
|
|
|
|
int64_t id = parentId().id;
|
|
assert(id);
|
|
//get Ranges in asc order
|
|
auto rangeList = getSortedRowsRanges(indexes, true);
|
|
assert(rangeList.size() > 0);
|
|
|
|
//split ranges between those who are above and those who below the destination
|
|
HighLowRanges highLowRanges;
|
|
highLowRanges.lowTo = to;
|
|
highLowRanges.highTo = to;
|
|
|
|
auto it = rangeList.cbegin();
|
|
while (it != rangeList.end())
|
|
{
|
|
if (it->second < to - 1)
|
|
highLowRanges.lowRanges.push_back(*it);
|
|
else if (it->first > to)
|
|
highLowRanges.highRanges.push_back(*it);
|
|
else
|
|
{
|
|
//range is overlapping destination, it doesn't need to be moved
|
|
highLowRanges.lowTo = it->first;
|
|
highLowRanges.highTo = it->second;
|
|
}
|
|
++it;
|
|
}
|
|
highLowRanges.lowRangeIt = highLowRanges.lowRanges.size();
|
|
highLowRanges.highRangeIt = 0;
|
|
|
|
m_transactionPending = true;
|
|
|
|
moveImpl(id, std::move(highLowRanges));
|
|
}
|
|
|
|
|
|
void MLPlaylistModel::removeImpl(int64_t playlistId, const std::vector<std::pair<int, int> >&& rangeList, size_t index)
|
|
{
|
|
if (index >= rangeList.size())
|
|
{
|
|
//we're done
|
|
endTransaction();
|
|
return;
|
|
}
|
|
|
|
std::pair<int, int> range = rangeList[index];
|
|
|
|
m_mediaLib->runOnMLThread(this,
|
|
//ML thread
|
|
[playlistId, range](vlc_medialibrary_t* ml) {
|
|
for (int i = range.second; i >= range.first; i--)
|
|
{
|
|
vlc_ml_playlist_remove(ml, playlistId, i);
|
|
}
|
|
},
|
|
//UI thread
|
|
[this, playlistId, range, rows = std::move(rangeList), index]() {
|
|
deleteRangeInCache(range.first, range.second);
|
|
removeImpl(playlistId, std::move(rows), index+1);
|
|
});
|
|
}
|
|
|
|
/* Q_INVOKABLE */ void MLPlaylistModel::remove(const QModelIndexList & indexes)
|
|
{
|
|
assert(m_mediaLib);
|
|
if (indexes.size() == 0)
|
|
return;
|
|
|
|
if (unlikely(m_transactionPending))
|
|
return;
|
|
|
|
int64_t id = parentId().id;
|
|
assert(id);
|
|
|
|
//get range in decreasing order to avoid having to update the index
|
|
auto rangeList = getSortedRowsRanges(indexes, false);
|
|
assert(rangeList.size() > 0);
|
|
|
|
m_transactionPending = true;
|
|
removeImpl(id, std::move(rangeList), 0);
|
|
}
|
|
|
|
void MLPlaylistModel::endTransaction()
|
|
{
|
|
m_transactionPending = false;
|
|
if (m_resetAfterTransaction)
|
|
{
|
|
m_resetAfterTransaction = false;
|
|
emit resetRequested();
|
|
}
|
|
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// QAbstractItemModel implementation
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
QHash<int, QByteArray> MLPlaylistModel::roleNames() const /* override */
|
|
{
|
|
return
|
|
{
|
|
{ MEDIA_ID, "id" },
|
|
{ MEDIA_IS_NEW, "isNew" },
|
|
{ MEDIA_TITLE, "title" },
|
|
{ MEDIA_THUMBNAIL, "thumbnail" },
|
|
{ MEDIA_DURATION, "duration" },
|
|
{ MEDIA_PROGRESS, "progress" },
|
|
{ MEDIA_PLAYCOUNT, "playcount" },
|
|
{ MEDIA_RESOLUTION, "resolution_name" },
|
|
{ MEDIA_CHANNEL, "channel" },
|
|
{ MEDIA_MRL, "mrl" },
|
|
{ MEDIA_DISPLAY_MRL, "display_mrl" },
|
|
{ MEDIA_AUDIO_TRACK, "audioDesc" },
|
|
{ MEDIA_VIDEO_TRACK, "videoDesc" },
|
|
{ MEDIA_TITLE_FIRST_SYMBOL, "title_first_symbol" }
|
|
};
|
|
}
|
|
|
|
QVariant MLPlaylistModel::itemRoleData(MLItem *item, int role) const /* override */
|
|
{
|
|
MLPlaylistMedia * media = static_cast<MLPlaylistMedia *>(item);
|
|
if (media == nullptr)
|
|
return QVariant();
|
|
|
|
switch (role)
|
|
{
|
|
case MEDIA_ID:
|
|
return QVariant::fromValue(media->getId());
|
|
case MEDIA_IS_NEW:
|
|
return QVariant::fromValue(media->isNew());
|
|
case MEDIA_TITLE:
|
|
return QVariant::fromValue(media->getTitle());
|
|
case MEDIA_THUMBNAIL:
|
|
{
|
|
vlc_ml_thumbnail_status_t status;
|
|
QString thumbnail = media->getThumbnail(&status);
|
|
if (media->getType() == VLC_ML_MEDIA_TYPE_AUDIO
|
|
&& (status == VLC_ML_THUMBNAIL_STATUS_MISSING || status == VLC_ML_THUMBNAIL_STATUS_FAILURE))
|
|
{
|
|
generateThumbnail(item->getId());
|
|
}
|
|
return QVariant::fromValue(thumbnail);
|
|
}
|
|
case MEDIA_DURATION:
|
|
return QVariant::fromValue(media->getDuration());
|
|
case MEDIA_PROGRESS:
|
|
return QVariant::fromValue(media->getProgress());
|
|
case MEDIA_PLAYCOUNT:
|
|
return QVariant::fromValue(media->getPlayCount());
|
|
case MEDIA_RESOLUTION:
|
|
return QVariant::fromValue(media->getResolutionName());
|
|
case MEDIA_CHANNEL:
|
|
return QVariant::fromValue(media->getChannel());
|
|
case MEDIA_MRL:
|
|
return QVariant::fromValue(media->getMRL());
|
|
case MEDIA_DISPLAY_MRL:
|
|
return QVariant::fromValue(media->getMRLDisplay());
|
|
case MEDIA_VIDEO_TRACK:
|
|
return QVariant::fromValue(media->getVideo());
|
|
case MEDIA_AUDIO_TRACK:
|
|
return QVariant::fromValue(media->getAudio());
|
|
case MEDIA_TITLE_FIRST_SYMBOL:
|
|
return QVariant::fromValue(getFirstSymbol(media->getTitle()));
|
|
default:
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// Protected MLBaseModel implementation
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
vlc_ml_sorting_criteria_t MLPlaylistModel::roleToCriteria(int role) const /* override */
|
|
{
|
|
switch (role)
|
|
{
|
|
case MEDIA_TITLE:
|
|
return VLC_ML_SORTING_ALPHA;
|
|
case MEDIA_DURATION:
|
|
return VLC_ML_SORTING_DURATION;
|
|
case MEDIA_PLAYCOUNT:
|
|
return VLC_ML_SORTING_PLAYCOUNT;
|
|
default:
|
|
return VLC_ML_SORTING_DEFAULT;
|
|
}
|
|
}
|
|
|
|
vlc_ml_sorting_criteria_t MLPlaylistModel::nameToCriteria(QByteArray name) const /* override */
|
|
{
|
|
return criterias.value(name, VLC_ML_SORTING_DEFAULT);
|
|
}
|
|
|
|
QByteArray MLPlaylistModel::criteriaToName(vlc_ml_sorting_criteria_t criteria) const /* override */
|
|
{
|
|
return criterias.key(criteria, "");
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
std::unique_ptr<MLBaseModel::BaseLoader>
|
|
MLPlaylistModel::createLoader() const /* override */
|
|
{
|
|
return std::make_unique<Loader>(*this);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// Protected MLBaseModel reimplementation
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
void MLPlaylistModel::onVlcMlEvent(const MLEvent & event) /* override */
|
|
{
|
|
switch (event.i_type)
|
|
{
|
|
case VLC_ML_EVENT_MEDIA_UPDATED:
|
|
{
|
|
MLItemId itemId(event.modification.i_entity_id, VLC_ML_PARENT_UNKNOWN);
|
|
updateItemInCache(itemId);
|
|
return;
|
|
}
|
|
case VLC_ML_EVENT_PLAYLIST_UPDATED:
|
|
{
|
|
MLItemId itemId(event.modification.i_entity_id, VLC_ML_PARENT_PLAYLIST);
|
|
if (m_parent == itemId)
|
|
{
|
|
if (m_transactionPending)
|
|
m_resetAfterTransaction = true;
|
|
else
|
|
emit resetRequested();
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
MLBaseModel::onVlcMlEvent(event);
|
|
}
|
|
|
|
void MLPlaylistModel::thumbnailUpdated(const QModelIndex& idx, MLItem* mlitem, const QString& mrl, vlc_ml_thumbnail_status_t status) /* override */
|
|
{
|
|
auto playlistItem = static_cast<MLPlaylistMedia*>(mlitem);
|
|
playlistItem->setThumbnail(mrl, status);
|
|
emit dataChanged(idx, idx, { MEDIA_THUMBNAIL });
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------------
|
|
// Private functions
|
|
//-------------------------------------------------------------------------------------------------
|
|
|
|
std::vector<std::pair<int, int>> MLPlaylistModel::getSortedRowsRanges(const QModelIndexList & indexes, bool asc) const
|
|
{
|
|
assert (indexes.size() > 0);;
|
|
|
|
QList<int> rows;
|
|
for (const QModelIndex & index : indexes)
|
|
rows.append(index.row());
|
|
|
|
std::sort(rows.begin(), rows.end());
|
|
|
|
//build continuous ranges
|
|
std::vector<std::pair<int, int>> rangeList;
|
|
auto it = rows.cbegin();
|
|
int high = *it;
|
|
int low = *it;
|
|
if (rows.count() == 1)
|
|
{
|
|
rangeList.emplace_back(low, high);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
while (it != rows.cend())
|
|
{
|
|
int value = *it;
|
|
if (value != high + 1)
|
|
{
|
|
rangeList.emplace_back(low, high);
|
|
low = value;
|
|
}
|
|
high = value;
|
|
++it;
|
|
}
|
|
rangeList.emplace_back(low, high);
|
|
}
|
|
|
|
if (!asc)
|
|
std::reverse(rangeList.begin(), rangeList.end());
|
|
|
|
return rangeList;
|
|
}
|
|
|
|
void MLPlaylistModel::generateThumbnail(const MLItemId& itemid) const
|
|
{
|
|
m_mediaLib->runOnMLThread(this,
|
|
//ML thread
|
|
[id = itemid.id](vlc_medialibrary_t* ml)
|
|
{
|
|
vlc_ml_media_generate_thumbnail(ml, id, VLC_ML_THUMBNAIL_SMALL,
|
|
512, 320, 0.15);
|
|
});
|
|
}
|
|
|
|
//=================================================================================================
|
|
// Loader
|
|
//=================================================================================================
|
|
|
|
MLPlaylistModel::Loader::Loader(const MLPlaylistModel & model) : MLBaseModel::BaseLoader(model) {}
|
|
|
|
size_t MLPlaylistModel::Loader::count(vlc_medialibrary_t* ml) const /* override */
|
|
{
|
|
vlc_ml_query_params_t params = getParams().toCQueryParams();
|
|
|
|
return vlc_ml_count_playlist_media(ml, ¶ms, m_parent.id);
|
|
}
|
|
|
|
std::vector<std::unique_ptr<MLItem>>
|
|
MLPlaylistModel::Loader::load(vlc_medialibrary_t* ml, size_t index, size_t count) const /* override */
|
|
{
|
|
vlc_ml_query_params_t params = getParams(index, count).toCQueryParams();
|
|
|
|
ml_unique_ptr<vlc_ml_media_list_t> list {
|
|
vlc_ml_list_playlist_media(ml, ¶ms, m_parent.id)
|
|
};
|
|
|
|
if (list == nullptr)
|
|
return {};
|
|
|
|
std::vector<std::unique_ptr<MLItem>> result;
|
|
|
|
for (const vlc_ml_media_t & media : ml_range_iterate<vlc_ml_media_t> (list))
|
|
{
|
|
result.emplace_back(std::make_unique<MLPlaylistMedia>(&media));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<MLItem>
|
|
MLPlaylistModel::Loader::loadItemById(vlc_medialibrary_t* ml, MLItemId itemId) const
|
|
{
|
|
assert(itemId.type == VLC_ML_PARENT_UNKNOWN);
|
|
ml_unique_ptr<vlc_ml_media_t> media(vlc_ml_get_media(ml, itemId.id));
|
|
if (!media)
|
|
return nullptr;
|
|
return std::make_unique<MLPlaylistMedia>(media.get());
|
|
}
|
|
|