| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "google_apis/youtube_music/youtube_music_api_requests.h" |
| |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| |
| #include "base/check.h" |
| #include "base/functional/bind.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "google_apis/common/api_error_codes.h" |
| #include "google_apis/common/request_sender.h" |
| #include "google_apis/youtube_music/youtube_music_api_response_types.h" |
| #include "net/base/url_util.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| constexpr char kContentTypeJson[] = "application/json; charset=utf-8"; |
| constexpr char kUpdateRequiredMessage[] = "UPDATE_REQUIRED"; |
| |
| // Returns true if a localized error message is expected in the response body |
| // for a response with error code `error`. |
| bool ErrorMessageExpected(google_apis::ApiErrorCode error) { |
| switch (error) { |
| case google_apis::HTTP_BAD_REQUEST: |
| case google_apis::HTTP_UNAUTHORIZED: |
| case google_apis::HTTP_FORBIDDEN: |
| case google_apis::HTTP_NOT_FOUND: |
| case google_apis::HTTP_CONFLICT: |
| case google_apis::HTTP_GONE: |
| case google_apis::HTTP_LENGTH_REQUIRED: |
| case google_apis::HTTP_PRECONDITION: |
| case google_apis::HTTP_INTERNAL_SERVER_ERROR: |
| case google_apis::HTTP_NOT_IMPLEMENTED: |
| case google_apis::HTTP_BAD_GATEWAY: |
| case google_apis::HTTP_SERVICE_UNAVAILABLE: |
| case google_apis::YOUTUBE_MUSIC_UPDATE_REQUIRED: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| template <class T> |
| void RunCallbackWithError(T callback, google_apis::ApiErrorCode error) { |
| std::move(callback).Run(base::unexpected(google_apis::youtube_music::ApiError{ |
| .error_code = error, .error_message = std::string()})); |
| } |
| |
| template <class T> |
| void RunErrorCallback( |
| google_apis::ApiErrorCode error_code, |
| base::OnceCallback< |
| void(base::expected<T, google_apis::youtube_music::ApiError>)> callback, |
| base::OnceClosure on_done, |
| std::string error_message) { |
| google_apis::youtube_music::ApiError error{ |
| .error_code = error_code, .error_message = std::move(error_message)}; |
| std::move(callback).Run(base::unexpected(std::move(error))); |
| std::move(on_done).Run(); |
| } |
| |
| // Attempts to parse `response_body` for the localized error message and uses it |
| // to populate the ApiError in `callback`. When complete, runs `finish_request`. |
| // Parses the `response_body` on `task_runner`. |
| template <class T> |
| void ParseErrorAsync( |
| base::SequencedTaskRunner* task_runner, |
| google_apis::ApiErrorCode error, |
| std::string response_body, |
| base::OnceCallback< |
| void(base::expected<T, google_apis::youtube_music::ApiError>)> callback, |
| base::OnceClosure finish_request) { |
| task_runner->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(google_apis::youtube_music::ParseErrorJson, |
| std::move(response_body)), |
| base::BindOnce(RunErrorCallback<T>, error, std::move(callback), |
| std::move(finish_request))); |
| } |
| |
| template <class T> |
| void HandleError( |
| base::SequencedTaskRunner* task_runner, |
| google_apis::ApiErrorCode error, |
| std::string&& response_body, |
| base::OnceCallback< |
| void(base::expected<T, google_apis::youtube_music::ApiError>)> callback, |
| base::OnceClosure finish_request) { |
| if (ErrorMessageExpected(error)) { |
| ParseErrorAsync(task_runner, error, std::move(response_body), |
| std::move(callback), std::move(finish_request)); |
| return; |
| } |
| |
| RunCallbackWithError(std::move(callback), error); |
| std::move(finish_request).Run(); |
| } |
| |
| // For expected `code` and `reason` combinations, re-maps the error to |
| // the service specific value. Otherwise, returns `code` unchanged. |
| google_apis::ApiErrorCode RemapError(google_apis::ApiErrorCode code, |
| std::string_view reason) { |
| if (code != google_apis::HTTP_BAD_REQUEST) { |
| return code; |
| } |
| |
| if (reason == kUpdateRequiredMessage) { |
| return google_apis::YOUTUBE_MUSIC_UPDATE_REQUIRED; |
| } |
| |
| return code; |
| } |
| |
| } // namespace |
| |
| namespace google_apis::youtube_music { |
| |
| GetMusicSectionRequest::GetMusicSectionRequest(RequestSender* sender, |
| const std::string& device_info, |
| Callback callback) |
| : UrlFetchRequestBase(sender, ProgressCallback(), ProgressCallback()), |
| callback_(std::move(callback)), |
| device_info_(device_info) { |
| CHECK(!callback_.is_null()); |
| } |
| |
| GetMusicSectionRequest::~GetMusicSectionRequest() = default; |
| |
| GURL GetMusicSectionRequest::GetURL() const { |
| // TODO(b/341324009): Move to an util file or class. |
| return GURL( |
| "https://youtubemediaconnect.googleapis.com/v1/musicSections/" |
| "root:load?intent=focus&category=music§ionRecommendationLimit=10"); |
| } |
| |
| ApiErrorCode GetMusicSectionRequest::MapReasonToError( |
| ApiErrorCode code, |
| const std::string& reason) { |
| return RemapError(code, reason); |
| } |
| |
| bool GetMusicSectionRequest::IsSuccessfulErrorCode(ApiErrorCode error) { |
| return error == HTTP_SUCCESS; |
| } |
| |
| std::vector<std::string> GetMusicSectionRequest::GetExtraRequestHeaders() |
| const { |
| return {device_info_}; |
| } |
| |
| void GetMusicSectionRequest::ProcessURLFetchResults( |
| const network::mojom::URLResponseHead* response_head, |
| base::FilePath response_file, |
| std::string response_body) { |
| ApiErrorCode error = GetErrorCode(); |
| if (error == HTTP_SUCCESS) { |
| blocking_task_runner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&GetMusicSectionRequest::Parse, |
| std::move(response_body)), |
| base::BindOnce(&GetMusicSectionRequest::OnDataParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| base::OnceClosure finish_request = |
| base::BindOnce(&GetMusicSectionRequest::OnProcessURLFetchResultsComplete, |
| weak_ptr_factory_.GetWeakPtr()); |
| HandleError(blocking_task_runner(), error, std::move(response_body), |
| std::move(callback_), std::move(finish_request)); |
| } |
| |
| void GetMusicSectionRequest::RunCallbackOnPrematureFailure(ApiErrorCode error) { |
| RunCallbackWithError(std::move(callback_), error); |
| } |
| |
| std::unique_ptr<TopLevelMusicRecommendations> GetMusicSectionRequest::Parse( |
| const std::string& json) { |
| std::unique_ptr<base::Value> value = ParseJson(json); |
| return value ? TopLevelMusicRecommendations::CreateFrom(*value) : nullptr; |
| } |
| |
| void GetMusicSectionRequest::OnDataParsed( |
| std::unique_ptr<TopLevelMusicRecommendations> recommendations) { |
| if (!recommendations) { |
| RunCallbackWithError(std::move(callback_), PARSE_ERROR); |
| } else { |
| std::move(callback_).Run(std::move(recommendations)); |
| } |
| OnProcessURLFetchResultsComplete(); |
| } |
| |
| GetPlaylistRequest::GetPlaylistRequest(RequestSender* sender, |
| const std::string& device_info, |
| const std::string& playlist_name, |
| Callback callback) |
| : UrlFetchRequestBase(sender, ProgressCallback(), ProgressCallback()), |
| device_info_(device_info), |
| playlist_name_(playlist_name), |
| callback_(std::move(callback)) { |
| CHECK(!callback_.is_null()); |
| } |
| |
| GetPlaylistRequest::~GetPlaylistRequest() = default; |
| |
| GURL GetPlaylistRequest::GetURL() const { |
| // TODO(b/341324009): Move to an util file or class. |
| return GURL( |
| base::StringPrintf("https://youtubemediaconnect.googleapis.com/v1/%s", |
| playlist_name_.c_str())); |
| } |
| |
| ApiErrorCode GetPlaylistRequest::MapReasonToError(ApiErrorCode code, |
| const std::string& reason) { |
| return RemapError(code, reason); |
| } |
| |
| bool GetPlaylistRequest::IsSuccessfulErrorCode(ApiErrorCode error) { |
| return error == HTTP_SUCCESS; |
| } |
| |
| std::vector<std::string> GetPlaylistRequest::GetExtraRequestHeaders() const { |
| return {device_info_}; |
| } |
| |
| void GetPlaylistRequest::ProcessURLFetchResults( |
| const network::mojom::URLResponseHead* response_head, |
| base::FilePath response_file, |
| std::string response_body) { |
| ApiErrorCode error = GetErrorCode(); |
| if (error == HTTP_SUCCESS) { |
| blocking_task_runner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&GetPlaylistRequest::Parse, std::move(response_body)), |
| base::BindOnce(&GetPlaylistRequest::OnDataParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| base::OnceClosure finish_request = |
| base::BindOnce(&GetPlaylistRequest::OnProcessURLFetchResultsComplete, |
| weak_ptr_factory_.GetWeakPtr()); |
| HandleError(blocking_task_runner(), error, std::move(response_body), |
| std::move(callback_), std::move(finish_request)); |
| } |
| |
| void GetPlaylistRequest::RunCallbackOnPrematureFailure(ApiErrorCode error) { |
| RunCallbackWithError(std::move(callback_), error); |
| } |
| |
| std::unique_ptr<Playlist> GetPlaylistRequest::Parse(const std::string& json) { |
| std::unique_ptr<base::Value> value = ParseJson(json); |
| return value ? Playlist::CreateFrom(*value) : nullptr; |
| } |
| |
| void GetPlaylistRequest::OnDataParsed(std::unique_ptr<Playlist> playlist) { |
| if (!playlist) { |
| RunCallbackWithError(std::move(callback_), PARSE_ERROR); |
| } else { |
| std::move(callback_).Run(std::move(playlist)); |
| } |
| OnProcessURLFetchResultsComplete(); |
| } |
| |
| PlaybackQueuePrepareRequest::PlaybackQueuePrepareRequest( |
| RequestSender* sender, |
| const PlaybackQueuePrepareRequestPayload& payload, |
| Callback callback) |
| : SignedRequest(sender), payload_(payload), callback_(std::move(callback)) { |
| CHECK(!callback_.is_null()); |
| } |
| |
| PlaybackQueuePrepareRequest::~PlaybackQueuePrepareRequest() = default; |
| |
| GURL PlaybackQueuePrepareRequest::GetURL() const { |
| // TODO(b/341324009): Move to an util file or class. |
| GURL url( |
| "https://youtubemediaconnect.googleapis.com/v1/queues/" |
| "default:preparePlayback"); |
| return url; |
| } |
| |
| ApiErrorCode PlaybackQueuePrepareRequest::MapReasonToError( |
| ApiErrorCode code, |
| const std::string& reason) { |
| return RemapError(code, reason); |
| } |
| |
| bool PlaybackQueuePrepareRequest::IsSuccessfulErrorCode(ApiErrorCode error) { |
| return error == HTTP_SUCCESS; |
| } |
| |
| bool PlaybackQueuePrepareRequest::GetContentData( |
| std::string* upload_content_type, |
| std::string* upload_content) { |
| *upload_content_type = kContentTypeJson; |
| *upload_content = payload_.ToJson(); |
| return true; |
| } |
| |
| void PlaybackQueuePrepareRequest::ProcessURLFetchResults( |
| const network::mojom::URLResponseHead* response_head, |
| base::FilePath response_file, |
| std::string response_body) { |
| ApiErrorCode error = GetErrorCode(); |
| if (error == HTTP_SUCCESS) { |
| blocking_task_runner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&PlaybackQueuePrepareRequest::Parse, |
| std::move(response_body)), |
| base::BindOnce(&PlaybackQueuePrepareRequest::OnDataParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| base::OnceClosure finish_request = base::BindOnce( |
| &PlaybackQueuePrepareRequest::OnProcessURLFetchResultsComplete, |
| weak_ptr_factory_.GetWeakPtr()); |
| HandleError(blocking_task_runner(), error, std::move(response_body), |
| std::move(callback_), std::move(finish_request)); |
| } |
| |
| void PlaybackQueuePrepareRequest::RunCallbackOnPrematureFailure( |
| ApiErrorCode error) { |
| RunCallbackWithError(std::move(callback_), error); |
| } |
| |
| std::unique_ptr<Queue> PlaybackQueuePrepareRequest::Parse( |
| const std::string& json) { |
| std::unique_ptr<base::Value> value = ParseJson(json); |
| return value ? Queue::CreateFrom(*value) : nullptr; |
| } |
| |
| void PlaybackQueuePrepareRequest::OnDataParsed(std::unique_ptr<Queue> queue) { |
| if (!queue) { |
| RunCallbackWithError(std::move(callback_), PARSE_ERROR); |
| } else { |
| std::move(callback_).Run(std::move(queue)); |
| } |
| OnProcessURLFetchResultsComplete(); |
| } |
| |
| PlaybackQueueNextRequest::PlaybackQueueNextRequest( |
| RequestSender* sender, |
| const PlaybackQueueNextRequestPayload& payload, |
| Callback callback, |
| const std::string& playback_queue_name) |
| : SignedRequest(sender), payload_(payload), callback_(std::move(callback)) { |
| CHECK(!callback_.is_null()); |
| playback_queue_name_ = playback_queue_name; |
| } |
| |
| PlaybackQueueNextRequest::~PlaybackQueueNextRequest() = default; |
| |
| GURL PlaybackQueueNextRequest::GetURL() const { |
| // TODO(b/341324009): Move to an util file or class. |
| return GURL(base::StringPrintf( |
| "https://youtubemediaconnect.googleapis.com/v1/%s:next", |
| playback_queue_name_.c_str())); |
| } |
| |
| ApiErrorCode PlaybackQueueNextRequest::MapReasonToError( |
| ApiErrorCode code, |
| const std::string& reason) { |
| return RemapError(code, reason); |
| } |
| |
| bool PlaybackQueueNextRequest::IsSuccessfulErrorCode(ApiErrorCode error) { |
| return error == HTTP_SUCCESS; |
| } |
| |
| bool PlaybackQueueNextRequest::GetContentData(std::string* upload_content_type, |
| std::string* upload_content) { |
| *upload_content_type = kContentTypeJson; |
| *upload_content = payload_.ToJson(); |
| return true; |
| } |
| |
| void PlaybackQueueNextRequest::ProcessURLFetchResults( |
| const network::mojom::URLResponseHead* response_head, |
| base::FilePath response_file, |
| std::string response_body) { |
| ApiErrorCode error = GetErrorCode(); |
| if (error == HTTP_SUCCESS) { |
| blocking_task_runner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&PlaybackQueueNextRequest::Parse, |
| std::move(response_body)), |
| base::BindOnce(&PlaybackQueueNextRequest::OnDataParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| base::OnceClosure finish_request = base::BindOnce( |
| &PlaybackQueueNextRequest::OnProcessURLFetchResultsComplete, |
| weak_ptr_factory_.GetWeakPtr()); |
| HandleError(blocking_task_runner(), error, std::move(response_body), |
| std::move(callback_), std::move(finish_request)); |
| } |
| |
| void PlaybackQueueNextRequest::RunCallbackOnPrematureFailure( |
| ApiErrorCode error) { |
| RunCallbackWithError(std::move(callback_), error); |
| } |
| |
| std::unique_ptr<QueueContainer> PlaybackQueueNextRequest::Parse( |
| const std::string& json) { |
| std::unique_ptr<base::Value> value = ParseJson(json); |
| return value ? QueueContainer::CreateFrom(*value) : nullptr; |
| } |
| |
| void PlaybackQueueNextRequest::OnDataParsed( |
| std::unique_ptr<QueueContainer> queue_container) { |
| if (!queue_container) { |
| RunCallbackWithError(std::move(callback_), PARSE_ERROR); |
| } else { |
| std::move(callback_).Run(std::move(queue_container)); |
| } |
| OnProcessURLFetchResultsComplete(); |
| } |
| |
| ReportPlaybackRequest::ReportPlaybackRequest( |
| RequestSender* sender, |
| std::unique_ptr<ReportPlaybackRequestPayload> payload, |
| Callback callback) |
| : SignedRequest(sender), |
| payload_(std::move(payload)), |
| base_url_("https://youtubemediaconnect.googleapis.com"), |
| callback_(std::move(callback)) { |
| CHECK(payload_); |
| CHECK(!callback_.is_null()); |
| } |
| |
| ReportPlaybackRequest::~ReportPlaybackRequest() = default; |
| |
| void ReportPlaybackRequest::SetBaseUrlForTesting(const GURL& base_url) { |
| base_url_ = base_url; |
| } |
| |
| GURL ReportPlaybackRequest::GetURL() const { |
| // TODO(b/341324009): Move to an util file or class. |
| return base_url_.Resolve("/v1/reports/playback"); |
| } |
| |
| ApiErrorCode ReportPlaybackRequest::MapReasonToError( |
| ApiErrorCode code, |
| const std::string& reason) { |
| return RemapError(code, reason); |
| } |
| |
| bool ReportPlaybackRequest::IsSuccessfulErrorCode(ApiErrorCode error) { |
| return error == HTTP_SUCCESS; |
| } |
| |
| bool ReportPlaybackRequest::GetContentData(std::string* upload_content_type, |
| std::string* upload_content) { |
| *upload_content_type = kContentTypeJson; |
| *upload_content = payload_->ToJson(); |
| return true; |
| } |
| |
| void ReportPlaybackRequest::ProcessURLFetchResults( |
| const network::mojom::URLResponseHead* response_head, |
| base::FilePath response_file, |
| std::string response_body) { |
| ApiErrorCode error = GetErrorCode(); |
| if (error == HTTP_SUCCESS) { |
| blocking_task_runner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&ReportPlaybackRequest::Parse, std::move(response_body)), |
| base::BindOnce(&ReportPlaybackRequest::OnDataParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| base::OnceClosure finish_request = |
| base::BindOnce(&ReportPlaybackRequest::OnProcessURLFetchResultsComplete, |
| weak_ptr_factory_.GetWeakPtr()); |
| HandleError(blocking_task_runner(), error, std::move(response_body), |
| std::move(callback_), std::move(finish_request)); |
| } |
| |
| void ReportPlaybackRequest::RunCallbackOnPrematureFailure(ApiErrorCode error) { |
| RunCallbackWithError(std::move(callback_), error); |
| } |
| |
| std::unique_ptr<ReportPlaybackResult> ReportPlaybackRequest::Parse( |
| const std::string& json) { |
| std::unique_ptr<base::Value> value = ParseJson(json); |
| return value ? ReportPlaybackResult::CreateFrom(*value) : nullptr; |
| } |
| |
| void ReportPlaybackRequest::OnDataParsed( |
| std::unique_ptr<ReportPlaybackResult> report_playback_result) { |
| if (!report_playback_result) { |
| RunCallbackWithError(std::move(callback_), PARSE_ERROR); |
| } else { |
| std::move(callback_).Run(std::move(report_playback_result)); |
| } |
| OnProcessURLFetchResultsComplete(); |
| } |
| |
| } // namespace google_apis::youtube_music |