From 04e3b250e7434450bafc1a4fee257239131f8d91 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Fri, 31 May 2019 19:45:03 +0300
Subject: [PATCH] Keep track of fully cached media files.

---
 Telegram/SourceFiles/data/data_document.cpp   |  50 +++++++-
 Telegram/SourceFiles/data/data_document.h     |   3 +
 Telegram/SourceFiles/data/data_session.cpp    |  11 +-
 .../history/media/history_media_video.cpp     |   2 +-
 .../media/streaming/media_streaming_file.cpp  |  17 +++
 .../media/streaming/media_streaming_file.h    |   2 +
 .../streaming/media_streaming_file_delegate.h |   1 +
 .../streaming/media_streaming_player.cpp      |  22 +++-
 .../media/streaming/media_streaming_player.h  |   4 +
 .../streaming/media_streaming_reader.cpp      | 116 +++++++++++++++++-
 .../media/streaming/media_streaming_reader.h  |  10 ++
 .../media/view/media_view_overlay_widget.cpp  |   5 +
 .../storage/cache/storage_cache_database.cpp  |  13 ++
 .../storage/cache/storage_cache_database.h    |   5 +
 .../cache/storage_cache_database_object.cpp   |  23 +++-
 .../cache/storage_cache_database_object.h     |   5 +
 Telegram/SourceFiles/storage/localstorage.cpp |  80 +++++++-----
 Telegram/SourceFiles/storage/localstorage.h   |   3 +-
 .../SourceFiles/ui/image/image_location.cpp   |  15 ++-
 .../SourceFiles/ui/image/image_location.h     |   9 +-
 20 files changed, 342 insertions(+), 54 deletions(-)

diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index 50232afac..e2df5f699 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -816,6 +816,35 @@ bool DocumentData::uploading() const {
 	return (uploadingData != nullptr);
 }
 
+bool DocumentData::loadedInMediaCache() const {
+	return (_flags & Flag::LoadedInMediaCache);
+}
+
+void DocumentData::setLoadedInMediaCache(bool loaded) {
+	const auto flags = loaded
+		? (_flags | Flag::LoadedInMediaCache)
+		: (_flags & ~Flag::LoadedInMediaCache);
+	if (_flags == flags) {
+		return;
+	}
+	_flags = flags;
+	if (!this->loaded()) {
+		if (loadedInMediaCache()) {
+			Local::writeFileLocation(
+				mediaKey(),
+				FileLocation::InMediaCacheLocation());
+		} else {
+			Local::removeFileLocation(mediaKey());
+		}
+		owner().requestDocumentViewRepaint(this);
+	}
+}
+
+void DocumentData::setLoadedInMediaCacheLocation() {
+	_location = FileLocation();
+	_flags |= Flag::LoadedInMediaCache;
+}
+
 void DocumentData::setWaitingForAlbum() {
 	if (uploading()) {
 		uploadingData->waitingForAlbum = true;
@@ -1010,13 +1039,21 @@ QByteArray DocumentData::data() const {
 
 const FileLocation &DocumentData::location(bool check) const {
 	if (check && !_location.check()) {
-		const_cast<DocumentData*>(this)->_location = Local::readFileLocation(mediaKey());
+		const auto location = Local::readFileLocation(mediaKey());
+		const auto that = const_cast<DocumentData*>(this);
+		if (location.inMediaCache()) {
+			that->setLoadedInMediaCacheLocation();
+		} else {
+			that->_location = location;
+		}
 	}
 	return _location;
 }
 
 void DocumentData::setLocation(const FileLocation &loc) {
-	if (loc.check()) {
+	if (loc.inMediaCache()) {
+		setLoadedInMediaCacheLocation();
+	} else if (loc.check()) {
 		_location = loc;
 	}
 }
@@ -1492,6 +1529,13 @@ void DocumentData::setRemoteLocation(
 				Local::writeFileLocation(mediaKey(), _location);
 			} else {
 				_location = Local::readFileLocation(mediaKey());
+				if (_location.inMediaCache()) {
+					setLoadedInMediaCacheLocation();
+				} else if (_location.isEmpty() && loadedInMediaCache()) {
+					Local::writeFileLocation(
+						mediaKey(),
+						FileLocation::InMediaCacheLocation());
+				}
 			}
 		}
 	}
@@ -1520,7 +1564,7 @@ void DocumentData::collectLocalData(not_null<DocumentData*> local) {
 			ActiveCache().up(this);
 		}
 	}
-	if (!local->_location.isEmpty()) {
+	if (!local->_location.inMediaCache() && !local->_location.isEmpty()) {
 		_location = local->_location;
 		Local::writeFileLocation(mediaKey(), _location);
 	}
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index 89add5cde..b0d23519e 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -115,6 +115,8 @@ public:
 	[[nodiscard]] float64 progress() const;
 	[[nodiscard]] int loadOffset() const;
 	[[nodiscard]] bool uploading() const;
+	[[nodiscard]] bool loadedInMediaCache() const;
+	void setLoadedInMediaCache(bool loaded);
 
 	void setWaitingForAlbum();
 	[[nodiscard]] bool waitingForAlbum() const;
@@ -266,6 +268,7 @@ private:
 	void validateLottieSticker();
 	void validateGoodThumbnail();
 	void setMaybeSupportsStreaming(bool supports);
+	void setLoadedInMediaCacheLocation();
 
 	void destroyLoader() const;
 
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index debf2bdc4..8d9f5bca9 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -2504,15 +2504,18 @@ void Session::documentApplyFields(
 	if (!date) {
 		return;
 	}
-	if (dc != 0 && access != 0) {
-		document->setRemoteLocation(dc, access, fileReference);
-	}
 	document->date = date;
 	document->setMimeString(mime);
 	document->updateThumbnails(thumbnailInline, thumbnail);
 	document->size = size;
-	document->recountIsImage();
 	document->setattributes(attributes);
+
+	// Uses 'type' that is computed from attributes.
+	document->recountIsImage();
+	if (dc != 0 && access != 0) {
+		document->setRemoteLocation(dc, access, fileReference);
+	}
+
 	if (document->sticker()
 		&& !document->sticker()->loc.valid()
 		&& thumbLocation.valid()) {
diff --git a/Telegram/SourceFiles/history/media/history_media_video.cpp b/Telegram/SourceFiles/history/media/history_media_video.cpp
index 1e28c0b96..a77791c0a 100644
--- a/Telegram/SourceFiles/history/media/history_media_video.cpp
+++ b/Telegram/SourceFiles/history/media/history_media_video.cpp
@@ -274,7 +274,7 @@ void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, crl
 void HistoryVideo::drawCornerStatus(Painter &p, bool selected) const {
 	const auto padding = st::msgDateImgPadding;
 	const auto radial = _animation && _animation->radial.animating();
-	const auto cornerDownload = downloadInCorner() && !_data->loaded();
+	const auto cornerDownload = downloadInCorner() && !_data->loaded() && !_data->loadedInMediaCache();
 	const auto addWidth = cornerDownload ? (st::historyVideoDownloadSize + 2 * padding.y()) : 0;
 	const auto downloadWidth = cornerDownload ? st::normalFont->width(_downloadSize) : 0;
 	const auto statusW = std::max(downloadWidth, st::normalFont->width(_statusText)) + 2 * padding.x() + addWidth;
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
index 01310b695..60fa317ad 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp
@@ -60,6 +60,9 @@ int File::Context::read(bytes::span buffer) {
 			return -1;
 		}
 	}
+
+	sendFullInCache();
+
 	_offset += amount;
 	return amount;
 }
@@ -244,6 +247,9 @@ void File::Context::start(crl::time position) {
 	}
 
 	_reader->headerDone();
+	if (_reader->isRemoteLoader()) {
+		sendFullInCache(true);
+	}
 	if (video.codec || audio.codec) {
 		seekToPosition(format.get(), video.codec ? video : audio, position);
 	}
@@ -258,6 +264,17 @@ void File::Context::start(crl::time position) {
 	_format = std::move(format);
 }
 
+void File::Context::sendFullInCache(bool force) {
+	const auto started = _fullInCache.has_value();
+	if (force || started) {
+		const auto nowFullInCache = _reader->fullInCache();
+		if (!started || *_fullInCache != nowFullInCache) {
+			_fullInCache = nowFullInCache;
+			_delegate->fileFullInCache(nowFullInCache);
+		}
+	}
+}
+
 void File::Context::readNextPacket() {
 	auto result = readPacket();
 	if (unroll()) {
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.h b/Telegram/SourceFiles/media/streaming/media_streaming_file.h
index 1c253b17a..e2e551849 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_file.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.h
@@ -81,6 +81,7 @@ private:
 		[[nodiscard]] base::variant<Packet, AvErrorWrap> readPacket();
 
 		void handleEndOfFile();
+		void sendFullInCache(bool force = false);
 
 		const not_null<FileDelegate*> _delegate;
 		const not_null<Reader*> _reader;
@@ -89,6 +90,7 @@ private:
 		int _size = 0;
 		bool _failed = false;
 		bool _readTillEnd = false;
+		std::optional<bool> _fullInCache;
 		crl::semaphore _semaphore;
 		std::atomic<bool> _interrupted = false;
 
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file_delegate.h b/Telegram/SourceFiles/media/streaming/media_streaming_file_delegate.h
index b24271b21..ed2d4cd94 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_file_delegate.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_file_delegate.h
@@ -22,6 +22,7 @@ public:
 		Stream &&audio) = 0;
 	virtual void fileError(Error error) = 0;
 	virtual void fileWaitingForData() = 0;
+	virtual void fileFullInCache(bool fullInCache) = 0;
 
 	// Return true if reading and processing more packets is desired.
 	// Return false if sleeping until 'wake()' is called is desired.
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
index 357da5d17..57e833101 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
@@ -174,7 +174,7 @@ void Player::trackSendReceivedTill(
 	Expects(state.duration != kTimeUnknown);
 	Expects(state.receivedTill != kTimeUnknown);
 
-	if (!_remoteLoader) {
+	if (!_remoteLoader || _fullInCacheSinceStart.value_or(false)) {
 		return;
 	}
 	const auto receivedTill = std::max(
@@ -302,6 +302,15 @@ void Player::fileError(Error error) {
 	});
 }
 
+void Player::fileFullInCache(bool fullInCache) {
+	crl::on_main(&_sessionGuard, [=] {
+		if (!_fullInCacheSinceStart.has_value()) {
+			_fullInCacheSinceStart = fullInCache;
+		}
+		_fullInCache.fire_copy(fullInCache);
+	});
+}
+
 void Player::fileWaitingForData() {
 	if (_waitingForData) {
 		return;
@@ -736,6 +745,10 @@ bool Player::finished() const {
 		&& (!_video || _videoFinished);
 }
 
+float64 Player::speed() const {
+	return _options.speed;
+}
+
 void Player::setSpeed(float64 speed) {
 	Expects(active());
 	Expects(speed >= 0.5 && speed <= 2.);
@@ -767,6 +780,10 @@ rpl::producer<Update, Error> Player::updates() const {
 	return _updates.events();
 }
 
+rpl::producer<bool> Player::fullInCache() const {
+	return _fullInCache.events();
+}
+
 QSize Player::videoSize() const {
 	return _information.video.size;
 }
@@ -803,7 +820,8 @@ Media::Player::TrackState Player::prepareLegacyState() const {
 	} else if (_options.loop && result.length > 0) {
 		result.position %= result.length;
 	}
-	result.receivedTill = _remoteLoader
+	result.receivedTill = (_remoteLoader
+		&& !_fullInCacheSinceStart.value_or(false))
 		? getCurrentReceivedTill(result.length)
 		: 0;
 	result.frequency = kMsFrequency;
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.h b/Telegram/SourceFiles/media/streaming/media_streaming_player.h
index 417472827..754ce9af3 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_player.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.h
@@ -57,6 +57,7 @@ public:
 	[[nodiscard]] bool finished() const;
 
 	[[nodiscard]] rpl::producer<Update, Error> updates() const;
+	[[nodiscard]] rpl::producer<bool> fullInCache() const;
 
 	[[nodiscard]] QSize videoSize() const;
 	[[nodiscard]] QImage frame(const FrameRequest &request) const;
@@ -82,6 +83,7 @@ private:
 	bool fileReady(int headerSize, Stream &&video, Stream &&audio) override;
 	void fileError(Error error) override;
 	void fileWaitingForData() override;
+	void fileFullInCache(bool fullInCache) override;
 	bool fileProcessPacket(Packet &&packet) override;
 	bool fileReadMore() override;
 
@@ -176,6 +178,8 @@ private:
 	crl::time _nextFrameTime = kTimeUnknown;
 	base::Timer _renderFrameTimer;
 	rpl::event_stream<Update, Error> _updates;
+	rpl::event_stream<bool> _fullInCache;
+	std::optional<bool> _fullInCacheSinceStart;
 
 	crl::time _totalDuration = kTimeUnknown;
 	crl::time _loopingShift = 0;
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
index ee881be47..3702dbfbd 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp
@@ -225,6 +225,7 @@ struct Reader::CacheHelper {
 
 	QMutex mutex;
 	base::flat_map<int, PartsMap> results;
+	std::vector<int> sizes;
 	std::atomic<crl::semaphore*> waiting = nullptr;
 };
 
@@ -385,6 +386,17 @@ int Reader::Slices::headerSize() const {
 	return _header.parts.size() * kPartSize;
 }
 
+bool Reader::Slices::fullInCache() const {
+	return _fullInCache;
+}
+
+int Reader::Slices::requestSliceSizesCount() const {
+	if (!headerModeUnknown() || isFullInHeader()) {
+		return 0;
+	}
+	return _data.size();
+}
+
 bool Reader::Slices::headerWontBeFilled() const {
 	return headerModeUnknown()
 		&& (_header.parts.size() >= kMaxPartsInHeader);
@@ -438,6 +450,7 @@ void Reader::Slices::processCacheResult(int sliceNumber, PartsMap &&result) {
 		return;
 	}
 	slice.processCacheData(std::move(result));
+	checkSliceFullLoaded(sliceNumber);
 	if (!sliceNumber) {
 		applyHeaderCacheData();
 		if (isGoodHeader()) {
@@ -448,6 +461,66 @@ void Reader::Slices::processCacheResult(int sliceNumber, PartsMap &&result) {
 	}
 }
 
+void Reader::Slices::processCachedSizes(const std::vector<int> &sizes) {
+	Expects(sizes.size() == _data.size());
+
+	using Flag = Slice::Flag;
+	const auto count = int(sizes.size());
+	auto loadedCount = 0;
+	for (auto i = 0; i != count; ++i) {
+		const auto sliceNumber = (i + 1);
+		const auto sliceSize = (sliceNumber < _data.size())
+			? kInSlice
+			: (_size - (sliceNumber - 1) * kInSlice);
+		const auto loaded = (sizes[i] == sliceSize);
+
+		if (_data[i].flags & Flag::FullInCache) {
+			++loadedCount;
+		} else if (loaded) {
+			_data[i].flags |= Flag::FullInCache;
+			++loadedCount;
+		}
+	}
+	_fullInCache = (loadedCount == count);
+}
+
+void Reader::Slices::checkSliceFullLoaded(int sliceNumber) {
+	if (!sliceNumber && !isFullInHeader()) {
+		return;
+	}
+	const auto partsCount = [&] {
+		if (!sliceNumber) {
+			return (_size + kPartSize - 1) / kPartSize;
+		}
+		return (sliceNumber < _data.size())
+			? kPartsInSlice
+			: ((_size - (sliceNumber - 1) * kInSlice + kPartSize - 1)
+				/ kPartSize);
+	}();
+	auto &slice = (sliceNumber ? _data[sliceNumber - 1] : _header);
+	const auto loaded = (slice.parts.size() == partsCount);
+
+	using Flag = Slice::Flag;
+	if ((slice.flags & Flag::FullInCache) && !loaded) {
+		slice.flags &= ~Flag::FullInCache;
+		_fullInCache = false;
+	} else if (!(slice.flags & Flag::FullInCache) && loaded) {
+		slice.flags |= Flag::FullInCache;
+		_fullInCache = checkFullInCache();
+	}
+}
+
+bool Reader::Slices::checkFullInCache() const {
+	using Flag = Slice::Flag;
+	if (isFullInHeader()) {
+		return (_header.flags & Flag::FullInCache);
+	}
+	const auto i = ranges::find_if(_data, [](const Slice &slice) {
+		return !(slice.flags & Flag::FullInCache);
+	});
+	return (i == end(_data));
+}
+
 void Reader::Slices::processPart(
 		int offset,
 		QByteArray &&bytes) {
@@ -455,6 +528,7 @@ void Reader::Slices::processPart(
 
 	if (isFullInHeader()) {
 		_header.addPart(offset, bytes);
+		checkSliceFullLoaded(0);
 		return;
 	} else if (_headerMode == HeaderMode::Unknown) {
 		if (_header.parts.contains(offset)) {
@@ -465,6 +539,7 @@ void Reader::Slices::processPart(
 	}
 	const auto index = offset / kInSlice;
 	_data[index].addPart(offset - index * kInSlice, std::move(bytes));
+	checkSliceFullLoaded(index + 1);
 }
 
 auto Reader::Slices::fill(int offset, bytes::span buffer) -> FillResult {
@@ -655,7 +730,7 @@ Reader::SerializedSlice Reader::Slices::serializeAndUnloadUnused() {
 		return false;
 	}();
 	if (noNeedToSaveToCache) {
-		_data[purgeSlice] = Slice();
+		unloadSlice(_data[purgeSlice]);
 		return {};
 	}
 	return serializeAndUnloadSlice(purgeSlice + 1);
@@ -707,13 +782,21 @@ Reader::SerializedSlice Reader::Slices::serializeAndUnloadSlice(
 	// HeaderMode::Good and we unload first slice. We still require
 	// header data to continue working, so don't really unload the header.
 	if (sliceNumber) {
-		slice = Slice();
+		unloadSlice(slice);
 	} else {
 		slice.flags &= ~Slice::Flag::ChangedSinceCache;
 	}
 	return result;
 }
 
+void Reader::Slices::unloadSlice(Slice &slice) const {
+	const auto full = (slice.flags & Slice::Flag::FullInCache);
+	slice = Slice();
+	if (full) {
+		slice.flags |= Slice::Flag::FullInCache;
+	}
+}
+
 QByteArray Reader::Slices::serializeComplexSlice(const Slice &slice) const {
 	auto result = QByteArray();
 	const auto count = slice.parts.size();
@@ -742,7 +825,7 @@ QByteArray Reader::Slices::serializeAndUnloadFirstSliceNoHeader() {
 		slice.parts.erase(offset);
 	}
 	auto result = serializeComplexSlice(slice);
-	slice = Slice();
+	unloadSlice(slice);
 	return result;
 }
 
@@ -1022,8 +1105,14 @@ void Reader::readFromCache(int sliceNumber) {
 	const auto key = _cacheHelper->key(sliceNumber);
 	const auto cache = std::weak_ptr<CacheHelper>(_cacheHelper);
 	const auto weak = base::make_weak(this);
-	_owner->cacheBigFile().get(key, [=](QByteArray &&result) {
-		crl::async([=, result = std::move(result)]{
+	const auto ready = [=](
+			QByteArray &&result,
+			std::vector<int> &&sizes = {}) {
+		crl::async([
+			=,
+			result = std::move(result),
+			sizes = std::move(sizes)
+		]() mutable{
 			auto entry = ParseCacheEntry(
 				bytes::make_span(result),
 				sliceNumber,
@@ -1034,6 +1123,7 @@ void Reader::readFromCache(int sliceNumber) {
 				if (!sliceNumber && entry.included) {
 					strong->results.emplace(1, std::move(*entry.included));
 				}
+				strong->sizes = std::move(sizes);
 				if (const auto waiting = strong->waiting.load()) {
 					strong->waiting.store(nullptr, std::memory_order_release);
 					waiting->release();
@@ -1044,7 +1134,13 @@ void Reader::readFromCache(int sliceNumber) {
 				}
 			}
 		});
-	});
+	};
+	auto keys = std::vector<Storage::Cache::Key>();
+	const auto count = _slices.requestSliceSizesCount();
+	for (auto i = 0; i != count; ++i) {
+		keys.push_back(_cacheHelper->key(i + 1));
+	}
+	_owner->cacheBigFile().getWithSizes(key, std::move(keys), ready);
 }
 
 bool Reader::readFromCacheForDownloader(int sliceNumber) {
@@ -1083,6 +1179,10 @@ int Reader::headerSize() const {
 	return _slices.headerSize();
 }
 
+bool Reader::fullInCache() const {
+	return _slices.fullInCache();
+}
+
 bool Reader::fill(
 		int offset,
 		bytes::span buffer,
@@ -1184,6 +1284,7 @@ bool Reader::processCacheResults() {
 
 	QMutexLocker lock(&_cacheHelper->mutex);
 	auto loaded = base::take(_cacheHelper->results);
+	auto sizes = base::take(_cacheHelper->sizes);
 	lock.unlock();
 
 	for (auto &[sliceNumber, cachedParts] : _downloaderReadCache) {
@@ -1201,6 +1302,9 @@ bool Reader::processCacheResults() {
 	for (auto &[sliceNumber, result] : loaded) {
 		_slices.processCacheResult(sliceNumber, std::move(result));
 	}
+	if (!sizes.empty()) {
+		_slices.processCachedSizes(sizes);
+	}
 	if (!loaded.empty()
 		&& (loaded.front().first == 0)
 		&& _slices.isGoodHeader()) {
diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
index a8eb7a852..4501b2242 100644
--- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
+++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h
@@ -50,6 +50,7 @@ public:
 	[[nodiscard]] std::optional<Error> streamingError() const;
 	void headerDone();
 	[[nodiscard]] int headerSize() const;
+	[[nodiscard]] bool fullInCache() const;
 
 	// Thread safe.
 	void startSleep(not_null<crl::semaphore*> wake);
@@ -104,6 +105,7 @@ private:
 			LoadingFromCache = 0x01,
 			LoadedFromCache = 0x02,
 			ChangedSinceCache = 0x04,
+			FullInCache = 0x08,
 		};
 		friend constexpr inline bool is_flag_type(Flag) { return true; }
 		using Flags = base::flags<Flag>;
@@ -135,13 +137,17 @@ private:
 
 		void headerDone(bool fromCache);
 		[[nodiscard]] int headerSize() const;
+		[[nodiscard]] bool fullInCache() const;
 		[[nodiscard]] bool headerWontBeFilled() const;
 		[[nodiscard]] bool headerModeUnknown() const;
 		[[nodiscard]] bool isFullInHeader() const;
 		[[nodiscard]] bool isGoodHeader() const;
 		[[nodiscard]] bool waitingForHeaderCache() const;
 
+		[[nodiscard]] int requestSliceSizesCount() const;
+
 		void processCacheResult(int sliceNumber, PartsMap &&result);
+		void processCachedSizes(const std::vector<int> &sizes);
 		void processPart(int offset, QByteArray &&bytes);
 
 		[[nodiscard]] FillResult fill(int offset, bytes::span buffer);
@@ -172,12 +178,16 @@ private:
 		[[nodiscard]] FillResult fillFromHeader(
 			int offset,
 			bytes::span buffer);
+		void unloadSlice(Slice &slice) const;
+		void checkSliceFullLoaded(int sliceNumber);
+		[[nodiscard]] bool checkFullInCache() const;
 
 		std::vector<Slice> _data;
 		Slice _header;
 		std::deque<int> _usedSlices;
 		int _size = 0;
 		HeaderMode _headerMode = HeaderMode::Unknown;
+		bool _fullInCache = false;
 
 	};
 
diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
index b9cce6c9f..5a398f8a1 100644
--- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
+++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
@@ -1993,6 +1993,11 @@ void OverlayWidget::initStreaming() {
 		handleStreamingError(std::move(error));
 	}, _streamed->player.lifetime());
 
+	_streamed->player.fullInCache(
+	) | rpl::start_with_next([=](bool fullInCache) {
+		_doc->setLoadedInMediaCache(fullInCache);
+	}, _streamed->player.lifetime());
+
 	restartAtSeekPosition(0);
 }
 
diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp b/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp
index d7486edce..dfe5d0454 100644
--- a/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp
+++ b/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp
@@ -153,6 +153,19 @@ void Database::getWithTag(
 	});
 }
 
+void Database::getWithSizes(
+		const Key &key,
+		std::vector<Key> &&keys,
+		FnMut<void(QByteArray&&, std::vector<int>&&)> &&done) {
+	_wrapped.with([
+		key,
+		keys = std::move(keys),
+		done = std::move(done)
+	](Implementation &unwrapped) mutable {
+		unwrapped.getWithSizes(key, std::move(keys), std::move(done));
+	});
+}
+
 auto Database::statsOnMain() const -> rpl::producer<Stats> {
 	return _wrapped.producer_on_main([](const Implementation &unwrapped) {
 		return unwrapped.stats();
diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database.h b/Telegram/SourceFiles/storage/cache/storage_cache_database.h
index d28f536a1..c7998fd2a 100644
--- a/Telegram/SourceFiles/storage/cache/storage_cache_database.h
+++ b/Telegram/SourceFiles/storage/cache/storage_cache_database.h
@@ -64,6 +64,11 @@ public:
 		FnMut<void(Error)> &&done = nullptr);
 	void getWithTag(const Key &key, FnMut<void(TaggedValue&&)> &&done);
 
+	void getWithSizes(
+		const Key &key,
+		std::vector<Key> &&keys,
+		FnMut<void(QByteArray&&, std::vector<int>&&)> &&done);
+
 	using Stats = details::Stats;
 	using TaggedSummary = details::TaggedSummary;
 	rpl::producer<Stats> statsOnMain() const;
diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp
index 96fa5bd58..ccdeda4ce 100644
--- a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp
+++ b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp
@@ -955,7 +955,28 @@ void DatabaseObject::get(
 	}
 }
 
-QByteArray DatabaseObject::readValueData(PlaceId place, size_type size) const {
+void DatabaseObject::getWithSizes(
+		const Key &key,
+		std::vector<Key> &&keys,
+		FnMut<void(QByteArray&&, std::vector<int>&&)> &&done) {
+	get(key, [&](TaggedValue &&value) {
+		if (value.bytes.isEmpty()) {
+			invokeCallback(done, QByteArray(), std::vector<int>());
+			return;
+		}
+
+		auto sizes = keys | ranges::view::transform([&](const Key &sizeKey) {
+			const auto i = _map.find(sizeKey);
+			return (i != end(_map)) ? int(i->second.size) : 0;
+		}) | ranges::to_vector;
+
+		invokeCallback(done, std::move(value.bytes), std::move(sizes));
+	});
+}
+
+QByteArray DatabaseObject::readValueData(
+		PlaceId place,
+		size_type size) const {
 	const auto path = placePath(place);
 	File data;
 	const auto result = data.open(path, File::Mode::Read, _key);
diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h
index 585304d33..f729b985f 100644
--- a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h
+++ b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h
@@ -56,6 +56,11 @@ public:
 		const Key &to,
 		FnMut<void(Error)> &&done);
 
+	void getWithSizes(
+		const Key &key,
+		std::vector<Key> &&keys,
+		FnMut<void(QByteArray&&, std::vector<int>&&)> &&done);
+
 	rpl::producer<Stats> stats() const;
 
 	void clear(FnMut<void(Error)> &&done);
diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp
index 660bf6289..6ca56195c 100644
--- a/Telegram/SourceFiles/storage/localstorage.cpp
+++ b/Telegram/SourceFiles/storage/localstorage.cpp
@@ -810,7 +810,9 @@ void _readLocations() {
 		MediaKey key(first, second);
 
 		_fileLocations.insert(key, loc);
-		_fileLocationPairs.insert(loc.fname, FileLocationPair(key, loc));
+		if (!loc.inMediaCache()) {
+			_fileLocationPairs.insert(loc.fname, FileLocationPair(key, loc));
+		}
 	}
 
 	if (endMarkFound) {
@@ -3133,52 +3135,68 @@ bool hasDraft(const PeerId &peer) {
 }
 
 void writeFileLocation(MediaKey location, const FileLocation &local) {
-	if (local.fname.isEmpty()) return;
-
-	FileLocationAliases::const_iterator aliasIt = _fileLocationAliases.constFind(location);
-	if (aliasIt != _fileLocationAliases.cend()) {
-		location = aliasIt.value();
+	if (local.fname.isEmpty()) {
+		return;
 	}
-
-	FileLocationPairs::iterator i = _fileLocationPairs.find(local.fname);
-	if (i != _fileLocationPairs.cend()) {
-		if (i.value().second == local) {
-			if (i.value().first != location) {
-				_fileLocationAliases.insert(location, i.value().first);
-				_writeLocations(WriteMapWhen::Fast);
-			}
-			return;
+	if (!local.inMediaCache()) {
+		FileLocationAliases::const_iterator aliasIt = _fileLocationAliases.constFind(location);
+		if (aliasIt != _fileLocationAliases.cend()) {
+			location = aliasIt.value();
 		}
-		if (i.value().first != location) {
-			for (FileLocations::iterator j = _fileLocations.find(i.value().first), e = _fileLocations.end(); (j != e) && (j.key() == i.value().first);) {
-				if (j.value() == i.value().second) {
-					_fileLocations.erase(j);
-					break;
+
+		FileLocationPairs::iterator i = _fileLocationPairs.find(local.fname);
+		if (i != _fileLocationPairs.cend()) {
+			if (i.value().second == local) {
+				if (i.value().first != location) {
+					_fileLocationAliases.insert(location, i.value().first);
+					_writeLocations(WriteMapWhen::Fast);
 				}
+				return;
 			}
-			_fileLocationPairs.erase(i);
+			if (i.value().first != location) {
+				for (FileLocations::iterator j = _fileLocations.find(i.value().first), e = _fileLocations.end(); (j != e) && (j.key() == i.value().first);) {
+					if (j.value() == i.value().second) {
+						_fileLocations.erase(j);
+						break;
+					}
+				}
+				_fileLocationPairs.erase(i);
+			}
+		}
+		_fileLocationPairs.insert(local.fname, FileLocationPair(location, local));
+	} else {
+		for (FileLocations::iterator i = _fileLocations.find(location); (i != _fileLocations.end()) && (i.key() == location);) {
+			if (i.value().inMediaCache() || i.value().check()) {
+				return;
+			}
+			i = _fileLocations.erase(i);
 		}
 	}
 	_fileLocations.insert(location, local);
-	_fileLocationPairs.insert(local.fname, FileLocationPair(location, local));
 	_writeLocations(WriteMapWhen::Fast);
 }
 
-FileLocation readFileLocation(MediaKey location, bool check) {
+void removeFileLocation(MediaKey location) {
+	FileLocations::iterator i = _fileLocations.find(location);
+	if (i == _fileLocations.end()) {
+		return;
+	}
+	while (i != _fileLocations.end() && (i.key() == location)) {
+		i = _fileLocations.erase(i);
+	}
+	_writeLocations(WriteMapWhen::Fast);
+}
+
+FileLocation readFileLocation(MediaKey location) {
 	FileLocationAliases::const_iterator aliasIt = _fileLocationAliases.constFind(location);
 	if (aliasIt != _fileLocationAliases.cend()) {
 		location = aliasIt.value();
 	}
 
 	FileLocations::iterator i = _fileLocations.find(location);
-	for (FileLocations::iterator i = _fileLocations.find(location); (i != _fileLocations.end()) && (i.key() == location);) {
-		if (check) {
-			if (!i.value().check()) {
-				_fileLocationPairs.remove(i.value().fname);
-				i = _fileLocations.erase(i);
-				_writeLocations();
-				continue;
-			}
+	if (i != _fileLocations.end()) {
+		if (i.value().inMediaCache()) {
+			int a = 0;
 		}
 		return i.value();
 	}
diff --git a/Telegram/SourceFiles/storage/localstorage.h b/Telegram/SourceFiles/storage/localstorage.h
index e4d403d24..5a54c5bea 100644
--- a/Telegram/SourceFiles/storage/localstorage.h
+++ b/Telegram/SourceFiles/storage/localstorage.h
@@ -110,7 +110,8 @@ bool hasDraftCursors(const PeerId &peer);
 bool hasDraft(const PeerId &peer);
 
 void writeFileLocation(MediaKey location, const FileLocation &local);
-FileLocation readFileLocation(MediaKey location, bool check = true);
+FileLocation readFileLocation(MediaKey location);
+void removeFileLocation(MediaKey location);
 
 Storage::EncryptionKey cacheKey();
 QString cachePath();
diff --git a/Telegram/SourceFiles/ui/image/image_location.cpp b/Telegram/SourceFiles/ui/image/image_location.cpp
index 8bae472c7..123d60a37 100644
--- a/Telegram/SourceFiles/ui/image/image_location.cpp
+++ b/Telegram/SourceFiles/ui/image/image_location.cpp
@@ -20,6 +20,7 @@ namespace {
 constexpr auto kDocumentBaseCacheTag = 0x0000000000010000ULL;
 constexpr auto kDocumentBaseCacheMask = 0x000000000000FF00ULL;
 constexpr auto kSerializeTypeShift = quint8(0x08);
+const auto kInMediaCacheLocation = QString("*media_cache*");
 
 MTPInputPeer GenerateInputPeer(
 		uint64 id,
@@ -622,7 +623,7 @@ ReadAccessEnabler::~ReadAccessEnabler() {
 }
 
 FileLocation::FileLocation(const QString &name) : fname(name) {
-	if (fname.isEmpty()) {
+	if (fname.isEmpty() || fname == kInMediaCacheLocation) {
 		size = 0;
 	} else {
 		setBookmark(psPathBookmark(name));
@@ -646,8 +647,14 @@ FileLocation::FileLocation(const QString &name) : fname(name) {
 	}
 }
 
+FileLocation FileLocation::InMediaCacheLocation() {
+	return FileLocation(kInMediaCacheLocation);
+}
+
 bool FileLocation::check() const {
-	if (fname.isEmpty()) return false;
+	if (fname.isEmpty() || fname == kInMediaCacheLocation) {
+		return false;
+	}
 
 	ReadAccessEnabler enabler(_bookmark);
 	if (enabler.failed()) {
@@ -683,6 +690,10 @@ QByteArray FileLocation::bookmark() const {
 	return _bookmark ? _bookmark->bookmark() : QByteArray();
 }
 
+bool FileLocation::inMediaCache() const {
+	return (fname == kInMediaCacheLocation);
+}
+
 void FileLocation::setBookmark(const QByteArray &bm) {
 	_bookmark.reset(bm.isEmpty() ? nullptr : new PsFileBookmark(bm));
 }
diff --git a/Telegram/SourceFiles/ui/image/image_location.h b/Telegram/SourceFiles/ui/image/image_location.h
index f43067b61..5a904f95c 100644
--- a/Telegram/SourceFiles/ui/image/image_location.h
+++ b/Telegram/SourceFiles/ui/image/image_location.h
@@ -434,13 +434,16 @@ public:
 	FileLocation() = default;
 	explicit FileLocation(const QString &name);
 
-	bool check() const;
-	const QString &name() const;
+	static FileLocation InMediaCacheLocation();
+
+	[[nodiscard]] bool check() const;
+	[[nodiscard]] const QString &name() const;
 	void setBookmark(const QByteArray &bookmark);
 	QByteArray bookmark() const;
-	bool isEmpty() const {
+	[[nodiscard]] bool isEmpty() const {
 		return name().isEmpty();
 	}
+	[[nodiscard]] bool inMediaCache() const;
 
 	bool accessEnable() const;
 	void accessDisable() const;