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.
 
 
 
 
 
 

814 lines
26 KiB

/*****************************************************************************
* Copyright (C) 2019 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.
*****************************************************************************/
#include "networkmediamodel.hpp"
#include "mediatreelistener.hpp"
#include "maininterface/mainctx.hpp"
#include "util/locallistbasemodel.hpp"
#include "playlist/media.hpp"
#include "playlist/playlist_controller.hpp"
#include <unordered_set>
#include <QSemaphore>
#include <QDateTime>
#include <QTimeZone>
namespace {
//use the same queue as in mlfoldermodel
static const char* const ML_FOLDER_ADD_QUEUE = "ML_FOLDER_ADD_QUEUE";
struct NetworkMediaItem
{
QString name;
QUrl mainMrl;
QString protocol;
bool indexed;
NetworkMediaModel::ItemType type;
bool canBeIndexed;
NetworkTreeItem tree;
QString artwork;
qint64 fileSize;
QDateTime fileModified;
};
using NetworkMediaItemPtr = std::shared_ptr<NetworkMediaItem>;
using NetworkMediaItemSet = std::unordered_set<NetworkMediaItemPtr>;
inline bool isADir(const NetworkMediaItemPtr& x)
{
return (x->type == NetworkMediaModel::ItemType::TYPE_DIRECTORY);
}
}
// ListCache specialisation
template<>
bool ListCache<NetworkMediaItemPtr>::compareItems(const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b)
{
//just compare the pointers here
return a == b;
}
// NetworkMediaModelPrivate
class NetworkMediaModelPrivate
: public LocalListBaseModelPrivate<NetworkMediaItemPtr>
{
Q_DECLARE_PUBLIC(NetworkMediaModel)
public:
NetworkMediaModelPrivate(NetworkMediaModel* pub)
: LocalListBaseModelPrivate<NetworkMediaItemPtr>(pub)
, m_preparseSem(1)
{}
public:
const NetworkMediaItem* getItemForRow(int row) const
{
const NetworkMediaItemPtr* ref = item(row);
if (ref)
return ref->get();
return nullptr;
}
void setIntexedState(const NetworkMediaItemPtr& item, bool indexed)
{
if (item->indexed == indexed)
return;
auto it = std::find(m_items.begin(), m_items.end(), item);
if (it != m_items.end())
{
//contruct by copy, don't mutate items in m_items,
//we want the change to be notified
auto newItem = std::make_shared<NetworkMediaItem>(*item);
newItem->indexed = indexed;
*it = newItem;
++m_revision;
invalidateCache();
}
}
void removeItem(const SharedInputItem& node, const std::vector<SharedInputItem>& itemsList)
{
Q_Q(NetworkMediaModel);
if (node != q->m_treeItem.media)
return;
for (auto p_item : itemsList)
{
QUrl itemUri = QUrl::fromEncoded(p_item->psz_uri);
auto it = std::find_if(
m_items.begin(), m_items.end(),
[&](const NetworkMediaItemPtr& i) {
return QString::compare( qfu(p_item->psz_name), i->name, Qt::CaseInsensitive ) == 0 &&
itemUri.scheme() == i->mainMrl.scheme();
});
if ( it == m_items.end() )
continue;
m_items.erase( it );
}
}
void refreshMediaList(
MediaTreePtr tree,
std::vector<SharedInputItem> children,
bool clear )
{
Q_Q(NetworkMediaModel);
if (clear)
m_items.clear();
std::vector<NetworkMediaItem> items;
for (const auto& inputItem: children)
{
auto item = std::make_shared<NetworkMediaItem>();
item->name = inputItem->psz_name;
item->protocol = "";
item->indexed = false;
item->type = static_cast<NetworkMediaModel::ItemType>(inputItem->i_type);
item->mainMrl = (item->type == NetworkMediaModel::TYPE_DIRECTORY || item->type == NetworkMediaModel::TYPE_NODE) ?
QUrl::fromEncoded(QByteArray(inputItem->psz_uri).append('/')) :
QUrl::fromEncoded(inputItem->psz_uri);
item->canBeIndexed = canBeIndexed( item->mainMrl , item->type );
input_item_t* intputItemRaw = inputItem.get();
char* str = input_item_GetArtworkURL(intputItemRaw);
if (str)
{
item->artwork = QString::fromUtf8(str);
free(str);
}
str = input_item_GetInfo(intputItemRaw, ".stat", "size");
if (str)
{
item->fileSize = QString(str).toLongLong();
free(str);
}
else
item->fileSize = 0;
str = input_item_GetInfo(intputItemRaw, ".stat", "mtime");
if (str)
{
bool ok;
qint64 time = QString(str).toLongLong(&ok);
free(str);
if (ok)
item->fileModified = QDateTime::fromSecsSinceEpoch(time, QTimeZone::systemTimeZone());
}
item->tree = NetworkTreeItem( tree, inputItem.get() );
if ( m_mediaLib && item->canBeIndexed)
{
struct Ctx {
bool succeed;
bool isIndexed;
};
m_mediaLib->runOnMLThread<Ctx>(q,
//ML thread
[item](vlc_medialibrary_t* ml, Ctx& ctx){
auto ret = vlc_ml_is_indexed( ml, qtu(item->mainMrl.toString( QUrl::FullyEncoded )), &ctx.isIndexed );
ctx.succeed = (ret == VLC_SUCCESS);
},
//UI thread
[this, item](quint64, Ctx& ctx){
if (!ctx.succeed)
return;
setIntexedState(item, ctx.isIndexed);
});
}
m_items.push_back(item);
}
++m_revision;
invalidateCache();
}
bool canBeIndexed(const QUrl& url , NetworkMediaModel::ItemType itemType )
{
return m_mediaLib
&& static_cast<input_item_type_e>(itemType) != ITEM_TYPE_FILE
&& (url.scheme() == "smb" || url.scheme() == "ftp" || url.scheme() == "file");
}
//BaseModelPrivateT interface implementation
public:
LocalListCacheLoader<NetworkMediaItemPtr>::ItemCompare getSortFunction() const override
{
if (m_sortCriteria == "mrl" )
{
if (m_sortOrder == Qt::SortOrder::DescendingOrder)
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return QString::compare(a->mainMrl.toString(), b->mainMrl.toString()) > 0;
};
else
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return QString::compare(a->mainMrl.toString(), b->mainMrl.toString()) < 0;
};
}
else if (m_sortCriteria == "fileSizeRaw64" )
{
if (m_sortOrder == Qt::SortOrder::DescendingOrder)
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return a->fileSize < b->fileSize;
};
else
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return a->fileSize > b->fileSize;
};
}
else if (m_sortCriteria == "fileModified")
{
if (m_sortOrder == Qt::SortOrder::DescendingOrder)
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return a->fileModified < b->fileModified;
};
else
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return a->fileModified > b->fileModified;
};
}
else // m_sortCriteria == "name"
{
if (m_sortOrder == Qt::SortOrder::DescendingOrder)
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return QString::compare(a->name, b->name, Qt::CaseInsensitive) > 0;
};
else
return [](const NetworkMediaItemPtr& a, const NetworkMediaItemPtr& b) -> bool {
if(isADir(a) != isADir(b)) return isADir(a);
return QString::compare(a->name, b->name, Qt::CaseInsensitive) < 0;
};
}
}
bool initializeModel() override
{
Q_Q(NetworkMediaModel);
if (!q->m_ctx || !m_hasTree || m_qmlInitializing)
return false;
auto libvlc = vlc_object_instance(q->m_ctx->getIntf());
m_listener.reset();
m_items.clear();
if (!q->m_treeItem)
return false;
auto tree = q->m_treeItem.tree.get();
auto l = std::make_unique<MediaTreeListener>( q->m_treeItem.tree,
std::make_unique<NetworkMediaModel::ListenerCb>(q) );
if ( l->listener == nullptr )
return false;
if (q->m_treeItem.media)
{
q->m_name = q->m_treeItem.media->psz_name;
emit q->nameChanged();
q->m_url = QUrl::fromEncoded( QByteArray{ q->m_treeItem.media->psz_uri }.append( '/' ) );
emit q->urlChanged();
q->m_type = static_cast<NetworkMediaModel::ItemType>(q->m_treeItem.media->i_type);
emit q->typeChanged();
q->m_canBeIndexed = canBeIndexed( q->m_url, q->m_type );
emit q->canBeIndexedChanged();
if (m_mediaLib)
{
auto uri = QByteArray(q->m_treeItem.media->psz_uri).append('/');
struct Ctx {
bool succeed;
bool isIndexed;
};
m_mediaLib->runOnMLThread<Ctx>(q,
//ML thread
[uri](vlc_medialibrary_t* ml, Ctx& ctx){
auto ret = vlc_ml_is_indexed( ml, uri.constData(), &ctx.isIndexed );
ctx.succeed = (ret == VLC_SUCCESS);
},
//ML thread
[this](quint64,Ctx& ctx){
Q_Q(NetworkMediaModel);
if (!ctx.succeed)
return;
q->m_indexed = ctx.isIndexed;
emit q->isIndexedChanged();
});
}
}
{
input_item_node_t* mediaNode = nullptr;
input_item_node_t* parent = nullptr;
vlc_media_tree_Lock(tree);
vlc_media_tree_PreparseCancel( libvlc, this );
std::vector<SharedInputItem> itemList;
q->m_path = {QVariant::fromValue(PathNode(q->m_treeItem, q->m_name))};
if (vlc_media_tree_Find( tree, q->m_treeItem.media.get(), &mediaNode, &parent))
{
itemList.reserve(mediaNode->i_children);
for (int i = 0; i < mediaNode->i_children; i++)
itemList.emplace_back(mediaNode->pp_children[i]->p_item);
while (parent && parent->p_item) {
q->m_path.push_front(QVariant::fromValue(PathNode(
NetworkTreeItem(q->m_treeItem.tree, parent->p_item),
parent->p_item->psz_name)));
input_item_node_t *node = nullptr;
input_item_node_t *grandParent = nullptr;
if (!vlc_media_tree_Find( tree, parent->p_item, &node, &grandParent)) {
break;
}
parent = grandParent;
}
}
vlc_media_tree_Unlock(tree);
if (!itemList.empty())
refreshMediaList( q->m_treeItem.tree, std::move( itemList ), true );
emit q->pathChanged();
}
m_preparseSem.acquire();
vlc_media_tree_Preparse( tree, libvlc, q->m_treeItem.media.get(), this );
m_listener = std::move( l );
return true;
}
// LocalListCacheLoader::ModelSource interface implementation
protected:
std::vector<NetworkMediaItemPtr> getModelData(const QString& pattern) const override
{
if (pattern.isEmpty())
return m_items;
std::vector<NetworkMediaItemPtr> items;
std::copy_if(
m_items.cbegin(), m_items.cend(),
std::back_inserter(items),
[&pattern](const NetworkMediaItemPtr& item){
return item->name.contains(pattern, Qt::CaseInsensitive);
});
return items;
}
public:
MediaLib* m_mediaLib;
bool m_hasTree = false;
QSemaphore m_preparseSem;
std::unique_ptr<MediaTreeListener> m_listener;
std::vector<NetworkMediaItemPtr> m_items;
};
// NetworkMediaModel::ListenerCb implementation
struct NetworkMediaModel::ListenerCb : public MediaTreeListener::MediaTreeListenerCb {
ListenerCb(NetworkMediaModel *model) : model(model) {}
void onItemCleared( MediaTreePtr tree, input_item_node_t* node ) override;
void onItemAdded( MediaTreePtr tree, input_item_node_t* parent, input_item_node_t *const children[], size_t count ) override;
void onItemRemoved( MediaTreePtr tree, input_item_node_t * node, input_item_node_t *const children[], size_t count ) override;
void onItemPreparseEnded( MediaTreePtr tree, input_item_node_t* node, enum input_item_preparse_status status ) override;
NetworkMediaModel *model;
};
// NetworkMediaModel implementation
NetworkMediaModel::NetworkMediaModel( QObject* parent )
: BaseModel( new NetworkMediaModelPrivate(this), parent )
{
}
NetworkMediaModel::~NetworkMediaModel()
{
Q_D(NetworkMediaModel);
//this can only be acquired from UI thread
if (!d->m_preparseSem.tryAcquire())
{
auto libvlc = vlc_object_instance(m_ctx->getIntf());
vlc_media_tree_PreparseCancel( libvlc, this );
//wait for the callback call on cancel
d->m_preparseSem.acquire();
}
}
QVariant NetworkMediaModel::data( const QModelIndex& index, int role ) const
{
Q_D(const NetworkMediaModel);
if (!m_ctx)
return {};
const NetworkMediaItem* item = d->getItemForRow(index.row());
if (!item)
return {};
switch ( role )
{
case NETWORK_NAME:
return item->name;
case NETWORK_MRL:
return item->mainMrl;
case NETWORK_INDEXED:
return item->indexed;
case NETWORK_CANINDEX:
return item->canBeIndexed;
case NETWORK_TYPE:
return item->type;
case NETWORK_PROTOCOL:
return item->protocol;
case NETWORK_TREE:
return QVariant::fromValue( item->tree );
case NETWORK_ARTWORK:
return item->artwork;
case NETWORK_FILE_SIZE:
return item->fileSize;
case NETWORK_FILE_MODIFIED:
return item->fileModified;
default:
return {};
}
}
QHash<int, QByteArray> NetworkMediaModel::roleNames() const
{
return {
{ NETWORK_NAME, "name" },
{ NETWORK_MRL, "mrl" },
{ NETWORK_INDEXED, "indexed" },
{ NETWORK_CANINDEX, "can_index" },
{ NETWORK_TYPE, "type" },
{ NETWORK_PROTOCOL, "protocol" },
{ NETWORK_TREE, "tree" },
{ NETWORK_ARTWORK, "artwork" },
{ NETWORK_FILE_SIZE, "fileSizeRaw64" },
{ NETWORK_FILE_MODIFIED, "fileModified" }
};
}
Qt::ItemFlags NetworkMediaModel::flags( const QModelIndex& idx ) const
{
return QAbstractListModel::flags( idx ) | Qt::ItemIsEditable;
}
bool NetworkMediaModel::setData( const QModelIndex& idx, const QVariant& value, int role )
{
Q_D(NetworkMediaModel);
if (!d->m_mediaLib)
return false;
if ( role != NETWORK_INDEXED )
return false;
bool enabled = value.toBool();
const NetworkMediaItemPtr* ref = d->item(idx.row());
if (!ref)
return false;
//local reference
NetworkMediaItemPtr item = *ref;
if ( item->indexed == enabled )
return true;
QUrl mainMrl = item->mainMrl;
struct Ctx {
bool succeed;
};
d->m_mediaLib->runOnMLThread<Ctx>(this,
//ML thread
[enabled, mainMrl]
(vlc_medialibrary_t* ml, Ctx& ctx){
int res;
if ( enabled )
res = vlc_ml_add_folder( ml, qtu( mainMrl.toString( QUrl::FullyEncoded ) ) );
else
res = vlc_ml_remove_folder( ml, qtu( mainMrl.toString( QUrl::FullyEncoded ) ) );
ctx.succeed = res == VLC_SUCCESS;
},
//UI thread
[this, item, enabled](qint64, Ctx& ctx){
Q_D(NetworkMediaModel);
if (!ctx.succeed)
return;
d->setIntexedState(item, enabled);
},
ML_FOLDER_ADD_QUEUE);
return true;
}
void NetworkMediaModel::setIndexed(bool indexed)
{
Q_D(NetworkMediaModel);
if (indexed == m_indexed || !m_canBeIndexed)
return;
QString url = m_url.toString( QUrl::FullyEncoded );
struct Ctx {
bool success;
};
d->m_mediaLib->runOnMLThread<Ctx>(this,
//ML thread
[url, indexed](vlc_medialibrary_t* ml, Ctx& ctx)
{
int res;
if ( indexed )
res = vlc_ml_add_folder( ml, qtu( url ) );
else
res = vlc_ml_remove_folder( ml, qtu( url ) );
ctx.success = (res == VLC_SUCCESS);
},
//UI thread
[this, indexed](quint64, Ctx& ctx){
if (ctx.success)
{
m_indexed = indexed;
emit isIndexedChanged();
}
},
ML_FOLDER_ADD_QUEUE);
}
void NetworkMediaModel::setCtx(MainCtx* ctx)
{
Q_D(NetworkMediaModel);
if (m_ctx == ctx)
return;
assert(ctx);
m_ctx = ctx;
d->m_mediaLib = ctx->getMediaLibrary();
d->initializeModel();
emit ctxChanged();
}
void NetworkMediaModel::setTree(QVariant parentTree)
{
Q_D(NetworkMediaModel);
if (parentTree.canConvert<NetworkTreeItem>())
m_treeItem = parentTree.value<NetworkTreeItem>();
else
m_treeItem = NetworkTreeItem();
d->m_hasTree = true;
d->initializeModel();
emit treeChanged();
}
bool NetworkMediaModel::insertIntoPlaylist(const QModelIndexList &itemIdList, const ssize_t playlistIndex)
{
Q_D(NetworkMediaModel);
if (!(m_ctx && d->m_hasTree))
return false;
QVector<vlc::playlist::Media> medias;
medias.reserve( itemIdList.size() );
for ( const QModelIndex &id : itemIdList )
{
if ( !id.isValid() )
continue;
const NetworkMediaItem* item = d->getItemForRow(id.row());
if (!item)
continue;
medias.append( vlc::playlist::Media {item->tree.media.get()} );
}
if (medias.isEmpty())
return false;
m_ctx->getIntf()->p_mainPlaylistController->insert(playlistIndex, medias, false);
return true;
}
bool NetworkMediaModel::addToPlaylist(const int index)
{
Q_D(NetworkMediaModel);
if (!(m_ctx && d->m_hasTree))
return false;
const NetworkMediaItem* item = d->getItemForRow(index);
if (!item)
return false;
vlc::playlist::Media media{ item->tree.media.get() };
m_ctx->getIntf()->p_mainPlaylistController->append( QVector<vlc::playlist::Media>{ media }, false);
return true;
}
bool NetworkMediaModel::addToPlaylist(const QVariantList &itemIdList)
{
bool ret = false;
for (const QVariant& varValue: itemIdList)
{
int index = -1;
if (varValue.canConvert<int>())
index = varValue.value<int>();
else if (varValue.canConvert<QModelIndex>())
index = varValue.value<QModelIndex>().row();
else
continue;
ret |= addToPlaylist(index);
}
return ret;
}
bool NetworkMediaModel::addToPlaylist(const QModelIndexList &itemIdList)
{
bool ret = false;
for (const QModelIndex& index: itemIdList)
{
if (!index.isValid())
continue;
ret |= addToPlaylist(index.row());
}
return ret;
}
bool NetworkMediaModel::addAndPlay(int index)
{
Q_D(NetworkMediaModel);
if (!(m_ctx && d->m_hasTree))
return false;
const NetworkMediaItem* item = d->getItemForRow(index);
if (!item)
return false;
vlc::playlist::Media media{ item->tree.media.get() };
m_ctx->getIntf()->p_mainPlaylistController->append( QVector<vlc::playlist::Media>{ media }, true);
return true;
}
bool NetworkMediaModel::addAndPlay(const QVariantList& itemIdList)
{
bool ret = false;
for (const QVariant& varValue: itemIdList)
{
int index = -1;
if (varValue.canConvert<int>())
index = varValue.value<int>();
else if (varValue.canConvert<QModelIndex>())
index = varValue.value<QModelIndex>().row();
else
continue;
if (!ret)
ret |= addAndPlay(index);
else
ret |= addToPlaylist(index);
}
return ret;
}
bool NetworkMediaModel::addAndPlay(const QModelIndexList& itemIdList)
{
bool ret = false;
for (const QModelIndex& index: itemIdList)
{
if (!index.isValid())
continue;
if (!ret)
ret |= addAndPlay(index.row());
else
ret |= addToPlaylist(index.row());
}
return ret;
}
/* Q_INVOKABLE */
QVariantList NetworkMediaModel::getItemsForIndexes(const QModelIndexList & indexes) const
{
Q_D(const NetworkMediaModel);
QVariantList items;
for (const QModelIndex & modelIndex : indexes)
{
int index = modelIndex.row();
const NetworkMediaItem* item = d->getItemForRow(index);
if (!item)
return {};
const NetworkTreeItem & tree = item->tree;
items.append(QVariant::fromValue(SharedInputItem(tree.media.get(), true)));
}
return items;
}
void NetworkMediaModel::ListenerCb::onItemCleared( MediaTreePtr tree, input_item_node_t* node)
{
SharedInputItem p_node { node->p_item };
QMetaObject::invokeMethod(model, [model=model, p_node = std::move(p_node), tree = std::move(tree)]() {
if (p_node != model->m_treeItem.media)
return;
input_item_node_t *res;
input_item_node_t *parent;
// XXX is tree == m_treeItem.tree?
vlc_media_tree_Lock( model->m_treeItem.tree.get() );
bool found = vlc_media_tree_Find( model->m_treeItem.tree.get(), model->m_treeItem.media.get(),
&res, &parent );
vlc_media_tree_Unlock( model->m_treeItem.tree.get() );
if (!found)
return;
std::vector<SharedInputItem> itemList;
itemList.reserve( static_cast<size_t>(res->i_children) );
for (int i = 0; i < res->i_children; i++)
itemList.emplace_back(res->pp_children[i]->p_item);
model->d_func()->refreshMediaList( std::move( tree ), std::move( itemList ), true );
}, Qt::QueuedConnection);
}
void NetworkMediaModel::ListenerCb::onItemAdded( MediaTreePtr tree, input_item_node_t* parent,
input_item_node_t *const children[],
size_t count )
{
SharedInputItem p_parent { parent->p_item };
std::vector<SharedInputItem> itemList;
itemList.reserve( count );
for (size_t i = 0; i < count; i++)
itemList.emplace_back(children[i]->p_item);
QMetaObject::invokeMethod(model, [model=model, p_parent = std::move(p_parent), tree = std::move(tree), itemList=std::move(itemList)]() {
if ( p_parent == model->m_treeItem.media )
model->d_func()->refreshMediaList( std::move( tree ), std::move( itemList ), false );
}, Qt::QueuedConnection);
}
void NetworkMediaModel::ListenerCb::onItemRemoved( MediaTreePtr, input_item_node_t * node,
input_item_node_t *const children[],
size_t count )
{
std::vector<SharedInputItem> itemList;
itemList.reserve( count );
for ( auto i = 0u; i < count; ++i )
itemList.emplace_back( children[i]->p_item );
SharedInputItem p_node { node->p_item };
QMetaObject::invokeMethod(
model,
[model=model, p_node=std::move(p_node), itemList=std::move(itemList)]() {
model->d_func()->removeItem(p_node, itemList);
}, Qt::QueuedConnection);
}
void NetworkMediaModel::ListenerCb::onItemPreparseEnded(MediaTreePtr, input_item_node_t* node, enum input_item_preparse_status )
{
model->d_func()->m_preparseSem.release();
SharedInputItem p_node { node->p_item };
QMetaObject::invokeMethod(model, [model=model, p_node=std::move(p_node)]() {
if (p_node != model->m_treeItem.media)
return;
model->d_func()->m_loading = false;
emit model->loadingChanged();
});
}