diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css
index f9727d68ac..6c67fd0709 100644
--- a/Telegram/Resources/export_html/css/style.css
+++ b/Telegram/Resources/export_html/css/style.css
@@ -440,6 +440,9 @@ div.toast_shown {
.section.stories {
background-image: url(../images/section_stories.png);
}
+.section.music {
+ background-image: url(../images/section_music.png);
+}
.section.web {
background-image: url(../images/section_web.png);
}
@@ -481,6 +484,16 @@ div.toast_shown {
.media_video .fill {
background-image: url(../images/media_video.png)
}
+.audio_icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background-color: #4f9cd9;
+ background-image: url(../images/media_music.png);
+ background-repeat: no-repeat;
+ background-position: 12px 12px;
+ background-size: 24px 24px;
+}
@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) {
.section.calls {
@@ -504,6 +517,9 @@ div.toast_shown {
.section.stories {
background-image: url(../images/section_stories@2x.png);
}
+.section.music {
+ background-image: url(../images/section_music@2x.png);
+}
.section.web {
background-image: url(../images/section_web@2x.png);
}
@@ -545,6 +561,9 @@ div.toast_shown {
.media_video .fill {
background-image: url(../images/media_video@2x.png)
}
+.audio_icon {
+ background-image: url(../images/media_music@2x.png);
+}
}
.spoiler {
diff --git a/Telegram/Resources/export_html/images/section_music.png b/Telegram/Resources/export_html/images/section_music.png
new file mode 100644
index 0000000000..a76a2449d5
Binary files /dev/null and b/Telegram/Resources/export_html/images/section_music.png differ
diff --git a/Telegram/Resources/export_html/images/section_music@2x.png b/Telegram/Resources/export_html/images/section_music@2x.png
new file mode 100644
index 0000000000..53dc7022c6
Binary files /dev/null and b/Telegram/Resources/export_html/images/section_music@2x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index e2e26509ae..28fe73360f 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -6304,6 +6304,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices.";
"lng_export_option_stories" = "Story archive";
"lng_export_option_stories_about" = "All stories you posted from Telegram mobile apps.";
+"lng_export_option_profile_music" = "Music on Profiles";
+"lng_export_option_profile_music_about" = "All tracks you saved to your playlist.";
"lng_export_option_sessions" = "Active sessions";
"lng_export_option_sessions_about" = "We may store this to display your connected devices in Settings > Privacy & Security > Show all sessions.";
"lng_export_header_other" = "Other";
diff --git a/Telegram/Resources/qrc/telegram/export.qrc b/Telegram/Resources/qrc/telegram/export.qrc
index 290d24a30b..e3656ae944 100644
--- a/Telegram/Resources/qrc/telegram/export.qrc
+++ b/Telegram/Resources/qrc/telegram/export.qrc
@@ -31,6 +31,8 @@
../../export_html/images/section_contacts@2x.png
../../export_html/images/section_frequent.png
../../export_html/images/section_frequent@2x.png
+ ../../export_html/images/section_music.png
+ ../../export_html/images/section_music@2x.png
../../export_html/images/section_other.png
../../export_html/images/section_other@2x.png
../../export_html/images/section_photos.png
diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h
index 3b9111dc09..278f6946ec 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.h
+++ b/Telegram/SourceFiles/export/data/export_data_types.h
@@ -89,6 +89,10 @@ struct StoriesInfo {
int count = 0;
};
+struct ProfileMusicInfo {
+ int count = 0;
+};
+
struct FileLocation {
int dcId = 0;
MTPInputFileLocation data;
@@ -929,6 +933,11 @@ StoriesSlice ParseStoriesSlice(
const MTPVector &data,
int baseIndex);
+struct ProfileMusicSlice {
+ std::vector list;
+ int skipped = 0;
+};
+
Message ParseMessage(
ParseMediaContext &context,
const MTPMessage &data,
diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp
index e378d9007c..bde995d42b 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.cpp
+++ b/Telegram/SourceFiles/export/export_api_wrap.cpp
@@ -32,6 +32,7 @@ constexpr auto kFileMaxSize = 4000 * int64(1024 * 1024);
constexpr auto kLocationCacheSize = 100'000;
constexpr auto kMaxEmojiPerRequest = 100;
constexpr auto kStoriesSliceLimit = 100;
+constexpr auto kProfileMusicSliceLimit = 100;
struct LocationKey {
uint64 type;
@@ -112,6 +113,7 @@ struct ApiWrap::StartProcess {
enum class Step {
UserpicsCount,
StoriesCount,
+ ProfileMusicCount,
SplitRanges,
DialogsCount,
LeftChannelsCount,
@@ -155,6 +157,19 @@ struct ApiWrap::StoriesProcess {
int fileIndex = 0;
};
+struct ApiWrap::ProfileMusicProcess {
+ FnMut start;
+ Fn fileProgress;
+ Fn handleSlice;
+ FnMut finish;
+
+ int processed = 0;
+ std::optional slice;
+ int offsetId = 0;
+ bool lastSlice = false;
+ int fileIndex = 0;
+};
+
struct ApiWrap::OtherDataProcess {
Data::File file;
FnMut done;
@@ -438,6 +453,9 @@ void ApiWrap::startExport(
if (_settings->types & Settings::Type::Stories) {
_startProcess->steps.push_back(Step::StoriesCount);
}
+ if (_settings->types & Settings::Type::ProfileMusic) {
+ _startProcess->steps.push_back(Step::ProfileMusicCount);
+ }
if (_settings->types & Settings::Type::AnyChatsMask) {
_startProcess->steps.push_back(Step::SplitRanges);
_startProcess->steps.push_back(Step::DialogsCount);
@@ -468,6 +486,8 @@ void ApiWrap::sendNextStartRequest() {
return requestUserpicsCount();
case Step::StoriesCount:
return requestStoriesCount();
+ case Step::ProfileMusicCount:
+ return requestProfileMusicCount();
case Step::SplitRanges:
return requestSplitRanges();
case Step::DialogsCount:
@@ -518,6 +538,34 @@ void ApiWrap::requestStoriesCount() {
}).send();
}
+void ApiWrap::requestProfileMusicCount() {
+ Expects(_startProcess != nullptr);
+
+ mainRequest(MTPusers_GetSavedMusic(
+ _user,
+ MTP_int(0), // offset
+ MTP_int(0), // limit
+ MTP_long(0) // hash
+ )).done([=](const MTPusers_SavedMusic &result) {
+ Expects(_settings != nullptr);
+ Expects(_startProcess != nullptr);
+
+ const auto count = result.match(
+ [](const MTPDusers_savedMusic &data) {
+ return data.vcount().v;
+ }, [](const MTPDusers_savedMusicNotModified &data) {
+ return -1;
+ });
+ if (count < 0) {
+ error("Unexpected messagesNotModified received.");
+ return;
+ }
+ _startProcess->info.profileMusicCount = count;
+
+ sendNextStartRequest();
+ }).send();
+}
+
void ApiWrap::requestSplitRanges() {
Expects(_startProcess != nullptr);
@@ -1062,6 +1110,215 @@ void ApiWrap::finishStories() {
base::take(_storiesProcess)->finish();
}
+void ApiWrap::requestProfileMusic(
+ FnMut start,
+ Fn progress,
+ Fn slice,
+ FnMut finish) {
+ Expects(_profileMusicProcess == nullptr);
+
+ _profileMusicProcess = std::make_unique();
+ _profileMusicProcess->start = std::move(start);
+ _profileMusicProcess->fileProgress = std::move(progress);
+ _profileMusicProcess->handleSlice = std::move(slice);
+ _profileMusicProcess->finish = std::move(finish);
+
+ mainRequest(MTPusers_GetSavedMusic(
+ _user,
+ MTP_int(0), // offset
+ MTP_int(kProfileMusicSliceLimit), // limit
+ MTP_long(0) // hash
+ )).done([=](const MTPusers_SavedMusic &result) mutable {
+ Expects(_profileMusicProcess != nullptr);
+
+ auto startInfo = result.match(
+ [](const MTPDusers_savedMusic &data) {
+ return Data::ProfileMusicInfo{ data.vcount().v };
+ }, [](const MTPDusers_savedMusicNotModified &data) {
+ return Data::ProfileMusicInfo{ 0 };
+ });
+ if (!_profileMusicProcess->start(std::move(startInfo))) {
+ return;
+ }
+
+ handleProfileMusicSlice(result);
+ }).send();
+}
+
+void ApiWrap::handleProfileMusicSlice(const MTPusers_SavedMusic &result) {
+ Expects(_profileMusicProcess != nullptr);
+ Expects(_selfId.has_value());
+
+ auto context = Data::ParseMediaContext();
+ context.selfPeerId = peerFromUser(*_selfId);
+
+ auto slice = result.match([&](const MTPDusers_savedMusic &data) {
+ if (data.vdocuments().v.size() < kProfileMusicSliceLimit) {
+ _profileMusicProcess->lastSlice = true;
+ }
+ auto result = Data::MessagesSlice();
+ for (const auto &doc : data.vdocuments().v) {
+ auto message = Data::Message();
+ message.id = ++_profileMusicProcess->processed;
+ message.date = 0;
+ message.media.content = Data::ParseDocument(
+ context,
+ doc,
+ "profile_music/",
+ 0);
+ result.list.push_back(std::move(message));
+ }
+ return result;
+ }, [&](const MTPDusers_savedMusicNotModified &) {
+ _profileMusicProcess->lastSlice = true;
+ return Data::MessagesSlice();
+ });
+
+ auto profileSlice = Data::ProfileMusicSlice();
+ profileSlice.list.reserve(slice.list.size());
+ for (auto &message : slice.list) {
+ if (v::is(message.media.content)) {
+ const auto &doc = v::get(message.media.content);
+ if (doc.isAudioFile) {
+ profileSlice.list.push_back(std::move(message));
+ }
+ }
+ }
+
+ loadProfileMusicFiles(std::move(profileSlice));
+}
+
+void ApiWrap::loadProfileMusicFiles(Data::ProfileMusicSlice &&slice) {
+ Expects(_profileMusicProcess != nullptr);
+ Expects(!_profileMusicProcess->slice.has_value());
+
+ if (slice.list.empty()) {
+ _profileMusicProcess->lastSlice = true;
+ }
+ _profileMusicProcess->slice = std::move(slice);
+ _profileMusicProcess->fileIndex = 0;
+ loadNextProfileMusic();
+}
+
+void ApiWrap::loadNextProfileMusic() {
+ Expects(_profileMusicProcess != nullptr);
+ Expects(_profileMusicProcess->slice.has_value());
+
+ for (auto &list = _profileMusicProcess->slice->list
+ ; _profileMusicProcess->fileIndex < list.size()
+ ; ++_profileMusicProcess->fileIndex) {
+ auto &message = list[_profileMusicProcess->fileIndex];
+ const auto origin = Data::FileOrigin{ .messageId = message.id };
+ const auto ready = processFileLoad(
+ message.file(),
+ origin,
+ [=](FileProgress value) { return loadProfileMusicProgress(value); },
+ [=](const QString &path) { loadProfileMusicDone(path); },
+ &message);
+ if (!ready) {
+ return;
+ }
+ const auto thumbProgress = [=](FileProgress value) {
+ return loadProfileMusicThumbProgress(value);
+ };
+ const auto thumbReady = processFileLoad(
+ message.thumb().file,
+ origin,
+ thumbProgress,
+ [=](const QString &path) { loadProfileMusicThumbDone(path); },
+ &message);
+ if (!thumbReady) {
+ return;
+ }
+ }
+ finishProfileMusicSlice();
+}
+
+void ApiWrap::finishProfileMusicSlice() {
+ Expects(_profileMusicProcess != nullptr);
+ Expects(_profileMusicProcess->slice.has_value());
+
+ auto slice = *base::take(_profileMusicProcess->slice);
+ if (!slice.list.empty()) {
+ _profileMusicProcess->processed += slice.list.size();
+ _profileMusicProcess->offsetId = slice.list.back().id;
+ if (!_profileMusicProcess->handleSlice(std::move(slice))) {
+ return;
+ }
+ }
+ if (_profileMusicProcess->lastSlice) {
+ finishProfileMusic();
+ return;
+ }
+
+ mainRequest(MTPusers_GetSavedMusic(
+ _user,
+ MTP_int(_profileMusicProcess->offsetId),
+ MTP_int(kProfileMusicSliceLimit),
+ MTP_long(0)
+ )).done([=](const MTPusers_SavedMusic &result) {
+ handleProfileMusicSlice(result);
+ }).send();
+}
+
+bool ApiWrap::loadProfileMusicProgress(FileProgress progress) {
+ Expects(_fileProcess != nullptr);
+ Expects(_profileMusicProcess != nullptr);
+ Expects(_profileMusicProcess->slice.has_value());
+ Expects((_profileMusicProcess->fileIndex >= 0)
+ && (_profileMusicProcess->fileIndex
+ < _profileMusicProcess->slice->list.size()));
+
+ return _profileMusicProcess->fileProgress(DownloadProgress{
+ _fileProcess->randomId,
+ _fileProcess->relativePath,
+ _profileMusicProcess->fileIndex,
+ progress.ready,
+ progress.total });
+}
+
+void ApiWrap::loadProfileMusicDone(const QString &relativePath) {
+ Expects(_profileMusicProcess != nullptr);
+ Expects(_profileMusicProcess->slice.has_value());
+ Expects((_profileMusicProcess->fileIndex >= 0)
+ && (_profileMusicProcess->fileIndex
+ < _profileMusicProcess->slice->list.size()));
+
+ const auto index = _profileMusicProcess->fileIndex;
+ auto &file = _profileMusicProcess->slice->list[index].file();
+ file.relativePath = relativePath;
+ if (relativePath.isEmpty()) {
+ file.skipReason = Data::File::SkipReason::Unavailable;
+ }
+ loadNextProfileMusic();
+}
+
+bool ApiWrap::loadProfileMusicThumbProgress(FileProgress progress) {
+ return loadProfileMusicProgress(progress);
+}
+
+void ApiWrap::loadProfileMusicThumbDone(const QString &relativePath) {
+ Expects(_profileMusicProcess != nullptr);
+ Expects(_profileMusicProcess->slice.has_value());
+ Expects((_profileMusicProcess->fileIndex >= 0)
+ && (_profileMusicProcess->fileIndex
+ < _profileMusicProcess->slice->list.size()));
+
+ const auto index = _profileMusicProcess->fileIndex;
+ auto &file = _profileMusicProcess->slice->list[index].thumb().file;
+ file.relativePath = relativePath;
+ if (relativePath.isEmpty()) {
+ file.skipReason = Data::File::SkipReason::Unavailable;
+ }
+ loadNextProfileMusic();
+}
+
+void ApiWrap::finishProfileMusic() {
+ Expects(_profileMusicProcess != nullptr);
+
+ base::take(_profileMusicProcess)->finish();
+}
+
void ApiWrap::requestContacts(FnMut done) {
Expects(_contactsProcess == nullptr);
diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h
index de76417bc7..4d43cd508d 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.h
+++ b/Telegram/SourceFiles/export/export_api_wrap.h
@@ -21,6 +21,8 @@ struct UserpicsInfo;
struct UserpicsSlice;
struct StoriesInfo;
struct StoriesSlice;
+struct ProfileMusicInfo;
+struct ProfileMusicSlice;
struct ContactsList;
struct SessionsList;
struct DialogsInfo;
@@ -50,6 +52,7 @@ public:
struct StartInfo {
int userpicsCount = 0;
int storiesCount = 0;
+ int profileMusicCount = 0;
int dialogsCount = 0;
};
void startExport(
@@ -86,6 +89,12 @@ public:
Fn slice,
FnMut finish);
+ void requestProfileMusic(
+ FnMut start,
+ Fn progress,
+ Fn slice,
+ FnMut finish);
+
void requestContacts(FnMut done);
void requestSessions(FnMut done);
@@ -109,6 +118,7 @@ private:
struct ContactsProcess;
struct UserpicsProcess;
struct StoriesProcess;
+ struct ProfileMusicProcess;
struct OtherDataProcess;
struct FileProcess;
struct FileProgress;
@@ -121,6 +131,7 @@ private:
void sendNextStartRequest();
void requestUserpicsCount();
void requestStoriesCount();
+ void requestProfileMusicCount();
void requestSplitRanges();
void requestDialogsCount();
void requestLeftChannelsCount();
@@ -146,6 +157,16 @@ private:
void finishStoriesSlice();
void finishStories();
+ void handleProfileMusicSlice(const MTPusers_SavedMusic &result);
+ void loadProfileMusicFiles(Data::ProfileMusicSlice &&slice);
+ void loadNextProfileMusic();
+ bool loadProfileMusicProgress(FileProgress value);
+ void loadProfileMusicDone(const QString &relativePath);
+ bool loadProfileMusicThumbProgress(FileProgress value);
+ void loadProfileMusicThumbDone(const QString &relativePath);
+ void finishProfileMusicSlice();
+ void finishProfileMusic();
+
void otherDataDone(const QString &relativePath);
bool useOnlyLastSplit() const;
@@ -258,6 +279,7 @@ private:
std::unique_ptr _contactsProcess;
std::unique_ptr _userpicsProcess;
std::unique_ptr _storiesProcess;
+ std::unique_ptr _profileMusicProcess;
std::unique_ptr _otherDataProcess;
std::unique_ptr _fileProcess;
std::unique_ptr _leftChannelsProcess;
diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp
index a6e12d28da..ba4714c485 100644
--- a/Telegram/SourceFiles/export/export_controller.cpp
+++ b/Telegram/SourceFiles/export/export_controller.cpp
@@ -76,6 +76,7 @@ private:
void exportPersonalInfo();
void exportUserpics();
void exportStories();
+ void exportProfileMusic();
void exportContacts();
void exportSessions();
void exportOtherData();
@@ -91,6 +92,7 @@ private:
ProcessingState statePersonalInfo() const;
ProcessingState stateUserpics(const DownloadProgress &progress) const;
ProcessingState stateStories(const DownloadProgress &progress) const;
+ ProcessingState stateProfileMusic(const DownloadProgress &progress) const;
ProcessingState stateContacts() const;
ProcessingState stateSessions() const;
ProcessingState stateOtherData() const;
@@ -119,6 +121,9 @@ private:
int _storiesWritten = 0;
int _storiesCount = 0;
+ int _profileMusicWritten = 0;
+ int _profileMusicCount = 0;
+
// rpl::variable fails to compile in MSVC :(
State _state;
rpl::event_stream _stateChanges;
@@ -281,6 +286,9 @@ void ControllerObject::fillExportSteps() {
if (_settings.types & Type::Stories) {
_steps.push_back(Step::Stories);
}
+ if (_settings.types & Type::ProfileMusic) {
+ _steps.push_back(Step::ProfileMusic);
+ }
if (_settings.types & Type::Contacts) {
_steps.push_back(Step::Contacts);
}
@@ -317,6 +325,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
if (_settings.types & Settings::Type::Stories) {
push(Step::Stories, 1);
}
+ if (_settings.types & Settings::Type::ProfileMusic) {
+ push(Step::ProfileMusic, 1);
+ }
if (_settings.types & Settings::Type::Contacts) {
push(Step::Contacts, 1);
}
@@ -356,6 +367,7 @@ void ControllerObject::exportNext() {
case Step::PersonalInfo: return exportPersonalInfo();
case Step::Userpics: return exportUserpics();
case Step::Stories: return exportStories();
+ case Step::ProfileMusic: return exportProfileMusic();
case Step::Contacts: return exportContacts();
case Step::Sessions: return exportSessions();
case Step::OtherData: return exportOtherData();
@@ -454,6 +466,32 @@ void ControllerObject::exportStories() {
});
}
+void ControllerObject::exportProfileMusic() {
+ _api.requestProfileMusic([=](Data::ProfileMusicInfo &&start) {
+ if (ioCatchError(_writer->writeProfileMusicStart(start))) {
+ return false;
+ }
+ _profileMusicWritten = 0;
+ _profileMusicCount = start.count;
+ return true;
+ }, [=](DownloadProgress progress) {
+ setState(stateProfileMusic(progress));
+ return true;
+ }, [=](Data::ProfileMusicSlice &&slice) {
+ if (ioCatchError(_writer->writeProfileMusicSlice(slice))) {
+ return false;
+ }
+ _profileMusicWritten += slice.list.size();
+ setState(stateProfileMusic(DownloadProgress()));
+ return true;
+ }, [=] {
+ if (ioCatchError(_writer->writeProfileMusicEnd())) {
+ return;
+ }
+ exportNext();
+ });
+}
+
void ControllerObject::exportContacts() {
setState(stateContacts());
_api.requestContacts([=](Data::ContactsList &&result) {
@@ -596,6 +634,21 @@ ProcessingState ControllerObject::stateStories(
});
}
+ProcessingState ControllerObject::stateProfileMusic(
+ const DownloadProgress &progress) const {
+ return prepareState(Step::ProfileMusic, [&](ProcessingState &result) {
+ result.entityIndex = _profileMusicWritten + progress.itemIndex;
+ result.entityCount = std::max(_profileMusicCount, result.entityIndex);
+ result.bytesRandomId = progress.randomId;
+ if (!progress.path.isEmpty()) {
+ const auto last = progress.path.lastIndexOf('/');
+ result.bytesName = progress.path.mid(last + 1);
+ }
+ result.bytesLoaded = progress.ready;
+ result.bytesCount = progress.total;
+ });
+}
+
ProcessingState ControllerObject::stateContacts() const {
return prepareState(Step::Contacts);
}
diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h
index b486a6d094..5209f9a6ed 100644
--- a/Telegram/SourceFiles/export/export_controller.h
+++ b/Telegram/SourceFiles/export/export_controller.h
@@ -39,6 +39,7 @@ struct ProcessingState {
PersonalInfo,
Userpics,
Stories,
+ ProfileMusic,
Contacts,
Sessions,
OtherData,
diff --git a/Telegram/SourceFiles/export/export_settings.h b/Telegram/SourceFiles/export/export_settings.h
index be2e1b782a..b19024e81b 100644
--- a/Telegram/SourceFiles/export/export_settings.h
+++ b/Telegram/SourceFiles/export/export_settings.h
@@ -58,6 +58,7 @@ struct Settings {
PrivateChannels = 0x200,
PublicChannels = 0x400,
Stories = 0x800,
+ ProfileMusic = 0x1000,
GroupsMask = PrivateGroups | PublicGroups,
ChannelsMask = PrivateChannels | PublicChannels,
@@ -68,6 +69,7 @@ struct Settings {
| Userpics
| Contacts
| Stories
+ | ProfileMusic
| Sessions),
AllMask = NonChatsMask | OtherData | AnyChatsMask,
};
@@ -97,6 +99,7 @@ struct Settings {
| Type::Userpics
| Type::Contacts
| Type::Stories
+ | Type::ProfileMusic
| Type::PersonalChats
| Type::PrivateGroups;
}
diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.h b/Telegram/SourceFiles/export/output/export_output_abstract.h
index 9ceef46ebd..611a4acace 100644
--- a/Telegram/SourceFiles/export/output/export_output_abstract.h
+++ b/Telegram/SourceFiles/export/output/export_output_abstract.h
@@ -16,6 +16,8 @@ struct UserpicsInfo;
struct UserpicsSlice;
struct StoriesInfo;
struct StoriesSlice;
+struct ProfileMusicInfo;
+struct ProfileMusicSlice;
struct ContactsList;
struct SessionsList;
struct DialogsInfo;
@@ -64,6 +66,12 @@ public:
const Data::StoriesSlice &data) = 0;
[[nodiscard]] virtual Result writeStoriesEnd() = 0;
+ [[nodiscard]] virtual Result writeProfileMusicStart(
+ const Data::ProfileMusicInfo &data) = 0;
+ [[nodiscard]] virtual Result writeProfileMusicSlice(
+ const Data::ProfileMusicSlice &data) = 0;
+ [[nodiscard]] virtual Result writeProfileMusicEnd() = 0;
+
[[nodiscard]] virtual Result writeContactsList(
const Data::ContactsList &data) = 0;
diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp
index d62ad4264f..4aa35d8e2f 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html.cpp
@@ -44,9 +44,10 @@ constexpr auto kContactsPriority = 2;
constexpr auto kFrequentContactsPriority = 3;
constexpr auto kUserpicsPriority = 4;
constexpr auto kStoriesPriority = 5;
-constexpr auto kSessionsPriority = 6;
-constexpr auto kWebSessionsPriority = 7;
-constexpr auto kOtherPriority = 8;
+constexpr auto kProfileMusicPriority = 6;
+constexpr auto kSessionsPriority = 7;
+constexpr auto kWebSessionsPriority = 8;
+constexpr auto kOtherPriority = 9;
const auto kLineBreak = QByteArrayLiteral("
");
@@ -538,6 +539,12 @@ public:
const std::vector &caption,
const QString &internalLinksDomain,
const QString &link = QString());
+ [[nodiscard]] QByteArray pushAudioEntry(
+ const QByteArray &name,
+ const QByteArray &info,
+ const QByteArrayList &details,
+ const QByteArray &duration,
+ const QString &link = QString());
[[nodiscard]] QByteArray pushSessionListEntry(
int apiId,
const QByteArray &name,
@@ -877,6 +884,54 @@ QByteArray HtmlWriter::Wrap::pushStoriesListEntry(
return result;
}
+QByteArray HtmlWriter::Wrap::pushAudioEntry(
+ const QByteArray &name,
+ const QByteArray &info,
+ const QByteArrayList &details,
+ const QByteArray &duration,
+ const QString &link) {
+ auto result = pushDiv("entry clearfix");
+ if (!link.isEmpty()) {
+ result.append(pushTag("a", {
+ { "class", "pull_left userpic_wrap" },
+ { "href", relativePath(link).toUtf8() + "#allow_back" },
+ }));
+ } else {
+ result.append(pushDiv("pull_left userpic_wrap"));
+ }
+ result.append(pushDiv("userpic audio_icon"));
+ result.append(popTag());
+ result.append(popTag());
+ result.append(pushDiv("body"));
+ if (!duration.isEmpty()) {
+ result.append(pushDiv("pull_right info details"));
+ result.append(SerializeString(duration));
+ result.append(popTag());
+ }
+ if (!info.isEmpty()) {
+ if (!link.isEmpty()) {
+ result.append(pushTag("a", {
+ { "class", "block_link expanded" },
+ { "href", relativePath(link).toUtf8() + "#allow_back" },
+ }));
+ }
+ result.append(pushDiv("name bold"));
+ result.append(SerializeString(info));
+ result.append(popTag());
+ if (!link.isEmpty()) {
+ result.append(popTag());
+ }
+ }
+ for (const auto &detail : details) {
+ result.append(pushDiv("details_entry details"));
+ result.append(SerializeString(detail));
+ result.append(popTag());
+ }
+ result.append(popTag());
+ result.append(popTag());
+ return result;
+}
+
QByteArray HtmlWriter::Wrap::pushSessionListEntry(
int apiId,
const QByteArray &name,
@@ -2706,6 +2761,7 @@ Result HtmlWriter::start(
"images/section_chats.png",
"images/section_contacts.png",
"images/section_frequent.png",
+ "images/section_music.png",
"images/section_other.png",
"images/section_photos.png",
"images/section_sessions.png",
@@ -3001,6 +3057,93 @@ Result HtmlWriter::writeStoriesEnd() {
return Result::Success();
}
+Result HtmlWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {
+ Expects(_summary != nullptr);
+ Expects(_profileMusic == nullptr);
+
+ _profileMusicCount = data.count;
+ if (!_profileMusicCount) {
+ return Result::Success();
+ }
+ _profileMusic = fileWithRelativePath(profileMusicFilePath());
+
+ auto block = _profileMusic->pushHeader(
+ "Profile Music",
+ mainFileRelativePath());
+ block.append(_profileMusic->pushDiv("page_body list_page"));
+ block.append(_profileMusic->pushDiv("entry_list"));
+ if (const auto result = _profileMusic->writeBlock(block); !result) {
+ return result;
+ }
+ return Result::Success();
+}
+
+Result HtmlWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {
+ Expects(_profileMusic != nullptr);
+
+ _profileMusicCount -= data.skipped;
+ if (data.list.empty()) {
+ return Result::Success();
+ }
+ auto block = QByteArray();
+ for (const auto &message : data.list) {
+ if (!v::is(message.media.content)) {
+ continue;
+ }
+ const auto &doc = v::get(message.media.content);
+ if (!doc.isAudioFile) {
+ continue;
+ }
+ using SkipReason = Data::File::SkipReason;
+ const auto &file = doc.file;
+ Assert(!file.relativePath.isEmpty()
+ || file.skipReason != SkipReason::None);
+ auto status = QByteArrayList();
+ status.append([&]() -> Data::Utf8String {
+ switch (file.skipReason) {
+ case SkipReason::Unavailable:
+ return "(File unavailable, please try again later)";
+ case SkipReason::FileSize:
+ return "(File exceeds maximum size. "
+ "Change data exporting settings to download.)";
+ case SkipReason::FileType:
+ return "(File not included. "
+ "Change data exporting settings to download.)";
+ case SkipReason::None: return Data::FormatFileSize(file.size);
+ }
+ Unexpected("Skip reason while writing profile music path.");
+ }());
+ const auto &path = file.relativePath;
+ const auto title = !doc.songTitle.isEmpty()
+ ? doc.songTitle
+ : !doc.name.isEmpty()
+ ? doc.name
+ : Data::Utf8String("Unknown Track");
+ const auto performer = !doc.songPerformer.isEmpty()
+ ? doc.songPerformer
+ : Data::Utf8String("Unknown Artist");
+ const auto info = performer + " - " + title;
+ const auto duration = doc.duration > 0
+ ? Data::FormatDuration(doc.duration)
+ : QByteArray();
+ block.append(_profileMusic->pushAudioEntry(
+ (path.isEmpty() ? QString("File unavailable") : path).toUtf8(),
+ info,
+ status,
+ duration,
+ path));
+ }
+ return _profileMusic->writeBlock(block);
+}
+
+Result HtmlWriter::writeProfileMusicEnd() {
+ pushProfileMusicSection();
+ if (_profileMusic) {
+ return base::take(_profileMusic)->close();
+ }
+ return Result::Success();
+}
+
QString HtmlWriter::storiesFilePath() const {
return "lists/stories.html";
}
@@ -3014,6 +3157,19 @@ void HtmlWriter::pushStoriesSection() {
storiesFilePath());
}
+QString HtmlWriter::profileMusicFilePath() const {
+ return "lists/profile_music.html";
+}
+
+void HtmlWriter::pushProfileMusicSection() {
+ pushSection(
+ kProfileMusicPriority,
+ "Profile Music",
+ "music",
+ _profileMusicCount,
+ profileMusicFilePath());
+}
+
Result HtmlWriter::writeContactsList(const Data::ContactsList &data) {
Expects(_summary != nullptr);
diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h
index 27d68a54a7..2ecc9aaab5 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.h
+++ b/Telegram/SourceFiles/export/output/export_output_html.h
@@ -64,6 +64,10 @@ public:
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
Result writeStoriesEnd() override;
+ Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
+ Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
+ Result writeProfileMusicEnd() override;
+
Result writeContactsList(const Data::ContactsList &data) override;
Result writeSessionsList(const Data::SessionsList &data) override;
@@ -131,9 +135,11 @@ private:
const QString &userpicPath);
void pushUserpicsSection();
void pushStoriesSection();
+ void pushProfileMusicSection();
[[nodiscard]] QString userpicsFilePath() const;
[[nodiscard]] QString storiesFilePath() const;
+ [[nodiscard]] QString profileMusicFilePath() const;
[[nodiscard]] QByteArray wrapMessageLink(
int messageId,
@@ -159,6 +165,9 @@ private:
int _storiesCount = 0;
std::unique_ptr _stories;
+ int _profileMusicCount = 0;
+ std::unique_ptr _profileMusic;
+
QString _dialogsRelativePath;
Data::DialogInfo _dialog;
DialogsMode _dialogsMode = DialogsMode::None;
diff --git a/Telegram/SourceFiles/export/output/export_output_html_and_json.cpp b/Telegram/SourceFiles/export/output/export_output_html_and_json.cpp
index 9220147ea8..98b6af8c98 100644
--- a/Telegram/SourceFiles/export/output/export_output_html_and_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html_and_json.cpp
@@ -73,6 +73,24 @@ Result HtmlAndJsonWriter::writeStoriesEnd() {
});
}
+Result HtmlAndJsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {
+ return invoke([&](WriterPtr w) {
+ return w->writeProfileMusicStart(data);
+ });
+}
+
+Result HtmlAndJsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {
+ return invoke([&](WriterPtr w) {
+ return w->writeProfileMusicSlice(data);
+ });
+}
+
+Result HtmlAndJsonWriter::writeProfileMusicEnd() {
+ return invoke([&](WriterPtr w) {
+ return w->writeProfileMusicEnd();
+ });
+}
+
Result HtmlAndJsonWriter::writeContactsList(const Data::ContactsList &data) {
return invoke([&](WriterPtr w) {
return w->writeContactsList(data);
diff --git a/Telegram/SourceFiles/export/output/export_output_html_and_json.h b/Telegram/SourceFiles/export/output/export_output_html_and_json.h
index 2a40833a8c..e3071a8c14 100644
--- a/Telegram/SourceFiles/export/output/export_output_html_and_json.h
+++ b/Telegram/SourceFiles/export/output/export_output_html_and_json.h
@@ -36,6 +36,10 @@ public:
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
Result writeStoriesEnd() override;
+ Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
+ Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
+ Result writeProfileMusicEnd() override;
+
Result writeContactsList(const Data::ContactsList &data) override;
Result writeSessionsList(const Data::SessionsList &data) override;
diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp
index 7b0b9e4fd1..221d4c056f 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_json.cpp
@@ -1318,6 +1318,34 @@ Result JsonWriter::writeStoriesEnd() {
return _output->writeBlock(popNesting());
}
+Result JsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {
+ Expects(_output != nullptr);
+
+ auto block = prepareObjectItemStart("profile_music");
+ return _output->writeBlock(block + pushNesting(Context::kArray));
+}
+
+Result JsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {
+ Expects(_output != nullptr);
+
+ if (data.list.empty()) {
+ return Result::Success();
+ }
+
+ auto block = QByteArray();
+ for (const auto &message : data.list) {
+ block.append(prepareArrayItemStart());
+ block.append(SerializeMessage(_context, message, {}, QString()));
+ }
+ return _output->writeBlock(block);
+}
+
+Result JsonWriter::writeProfileMusicEnd() {
+ Expects(_output != nullptr);
+
+ return _output->writeBlock(popNesting());
+}
+
Result JsonWriter::writeContactsList(const Data::ContactsList &data) {
Expects(_output != nullptr);
diff --git a/Telegram/SourceFiles/export/output/export_output_json.h b/Telegram/SourceFiles/export/output/export_output_json.h
index 879918fcee..a50e547f58 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.h
+++ b/Telegram/SourceFiles/export/output/export_output_json.h
@@ -48,6 +48,10 @@ public:
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
Result writeStoriesEnd() override;
+ Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
+ Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
+ Result writeProfileMusicEnd() override;
+
Result writeContactsList(const Data::ContactsList &data) override;
Result writeSessionsList(const Data::SessionsList &data) override;
diff --git a/Telegram/SourceFiles/export/view/export_view_content.cpp b/Telegram/SourceFiles/export/view/export_view_content.cpp
index 9813321efe..fad7f99220 100644
--- a/Telegram/SourceFiles/export/view/export_view_content.cpp
+++ b/Telegram/SourceFiles/export/view/export_view_content.cpp
@@ -96,6 +96,13 @@ Content ContentFromState(
state.bytesName,
state.bytesRandomId);
break;
+ case Step::ProfileMusic:
+ pushMain(tr::lng_export_option_profile_music(tr::now));
+ pushBytes(
+ "music" + QString::number(state.entityIndex),
+ state.bytesName,
+ state.bytesRandomId);
+ break;
case Step::Sessions:
pushMain(tr::lng_export_option_sessions(tr::now));
break;
diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp
index e610471279..9fb09807be 100644
--- a/Telegram/SourceFiles/export/view/export_view_settings.cpp
+++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp
@@ -183,6 +183,11 @@ void SettingsWidget::setupFullExportOptions(
tr::lng_export_option_stories(tr::now),
Type::Stories,
tr::lng_export_option_stories_about(tr::now));
+ addOptionWithAbout(
+ container,
+ tr::lng_export_option_profile_music(tr::now),
+ Type::ProfileMusic,
+ tr::lng_export_option_profile_music_about(tr::now));
addHeader(container, tr::lng_export_header_chats(tr::now));
addOption(
container,
@@ -233,7 +238,8 @@ void SettingsWidget::setupMediaOptions(
| Type::PrivateGroups
| Type::PrivateChannels
| Type::PublicGroups
- | Type::PublicChannels)) != 0, anim::type::normal);
+ | Type::PublicChannels
+ | Type::ProfileMusic)) != 0, anim::type::normal);
}, mediaWrap->lifetime());
widthValue(