blob: 2e30b2d4a671d4afa11deb8a00efba7318d6a10e [file] [log] [blame]
// 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&sectionRecommendationLimit=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