From f41fdcdb98ad9b10f7e78c063bb26fc3964f49c8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 20 Nov 2025 13:55:25 +0300 Subject: [PATCH] Added saved music to data export. --- Telegram/Resources/export_html/css/style.css | 19 ++ .../export_html/images/section_music.png | Bin 0 -> 446 bytes .../export_html/images/section_music@2x.png | Bin 0 -> 777 bytes Telegram/Resources/langs/lang.strings | 2 + Telegram/Resources/qrc/telegram/export.qrc | 2 + .../export/data/export_data_types.h | 9 + .../SourceFiles/export/export_api_wrap.cpp | 257 ++++++++++++++++++ Telegram/SourceFiles/export/export_api_wrap.h | 22 ++ .../SourceFiles/export/export_controller.cpp | 53 ++++ .../SourceFiles/export/export_controller.h | 1 + Telegram/SourceFiles/export/export_settings.h | 3 + .../export/output/export_output_abstract.h | 8 + .../export/output/export_output_html.cpp | 162 ++++++++++- .../export/output/export_output_html.h | 9 + .../output/export_output_html_and_json.cpp | 18 ++ .../output/export_output_html_and_json.h | 4 + .../export/output/export_output_json.cpp | 28 ++ .../export/output/export_output_json.h | 4 + .../export/view/export_view_content.cpp | 7 + .../export/view/export_view_settings.cpp | 8 +- 20 files changed, 612 insertions(+), 4 deletions(-) create mode 100644 Telegram/Resources/export_html/images/section_music.png create mode 100644 Telegram/Resources/export_html/images/section_music@2x.png 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 0000000000000000000000000000000000000000..a76a2449d54fd270397f50f414bc6a0c5f893fe6 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgfOvBT~F~oy6 zIYEMz#i?OpgH2mWV&&I*9>)a^_37GSn`dm$T((TYQY_&sqn`W3WJbNs8@e_ph^jgX zgwI%utP+cH*X4bi95Oiyeu`t$DB@gMi+O>CSfJj3Z5gW)xq?s-)SHFHjiq*!o2 ze&2oIg8-8hU$8t^vViwvA6JFx%)E?J=Qd29*y{N1e0^AC+kc+h9i|e?_?EN&mz0!o z=#uzvu;lyJZFckST>a28wb4{`*~AE`<*Y0`6@CtKdcDSc690{57=9^zvgba0X3?ux zSMKkR{Qp1ghpq6U$sZN&urTq=5&M&tpqrfd<1JrLN_m?-Pk)0TpYbbO<6gB0F2&*5QUU@G=>aSX{| zeH(E$OTD=tGNa$43bBmHKOl&%r)y^q(dTDgeeSBf=4^fF9IwsLo zMVvF{9OA#}nNq{EUM~Fur+srd^TF#a_r(p1{T2kR-qjyBAC&BB*8&tLt){)D^t zk2US#P>eai{K8k~@7|VP@r%1!{^doOeq6uTA#RJ#0*S?YO~@A^Cy?FH824qgS6Ag-(B#Z2fLhgKk^a z|6tLs7rRwHM6ym(oE?z5)t<#MX!*}o+t$^-X8Dk_XK`a;*j{Vy9qYU(9+rb&K{JhRDik#%*Nh27&lwu?NZAJlBW zo^{VZnBPclmxZ|$Z(;JMTVc^B1i$QW3cuy$m9Rmv&NTmO*4Ay@Ve|Z@3g+Ek_)q@$ z%ZhWhHc@*7n{xL>ME}3%jS;gJcNtyS0!oISu6{1-oD!M../../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(