/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_saved_music.h" #include "api/api_hash.h" #include "apiwrap.h" #include "base/unixtime.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/history.h" #include "main/main_session.h" #include "ui/ui_utility.h" namespace Data { namespace { constexpr auto kPerPage = 50; constexpr auto kReloadIdsEvery = 30 * crl::time(1000); [[nodiscard]] not_null ItemDocument( not_null item) { return item->media()->document(); } } // namespace SavedMusic::SavedMusic(not_null owner) : _owner(owner) { } SavedMusic::~SavedMusic() { Expects(_entries.empty()); } void SavedMusic::clear() { base::take(_entries); } bool SavedMusic::Supported(PeerId peerId) { return peerId && peerIsUser(peerId); } not_null SavedMusic::musicIdToMsg( PeerId peerId, Entry &entry, not_null id) { const auto i = entry.musicIdToMsg.find(id); if (i != end(entry.musicIdToMsg)) { return i->second.get(); } else if (!entry.history) { entry.history = _owner->history(peerId); } return entry.musicIdToMsg.emplace(id, entry.history->makeMessage({ .id = entry.history->nextNonHistoryEntryId(), .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | MessageFlag::SavedMusicItem), .from = entry.history->peer->id, .date = base::unixtime::now(), }, id, TextWithEntities())).first->second.get(); } void SavedMusic::loadIds() { if (_loadIdsRequest || (_lastReceived && (crl::now() - _lastReceived < kReloadIdsEvery))) { return; } _loadIdsRequest = _owner->session().api().request( MTPaccount_GetSavedMusicIds(MTP_long(Api::CountHash(_myIds))) ).done([=](const MTPaccount_SavedMusicIds &result) { _loadIdsRequest = 0; _lastReceived = crl::now(); result.match([&](const MTPDaccount_savedMusicIds &data) { _myIds = data.vids().v | ranges::views::transform(&MTPlong::v) | ranges::to_vector; }, [](const MTPDaccount_savedMusicIdsNotModified &) { }); }).fail([=] { _loadIdsRequest = 0; _lastReceived = crl::now(); }).send(); } bool SavedMusic::has(not_null document) const { return ranges::contains(_myIds, document->id); } void SavedMusic::save( not_null document, Data::FileOrigin origin) { const auto peerId = _owner->session().userPeerId(); auto &entry = _entries[peerId]; if (entry.list.empty() && !entry.loaded) { loadMore(peerId); } if (has(document)) { return; } const auto item = musicIdToMsg(peerId, entry, document); entry.list.insert(begin(entry.list), item); if (entry.total >= 0) { ++entry.total; } _myIds.insert(begin(_myIds), document->id); const auto send = [=](auto resend) -> void { const auto usedFileReference = document->fileReference(); _owner->session().api().request(MTPaccount_SaveMusic( MTP_flags(0), document->mtpInput(), MTPInputDocument() )).fail([=](const MTP::Error &error) { if (error.code() == 400 && error.type().startsWith(u"FILE_REFERENCE_"_q)) { document->session().api().refreshFileReference(origin, [=]( const auto &) { if (document->fileReference() != usedFileReference) { resend(resend); } }); } }).send(); }; send(send); _changed.fire_copy(peerId); } void SavedMusic::remove(not_null document) { const auto peerId = _owner->session().userPeerId(); auto &entry = _entries[peerId]; const auto i = ranges::find(entry.list, document, ItemDocument); if (i != end(entry.list)) { entry.musicIdFromMsgId.remove((*i)->id); entry.list.erase(i); if (entry.total > 0) { entry.total = std::max(entry.total - 1, 0); } } entry.musicIdToMsg.remove(document); _myIds.erase(ranges::remove(_myIds, document->id), end(_myIds)); _owner->session().api().request(MTPaccount_SaveMusic( MTP_flags(MTPaccount_SaveMusic::Flag::f_unsave), document->mtpInput(), MTPInputDocument() )).send(); _changed.fire_copy(peerId); } void SavedMusic::reorder(int oldPosition, int newPosition) { const auto peerId = _owner->session().userPeerId(); auto &entry = _entries[peerId]; if (oldPosition < 0 || newPosition < 0 || oldPosition >= entry.list.size() || newPosition >= entry.list.size() || oldPosition == newPosition) { return; } const auto item = entry.list[oldPosition]; const auto document = ItemDocument(item); base::reorder(entry.list, oldPosition, newPosition); const auto afterDocument = (newPosition > 0) ? ItemDocument(entry.list[newPosition - 1]).get() : nullptr; _owner->session().api().request(MTPaccount_SaveMusic( MTP_flags(afterDocument ? MTPaccount_SaveMusic::Flag::f_after_id : MTPaccount_SaveMusic::Flags(0)), document->mtpInput(), afterDocument ? afterDocument->mtpInput() : MTPInputDocument() )).done([=] { }).fail([=](const MTP::Error &error) { }).send(); _changed.fire_copy(peerId); } void SavedMusic::apply(not_null user, const MTPDocument *last) { const auto peerId = user->id; auto &entry = _entries[peerId]; if (!last) { if (const auto requestId = base::take(entry.requestId)) { _owner->session().api().request(requestId).cancel(); } entry = Entry{ .total = 0, .loaded = true }; _changed.fire_copy(peerId); return; } const auto document = _owner->processDocument(*last); const auto i = ranges::find(entry.list, document, ItemDocument); if (i != end(entry.list)) { if (i == begin(entry.list)) { return; } ranges::rotate(begin(entry.list), i, i + 1); _changed.fire_copy(peerId); loadMore(peerId, true); return; } entry.list.insert( begin(entry.list), musicIdToMsg(peerId, entry, document)); _changed.fire_copy(peerId); if (entry.loaded) { loadMore(peerId, true); } } bool SavedMusic::countKnown(PeerId peerId) const { if (!Supported(peerId)) { return true; } const auto entry = lookupEntry(peerId); return entry && entry->total >= 0; } int SavedMusic::count(PeerId peerId) const { if (!Supported(peerId)) { return 0; } const auto entry = lookupEntry(peerId); return entry ? std::max(entry->total, 0) : 0; } const std::vector> &SavedMusic::list( PeerId peerId) const { static const auto empty = std::vector>(); if (!Supported(peerId)) { return empty; } const auto entry = lookupEntry(peerId); return entry ? entry->list : empty; } void SavedMusic::loadMore(PeerId peerId) { loadMore(peerId, false); } void SavedMusic::loadMore(PeerId peerId, bool reload) { if (!Supported(peerId)) { return; } auto &entry = _entries[peerId]; if (!entry.reloading && reload) { _owner->session().api().request( base::take(entry.requestId)).cancel(); } if ((!reload && entry.loaded) || entry.requestId) { return; } const auto user = _owner->peer(peerId)->asUser(); Assert(user != nullptr); entry.reloading = reload; entry.requestId = _owner->session().api().request(MTPusers_GetSavedMusic( user->inputUser(), MTP_int(reload ? 0 : entry.list.size()), MTP_int(kPerPage), MTP_long(reload ? firstPageHash(entry) : 0) )).done([=](const MTPusers_SavedMusic &result) { auto &entry = _entries[peerId]; entry.requestId = 0; const auto reloaded = base::take(entry.reloading); result.match([&](const MTPDusers_savedMusicNotModified &) { }, [&](const MTPDusers_savedMusic &data) { const auto list = data.vdocuments().v; const auto count = int(list.size()); entry.total = std::max(count, data.vcount().v); if (reloaded) { entry.list.clear(); } for (const auto &item : list) { const auto document = _owner->processDocument(item); if (!ranges::contains(entry.list, document, ItemDocument)) { entry.list.push_back( musicIdToMsg(peerId, entry, document)); } } entry.loaded = list.empty() || (entry.total == entry.list.size()); }); _changed.fire_copy(peerId); }).fail([=](const MTP::Error &error) { auto &entry = _entries[peerId]; entry.requestId = 0; entry.total = int(entry.list.size()); entry.loaded = true; _changed.fire_copy(peerId); }).send(); } uint64 SavedMusic::firstPageHash(const Entry &entry) const { return Api::CountHash(entry.list | ranges::views::transform(ItemDocument) | ranges::views::transform(&DocumentData::id) | ranges::views::take(kPerPage)); } rpl::producer SavedMusic::changed() const { return _changed.events(); } SavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) { if (!Supported(peerId)) { return nullptr; } auto it = _entries.find(peerId); if (it == end(_entries)) { return nullptr; } return &it->second; } const SavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) const { return const_cast(this)->lookupEntry(peerId); } rpl::producer SavedMusicList( not_null peer, HistoryItem *aroundId, int limit) { if (!peer->isUser()) { return rpl::single(SavedMusicSlice({}, 0, 0, 0)); } return [=](auto consumer) { auto lifetime = rpl::lifetime(); struct State { SavedMusicSlice slice; base::has_weak_ptr guard; bool scheduled = false; }; const auto state = lifetime.make_state(); const auto push = [=] { state->scheduled = false; const auto peerId = peer->id; const auto savedMusic = &peer->owner().savedMusic(); if (!savedMusic->countKnown(peerId)) { return; } const auto &loaded = savedMusic->list(peerId); const auto count = savedMusic->count(peerId); auto i = aroundId ? ranges::find(loaded, not_null(aroundId)) : begin(loaded); if (i == end(loaded)) { i = begin(loaded); } const auto hasAfter = int(i - begin(loaded)); const auto hasBefore = int(end(loaded) - i); if (hasBefore < limit) { savedMusic->loadMore(peerId); } const auto takeAfter = std::min(hasAfter, limit); const auto takeBefore = std::min(hasBefore, limit); auto ids = std::vector>(); ids.reserve(takeAfter + takeBefore); for (auto j = i - takeAfter; j != i + takeBefore; ++j) { ids.push_back(*j); } const auto added = int(ids.size()); state->slice = SavedMusicSlice( std::move(ids), count, count - (hasAfter - takeAfter) - added, hasAfter - takeAfter); consumer.put_next_copy(state->slice); }; const auto schedule = [=] { if (state->scheduled) { return; } state->scheduled = true; Ui::PostponeCall(&state->guard, [=] { if (state->scheduled) { push(); } }); }; const auto peerId = peer->id; const auto savedMusic = &peer->owner().savedMusic(); savedMusic->changed( ) | rpl::filter( rpl::mappers::_1 == peerId ) | rpl::on_next(schedule, lifetime); if (!savedMusic->countKnown(peerId)) { savedMusic->loadMore(peerId); } push(); return lifetime; }; } } // namespace Data